自定义页面过滤器中的 ASP .NET Core 注入服务

Posted

技术标签:

【中文标题】自定义页面过滤器中的 ASP .NET Core 注入服务【英文标题】:ASP .NET Core Inject Service in custom page filter 【发布时间】:2019-12-23 16:23:44 【问题描述】:

我设置了一个过滤器来处理特定文件夹和其中的所有页面。我需要使用声明访问数据库。问题是我似乎无法在启动服务上向 DI 注册我的过滤器,因为它找不到数据库连接

services.AddMvc()
         .AddRazorPagesOptions(options =>
         
             options.AllowAreas = true;
             options.Conventions.AuthorizeAreaFolder("Administration", "/Account");
             options.Conventions.AuthorizeAreaFolder("Production", "/Account");
             options.Conventions.AuthorizeAreaFolder("Robotics", "/Account");
             options.Conventions.AddAreaFolderApplicationModelConvention("Production", "/FrontEnd", 
                 model => model.Filters.Add(
                     new LockdownFilter(
                         new ProducaoRegistoService(new ProductionContext()), 
                         new UrlHelperFactory(), 
                         new HttpContextAccessor())));
         )

过滤器。

public class LockdownFilter : IAsyncPageFilter

    private readonly IProducaoRegistoService _producaoRegistoService;
    private readonly IUrlHelperFactory _urlHelperFactory;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public LockdownFilter(IProducaoRegistoService producaoRegistoService, IUrlHelperFactory urlHelperFactory, IHttpContextAccessor httpContextAccessor)
    
        _producaoRegistoService = producaoRegistoService;
        _urlHelperFactory = urlHelperFactory;
        _httpContextAccessor = httpContextAccessor;
    

    public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context, PageHandlerExecutionDelegate next)
    
        int registoId;
        if(!int.TryParse(_httpContextAccessor.HttpContext.User.GetRegistoId(), out registoId))
        
            // TODO
        

        var registo = _producaoRegistoService.GetById(registoId);

        await next.Invoke();
    

    public async Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
    
        await Task.CompletedTask;
    

错误是

InvalidOperationException:未配置数据库提供程序 对于这个 DbContext。可以通过覆盖来配置提供程序 DbContext.OnConfiguring 方法或通过使用 AddDbContext 上 应用服务提供商。如果使用了 AddDbContext,那么也 确保您的 DbContext 类型接受 DbContextOptions 构造函数中的对象并将其传递给基构造函数 数据库上下文。

这是整个启动类

