Search code examples
c#dapper

Hydrating an enumeration class with Dapper


I'm using Dapper to hydrate a C# class. I recently moved from collections of string constants to "enumeration classes" as defined here: https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/enumeration-classes-over-enum-types

My enumeration looks like this:

public abstract class Enumeration : IComparable
    {
        public string Name { get; }

        protected Enumeration(string name)
        {
            Name = name;
        }

        public static IEnumerable<T> GetAll<T>() where T : Enumeration
        {
            var fields = typeof(T).GetFields(BindingFlags.Public |
                                             BindingFlags.Static |
                                             BindingFlags.DeclaredOnly);

            return fields.Select(f => f.GetValue(null)).Cast<T>();
        }

        public static IEnumerable<T> ToSortedEnumerable<T>() where T : Enumeration
        {
            List<T> values = GetAll<T>().ToList();
            values.Sort();
            return values;
        }

        public int CompareTo(object other) =>
            string.Compare(Name, ((Enumeration) other).Name, StringComparison.Ordinal);

        public static implicit operator string(Enumeration enumeration)
        {
            return enumeration?.ToString();
        }

        public static bool operator ==(Enumeration e1, Enumeration e2)
        {
            return Equals(e1, e2);
        }

        public static bool operator !=(Enumeration e1, Enumeration e2)
        {
            return !Equals(e1, e2);
        }

        public static bool HasValue<T>(string valueToCheck) where T : Enumeration
        {
            return Enumeration.GetAll<T>().Any(x => x.Name.Equals(valueToCheck, StringComparison.OrdinalIgnoreCase));
        }

        public static bool TryGetEnumeration<T>(string valueToCheck, out T result) where T : Enumeration
        {
            result = Enumeration.GetAll<T>()
                                .FirstOrDefault(
                                    x => x.Name.Equals(valueToCheck, StringComparison.OrdinalIgnoreCase));

            return result != null;
        }

        public static T GetEnumeration<T>(string valueToCheck) where T : Enumeration
        {
            var result = Enumeration.GetAll<T>()
                                .FirstOrDefault(
                                    x => x.Name.Equals(valueToCheck, StringComparison.OrdinalIgnoreCase));

            if (result == null)
            {
                throw new ArgumentException($"Invalid {typeof(T).Name}: {valueToCheck}");
            }

            return result;
        }

        public override bool Equals(object obj)
        {
            var otherValue = obj as Enumeration;

            if (otherValue == null)
                return false;

            bool typeMatches = this.GetType() == obj.GetType();
            bool valueMatches = this.Name.Equals(otherValue.Name);

            return typeMatches && valueMatches;
        }

        public override int GetHashCode()
        {
            return 539060726 + EqualityComparer<string>.Default.GetHashCode(this.Name);
        }

        public override string ToString() => this.Name;
    }

and my Race class looks like this:

public class Race : Enumeration
    {
        public static Race White = new Race("White");
        public static Race Hawaiian = new Race("Native Hawaiian");
        public static Race Filipino = new Race("Filipino");
        public static Race Black = new Race("Black / African American");
        public static Race Chinese = new Race("Chinese");
        public static Race Japanese = new Race("Japanese");
        public static Race Korean = new Race("Korean");
        public static Race Vietnamese = new Race("Vietnamese");
        public static Race AsianIndian = new Race("Asian Indian");
        public static Race OtherAsian = new Race("Other Asian");
        public static Race Samoan = new Race("Samoan");
        public static Race AmericanIndian = new Race("American Indian");
        public static Race AlaskaNative = new Race("Alaska Native");
        public static Race Guamanian = new Race("Guamanian");
        public static Race Chamorro = new Race("Chamorro");
        public static Race OtherPacificIslander = new Race("Other Pacific Islander");
        public static Race Other = new Race("Other");

        public Race(string name) : base(name)
        { }
    }

My simplified Person object looks like this:

public class Person
{
    public Person(Guid personId, Race race){
        PersonId = personId;
        Race = race;
    }
    public Race Race {get;}
    public Guid PersonId {get;}
}

Here's a simplified Dapper command (talking to postgresql) that works (PersonId is hydrated correctly), but Race is always NULL.

return connection.Query<Person>(sql: @"
    SELECT person_id as PersonId
    ,race
    FROM public.people");

I have tried adjusting my SQL to this:

return connection.Query<Person>(sql: @"
    SELECT person_id as PersonId
    ,race as Name
    FROM public.people");

but that also results in a null value for Race.

Is what I'm attempting even possible? Do I have to do a splitOn for this? I've avoided that because my real class has dozens of such properties and they'd all have to be Name and . . . well, I just didn't want to go there if I was missing something silly here. I honestly kind of thought that the

public static implicit operator string(Enumeration enumeration)

would take care of this for me.

Thoughts anyone? Help is always appreciated.


Solution

  • Ok, figured it out. Two things:

    First, splitOn is the way to do this. A different, but related final version looks like this:

    return connection.Query<Program,
        AssistanceProgramCategory,
        AssistanceProgramType,
        AssistanceProgramLegalType,
        ProgramAuthority,
        Program>(sql: Constants.SqlStatements.SELECT_PROGRAMS_SQL,
        (program, category, programType, legalType, authority) =>
        {
            program.AssistanceCategory = category;
            program.ProgramType = programType;
            program.ProgramLegalType = legalType;
            program.Authority = authority;
            return program;
        }, splitOn: "Name,Jurisdiction");
    

    where AssistanceProgramCategory, AssistanceProgramType, and AssistanceProgramLegalType are all children of Enumeration.

    Second, the SQL does have to deliver the columns up with Name, as in:

    SELECT global_id as GlobalId
    ,tier
    ,program_description as Name
    ,program_type as Name
    ,program_legal_type as Name
    ,jurisdiction as Jurisdiction
    ,customer_id as CustomerId
    ,program_name as ProgramNameForJurisdiction
    ,program_description as ProgramName
    FROM public.assistance_programs
    

    Third, I only had to put "Name" in the splitOn once - every instance of Name caused a new object to be created.

    Finally, I had to swap Jurisdiction and CustomerId because CustomerId can be null, and when NULL, it doesn't fire the final hydration into ProgramAuthority. Jurisdiction is always present, so problem solved by swapping the columns in the SQL.

    Hope this helps someone.

    All the best,

    V