有没有办法在不使用 Web 服务作为中间人的情况下避免将对 Entity Framework 的引用添加到我的 UI 项目?

Is there a way to avoid adding a reference to Entity Framework to my UI project without using a web service as a go-between?

我正在尝试为我的 UI class 库寻找一种方法,无论是 Win Forms、WPF、ASP.NET MVC 等,都没有任何知识Entity Framework 或其配置文件中的连接字符串。基本上,我希望我的 UI 项目不关心我的数据访问项目使用的数据访问技术。我不希望它担心它是否使用 LINQ、Entity Framework、基本 ADO.NET 或其他任何东西。

到目前为止,我所知道的实现此目的的唯一方法是使用 Web 服务并让我的 UI 项目使用该服务。总的来说,我真的很喜欢 Entity Framework,尤其是 Code-First 选项为我提供了比 LINQ 更精简的实体的方式,除非我正在努力克服这个特殊的障碍。

基本上,您为服务层(库项目)创建一个接口并使用依赖注入来注入该服务。 ASP.NET Core 内置了依赖注入,因此我将以此为例。对于您的其他 UI 项目,这是相同的概念。唯一的障碍是学习如何将依赖注入添加到那些 UI 层,如果它们不提供的话。

我将使用 ASP.NET 核心,因为它包括开箱即用的依赖注入,这就是您提到的一个。您只需按照您所说的那样创建一个服务层,并为其提供一个接口,以便将其注入。您还将使用此接口将服务层注入到您的其他 UI 项目中。

如果您刚开始一个新项目,这通常不会有太多工作。如果您将其添加到现有项目中,可能需要进行大量重构才能实现所需的架构。请务必按照深思熟虑的步骤进行重构,因为如果您的 UI 层已经直接依赖于数据访问层。

首先,您必须选择存储连接字符串、API 键和其他配置要求的位置。 一种选择是使用用户机密。请参阅帮助:https://docs.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-5.0&tabs=windows 您也可以找到其他选项并了解它们的实现。例如 Azure Key Vault。

此示例(项目)需要这些图层:

  • UI层
  • 业务层(你可能有一个或多个,所以最好有一个 合同层也是如此)
  • 合同层(以便各种 UI 层和各种业务层可以使用它)
  • 数据访问层(DbContext 使用 Entity Framework 核心,可以是您选择的任何数据访问技术)

UI 层将调用业务层并使用来自合同层的 DTO 来回传递数据。业务层需要将 DTO 转换为数据访问层使用的对象或实体。这使层分离。

然后您必须将业务库注入您的 UI 项目。 ASP.NET Core 中的关键是在 Starup.cs 中注册你的库配置选项的一个实例,这样,当库被注入到 UI 项目中时,库本身可以被注入一个对象,为它提供向下传递到数据访问层所需的信息,例如连接字符串。如果需要某些设置,您还可以使用此对象来配置您的业务层。比如传递一个APIKey给另一个服务层。

Startup.cs中的IConfiguration实例将加载Providers(这是Configuration对象上的一个属性,创建一个断点,你可以看到它加载了什么),其中一个Providers将是您的用户机密中的 JSON。它还有一个名为 GetSection 的方法,该方法 return 是来自 Microsoft.Extensions.Options 的 IOptions 实例,您可以使用它来创建您的库需要配置自身或向下传递到其他层的配置选项的实例。在此示例中,我将向数据访问层传递一个连接字符串。

要创建您的 BusinessSerivceOptions 实例,您必须注册它以便 DI 知道它。您可以通过从用户机密中读取它来执行此操作。这是通过在 Startup.cs 中的服务集合上使用 Confiure 方法来完成的。 Confirure 方法从 Microsoft.Extensions.Options 中获取 TOptions 的参数,您可以通过调用 Configuration.GetSection("name-of-section-in-user-secrets-json").

在 UI 项目中:

namespace SomeAspNetCoreUILayer
{
    public class Startup
    {
        private readonly IConfiguration _configuration;
        public Startup(IConfiguration configuration)
        {
            _configuration = configuration;
        }
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddOptions;
            services.AddTransient<IBusinessServiceFactory, BusinessServiceFactory>()
            services.Configure<BusinessServiceOptions>(_configuration.GetSection(nameof(BusinessServiceOptions)));
            // ... The rest of your ConfigureServices code.
        }
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 
        {
            // ... Your Configure code.
        }
    }
}

这就是结果。当然,您应该在这里使用 ViewModel 而不是 DTO,但我不会深入探讨:

namespace SomeAspNetCoreUiLayer.Controllers
{
    public class PersonController : Controller
    {
        private readonly BusinessServiceFactory _businessServices;
        public PersonController(IBusinessServiceFactory businessServiceFactory)
        {
            _businessServices = businessServiceFactory;
        }
        public IActionResult GetPerson(string personId)
        {
            PersonDTO = _businessService.SomeService.GetPerson(personId);
            return View(PersonDTO);
        }
    }
}

该库必须有一个 class,它模拟您告诉 GetSection 查找的用户机密部分中的结构,以便它可以在由 services.Configure 注册时序列化到该对象中().

在您的用户机密中:

