Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mapping enum to "sub-enum"

Tags:

c#

.net

enums

I have a database containing Products. These products are categorized, having a Category and a Subcategory.

For example:

Product p1 = new Product()
{ 
    Category = Category.Fruit, 
    Subcategory = Subcategory.Apple 
 };

My issue is the fact that I want to restrict the subcategory, depending on the category.

The below example should not be possible:

Product p2 = new Product()
{ 
    Category = Category.Fruit, 
    Subcategory = Subcategory.Cheese 
};

Furthermore I'd like to be able to return an array of strings (matching each Category enum) which each has an array of the corresponding subcategories.

I've been thinking for a while but have come up with nothing, nor have I found any solutions online.

What would be advised?

like image 298
JensOlsen112 Avatar asked Oct 20 '14 18:10

JensOlsen112


3 Answers

I like the map rule. You can also put a custom attribute on your enum values.

For example:

public enum Subcategory {
    [SubcategoryOf(Category.Fruit)]
    Apple,
    [SubcategoryOf(Category.Dairy)]
    Emmenthaler
}

This requires that you write a SubcategoryOfAttribute class (see here for the MS guide). Then you can write a verifier that can look at any subcategory and get the legal parent category from it.

The advantage to this over the map is that the relationship is spelled out in the declaration nicely.

The disadvantage is that each subcategory can have a maximum of one parent category.

I found this in intriguing, so I stubbed it out. First the attribute:

[AttributeUsage(AttributeTargets.Field)]
public class SubcategoryOf : Attribute {
    public SubcategoryOf(Category cat) {
        Category = cat;
    }
    public Category Category { get; private set; }
}

Then we make some mock enums

public enum Category {
    Fruit,
    Dairy,
    Vegetable,
    Electronics
}

public enum Subcategory {
    [SubcategoryOf(Category.Fruit)]
    Apple,
    [SubcategoryOf(Category.Dairy)]
    Buttermilk,
    [SubcategoryOf(Category.Dairy)]
    Emmenthaler,
    [SubcategoryOf(Category.Fruit)]
    Orange,
    [SubcategoryOf(Category.Electronics)]
    Mp3Player
}

Now we need a predicate to determine if a subcategory matches a category (note: you can have multiple parent categories if you want - you need to modify the attribute and this predicate to get all attributes and check each one.

public static class Extensions {
    public static bool IsSubcategoryOf(this Subcategory sub, Category cat) {
        Type t = typeof(Subcategory);
        MemberInfo mi = t.GetMember(sub.ToString()).FirstOrDefault(m => m.GetCustomAttribute(typeof(SubcategoryOf)) != null);
        if (mi == null) throw new ArgumentException("Subcategory " + sub + " has no category.");
        SubcategoryOf subAttr = (SubcategoryOf)mi.GetCustomAttribute(typeof(SubcategoryOf));
        return subAttr.Category == cat;
    }
}

Then you put in your product type to test it out:

public class Product {
    public Product(Category cat, Subcategory sub) {
        if (!sub.IsSubcategoryOf(cat)) throw new ArgumentException(
            String.Format("{0} is not a sub category of {1}.", sub, cat), "sub");
        Category = cat;
        Subcategory = sub;
    }

    public Category Category { get; private set; }
    public Subcategory Subcategory { get; private set; }
}

Test code:

Product p = new Product(Category.Electronics, Subcategory.Mp3Player); // succeeds
Product q = new Product(Category.Dairy, Subcategory.Apple); // throws an exception
like image 65
plinth Avatar answered Sep 17 '22 21:09

plinth


What you are trying to do is to represent first order logic (http://en.wikipedia.org/wiki/First-order_logic) using enums. And syncing with a database. It's not an easy task when hard-coding it in code. Many good solutions have already been suggested.

For my part, I would just use strings (or unique ids) for Category and SubCategory and enforce the integrity using the rules defined in the database. But if you end up using it in code, it won't be compile-time.

The problem with Enum is that it must match with your external source and your code. Also, it becomes difficult to attach more information to it, like the price or country or even if you have different kind of apples.

like image 24
Sauleil Avatar answered Sep 16 '22 21:09

Sauleil


My suggestion would be to have a Dictionary<SubCategory, Category>, that maps your SubCategory to your Category.

After that, you can just get rid of the Category on your product all together, or you can just use a helper method

public class Product
{
    static Dictionary<SubCategory, Category> _categoriesMap;

    public static Product()
    {
        _categoriesMap = new Dictionary<SubCategory, Category>();
        _categoriesMap.Add(SubCategory.Apple, Category.Fruit);
    }

    public SubCategory SubCategory { get; set; }

    public Category Category
    {
        get { return _categoriesMap[this.SubCategory]; }
    }
}
like image 36
Sam I am says Reinstate Monica Avatar answered Sep 20 '22 21:09

Sam I am says Reinstate Monica