LightObjects 10.0.0

Prefix Reserved
dotnet add package LightObjects --version 10.0.0
                    
NuGet\Install-Package LightObjects -Version 10.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="LightObjects" Version="10.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="LightObjects" Version="10.0.0" />
                    
Directory.Packages.props
<PackageReference Include="LightObjects" />
                    
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 LightObjects --version 10.0.0
                    
#r "nuget: LightObjects, 10.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 LightObjects@10.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=LightObjects&version=10.0.0
                    
Install as a Cake Addin
#tool nuget:?package=LightObjects&version=10.0.0
                    
Install as a Cake Tool

Banner

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.

test nuget downloads

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 defines Create and TryCreate.
  • The IParsableValueObject<TSelf> interface defines Parse and TryParse.
  • The IConvertibleValueObject<TSource, TSelf> interface defines Convert and TryConvert.
  • The ICloneableValueObject<TSelf> interface defines Clone.
  • 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.

  1. Remove the LightObjects.Generated package reference.
  2. Add or update the LightObjects package reference to version 10.0.0 or later.
  3. Keep existing using LightObjects.Generated; statements. The generated attribute namespace has not changed.
Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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