3

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 (concrete - 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.

RPM1984
  • 72,246
  • 58
  • 225
  • 350

3 Answers3

4

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");
}
dtb
  • 213,145
  • 36
  • 401
  • 431
  • Very, very interesting (and creative) +1. I'll try this out and get back to you. Why is this bad though? (referring to your Lippert point). – RPM1984 Dec 09 '10 at 01:36
  • 1
    @RPM1984: It's somewhat fragile and using generics in a way they're not intended for. Essentially, the relations between the classes here cannot be enforced, and the solution only works as long as all users behave nicely. So, don't expose this in an API. I see no harm in using it internally though. – dtb Dec 09 '10 at 01:43
  • One problem i'm having, because of the `new()` constraint on the `FruitAssociation` abstract class, i am unable to get a "mixed bag" of fruit, because to do that (i was doing that before), the type needs to be `Fruit`, which is abstract, and hence the `new()` constraint is enforcing that. Can you give me an example usage of how i would do that? (e.g using the `Manufacturers` association, which exists on `Fruit`). – RPM1984 Dec 09 '10 at 02:26
  • 1
    @RPM1984: I've added an example to my answer. You cannot find fruits and include both Apple associations and Orange associations at the same time using this solution though. – dtb Dec 09 '10 at 02:33
  • It still won't work though, because the type arguments can't be inferred. You need the type params after the `FindFruit`. And you can't use `FindFruit` because it's abstract, hence the `new()` issue - know what i mean? – RPM1984 Dec 09 '10 at 02:51
  • And yes - i dont need Apple and Orange associations, but i need Fruit and Apple or Fruit and Orange, or just Fruit - which is the issue im having atm. – RPM1984 Dec 09 '10 at 02:52
  • in other words, this works: `FindFruit(OrangeAssociation.OrangeFamilies)`. This does not: `FindFruit(FruitAssociation.Color)`. (incorrect number of type params) – RPM1984 Dec 09 '10 at 03:00
  • oh wait, my bad - i didn't notice `sealed class FruitAssociation`. – RPM1984 Dec 09 '10 at 03:05
2

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.");
    }
}
Scott Whitlock
  • 13,739
  • 7
  • 65
  • 114
  • Interesting...although i really try and avoid attributes (and reflection - as it looks like your using). I'll give it a go though. – RPM1984 Dec 09 '10 at 01:36
  • @RPM1984: I assumed you had some reason that you had to use enums. If you really don't have that requirement, and you can use classes, then by all means, classes play better with generics. – Scott Whitlock Dec 09 '10 at 01:42
  • Also remember, instead of using reflection, you can just inspect the Enum type, cast it, and then switch into an appropriate method. – Scott Whitlock Dec 09 '10 at 01:43
  • Yep - i'm trying to get @dtb's solution working, but it's not working so far. If i can't get it to work i'll be inspecting the enum then calling method as you say. Just wanted to try and avoid that stitching (if possible). – RPM1984 Dec 09 '10 at 02:54
0

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

Community
  • 1
  • 1
Matthew Whited
  • 22,160
  • 4
  • 52
  • 69
  • 1
    Yes, but i'm trying to achieve type safety here. If i just had a class with static fields, how could i enforce type safety? (e.g ensure my method above only accepts a certain list of values - which is what im doing at the moment). – RPM1984 Dec 09 '10 at 11:57
  • You can return a complex class or struct as the static fields. – Matthew Whited Dec 18 '10 at 02:10
  • @MatthewWhited enums do expose a public constructor, just like any other value type. – nawfal Jun 11 '13 at 09:38
  • If we are being overly technical they don't define a constructor at all. Yes you could call "new" on an enum but it would be uninitialized and would be better to explicitly choose the value you want from your enumeration. – Matthew Whited Jun 11 '13 at 17:13