Subro.RecordForge 1.0.0

Prefix Reserved
dotnet add package Subro.RecordForge --version 1.0.0
                    
NuGet\Install-Package Subro.RecordForge -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Subro.RecordForge" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Subro.RecordForge" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Subro.RecordForge" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Subro.RecordForge --version 1.0.0
                    
#r "nuget: Subro.RecordForge, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Subro.RecordForge@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Subro.RecordForge&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Subro.RecordForge&version=1.0.0
                    
Install as a Cake Tool

RecordForge

NuGet

RecordForge is a Roslyn incremental source generator that forges a concrete record, record struct, struct or class (or their readonly versions) implementation from a plain interface. Define the contract once – let the generator produce the boilerplate.

using Subro.RecordForge;

[GenerateRecord]
public interface IPerson
{
    string Name { get; }
    DateOnly DateOfBirth { get; }
}

// --- generated (roughly) ---
// public partial record Person(string Name, System.DateOnly DateOfBirth):IPerson;

That is all if you want the defaults. No partial classes to babysit, no T4 templates, no reflection at runtime. So AOT safe.


Contents


Why

Interface-first design is a natural fit for many .NET projects — it keeps your domain contracts clean, supports dependency injection, enables mocking in tests, and gives you a stable serialization surface. The friction starts when you need a concrete implementation: even with record types and IntelliSense, it is still a another thing to write and a another thing to keep in sync.

RecordForge removes that overhead. You own the interface — and with it the full freedom to use it for DI, mocking, multiple implementations, or serialization contracts. The generator simply gives you one default implementation for free, so you can stop writing boilerplate and focus on the parts that actually differ.

By default the types are made partial so they can be easily extended to in case it is needed.


Extensibility & Roadmap

Because generated types are partial by default, you can extend them in your own files without touching the generated output. A practical example is JSON serialization: you can designate the generated record as the default (de)serializer for its interface today simply by adding a [JsonConverter] attribute or a custom converter in a separate partial file.

This is also the direction RecordForge is heading. A planned companion library will combine record generation with JSON type coupling, so the serialization wiring is handled automatically alongside the implementation. Roslyn does not allow source generators to build on each other's output directly, which is why this will ship as an integrated library rather than a stacked generator — but the partial design means you are not blocked in the meantime.


Installation

dotnet add package Subro.RecordForge

The package contains both the runtime attributes and the source generator. There is nothing else to wire up.


Requirements

  • Roslyn 4.x / Visual Studio 2022 or .NET SDK 6+ on the compiler side (required by any incremental source generator).
  • Language version: C# 9 or newer in the consuming project so that record types are available. If you only target class / struct you can use older C# language versions, but record and record struct require C# 9 and 10 respectively. (language version, not .net version)

The generator itself targets netstandard2.0, so it runs in every modern build environment.


Quick start

Annotate an interface with [GenerateRecord]:

using Subro.RecordForge;

namespace MyApp.Domain;

[GenerateRecord]
public interface IPerson
{
    string Name { get; }
    DateOnly DateOfBirth { get; }
    int LoginCount { get; set; } 
}

The generator produces a Person record in the same namespace:

// auto-generated
namespace MyApp.Domain
{
    public partial record Person(string Name, System.DateOnly DateOfBirth):IPerson
    {
		public int LoginCount{get;set;}
    }
}

Note that the default implementation creates a property for interface properties that are not readonly, but does not add it in the constructor. That behaviour can be overridden though (see constructors ))

Consume the generated type like any other record:

IPerson p = new Person("Ada", 36);

By default:

  • The generated type is a record.
  • The generated name is the interface name without a leading I (so IPerson becomes Person).
  • The generated type lives in the interface's namespace.
  • The generated type is partial, so you can add your own members in another file.
  • A property is only explicitly created when needed.

Choosing the kind of type

Pass a RecordKind to pick what should be generated:

[GenerateRecord(RecordKind.RecordStruct)]
public interface IPoint { double X { get; } double Y { get; } }

[GenerateRecord(RecordKind.ReadOnlyRecordStruct)]
public interface IVector { double X { get; } double Y { get; } }

[GenerateRecord(RecordKind.Struct)]
public interface ISize { double Width { get; } double Height { get; } }

[GenerateRecord(RecordKind.ReadonlyStruct)]
public interface IRange { int Min { get; } int Max { get; } }

[GenerateRecord(RecordKind.Class)]
public interface IAccount { string Id { get; } decimal Balance { get; set; } }

The available kinds are:

  • RecordKind.Record (default)public record.
  • RecordKind.RecordStructpublic record struct.
  • RecordKind.ReadOnlyRecordStructpublic readonly record struct.
  • RecordKind.Struct — plain public struct.
  • RecordKind.ReadonlyStructpublic readonly struct.
  • RecordKind.Class — plain public class.

For readonly kinds, every property becomes init (or throws NotSupportedException through an explicit interface implementation if the interface insists on a setter).


Naming and namespace overrides

By default the generated name is the interface name without its leading I, in the same namespace. Both can be overridden:

