.NET7 Blazor WASM中避免BuildServiceProvider警告的服务访问方案
我有一个Blazor WASM项目,使用外部Identity Server服务完成用户认证与登录。在用户完成认证、重定向至Blazor应用之前,需要查询并填充一系列应用专属的claims。目前在服务器端Program.cs文件中,通过AddOpenIdConnect提供的OnUserInformationReceived事件实现该逻辑,但代码中using (ServiceProvider serviceProvider = builder.Services.BuildServiceProvider())处收到如下警告:
Calling 'BuildServiceProvider' from application code results in an additional copy of singleton services being created. Consider alternatives such as dependency injecting services as parameters to 'Configure'.
使用框架为.NET7,完整Program.cs代码如下:
public partial class Program { public static void Main(string[] args) { var builder = WebApplication.CreateBuilder(args); var CommonConnectionString = builder.Configuration.GetConnectionString("CommonConnection"); builder.Services.AddDbContext<CommonDbContext>(options => { options.UseSqlServer(CommonConnectionString); options.EnableSensitiveDataLogging(true); }); builder.Services.AddControllersWithViews(); builder.Services.AddRazorPages(); builder.Services.AddBff().AddServerSideSessions(); builder.Services.AddScoped<IUnitOfWork, UnitOfWork>(); builder.Services.AddScoped<Hermes.Services.User.IUserService, UserService>(); builder.Services.AddAuthentication(options => { options.DefaultScheme = "cookie"; options.DefaultChallengeScheme = "oidc"; options.DefaultSignOutScheme = "oidc"; }) .AddCookie("cookie", options => { options.Cookie.Name = "__Host-blazor"; options.Cookie.SameSite = SameSiteMode.Strict; options.ExpireTimeSpan = TimeSpan.FromMinutes(15); options.SlidingExpiration = false; }) .AddOpenIdConnect("oidc", options => { options.Authority = "https://localhost:5001"; options.ClientId = "bff"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("offline_access"); options.Scope.Add("email"); options.MapInboundClaims = false; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.Events = new OpenIdConnectEvents { OnUserInformationReceived = async ctx => { var sub = ctx.Principal.Claims.FirstOrDefault(c => c.Type == "sub").Value; using (ServiceProvider serviceProvider = builder.Services.BuildServiceProvider()) { var userService = serviceProvider.GetRequiredService<Hermes.Services.User.IUserService>(); var claims = await userService.GetUserDetails(sub); var appIdentity = new ClaimsIdentity(claims); ctx.Principal.AddIdentity(appIdentity); } } }; }); var QualityAssuranceConnectionString = builder.Configuration.GetConnectionString("QualityAssuranceConnection"); builder.Services.AddDbContext<QualityAssuranceDbContext>(options => { options.UseSqlServer(QualityAssuranceConnectionString); options.EnableSensitiveDataLogging(true); }); builder.Services.AddScoped<IUnitOfWorkQA, UnitOfWorkQA>(); builder.Services.AddScoped<IQualityAssuranceService, QualityAssuranceService>(); var app = builder.Build(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseWebAssemblyDebugging(); } else { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseBlazorFrameworkFiles(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseBff(); app.UseAuthorization(); app.MapBffManagementEndpoints(); app.MapRazorPages(); app.MapControllers(); app.MapFallbackToFile("index.html"); app.Run(); } }
优化方案
方案1:直接使用HttpContext内置服务容器
OnUserInformationReceived上下文的HttpContext已经提供了请求级的服务容器,无需手动构建ServiceProvider,直接获取所需服务即可:
修改后的OnUserInformationReceived事件代码:
options.Events = new OpenIdConnectEvents { OnUserInformationReceived = async ctx => { var sub = ctx.Principal.Claims.FirstOrDefault(c => c.Type == "sub")?.Value; if (string.IsNullOrEmpty(sub)) return; // 从HttpContext获取已初始化的服务实例 var userService = ctx.HttpContext.RequestServices.GetRequiredService<Hermes.Services.User.IUserService>(); var claims = await userService.GetUserDetails(sub); var appIdentity = new ClaimsIdentity(claims); ctx.Principal.AddIdentity(appIdentity); } };
这种方式完全遵循依赖注入最佳实践,不会创建额外的单例服务副本,直接消除警告,同时代码更简洁。
方案2:抽离事件处理类(解耦实现)
如果希望事件逻辑与Program.cs解耦,可将事件处理逻辑封装为独立类,通过构造函数注入所需服务:
1. 创建自定义事件处理类
public class CustomOpenIdConnectEvents : OpenIdConnectEvents { private readonly Hermes.Services.User.IUserService _userService; // 构造函数注入依赖 public CustomOpenIdConnectEvents(Hermes.Services.User.IUserService userService) { _userService = userService; } public override async Task UserInformationReceived(UserInformationReceivedContext context) { var sub = context.Principal.Claims.FirstOrDefault(c => c.Type == "sub")?.Value; if (string.IsNullOrEmpty(sub)) return; var claims = await _userService.GetUserDetails(sub); var appIdentity = new ClaimsIdentity(claims); context.Principal.AddIdentity(appIdentity); await base.UserInformationReceived(context); } }
2. 注册事件处理类
在Program.cs的服务注册区域添加:
builder.Services.AddScoped<CustomOpenIdConnectEvents>();
3. 在AddOpenIdConnect中使用该类
修改AddOpenIdConnect的配置代码,改为使用注入的事件处理类:
.AddOpenIdConnect("oidc", (services, options) => { // 原有配置保持不变 options.Authority = "https://localhost:5001"; options.ClientId = "bff"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("offline_access"); options.Scope.Add("email"); options.MapInboundClaims = false; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; // 使用注入的自定义事件处理类 options.Events = services.GetRequiredService<CustomOpenIdConnectEvents>(); });
这种方式将事件逻辑与启动配置分离,符合单一职责原则,便于后续维护和扩展。
内容的提问来源于stack exchange,提问作者Hannah Hayes

