CoreOne.Generators
1.1.0
See the version list below for details.
dotnet add package CoreOne.Generators --version 1.1.0
NuGet\Install-Package CoreOne.Generators -Version 1.1.0
<PackageReference Include="CoreOne.Generators" Version="1.1.0" />
<PackageVersion Include="CoreOne.Generators" Version="1.1.0" />
<PackageReference Include="CoreOne.Generators" />
paket add CoreOne.Generators --version 1.1.0
#r "nuget: CoreOne.Generators, 1.1.0"
#:package CoreOne.Generators@1.1.0
#addin nuget:?package=CoreOne.Generators&version=1.1.0
#tool nuget:?package=CoreOne.Generators&version=1.1.0
CoreOne.Generators
A Roslyn source generator that produces strongly-typed IDs from a single attribute. Eliminates boilerplate for value types that wrap a primitive (e.g. OrderId wrapping a Guid).
Usage
Decorate any partial type with [StronglyTypedId]:
// Generic form (C# 11 / .NET 7+)
[StronglyTypedId<Guid>]
public partial struct OrderId { }
// Non-generic form (works everywhere)
[StronglyTypedId(typeof(int))]
public partial struct UserId { }
The generator emits a .g.cs file alongside your type with all members filled in automatically.
What Gets Generated
Core Members
| Member | Notes |
|---|---|
T Value |
The underlying value |
static T Empty |
Default/empty instance (Guid.Empty, "", etc.) |
string ValueAsString |
Culture-invariant string representation |
TypeName() |
Default constructor |
static TypeName From{T}(T value) |
Factory method |
static TypeName New() |
Guid types — wraps Guid.NewGuid() |
static TypeName Create() |
Guid types — wraps ID.Create() (sequential GUID) |
ToString(), GetHashCode(), Equals() |
Value-based equality |
==, != operators |
Value-based comparison |
TryParse, Parse |
String parsing |
IParsable<T>, ISpanParsable<T> |
.NET 7+ only |
IComparable, <, <=, >, >= |
When declared on the partial type |
All members are additive — any member you define yourself is skipped by the generator.
Serialization Converters
By default, the generator also emits nested converter classes and applies the corresponding attributes to your type:
| Converter | Generated Type |
|---|---|
System.Text.Json |
{TypeName}JsonConverter |
Newtonsoft.Json |
{TypeName}NewtonsoftJsonConverter |
System.ComponentModel.TypeConverter |
{TypeName}TypeConverter |
MongoDB.Bson |
{TypeName}BsonConverter |
Each converter is only emitted when the relevant library is detected in the consuming project.
Supported Underlying Types
22 primitive types are supported:
bool · byte · sbyte · short · ushort · int · uint · long · ulong ·
Int128 · UInt128 · BigInteger · float · double · decimal · Half ·
Guid · string · DateTime · DateTimeOffset · MongoDB.Bson.ObjectId
Attribute Options
[StronglyTypedId<Guid>(
generateSystemTextJsonConverter: true, // default
generateNewtonsoftJsonConverter: true, // default
generateSystemComponentModelTypeConverter: true, // default
generateMongoDBBsonSerialization: true, // default
addCodeGeneratedAttribute: true, // default
StringComparison = StringComparison.Ordinal, // string IDs only
GenerateToStringAsRecord = false
)]
public partial struct OrderId { }
Set any converter flag to false to suppress that converter entirely.
Sequential GUIDs
For Guid-backed IDs, Create() uses ID.Create() from the CoreOne library, which generates sequential (v7-style) GUIDs. This is friendlier to database indexes than random GUIDs.
var id = OrderId.Create(); // sequential GUID — good for DB inserts
var id = OrderId.New(); // random Guid.NewGuid()
Setup
Add the generator project as an analyzer reference — no assembly reference is needed at runtime:
<ProjectReference Include="..\CoreOne.Generators\CoreOne.Generators.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
Proxy Generator (Compile-Time AOP)
The Proxy Generator enables compile-time Aspect-Oriented Programming (AOP). It automatically generates a {ClassName}Proxy subclass that wraps every virtual method with a configurable interceptor pipeline — with zero runtime overhead from dynamic proxies or reflection at call time.
How It Works
- Decorate a class with
[InterceptedBy]to register one or more interceptors. - The generator detects the attribute at compile time and emits a
{ClassName}Proxyclass. - The proxy overrides every
virtualoroverridemethod, routing each call through the interceptor chain before (optionally) calling the base implementation. - Register your services using
RegisterTypesfromAssembly<T>()— it automatically substitutes the proxy where the original type is expected.
Usage
1. Implement IAsyncInterceptor
using CoreOne;
public class LoggingInterceptor : IAsyncInterceptor
{
public async Task<object?> InterceptAsync(IInvocation invocation)
{
Console.WriteLine($"Calling {invocation.MethodName}");
var result = await invocation.ProceedAsync();
Console.WriteLine($"Finished {invocation.MethodName}");
return result;
}
}
2. Decorate the target class
using CoreOne.Attributes;
// Generic form (preferred)
[InterceptedBy<LoggingInterceptor>]
public class OrderService
{
public virtual async Task<Order> CreateOrderAsync(OrderRequest request)
{
// business logic
}
public virtual Order GetOrder(int id)
{
// business logic
}
}
// Non-generic form (works with older C# targets)
[InterceptedBy(typeof(LoggingInterceptor))]
public class PaymentService
{
public virtual Task<bool> ChargeAsync(decimal amount) { ... }
}
3. Register with DI
// Automatically discovers OrderServiceProxy and registers it as OrderService
services.RegisterTypesfromAssembly<OrderService>();
// Manual registration — inject OrderServiceProxy wherever OrderService is expected
services.AddScoped<LoggingInterceptor>();
services.AddScoped<OrderService, OrderServiceProxy>();
What Gets Generated
For the following class:
[InterceptedBy<LoggingInterceptor>]
public class OrderService
{
public virtual Task<Order> CreateOrderAsync(int id) { ... }
}
The generator emits approximately:
// <auto-generated/>
public partial class OrderServiceProxy : OrderService
{
private static readonly MethodInfo _CreateOrderAsync_Int32Method = ...;
private readonly IAsyncInterceptor[] _interceptors;
public OrderServiceProxy(LoggingInterceptor interceptor0)
{
_interceptors = new IAsyncInterceptor[] { interceptor0 };
}
public override async Task<Order> CreateOrderAsync(int id)
{
var invocation = new Invocation
{
MethodName = "CreateOrderAsync",
Method = _CreateOrderAsync_Int32Method,
Arguments = new object[] { id },
ProceedAsync = async () => return await base.CreateOrderAsync(id),
};
// Build the interceptor pipeline (last-in, first-out)
Func<IInvocation, Task<object?>> next = (inv) => inv.ProceedAsync();
for (int i = _interceptors.Length - 1; i >= 0; i--)
{
var interceptor = _interceptors[i];
var currentNext = next;
next = (inv) => interceptor.InterceptAsync(new Invocation { ..., ProceedAsync = () => currentNext(inv) });
}
var result = await next(invocation);
return (Order)result!;
}
}
Interceptor Pipeline
Multiple interceptors are composed into a pipeline using middleware-style chaining (similar to ASP.NET Core middleware). Interceptors are applied last-registered, first-executed:
[InterceptedBy<LoggingInterceptor>]
[InterceptedBy<TimingInterceptor>]
[InterceptedBy<CachingInterceptor>]
public class ProductService
{
public virtual Task<Product> GetProductAsync(int id) { ... }
}
// Execution order: CachingInterceptor → TimingInterceptor → LoggingInterceptor → base method
Each interceptor receives an IInvocation and calls ProceedAsync() to advance to the next stage:
public class TimingInterceptor : IAsyncInterceptor
{
public async Task<object?> InterceptAsync(IInvocation invocation)
{
var sw = Stopwatch.StartNew();
var result = await invocation.ProceedAsync(); // advance pipeline
Console.WriteLine($"{invocation.MethodName} took {sw.ElapsedMilliseconds}ms");
return result;
}
}
An interceptor can short-circuit the pipeline by returning early without calling ProceedAsync():
public class CachingInterceptor : IAsyncInterceptor
{
public async Task<object?> InterceptAsync(IInvocation invocation)
{
var key = invocation.MethodName;
if (_cache.TryGet(key, out var cached))
return cached; // skip base method entirely
var result = await invocation.ProceedAsync();
_cache.Set(key, result);
return result;
}
}
IInvocation Reference
| Member | Type | Description |
|---|---|---|
MethodName |
string |
Name of the method being intercepted |
Method |
MethodInfo |
Cached MethodInfo for the method |
Arguments |
object[] |
Arguments passed to the method call |
ProceedAsync |
Func<Task<object?>> |
Delegate that advances to the next interceptor or base method |
[InterceptedBy] Attribute Options
| Form | Example |
|---|---|
| Generic (preferred) | [InterceptedBy<MyInterceptor>] |
| Non-generic | [InterceptedBy(typeof(MyInterceptor))] |
| With lifetime | [InterceptedBy<MyInterceptor>(ServiceLifetime.Singleton)] |
| Multiple interceptors | Stack multiple [InterceptedBy] attributes |
Supported Method Signatures
The generator intercepts all virtual and override ordinary methods including:
| Signature | Supported |
|---|---|
void methods |
✅ |
Task (async, no return) |
✅ |
Task<T> (async with return) |
✅ |
| Synchronous return values | ✅ |
Generic methods Method<T>(...) |
✅ |
| Overloaded methods | ✅ (distinguished by parameter types) |
Note: Non-virtual methods are not intercepted. Mark methods
virtualto include them in the proxy.
DI Auto-Registration
RegisterTypesfromAssembly<T>() scans the assembly and automatically wires the proxy:
// Registers OrderServiceProxy as the implementation for OrderService
services.RegisterTypesfromAssembly<OrderService>();
The scanner finds any type named {OriginalName}Proxy in the same namespace that is a subclass of the original type, and substitutes it transparently in the DI container.
Learn more about Target Frameworks and .NET Standard.
-
.NETStandard 2.0
- Microsoft.CodeAnalysis.CSharp (>= 4.14.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.