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

Should I inject service factories into my Blazor pages like this?

我有一个看起来像这样的 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();
      }
   }

}

服务 class 如下所示:

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();
   }
}

当用户更改文本框中的值时,我使用我的服务 class 获取一些数据,更新它,然后使用 Entity Framework 上下文将其保存到数据库中。数据库操作很快(最多只需要几秒钟),但用户可以在第一个值完成处理之前输入第二个值,然后启动对 GetData 的第二次调用和SaveChanges 第一个仍在处理中。

其中一个问题是这会导致异常 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有几个子服务依赖被注入,它们也使用了Entity Framework上下文。 MyService 的这么多方法会导致它的子服务也实例化新的 Entity Framework 上下文,这导致同样的问题,一些数据仍在处理中,但它拥有的上下文已被处置。

为了解决这个问题,我决定在每次调用页面的 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. 的服务的信息
  4. 有没有更好的方法来处理这个问题?

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

  1. 在每个方法中使用 dbcontext 工厂。请注意,上下文现在仅限于该方法。 '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

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

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

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

     @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");
                    }
                }
            }