ZibStack.NET.Core
Source generator for shared attributes used across ZibStack.NET packages — relationships, entity configuration, and TypeScript-style utility types. No reflection, no runtime overhead.
Note: This package replaces
ZibStack.NET.Utils. The utility type attributes moved from theZibStack.NET.Utilsnamespace toZibStack.NET.Core. Relationship and entity attributes moved fromZibStack.NET.UItoZibStack.NET.Core.
Install
Section titled “Install”dotnet add package ZibStack.NET.Core[PartialFrom] requires ZibStack.NET.Dto for PatchField<T>. The other utility type attributes ([PickFrom], [OmitFrom], [IntersectFrom]) emit plain properties and have no extra dependency.
Relationship Attributes
Section titled “Relationship Attributes”These attributes live in ZibStack.NET.Core because they are read by multiple generators — keeping them in a single dependency-free package avoids cyclic references and duplicate declarations across ZibStack.NET.Dto, ZibStack.NET.Query, and ZibStack.NET.UI.
The attributes themselves are pure markers (no EF Core or ASP.NET runtime dependency). Each consuming generator picks up only the parts it cares about.
[OneToOne]
Section titled “[OneToOne]”Declares a one-to-one navigation property. Consumed by:
ZibStack.NET.Dto/ZibStack.NET.Query— expands the navigation into the generatedQueryDtofilter allowlist sofilter=Team.Name=Lakers,sort=Team.City, andselect=Team.Nameall work. Without this marker the Dto generator skips the navigation entirely (complex types aren’t valid query parameters on their own), so relational filtering is silently unavailable.ZibStack.NET.UI— excludes the navigation from auto-generated forms/tables, registers it as a related entity for drill-down, and (when combined with[Entity]) emits the EF CoreHasOne().WithOne()configuration.
using ZibStack.NET.Core;
public class Player{ public int TeamId { get; set; }
[OneToOne] public Team? Team { get; set; }}Properties:
ForeignKey— foreign key property name on this type (auto-detected by the{NavProp}Idconvention if omitted — e.g.Team→TeamId)Label— display label used by the UI generatorSchemaUrl/FormSchemaUrl— override URLs for UI schema resolution
[OneToMany]
Section titled “[OneToMany]”Declares a one-to-many relationship on a collection navigation property. Consumed by:
ZibStack.NET.Dto/ZibStack.NET.Query— lets the query DSL reach into the collection.filter=Players.Name=*skitranslates to “any player named *ski”,filter=Players.Count>5to “teams with more than 5 players”. Without this marker the collection is invisible to the filter allowlist.ZibStack.NET.UI— emits a child table for hierarchical ERP-style drill-down on the parent’s detail view, and (when combined with[Entity]) generates the EF CoreHasMany().WithOne()configuration.
public class Team{ public int Id { get; set; } public string Name { get; set; }
[OneToMany(Label = "Players")] public ICollection<Player> Players { get; set; } = new List<Player>();}Properties:
ForeignKey— foreign key property name on the child type (auto-detected by convention if omitted)Label— display label used by the UI generator for the child table tabSchemaUrl/FormSchemaUrl— override URLs for UI schema resolution
[Entity]
Section titled “[Entity]”Opt-in marker that tells the ZibStack.NET.UI generator to emit an EF Core IEntityTypeConfiguration<T> for this class — including table mapping, key configuration, and any HasOne / HasMany calls derived from [OneToOne] / [OneToMany] navigations on the same type.
The attribute itself has no dependency on Microsoft.EntityFrameworkCore; only the generated configuration class references EF Core, so consumers who don’t use EF Core simply don’t apply [Entity] and pay nothing.
[Entity(TableName = "Players", Schema = "dbo")]public partial class Player { ... }Properties:
TableName— overrides the database table name (defaults to the class name)Schema— database schema name
Utility Type Attributes
Section titled “Utility Type Attributes”[PartialFrom(typeof(T))]
Section titled “[PartialFrom(typeof(T))]”Like TypeScript’s Partial<T> — every property of the target type becomes a PatchField<T> so you can model partial updates (the value can be unset, set to a value, or explicitly set to null). An ApplyTo(target) method writes only the fields that were actually provided. Used by Dto-style Update*Request shapes.
using ZibStack.NET.Core;
[PartialFrom(typeof(Player))]public partial record PartialPlayer;
// Generated:// public PatchField<string> Name { get; init; }// public PatchField<int> Level { get; init; }// public PatchField<string?> Email { get; init; }// ...// public void ApplyTo(Player target) { /* writes only fields with HasValue */ }Requires
ZibStack.NET.DtoforPatchField<T>. The other three utility-type attributes below do not usePatchField— they emit plain properties.
[PickFrom(typeof(T), ...)]
Section titled “[PickFrom(typeof(T), ...)]”Like TypeScript’s Pick<T, K> — generates a record with the whitelisted properties as plain fields (their original types, not PatchField), plus a static FromEntity(source) factory that copies them from the source. Use it for projections / lightweight DTOs.
[PickFrom(typeof(Player), nameof(Player.Name), nameof(Player.Level))]public partial record PlayerSummary;
// Generated:// public string Name { get; init; } = default!;// public int Level { get; init; } = default!;// public static PlayerSummary FromEntity(Player source) => new() { Name = source.Name, Level = source.Level };
// Usage:var summary = PlayerSummary.FromEntity(player);[OmitFrom(typeof(T), ...)]
Section titled “[OmitFrom(typeof(T), ...)]”Like TypeScript’s Omit<T, K> — same as PickFrom but you list the properties to exclude. Also emits plain properties + FromEntity(source).
[OmitFrom(typeof(Player), nameof(Player.Id), nameof(Player.CreatedAt))]public partial record PlayerWithoutMeta;
// Generated:// public string Name { get; init; } = default!;// public int Level { get; init; } = default!;// public string? Email { get; init; } = default!;// public static PlayerWithoutMeta FromEntity(Player source) => new() { /* all included fields */ };[IntersectFrom(typeof(T))]
Section titled “[IntersectFrom(typeof(T))]”Like TypeScript’s & operator — generates a record that merges properties from multiple sources (deduplicated by name; first source wins on conflict). Emits plain properties, plus a FromEntity(source) per source type and an ApplyTo(target) per source type.
[IntersectFrom(typeof(Player))][IntersectFrom(typeof(Address))]public partial record PlayerWithAddress;
// Generated:// public string Name { get; init; } = default!; // from Player// public int Level { get; init; } = default!; // from Player// public string City { get; init; } = default!; // from Address// public string Street { get; init; } = default!; // from Address//// public static PlayerWithAddress FromEntity(Player source) => new() { /* fills Player fields */ };// public static PlayerWithAddress FromEntity(Address source) => new() { /* fills Address fields */ };// public void ApplyTo(Player target) { /* writes Player fields back */ }// public void ApplyTo(Address target) { /* writes Address fields back */ }
// Usage — chain FromEntity + with-expression to merge:var combined = PlayerWithAddress.FromEntity(player) with{ City = address.City, Street = address.Street,};combined.ApplyTo(somePlayer); // writes Player-side fields backcombined.ApplyTo(someAddress); // writes Address-side fields backUnlike [PartialFrom], none of [PickFrom]/[OmitFrom]/[IntersectFrom] use PatchField — they model shape transformations, not partial updates. If you want a partial-update DTO, use [PartialFrom] (or the Update*Request shapes generated by [CrudApi] in ZibStack.NET.Dto).
JS-Style Destructuring
Section titled “JS-Style Destructuring”[Destructurable<TSource>]
Section titled “[Destructurable<TSource>]”Brings JS-style { picked, ...rest } destructuring to C# — both sides typed end-to-end.
You declare a partial record describing the shape you want to pick; the generator
fills it with a Split(source) factory and a nested Rest record holding the complement.
using ZibStack.NET.Core;
// Source — plain record, no attributes here.public record Person(string Name, int Id, string Email, int Age, string City);
// Shape — primary-ctor record listing the picked properties.[Destructurable<Person>]public partial record PersonNameId(string Name, int Id);The generator emits onto PersonNameId:
public partial record PersonNameId{ public sealed record Rest(string Email, int Age, string City);
public static PersonNameId FromSource(Person src) => new(src.Name, src.Id); public static Rest RestOf(Person src) => new(src.Email, src.Age, src.City); public static (PersonNameId Picked, Rest Remaining) Split(Person src) => (FromSource(src), RestOf(src));}Usage:
var person = new Person("Alice", 42, "a@b.c", 30, "Warsaw");
var (picked, rest) = PersonNameId.Split(person);
picked.Name // "Alice" — typedpicked.Id // 42 — typedrest.Email // "a@b.c" — typed, IDE-autocompletedrest.Age // 30 — typedrest.City // "Warsaw" — typedWhy a shape record (and not a lambda or method-name encoding)? Anonymous types in C# are nominal, not structural — they have no source-writable name a generator can emit code against. The C# language team has explicitly declined both anonymous-type deconstruction and spread/rest object syntax, positioning property mapping as “a job for a library method.” Library methods, in turn, need a named type to hand back as the rest container — that’s what the shape record is.
The upside: every shape is also a regular DTO. Reuse it in responses, log payloads,
mappers. No throwaway anon, no dynamic, no untyped dictionary.
Two declaration styles supported. Primary-ctor records use positional construction; body-style records fall back to object initializers:
// Primary-ctor — generator emits `new(src.Name, src.Id)`[Destructurable<Person>]public partial record PersonNameId(string Name, int Id);
// Body — generator emits `new() { Name = src.Name, Email = src.Email }`[Destructurable<Person>]public partial record PersonContact{ public required string Name { get; init; } public required string Email { get; init; }}Diagnostics. Property name and type are validated against the source at compile time:
| ID | Severity | Trigger |
|---|---|---|
ZDS0001 | Error | Shape property does not exist on the source type |
ZDS0002 | Error | Shape property’s type does not match source’s declared type |
ZDS0003 | Warning | Shape carries [Destructurable<>] but isn’t partial (nothing emitted) |
Renames are caught at the next build — no chance of silent drift between shape and source.
Pattern matching on the Split tuple
Section titled “Pattern matching on the Split tuple”Split(src) returns a regular ValueTuple<TPicked, TRest>, so it plugs into the full
C# pattern-matching grammar — positional, property, switch with when guards.
// Positional pattern — match the whole shape in one goif (PersonNameId.Split(person) is ({ Name: "Admin", Id: 0 }, _)){ // newly created admin}
// Descend into rest via a property patternif (PersonNameId.Split(person) is (var p, { Age: < 18 })){ Console.WriteLine($"underage: {p.Name}");}
// switch with when-guards — combine picked + rest constraints freelyvar label = PersonNameId.Split(person) switch{ ({ Name: var n }, _) when n.StartsWith("Guest") => $"guest: {n}", ({ Id: 0 }, _) => "unsaved", (var p, { Age: < 18 }) => $"minor: {p.Name}", (var p, { Age: >= 65 }) => $"senior: {p.Name}", ({ Name: "Admin" }, { Email: var e }) when e.EndsWith("@company.com") => $"internal admin ({e})", (var p, var rest) when rest.Age > 18 && rest.City == rest.Email.Split('@')[1] => $"{p.Name} emails from home", (var p, _) => $"regular: {p.Name}",};Why this matters if you come from TypeScript: const { name, id, ...rest } = person
in TS gives you rest as a plain object, and you reach into it with rest.age. The
shape-record approach gives you the same destructured shape (picked + typed rest)
plus a named, reusable type for both halves. Where TS bakes the shape into the
destructuring expression, C# moves it one level up into a partial record — the cost is
one declaration line, the win is that the shape can be reused outside the destructure.
Requirements
Section titled “Requirements”- .NET 6+ (or .NET Framework with SDK-style projects)
ZibStack.NET.DtoforPatchField<T>support (utility types only)
License
Section titled “License”MIT