Reputation: 1299
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?
Upvotes: 7
Views: 8124
Reputation: 39
I was able to modify the code posted by @plinth
[AttributeUsage(AttributeTargets.Field,
AllowMultiple = true)]
public class SubcategoryOf : Attribute
{
public Enum? Category { get; }
public SubcategoryOf(object category)
{
if (category is Enum categoryEnum) Category = categoryEnum;
}
}
public static class CategoryExtension
{
public static bool IsSubcategoryOf(this Enum subcategory,
Enum category)
{
var t = subcategory.GetType();
/*This section is for returning true if enum is
of the same category (eg. Product.Fruit is Product.Fruit) */
if (Equals(subcategory,
category))
return true;
var memberInfo = t.GetMember(subcategory.ToString());
/*This section loops through all attributes for a match of
declared category and returns its result true/false */
foreach (var member in memberInfo)
if (member.IsDefined(typeof(SubcategoryOf)))
return member.GetCustomAttributes(typeof(SubcategoryOf))
.Cast<SubcategoryOf?>()
.Any(subCatOf => Equals(subCatOf!.Category,
category));
/*If a category is not assigned a warning is posted to the
debug console that an attempt to compare an enum without a
category was made and returns false, this does not stop the
program with a throw exception as this should not break the
intended use of comparing.*/
Debug.WriteLine($"the enum {subcategory} does not contain a category and was being compared.",
"Warning(CategoryExtension.IsSubcategoryOf)");
return false;
}
}
The following is example of using the classes above:
public enum Proficiency
{
ArmorProficiency,
WeaponProficiency,
ToolProficiency,
LanguageProficiency
}
public enum ArmorProficiency
{
[IsSubcategoryOf(Proficiency.ArmorProficiency)]
Light,
[IsSubcategoryOf(Proficiency.ArmorProficiency)]
Medium,
[IsSubcategoryOf(Proficiency.ArmorProficiency)]
Heavy,
[IsSubcategoryOf(Proficiency.ArmorProficiency)]
Shield,
}
public enum LightArmorProficiency
{
[IsSubcategoryOf(ArmorProficiency.Light)]
[IsSubcategoryOf(Proficiency.ArmorProficiency)]
PaddedArmor,
[IsSubcategoryOf(ArmorProficiency.Light)]
[IsSubcategoryOf(Proficiency.ArmorProficiency)]
LeatherArmor,
[IsSubcategoryOf(ArmorProficiency.Light)]
[IsSubcategoryOf(Proficiency.ArmorProficiency)]
StuddedLeatherArmor,
}
public enum HeavyArmorProficiency
{
[IsSubcategoryOf(ArmorProficiency.Heavy)]
[IsSubcategoryOf(Proficiency.ArmorProficiency)]
RingMail,
[IsSubcategoryOf(ArmorProficiency.Heavy)]
[IsSubcategoryOf(Proficiency.ArmorProficiency)]
ChainMail,
[IsSubcategoryOf(ArmorProficiency.Heavy)]
[IsSubcategoryOf(Proficiency.ArmorProficiency)]
SplintMail,
[IsSubcategoryOf(ArmorProficiency.Heavy)]
[IsSubcategoryOf(Proficiency.ArmorProficiency)]
PlateMail,
}
The following is an example of how you could use the Category classes with enum.
public class Proficiencies
{
private readonly Dictionary<Enum, ProficiencyType> _armorProficiencies;
private readonly Dictionary<Enum, ProficiencyType> _weaponProficiencies;
private readonly Dictionary<Enum, ProficiencyType> _toolProficiencies;
private readonly Dictionary<Enum, LanguageComprehension> _languageProficiencies;
public IReadOnlyDictionary<Enum, ProficiencyType> ArmorProficiencies =>
_armorProficiencies;
public IReadOnlyDictionary<Enum, ProficiencyType> WeaponProficiencies =>
_weaponProficiencies;
public IReadOnlyDictionary<Enum, ProficiencyType> ToolProficiencies =>
_toolProficiencies;
public IReadOnlyDictionary<Enum, LanguageComprehension> LanguageProficiencies =>
_languageProficiencies;
public Proficiencies(bool startsWithCommon,
LanguageComprehension comprehensionLevel = LanguageComprehension.ReadAndWrite)
{
_armorProficiencies = new Dictionary<Enum, ProficiencyType>();
_weaponProficiencies = new Dictionary<Enum, ProficiencyType>();
_toolProficiencies = new Dictionary<Enum, ProficiencyType>();
_languageProficiencies = new Dictionary<Enum, LanguageComprehension>();
if (startsWithCommon)
_languageProficiencies.Add(StandardLanguageProficiency.Common,
comprehensionLevel);
}
public void AddNonLanguageProficiency(Enum proficiency,
ProficiencyType proficiencyType)
{
if (proficiency.IsSubcategoryOf(Proficiency.ArmorProficiency))
{
if (_armorProficiencies.ContainsKey(proficiency))
_armorProficiencies[proficiency] = proficiencyType;
else
_armorProficiencies.Add(proficiency,
proficiencyType);
}
else if (proficiency.IsSubcategoryOf(Proficiency.WeaponProficiency))
{
if (_weaponProficiencies.ContainsKey(proficiency))
_weaponProficiencies[proficiency] = proficiencyType;
else
_weaponProficiencies.Add(proficiency,
proficiencyType);
}
else if (proficiency.IsSubcategoryOf(Proficiency.ToolProficiency))
{
if (_toolProficiencies.ContainsKey(proficiency))
_toolProficiencies[proficiency] = proficiencyType;
else
_toolProficiencies.Add(proficiency,
proficiencyType);
}
else
{
Debug.WriteLine($"The enum {proficiency} is not a valid proficiency and was being added.",
"Warning(Proficiencies.AddProficiency)");
}
}
public void RemoveProficiency(Enum proficiency)
{
if ( _armorProficiencies.ContainsKey(proficiency) )
_armorProficiencies.Remove(proficiency);
else if ( _weaponProficiencies.ContainsKey(proficiency) )
_weaponProficiencies.Remove(proficiency);
else if ( _toolProficiencies.ContainsKey(proficiency) )
_toolProficiencies.Remove(proficiency);
else if ( _languageProficiencies.ContainsKey(proficiency))
_languageProficiencies.Remove(proficiency);
}
public void AddLanguageProficiency(Enum language,
LanguageComprehension comprehension)
{
if (!language.IsSubcategoryOf(Proficiency.LanguageProficiency)) return;
if (_languageProficiencies.ContainsKey(language))
_languageProficiencies[language] = comprehension;
else
_languageProficiencies.Add(language,
comprehension);
}
}
Summery: In this case the main category is now defined by the enum supplied and not hard coded, the subcategoryOf class cares not what is given as long as it is object type enum.
The extension class now only checks to see if subcategory belongs to the category or other categories by the attributes and does not hard code the enum type. This in turn allows for multiple enums collections to be used without needing to force everything into a single enum as well as if a subcategory doesn't belong it will return false.
Upvotes: 0
Reputation: 49179
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
Upvotes: 13
Reputation: 2603
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.
Upvotes: 3
Reputation: 31184
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]; }
}
}
Upvotes: 2