【问题标题】:Should I inject service factories into my Blazor pages like this?我应该像这样将服务工厂注入我的 Blazor 页面吗?
【发布时间】: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 实例。

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

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

【问题讨论】:

    标签: c# dependency-injection entity-framework-core blazor blazor-server-side


    【解决方案1】:

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

    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);
      }
      
    2. 将实体传递给更新方法。使用 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 组件的方法中使用,它调用注入的服务 ? 但是,由于信号量在全球范围内工作,您可能可以也可以直接在您的所有服务中使用它。我没有对此进行测试,但它也应该可以工作:)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多