ASP .NET Core Inject Service in custom page filter
Asked Answered
A

2

7

I have set a filter to work upon a specific folder and all pages inside it. I need to access database using a claim. The problem is that I cannot seem to register my filter with DI on startup services cause it does not find the database connection

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

the filter.

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

the error is

InvalidOperationException: No database provider has been configured for this DbContext. A provider can be configured by overriding the DbContext.OnConfiguring method or by using AddDbContext on the application service provider. If AddDbContext is used, then also ensure that your DbContext type accepts a DbContextOptions object in its constructor and passes it to the base constructor for DbContext.

here is the whole startup class

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

my context

 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();
    }
 ...
}
Akvavit answered 17/8, 2019 at 21:12 Comment(0)
T
7

There's a lot of code in your question, so I'll highlight the code of interest first:

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

Now, let's have another look at the error message:

InvalidOperationException: No database provider has been configured for this DbContext. A provider can be configured by overriding the DbContext.OnConfiguring method or by using AddDbContext on the application service provider. If AddDbContext is used, then also ensure that your DbContext type accepts a DbContextOptions object in its constructor and passes it to the base constructor for DbContext.

In your situation, the instance of ProductionContext that's being created in the code I called out isn't being configured. You might think it is being configured because of how you've used AddDbContext elsewhere in your ConfigureServices method, but that's not the case.

AddDbContext sets up DI with everything it needs to give you an instance of ProductionContext that is configured according to your setup (using SQL Server with the DefaultConnection connection-string). However, by creating your own instance of ProductionContext and passing that into the filter, the DI-configured instance isn't being used at all.

An obvious solution here would be to use DI for those services, but that's not so straight forward as you don't have access to DI when creating your instance of LockdownFilter. This is where TypeFilterAttribute and ServiceFilterAttribute come in, which are well-documented in Filters in ASP.NET Core: Dependency injection. Here's an updated version of the code I called out, which uses TypeFilterAttribute:

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

Using this approach, the arguments passed in to your LockdownFilter constructor will be resolved from DI. It's clear from your quesiton that the three services are all registered with the DI container, so this should work as is.

Transparent answered 17/8, 2019 at 21:44 Comment(0)
C
0

There are two types of filters that you can use - ServiceFilters or TypeFilters.

Service Filters

Service filters allow you to resolve a filter instance directly from the DI. This means that the filter must first be registered in the container and constructor injection is supported. Think of ServiceFilter as a provider of filters. In fact, ServiceFilter is an implementation of a simple IFilterFactory interface. The role of a filter factory is to provide an instance of an IFilter which can be used within the MVC pipeline. The default filter provider will then attempt to cast each filter to IFilterFactory and if it succeeds, invokes the CreateInstance method, otherwise, it will simply treat the filter as a general IFilter.

In practice, the code would look like this in a controller:

[ServiceFilter(typeof(LockdownFilter))]  
[HttpGet("")]  
public IEnumerable<Item> Get()  
{  
    return this.repository.GetAll();  
} 

Your custom filter needs to be registered for the above to work. Do this in your Program or Startup class, inside ConfigureServices.

{  
    services.AddSingleton<IItemRepository, DefaultItemRepository>();  
    services.AddSingleton<LockdownFilter>();

    services.AddMvc()  
}

Type Filters

Type filters on the other hand are also implementations of IFilterFactory and can allow you to have dependencies injected into your filter.

The difference between service filters and type filters is that Types that are resolved through type filters do not get resolved directly from DI, but rather use ObjectFactory for instantiation. This allows you to use TypeFilterAttribute with filters that have not been registered with DI.

The code would look like this in a controller:

[TypeFilter(typeof(LogFilter))]  
[HttpGet("")]  
public IEnumerable<Item> Get()  
{  
    return this.repository.GetAll();  
}

Solution

An ideal solution would be to use an extension method to pass your custom filter to a type using TypeFilterAttribute so as to ensure arguments are added using DI:

model => model.Filters.Add<LockdownFilter>();

And the extension method will look as follows:

using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

public static void Add<TFilterType>(this ICollection<IFilterMetadata> filters) where TFilterType : IFilterMetadata {
    filters.Add(new TypeFilterAttribute(typeof(TFilterType)));
}
Chamfer answered 14/12, 2023 at 19:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.