Pull to refresh

Как создать DbContext внутри Visual Studio, или «Что делать, если хочется странного?»

Developer Soft corporate blog .NET *


Начиная с версии 14.1, в XtraReports появилась встроенная поддержка ORM Entity Framework. Если раньше разработчику приходилось использовать стандартный компонент BindingSource для привязки элементов отчета к данным и затем вручную писать код для загрузки данных из EF модели, то сейчас ему достаточно только выбрать конкретный контекст (из текущего проекта или сборки, указанной в References проекта) и указать используемую строку подключения. Компонент EFDataSource сам создаст контекст с нужной строкой подключения и вернет данные отчету.

Что это дает разработчику, кроме удобства:
Во-первых, это облегчает первоначальное знакомство с XtraReports. Уже не надо думать:  “А как же здесь использовать данные из Entity Framework?”. Есть простой мастер, где достаточно ответить на пару вопросов из серии “А что конкретно тебе надо”.
Во-вторых, это дает возможность увидеть реальные данные в Preview отчета в Visual Studio, что облегчает собственно само создание отчета, так как всегда можно проконтролировать результат без запуска отдельного приложения.
В-третьих, разработчик теперь может дать конечным пользователям своего приложения самим создавать отчеты с использованием данных из модели EntityFramework.



Ну а теперь, когда с необходимым предисловием закончено, можно перейти к более интересным вещам, а именно — как это все устроено и как работает.

(Здесь и далее — курсивом выделены некие личные впечатления, призванные разбавить скучные и занудные технические подробности)Казалось бы, сделать такой компонент ничего не стоит. Нарисовать некоторое количество форм, придумать несложное API. Однако, тут есть нюанс — надо получить реальные данные из модели внутри Visual Studio. Как говорил Боромир, “Нельзя просто так взять и создать инстанс пользовательского DbContext в процессе VisualStudio”.

По умолчанию Entity Framework сохраняет строку подключения к базе данных в app/web.config. Соответственно, при попытке создания контекста из процесса Visual Studio это сразу же приводит к ошибке, так как студийный devenv.exe.config не содержит ту строку подключения, с которой был создан контекст. Эту проблему можно было бы обойти, заставив разработчика отчета создать у модели данных нужный конструктор по умолчанию, но это не наш путь. Желательно, чтобы в простейшем случае (а именно, это случай, когда data context был создан в результате работы Visual Studio и в него не вносилось никаких изменений) от разработчика не требовалось никаких дополнительных действий.

Кроме того, Entity Framework поддерживает самые разнообразные СУБД посредством сторонних провайдеров данных. Для использования какого-либо провайдера данных, отличного от дефолтного MS SQL, Entity Framework необходимо правильным образом настроить (через app.config или в коде), и сделать доступными все нужные сборки, положив их в GAC либо рядом с запускаемым проектом. В случае запуска из Visual Studio это тоже не так то просто обеспечить:
Во-первых, уже упомянутая проблема devenv.exe.config.

Во-вторых, сборки провайдеров сторонних СУБД зачастую качаются NuGet’ом и хранятся локально в проекте, а не в GAC’е, что тоже приводит к невозможности напрямую создать указанный пользователем DbContext.

Итак, нам требуется:
  1. Найти в проекте пользователя строку подключения с заданным именем
  2. Создать пользоовательский DbContext при условии, что там может не быть нужного конструктора, принимающего строку подключения, а в GAC нет сборок, от которых он зависит (в первую очередь EntityFramework.dll)
  3. Сконфигурировать EntityFramework для работы с пользовательской СУБД, если она отличная от стандартного Microsoft SQL Server.


По умолчанию, строка подключения создается с тем же именем, как у модели данных. Однако, её имя может быть легко изменено и узнать, какую именно строку подключения хотел использовать разработчик, невозможно. Тут нет выхода — возможно только спросить это у самого разработчика.Вообще, при разработке компонентов надо стараться минимизировать количество допущений и предположений. Чем меньше твой компонент решает что-либо за разработчика, тем лучше. Всегда лучше спросить, чем сделать не так.



