我应该像这样将服务工厂注入我的 Blazor 页面吗?

Posted

技术标签:

【中文标题】我应该像这样将服务工厂注入我的 Blazor 页面吗?【英文标题】:Should I inject service factories into my Blazor pages like this? 【发布时间】:2021-11-06 12:12:10 【问题描述】:

我有一个看起来像这样的 Blazor 页面。

@inject IMyService MyService

<input value=@myValue @onchange="DoSomethingOnValueChanged">

@code


   private string myValue;

   private async Task DoSomethingOnValueChanged()
   
      var myValue = await this.MyService.GetData(this.myValue);

      if (myValue != null)
      
         myValue.SomeField = "some new value";
         await this.MyService.SaveChanges();
      
   


服务类如下所示:

public class MyService : IMyService

   private MyContext context;

   public MyService(MyContext context)
     
      this.context = context;
   

   public async Task<MyObject> GetData(string id)
   
      return await this.context.MyDataObjects.FirstOrDefaultAsync(p => p.Id == id);
   
 
   public async Task SaveChanges()
   
      await this.context.SaveChangesAsync();
   

当用户更改文本框中的值时,我使用我的服务类来获取一些数据,对其进行更新,然后使用实体框架上下文将其保存到数据库中。数据库操作很快(最多需要几秒钟),但用户可以在第一个值完成处理之前输入第二个值,然后开始第二次调用 GetData 和 @987654324 @ 第一个仍在处理中。

其中一个问题是这会导致异常A second operation was started on this context before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext.

我可以通过将上下文工厂注入MyService 构造函数并在每次发出请求时创建一个新上下文来解决这个问题,就像这样

public class MyService 

   private MyContext context;

   private IDbContextFactory<MyContext> contextFactory;

   public MyService(MyContext contextFactory)
     
      this.contextFactory = contextFactory;
   

   public async Task MyObject GetData(string id)
   
      this.context?.Dispose();
      this.context = this.contextFactory.CreateDbContext();
      return await this.context.MyDataObjects.FirstOrDefaultAsync(p => p.Id == id);
   
 
   public async Task SaveChanges()
   
      if (this.context != null)
      
         await this.context.SaveChangesAsync();
      
   

但是现在的问题是,如果对 GetData 的第二次调用发生在对 SaveChanges 的第一次调用发生之前,则原始数据所附加到的上下文已被释放,因此 SaveChanges 在第一个值上会失败。

另一个问题是,在实际程序中,MyService 有几个注入的子服务依赖项,它们也使用实体框架上下文。 MyService 的这么多方法会导致其子服务也实例化新的实体框架上下文,这会导致同样的问题,即某些数据仍在处理中,但其拥有的上下文已被释放。

为了解决这个问题,我决定每次调用页面的DoSomethingOnValueChanged 方法时都实例化一个新服务。该代码如下所示:

public interface ITypeFactory<T>

   Func<T> CreateFunction  get; set; 
   
   T Create();


public class TypeFactory<T> : ITypeFactory<T>

    public Func<T> CreateFunction  get; set; 

    public T Create()
    
        return CreateFunction();
    

Startup.cs

