Jak na číselníky v ASP.NET MVC a EF
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
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.