我应该像这样将服务工厂注入我的 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
工厂的信息,并且我还没有找到任何讨论使用工厂来提供使用 DbContext
s 的服务的信息
有没有更好的方法来处理这个问题?
【问题讨论】:
【参考方案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 服务器端中的分离 CS 库