skip to Main Content

I’m new to .NET, and I’ve been struggling with using Enum as data annotation for validation in my requests. It took me a full day to solve most of the issues, but I’m not confident that my solution is good practice.

My goal is to implement validation on a FormBody property where the type of that property is an Enum.

Here’s an example of the Enum:

public enum Color
{
    Red,
    Green,
    Blue
}

Now, to simplify the question, I have two models that use this Color. Let’s call them Car and CarDtoRequest.

In the CarDtoRequest, which is used as the type for FromBody, it looks like this:

public class CarDtoRequest
{
    [Required]
    [EnumDataType(typeof(Color), ErrorMessage = "Color must be either 'Red', 'Green', or 'Blue'")]
    public string Color { get; set; }
}

Also, the actual Car type class looks like this:

public class Car
{
    [Column(TypeName = "nvarchar(50)")]
    public Color Color { get; set; }
}

My first question is, it took me 3-4 hours, and I still can’t assign a dynamic string into the ErrorMessage that joins all the values from the Color (any non-static string shows the error: CS0182: An attribute argument must be a constant expression, typeof expression, or array creation expression of an attribute parameter type).

Additionally, I migrated to the database in the DbContext class as follows:

public DbSet<Car> Cars { get; set; }

The validation works, and it will only work if the FromBody color is one of the enum values. Also, it saves it in the database as a string, but in my code, it still gets saved as a number.

I’m using AutoMapper to convert from the domain model to the DTO model and vice versa.

However, all of this doesn’t seem like good practice and professional. Any suggestions for improvement would be appreciated.

2

Answers


  1. Try a custom Get/Set

        public class Car
        {
            private Color c { get; set; }
            public string Color {
                get { return c.ToString(); } 
                set { c = (Color)Enum.Parse(typeof(Color), value); }
            }
        }
    
    Login or Signup to reply.
  2. You won’t be able to generate the error message dynamically from the caller/user of the attribute, but you can generate the message inside the attribute itself. Here is an example of how that would look like:

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public sealed class EnumDataTypeAttribute<T> : ValidationAttribute
        where T : struct, Enum
    {
        public EnumDataTypeAttribute()
            : base(BuildErrorMessage())
        {
        }
    
        public override bool IsValid(object? value) => value switch
        {
            null => false,
            string stringValue => Enum.TryParse<T>(stringValue, out _),
            _ => throw new InvalidOperationException("Validator can only be used with 'string's."),
        };
    
        private static string BuildErrorMessage()
        {
            var enumValues = Enum.GetValues<T>() switch
            {
                [var single] => $"'{single}'",
                [var first, var second] => $"either '{first}' or '{second}'",
                [.. var allButLast , var last] => $"one of {string.Join(", ", allButLast.Select(x => $"'{x}'"))} or '{last}'",
                _ => throw new InvalidOperationException("This validation probably doesn't make sense for an empty enum?"),
            };
    
            return $"{{0}} must be {enumValues}.";
        }
    }
    

    Notice how in the constructor, we call into a private static method to generate the error message, and then pass that error message to the base constructor.

    Keep in mind the logic above is just an example and should probably be optimized a bit before using in any critical code path.

    When using the attribute like this with a DayOfWeek enum, such as:

    public sealed class SampleModel
    {
        [EnumDataTypeAttribute<DayOfWeek>]
        public required string DayOfWeek { get; set; }
    }
    

    And passing an incorrect value to it:

    SampleModel sample = new() { DayOfWeek = "Invalid" };
    ICollection<ValidationResult> validationResults = [];
    if (!Validator.TryValidateObject(sample, new ValidationContext(sample), validationResults, true)) 
    {
        foreach (var validationResult in validationResults)
        {
            Console.WriteLine(validationResult);
        }
    }
    

    This will produce the following error message:

    DayOfWeek must be either ‘Sunday’, ‘Monday’, ‘Tuesday’, ‘Wednesday’, ‘Thursday’, ‘Friday’ or ‘Saturday’.

    Few comments on this implementation:

    1. I’m leveraging the native placeholder capabilities of DataAnnotations here by producing a string with a {0} in it. When the error message is generated, this placeholder is replaced with the actual property name that is annotated. This is preferred to hardcoding property names or passing generic names in the message.
    2. I’m leveraging generic attributes, which were introduced as part of C# 11. This leads to simpler code as you can pass the enum type directly instead of passing a Type object.
    3. I’m using array pattern matching to build the final concatenated message to diffentiate the last element from the rest. There might be cleaner ways to do this, again, just an example.

    Before you commit to this however, I’d suggest considering the FluentValidation framework, which has built-in enum validation that you can more easily tap into.


    As for your question regarding handling these enums as text instead of numbers in EntityFrameworkCore, you can achieve this fairly easily by indicating to EF that the enum has a conversion to string:

    public sealed class SampleDbContext : DbContext
    {
        public DbSet<SampleModel> SampleModels => this.Set<SampleModel>();
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<SampleModel>()
                .Property(x => x.DayOfWeek).HasConversion<string>();
        }
    }
    

    Alternatively, you can define this in a dedicated IEntityTypeConfiguration implementation of course:

    public sealed class SampleModelEntityConfiguration : IEntityTypeConfiguration<SampleModel>
    {
        void IEntityTypeConfiguration<SampleModel>.Configure(EntityTypeBuilder<SampleModel> builder)
        {
            builder.Property(m => m.DayOfWeek).HasConversion<string>();
        }
    }
    

    And add it to the context:

    public sealed class SampleDbContext : DbContext
    {
        public DbSet<SampleModel> SampleModels => this.Set<SampleModel>();
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.ApplyConfiguration(new SampleModelEntityConfiguration())
    
            // Or more dynamically
            // modelBuilder.ApplyConfigurationsFromAssembly(typeof(Program).Assembly);
        }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search