Skip to main content

Usage Patterns

Dictionary Keys

The most common motivation for correct GetHashCode + Equals is using a type as a dictionary key. The generator makes this safe and allocation-free.

[ValueObject]
public partial class CacheKey
{
public string Region { get; }
public string Id { get; }
public string Version { get; }

public CacheKey(string region, string id, string version)
{
Region = region; Id = id; Version = version;
}
}

// In a caching layer
var cache = new Dictionary<CacheKey, byte[]>();

void Store(string region, string id, string version, byte[] data)
=> cache[new CacheKey(region, id, version)] = data;

byte[]? Fetch(string region, string id, string version)
{
var key = new CacheKey(region, id, version);
return cache.TryGetValue(key, out var data) ? data : null;
}

Every dictionary lookup calls GetHashCode() once and Equals() on collision — both are zero-allocation.


HashSets

Use HashSet<T> with value objects for deduplication or membership tests.

[ValueObject]
public partial class Tag
{
public string Value { get; }
public Tag(string value) => Value = value.ToLowerInvariant().Trim();
}

// Deduplication
var rawTags = new[] { "C#", "c#", " C# ", "dotnet", "DotNet" };
var uniqueTags = rawTags.Select(t => new Tag(t)).ToHashSet();
// Result: { "c#", "dotnet" }

// Membership test
bool hasTag = uniqueTags.Contains(new Tag("C#")); // true

LINQ GroupBy

GroupBy uses a dictionary internally. Correct equality makes grouping by value objects work naturally.

[ValueObject]
public readonly partial struct Department
{
public string Code { get; }
public string Name { get; }
public Department(string code, string name) { Code = code; Name = name; }
}

var employees = GetEmployees();

var byDepartment = employees
.GroupBy(e => e.Department)
.ToDictionary(g => g.Key, g => g.ToList());

Employees with the same Department value object land in the same group.


EF Core — Owned Entities

Value objects commonly map to EF Core owned entities. [ValueObject] handles equality at the domain layer; EF Core handles persistence.

[ValueObject]
public partial class PostalAddress
{
[EqualityMember] public string Line1 { get; }
[EqualityMember] public string? Line2 { get; }
[EqualityMember] public string City { get; }
[EqualityMember] public string PostalCode { get; }
[EqualityMember] public string Country { get; }

// Audit field — not part of domain identity
public DateTime UpdatedAt { get; private set; }

public PostalAddress(string line1, string? line2, string city, string postalCode, string country)
{
Line1 = line1; Line2 = line2; City = city;
PostalCode = postalCode; Country = country;
UpdatedAt = DateTime.UtcNow;
}
}

// EF Core mapping
public class OrderConfiguration : IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.OwnsOne(o => o.ShippingAddress, addr =>
{
addr.Property(a => a.Line1).HasMaxLength(200);
addr.Property(a => a.City).HasMaxLength(100);
addr.Property(a => a.PostalCode).HasMaxLength(20);
addr.Property(a => a.Country).HasMaxLength(2);
});
}
}

EF Core — Value Converters

For single-value wrapper types (strongly typed IDs), use a value converter:

[ValueObject]
public partial class EmailAddress
{
public string Value { get; }
public EmailAddress(string value) => Value = value.ToLowerInvariant().Trim();
}

// In DbContext or IEntityTypeConfiguration
modelBuilder.Entity<User>()
.Property(u => u.Email)
.HasConversion(
e => e.Value,
s => new EmailAddress(s));

Pattern Matching

Value objects work naturally with C# switch expressions.

[ValueObject]
public partial class OrderStatus
{
public string Value { get; }
public OrderStatus(string value) => Value = value;

public static readonly OrderStatus Pending = new("Pending");
public static readonly OrderStatus Confirmed = new("Confirmed");
public static readonly OrderStatus Shipped = new("Shipped");
public static readonly OrderStatus Cancelled = new("Cancelled");
}

string Describe(OrderStatus status) => status switch
{
_ when status == OrderStatus.Pending => "Awaiting confirmation",
_ when status == OrderStatus.Confirmed => "Processing",
_ when status == OrderStatus.Shipped => "On its way",
_ when status == OrderStatus.Cancelled => "Cancelled",
_ => "Unknown"
};

JSON Serialization (System.Text.Json)

For types with constructor parameters, register a custom converter:

[ValueObject]
[JsonConverter(typeof(EmailAddressConverter))]
public partial class EmailAddress
{
public string Value { get; }
public EmailAddress(string value) => Value = value.ToLowerInvariant().Trim();
}

public class EmailAddressConverter : JsonConverter<EmailAddress>
{
public override EmailAddress Read(ref Utf8JsonReader reader, Type type, JsonSerializerOptions options)
=> new(reader.GetString()!);

public override void Write(Utf8JsonWriter writer, EmailAddress value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.Value);
}