Убираем зависимость приложения от Entity Framework

Многие приложения не обращаются к Entity Framework напрямую. Для этого используются различные сервисы, репозитории, команды, запросы и. т. д. При этом вся непосредственная работа с базой данных вынесена в отдельную сборку. Однако, Entity Framework все равно приходится добавлять в запускаемые приложения. Но является ли это обязательным или можно этого как-то избежать?

А зачем это надо?

На первый взгляд лишнее указание в References никому не мешает. Однако:

  • Различные абстракции и шаблоны проектирования (например, CQRS) используются для сокрытия подробностей хранения данных от приложения. При этом тут же в web.config/app.config указываются connectionStrings и другие параметры, по сути раскрывающие их.
  • Если в проекте более 1 приложения, то для каждого из них приходится задавать (подключать) и поддерживать в актуальном состоянии все требуемые настройки (например, connectionStrings).
  • С выходом новой версии Entity Framework придется обновлять, компилировать и публиковать все что от неё зависит. Хотя на самом деле необходимость делать это есть только для  *.DataAccess сборки.

Давайте рассмотрим как можно исключить зависимость от Entity Framework у приложения SomeProject.WebUI. Пусть также имеется сборка SomeProject.Infrastructure.DataAccess, которая и предназначена для доступа к данным.

Данное описание подойдет и для создания нового проекта. Просто не потребуется первый шаг.

Шаг 1 – Избавляемся от зависимости в приложении SomeProject.WebUI

Необходимо из существующего веб-приложения удалить Entity Framework:

PM> uninstall-package EntityFramework -ProjectName SomeProject.WebUI

Из файла конфигурации (web.config/app.config) необходимо убрать все связанные c Entity Framework секции. При этом данные connectionStrings можно где-то временно сохранить, т.к. они пригодятся в дальнейшем.

В результате приложение больше не зависит от Entity Framework и не содержит параметры её настройки.

Шаг 2 – Задаем строку соединения с БД в SomeProject.Infrastructure.DataAccess

Добавим в сборку SomeProject.Infrastructure.DataAccess конфигурацию SomeProject.Infrastructure.DataAccess.config, содержащую строку соединения:

<?xml version="1.0" encoding="utf-8"?>
<configuration>

  <connectionStrings>
    <add name="SomeProjectDatabase" providerName="System.Data.SqlClient"
         connectionString="…" />
  </connectionStrings>

</configuration>

В его свойствах этого файла нужно указать необходимость его копирования при сборке проекта (Copy to Output Directory: Copy always).

Данный подход работает в случаях, если нет необходимости в трансформациях файла конфигурации или они реализуются скриптом на build-сервере. При необходимости изменений для отладки или публикации с компьютера разработчика можно воспользоваться описанным тут советом.

Заданное выше значение строки соединения необходимо передать в класс, который реализует контекст. Вот пример кода для решения этой задачи:

internal class SomeProjectContext : DbContext
{
    private const string _cfgFileName = "SomeProject.Infrastructure.DataAcceess.config";
    private const string _connectionStringName = "SomeProjectDatabase";

    private static readonly string _connectionString;

    static SomeProjectContext()
    {
        // Получаем полное имя файла конфигурации
        string codeBase = Assembly.GetExecutingAssembly().CodeBase;
        var uri = new UriBuilder(codeBase);
        string path = Uri.UnescapeDataString(uri.Path);
        var fullCfgFileName = string.Format("{0}/{1}",
            Path.GetDirectoryName(path),
            SomeProjectContext._cfgFileName);

        // Загружаем файл конфигурации 
        var configFileMap = new ExeConfigurationFileMap { ExeConfigFilename = fullCfgFileName };
        Configuration config = ConfigurationManager.OpenMappedExeConfiguration(
            configFileMap, ConfigurationUserLevel.None);

        // Получаем строку соединения
        var connectionStrings = config.ConnectionStrings.ConnectionStrings;
        for (int i = 0; i < connectionStrings.Count; ++i) {
            if (string.Equals(connectionStrings[i].Name, SomeProjectContext._connectionStringName, StringComparison.OrdinalIgnoreCase)) {
                SomeProjectContext._connectionString = connectionStrings[i].ConnectionString;
                break;
            }
        }

        if (string.IsNullOrEmpty(SomeProjectContext._connectionString))
            throw new ConfigurationErrorsException("Connection string was not found.");
    }

