HomeNikola Knezevic

In this article

Banner

Smart Enums in ASP.NET Core

10 Sept 2026
6 min

Sponsor Newsletter

In many applications we model fixed sets of values like order status, payment type or user role.

C# enums are the default choice, but they come with trade-offs. You can't attach behavior, you can't restrict values at compile time beyond the declared members and comparing or listing all values often means reaching for reflection or magic numbers.

That becomes painful in ASP.NET Core when you need clean API contracts, safe database mapping and predictable validation.

One of the simplest ways to get enum-like ergonomics with richer behavior is by using smart enums.

What Are Smart Enums

A smart enum is a class that represents a fixed set of values as static readonly instances instead of a language enum.

Each instance carries an Id and a Name, similar to how enums map to integers and names under the hood.

The difference is that you get full object semantics. You can add methods, enforce invariants and resolve values safely with FromId or FromName instead of casting raw integers from the database or request body.

If you've worked with Java, this might feel familiar. Java enums are full classes out of the box. Each constant is a singleton instance, you can add fields, constructors and methods and the language gives you values() and valueOf() for free.

java
public enum OrderStatus {
    PENDING(1),
    PROCESSING(2),
    COMPLETED(3),
    CANCELLED(4);

    private final int id;

    OrderStatus(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }
}

C# enums are value types with none of that built in. This isn't just a language quirk, the CLR specification explicitly restricts enum types. They may only have a single underlying integer field and they cannot define their own methods, properties or interface implementations.

That design keeps enums lightweight for performance, interop with unmanaged code and flag combinations with bitwise operators. The trade-off is that you can't attach domain behavior directly to a C# enum declaration.

Java isn't alone here either. Kotlin, Rust and Swift all offer class-like or algebraic enums in the language itself. C# simply can't, so we reach for a smart enum pattern to recreate what those developers get for free. For the full list of CLR enum restrictions, see the Common Type System documentation.

Here's a quick look at what regular enums feel like compared to smart enums in C#:

  • Regular enum - Lightweight, but no behavior and easy to cast invalid values like (OrderStatus)99.
  • Smart enum - Slightly more code, but type-safe instances, explicit lookup and room for domain logic.

Building the Base Class

We'll start with a reusable base class. Every concrete smart enum inherits from it and registers its instances automatically when static fields are initialized.

csharp
public abstract class SmartEnum<TEnum> : IEquatable<SmartEnum<TEnum>>
    where TEnum : SmartEnum<TEnum>
{
    private static readonly List<TEnum> _all = new();

    public int Id { get; }
    public string Name { get; }

    protected SmartEnum(int id, string name)
    {
        Id = id;
        Name = name;

        _all.Add((TEnum)this);
    }

    public static IReadOnlyList<TEnum> List() => _all;

    public static TEnum FromId(int id) =>
        _all.FirstOrDefault(x => x.Id == id)
        ?? throw new ArgumentException($"No {typeof(TEnum).Name} with Id {id} found.");

    public static TEnum FromName(string name) =>
        _all.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase))
        ?? throw new ArgumentException($"No {typeof(TEnum).Name} with Name '{name}' found.");

    public bool Equals(SmartEnum<TEnum>? other) =>
        other is not null && Id == other.Id;

    public override bool Equals(object? obj) =>
        obj is SmartEnum<TEnum> other && Equals(other);

    public override int GetHashCode() =>
        Id.GetHashCode();

    public override string ToString() => Name;
}

The important parts here are the static registry in _all, lookup helpers and equality based on Id rather than reference identity.

Because each static field runs the protected constructor, every declared value is collected automatically. No manual registration list to maintain.

Defining an Enum Type

With the base in place, defining a concrete smart enum is straightforward. Here's an OrderStatus type we can use across an order API:

csharp
public sealed class OrderStatus : SmartEnum<OrderStatus>
{
    public static readonly OrderStatus Pending = new(1, "Pending");
    public static readonly OrderStatus Processing = new(2, "Processing");
    public static readonly OrderStatus Completed = new(3, "Completed");
    public static readonly OrderStatus Cancelled = new(4, "Cancelled");