Дальше необходимо получить конкретную строку подключения из конфигурационного файла текущего проекта — ConfigurationManager с этим не поможет. Зато, нам поможет объектная модель автоматизации VisualStudio EnvDTE, а в частности интерфейс  Microsoft.VSDesigner.VSDesignerPackage.IGlobalConnectionService (к сожалению, недокументированный):

public interface IGlobalConnectionService    {
        DataConnection[] GetConnections(System.IServiceProvider serviceProvider, Project project);
        bool AddConnectionToServerExplorer(System.IServiceProvider serviceProvider, DataConnection connection);
        bool AddConnectionToAppSettings(System.IServiceProvider serviceProvider, Project project, DataConnection connection);
        bool RemoveConnectionFromAppSettings(System.IServiceProvider serviceProvider, Project project, DataConnection connection);
        bool UpdateConnectionInAppSettings(System.IServiceProvider serviceProvider, Project project, DataConnection oldConnection, DataConnection newConnection);
        bool IsValidPropertyName(System.IServiceProvider serviceProvider, Project project, string propertyName);
        bool RefreshApplicationSettings(System.IServiceProvider serviceProvider, Project project);    
}


Здесь нас интересует метод GetConnections, который возвращает массив объектов DataConnection. Более того, этот метод находит строки не только в app.config, но и в Server Explorer’е и в machine.config. Более подробную информацию про IGlobalConnectionService можно почерпнуть из рефлектора и сборки Microsoft.VSDesigner.

Какой-либо рефлектор (чаще всего я использую бесплатный ILSpy) при разработке компонентов или плагинов к студии вещь незаменимая — как правило, чтобы сделать что-то, приходится “подсматривать” отладчиком, как реализована похожая функциональность у Microsoft и потом анализировать исходные коды “подсмотренных” сборок. В нашем случае, искомый сервис подсказали студийные мастеры Add New DataSet и Add New ADO.NET  Entity Data Model.

И вот, у нас есть строка подключения. Но что с ней делать в случае, если у пользовательской модели нет конструктора, принимающего строку подключения? Ответ одновременно и простой и сложный — надо при помощи Reflection.Emit сделать динамическую сборку, в ней создать свой потомок пользовательской модели данных и в нем сделать нужный конструктор. И тут внимательный читатель может задать вопрос: А как мы создадим свой конструктор  в классе потомке, если конструктора с необходимыми параметрами нет в базовом классе?   Ответ снова простой — в IL вызов конструктора базового класса является необязательным, и можно позвать любой конструктор любого предка в иерархии.

.class public auto ansi DevExpress.DataAccess.Tests.Entity.AdventureWorksCFEntities
    extends [DevExpress.DataAccess.v14.2.Tests.MsSqlEF6]DevExpress.DataAccess.Tests.Entity.AdventureWorksCFEntities
{
    .method public specialname rtspecialname 
        instance void .ctor (
            string ''
        ) cil managed 
    {
        .maxstack 2
        IL_0000: ldarg.0
        IL_0001: ldarg.1
        IL_0002: call instance void [EntityFramework]System.Data.Entity.DbContext::.ctor(string)
        IL_0007: ret
    } 
} 

Да, это выглядит как “грязный хак” — но тем не менее это работает и разрешено IL. Собственно, альтернативным способом “подсунуть” нужную строку подключения EntityFramework’у является не намного более “чистый” хак с ConfigurationManager’ом, описанный например здесь.


Создание динамической сборки, помимо собственно создания потомка пользовательской модели, позволяет также решить проблему с референсом на EntityFramework.dll. Для создания вызова System.Data.Entity.DbContext::.ctor(string)  в любом случае требуется загрузить сборку EntityFramework.dll и получить оттуда тип DbContext.  Искать её в GAC или в текущей директории дело неблагодарное из-за того, что скорее всего она лежит локально где-то в репозитории NuGet. Поэтому, приходится снова воспользоваться объектной моделью автоматизации студии, в частности ITypesDiscoveryService, и поискать EntityFramework.dll в референсах проекта. Забегая вперед — там же можно найти сборку кастомного провайдера данных для EntityFramework, если таковой необходим.