public void ConfigureServices(IServiceCollection services)

   services.AddTransient(f =>
   
      ITypeFactory<IMyService> factory = ActivatorUtilities.CreateInstance<TypeFactory<IMyService>>(f);
      factory.CreateFunction = () => ActivatorUtilities.CreateInstance<MyService>(f);
      return factory;
   

然后我将该工厂注入我的 Razor 页面并在每次调用 DoSomethingOnValueChanged 时创建一个新的 MyService 实例。

这个系统有效,它​​避免了同一个上下文被同时使用两次的问题,但我担心如果服务开始有很多依赖项,那么频繁地创建一个新服务会导致明显的性能损失注入其中,或者上下文中的模型配置量变得非常大。

    现在,创建服务实例似乎需要大约 0.05 秒。对于具有复杂依赖关系图的服务或大量 EF 上下文配置,这会大大降低性能吗? 这种服务工厂系统是一种合理的处理方式吗?当我搜索这个时,我只能找到有关使用 DbContext 工厂的信息,并且我还没有找到任何讨论使用工厂来提供使用 DbContexts 的服务的信息 有没有更好的方法来处理这个问题?

【问题讨论】:

【参考方案1】:

像这样实例化服务不是很干净。您可以执行以下操作:

    在每个方法中都使用 dbcontext factory。请注意,上下文现在限定为方法。 'using' 关键字会在方法退出后立即处理上下文,因此您不必手动检查。

    public async Task<MyObject> GetData(string id)
    
       using var ctx = this.contextFactory.CreateDbContext();
       return await ctx.MyDataObjects.FirstOrDefaultAsync(p => p.Id == id);
    
    

    将实体传递给更新方法。使用 dbcontext Update() 更新并保存更新的实体

    public async Task SaveChanges(MyObject obj)
    
       using var ctx = this.contextFactory.CreateDbContext();
       ctx.Update(obj);
       await ctx.SaveChangesAsync();
    
    

这应该可以解决您的问题。您不需要手动实例化服务并解决随之而来的复杂性。 需要记住的几件事: Blazor 主要适用于断开连接的场景。因此,您将负责管理状态以及由此产生的并发问题。在此处阅读有关断开连接的情况:https://docs.microsoft.com/en-us/ef/core/saving/disconnected-entities

您可以在 blazor 组件中直接使用 DbContext。在此处阅读 OwningComponentBase:https://docs.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-5.0&pivots=server#utility-base-component-classes-to-manage-a-di-scope-1

【讨论】:

感谢您的回答。关于在每个服务的方法中创建新上下文,我担心的一件事是我有一个操作 ProcessData 调用数十个 Add/Get/Save 服务方法来完成。在那种情况下,当我调用ProcessData 时,我会实例化几十个新的上下文。你会怎么处理呢? 使用OwningComponentBase,您是否建议每次我需要一个新的服务实例来处理用户输入时调用GetRequiredService 1. DbContext 应该是轻量级的,易于创建和处理。我认为您不必太担心必须创建许多 DbContext 实例。事实上,许多人建议 DbContext 应尽可能短。专注于对工作单元操作的正确抽象,而不是担心上下文创建。 2. OwningComponentBase 的存在是因为您希望 DbContext 的生命周期与组件的生命周期相匹配。所以你只创建一次(初始化)。请注意并发性。 我对 OwningComponentBase 限制生命周期感到困惑。将服务注册为瞬态并将其注入组件是否具有相同的效果(无论如何在服务器端)? 瞬态和作用域在 Blazor 服务器中很混乱。问题是该请求不存在“HTTP API 请求”。一切都在范围内,因为 SignalR 连接始终处于打开状态。来自文档 “在 ASP.NET Core 应用程序中,作用域服务通常限定为当前请求。请求完成后,DI 系统会释放任何作用域或临时服务。在 Blazor Server 应用程序中,请求作用域持续在客户端连接期间,这可能会导致瞬态和范围服务的寿命比预期的长得多..."【参考方案2】:

我会使用Semamphore 来防止同时调用数据库。那么你就不需要创建一个新的上下文了。

起初我也尝试过,并成功实现了 DbContextFactory - 尽管我不再收到“...第二次操作已开始...”错误,但我意识到我需要更改跟踪(通过单个上下文) 跨不同的服务和组件,以防止数据库不一致问题。

在我的一个组件中,我有多个输入字段,它们必须在@onfocusout 触发更新功能。如果用户通过按住 Tab 键在字段之间快速跳转,则下面的信号量方法将“堆叠”所有更新操作并一个接一个地完成它们。

     @code 
            static Semaphore semaphore;
            //code ommitted for brevity
            
             private async Task DoSomethingOnValueChanged()
             
                 
                try
                
                    //First open global semaphore
                    if (!Semaphore.TryOpenExisting("GlobalSemaphore", out semaphore))
                    
                         semaphore = new Semaphore(1, 1, "GlobalSemaphore");
                    
    
                    while (!semaphore.WaitOne(TimeSpan.FromTicks(1)))
                    
                        await Task.Delay(TimeSpan.FromSeconds(1));
                    
                    //If while loop is exited or skipped, previous service calls are completed.
                    var myValue = await this.MyService.GetData(this.myValue);
                    if (myValue != null)
                    
                        myValue.SomeField = "some new value";
                        await this.MyService.SaveChanges();
                         
                 
                 
                 finally
                 
                    try
                    
                        semaphore.Release();
                    
                    catch (Exception ex)
                    
                        Console.WriteLine("ex.Message");
                    
                
            

【讨论】:

感谢您的回答,这是一个很好的解决方案。您将信号量放在 Blazor 组件中(而不是放在服务类中),对吗? @Ben - 是的,这是正确的,信号量在 Blazor 组件的方法中使用,它调用注入的服务 ? 但是,由于信号量在全球范围内工作,您可能可以也可以直接在您的所有服务中使用它。我没有对此进行测试,但它也应该可以工作:)

以上是关于我应该像这样将服务工厂注入我的 Blazor 页面吗?的主要内容,如果未能解决你的问题,请参考以下文章

双向数据绑定到单例服务Blazor服务器端

如何制作 Blazor 页面互斥锁

将组件的依赖注入方法移动到 Blazor 服务器端中的分离 CS 库

如何将 Blazor 中的所有注册服务添加到 Simple Injector?

在 Blazor 服务器中使用数据库上下文工厂

Blazor - 从 C# 类调用 JavaScript