    private OrderStatus(int id, string name) : base(id, name) { }
}

Usage feels familiar. You reference well-known instances directly and resolve persisted or incoming values through the lookup methods:

csharp
var a = OrderStatus.Pending;
var b = OrderStatus.FromId(1);

Console.WriteLine(a == b);
Console.WriteLine(a.Equals(b));

foreach (var value in OrderStatus.List())
{
    Console.WriteLine(value);
}

Both references point to the same logical value and List() gives you every defined status without reflection.

Using Smart Enums

The real payoff shows up when you wire smart enums into persistence and HTTP APIs. We'll keep the examples focused on the two places that usually cause friction, EF Core and JSON.

EF Core Mapping

Store the numeric Id in PostgreSQL and map it back to the smart enum instance on read. This is the same idea as enum-to-int storage, but with explicit conversion at the boundary:

csharp
public sealed class Order
{
    public Guid Id { get; set; }
    public OrderStatus Status { get; set; }
}

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Order>()
        .Property(e => e.Status)
        .HasConversion(
            v => v.Id,
            v => OrderStatus.FromId(v));
}

If you prefer storing names instead, swap the conversion to use Name and FromName. For more background on value conversions in EF Core, check out my previous blog post on Value Conversions in EF Core.

JSON Serialization

By default, System.Text.Json won't know how to serialize a smart enum. A small converter keeps API responses readable and accepts incoming string values on requests:

csharp
public sealed class SmartEnumJsonConverter<TEnum> : JsonConverter<TEnum>
    where TEnum : SmartEnum<TEnum>
{
    public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
        SmartEnum<TEnum>.FromName(reader.GetString()!);

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

Register it once during startup and every endpoint that uses OrderStatus gets consistent JSON:

csharp
builder.Services.ConfigureHttpJsonOptions(options =>
{
    options.SerializerOptions.Converters.Add(new SmartEnumJsonConverter<OrderStatus>());
});

A POST body with "Status": "Processing" binds cleanly and responses return the name instead of an opaque object shape.

API Validation

Invalid enum casts fail silently or produce confusing results. Smart enums fail fast. FromName and FromId throw when the value doesn't exist, which you can turn into a 400 response in middleware or a custom model binder.

For dropdowns or admin screens, expose the allowed values directly from the type:

csharp
app.MapGet("/order-statuses", () =>
    OrderStatus.List().Select(s => new { s.Id, s.Name }));

Clients get a single source of truth straight from the domain type. No duplicated string constants in the frontend.

When to use them

Smart enums shine when a fixed set of values needs behavior, safe parsing or a stable contract across layers.

Stick with regular enums when you only need a simple flag or a lightweight internal constant and none of the extra structure matters.

If you'd rather pull in a battle-tested package, the community Ardalis SmartEnum library extends the idea with flags, Dapper and EF Core helpers and more. The hand-rolled version in this post keeps dependencies minimal and shows exactly what's happening under the hood.

Conclusion

Smart enums give you the clarity of named constants without giving up object-oriented design.

In ASP.NET Core they make persistence mapping, JSON contracts and validation more predictable. You store an Id or Name, resolve to a known instance and work with a real domain object from there.

Start with one status or type enum in your project, wire up EF Core and JSON conversion once and you'll quickly see whether the pattern fits your codebase.

If you want to check out examples I created, you can find the source code here:

Source Code

I hope you enjoyed it, subscribe and get a notification when a new blog is up!

Related posts
Value Objects in .NET
Value Objects in .NET

Primitives hide domain meaning. Value objects model concepts like Money and Address with value equality, immutability and rules built in.

Value Conversions in EF Core
Value Conversions in EF Core

Value Converters in EF Core give you the flexibility to bridge the gap between your domain model and the database schema without compromising either.

Anemic vs Rich Models in ASP.NET Core
Anemic vs Rich Models in ASP.NET Core

Domain models can be passive data containers or active business objects, a practical approach is starting with anemic models and evolving them into rich ones as the domain grows in complexity.

Subscribe

Stay tuned for valuable insights every Thursday morning.