NHibernate: Enum to list and store values in database

asp.net-mvc-3, nhibernate

Solution

Since using lists is used for separate tables by default in Nhibernate, you'll have to use a mapping functions to map a single value from the database to a list of values in your model.

I've demonstrated how to do it here with a working sample, but I'll sum it up once more on your question.

There are 2 major mapping function types based on your problem. They both include that you:

- move your `MyEnumSelector` attribute from the `Record` class to your model class,

- change the type of your `Record`'s `MyEnumCheckBox` property and

- change your model's `MyEnumCheckBox` property so that it maps to and from your `Record`'s `MyEnumCheckBox` property

So those mapping functions are:

- `int` to `List<MyEnumSelector>` - this is the same technique I've shown in the linking post. It includes marking your `MyEnumSelector` enum with the `[Flags]` attribute and setting it's items to be of values of subsequent powers of 2. Your `Record`'s `MyEnumCheckBox` property should be of type `int`. The mapping part then involves mapping the bits of the `Record`'s `MyEnumCheckBox` property to and from the list of corresponding `MyEnumSelector` values

- `string` to `List<MyEnumSelector>` - this involves setting your `Record`'s `MyEnumCheckBox` property to be of type `string`. The mapping part then involves mapping the delimited string of the `Record`'s `MyEnumCheckBox` property to and from the list of corresponding `MyEnumSelector` values. Delimiter could be comma or semicolon or some other character that you won't use as a name of your `MyEnumSelector` item values.

Major differences of these 2 approaches are:

- Enum size - when using `int` as your database type your're limited to 32 bits of data. This means that you can't have more than 32 items in your enum. `String`s don't have this kind of limitation

- Ease of searching the database for specific values - when using `int`, you'll have to deal with bit-wise database operators which are quite messy and not that easy to work with, while you could use simple `LIKE` operator when using `string` mapping

