开发者

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:

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

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