LightObjects 10.0.0
Prefix Reserveddotnet add package LightObjects --version 10.0.0
NuGet\Install-Package LightObjects -Version 10.0.0
<PackageReference Include="LightObjects" Version="10.0.0" />
<PackageVersion Include="LightObjects" Version="10.0.0" />
<PackageReference Include="LightObjects" />
paket add LightObjects --version 10.0.0
#r "nuget: LightObjects, 10.0.0"
#:package LightObjects@10.0.0
#addin nuget:?package=LightObjects&version=10.0.0
#tool nuget:?package=LightObjects&version=10.0.0
LightObjects - Value Objects and Strongly Typed Identifiers for .NET
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.
References
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.
Installation
Install the library from NuGet:
dotnet add package LightObjects
The old LightObjects.Generated NuGet package is deprecated. Source generator support is now included
in the single LightObjects package.
Dependencies
This library depends on LightResults for creation, parsing, and conversion results. No separate source generator package is required.
Advantages of this library
- 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.
Getting Started
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.
Creating a generated identifier
Add the GeneratedIdentifier attribute to a partial struct or class.
using LightObjects.Generated;
namespace MyProject.Identifiers;
[GeneratedIdentifier<Guid>]
public readonly partial struct CustomerId;
The generator supports short, int, long, string, and Guid identifiers.
Creating identifiers
Generated identifiers expose Create and TryCreate.
var customerId = CustomerId.Create(Guid.NewGuid());
var result = CustomerId.TryCreate(Guid.NewGuid());
if (result.IsSuccess(out var identifier, out var error))
{
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.
Defining static identifier values
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.
using LightObjects.Generated;
namespace MyProject.Identifiers;
[GeneratedIdentifier<int>]
public readonly partial struct StatusId
{
public static StatusId Pending { get; } = Create(1);
public static StatusId Enabled { get; } = Create(2);
public static StatusId Disabled { get; } = Create(3);
public static StatusId Archived { get; } = Create(4);
}
When the identifier mirrors an enum or lookup table, cast the enum value to the underlying identifier type.
public enum Status
{
Pending = 1,
Enabled = 2,
Disabled = 3,
Archived = 4,
}
[GeneratedIdentifier<int>]
public readonly partial struct StatusId
{
public static StatusId Pending { get; } = Create((int)Status.Pending);
public static StatusId Enabled { get; } = Create((int)Status.Enabled);
public static StatusId Disabled { get; } = Create((int)Status.Disabled);
public static StatusId Archived { get; } = Create((int)Status.Archived);
public static IReadOnlyList<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.
Parsing identifiers
Generated non-string identifiers expose Parse and TryParse.
var customerId = CustomerId.Parse("9b6f1bc8-51f2-4f2d-b48e-3ff1a6ed95e9");
if (CustomerId.TryParse(input, out var parsedCustomerId))
{
Console.WriteLine(parsedCustomerId);
}
The TryParse(string) overload returns a Result<TIdentifier> when you want the failure message.
var result = CustomerId.TryParse(input);
if (result.IsFailure(out var error))
{
Console.WriteLine(error.Message);
}
Creating string identifiers
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.
using LightObjects.Generated;
namespace MyProject.Identifiers;
[GeneratedIdentifier<string>]
public sealed partial class ProductCode;
var productCode = ProductCode.Create("ABC-123");
Custom validation
Generated identifiers can use custom validation. Add a Validate method to the partial identifier
type with this exact signature:
private static Result Validate(TValue value)
The method name and casing, private accessibility, static modifier, LightResults.Result return
type, and single input parameter type must all match exactly.
using LightObjects.Generated;
using LightResults;
namespace MyProject.Identifiers;
[GeneratedIdentifier<int>]
public readonly partial struct PositiveOrderId
{
private static Result Validate(int value)
{
if (value <= 0)
return Result.Failure("The value must be greater than zero.");
return Result.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.
Accessing the underlying value
Generated numeric and Guid identifiers expose a typed conversion method.
[GeneratedIdentifier<int>]
public readonly partial struct OrderId;
var orderId = OrderId.Create(42);
var value = orderId.ToInt32();
All generated identifiers also implement IValueObject<TValue, TSelf>.
var rawValue = ((IValueObject<Guid, CustomerId>)customerId).Value;
JSON and type conversion
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.
using System.Text.Json;
public sealed record Customer
{
public required CustomerId Id { get; init; }
public required string Name { get; init; }
}
var customer = new Customer
{
Id = CustomerId.Create(Guid.NewGuid()),
Name = "Ada",
};
var json = JsonSerializer.Serialize(customer);
var roundTripped = JsonSerializer.Deserialize<Customer>(json);
Creating a manual value object
You can implement the interfaces directly when a value object needs custom behavior.
using LightObjects;
using LightResults;
public readonly record struct EmailAddress :
ICreatableValueObject<string, EmailAddress>,
IValueObject<string, EmailAddress>
{
public string Value { get; init; }
public static EmailAddress Create(string value)
{
var result = TryCreate(value);
if (result.IsSuccess(out var emailAddress, out var error))
return emailAddress;
throw new ValueObjectException(error.Message);
}
public static Result<EmailAddress> TryCreate(string value)
{
if (string.IsNullOrWhiteSpace(value) || !value.Contains('@', StringComparison.Ordinal))
return Result.Failure<EmailAddress>("The email address is invalid.");
return Result.Success(new EmailAddress { Value = value });
}
}
Detecting value object types
TypeExtensions can detect whether a type implements IValueObject<TValue, TSelf> and expose the
underlying value type.
if (typeof(CustomerId).IsValueObjectType(out var valueType))
{
Console.WriteLine(valueType.Name);
}
What's new in v10.0
LightObjects 10.0 ships the runtime library and source generator in one NuGet package. Consumers only
need to reference LightObjects.
Migrating from LightObjects.Generated
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.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. 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 is compatible. 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 is compatible. 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. |
-
net10.0
- LightResults (>= 10.0.0)
-
net8.0
- LightResults (>= 10.0.0)
-
net9.0
- LightResults (>= 10.0.0)
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 |
|---|---|---|
| 10.0.0 | 34 | 6/20/2026 |
| 10.0.0-preview.1 | 10,086 | 11/12/2025 |
| 9.0.0-preview.2 | 7,952 | 3/15/2025 |
| 9.0.0-preview.1 | 144 | 3/2/2025 |
