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.