LightObjects is an extremely light and modern .NET library that provides small interfaces, helpers, and a source generator for building value objects and strongly typed identifiers. It is designed for applications that want explicit domain types without adding unnecessary runtime overhead or allocation-heavy abstractions.
This library currently targets .NET 8.0, .NET 9.0, and .NET 10.0. The source generator is included
in the LightObjects package and is delivered as a Roslyn analyzer.
Install the library from NuGet:
dotnet add package LightObjectsThe old LightObjects.Generated NuGet package is deprecated. Source generator support is now included
in the single LightObjects package.
This library depends on LightResults for creation, parsing, and conversion results. No separate source generator package is required.
- Lightweight - Only contains what's necessary to define value object contracts and generated identifiers.
- Explicit - Strongly typed identifiers prevent accidentally mixing unrelated primitive values.
- Generated - Common identifier behavior can be generated from a single partial type declaration.
- Immutable - Generated structs are readonly and generated classes expose no mutable state.
- Modern - Built against the latest version of .NET using static abstract interface members.
- Native - Written, compiled, and tested against current .NET releases.
- Compatible - Multi-targeted for current LTS and STS releases.
- Trimmable - The runtime library is compatible with ahead-of-time compilation (AOT).
- Performant - Generated code uses direct value comparisons, ordinal string comparisons, and minimal allocations.
LightObjects centers on value object contracts and generated strongly typed identifiers.
- The
IValueObject<TValue, TSelf>interface exposes the underlying value contract. - The
ICreatableValueObject<TValue, TSelf>interface definesCreateandTryCreate. - The
IParsableValueObject<TSelf>interface definesParseandTryParse. - The
IConvertibleValueObject<TSource, TSelf>interface definesConvertandTryConvert. - The
ICloneableValueObject<TSelf>interface definesClone. - The
[GeneratedIdentifier<T>]attribute generates strongly typed identifier implementations.
Add the GeneratedIdentifier attribute to a partial struct or class.
usingLightObjects.Generated;namespaceMyProject.Identifiers;[GeneratedIdentifier<Guid>]publicreadonlypartialstructCustomerId;The generator supports short, int, long, string, and Guid identifiers.
Generated identifiers expose Create and TryCreate.
varcustomerId=CustomerId.Create(Guid.NewGuid());varresult=CustomerId.TryCreate(Guid.NewGuid());if(result.IsSuccess(outvaridentifier,outvarerror)){Console.WriteLine(identifier);}else{Console.WriteLine(error.Message);}Create throws a ValueObjectException when validation fails. TryCreate returns a
Result<TIdentifier> so failures can be handled without exceptions.
Because generated identifiers are partial types, you can add well-known static values directly to
the user-authored declaration. Initialize each value through the generated Create method.
usingLightObjects.Generated;namespaceMyProject.Identifiers;[GeneratedIdentifier<int>]publicreadonlypartialstructStatusId{publicstaticStatusIdPending{get;}=Create(1);publicstaticStatusIdEnabled{get;}=Create(2);publicstaticStatusIdDisabled{get;}=Create(3);publicstaticStatusIdArchived{get;}=Create(4);}When the identifier mirrors an enum or lookup table, cast the enum value to the underlying identifier type.
publicenumStatus{Pending=1,Enabled=2,Disabled=3,Archived=4,}[GeneratedIdentifier<int>]publicreadonlypartialstructStatusId{publicstaticStatusIdPending{get;}=Create((int)Status.Pending);publicstaticStatusIdEnabled{get;}=Create((int)Status.Enabled);publicstaticStatusIdDisabled{get;}=Create((int)Status.Disabled);publicstaticStatusIdArchived{get;}=Create((int)Status.Archived);publicstaticIReadOnlyList<StatusId>All{get;}=[Pending,Enabled,Disabled,Archived,];}This keeps call sites strongly typed while still making fixed database, enum, or lookup identifiers easy to reuse.
Generated non-string identifiers expose Parse and TryParse.
varcustomerId=CustomerId.Parse("9b6f1bc8-51f2-4f2d-b48e-3ff1a6ed95e9");if(CustomerId.TryParse(input,outvarparsedCustomerId)){Console.WriteLine(parsedCustomerId);}The TryParse(string) overload returns a Result<TIdentifier> when you want the failure message.
varresult=CustomerId.TryParse(input);if(result.IsFailure(outvarerror)){Console.WriteLine(error.Message);}String identifiers must be declared as classes. The generator reports a warning for
[GeneratedIdentifier<string>] structs because the default value of a string-backed struct can hold
null. String identifiers validate that the value is not null, empty, or whitespace.
usingLightObjects.Generated;namespaceMyProject.Identifiers;[GeneratedIdentifier<string>]publicsealedpartialclassProductCode;varproductCode=ProductCode.Create("ABC-123");Generated identifiers can use custom validation. Add a Validate method to the partial identifier
type with this exact signature:
privatestaticResultValidate(TValuevalue)The method name and casing, private accessibility, static modifier, LightResults.Result return
type, and single input parameter type must all match exactly.
usingLightObjects.Generated;usingLightResults;namespaceMyProject.Identifiers;[GeneratedIdentifier<int>]publicreadonlypartialstructPositiveOrderId{privatestaticResultValidate(intvalue){if(value<=0)returnResult.Failure("The value must be greater than zero.");returnResult.Success();}}When the generator detects the exact private static Result Validate(TValue value) signature, it does
not emit its default validation method and the generated Create, TryCreate, Parse, and TryParse
methods call the custom method instead.
If the method does not match exactly, it is not treated as custom validation and the generator emits the default validation method.
For string identifiers, custom validation replaces the default null, empty, and whitespace validation, so include those checks yourself when they still matter.
Generated numeric and Guid identifiers expose a typed conversion method.
[GeneratedIdentifier<int>]publicreadonlypartialstructOrderId;varorderId=OrderId.Create(42);varvalue=orderId.ToInt32();All generated identifiers also implement IValueObject<TValue, TSelf>.
varrawValue=((IValueObject<Guid,CustomerId>)customerId).Value;Generated identifiers include System.Text.Json converters. Non-generic identifiers and generic
class identifiers also support TypeConverter conversion. Generic struct identifiers intentionally
omit TypeConverter metadata because .NET does not provide a component-model attribute path that can
pass the closed generic struct type to the converter.
usingSystem.Text.Json;publicsealedrecordCustomer{publicrequiredCustomerIdId{get;init;}publicrequiredstringName{get;init;}}varcustomer=newCustomer{Id=CustomerId.Create(Guid.NewGuid()),Name="Ada",};varjson=JsonSerializer.Serialize(customer);varroundTripped=JsonSerializer.Deserialize<Customer>(json);You can implement the interfaces directly when a value object needs custom behavior.
usingLightObjects;usingLightResults;publicreadonlyrecordstructEmailAddress:ICreatableValueObject<string,EmailAddress>,IValueObject<string,EmailAddress>{publicstringValue{get;init;}publicstaticEmailAddressCreate(stringvalue){varresult=TryCreate(value);if(result.IsSuccess(outvaremailAddress,outvarerror))returnemailAddress;thrownewValueObjectException(error.Message);}publicstaticResult<EmailAddress>TryCreate(stringvalue){if(string.IsNullOrWhiteSpace(value)||!value.Contains('@',StringComparison.Ordinal))returnResult.Failure<EmailAddress>("The email address is invalid.");returnResult.Success(newEmailAddress{Value=value});}}TypeExtensions can detect whether a type implements IValueObject<TValue, TSelf> and expose the
underlying value type.
if(typeof(CustomerId).IsValueObjectType(outvarvalueType)){Console.WriteLine(valueType.Name);}LightObjects 10.0 ships the runtime library and source generator in one NuGet package. Consumers only
need to reference LightObjects.
The old LightObjects.Generated NuGet package is deprecated and replaced by the single LightObjects
package.
- Remove the
LightObjects.Generatedpackage reference. - Add or update the
LightObjectspackage reference to version10.0.0or later. - Keep existing
using LightObjects.Generated;statements. The generated attribute namespace has not changed.
