Simulating Polymorphic/Generic Enumerations - C#
I know it's a bizarre title, and i know inheritance is not possible with enums, so let me explain.
If i have the following classes:
- Fruit (abstract)
- Apple (concr开发者_如何转开发ete - derived from Fruit)
- Orange (concrete - derived from Fruit)
And i have the following method, implemented with generics:
public ICollection<T> FindFruit<T>() where T : Fruit
{
return _fruitRepository
.Fruits
.OfType<T>()
.ToList();
}
And i use it like this:
var apples = _fruitServices.FindFruit<Apple>();
All fine.
Now - i currently have the following enum:
public enum FruitAssociations
{
Color,
Manufacturers
}
Basically a "Fruit" can have many associations, which designates what associations to include in the result from my repository.
So the FindFruit
method above is actually like this:
public ICollection<T> FindFruit<T>(FruitAssociations[] assocs) where T : Fruit
{
return _fruitRepository
.Fruits
.OfType<T>()
.IncludeAssocs(assocs) // ObjectQuery<Fruit> extension method.
.ToList();
}
Here is that extension method:
public static ObjectQuery<Fruit> IncludeAssocs(this ObjectQuery<Fruit> source, FruitAssociations[] assocs)
{
if (assocs.Contains(FruitAssociations.Color))
query = query.Include("Color");
// etc
}
Also works fine. The problem i am now facing however is i have associations on particular Fruit.
E.g
public enum OrangeAssociations
{
OrangeFamilies
}
And i'm not sure how to have a single FindFruit
method that is capable of returning associations based on the type of T.
This is the end result i would like to be able to do:
var oranges = _fruitServices.FindFruit<Orange>(new[] { OrangeAssociations.OrangeFamilies });
var apples = _fruitServices.FindFruit<Apple>(new[] { AppleAssociations.AppleFamilies });
But i can't figure out how to do that without having seperate FindFruit
methods for each fruit type, and hence defeating the point of the generic T type parameter.
For those who are curious as to what i'm doing here - i'm using eager loading with Entity Framework 4, and therefore using the .Include
method (which takes a string designating the navigational property). So my service accepts an array of enumerations for a given entity, then i use an extension method to translate that to a string used in the .Include
statement.
I'm thinking the solution is to instead of accepting an array of enums, accept a generic class:
public ICollection<T> FindFruit<T>(FruitAssociations<T> assocs) where T : Fruit
And use it like this:
var associations = new FruitAssociations<Orange>(OrangeAssociations.OrangeFamilies);
var apples = _fruitServices.FindFruit<Apple>(associations);
But i'm not sure how that would work, or how to implement it.
Hopefully my question makes sense and is not too long/complicated.
Eric Lippert will come to my house and beat me with a stick for this, but it works.
Usage:
// finds fruits of type Orange, includes Color and OrangeFamilies
var result = FindFruit(OrangeAssociation.Color,
OrangeAssociation.OrangeFamilies);
or
// finds fruits of type Fruit, includes Manufacturers
var result = FindFruit(FruitAssociation.Manufacturers);
Implementation:
static ICollection<TFruit> FindFruit<TAssoc, TFruit>(
params FruitAssociation<TAssoc, TFruit>[] assocs)
where TAssoc : FruitAssociation<TAssoc, TFruit>, new()
where TFruit : Fruit
{
var query = _fruitRepository.Fruits.OfType<TFruit>();
foreach (var assoc in assocs)
{
query = query.Include(assoc.Name);
}
return query.ToList();
}
with
abstract class FruitAssociation<TAssoc, TFruit>
where TAssoc : FruitAssociation<TAssoc, TFruit>, new()
where TFruit : Fruit
{
public static readonly TAssoc Color = Define("Color");
public static readonly TAssoc Manufacturers = Define("Manufacturers");
protected static TAssoc Define(string name)
{
return new TAssoc { Name = name };
}
public string Name
{
get;
private set;
}
}
sealed class FruitAssociation : FruitAssociation<FruitAssociation, Fruit>
{
}
sealed class OrangeAssociation : FruitAssociation<OrangeAssociation, Orange>
{
public static readonly OrangeAssociation OrangeFamilies =
Define("OrangeFamilies");
}
You're running into a situation where Enum's and Generics don't always play well. However, you could do something like:
public static ObjectQuery<Fruit> IncludeAssocs(
this ObjectQuery<Fruit> source, Enum[] assocs)
{
foreach(var assoc in assocs)
{
query = query.Include(assoc.ToAssociationQueryString());
}
}
In this case I suggest defining a new attribute, like AssociationQueryString
, and apply it to the enums like this:
public enum OrangeAssociations
{
[AssociationQueryString("Orange Families")]
OrangeFamilies
}
Here's the custom attribute:
public class AssociationQueryStringAttribute : System.Attribute
{
private readonly string value;
public string Value { get { return this.value; } }
public AssociationQueryStringAttribute(string value)
{
this.value = value;
}
}
Here's the .ToAssociationQueryString()
extension method that returns the value of the attribute:
public string ToAssociationQueryString(this Enum value)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
AssociationQueryStringAttribute[] attributes =
(AssociationQueryStringAttribute[])fi.GetCustomAttributes(
typeof(AssociationQueryStringAttribute), false);
if (attributes.Length > 0)
{
return attributes[0].Value;
}
else
{
throw new InvalidOperationException(
"Must use an enum decorated with the AssociationQueryString attribute.");
}
}
Enumerations aren't as magical as everyone assumes. They are just a type with a bunch of static readonly fields and a private constructor. If you want inheritance use a normal class and either public static fields or properties that return instances of the class with the values you want.
Example from another question
精彩评论