Неоднозначность сочетания родового и специального метода расширения C # - PullRequest
3 голосов
/ 02 февраля 2011

У меня есть абстрактный класс , называемый Fruit. Затем у меня есть производный класс , называемый Apple .

У меня есть два метода расширения:

public static IQueryable<TFruit> WithEagerLoading<TFruit>(this IQueryable<TFruit> query) where TFruit : Fruit
{
   return query.EagerLoad(x => x.Distributors); // Fruit.Distributors
}

public static IQueryable<Apple> WithEagerLoading(this IQueryable<Apple> query)
{
   query = query.EagerLoad(x => x.AppleBrands); // Apple.AppleBrands

   // now bubble up to base extension method
   return query.WithEagerLoading<Apple>();
}

Теперь вот общий метод, который у меня есть в репозитории:

public TFruit FindById<TFruit>(int fruitId) where TFruit : Fruit
{
   var query = _ctx.Fruits // IQueryable<Fruit>
                   .OfType<TFruit>(); // IQueryable<TFruit>

   query = query.WithEagerLoading();

   return query.SingleOrDefault(x => x.FruitId == fruitId);
}

У меня проблема, когда я делаю это:

var apple = repository.FindById<Apple>(1);

Входит в метод расширения IQueryable<Fruit>.

Я хочу использовать метод расширения IQueryable<Apple>. Для других типов фруктов следует использовать метод расширения IQueryable<TFruit>.

Я думал, что компилятор выберет самый специфический метод расширения.

Есть идеи?

EDIT

Спасибо за комментарии / ответ. Теперь я понимаю, почему это не работает.

Так, каковы варианты решения этой проблемы? Если я создаю метод:

public static IQueryable<Apple> WithAppleEagerLoading(this IQueryable<Apple> query)

Как бы я назвал это из моего общего хранилища? Мне бы пришлось проверить тип TFruit:

public TFruit FindById<TFruit>(int fruitId) where TFruit : Fruit
{
   var query = _ctx.Fruits // IQueryable<Fruit>
                   .OfType<TFruit>(); // IQueryable<TFruit>

   if (typeof(TFruit) == typeof(Apple))
       query = query.WithAppleEagerLoading();
   else 
       query = query.WithEagerLoading();

   return query.SingleOrDefault(x => x.FruitId == fruitId);
}

Что нехорошо - учитывая, что у меня есть около 20 производных типов.

Может кто-нибудь предложить альтернативу тому, что я пытаюсь сделать?

Ответы [ 3 ]

2 голосов
/ 02 февраля 2011

У меня была похожая проблема, когда я хотел эффективно изменить приоритет метода, чтобы он сначала разрешил «специализированную» версию.

Вы можете достичь этого, не изменяя свой вызывающий код, но решение может быть непопулярным, поскольку оно использует отражение во время выполнения и генерацию кода. В любом случае, я просто собираюсь его там (я пытаюсь выложить как минимум один ответ в день на ТАК!).

Обратите внимание, что этот код представляет собой абстрактный шаблон, который необходимо адаптировать к вашему сценарию.

public class Base
{
  public string BaseString { get; set; }
}

public class Derived : Base
{
  public string DerivedString { get; set; }
}

public static class SO4870831Extensions
{
  private static Dictionary<Type, Action<Base>> _helpers = 
    new Dictionary<Type,Action<Base>>();

  public static void Extension<TBase>(this TBase instance) 
    where TBase :Base
  {
    //see if we have a helper for the absolute type of the instance
    var derivedhelper = ResolveHelper<TBase>(instance);

    if (derivedhelper != null)
      derivedhelper(instance);
    else
      ExtensionHelper(instance);
  }

  public static void ExtensionHelper(this Base instance)
  {
    Console.WriteLine("Base string: {0}", 
      instance.BaseString ?? "[null]");
  }

  /// <summary>
  /// By Default this method is resolved dynamically, but is also 
  /// available explicitly.
  /// </summary>
  /// <param name="instance"></param>
  public static void ExtensionHelper(this Derived instance)
  {
    Console.WriteLine("Derived string: {0}", 
      instance.DerivedString ?? "[null]");
    //call the 'base' version - need the cast to avoid Stack Overflow(!)
    ((Base)instance).ExtensionHelper();
  }

