H073.SurfaceKit.Generators
2.0.0
Prefix Reserved
dotnet add package H073.SurfaceKit.Generators --version 2.0.0
NuGet\Install-Package H073.SurfaceKit.Generators -Version 2.0.0
<PackageReference Include="H073.SurfaceKit.Generators" Version="2.0.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
<PackageVersion Include="H073.SurfaceKit.Generators" Version="2.0.0" />
<PackageReference Include="H073.SurfaceKit.Generators"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> </PackageReference>
paket add H073.SurfaceKit.Generators --version 2.0.0
#r "nuget: H073.SurfaceKit.Generators, 2.0.0"
#:package H073.SurfaceKit.Generators@2.0.0
#addin nuget:?package=H073.SurfaceKit.Generators&version=2.0.0
#tool nuget:?package=H073.SurfaceKit.Generators&version=2.0.0
H073.SurfaceKit.Generators
This README was generated by a locally running AI model from a direct reading of the source.
An optional Roslyn source generator for SurfaceKit. It removes the boilerplate around component state by turning [State]/[Prop] fields into the wiring you would otherwise write by hand.
You do not need this package to use SurfaceKit — state works with State<T> on its own. Add this package when you want the [State] sugar.
Install
dotnet add package H073.SurfaceKit.Generators
It installs as a build-time analyzer: no runtime dependency, no DLL in your output. It targets netstandard2.0 (Roslyn's requirement); your own project can target anything.
What you write vs. what SurfaceKit does without it
Without this package — plain State<T>:
public class CounterView : View
{
private readonly State<int> _count = new(0);
public override IView Body => new Button(
new Text($"Count: {_count.Value}"),
() => _count.Value++);
}
With this package — [State] on a partial class:
public partial class CounterView : View
{
[State] private int _count = 0;
public override IView Body => new Button(
new Text($"Count: {Count}"), // use the generated property
() => Count++);
}
Both behave identically at runtime. The generator just writes the State<T> plumbing for you.
What [State] does (and what gets generated)
For a [State] int _count = 0; field on a partial class : View, the generator emits a partial containing:
private State<int>? __sk_state_count;
public int Count
{
get => (__sk_state_count ??= new State<int>(_count)).Value; // lazy-seeded from the field initializer
set => (__sk_state_count ??= new State<int>(_count)).Value = value;
}
- Lazy seeding — the backing
State<int>is created on first access, seeded from your field's initializer (_count = 0). - Auto-subscribe — reading
CountinsideBodyregisters this component as a dependent of the state. - Invalidate on change — writing
Countonly invalidates (and re-evaluates) the component when the value actually changes, compared withEqualityComparer<T>.Default.
The generator also emits __TransferProps for every partial class : View. This lets the reconciler reuse a component instance across frames and copy the non-state ([Prop]) fields from the new description onto the retained instance without allocating a new object. [State]/[Binding]/readonly/const fields are excluded from the transfer.
All of this uses only generics and EqualityComparer<T>.Default — no reflection — so it stays Native-AOT compatible.
Property naming
The generated property name is the field name with a leading _ or m_ stripped and the first letter upper-cased:
| Field | Generated property |
|---|---|
_count |
Count |
m_isOpen |
IsOpen |
value |
Value |
If stripping the prefix would collide with the original field name, no property is emitted for that field.
What to watch out for
- The class must be
partial.[State]/[Prop]live on apartial class : View; withoutpartial, the generator can't add the other half. - Use the generated property, not the field. Read/write
Count, not_count, insideBodyand handlers — only the property path subscribes and invalidates. The raw field is just the initial seed. - The field initializer is the initial value only. After first access the value lives in the backing
State<T>; later assignments to the field don't move the state. - Don't also declare the generated name. Declaring your own
Countalongside_countcollides — rename one. - Reference types compare by reference. Invalidation uses
EqualityComparer<T>.Default; mutating aList<T>in place won't be detected. Reassign (Items = newList) or use an immutable/value type to trigger a refresh. - Match versions. Keep this package's version aligned with your
H073.SurfaceKitversion.
License
MIT
Learn more about Target Frameworks and .NET Standard.
This package has 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.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.0.0 | 93 | 7/1/2026 |