Miroslav Holec
Premium

Jak na číselníky v ASP.NET MVC a EF

Miroslav Holec   20. června 2016  update 26. června 2016

Tento článek byl napsán v roce 2016. Vývojářské technologie se neustále inovují a článek již nemusí popisovat aktuální stav technologie, ideální řešení a můj současný pohled na dané téma.

V řadě MVC / EF aplikací jsem se setkal s různými praktikami, jak pracovat s číselníky. V tomto článku popíšu postup, který se osvědčil mně samotnému ve většině aplikací. Protože se jedná o řešení poskládané z řady různých myšlenek, každý si odsud může vybrat to, co mu bude vyhovovat.

Základní požadavky

Řešení, které zde popíšu je vhodné pro systémové číselníky, které se projevují určitým způsobem v databázi. Příkladem mohou být: Visibility, Role, State aj. Jedná se typicky o číselníky, které v databázi nemá smysl mít, protože jejich změna je vždy spojena s úpravami aplikačního kódu. Zároveň neustálé načítání číselníků z databáze by bylo neefektivní. Vytvářet joiny v téměř každém dotazu, abychom zjistili například název stavu entity by znamenalo zbytečnou zátěž.

Jak uchovávat číselníky tak, aby byly použitelné v aplikačním kódu, daly se efektivně použít při psaní LINQ dotazů a daly se enumerovat například kvůli dropdown menu? Řešením problému jsou obyčejné enumy a jedna šikovná třída pracující a atributy.

Enumy

Enum je primitivní struktura, která dobře poslouží jako číselník. Protože Entity Framework podporuje pouze enumy číselných typů, je vhodné použít výchozí nastavení (enum typu int). Proč v tomto případě nepracovat s jiným typem (byte, short) se dočtete v článku "Jaký zvolit celočíselný datový typ? Skoro vždy integer". Typický enum může vypadat takto

public enum Visibility : int // int uvádím jen pro úplnost
{
	Private = 0,
	Public = 10
}

I když jsou enumy zero-based, doporučuji explicitně hodnotu uvést. Protože některé enumy jsou hierarchické, doporučuji také volit čísla s určitou rezervou. Díky tomu si mohu za půl roku dovolit číselník upravit na:

public enum Visibility 
{
	Private = 0,
	Community = 5, // můžu vložit mezi 0 a 10 a pořád mám rezervu na další rozšíření
	Public = 10
}

aniž bych se dostal do situace, kdy čísla neodpovídají hierarchické realitě. S takovým běžným enumem umí EF velmi dobře pracovat:

public class Article
{
	public int ArticleId {get; set;}
	public Visibility Visibility {get; set;} // persistován v db jako INT
}

Popis

V aplikaci lze použít LINQ dotazy ve smyslu

Context.Articles.Where(x => x.Visibility == Visibility.Public);

Zápis je velmi přehledný a dobře čitelný. Otázka je, jak převést Public na nějaký text zobrazitelný na front-endu a jak enumerovat všemi prvky v enumu. Problém s popisy lze řešit elegantně pomocí DescriptionAttribute:

public enum Visibility 
{
	[Description("Soukromý")]
	Private = 0,

	[Description("Komunitní")]
	Community = 5,

	[Description("Veřejný")]
	Public = 10
}

Přečíst si obsah Description atributu už není komplikované. Stejným způsobem lze přidat další vlastní atributy.

Enumerace a práce s popisy

Abychom měli pohromadě všechny funkce pro práci s enumem, můžeme si vytvořit třídu podobné této:

public static class Enums
{
    /// <summary>
    /// Returns all enum items
    /// </summary>
    public static Dictionary<int, string> All<T>() where T : struct
    {
        Type t = typeof(T);
        if (!t.IsEnum)
        {
            throw new ArgumentException($"{t.Name} is not enum");
        }

        return Enum.GetValues(t)
            .Cast<object>()
            .ToList()
            .ToDictionary(x => (int) x, x => ((Enum) x).GetDescription());
    }

    /// <summary>
    /// Returns enum item name based on enumId
    /// </summary>
    public static string GetName<T>(int enumId) where T : struct
    {
        return All<T>().FirstOrDefault(x => x.Key == enumId).Value;
    }

    public static string GetName<T>(T @enum) where T : struct
    {
        var t = Convert.ToInt32(@enum);
        return All<T>().FirstOrDefault(x => x.Key == t).Value;
    }

    /// <summary>
    /// Returns enum item ID based on name in [Description]
    /// </summary>
    /// <returns></returns>
    public static int GetId<T>(string name) where T : struct
    {
        return All<T>().FirstOrDefault(x => x.Value.Equals(name, StringComparison.InvariantCultureIgnoreCase)).Key;
    }

    /// <summary>
    /// Extract enum item description from [Description] attribute
    /// </summary>
    public static string GetDescription(this Enum value)
    {
        Type type = value.GetType();
        string name = Enum.GetName(type, value);
        if (name != null)
        {
            FieldInfo field = type.GetField(name);
            if (field != null)
            {
                DescriptionAttribute attr = Attribute.GetCustomAttribute(field, typeof(DescriptionAttribute)) as DescriptionAttribute;
                if (attr != null)
                {
                    return attr.Description;
                }
            }
        }

        return string.Empty;
    }
}

Výše uvedená třída pokrývá většinu mých potřeb pro práci s enumy.

Enumerace enumem v MVC View

Obvyklý případ použití je zobrazení všech prvků z enumu v MVC View:

@Html.DropDownFor(x => x.Visibility, Enums.All<Visibility>().ToSelectListItems())

Výsledkem jsou páry: 0:Soukromý, 5:Komunitní, 10:Veřejný Extension metoda ToSelectListItems pouze převede Dictionary na List (ke stažení zde). Díky tomuto přístupu se mi podařilo zbavit celé řady zbytečných předávání číselníků skrze ViewModely a zanechat tak ViewModely mnohem čistější.

Pokud je potřeba získat text jen z jedné hodnoty, lze opět použít stejnou třídu:

string enumText = Enums.GetDescription(Visibility.Public); // "Veřejný"

Běžné konverze

Konverze enumů je velmi snadná. Typicky pro ViewModely POSTované na server vývojář často nepotřebuje enum ale číselnou hodnotu, kterou předá (v URL, v body atd).

Article entity = Context.Articles.FirstOrDefault();
var viewModel = new ArticleViewModel
{
	VisibilityId = (int)entity.Visibility;
}

a to samé lze udělat obráceně:

Article entity = Context.Articles.FirstOrDefault();
entity.Visibility = (Visibility)viewModel.VisibilityId;

Pokud je potřeba dostat textovou hodnotu enumu (Private, Community, Public), stačí převést enum na string:

string enumText = Visibility.Public.ToString();  // bude "Public"

Závěr

Samotné řešení mi vyhovuje v naprosté většině případů. Pokud potřebuji compile-time typ (například pro nějaký atribut v controlleru), potom si vytvářím vedle enumu pro tento účel ještě statickou třídu s konstantami typu int a hodnoty přiřazuji pomocí konverze enumu na int. Projekt s enumy obvykle zpřístupňuji celé aplikaci, abych je mohl používat jak ve Views tak v entitách pro EF.