public class Startup

    public Startup(IConfiguration configuration)
    
        Configuration = configuration;
    

    public IConfiguration Configuration  get; 

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    
        services.AddAuthentication(options =>
        

        )
         .AddCookie("ProductionUserAuth", options =>
         
             options.ExpireTimeSpan = TimeSpan.FromDays(1);
             options.LoginPath = new PathString("/Production/FrontEnd/Login");
             options.LogoutPath = new PathString("/Production/FrontEnd/Logout");
             options.AccessDeniedPath = new PathString("/Production/FrontEnd/AccessDenied");
             options.SlidingExpiration = true;
             options.Cookie.Name = "NoPaper.ProductionUser";
             options.Cookie.Expiration = TimeSpan.FromDays(1);
         )
             .AddCookie("ProductionAdminAuth", options =>
             
                 options.ExpireTimeSpan = TimeSpan.FromDays(1);
                 options.LoginPath = new PathString("/Production/BackOffice/Login");
                 options.LogoutPath = new PathString("/Production/BackOffice/Logout");
                 options.AccessDeniedPath = new PathString("/Production/BackOffice/AccessDenied");
                 options.SlidingExpiration = true;
                 options.Cookie.Name = "NoPaper.ProductionAdmin";
                 options.Cookie.Expiration = TimeSpan.FromDays(1);
             )
        .AddCookie("AdministrationAuth", options =>
        
            options.ExpireTimeSpan = TimeSpan.FromDays(1);
            options.LoginPath = new PathString("/Administration/Index");
            options.LogoutPath = new PathString("/Administration/Logout");
            options.AccessDeniedPath = new PathString("/Administration/AccessDenied");
            options.SlidingExpiration = true;
            options.Cookie.Name = "NoPaper.Administration";
            options.Cookie.Expiration = TimeSpan.FromDays(1);
        );

        services.AddAuthorization();

        services.AddMemoryCache();
        services.AddAutoMapper(typeof(Startup));

        services.AddMvc()
         .AddRazorPagesOptions(options =>
         
             options.AllowAreas = true;
             options.Conventions.AuthorizeAreaFolder("Administration", "/Account");
             options.Conventions.AuthorizeAreaFolder("Production", "/Account");
                           options.Conventions.AddAreaFolderApplicationModelConvention("Production", "/FrontEnd", 
                 model => model.Filters.Add(
                     new LockdownFilter(
                         new ProducaoRegistoService(new ProductionContext(new DbContextOptions<ProductionContext>())), 
                         new UrlHelperFactory(), 
                         new HttpContextAccessor())));
         )
         .AddNToastNotifyToastr(new ToastrOptions()
         
             ProgressBar = true,
             TimeOut = 3000,
             PositionClass = ToastPositions.TopFullWidth,
             PreventDuplicates = true,
             TapToDismiss = true
         )
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

        services.AddRouting(options =>
        
            options.LowercaseUrls = true;
            options.LowercaseQueryStrings = true;
        );

        services.AddDbContext<DatabaseContext>(options =>
        
            options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), sqlServerOptionsAction: sqlOptions =>
            
                sqlOptions.EnableRetryOnFailure(
                    maxRetryCount: 2,
                    maxRetryDelay: TimeSpan.FromSeconds(1),
                    errorNumbersToAdd: null);
                sqlOptions.MigrationsHistoryTable("hEFMigrations", "Admin");
            );
        );

        services.AddDbContext<ProductionContext>(options =>
          options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), c => c.MigrationsHistoryTable("hEFMigrations", "Admin")
     ));

        services.AddHttpContextAccessor();
        services.AddSingleton<IFileProvider>(new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot/files")));
        services.AddTransient<IAuthorizationHandler, HasArranqueActivoHandler>();
        services.AddTransient<IAuthorizationHandler, HasArranqueInactivoHandler>();
        services.AddTransient<IAuthorizationHandler, IsParagemNotOnGoingHandler>();
        services.AddTransient<IAuthorizationHandler, IsParagemOnGoingHandler>();


        services.AddTransient<Services.Interfaces.IUserService, Services.UserService>();

        #region AreaProduction
        services.AddTransient<Production.Interfaces.IComponenteService, Production.ComponenteService>();
        services.AddTransient<Production.Interfaces.IReferenciaService, Production.ReferenciaService>();
        services.AddTransient<Production.Interfaces.IProducaoRegistoService, Production.ProducaoRegistoService>();
        services.AddTransient<Production.Interfaces.IParagemService, Production.ParagemService>();
        services.AddTransient<Production.Interfaces.ICelulaService, Production.CelulaService>();
        services.AddTransient<Production.Interfaces.IUapService, Production.UapService>();
        services.AddTransient<Production.Interfaces.ICelulaTipoService, CelulaTipoService>();
        services.AddTransient<Production.Interfaces.IMatrizService, MatrizService>();
        services.AddTransient<Production.Interfaces.IOperadorService, Production.OperadorService>();
        services.AddTransient<Production.Interfaces.IEtiquetaService, Production.EtiquetaService>();
        services.AddTransient<Production.Interfaces.IPokayokeService, Production.PokayokeService>();
        services.AddTransient<Production.Interfaces.IGeometriaService, Production.GeometriaService>();
        services.AddTransient<Production.Interfaces.IEmpregadoService, Production.EmpregadoService>();
        services.AddTransient<Production.Interfaces.IPecaService, Production.PecaService>();
        services.AddTransient<Production.Interfaces.IDefeitoService, Production.DefeitoService>();
        services.AddTransient<Production.Interfaces.ITurnoService, Production.TurnoService>();
        #endregion


    

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    
        if (env.IsDevelopment())
        
            app.UseDeveloperExceptionPage();
        
        else
        
            app.UseExceptionHandler(errorApp =>
            
                errorApp.Run(async context =>
                
                    var exceptionHandlerPathFeature =
                        context.Features.Get<IExceptionHandlerPathFeature>();

                    // Use exceptionHandlerPathFeature to process the exception (for example, 
                    // logging), but do NOT expose sensitive error information directly to 
                    // the client.

                    if (exceptionHandlerPathFeature.Path.Contains("/Administration/") ||
                        exceptionHandlerPathFeature.Path.Contains("/administration/"))
                    
                        context.Response.Redirect("/Administration/Error");
                    

                    if (exceptionHandlerPathFeature.Path.Contains("/Production/") ||
                        exceptionHandlerPathFeature.Path.Contains("/production/"))
                    
                        context.Response.Redirect("/Production/Error");
                    
                );
            );
        

        app.UseNToastNotify();
        app.UseAuthentication();

        app.UseStaticFiles();

        app.UseMvc(routes =>
        
            routes.MapRoute(
            name: "areas",
            template: "area:exists/controller=Home/action=Index/id?"
          );

            routes.MapRoute(
                name: "default",
                template: "controller=Home/action=Index/id?");
        );
    

我的背景

 public class ProductionContext : DbContext

    //static LoggerFactory object
    public static readonly ILoggerFactory loggerFactory = new LoggerFactory(new[] 
          new ConsoleLoggerProvider((_, __) => true, true)
    );

    public ProductionContext()
    

    

    public ProductionContext(DbContextOptions<ProductionContext> options) : base(options)
    

    

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    
        optionsBuilder.UseLoggerFactory(loggerFactory)  //tie-up DbContext with LoggerFactory object
            .EnableSensitiveDataLogging();
    
 ...

【问题讨论】:

【参考方案1】:

您的问题中有很多代码,所以我将首先突出显示感兴趣的代码:

options.Conventions.AddAreaFolderApplicationModelConvention("Production", "/FrontEnd", 
    model => model.Filters.Add(
        new LockdownFilter(
            new ProducaoRegistoService(new ProductionContext()), 
            new UrlHelperFactory(), 
            new HttpContextAccessor())));

现在,让我们再看看错误信息:

InvalidOperationException:没有为此 DbContext 配置数据库提供程序。可以通过重写 DbContext.OnConfiguring 方法或在应用程序服务提供者上使用 AddDbContext 来配置提供者。如果使用了 AddDbContext,那么还要确保您的 DbContext 类型在其构造函数中接受 DbContextOptions 对象并将其传递给 DbContext 的基本构造函数。

在您的情况下,在我调用的代码中创建的ProductionContext 实例没有被配置。您可能会认为它正在配置,因为您在 ConfigureServices 方法的其他地方使用了 AddDbContext,但事实并非如此。

AddDbContext 设置 DI 所需的一切,为您提供一个根据您的设置配置的 ProductionContext 实例(使用带有DefaultConnection 连接字符串的 SQL Server)。但是,通过创建您自己的 ProductionContext 实例并将其传递给过滤器,DI 配置的实例根本不会被使用。

一个明显的解决方案是为这些服务使用 DI,但这并不是那么简单,因为在创建 LockdownFilter 的实例时您无法访问 DI。这就是TypeFilterAttributeServiceFilterAttribute 的用武之地,它们在Filters in ASP.NET Core: Dependency injection 中有详细记录。这是我调用的代码的更新版本,它使用TypeFilterAttribute

options.Conventions.AddAreaFolderApplicationModelConvention("Production", "/FrontEnd", 
    model => model.Filters.Add(new TypeFilterAttribute(typeof(LockdownFilter))));

使用这种方法,传入 LockdownFilter 构造函数的参数将从 DI 中解析。从您的问题中可以清楚地看出,这三个服务都已在 DI 容器中注册,所以这应该可以正常工作。

【讨论】:

以上是关于自定义页面过滤器中的 ASP .NET Core 注入服务的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET Core 3.1 WebAPI 自定义ActionFilter过滤器

自定义授权过滤器在 ASP.NET Core 3 中不起作用

ASP.NET CORE 2.2 WEB APP中的会话过期问题

十二个 ASP.NET Core 例子——过滤器

剃须刀页面上自定义验证属性的 ASP.NET Core 客户端验证

ASP.NET Core 中的按操作身份验证处理程序