    public SomeProjectContext()
        : base(SomeProjectContext._connectionString)
    {
    }

    ...
}

Шаг 3 – Настройка Entity Framework (при необходимости)

Дополнительно можно настроить стратегию исполнения запросов (execution strategy) или фабрику соединений (connection factory). Раньше для этого использовался web.config/app.config. Теперь тоже самое сделаем в коде приложения. Необходимо создать наследника DbConfiguration и задать в нем требуемые параметры настройки. Например, для поддержки SQL Azure это может выглядеть так:

internal class SomeProjectDatabaseConfiguration : DbConfiguration
{
    public SomeProjectDatabaseConfiguration()
    {
        this.SetDefaultConnectionFactory(new SqlConnectionFactory());
        this.SetProviderServices(SqlProviderServices.ProviderInvariantName, SqlProviderServices.Instance);

        this.SetExecutionStrategy(SqlProviderServices.ProviderInvariantName,
            () => new SqlAzureExecutionStrategy());
    }
}

Созданная конфигурация связываться с контекстом при помощи атрибута:

[DbConfigurationType(typeof(SomeProjectDatabaseConfiguration))]
internal class SomeProjectContext : DbContext { … }

Шаг 4 – Исправляем развертывание проекта на сервере

В текущем состоянии проект может быть запущен в отладке. Однако при развертывании на сервере он выдаст ошибку. Если начать искать причину, то окажется что на сервер не была скопирована сборка EntityFramework.SqlServer.dll. Чтобы исправить ситуацию, необходимо обратиться к ней в любом месте кода. Например вот так:

bool instanceExists = System.Data.Entity.SqlServer.SqlProviderServices.Instance != null;

Данная строка, конечно, не имеет смысловой нагрузки. Однако это заставит включить требуемую dll в список для копирования на сервер.

Теперь проект избавлен от ненужной зависимости и полностью работоспособен.

Комментарии (8) -

А как в этом случае быть с Web Transform? Когда для разных билдов разные настройки

@ Alexey: В случае с build server (TFS) это реализуется в билд скрипте (в проекте, где описанный подход был реализован, сделано именно так).

Если же речь о локальной сборке, то вопрос хороший. Вскоре я обновлю статью, добавив ответ на него.

@ Alexey: Как и обещал - добавил необходимую информацию.

Андрей 18.08.2014 14:10:49

Прошу прощения, а как избавится от контекста полностью. Не получается что-то. Для теста создал репозиторий, слинковал в консольный проект. И при создании обьекта репозитория в консольном проекте вываливает ошибка, что нет линка на Entity Framework.
Спасибо.

Андрей не понял что подразумевается под "избавиться от контекста". От DbContext?

Андрей 18.08.2014 20:44:16

Именно. DBContext родитель репозитория и создание обьекта типа репозиторий требует наличие доступа к нему. Сделал как описано у вас, ибо давно хочу избавить приложение от зависимости Ентити фреймворка, но пока не получается.

Андрей 19.08.2014 21:11:36

В смысле от Entity Framework. Имею
public class Repository : BaseRepository<MyCustomConext>{} где
BaseRepository<T> : where T : DbConext, new() {} ну и
MyCustomConext: DbContext{ инициализация подключения к нужной ДБ}
вот и получаю при попытке создания
using(var rp = new Repository){

}  ошибку отсутствия пакета Entity Framework в тестовом консольном приложении.
Как это побороть?
Спасибо.

А если не наследовать BaseRepository а сделать декоратор?

Добавить комментарий