Skip to content

Configuration Strongly Typed IDs

quyvu edited this page Oct 13, 2026 · 2 revisions

Strongly Typed IDs

Configure FxMap to work seamlessly with strongly typed ID patterns, ensuring type safety throughout your application.

What Are Strongly Typed IDs?

Strongly typed IDs are custom value types that wrap primitive types (like string or Guid) to provide type safety and prevent accidentally mixing different types of IDs.

Without Strongly Typed IDs

public class Order
{
    public string Id { get; set; }
    public string UserId { get; set; }
    public string ProductId { get; set; }
}
 
// ❌ Easy to make mistakes!
order.UserId = product.Id;  // Wrong ID type!

With Strongly Typed IDs

public class Order
{
    public OrderId Id { get; set; }
    public UserId UserId { get; set; }
    public ProductId ProductId { get; set; }
}
 
// ✅ Compile-time type safety!
order.UserId = productId;  // Compiler error!

The Challenge with FxMap

FxMap internally uses strings for ID lookups and comparisons. When you use strongly typed IDs, FxMap needs to know how to convert between your custom ID types and the string representation.

Why Configuration Is Needed

FxMap needs explicit converters to translate from string IDs (used in queries) to your strongly typed ID instances.

Configuring ID Converters

Use AddStronglyTypeIdConverter to register your ID converters:

builder.Services.AddFxMap(cfg =>
{
    cfg.AddEntitiesFromAssemblyContaining<SomeEntityAssemblyMarker>();
    cfg.AddProfilesFromAssemblyContaining<SomeProfileAssemblyMarker>();
 
    // Register strongly typed ID converters
    cfg.AddStronglyTypeIdConverter(a => a.OfType<StronglyTypeIdConverters>());
});

Implementing ID Converters

Create a class that implements IStronglyTypeConverter<T> for each strongly typed ID:

using FxMap.Abstractions;
 
// Example strongly typed IDs with different underlying types
public record struct UserId(string Value);      // string-based ID
public record struct OrderId(Guid Value);       // Guid-based ID
public record struct ProvinceId(long Value);    // long-based ID
public record struct ProductId(int Value);      // int-based ID
 
// Converter for UserId (string)
public class UserIdConverter : IStronglyTypeConverter<UserId>
{
    public UserId Convert(string input) => new UserId(input);
    public bool CanConvert(string input) => !string.IsNullOrWhiteSpace(input);
}
 
// Converter for OrderId (Guid)
public class OrderIdConverter : IStronglyTypeConverter<OrderId>
{
    public OrderId Convert(string input) => new OrderId(Guid.Parse(input));
    public bool CanConvert(string input) => Guid.TryParse(input, out _);
}
 
// Converter for ProvinceId (long)
public class ProvinceIdConverter : IStronglyTypeConverter<ProvinceId>
{
    public ProvinceId Convert(string input) => new ProvinceId(long.Parse(input));
    public bool CanConvert(string input) => long.TryParse(input, out _);
}
 
// Converter for ProductId (int)
public class ProductIdConverter : IStronglyTypeConverter<ProductId>
{
    public ProductId Convert(string input) => new ProductId(int.Parse(input));
    public bool CanConvert(string input) => int.TryParse(input, out _);
}

Creating a Converter Registry

Organize all your converters in a single registry class:

using FxMap.Abstractions;
 
public class StronglyTypeIdConverters
{
    // Converter for UserId (string)
    public class UserIdConverter : IStronglyTypeConverter<UserId>
    {
        public UserId Convert(string input) => new UserId(input);
        public bool CanConvert(string input) => !string.IsNullOrWhiteSpace(input);
    }
 
    // Converter for OrderId (Guid)
    public class OrderIdConverter : IStronglyTypeConverter<OrderId>
    {
        public OrderId Convert(string input) => new OrderId(Guid.Parse(input));
        public bool CanConvert(string input) => Guid.TryParse(input, out _);
    }
 
    // Converter for ProvinceId (long)
    public class ProvinceIdConverter : IStronglyTypeConverter<ProvinceId>
    {
        public ProvinceId Convert(string input) => new ProvinceId(long.Parse(input));
        public bool CanConvert(string input) => long.TryParse(input, out _);
    }
 
    // Converter for ProductId (int)
    public class ProductIdConverter : IStronglyTypeConverter<ProductId>
    {
        public ProductId Convert(string input) => new ProductId(int.Parse(input));
        public bool CanConvert(string input) => int.TryParse(input, out _);
    }
 
    // Converter for CountryId (string)
    public class CountryIdConverter : IStronglyTypeConverter<CountryId>
    {
        public CountryId Convert(string input) => new CountryId(input);
        public bool CanConvert(string input) => !string.IsNullOrWhiteSpace(input);
    }
}

Complete Example

Here's a complete example showing strongly typed IDs with FxMap:

// Define strongly typed IDs with different underlying types
public record struct UserId(string Value);      // string-based
public record struct OrderId(Guid Value);       // Guid-based
public record struct ProvinceId(long Value);    // long-based
public record struct CountryId(string Value);   // string-based
 
