Сделать так, чтобы веб-приложение .NET Core могло ссылаться на EF DbContext через интерфейс, а не напрямую

У меня есть сценарий, в котором в StartUp.cs мне нужно настроить EF с DbContext следующим образом:

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddDbContext<TestProjDbContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddMvc();
}

Однако мое решение разбито на несколько уровней ... Например: Api, Data, Domain, Services ...

Я пытаюсь сделать так, чтобы проект Api ссылался только на проекты домена и служб. Однако DbContext меня все испортил. TestProjDbContext находится в проекте Data вместе с моими репозиториями, миграциями, классом единиц работы и т. Д.

В моем проекте Domain у меня есть несколько интерфейсов, таких как IUnitOfWork, ICustomerRepository и т. Д. Я также создал интерфейс ITestProjDbContext в этом проекте, думая, что каким-то образом смогу передать его в services.AddDbContext в StartUp.cs. Однако это кажется невозможным.

Как я могу сохранить файл TestProjDbContext.cs внутри моего уровня данных, чтобы Api не знал об уровне данных и только уровне домена? Или DbContext не должен находиться на уровне данных, а просто находиться на уровне домена без использования интерфейса?


person Blake Rivell    schedule 18.04.2017    source источник
comment
Зачем задавать один и тот же вопрос двумя разными способами в двух разных сообщениях?   -  person Erik Funkenbusch    schedule 18.04.2017
comment
Кое-что рассмотреть ... DbContext по сути является реализацией шаблонов Unit Of Work и Repository. Вы можете сэкономить много времени и кода, поместив свой конкретный DbContext в домен и отказавшись от всех интерфейсов IRepository.   -  person Brad    schedule 18.04.2017
comment
В этом нет смысла, это нормально, если проект с корнем композиции (WebApi, MVC, консоль, приложение WPF, UWP) может ссылаться на все другие сборки и уровни. Уровень домена и уровня служб не должен иметь ссылки на Данные, поскольку Данные - это инфраструктура ( База данных, веб-фреймворк, например mvc, веб-API и т. Д.)   -  person Tseng    schedule 18.04.2017
comment
Способ отделить доступ к базе данных от домена - это использовать шаблон репозитория (для использования CRUDy) или CQRS (для расширенного использования, когда чтение и запись должны быть разделены и для более ориентированного на сообщения подхода) и инкапсулировать запросы в репозитории. или обработчики команд / запросов (в CQRS). Однако в обоих случаях вы теряете большую часть преимуществ ORM, за исключением репозитория / обработчиков CQ.   -  person Tseng    schedule 18.04.2017


Ответы (1)


Да, можно предоставить интерфейс для вашего DbContext и вставить его в свой startup.cs и использовать интерфейс (например, ITestDbContext) в своих контроллерах API. .

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

Джерри Пелсер - Разрешите свой DbContext как интерфейс с использованием инфраструктуры внедрения зависимостей ASP.NET 5

Шаг 1. Объявите свой ITestDbContext интерфейс и добавьте свой DbSets

public interface ITestDbContext
{
    DbSet<Episode> Episodes { get; set; }
    DbSet<ApplicationUser> Users { get; set; }
    // ....
    int SaveChanges();
    Task<int> SaveChangesAsync(CancellationToken cancellationToken);
}

Шаг 2 - Реализуйте ITestDbContext как конкретный класс TestDbContext

public class TestDbContext : IdentityDbContext<ApplicationUser>, ITestDbContext
{
    public virtual DbSet<Episode> Episodes { get; set; }

    public ApplicationDbContext()
    {
        // ...
    }
}

Шаг 3 - Настройте внедрение зависимостей в интерфейсе в методе ConfigureServices.

public void ConfigureServices(IServiceCollection services)
{
    ...

    // Add EF services to the services container.
    services.AddEntityFramework()
        .AddSqlServer()
        .AddDbContext<TestDbContext>(options =>
            options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]));

    // Register the service and implementation for the database context
    services.AddScoped<ITestDbContext>(provider => provider.GetService<TestDbContext>());

  //    ...
}

Ключевым моментом, на который следует обратить внимание, является следующее

services.AddScoped<ITestDbContext>(provider => provider.GetService<TestDbContext>());

ПРИМЕЧАНИЕ. Прочтите, как автор Джерри Пелсер использовал этот подход в его сообщение в блоге.

Шаг 4 - Используйте интерфейс ITestDbContext в своих контроллерах Api

public class EpisodesController : Controller
{
    private readonly ITestDbContext dbContext;

    public EpisodesController(ITestDbContext dbContext)
    {
        this.dbContext = dbContext;
    }

