Начиная с версии 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.
Автор: shurygin_s