- Size used in database - `int` uses always 4 bytes (32-bits) while `string` mapping uses that amount (32-bits) for only one character inside the string (if it's of type `char`, `varchar` or `text`, and double the size - 64-bits when using `nchar`, `nvarchar` or `ntext`) so it will use much more space for each row in the database

- Speed of mapping - bit-wise mapping used in `int` mapping is quite fast while `string` mapping uses string manipulation functions which are much slower. This shouldn't be the problem though if you're dealing with a small amount of data. But if you're going to deal with a massive amount of data this could be a huge issue.

Problem

I am using `enum`'s and a custom `Selector` class to help choose between radiobuttons, dropdowns, checkboxes, etc. I am using NHibernate. With a single selection (radiobuttons, dropdowns), the value from attribute `[Display(Name = "[Some Text]")]` will be populated in the database table (NOTE: I am using an extension to use `Display(Name)`). However, with multiple selections (checkboxes, multilist), I cannot figure out how to get the values of the `enum` selections into the database. Here are parts of my model (each in separate files) (EDIT: I gave them generic names so as not to further confuse the issue): ``` public enum MyEnum { [Display(Name = "Text for enum1")] enum1, //Left out 2 - 10 for brevity [Display(Name = "Text for enum10")] enum10 } ... public class MyEnumSelectorAttribute : SelectorAttribute { public override IEnumerable<SelectListItem> GetItems() { return Selector.GetItemsFromEnum<MyEnum>(); } } ... [Display(Name = "This is a checkboxlist (select one or more check boxes)?")] [MyEnumSelector(BulkSelectionThreshold = 10)] public virtual List<string> MyEnumCheckBox { get; set; } ... public List<string> MyEnumCheckBox { get { return Record.MyEnumCheckBox; } set { Record.MyEnumCheckBox = value; } } ``` And here is the `Selector.cs` class (in case it's relevant to the problem) that help's to choose radiobuttons, checkboxes, dropdowns, etc.: ``` public class Selector { public IEnumerable<SelectListItem> Items { get; set; } public string OptionLabel { get; set; } public bool AllowMultipleSelection { get; set; } public int BulkSelectionThreshold { get; set; } public static string GetEnumDescription(string value, Type enumType) { var fi = enumType.GetField(value.ToString()); var display = fi .GetCustomAttributes(typeof(DisplayAttribute), false) .OfType<DisplayAttribute>() .FirstOrDefault(); if (display != null) { return display.Name; } return value; } public static IEnumerable<SelectListItem> GetItemsFromEnum<T> (T selectedValue = default(T)) where T : struct { return from name in Enum.GetNames(typeof(T)) let enumValue = Convert.ToString((T)Enum.Parse (typeof(T), name, true)) select new SelectListItem { Text = GetEnumDescription(name, typeof(T)), Value = enumValue, Selected = enumValue.Equals(selectedValue) }; } } public static class SelectorHelper { public static IEnumerable<SelectListItem> ToSelectList (this IEnumerable data) { return new SelectList(data); } public static IEnumerable<SelectListItem> ToSelectList (this IEnumerable data, string dataValueField, string dataTextField) { return new SelectList(data, dataValueField, dataTextField); } public static IEnumerable<SelectListItem> ToSelectList<T> (this IEnumerable<T> data, Expression<Func<T, object>> dataValueFieldSelector, Expression<Func<T, string>> dataTextFieldSelector) { var dataValueField = dataValueFieldSelector.ToPropertyInfo().Name; var dataTextField = dataTextFieldSelector.ToPropertyInfo().Name; return ToSelectList(data, dataValueField, dataTextField); } } ``` The `Selector` class is paired with a template `Selector.cshtml` that has some logic to figure out which to pick (radiobutton, checkboxes, etc.). I am getting various errors trying either `List<string>`, `List<MyEnum>`, `IList<string>`, `IList<MyEnum>`, `IEnumerable<MyEnum>` and `IEnumerable<MyEnum>`. This error only comes with checkboxes or multilists since they use `List<string>`. Dropdowns, for example, work fine with no errors. Here is a sample dropdown model (can reuse `enum` above) that works and will allow mapping to the DB through NHibernate: ``` [Required(ErrorMessage = "Please select one option")] [Display(Name = "This is a dropdown list (select one option)?")] [MyEnumSelector(BulkSelectionThreshold = 0)] //0 selects dropdown public virtual MyEnum? MyEnumDropDown { get; set; } public MyEnum? MyEnumDropDown { get { return Record.MyEnumDropDown; } set { Record.MyEnumDropDown = value; } } ``` Here are some of the errors I am getting based on what I've tried: `List<string>` error: NHibernate.Transaction.ITransactionFactory - DTC transaction prepre phase failed NHibernate.PropertyAccessException: Invalid Cast (check your mapping for property type mismatches); setter of MyNameSpace.Models.MyRecord ---> System.InvalidCastException: Unable to cast object of type 'NHibernate.Collection.Generic.PersistentGenericBag`1[System.String]' to type 'System.Collections.Generic.List`1[System.String]'. `List<MyEnum>` error: NHibernate.Transaction.ITransactionFactory - DTC transaction prepre phase failed System.InvalidCastException: Unable to cast object of type 'System.Collections.Generic.List`1[MyNameSpace.Models.MyEnum]' to type 'System.Collections.Generic.ICollection`1[System.String]'. `IList<string>` error: NHibernate.AdoNet.AbstractBatcher - Could not execute command: INSERT INTO MyEnumCheckBox (MyRecord_id, Value) VALUES (@p0, @p1) System.Data.SqlServerCe.SqlCeException (0x80004005): The specified table does not exist. [ MyEnumCheckBox ] The other variations I tried were similar errors, except that if I used `<MyEnum>` it would show an error like this: System.Collections.Generic.List`1[MyNameSpace.Models.MyEnum]' to type 'System.Collections.Generic.ICollection`1[System.String]'. Any thoughts on how to use `enum`'s in this scenario when trying to insert multiple selected `enum`s using NHibernate?

Original source