// Define converters
public class StronglyTypeIdConverters
{
    public class UserIdConverter : IStronglyTypeConverter<UserId>
    {
        public UserId Convert(string input) => new UserId(input);
        public bool CanConvert(string input) => !string.IsNullOrWhiteSpace(input);
    }
 
    public class OrderIdConverter : IStronglyTypeConverter<OrderId>
    {
        public OrderId Convert(string input) => new OrderId(Guid.Parse(input));
        public bool CanConvert(string input) => Guid.TryParse(input, out _);
    }
 
    public class ProvinceIdConverter : IStronglyTypeConverter<ProvinceId>
    {
        public ProvinceId Convert(string input) => new ProvinceId(long.Parse(input));
        public bool CanConvert(string input) => long.TryParse(input, out _);
    }
 
    public class CountryIdConverter : IStronglyTypeConverter<CountryId>
    {
        public CountryId Convert(string input) => new CountryId(input);
        public bool CanConvert(string input) => !string.IsNullOrWhiteSpace(input);
    }
}
 
// Entity configs with strongly typed IDs
public class UserConfig : EntityConfigureOf<User>
{
    protected override void Configure()
    {
        Id(x => x.Id);
        DefaultProperty(x => x.Name);
        UseDistributedKey<UserDistributedKey>();
    }
}
 
public class ProvinceConfig : EntityConfigureOf<Province>
{
    protected override void Configure()
    {
        Id(x => x.Id);
        DefaultProperty(x => x.Name);
        UseDistributedKey<ProvinceDistributedKey>();
    }
}
 
// Domain models with strongly typed IDs
public class User
{
    public UserId Id { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public ProvinceId ProvinceId { get; set; }
}
 
public class Province
{
    public ProvinceId Id { get; set; }
    public string Name { get; set; }
    public CountryId CountryId { get; set; }
    public Country Country { get; set; }
}
 
// Configure FxMap
builder.Services.AddFxMap(cfg =>
{
    cfg.AddEntitiesFromAssemblyContaining<SomeEntityAssemblyMarker>();
    cfg.AddProfilesFromAssemblyContaining<SomeProfileAssemblyMarker>();
 
    // Register the strongly typed ID converters
    cfg.AddStronglyTypeIdConverter(a => a.OfType<StronglyTypeIdConverters>());
})
.AddEntityFrameworkCore(options => options.AddDbContexts(typeof(TestDbContext), typeof(OtherDbContext)...)});

Using Strongly Typed IDs in DTOs

Once configured, you can use strongly typed IDs in your response DTOs:

// DTO with strongly typed IDs
public class OrderResponse
{
    public OrderId Id { get; set; }        // Guid-based ID
    public UserId UserId { get; set; }     // string-based ID
    public string UserName { get; set; }
    public string UserEmail { get; set; }
    public ProvinceId UserProvinceId { get; set; }
}
 
// Profile using strongly typed ID in .Of()
public class OrderResponseProfile : ProfileOf<OrderResponse>
{
    protected override void Configure()
    {
        // FxMap automatically converts UserId (string) for lookup
        UseDistributedKey<UserDistributedKey>()
            .Of(x => x.UserId)           // strongly typed UserId
            .For(x => x.UserName)        // default property (Name)
            .For(x => x.UserEmail, "Email")
            .For(x => x.UserProvinceId, "ProvinceId"); // maps to strongly typed ProvinceId
    }
}
 
// Usage
var response = new OrderResponse
{
    Id = new OrderId(Guid.NewGuid()),
    UserId = new UserId("user-456")
};
 
await _mapper.MapDataAsync(response);
// UserName, UserEmail, and UserProvinceId are now populated!

Generic Converter Pattern

For IDs following a common pattern, you can create a generic converter:

// Base interface for strongly typed IDs
public interface IStronglyTypedId<T>
{
    T Value { get; }
}
 
// Generic strongly typed ID record
public record struct StronglyTypedId<T>(T Value) : IStronglyTypedId<T>;
 
// Generic converter
public class GenericStronglyTypedIdConverter<TId, TValue>
    : IStronglyTypeConverter<StronglyTypedId<TValue>>
    where TId : IStronglyTypedId<TValue>
{
    public StronglyTypedId<TValue> Convert(string input)
    {
        var value = (TValue)System.Convert.ChangeType(input, typeof(TValue));
        return new StronglyTypedId<TValue>(value);
    }
 
    public bool CanConvert(string input)
    {
        return !string.IsNullOrWhiteSpace(input);
    }
}
 
// Usage
public record struct UserId(string Value) : IStronglyTypedId<string>;
public record struct ProductId(Guid Value) : IStronglyTypedId<Guid>;

Best Practices

  • Use records or readonly structs for immutable ID types
  • Keep all converters in a single registry class for easy management
  • Implement validation in the converter if needed
  • Consider using a base interface for consistency across all IDs

Common Issues

  • Forgetting to register converters will cause runtime errors when using strongly typed IDs
  • The CanConvert method should validate the input string before conversion
  • Converters are only needed for ID properties, not all strongly typed properties

Source: https://fxmapper.net/docs/configuration/strongly-typed-ids/ — the website is always the most up to date version.

Clone this wiki locally