Persisting a Smart Enum with Entity Framework Core

February 10, 2022#Software Development
Article
Author image.

Kyle McMaster, Senior Consultant

A Smart Enum is an enhancement of the C# enum that provides a strongly typed and object oriented approach to enum types.

Smart Enums can be used when a enum needs to be extended to include multiple fields that are tightly coupled or for encapsulating behavior into an enum like class.

As a quick refresher on enums, let’s add a simple enum to the domain of the Clean Architecture template by adding a priority field to the Project class. You can learn how to get started with this template in this blog post. In our example, this PriorityStatus enum has two values that represent the Project’s priority as BACKLOG or CRITICAL.

enum PriorityStatus
{
  BACKLOG,
  CRITICAL
}

This works great for simple values but what if we want to display a friendly name like “Backlog” or “Critical” to a presentation layer? Do we want to have mapping code for each response that returns that display name? Do we need to persist application specific display names to the database too? What if there are behaviors that we want to encapsulate to represent the transition from BACKLOG to CRITICAL? This is where Smart Enum can help us accomplish these concerns in a elegant manner.

Enter Ardalis.SmartEnum

Let’s refactor our enum to utilize the Smart Enum base class. First we’ll need to add the Ardalis.SmartEnum package to the Core project.

dotnet add package Ardalis.SmartEnum --version 2.0.1

Then, we’ll make PriorityStatus a class that inherits from the SmartEnum base class. The base class constructor takes two parameters, one string for the Name and one integer for the Value. In this case, we’ll add one additional field to our Smart Enum to show how they can be customized to be more purposeful and expressive.

public class PriorityStatus : SmartEnum<PriorityStatus>
{
  public static readonly PriorityStatus BACKLOG = new PriorityStatus(nameof(BACKLOG), "Backlog", 0);
  public static readonly PriorityStatus CRITICAL = new PriorityStatus(nameof(CRITICAL), "Critical", 1);

  public string DisplayName { get; }

  protected PriorityStatus(string name, string displayName, int value) : base(name, value)
  {
    DisplayName = displayName;
  }
}

The Project class should now look like this. Note that the Project constructor now takes a parameter for PriorityStatus and this will break the calls to the constructor throughout the Clean Architecture template. This is easily fixed by passing an arbitrary value to the places the constructor is used for now.

public class Project : BaseEntity, IAggregateRoot
{
  public string Name { get; private set; }

  private List<ToDoItem> _items = new List<ToDoItem>();
  public IEnumerable<ToDoItem> Items => _items.AsReadOnly();
  public ProjectStatus Status => _items.All(i => i.IsDone) ? ProjectStatus.Complete : ProjectStatus.InProgress;

  public PriorityStatus Priority { get; private set; }

  public Project(string name, PriorityStatus priority)
  {
    Name = Guard.Against.NullOrEmpty(name, nameof(name));
    Priority = priority;
  }

  // other methods left out
}

Persisting values with Entity Framework Core

At this point, Entity Framework Core will complain about not knowing how to construct a PriorityStatus instance as it is not configured as an Entity and there is no parameterless constructor provided. We can see this in action if we run the tests.

System.InvalidOperationException : No suitable constructor was found for entity type 'PriorityStatus'. The following constructors had parameters that could not be bound to properties of the entity type: cannot bind 'name', 'displayName', 'value' in 'PriorityStatus(string name, string displayName, int value)'.

We can fix this by telling Entity Framework Core how to convert the object to and from the database using value conversions introduced into Entity Framework Core in version 2.1. We can define a value conversion in the Configure method in ProjectConfiguration.cs in the Infrastructure project. Note that in previous versions of Entity Framework Core, a parameterless constructor was required in order to populate the SmartEnum class but as of version 6.x this is no longer a requirement.

public class ProjectConfiguration : IEntityTypeConfiguration<Project>
{
  public void Configure(EntityTypeBuilder<Project> builder)
  {
    builder.Property(p => p.Name)
        .HasMaxLength(100)
        .IsRequired();

    builder.Property(p => p.Priority)
        .HasConversion(
            p => p.Value,
            p => PriorityStatus.FromValue(p));
  }
}

After adding the conversion expressions, we can run the the tests again to see that they are now passing. The HasConversion method tells Entity Framework how to read and write to the database for a given property on our model. The first parameter is for converting the value when persisting to the database. The second parameter is for converting the value when reading from the database. This second parameter makes use of the FromValue method provided by the SmartEnum base class which takes an integer and returns an instance of the SmartEnum. It’s important to note that these conversion methods are flexible, so if the database was storing the Name string rather than the Value integer of the SmartEnum, use of the FromName method would also be possible. You can find a simplified implementation of the Smart Enum used in this example here.

Additional Resources


Copyright © 2024 NimblePros - All Rights Reserved