Итак, две из трех проблем решены. Осталась самая простая, но трудоемкая из всех — регистрация произвольного провайдера данных. Как я уже писал, Entity Framework способна работать с самыми разными СУБД через произвольные провайдеры данных. Самый простой способ их использовать — это прописать необходимые настройки в app.config.  Другим способом является использование класса DBConfiguration, который представляет собой реализацию паттерна Chain-of-Responsibility и хранящий список резолверов IDbDependencyResolver. Каждый их них в свою очередь реализует паттерн Service Locator. Entity Framework во время процедуры инициализации ищет потомка DBConfiguration в той же сборке, что и модель данных, и если находит — то запрашивает у него имя используемого провайдера данных, фабрики DbProviderServices и DbProviderFactory, и так далее.

Даже сами разработчики EF в документации оправдываются — “Да, мы знаем что Service Locator это антипаттерн, но мы знаем что делаем и в нашем случае его использование оправдано.”

Вот пример настройки Entity Framework для использования SqlCE:


public class SqlCEConfiguration : DbConfiguration {
    public SqlCEConfiguration() {
        SetProviderServices(  SqlCeProviderServices.ProviderInvariantName,
            SqlCeProviderServices.Instance);
        SetDefaultConnectionFactory(
            new SqlCeConnectionFactory(SqlCeProviderServices.ProviderInvariantName));
    }
}


Так как потомок класса DbConfiguration должен лежать в одной сборке с DbContext’ом, соответственно его необходимо создать в той же динамической сборке, в которой мы чуть ранее создали потомка пользовательской модели. Тут придется написать немного более сложный код, разный для разных провайдеров данных. И для этого потребуются типы из сборок соответствующих провайдеров данных — их можно найти через тот же ITypesDiscoveryService, при условии, что нужные сборки есть в референсах проекта.

Написание кода на Reflection.Emit, который создает сборку с требуемым IL достаточно муторное занятие — однако, его может очень облегчить плагин ReflectionEmitLanguage к Reflector’у.  Он не создает на 100% рабочий код, однако помогает избежать “глупых” ошибок при переписывании инструкций IL.

Подведем итог: Получить данные из произвольной EF модели внутри процесса Visual Studio непросто, так как для этого необходимо “подсунуть” ей нужную строку подключения и настроить Entity Framework для работы с произвольным провайдером данных.Если все таки очень хочется это сделать, то для этого придется:
  • разобраться с объектной моделью автоматизации VisualStudio EnvDTE,
  • освоить программирование на IL с помощью Reflection.Emit,
  • изучить способы конфигурирования EF с помощью класса  DbConfiguration.

Понятно, что в рамках статьи невозможно осветить весь опыт работы с EF в нашей компании. Возникали (и возникают) разные проблемы и не все их удавалось решить, так не все зависит от нас как разработчиков компонентов. Но я считаю, что данный подход, хоть он и не работает в абсолютно всех случаях, все же улучшил жизнь для наших пользователей — разработчиков ПО.

Существует и иное мнение — что компоненты не должны давать разработчику работать с реальными данными.  Каких либо фундаментальных причин для этого нет, и сама Visual Studio дает это делать (например, при создании датасетов). Как я думаю, это основано как раз на подобном опыте и понимании того, что просто так сделать не получится, так как существует достаточно большая вероятность столкнуться с проблемой в какой либо из неподконтрольной себе областей — внутри Visual Studio, .NET Framework или Entity Framework.

И, наконец, последнее замечание: описанный механизм создания DbContext’а внутри Visual Studio впервые появился в наших WPF контролах в механизме Scaffolding. Он не был предназначен для получения данных, но в нем первоначально появилась идея с временной сборкой и генерацией в ней потомка DbContext’а.

Вот и все, что я хотел рассказать в этой статье. Готов ответить на любые вопросы в комментариях.

PS. Автор заглавного фото с корги — vk.com/kudma.
Tags: devexpress.netwinformsentity frameworkxtrareportsvisual studio
Hubs: Developer Soft corporate blog .NET
Total votes 35: ↑33 and ↓2 +31
Comments 3
Comments Comments 3

Information

Founded
1998
Location
Россия
Website
www.developersoft.ru
Employees
201–500 employees
Registered