[GenerateRecord(RecordName = "PersonDto", NameSpace = "MyApp.Contracts")]
public interface IPerson
{
    string Name { get; }
    DateOnly DateOfBirth { get; }
}

This generates MyApp.Contracts.PersonDto, still implementing IPerson.


Controlling constructors

Use ConstructorUsage to decide which constructors are generated. For flags with multiple bits, combine them with |.

[GenerateRecord(
    RecordKind.Class,
    ConstructorUsage = ConstructorUsage.Empty | ConstructorUsage.AllProperties)]
public interface IOrder
{
    Guid    Id       { get; }
    string  Customer { get; }
    decimal Total    { get; set; }
}

Generates:

    public partial class Order:IOrder
    {
        public Order()
        {
        }

        public Order(System.Guid Id, string Customer, decimal Total)
        {
            this.Id = Id;
            this.Customer = Customer;
            this.Total = Total;
        }

		public System.Guid Id{get;}
		public string Customer{get;}
		public decimal Total{get;set;}
    }

Values:

  • ConstructorUsage.Automatic (default) — pick a sensible default based on the RecordKind:
    • Readonly kinds: all properties go through the constructor.
    • Records / record structs: only readonly properties.
    • Plain class / struct: parameterless constructor.
  • ConstructorUsage.Empty — parameterless constructor.
  • ConstructorUsage.ReadonlyProperties — constructor for { get; } properties only.
  • ConstructorUsage.ReadonlyAndInitProperties — constructor for { get; } and { get; init; } properties (setters are property-only).
  • ConstructorUsage.AllProperties — constructor for every property on the interface.

When more than one value is requested, RecordForge emits a primary constructor (for record kinds) plus the additional constructors, all chaining to the smallest one. For non record type, normal (not a primary) constructors are used, so that the option exist to use those in older frameworks.


Setters, abstract and partial

The GenerateRecord attribute exposes a few extra knobs:

[GenerateRecord(
    RecordKind.Record,
    AsPartial            = true,   // default: true – allows you to extend the generated type
    AsAbstract           = false,  // default: false
    AlwaysCreateSetters  = true)]  // turn every interface `get` into `get; set;` / `get; init;`
public interface IUser
{
    string Id       { get; }
    string Username { get; }
}
  • AsPartial — emits the type as partial so you can add members in another file.
  • AsAbstract — emits the type as abstract. Useful for shared bases.
  • AlwaysCreateSetters — promote every get-only property to a settable one. On readonly kinds the setter becomes init to stay valid.

Assembly level generation

Sometimes you cannot (or do not want to) put the attribute on the interface itself, for instance because the interface lives in a library you do not own, or because you want to keep the implementation in a different project. Use [assembly: GenerateRecordFromInterface(...)] instead:

using Subro.RecordForge;

// One interface
[assembly: GenerateRecordFromInterface(typeof(MyLib.IPerson))]

// Same interface but a different generated name
[assembly: GenerateRecordFromInterface(typeof(MyLib.IPerson), RecordName = "PersonDto")]

// Several interfaces at once
[assembly: GenerateRecordFromInterface(new[] { typeof(MyLib.IPerson), typeof(MyLib.IAddress) })]

// Pick a specific kind
[assembly: GenerateRecordFromInterface(typeof(MyLib.IPoint), RecordKind.ReadOnlyRecordStruct)]

All the same options (RecordName, NameSpace, AsPartial, AsAbstract, AlwaysCreateSetters, ConstructorUsage) are available.


Cross-assembly generation

Because GenerateRecordFromInterface can be placed on the assembly, the interface and the generated implementation can live in different assemblies:

// In Contracts.dll – just the interface, no generator dependency needed here.
namespace Contracts;
public interface IPerson { string Name { get; } int Age { get; } }
// In MyApp.dll – references Contracts.dll and the Subro.RecordForge NuGet.
using Subro.RecordForge;

[assembly: GenerateRecordFromInterface(typeof(Contracts.IPerson))]

The generator emits Contracts.Person (or wherever you point NameSpace) inside MyApp.dll.


FAQ

What happens if my interface has methods, not just properties?

Methods (and properties without a getter) are ignored by the generator. Because the generated type is partial by default, you are expected to provide the method implementation yourself in a separate file.

Does it support nullable annotations, custom types, collections, …?

Yes. The generator copies the property type verbatim from the interface, including nullable annotations, generics and ref-like types where they are valid on the chosen RecordKind.

Does the NuGet package add a runtime dependency?

No. Subro.RecordForge only adds compile-time metadata (the attributes) and the generator DLL loaded by Roslyn. Your published binaries do not carry any extra runtime dependency.

Can I use this from a library that targets netstandard2.0?

Yes for class / struct output. For record / record struct output your project needs a C# language version that supports those constructs (C# 9 / 10). A common pattern on netstandard2.0 is to add an IsExternalInit polyfill, which is exactly what this package does for its own attributes.


License

MIT — see LICENSE in the repository.

External reference

Built on top of Subro.Generators.TransformResult for clean transform pipelines and diagnostics.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  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 was computed.  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 was computed.  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 was computed.  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 was computed.  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. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
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
1.0.0 96 4/21/2026
0.1.1 123 4/18/2026