ErikLieben.FA.StronglyTypedIds 2.0.5

There is a newer version of this package available.
See the version list below for details.
dotnet add package ErikLieben.FA.StronglyTypedIds --version 2.0.5
                    
NuGet\Install-Package ErikLieben.FA.StronglyTypedIds -Version 2.0.5
                    
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="ErikLieben.FA.StronglyTypedIds" Version="2.0.5" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ErikLieben.FA.StronglyTypedIds" Version="2.0.5" />
                    
Directory.Packages.props
<PackageReference Include="ErikLieben.FA.StronglyTypedIds" />
                    
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 ErikLieben.FA.StronglyTypedIds --version 2.0.5
                    
#r "nuget: ErikLieben.FA.StronglyTypedIds, 2.0.5"
                    
#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 ErikLieben.FA.StronglyTypedIds@2.0.5
                    
#: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=ErikLieben.FA.StronglyTypedIds&version=2.0.5
                    
Install as a Cake Addin
#tool nuget:?package=ErikLieben.FA.StronglyTypedIds&version=2.0.5
                    
Install as a Cake Tool

ErikLieben.FA.StronglyTypedIds

NuGet Changelog .NET 9.0

Quality Gate Status Maintainability Rating Security Rating Technical Debt Lines of Code Coverage Known Vulnerabilities

Minimal, allocation-friendly strongly typed IDs for .NET, with a Roslyn source generator that adds JSON conversion, parsing, comparison operators, and helpful extensions.

๐Ÿ‘‹ A Friendly Note

This is an opinionated library built primarily for my own projects and coding style. You're absolutely free to use it (it's MIT licensed!), but please don't expect free support or feature requests. If it works for you, great! If not, there are many other excellent libraries in the .NET ecosystem.

That said, I do welcome bug reports and thoughtful contributions. If you're thinking about a feature or change, please open an issue first to discuss it - this helps avoid disappointment if it doesn't align with the library's direction. ๐Ÿ˜Š

๐Ÿš€ Why Strongly Typed IDs?

Raw GUIDs and primitive types in domain models lead to confusion and bugs. Strongly typed IDs provide:

  • ๐ŸŽฏ Explicit intent - AccountId vs ProductId instead of Guid vs Guid
  • ๐Ÿ”’ Type safety - Prevent accidental mixups between different entity IDs
  • ๐Ÿงช Better testing - Clear, expressive domain models that are easy to test
  • โš™๏ธ First-class tooling - JSON serialization, parsing, and conversions work seamlessly

Perfect for Domain-Driven Design, clean architecture, and any application where entity identity matters.

โŒ When NOT to Use This Library

Consider alternatives when:

  • Performance is absolutely critical - The abstraction adds minimal but measurable overhead
  • Simple applications - Basic CRUD without complex domain modeling might not benefit
  • Team unfamiliarity - Your team isn't comfortable with strongly typed patterns
  • Legacy constraints - Existing systems heavily depend on primitive ID types
  • Over-engineering risk - Adding typed IDs would increase complexity without clear benefit

๐Ÿ“ฆ Installation

# Core library with base types
dotnet add package ErikLieben.FA.StronglyTypedIds

# Generator for JSON, parsing, and additional features (recommended)
dotnet add package ErikLieben.FA.StronglyTypedIds.Generator

โšก Quick Start

Define your ID as a partial record and annotate with [GenerateStronglyTypedIdSupport]:

using ErikLieben.FA.StronglyTypedIds;

[GenerateStronglyTypedIdSupport]
public partial record AccountId(Guid Value) : StronglyTypedId<Guid>(Value);

[GenerateStronglyTypedIdSupport]
public partial record ProductId(int Value) : StronglyTypedId<int>(Value);

Use them like value objects with generated capabilities:

// Factory methods
var accountId = AccountId.New();           // Generates new Guid
var productId = ProductId.New();           // Random int

// Parsing
var parsed = AccountId.From("550e8400-e29b-41d4-a716-446655440000");
if (ProductId.TryParse("123", out var tryParsed))
{
    Console.WriteLine($"Parsed: {tryParsed}");
}

// Type safety - this won't compile!
// ProcessAccount(productId);  // โŒ Compiler error

ProcessAccount(accountId);     // โœ… Type safe

// JSON serialization works automatically
var json = JsonSerializer.Serialize(accountId);
var roundTrip = JsonSerializer.Deserialize<AccountId>(json);

๐Ÿ—๏ธ Core Architecture

Base Types

// Minimal base record
public abstract record StronglyTypedId<T>(T Value) : IStronglyTypedId<T> 
    where T : IEquatable<T>

// Interface for generic constraints
public interface IStronglyTypedId<out T>
{
    T Value { get; }
}

Generator Attribute

[GenerateStronglyTypedIdSupport]  // Enables all features by default
public partial record YourId(Type Value) : StronglyTypedId<Type>(Value);

๐Ÿ› ๏ธ Supported Underlying Types

The generator provides tailored support for common ID types:

Guid-based IDs (Most Common)

