Slovenská verzeSlovensky

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

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.

Miroslav Holec

Miroslav Holec

20. června. 2016
upraven 26. června. 2016

Tento článek je již velmi zastaralý. Zastaralé články nemusí popisovat aktuální stav technologií, nejideálnější řešení a můj aktuální pohled na danou technologii.

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.


👨‍🎓 Webináře pro vývojáře

Vzdělávat se můžete i z pohodlí domova. Klasická školení jsem doplnil o související témata, která si můžete poslechnout v podobě živých webinářů. Přidejte se téměř 200 vývojářům, kteří se již připojili k mým webinářům!

Termín Místo
🍀 Konzumace REST API v .NET Core červen 2020 on-line více
🍀 Nástroje a šablony projektů v .NET Core červen 2020 on-line více
🍀 Entity Framework Core červen 2020 on-line více
Loading
Otevřít chat