-
Notifications
You must be signed in to change notification settings - Fork 5
Smart Enums Framework Integration
Smart Enums seamlessly integrate with popular .NET frameworks and libraries. This section covers integration with JSON serialization, MessagePack, ASP.NET Core, and Entity Framework Core.
Note: The integrations described in this document apply to keyed Smart Enums (
[SmartEnum<TKey>]). Keyless Smart Enums ([SmartEnum]) have no underlying key value, so they do not support JSON serialization, MessagePack serialization, or EF Core value conversion.
- JSON Serialization
- Zero-Allocation JSON Deserialization (.NET 9+)
- MessagePack Serialization
- Multi-Format Serialization
- ASP.NET Core Integration
- OpenAPI/Swashbuckle Integration
- Entity Framework Core Integration
Smart Enums support both major JSON serialization libraries in .NET:
You have two options for enabling JSON serialization:
The easiest way is to make Thinktecture.Runtime.Extensions.Json / Thinktecture.Runtime.Extensions.Newtonsoft.Json a dependency of the project(s) the Smart Enums are in. The dependency doesn't have to be a direct one but transitive as well.
Both NuGet packages activate generation of additional code that flags the Smart Enum with a JsonConverterAttribute. This way the Smart Enum can be converted to and from JSON without extra code.
If making previously mentioned NuGet package a dependency of project(s) with Smart Enums is not possible or desirable, then the other option is to register a JSON converter with JSON serializer settings. By using a JSON converter directly, the NuGet package can be installed in any project where the JSON settings are configured.
- Use
ThinktectureJsonConverterFactorywithSystem.Text.Json - Use
ThinktectureNewtonsoftJsonConverterFactorywithNewtonsoft.Json
An example for ASP.NET Core application using System.Text.Json:
var webHost = new HostBuilder()
.ConfigureServices(collection =>
{
collection.AddMvc()
.AddJsonOptions(options => options.JsonSerializerOptions
.Converters
.Add(new ThinktectureJsonConverterFactory()));
});An example for minimal apis:
var builder = WebApplication.CreateBuilder();
builder.Services
.ConfigureHttpJsonOptions(options => options.SerializerOptions
.Converters
.Add(new ThinktectureJsonConverterFactory()));The code for Newtonsoft.Json is almost identical:
var webHost = new HostBuilder()
.ConfigureServices(collection =>
{
collection.AddMvc()
.AddNewtonsoftJson(options => options.SerializerSettings
.Converters
.Add(new ThinktectureNewtonsoftJsonConverterFactory()));
});By default, the source generator produces serialization support for all referenced frameworks. To limit code generation to specific frameworks, use the SerializationFrameworks property on the SmartEnumAttribute:
[SmartEnum<string>(SerializationFrameworks = SerializationFrameworks.SystemTextJson)]
public partial class ProductType
{Available values: SerializationFrameworks.All (default), SerializationFrameworks.SystemTextJson, SerializationFrameworks.NewtonsoftJson, SerializationFrameworks.MessagePack. Values can be combined with |.
On .NET 9+, string-based Smart Enums automatically benefit from zero-allocation JSON deserialization when using System.Text.Json. The library transcodes the raw UTF-8 JSON bytes directly to a ReadOnlySpan<char> and performs the enum lookup without allocating a string on the heap.
This happens transparently -- no code changes required. Both the source-generated [JsonConverterAttribute] and the ThinktectureJsonConverterFactory automatically select the span-based converter when available.
To disable this optimization per Smart Enum, use DisableSpanBasedJsonConversion = true on the SmartEnumAttribute:
[SmartEnum<string>(DisableSpanBasedJsonConversion = true)]
public partial class ProductType
{
public static readonly ProductType Electronics = new("Electronics");
public static readonly ProductType Clothing = new("Clothing");
}When using ThinktectureJsonConverterFactory directly, you can also control span-based deserialization at runtime:
// Opt out of span-based deserialization for specific types at runtime
var factory = new ThinktectureJsonConverterFactory(
skipObjectsWithJsonConverterAttribute: true,
skipSpanBasedDeserialization: type => type == typeof(SomeSpecificType));MessagePack is a fast and compact binary serialization format. Smart Enums provide full support for MessagePack serialization.
You have two options for enabling MessagePack serialization:
The easiest way is to make Thinktecture.Runtime.Extensions.MessagePack a dependency of the project(s) the Smart Enums are in. The dependency doesn't have to be a direct one but transitive as well.
The NuGet package activates generation of additional code that flags the Smart Enums with a MessagePackFormatterAttribute. This way the Smart Enum can be converted to and from MessagePack without extra code.
If making previously mentioned NuGet package a dependency of project(s) with Smart Enums is not possible or desirable, then the other option is to register the MessagePack formatter with MessagePack serializer options. By using the ThinktectureMessageFormatterResolver directly, the NuGet package can be installed in any project where the MessagePack options are configured.
An example of a round-trip-serialization of the Smart Enum ProductType.Electronics:
// Use "ThinktectureMessageFormatterResolver"
var resolver = CompositeResolver.Create(ThinktectureMessageFormatterResolver.Instance, StandardResolver.Instance);
var options = MessagePackSerializerOptions.Standard.WithResolver(resolver);
var productType = ProductType.Electronics;
// Serialize to MessagePack
var bytes = MessagePackSerializer.Serialize(productType, options, CancellationToken.None);
// Deserialize from MessagePack
var deserializedProductType = MessagePackSerializer.Deserialize<ProductType>(bytes, options, CancellationToken.None);When your application uses multiple serialization frameworks simultaneously (System.Text.Json, Newtonsoft.Json, MessagePack), the simplest way to ensure all three work is the project dependency approach: reference all three integration packages from the project containing your Smart Enums.
-
Thinktecture.Runtime.Extensions.Json(System.Text.Json) -
Thinktecture.Runtime.Extensions.Newtonsoft.Json(Newtonsoft.Json) -
Thinktecture.Runtime.Extensions.MessagePack(MessagePack)
Each package triggers source-generated converters that use the same key-based Get lookup, so all formats produce consistent round-trip results automatically. This is all you need -- no [ObjectFactory] attribute required.
Note: Zero-allocation JSON deserialization (.NET 9+) is a System.Text.Json-only optimization. The other formats continue using standard key-based conversion.
Note: Keyless Smart Enums (
[SmartEnum]) have no key, so they require[ObjectFactory<T>]for any serialization. See Object Factories -- Smart Enums for details.
If you need to override the default key-based conversion with a custom format, apply [ObjectFactory<T>] with UseForSerialization = SerializationFrameworks.All. All three frameworks will then use your custom Validate and ToValue methods instead of the key.
The idiomatic Smart Enum approach stores the serialization value as per-item data in a constructor parameter:
[SmartEnum<int>]
[ObjectFactory<string>(UseForSerialization = SerializationFrameworks.All)]
public partial class ShippingMethod
{
public static readonly ShippingMethod Standard = new(1, slug: "standard");
public static readonly ShippingMethod Express = new(2, slug: "express");
public static readonly ShippingMethod Overnight = new(3, slug: "overnight");
public string Slug { get; }
private ShippingMethod(int key, string slug) : this(key)
{
Slug = slug;
}
public static ValidationError? Validate(
string? value, IFormatProvider? provider, out ShippingMethod? item)
{
item = Items.FirstOrDefault(i => i.Slug.Equals(value, StringComparison.OrdinalIgnoreCase));
return item is null ? new ValidationError($"Unknown shipping method '{value}'") : null;
}
public string ToValue() => Slug;
}All three frameworks will serialize ShippingMethod.Express as "express" and deserialize it using the same Validate method.
Use the SerializationFrameworks enum to target individual frameworks. This is useful when one framework needs a different representation (e.g., byte[] for MessagePack):
| Value | Description |
|---|---|
None |
No serialization integration |
All |
All supported frameworks |
Json |
System.Text.Json + Newtonsoft.Json |
SystemTextJson |
System.Text.Json only |
NewtonsoftJson |
Newtonsoft.Json only |
MessagePack |
MessagePack only |
Multiple [ObjectFactory] attributes can target different frameworks with non-overlapping UseForSerialization values. See Object Factories -- Multiple Object Factories for details.
Tip: The
SerializationFrameworksproperty on[SmartEnum<TKey>]controls which frameworks get key-based converters.[ObjectFactory<T>]provides alternative custom conversion that takes priority over key-based conversion when both are present.
For seamless integration with ASP.NET Core we need both JSON serialization and model binding support.
Tip: See Convert from/to non-key type to customize model binding behavior by implementing string conversion methods.
The parameter binding of Minimal APIs is not as capable as the model binding of MVC controllers. To make a type bindable it has to implement either TryParse or BindAsync. A Smart Enum implements TryParse (interface IParsable<T>) if the key implements IParsable<T>, so it can be used with Minimal Apis without any changes.
Limitation: Minimal API binding has limited support for returning detailed validation errors. All binding means (
TryParseandBindAsync) don't allow passing custom validation errors to be returned to the client. The only information that can be passed is aboolindicating whether the parameter could be bound or not. If binding fails, it typically results in a generic400 Bad Requestresponse.
ASP.NET MVC gives us more control during model binding. For example, if we expect from client a ProductType and receive the (invalid) value SomeValue, then the ASP.NET Core ModelState must become invalid. In this case we can reject (or let ApiControllerAttribute reject) the request.
By rejecting the request, the client gets the status code BadRequest (400) and the error:
{
"productType": [
"The enumeration item of type 'ProductType' with identifier 'SomeValue' is not valid."
]
}To help out the Model Binding we have to register the ThinktectureModelBinderProvider with ASP.NET Core. By using the custom model binder, the NuGet package can be installed in any project where ASP.NET Core is configured.
Place the "ThinktectureModelBinderProvider" before default providers, so they don't try to bind Smart Enums and Value Objects.
var webHost = new HostBuilder()
.ConfigureServices(collection =>
{
collection.AddMvc(options => options.ModelBinderProviders
.Insert(0, new ThinktectureModelBinderProvider()));
});The ThinktectureModelBinderProvider supports several configuration options to customize how Smart Enums are handled during model binding:
// Configure with custom options
var provider = new ThinktectureModelBinderProvider(
skipBindingFromBody: true // Default: true
);- skipBindingFromBody: When true (default), skips model binding for values coming from request body, allowing the JSON serializer to handle the conversion instead.
Smart Enums can be integrated with Swashbuckle to provide OpenAPI documentation for Web APIs. This generates OpenAPI schemas that represent Smart Enums as their key type with enumerated allowed values, so API consumers see the valid options in the documentation.
To enable OpenAPI support for Smart Enums, register the Thinktecture OpenAPI filters with dependency injection:
services.AddEndpointsApiExplorer()
.AddSwaggerGen(options => options.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" }))
.AddThinktectureOpenApiFilters();You can customize the OpenAPI schema generation with options:
services.AddThinktectureOpenApiFilters(options =>
{
// Configure the schema filter type
options.SmartEnumSchemaFilter = SmartEnumSchemaFilter.Default;
// Additionally configure how Smart Enums are represented in the schema
options.SmartEnumSchemaExtension = SmartEnumSchemaExtension.VarNamesFromStringRepresentation;
});Schema Filters
The SmartEnumSchemaFilter option determines how Smart Enums are represented in the OpenAPI schema:
-
Default: Usesenum: [ key1, key2 ] -
OneOf: UsesoneOf: [ { "title": "key1", "const": "key1" } ] -
AnyOf: UsesanyOf: [ { "title": "key1", "const": "key1" } ] -
AllOf: UsesallOf: [ { "title": "key1", "const": "key1" } ] -
FromDependencyInjection: Resolves implementation ofISmartEnumSchemaFilterfrom dependency injection
Schema Extensions
The SmartEnumSchemaExtension option controls what additional information is added to the schema:
-
None(default): No additional schema extensions -
VarNamesFromStringRepresentation: Extends the schema withx-enum-varnamesusing the string representation of the items -
VarNamesFromDotnetIdentifiers: Extends the schema withx-enum-varnamesusing the .NET identifiers of the items -
FromDependencyInjection: Resolves implementation ofISmartEnumSchemaExtensionfrom dependency injection
With the default configuration, the generated schema for a string-based Smart Enum like ProductType looks like this:
"schemas": {
"ProductType": {
"enum": [
"Books",
"Electronics",
...
],
"type": "string"
}
}Entity Framework Core provides Value Conversion. By providing a value converter, EF Core can convert a Smart Enum (like ProductType) to and from a primitive type (like string) when persisting the data and when reading the value from database.
Choose the package that matches your EF Core version:
You have seven options for setting up EF Core integration:
| Option | Scope |
|---|---|
Option 1: Via DbContextOptionsBuilder (recommended) |
Global registration without touching OnModelCreating
|
Option 2: Via ModelBuilder
|
Automatic registration of all Smart Enums and Value Objects in the entire model |
Option 3: Via EntityTypeBuilder
|
Selective registration for specific entities, owned types, or complex types |
Option 4: Via PropertyBuilder
|
Individual property registration within an entity configuration |
Option 5: Via ComplexTypePropertyBuilder
|
Individual property registration within a complex type |
Option 6: Via PrimitiveCollectionBuilder
|
Registration for a primitive collection property |
| Option 7: Manual registration | Full control over individual property conversions; no additional NuGet package required |
Configuration support: All options except Option 7 accept an optional
Configurationparameter for controlling max length strategies and other settings. Their parameterless overloads useConfiguration.Default, which automatically calculates max length for string-based Smart Enums (usingDefaultSmartEnumMaxLengthStrategy). See Configuration and Max Length Strategies for details.
The recommended approach is to use the extension method UseThinktectureValueConverters for the DbContextOptionsBuilder. This applies globally without requiring changes to OnModelCreating.
Install the appropriate NuGet package for EF Core 8, EF Core 9 or EF Core 10.
Default configuration (recommended):
services
.AddDbContext<DemoDbContext>(builder => builder
.UseSqlServer(connectionString)
.UseThinktectureValueConverters());You can use the extension method AddThinktectureValueConverters on ModelBuilder to register value converters for all Smart Enums and Value Objects in the entire model.
Basic usage (automatic configuration):
public class ProductsDbContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.AddThinktectureValueConverters();
}
}You can configure value converters at the entity level using the AddThinktectureValueConverters extension method on EntityTypeBuilder. This approach allows you to apply value converters only to specific entities.
public class ProductsDbContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Product>(builder =>
{
builder.HasKey(p => p.Id);
// Apply value converters to all Smart Enums and Value Objects in this entity
builder.AddThinktectureValueConverters();
});
}
}This method is also available for owned entities through the OwnedNavigationBuilder:
modelBuilder.Entity<Order>(builder =>
{
builder.OwnsOne(o => o.ShippingDetails, detailsBuilder =>
{
// Apply value converters to all Smart Enums and Value Objects in this owned entity
detailsBuilder.AddThinktectureValueConverters();
});
});Method overload for ComplexPropertyBuilder:
In EF Core 8+, you can also use the AddThinktectureValueConverters extension method with ComplexPropertyBuilder to apply value converters to complex types:
modelBuilder.Entity<Product>(builder =>
{
builder.ComplexProperty(p => p.Details, detailsBuilder =>
{
// Apply value converters to all Smart Enums and Value Objects in this complex type
detailsBuilder.AddThinktectureValueConverters();
});
});You can configure a single property to use a value converter with the HasThinktectureValueConverter extension method on PropertyBuilder<T>.
public class ProductsDbContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Product>(builder =>
{
builder.Property(p => p.ProductType)
.HasThinktectureValueConverter();
});
}
}You can configure a property inside a complex type to use a value converter with the HasThinktectureValueConverter extension method on ComplexTypePropertyBuilder<T>.
public class ProductsDbContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Product>(builder =>
{
builder.ComplexProperty(p => p.Details, detailsBuilder =>
{
detailsBuilder.Property(d => d.ProductType)
.HasThinktectureValueConverter();
});
});
}
}You can configure a primitive collection property to use value conversion with the HasThinktectureValueConverter extension method on PrimitiveCollectionBuilder<T>.
public class ProductsDbContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Product>(builder =>
{
builder.PrimitiveCollection(p => p.ProductTypes)
.HasThinktectureValueConverter();
});
}
}The registration of a value converter can be done manually by using one of the method overloads of HasConversion in OnModelCreating. This approach does not require any additional NuGet package.
// Entity
public class Product
{
// other properties...
public ProductType ProductType { get; private set; }
}
public class ProductsDbContext : DbContext
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<Product>(builder =>
{
builder.Property(p => p.ProductType)
.HasConversion(p => p.Key,
key => ProductType.Get(key));
});
}
}All options except Option 7 accept an optional Configuration parameter. The parameterless overloads use Configuration.Default, which automatically calculates max length for string-based Smart Enums.
Configuration.Default automatically:
- Calculates max length for string-based Smart Enums from their items
- Rounds up to the next multiple of 10
- Does not overwrite existing max length configurations
Custom Configuration example:
new Configuration
{
SmartEnums = new SmartEnumConfiguration
{
// Choose a max length strategy
MaxLengthStrategy = DefaultSmartEnumMaxLengthStrategy.Instance
},
KeyedValueObjects = new KeyedValueObjectConfiguration
{
MaxLengthStrategy = new CustomKeyedValueObjectMaxLengthStrategy((type, keyType) =>
{
if (type == typeof(ProductName))
return 200;
return MaxLengthChange.None;
})
}
}Available max length strategies:
For Smart Enums:
-
DefaultSmartEnumMaxLengthStrategy.Instance- Automatically calculates max length from string-based enum items, rounds to next 10 -
FixedSmartEnumMaxLengthStrategy- Sets a fixed max length for all smart enums -
CustomSmartEnumMaxLengthStrategy- Custom logic via delegate -
NoOpSmartEnumMaxLengthStrategy.Instance- No automatic max length configuration
For Keyed Value Objects:
-
FixedKeyedValueObjectMaxLengthStrategy- Sets a fixed max length for all keyed value objects -
CustomKeyedValueObjectMaxLengthStrategy- Custom logic via delegate -
NoOpKeyedValueObjectMaxLengthStrategy.Instance- No automatic max length configuration (default)
- Home
- Smart Enums
- Value Objects
- Discriminated Unions
- Object Factories
- Analyzer Diagnostics
- Source Generator Configuration
- Convenience methods and classes
- Migrations
- Version 7
- Version 8