Приведение динамического объекта и передача в шаблон UnitOfWork и репозитория. Вызывает исключение

#c# #repository-pattern #unit-of-work #expandoobject #dynamicobject

#c# #репозиторий-шаблон #единица работы #expandoobject #dynamicobject

Вопрос:

Это очень специфическая проблема. Не совсем уверен, как это даже сформулировать. По сути, я реализую шаблон unit of work и репозитория, у меня есть динамический объект, который я преобразую в int, но если я использую var , он выдаст исключение при попытке вызвать метод.

Я попытался удалить все тривиальные переменные для этой проблемы, которые я могу. По какой-то причине я вижу, что это происходит только с этими двумя шаблонами проектирования. Исключение, которое я получаю, это Additional information: 'BlackMagic.ITacoRepo' does not contain a definition for 'DoStuff'

Вот код:

 class BlackMagic
{
    static void Main(string[] args)
    {
        dynamic obj = new ExpandoObject();
        obj.I = 69;

        UnitOfWork uow = new UnitOfWork();

        int i1 = Convert.ToInt32(obj.I);
        var i2 = Convert.ToInt32(obj.I);

        if(i1.Equals(i2))
        {
            uow.TacoRepo.DoStuff(i1); // Works fine
            uow.TacoRepo.DoStuff(i2); // Throws Exception
        }
    }
}

class UnitOfWork
{
    public ITacoRepo TacoRepo { get; set; }

    public UnitOfWork()
    {
        TacoRepo = new TacoRepo();
    }
}

class Repo<T> : IRepo<T> where T : class
{
    public void DoStuff(int i)
    {
    }
}

interface IRepo<T> where T : class
{
    void DoStuff(int i);
}

class TacoRepo : Repo<Taco>, ITacoRepo
{
}

interface ITacoRepo : IRepo<Taco>
{
}

class Taco
{
}
  

РЕДАКТИРОВАТЬ: Главный вопрос, на который я пытаюсь найти ответ, заключается в том, почему исключение генерируется при вызове DoStuff внутри единицы работы (при использовании репозитория), но не генерируется, если в BlackMagic классе существует doStuff.

Комментарии:

1. Можете ли вы ввести точку останова перед DoStuff вызовом и посмотреть, каков фактический тип времени выполнения i2 ?

2. Это происходит потому, что среда выполнения использует отражение для того, чтобы попытаться определить тип i2 . Однако, поскольку i2 исходил из ExpandoObject , его объявление и отраженный тип равны null, и в результате он не соответствует int требованию DoStuff . Действительно, странно, возможно, вам следует просто избегать использования dynamic здесь таким образом.

3. @EdPlunkett Да, это действительно странно, потому что я тестировал эту проблему многими различными способами. Но, похоже, это произошло только тогда, когда я использовал шаблон unit of work и репозитория. Я не смог воспроизвести его каким-либо другим способом.

4. Хотя я не уверен в том, почему, я могу, по крайней мере, сказать вам, что связующим элементом среды выполнения является who при определении типа.

