有没有办法避免在我的 UI 项目中添加对实体框架的引用而不使用 Web 服务作为中间人?

Posted

技术标签:

【中文标题】有没有办法避免在我的 UI 项目中添加对实体框架的引用而不使用 Web 服务作为中间人?【英文标题】: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? 【发布时间】:2021-08-06 01:43:53 【问题描述】:

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

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

【问题讨论】:

第一个问题是“为什么是障碍?”我不建议仅仅为了抽象它而抽象掉 EF。在处理数据并将其抽象出来时,EF 为您提供了强大的功能,这会导致代码过于复杂、代码性能不佳或两者兼而有之。您可以做的最糟糕的事情是尝试抽象出它的实现“以防万一”您以后决定可能更改为其他内容。相信我,您几乎肯定不会,并且采取措施尝试这样做很可能会导致您认为需要这样做的问题。 我的 UI 项目不应该关心数据访问层使用了哪种数据访问技术。 UI 项目应该对数据库、OEM 或数据库连接字符串一无所知。我的障碍是我试图防止这种不必要的和限制性的耦合。 EF Core 本身不需要任何配置文件。它所需要的只是填充DbContextOptions。你在哪里以及如何填充它是你的事,而不是他们关心的问题。如果您希望它位于数据访问层 - 很好,只需找到一种方法如何为数据访问层提供该信息。你可以使用任何你认为合适的东西,但无论如何都必须有某种外部可配置的存储,以防你不对连接字符串和数据库类型进行硬编码。 如果您的 UI 需要与各种数据源(无论是您自己的还是第三方的、数据库或分布式服务/代理等)通信,那么您需要定义一个表示对数据采取的操作的通用接口,以及 UI 所需数据的 DTO 定义。我的观点是除非这种抽象是你的 UI 的硬性要求,在应用程序中实现这样的抽象会在复杂性和最终性能方面付出巨大的代价,而这只不过是自我满足或“只是在案子”。仔细聆听 YAGNI 的警告声。 您的 UI 将从您的 UI 层请求数据。它不会传入连接字符串,也不会关心数据的存储位置或数据的检索方式。 【参考方案1】:

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

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

如果您是从一个新项目开始,这通常不是太多的工作。如果您要将其添加到现有项目中,则可能需要进行大量重构才能实现所需的架构。请务必在经过深思熟虑的步骤中进行重构,因为如果您的 UI 层已经直接依赖于数据访问层,您可能会发现许多设计缺陷必须进行更改才能创建这种抽象。

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

本示例(项目)将需要这些层:

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

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

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

Startup.cs 中的 IConfiguration 实例将加载 Providers(这是 Configuration 对象上的一个属性,创建一个断点,您可以看到它加载的内容),其中一个 Providers 将是您的 User Secrets 中的 JSON。它还有一个名为 GetSection 的方法,该方法从 Microsoft.Extensions.Options 返回一个 IOptions 实例,您可以使用它来创建库需要配置自身或传递给其他层的配置选项的实例。在本例中,我将向数据访问层传递一个连接字符串。

要创建您的 BusinessSerivceOptions 的此实例,您必须注册它以便 DI 知道它。您可以通过从 User Secrets 中读取它来做到这一点。这是通过对 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);
        
    

该库必须具有一个类,该类对您告诉 GetSection 查找的 User Secrets 部分中的结构进行建模,以便在通过 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 项目中添加对实体框架的引用 删除UI 配置文件中的连接字符串知识,方法是取出它们并使用外部秘密存储 从 UI 中消除了对 DAL 使用的数据访问技术知识的担忧 不依赖于从 Web 服务或任何其他外部服务请求连接字符串。

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

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

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

你永远不想硬编码,所以包含已被淘汰。

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

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

我唯一能想到的就是设计一个应用程序,让所有 UI 和库都依赖它来运行并启动它们,并且这种依赖关系会将所有东西耦合在一起,这违背了解耦应用程序层的目的。现在,您的 UI 或其中的一些库将无法独立于“一个应用程序来统治它们”。

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

【讨论】:

我很欣赏详细而翔实的回复。但是,我对以下声明感到担忧。 “库本身可以注入一个对象,该对象为其提供传递给数据访问层所需的信息,例如连接字符串。”在我看来,UI 层应该只从数据访问层请求数据,而不是提供连接字符串或任何数据访问任务的详细信息。你的 UI 层不应该知道什么是连接字符串,就像数据访问层不应该知道什么是 TextBox。 具体来说,我不喜欢不允许 UI 不关心数据访问库使用实体框架这一事实的方式。说它可以传入依赖是一回事。说UI层需要对数据访问技术的依赖完全是另一回事。 深入了解我的回答,UI 完全不知道 DAL 中正在实施什么数据技术。 请看我答案底部的编辑。我添加它是为了解决您对在将库注册到依赖注入实现之前使用信息注册库配置对象的担忧。 我相信我准确地回答了您的原始问题,并且它将帮助任何试图回答该特定问题的人。看来我的回答已经引出了您想要探索的真正主题。如果您同意问题已更改,如果您认为我回答了原始问题,您可能希望将其标记为已回答,这可能对其他人有所帮助,并打开一个新线程,您可以在其中改写您的问题,我们可以讨论您的任何进一步细节可能还有。看起来我们现在正在研究一个不同的话题。这不再是关于 UI 中的 EF 依赖。

以上是关于有没有办法避免在我的 UI 项目中添加对实体框架的引用而不使用 Web 服务作为中间人?的主要内容,如果未能解决你的问题,请参考以下文章

带有 Ntier 的实体框架

我如何避免为实体框架添加哈希到我的实体名称

有没有办法找出哪个表列导致与实体框架的转换出错?

避免在 C++/CLI 项目中加载 .Net Dlls?

实体框架代码优先 - 通过主键将子实体添加到父实体

有没有办法使用 Dapper 将列名映射到我的实体的属性?