有没有一种方法可以避免在我的UI项目中添加对实体框架的引用,而不使用web服务作为中间人



我正在想办法让我的UI类库,无论是Win Forms、WPF、ASP.NET MVC等,都不了解实体框架或其配置文件中的连接字符串。基本上,我希望我的UI项目不关心我的数据访问项目使用的数据访问技术。我不想让它担心它是否使用LINQ、实体框架、基本的ADO.NET或其他什么。

到目前为止,我知道实现这一点的唯一方法是使用web服务,并让我的UI项目使用该服务。总的来说,我真的很喜欢实体框架,尤其是代码优先选项给我的实体比LINQ多得多,只是我正在努力克服这个特殊的障碍。

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

我将使用ASP.NET Core,因为它包括开箱即用的依赖项注入,这就是您提到的。您只需如前所述创建一个服务层,并为其提供一个接口,以便可以对其进行注入。您还将使用此接口将服务层注入到其他UI项目中。

如果你正在开始一个新项目,这通常不是太多的工作。如果将其添加到现有项目中,可能需要进行大量的重构才能实现所需的体系结构。一定要按照深思熟虑的步骤进行重构,因为如果UI层已经直接依赖于数据访问层,那么在重构过程中可能会发现很多设计缺陷,必须对这些缺陷进行更改才能创建这种抽象。

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

这个例子(项目)需要这些层:

  • UI层
  • 业务层(你可能有一个或多个这样的,所以有一个合同层)
  • 合同层(这样各个UI层和各个业务层都可以使用它)
  • 数据访问层(使用实体框架核心的DbContext,可以是您选择的任何数据访问技术)

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

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

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

要创建BusinessServceOptions的此实例,您必须注册它,以便DI了解它。您可以通过从用户机密中读取它来完成此操作。这是通过在Startup.cs中的服务集合上使用Confure方法来完成的。Confirure方法从Microsoft.Extension.Options中获取一个TOptions参数,您可以通过调用Configuration.GetSection("用户机密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.
}
}
}

这就是结果。当然,你应该在这里使用ViewModels而不是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查找的"用户机密"部分中的结构进行建模,以便在服务注册时可以将其序列化到该对象中。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或它的一些库将无法独立于";统治所有人的唯一应用程序">

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

最新更新