  private static Action<Base> ResolveHelper<TBase>(TBase instance) 
    where TBase : Base
  {
    Action<Base> toReturn = null;
    Type instanceType = instance.GetType();
    if (_helpers.TryGetValue(instance.GetType(), out toReturn))
      return toReturn;  //could be null - that's fine

    //see if we can find a method in this class for that type
    //this could become more complicated, for example, reflecting
    //the type itself, or using attributes for richer metadata
    MethodInfo helperInfo = typeof(SO4870831Extensions).GetMethod(
      "BaseExtensionHelper", 
      BindingFlags.Public | BindingFlags.Static, 
      null, 
      new Type[] { instanceType }, 
      null);

    if (helperInfo != null)
    {
      ParameterExpression p1 = Expression.Parameter(typeof(Base), "p1");
      toReturn =
        Expression.Lambda<Action<Base>>(
        /* body */
          Expression.Call(
            helperInfo,
            Expression.Convert(p1, instanceType)),
        /* param */
          p1).Compile();
      _helpers.Add(instanceType, toReturn);
    }
    else
      //cache the null lookup so we don't expend energy doing it again
      _helpers.Add(instanceType, null);
    return toReturn;
  }
}
/// <summary>
/// Summary description for UnitTest1
/// </summary>
[TestClass]
public class UnitTest1
{
  [TestMethod]
  public void TestMethod1()
  {
    var a = new Base() { BaseString = "Base Only" };
    var b = new Derived() { DerivedString = "Derived", BaseString = "Base" };

    a.Extension();
    //Console output reads:
    //"Base String: Base Only"
    b.Extension();
    //Console output reads:
    //"Derived String: Derived"
    //"Base String: Base"
  }

Я не говорю, что этот шаблон лучше, чем поиск полиморфного решения, в котором используется более традиционный шаблон, предоставляемый языком - но это решение:)

Этот шаблон можно применять к большинству методов расширения, но в его текущей форме вам придется повторять шаблон для каждого метода расширения, который вы хотите написать. Точно так же, если эти расширения требуют параметров ref / out, это будет сложнее.

Как я также сказал в моих комментариях, вы можете рассмотреть вопрос об изменении поиска метода для поддержки определения его в самом типе экземпляра (объединяет соответствующий код).

Вы должны были бы проделать небольшую работу, чтобы сделать это правильно для вашего IQueryable - извините, я не сделал это решение более прямо относящимся к вашему сценарию, просто проще смоделировать тестовое решение, которое выводит большую часть общего ада!

1 голос
/ 02 февраля 2011

В конечном итоге вам нужно найти способ ввести некоторый полиморфизм - вам нужно специальное поведение для загрузки яблок, которое расширяет базовое поведение для загрузки фруктов. Иногда самый простой способ сделать это - создать классы репозитория, такие как:

class Repository<T> : IRepository<T>
{

   public virtual T FindById(int id)
   { ... }
}

class FruitRepository<T> : Repository<T> where T : Fruit
{
   public override T FindById(int id)
   {  ... }
}

class AppleRepository : FruitRepository<Apple>
{
   public override T FindById(int id)
   {  ... }
}

Теперь FindByID не нужно иметь универсальный параметр на уровне метода, он просто использует универсальный параметр на уровне класса. Затем вы можете настроить FruitRepository и AppleRepository переопределить FindByID в зависимости от ситуации. Ваш код использования будет немного другим, потому что вы должны убедиться, что ваш экземпляр репозитория подходит для поиска яблок.

Если вы используете контейнер IoC, вы можете зарегистрировать это, когда что-то запрашивает IRepository<Apple>, что оно возвращает экземпляр AppleRepository. Тогда вы бы использовали это так:

// ideally you would resolve this via constructor injection, but whatever.
var repository = container.Resolve<IRepository<Apple>>(); 
var apple = repository.FindByID(1);

А если вы не используете контейнер IoC ... ну .. вы должны быть:)

0 голосов
/ 02 февраля 2011

Разрешение метода расширения происходит во время компиляции. Переменная запроса имеет тип IQueryable<TFruit>, поэтому компилятор выбирает наиболее конкретный метод, который соответствует WithEagerLoading<Fruit>. Он не может выбрать яблочный, потому что знает только, что TFruit - это что-то вроде Fruit. Он выбирает его один раз и навсегда.

То, что вы предлагаете, потребует от него динамического решения, какой метод расширения использовать на основе типа во время выполнения, или для компиляции отдельных версий IQueryable<TFruit>, которые разрешают методы по-разному, в зависимости от конкретных значений TFruit.

Изменить для ответа на дополнительный вопрос

Ну, специальный корпус не супер ужасен, потому что вы можете использовать оператор switch. Но я согласен, не идеально, если у вас много типов. Что касается делегирования подклассам, я бы немного скорректировал ответ Пола:

abstract class FruitRepository : IRepository<T> where TFruit : Fruit
{
   public TFruit FindByID(int fruitID)
   {
       //query stuff here

       query = AddEagerLoading(query)
                  .WithEagerLoading();
    }

    //this could also be abstract to prevent you from forgetting
    public virtual IQueryable<TFruit> AddEagerLoading(IQueryable<TFruit> query)
    {
        return query;
    }
}

, а затем

class AppleRepository : FruitRepository<Apple>
{
    public override AddEagerLoading(IQueryable<Apple> query)
    {
        return query.EagerLoad(x => x.AppleBrands);
    }
}

Таким образом, у вас будет минимальный код для каждого подкласса.

...