[GenerateStronglyTypedIdSupport]
public partial record CustomerId(Guid Value) : StronglyTypedId<Guid>(Value);

var id = CustomerId.New();                    // Guid.NewGuid()
var parsed = CustomerId.From("guid-string");  // Validates and parses
bool isEmpty = id.IsEmpty();                  // Checks for Guid.Empty

Integer-based IDs

[GenerateStronglyTypedIdSupport]
public partial record ProductId(int Value) : StronglyTypedId<int>(Value);

var id = ProductId.New();        // Random.Shared.Next()
var specific = ProductId.From("42"); // int.Parse("42")

// Comparison operators work naturally
ProductId a = ProductId.From("1");
ProductId b = ProductId.From("2");
bool less = a < b;  // true

Long-based IDs

[GenerateStronglyTypedIdSupport]
public partial record OrderId(long Value) : StronglyTypedId<long>(Value);

var id = OrderId.New();  // Random.Shared.NextInt64()

String-based IDs

[GenerateStronglyTypedIdSupport]
public partial record ExternalKey(string Value) : StronglyTypedId<string>(Value);

var id = ExternalKey.New();              // Guid.NewGuid().ToString()
var custom = ExternalKey.From("ABC-123"); // Direct assignment

DateTime-based IDs

[GenerateStronglyTypedIdSupport]
public partial record TimestampId(DateTimeOffset Value) : StronglyTypedId<DateTimeOffset>(Value);

var id = TimestampId.New();     // DateTimeOffset.UtcNow
bool isEmpty = id.IsEmpty();    // Checks for DateTimeOffset.MinValue

๐Ÿ”ง Generated Features

The [GenerateStronglyTypedIdSupport] attribute generates:

JSON Serialization

[GenerateStronglyTypedIdSupport]
public partial record UserId(Guid Value) : StronglyTypedId<Guid>(Value);

var user = UserId.New();
var json = JsonSerializer.Serialize(user);
// Output: "550e8400-e29b-41d4-a716-446655440000"

var deserialized = JsonSerializer.Deserialize<UserId>(json);
// Works seamlessly with System.Text.Json

Parsing Methods

// Safe parsing with validation
if (UserId.TryParse("invalid-guid", out var userId))
{
    // Won't execute - invalid format
}

// Direct parsing (throws on invalid input)
var validId = UserId.From("550e8400-e29b-41d4-a716-446655440000");

Comparison Operations

var early = TimestampId.From("2024-01-01T00:00:00Z");
var later = TimestampId.From("2024-12-31T23:59:59Z");

bool isEarlier = early < later;   // true
bool isSame = early == later;     // false (record equality)
bool isLaterOrEqual = later >= early; // true

Factory Methods

// Type-specific intelligent defaults
var accountId = AccountId.New();    // New Guid
var productId = ProductId.New();    // Random int
var timestamp = TimestampId.New();  // Current UTC time
var key = ExternalKey.New();        // Guid as string

Extension Methods

// Empty checks for applicable types
var emptyGuid = new UserId(Guid.Empty);
bool isEmpty = emptyGuid.IsEmpty();     // true
bool hasValue = emptyGuid.IsNotEmpty(); // false

// Collection helpers
var userIds = new[] { UserId.New(), UserId.New(), UserId.New() };
Guid[] values = userIds.ToValues().ToArray();           // Extract underlying values
HashSet<Guid> uniqueValues = userIds.ToValueSet();      // Unique underlying values
Dictionary<Guid, string> lookup = userIds.ToValueDictionary(id => $"User-{id}");

โš™๏ธ Customizing Generation

Control which features are generated using attribute properties:

[GenerateStronglyTypedIdSupport(
    GenerateJsonConverter = true,    // System.Text.Json support
    GenerateTypeConverter = true,    // System.ComponentModel.TypeConverter
    GenerateParseMethod = true,      // From() method
    GenerateTryParseMethod = true,   // TryParse() method
    GenerateComparisons = true,      // <, <=, >, >= operators
    GenerateNewMethod = true,        // New() factory method
    GenerateExtensions = true        // IsEmpty(), collection helpers
)]
public partial record ConfigurableId(Guid Value) : StronglyTypedId<Guid>(Value);

Feature Details

Feature When Enabled When Disabled
JsonConverter Automatic serialization with System.Text.Json Manual converter required
TypeConverter Works with model binding, configuration Manual conversion needed
ParseMethod YourId.From(string) available Create instances manually
TryParseMethod YourId.TryParse(string, out result) available Manual validation required
Comparisons <, <=, >, >= operators work Only equality (==, !=) available
NewMethod YourId.New() factory available Use constructor: new YourId(value)
Extensions IsEmpty(), collection helpers available Use .Value property directly

๐Ÿ“‹ Complete Domain Example

using System.Text.Json;
using ErikLieben.FA.StronglyTypedIds;

// Define strongly typed IDs for your domain
[GenerateStronglyTypedIdSupport]
public partial record CustomerId(Guid Value) : StronglyTypedId<Guid>(Value);

[GenerateStronglyTypedIdSupport]
public partial record OrderId(long Value) : StronglyTypedId<long>(Value);

