Как создать DbContext внутри Visual Studio, или “Что делать, если хочется странного?”
Начиная с версии 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.
Итак, нам требуется:
Найти в проекте пользователя строку подключения с заданным именем Создать пользоовательский DbContext при условии, что там может не быть нужного конструктора, принимающего строку подключения, а в GAC нет сборок, от которых он зависит (в первую очередь EntityFramework.dll) Сконфигурировать 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.