    ...
}
person SeanPatel    schedule 18.04.2017
comment
Это не сработает. Чтобы использовать DbSet<T> или .Include(), вам все равно потребуется ссылаться на сборку EF Core. Кроме того, поскольку проект WebApi является корнем композиции, он должен знать о сборке данных, содержащей EF Core (ну, этого можно было бы избежать, используя сложную загрузку динамического модуля, но это не имеет смысла, поскольку вам нужно настроить DbContext в Startup .cs тоже со строкой подключения) - person Tseng; 18.04.2017
comment
И последнее, но не менее важное: для настройки строки подключения вам также необходимо указать EF Core (и / или проект с конкретной реализацией DbContext), чтобы настроить его для DI и строки подключения с помощью .AddDbContext<MyDbContext>( options => options.UseSqlServer(myConnectionString)); - person Tseng; 18.04.2017
comment
@tseng Спасибо за полезные ответы. Вы бы сохранили DbContext на уровне данных с остальной частью EF или на уровне домена с интерфейсами и бизнес-объектами? - person Blake Rivell; 18.04.2017
comment
@tseng Я знаю, что многие из этих высказываний являются самоуверенными, но не могли бы вы поделиться своими мыслями о следующем: 4 проекта - API, домен, службы, данные. Похоже, что проект API должен будет ссылаться как на проекты данных, так и на сервисы. Таким образом, я могу внедрять как репозитории, так и сервисы в свои контроллеры API. Это правильно, или я должен вводить только сервисы и делать так, чтобы только сервисы знали об уровне данных. - person Blake Rivell; 18.04.2017
comment
@Tseng In order to use DbSet<T> or .Include() you will still need to reference EF Core assembly. Никто с этим не спорит. OP хотел использовать интерфейс для DbContext, и мое решение делает это. Пожалуйста, объясните себя ответом вместо того, чтобы быть расплывчатым в комментариях. - person SeanPatel; 19.04.2017
comment
Идея использования интерфейса в конкретном классе состоит в том, чтобы отделить его и удалить зависимости, т.е. когда у вас есть репозиторий, который возвращает только конкретные модели или IEnumerable<T>, тогда вы можете использовать этот IRepository<T> интерфейс в своей модели предметной области / бизнес-уровне без ссылка на уровень данных (и в данном случае для конкретной базы данных, например EFCore и EFCore.SqlServer). Но в приведенном выше случае вы просто добавляете к нему интерфейс, но по-прежнему имеете зависимости от уровня базы данных и EFCore, так что небольшая выгода. - person Tseng; 19.04.2017
comment
И для UnitTests это тоже не имеет большого значения, поскольку имитировать DBContext или IQuerybale довольно сложно, и это скорее приводит к интеграционным тестам с поставщиком EF Core InMemory (для чего он был создан). Но абстрагируя EFCore от CQRS / Repository с другой стороны, вы потеряете преимущества использования ORM (то есть вы не сможете настраивать свои запросы вне репозитория, добавляя дополнительные фильтры или прогнозы). В большинстве случаев это приводит к прямому использованию EF Core в обработчиках команд или репозитория, чтобы абстрагировать его от остальных, вместо того, чтобы пытаться абстрагировать сам EF. - person Tseng; 19.04.2017
comment
@BlakeRivell: Ну, это DbContext принадлежит уровню данных, поскольку он является частью инфраструктуры, а не вашей бизнес-логики / домена. Но ваше приложение по-прежнему должно об этом знать. Вкратце: домен (где находится бизнес-логика) не должен знать об этом, а службы не должны знать об уровне данных. Прикладной уровень (весь конкретный код приложения, то есть UWP или WPF, такой как службы навигации или всплывающие окна) может знать об этом. Уровень домена / бизнеса - единственное, что не должно знать ни о каком другом слое. - person Tseng; 19.04.2017
comment
@Tseng, поэтому вы рекомендуете, чтобы я оставил DbContext на уровне данных. Вы также говорите, что мое приложение ссылается на уровень данных, чтобы знать об этом, не проблема. Главное правило: домен ни на что не ссылается. Также похоже, что вы предлагаете мне не создавать проект служб, а просто поместить всю свою бизнес-логику в проект домена. Если я сделаю это, то что будет хорошим соглашением об именах для этих классов бизнес-логики. Обычно я бы сделал CustomerService или CustomerManager .. - person Blake Rivell; 19.04.2017
comment
@BlakeRivell: немного выходит за рамки, но есть 2 школы по работе с доменами: анемичный домен, модель (без логики и всей логики в сервисах) и богатая модель предметной области (как можно больше логики в сущностях предметной области, таких как Customer и только тонкий служебный слой для логики, которая туда не влезает). Но это большая тема, ею можно охватывать целые книги (во множественном числе). Используйте Google. Услуги: Зависит. Существуют сервисы домена и приложения, это определяет, где они будут реализованы. то есть служба навигации тесно связана с пользовательским интерфейсом (бизнес-логика практически отсутствует), поэтому она переходит на уровень приложения. - person Tseng; 19.04.2017