5. @TravisJ Я согласен с Ed в этом. Вы правильно подметили о преобразовании динамического объекта, но ошибка не выдается, если я вынимаю DoStuff из репозитория и помещаю его в основной класс `BlackMagic’, а затем передаю i2 в

Ответ №1:

Это одна из ошибок, о которых я сообщил в Microsoft более 5 лет назад, вскоре после того, как dynamic был представлен. Насколько я знаю, это считается очень низким приоритетом в их списке и, возможно, никогда не будет исправлено.

Вот простые шаги воспроизведения:

 using System.Collections;

class C
{
    static void Main()
    {
        object[] array = { };
        IList list = new ArrayList();
        list.CopyTo(array, 0); // Works okay
        dynamic index = 0;
        list.CopyTo(array, index); // Microsoft.CSharp.RuntimeBinder.RuntimeBinderException: 'System.Collections.IList' does not contain a definition for 'CopyTo'
    }
}
  

Вот объяснение проблемы. Когда элемент функции (метод или индексатор) вызывается в выражении, статический тип которого является типом интерфейса, и по крайней мере один из аргументов вызова имеет тип dynamic (что означает, что полный процесс поиска элемента — вывода типа — разрешения перегрузки откладывается до времени выполнения и становится обязанностью связующего во время выполнения, а не компилятора; компилятор выполняет только частичный набор проверок на основе неполной информации о типе), и вызываемый элемент наследуется интерфейсом от одного из своих базовых интерфейсов (а не объявленных в самом интерфейсе), то связующему среды выполнения не удается должным образом обойти дерево базовых интерфейсов, чтобы найти унаследованный элемент, и выдает исключение во время выполнения, сообщая, что требуемый элемент не найден. Обратите внимание, что это ошибка только связующего среды выполнения — компилятор правильно принял вызов (но отклонил бы его, если, например, вы допустили опечатку в имени метода).

Возможный обходной путь: приведите выражение, в котором вы вызываете элемент, к базовому интерфейсу, который фактически объявляет элемент, который вы пытаетесь вызвать. Например, программа из описанных выше шагов воспроизведения может быть исправлена следующим образом:

 using System.Collections;

class C
{
    static void Main()
    {
        object[] array = { };
        IList list = new ArrayList();
        list.CopyTo(array, 0); // Works okay
        dynamic index = 0;
        ((ICollection) list).CopyTo(array, index); // Works okay
    }
}
  

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

 using System.Collections;

class C
{
    static void Main()
    {
        object[] array = { };
        IList list = new ArrayList();
        list.CopyTo(array, 0); // Works okay
        dynamic index = 0;
        list.CopyTo(array, (int) index); // Works okay
    }
}
  

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

Комментарии:

1. То есть, по сути, вы хотите сказать, что, поскольку интерфейс ITacoRepo явно не реализует метод IRepo<Taco> DoStuff интерфейса, он не знает, где еще искать? Что это просто просмотр ITacoRepo ? Итак, технически проблема в интерфейсах, реализующих интерфейсы при передаче динамики?

2. Интерфейсы не могут реализовывать другие интерфейсы, они могут только наследовать от них. Они могут повторно объявлять унаследованные абстрактные члены, но это не является реализацией. Конечно, переменная типа интерфейса во время выполнения (если не null) будет содержать ссылку на объект некоторого конкретного типа, который имеет реализации всех членов интерфейса, как объявленных (или повторно объявленных) в нем, так и унаследованных. Но разрешение перегрузки должно происходить на основе статически известного типа получателя, то есть типа интерфейса. Связующему среды выполнения не удается должным образом собрать унаследованных кандидатов.

3. Однако во время выполнения, когда вы вызываете GetType on i2 , он сообщает вам, что это Int32 . Почему это все еще вызывает ту же проблему? Я понимаю, почему это было бы, если бы вы сохранили его как dynamic , но если проблема возникает во время выполнения, я не понимаю, почему он не принял бы int

4. Переменная i2 объявлена как var , что означает, что ее тип во время компиляции определяется из ее инициализатора. Его инициализатор содержит подвыражение obj типа dynamic , поэтому тип инициализатора также dynamic , как и тип i2 (точно так же, как если бы вы явно объявили его как dynamic ). Это означает, что любой вызов метода, использующий i2 в качестве аргумента, должен разрешаться динамически во время выполнения, используя тип среды выполнения объекта, на который он ссылается, для проверки применимости и разрешения перегрузки. Ошибка в runtime binder не позволяет ему найти подходящий унаследованный метод.

Ответ №2:

Похоже, что RuntimeBinder не пересекает иерархию наследования, поэтому он ищет только в непосредственном интерфейсе ITacoRepo определение doStuff.

Если вы сделаете UnitOfWork use IRepo<Taco> вместо ITacoRepo , он сможет найти определение метода.

Комментарии:

1. Почему это должно быть верно для var i2, а не для int i1?

2. @TravisJ Потому что RuntimeBinder не участвует в вызове с i1. Это все время компиляции.

3. Итак, тогда почему, если я создам локальный, IRepo<Taco> r = new TacoRepo() а затем вызову r.DoStuff(i2) , я не получу ошибку?

4. @THEStephenStanton Что произойдет, если вы определите r как ITacoRepo , подобно определению в UnitOfWork ? Я думаю, проблема в типе, который вы используете, а не в том, где находится переменная.

5. @KendallFrey Итак, вы затронули интересный момент, который я пытался (если я вас правильно понимаю). Если у меня есть только что have TacoRepo и ITacoRepo , и я перемещаю подпись DoStuff в ITacoRepo , затем реализую ее в ‘TacoRepo’ и просто делаю ‘ITacoRepo r = new TacoRepo()’, затем делаю ‘r.doStuff(i2)’, исключения нет. Вот почему так странно, что это происходит только тогда, когда я использую этот шаблон проектирования