DataNormalizer 0.0.1
See the version list below for details.
dotnet add package DataNormalizer --version 0.0.1
NuGet\Install-Package DataNormalizer -Version 0.0.1
<PackageReference Include="DataNormalizer" Version="0.0.1" />
<PackageVersion Include="DataNormalizer" Version="0.0.1" />
<PackageReference Include="DataNormalizer" />
paket add DataNormalizer --version 0.0.1
#r "nuget: DataNormalizer, 0.0.1"
#:package DataNormalizer@0.0.1
#addin nuget:?package=DataNormalizer&version=0.0.1
#tool nuget:?package=DataNormalizer&version=0.0.1
DataNormalizer
A .NET source generator that normalizes nested object graphs into flat, deduplicated, JSON-serializable containers.
What It Does
Given an object graph with shared references:
var sharedAddress = new Address { City = "Seattle", Zip = "98101" };
var team = new Team
{
Name = "Engineering",
Members = new[]
{
new Person { Name = "Alice", Home = sharedAddress },
new Person { Name = "Bob", Home = sharedAddress },
},
};
One call normalizes the entire graph into a flat, deduplicated container:
var result = AppNormalization.Normalize(team);
var json = JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true });
{
"TeamList": [
{ "Name": "Engineering", "MembersIndices": [0, 1] }
],
"PersonList": [
{ "Name": "Alice", "HomeIndex": 0 },
{ "Name": "Bob", "HomeIndex": 0 }
],
"AddressList": [
{ "City": "Seattle", "Zip": "98101" }
]
}
The shared Address is stored once. Nested objects become integer indices into typed arrays. The whole container serializes directly with System.Text.Json and is straightforward to reverse on any frontend.
Installation
dotnet add package DataNormalizer
Supports .NET 6, .NET 7, .NET 8, .NET 9, and .NET 10.
Quick Start
1. Define your domain types
public class Team
{
public string Name { get; set; }
public Person[] Members { get; set; }
}
public class Person
{
public string Name { get; set; }
public Address Home { get; set; }
}
public class Address
{
public string City { get; set; }
public string Zip { get; set; }
}
2. Create a configuration class
using DataNormalizer.Attributes;
using DataNormalizer.Configuration;
[NormalizeConfiguration]
public partial class AppNormalization : NormalizationConfig
{
protected override void Configure(NormalizeBuilder builder)
{
builder.NormalizeGraph<Team>(); // discovers Person, Address
}
}
3. Normalize, use, and denormalize
// Normalize
var result = AppNormalization.Normalize(team);
// Access the root entity (always at index 0)
var root = result.TeamList[0];
Console.WriteLine(root.Name); // "Engineering"
Console.WriteLine(root.MembersIndices); // [0, 1]
// Access entity lists directly
Console.WriteLine(result.PersonList.Length); // 2
Console.WriteLine(result.AddressList.Length); // 1 (deduplicated)
// Serialize the entire container to JSON
var json = JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true });
// Denormalize back to the original object graph
var restored = AppNormalization.Denormalize(result);
The source generator produces Normalize and Denormalize static methods, per-type DTOs, and the container result class at compile time.
How It Works
For each NormalizeGraph<T>() call, the generator produces:
Per-type DTOs (
Normalized{TypeName}) — partial classes implementingIEquatable<T>for value-based deduplication. Nested object references becomeintindices ({Name}Index), collections becomeint[]({Name}Indices).A container result (
Normalized{TypeName}Result) — holds a{TypeName}Listarray for every entity type in the graph. The root entity is always at index 0 in the root type's list. This is the primary output ofNormalize()and the input toDenormalize().Normalize(T)/Denormalize(Normalized{T}Result)— static methods on the configuration class.
All generated types are partial, so you can extend them with additional members.
Target Frameworks
| Component | Targets |
|---|---|
| Runtime library | net6.0, net7.0, net8.0, net9.0, net10.0 |
| Source generator | netstandard2.0 (Roslyn requirement, bundled in NuGet package) |
The generator runs at compile time regardless of your target framework. The runtime library provides the NormalizationContext used internally by generated code.
Configuration Options
Auto-discovery
NormalizeGraph<T>() walks the type graph starting from T and discovers all referenced complex types automatically.
builder.NormalizeGraph<Team>(); // discovers Person, Address, etc.
Opt-out (Inline)
Keep a type inline instead of normalizing it into a separate collection:
builder.NormalizeGraph<Person>(graph =>
{
graph.Inline<Metadata>(); // Metadata stays nested, not extracted
});
Ignore a property
Exclude a property from the generated DTO:
builder.ForType<Person>(p => p.IgnoreProperty(x => x.Secret));
Or use the attribute:
public class Person
{
public string Name { get; set; }
[NormalizeIgnore]
public string Secret { get; set; }
}
ExplicitOnly mode
Only include properties that are explicitly opted-in:
builder.ForType<Person>(p =>
{
p.UsePropertyMode(PropertyMode.ExplicitOnly);
p.IncludeProperty(x => x.Name);
});
Or use attributes:
public class Person
{
[NormalizeInclude]
public string Name { get; set; }
public string NotIncluded { get; set; }
}
Multiple root types
Register multiple roots to generate separate container types and Normalize()/Denormalize() overloads:
protected override void Configure(NormalizeBuilder builder)
{
builder.NormalizeGraph<Team>(); // → NormalizedTeamResult
builder.NormalizeGraph<Order>(); // → NormalizedOrderResult
}
Each container includes only the entity lists reachable from its root type.
Reversing Normalized Data
Any consumer (frontend, API client, other language) can reconstruct the original object graph from the serialized container:
- Parse JSON into the container shape
- Reconstruct leaf entities from their lists
- Reconstruct composite entities by resolving index references into entity lists
- The root entity is always at index 0 in the root type's list
Shared references are preserved: multiple indices pointing to the same list entry reconstruct as the same object reference.
Circular References
- The generator detects cycles at compile time and emits a DN0001 warning.
- Suppress with
<NoWarn>$(NoWarn);DN0001</NoWarn>in your.csprojif the cycle is intentional. - Normalization handles cycles correctly via value-equality-based deduplication.
- Denormalization uses a two-pass approach: create all objects first, then resolve references.
Diagnostics
| ID | Severity | Description | Resolution |
|---|---|---|---|
| DN0001 | Warning | Circular reference detected | Add <NoWarn>DN0001</NoWarn> if intentional |
| DN0002 | Error | Configuration class must be partial |
Add the partial keyword to the class declaration |
| DN0003 | Error | Type has no public properties | Add public properties or exclude the type |
| DN0004 | Info | Unmapped complex type will be inlined | Use graph.Inline<T>() explicitly, or add to the graph |
Known Constraints
- For circular types, back-edge properties (those creating the cycle) use shape-based comparison (null/non-null, collection count) rather than full structural comparison. All non-circular properties — including nested complex subtrees — are fully compared. False dedup only occurs if two objects in a cycle have identical simple properties, identical non-circular subtree structure, AND identical circular reference shapes.
License
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- No dependencies.
-
net6.0
- No dependencies.
-
net7.0
- No dependencies.
-
net8.0
- No dependencies.
-
net9.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.