{
    "BusinessServiceOptions": {
        "ConnectionStrings": {
            "MyDatabaseConnectionStringName": "...your connections string"
        },
        "ApiKeys": {
            "MyApiKey": "...your Api Key",
            "MyApiOtherKey": "...your other Api Key"
        }
}

在商业图书馆项目中:

namespace MyBusinessLibrary.Options
{
    public class BusinessServiceOptions
    {
        public ServiceConnectionStrings ConnectionStrings { get; set; }
        public ServiceApiKeys ApiKeys { get; set; }
        public class ServiceConnectionStrings
        {
            public string MyDatabaseConnectionStringName { get; set; }
        }
        public class ServiceApiKeys
        {
            public string MyApiKey { get; set; }
            public string MyOtherApiKey { get; set; }
        }
    }
}
namespace MyBusinessLibrary.Interface
{
    public interface IBusinessServiceFactory
    {
        BusinessServiceOptions Options { get; }
        SomeBusinessService SomeBusinessService { get; }
        SomeBusinessOtherService SomeBusinessOtherService { get; }
    }
    public class BusinessServiceFactory : IBusinessServiceFactory
    {
        private readonly BusinessServiceOptions _options;
        // This is so that the Configure<T>() can register the service in Startup.cs
        public BusinessServiceFactory(IOptions<BusinessServiceOptions> options)
        { 
            _options = options.Value;
        }
        // This will allow you to pass this BusinessServiceFactory instance to 
        // the service itself so it has access to the factory to call other
        // services if it needs them. (Without needing to implement IOptions in the services).
        public BusinessServiceFactory(BusinessServiceOptions options)
        {
            _options = options;
        }
        BusinessServiceOptions Options => _options;
        public SomeBusinessService SomeBusinessService => new SomeBusinessService(_options);
        public SomeBusinessOtherService SomeBusinessOtherService => new SomeBusinessOtherService(this);
    }
}
namespace MyBusinessLibrary.Services
{
    // Consider creating a BusinessServiceBase class and passing the passing options to the
    // base() instead, so that you don't have to write this code in every service that needs
    // access to the DbContext.
    public class SomeBusinessService
    {
        private readonly SomeDatabaseDbContext _SomeDatabase;
        public SomeBusinessService(BusinessServiceOptions options)
        {
            var SomeDatabaseDbContextOptionsBuilder = SqlServerDbContextOptionsExtensions.UseSqlServer(
            new DbContextOptionsBuilder<SomeDatabaseDbContext>(),
            options.ConnectionStrings.MyDatabaseConnectionStringName,
            sqlServerOptions => sqlServerOptions.CommandTimeout((int)TimeSpan.FromMinutes(1).TotalSeconds));
            _SomeDatabase = new SomeDatabaseDbContext(SomeDatabaseDbContextOptionsBuilder.Options);
        }
        private PersonDTO GetPerson(string personId)
        {
            // Person will be the an Entity Framework Core class.
            Person person = _SomeDatabase.Person.Where(x => x.Id = personId).FirstOrDefault();
            // PersonDTO will be from your Contracts project so that the UI layer doesn't depend on the database layer classes.
            PersonDTO = new PersonDTO() { Id = person.Id, Name = person.Name }
            return PersonDTO;
        }
    }
}

在数据库库项目中:

namespace MyDatabaseLayer.DAL.Models
{
    public partial class SomeDatabaseDbContext : DbContext
    {
        public SomeDatabaseDbContext(DbContextOptions<SomeDatabaseDbContext> options)
            : base(options)
        {
        }
        public virtual DbSet<Person> Person { get; set; }
        // .. The rest of your DbContext code.
    }
    public partial class Person
    {
        public string Id { get; set; }
        public string Name { get; set; }
    }
}

在合同库项目中:

namespace MyContractsProject.DataTransferObjects
{
    public class PersonDTO
    {
        public string Id { get; set; }
        public string Name { get; set; }
    }
}

解决您对通过此答案中的 UI 启动代码传递连接字符串的担忧:

我觉得这个答案解决了您问题中的所有原始问题。

  • 避免在您的 UI 项目中添加对 Entity Framework 的引用
  • 删除 UI 配置文件 中的连接字符串知识,方法是将它们取出并使用外部秘密存储
  • 从 UI
  • 中消除对 DAL 使用的数据访问技术知识的关注
  • 不依赖于从 Web 服务或任何其他外部服务请求连接字符串。

问题不应该是“我们能否避免通过 UI 通过服务层配置选项的依赖注入来传递连接字符串。”答案是肯定的。真正的问题应该是,“我们什么时候应该做,什么时候应该做,我们应该怎么做?什么时候不应该做,我们有什么选择?”

让我们多想想这个过程。我想您会同意 UI 应该能够配置它使用的库。可以理解,您不喜欢它传递连接字符串。

但是,让我们站在图书馆一边,考虑一下您不使用 Web 服务的请求。共有三个选项。库被传递、请求或包含字符串。

您永远不想硬编码,所以包含已经过时了。

库不应该依赖于调用它的人,也不应该知道它与连接字符串的位置关系。如果这是一项要求,那么图书馆的消费者必须知道这一点并以这种方式构建。基本上,消费者必须了解库的逻辑以获取其依赖项。因为这耦合了 UI 和库,所以库不应该预测连接字符串的位置。

应该向库传递字符串或说明如何通过配置选项查找字符串。在您的问题中,您说您不想使用外部服务。但是假设你做到了。即使图书馆使用外部服务来请求该信息,图书馆也需要告诉该外部服务要查找哪个字符串,以便它可以 return 它。如果识别字符串的信息没有硬编码或传递给图书馆以便请求服务,图书馆将如何知道要请求什么字符串?一个库需要一个应用程序来实例化它的对象。如果您不希望 UI 或其依赖库之一将其传入,您没有更合适的选择。

我唯一能想到的就是设计一个应用程序,所有 UI 和库都依赖于 运行 来启动它们,而这种依赖性会将所有东西结合在一起,这违背了目的解耦你的应用程序层。现在,您的 UI 或它的某些库将无法独立于“统领一切的单一应用程序”工作。

看来您可能认为您正在学习的设计模式是硬性规定。成为一名软件工程师既是一门硬科学,也是一门实用的艺术。作为开发人员和工程师,我们需要关注的重要事情是理解模式并根据实际需要实现适当的设计。