[GenerateStronglyTypedIdSupport]
public partial record ProductId(int Value) : StronglyTypedId<int>(Value);

// Domain entities using typed IDs
public record Customer(CustomerId Id, string Name, string Email);
public record Product(ProductId Id, string Name, decimal Price);
public record OrderLine(ProductId ProductId, int Quantity, decimal UnitPrice);
public record Order(OrderId Id, CustomerId CustomerId, OrderLine[] Lines, DateTimeOffset CreatedAt);

// Service methods are type-safe
public class OrderService
{
    public Order CreateOrder(CustomerId customerId, OrderLine[] lines)
    {
        var orderId = OrderId.New();  // Generated factory method
        return new Order(orderId, customerId, lines, DateTimeOffset.UtcNow);
    }
    
    public Customer? FindCustomer(CustomerId id)
    {
        // Type safety prevents mixing up different ID types
        // This won't compile: FindCustomer(OrderId.New())
        return _customers.Find(c => c.Id == id);
    }
}

// JSON serialization works seamlessly
var customer = new Customer(CustomerId.New(), "John Doe", "john@example.com");
var json = JsonSerializer.Serialize(customer);
var deserialized = JsonSerializer.Deserialize<Customer>(json);

// Parse from external systems
if (CustomerId.TryParse(externalSystemId, out var parsedId))
{
    var customer = orderService.FindCustomer(parsedId);
}

// Work with collections
var customerIds = new[] { CustomerId.New(), CustomerId.New() };
var guidValues = customerIds.ToValues();  // Extract underlying Guids
var uniqueIds = customerIds.ToValueSet(); // HashSet<Guid>

๐Ÿ” How the Generator Works

The Roslyn source generator:

  1. Scans your compilation for records inheriting from StronglyTypedId<T>
  2. Finds the attribute [GenerateStronglyTypedIdSupport]
  3. Analyzes the underlying type (Guid, int, string, etc.)
  4. Generates appropriate code for each enabled feature
  5. Emits partial classes that extend your ID types

Generated code includes:

  • JSON converters compatible with System.Text.Json
  • Type converters for model binding and configuration
  • Static factory and parsing methods
  • Comparison operators when applicable
  • Extension methods for common operations

All generation happens at compile time - there's no runtime reflection or performance impact.

๐Ÿ’ก Best Practices

Do's โœ…

  • Use descriptive names - CustomerId, ProductId, OrderId instead of generic Id
  • Be consistent - Use the same underlying type for similar concepts
  • Leverage type safety - Let the compiler catch ID mixups at build time
  • Generate all features - Unless you have specific performance concerns
  • Test with real IDs - Use the New() factory in unit tests for realistic scenarios

Don'ts โŒ

  • Don't use for all primitives - Only create typed IDs for entity identifiers
  • Don't mix underlying types - Stick to one type (usually Guid) across your domain
  • Don't disable safety features - Keep JSON and parsing support enabled unless necessary
  • Don't over-engineer - Simple lookup keys might not need strongly typed IDs

๐Ÿ“š Inspiration & Prior Art

This library builds on foundational work in the .NET community around strongly typed IDs and combating primitive obsession:

Primitive Obsession (1999)

The core concept was first formalized by Martin Fowler in his seminal book "Refactoring: Improving the Design of Existing Code", where he identified "Primitive Obsession" as a code smell that occurs when primitive types are overused to represent domain concepts.

Andrew Lock's Pioneering Work

This library was significantly inspired by Andrew Lock's groundbreaking blog series and StronglyTypedId library. His comprehensive work brought strongly typed IDs to mainstream .NET development:

Original Blog Series (2019-2021):

Library Evolution Updates:

GitHub Repository: andrewlock/StronglyTypedId

Andrew's work demonstrated the value of strongly typed IDs and provided the first widely-adopted source generator solution for .NET. His library uses a struct-based approach with extensive customization options through a template system, evolving from CodeGeneration.Roslyn to native source generators.

While less customizable than Andrew's template system, this library aims to provide a simpler API that covers the majority of use cases for my use cases with minimal complexity.

โ“ FAQ

Q: Do I need the generator package? A: Technically no - the core types work without it. But you'll miss JSON serialization, parsing helpers, comparisons, and extensions that make strongly typed IDs practical.

Q: Does this work with Entity Framework Core? A: Yes, but you may need custom value converters. The generated TypeConverter can help, or you can map to the underlying Value property directly.

Q: Is this compatible with Native AOT? A: Yes! The generator produces regular C# code at compile time. No reflection or runtime code generation is used.

Q: Can I use this with ASP.NET Core model binding? A: Yes, the generated TypeConverter enables automatic conversion from route parameters and form data.

Q: What about performance? A: Minimal overhead - records are value types with efficient equality. The wrapper adds one level of indirection but optimizes well.

๐Ÿ“„ License

MIT License - see the LICENSE file for details.

Product Compatible and additional computed target framework versions.
.NET 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net9.0

    • 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
2.0.6 13 9/13/2025
2.0.5 117 8/24/2025
2.0.4 58 8/23/2025