Cannot get IValidator<T> of FastEndpoint validator instance from ServiceProvider in ISchemaFilter

263 views Asked by At

I am using FastEndpoint to perfom some validations in requests, so, I need to obtain a instance of IValidator<T> in order to get its properties using reflection but it returns null for some reason

SwaggerFluentValidationRules .cs

using FluentValidation;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using PakEnergy.Services.SharedKernel.Request;
using PakEnergy.Services.SharedKernel.Validator;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace PakEnergy.Services.SharedKernel.Swagger;

public class SwaggerFluentValidationRules : ISchemaFilter
{
    private readonly IServiceProvider _serviceProvider;

    public SwaggerFluentValidationRules(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public void Apply(OpenApiSchema model, SchemaFilterContext context)
    {
        
        var genericType = typeof(IValidator<>).MakeGenericType(context.Type);
        // validator is always null
        var validator = _serviceProvider.GetService(genericType) as IValidator; 

        // More logic here
    }
}

Startup.cs

namespace PakEnergy.Services.CompanyService.Api;

[ExcludeFromCodeCoverage]
public abstract class Startup
{
    const string CorsPolicyName = "CorsPolicy";

    public static void Run(string[] args)
    {
        var builder = WebApplication.CreateBuilder(args);

        builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
        builder.Host.UseSerilog((ctx, config) =>
            {
                var configuration = new ConfigurationBuilder().AddConfiguration(ctx.Configuration).Build();
                config.ReadFrom.Configuration(configuration);
            })
            .ConfigureContainer<ContainerBuilder>(containerBuilder =>
            {
                containerBuilder.RegisterModule(new DefaultCoreModule());
                containerBuilder.RegisterModule(new DefaultInfrastructureModule());
                containerBuilder.RegisterModule(new SharedPolicyHandlersModule());
            });

        // Extracted into it's own method
        ConfigureServices(builder);

        var app = builder.Build();
        app.UseSerilogRequestLogging();
        app.UseDefaultExceptionHandler(logStructuredException: true);

        if (app.Environment.IsEnvironment("PreProduction") || app.Environment.IsProduction())
        {
            app.UseHttpsRedirection();
        }

        if (!app.Environment.IsProduction())
        {
            app.UseSwagger();
            app.UseSwaggerUI(
                c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "PakEnergy.Services.CompanyService.Api V1"); });
        }

        app.UseCors(CorsPolicyName);
        app.UseRouting();
        app.UseFastEndpoints(c =>
        {
            c.Endpoints.RoutePrefix = "api";
            c.Endpoints.Configurator = ep =>
            {
                ep.PreProcessors(Order.Before, new IfMatchHeaderValidator<Company>());
                ep.ResponseInterceptor(new ETagResponseInterceptor());
            };

            c.Binding.ValueParserFor<ProductIdEnum>(ProductIdEnumParser.Parse);
        });

        app.UseAuthorization();

        using var scope = app.Services.CreateScope();
        var services = scope.ServiceProvider;

        var context = services.GetRequiredService<AppDbContext>();
        context.Database.Migrate();

        if (app.Environment.IsDevelopment() || app.Environment.IsEnvironment("QA"))
            app.UseDbSeeding(context);

        var separator = new string('#', 51);
        Console.WriteLine(separator);
        Console.WriteLine(
            $"### Starting PakEnergy.Services.CompanyService.Api in {builder.Environment.EnvironmentName}\t\t###");
        Console.WriteLine(separator);

        app.Run();
    }

    private static void ConfigureServices(WebApplicationBuilder builder)
    {
        var connectionString = builder.Configuration.GetConnectionString("DefaultConnection");
        
        var services = builder.Services;
        builder.Services.AddOptions();
        builder.Services.Configure<ConnectionStringSettings>(builder.Configuration.GetSection("ConnectionStrings"));
        services.AddSingleton<IConnectionStringSettings>(provider =>
            provider.GetRequiredService<IOptions<ConnectionStringSettings>>().Value);
        builder.Services.Configure<ProvisioningSettings>(builder.Configuration.GetSection("Provisioning"));
        services.AddSingleton<IProvisioningSettings>(provider =>
            provider.GetRequiredService<IOptions<ProvisioningSettings>>().Value);

        services.AddDbContext<AppDbContext>((sp, optionsBuilder) =>
        {
            var auditInterceptor = sp.GetRequiredService<EntityBaseAuditInterceptor>();
            var softDeleteInterceptor = sp.GetRequiredService<EntityBaseSoftDeleteInterceptor>();
            optionsBuilder.UseSqlServer(connectionString)
                .AddInterceptors(auditInterceptor, softDeleteInterceptor);
        });
        
        services.AddHttpContextAccessor();
        services.AddAutoMapper(typeof(CreateCompanyMapper));
        services.AddCors(options =>
        {
            options.AddPolicy(CorsPolicyName, policyBuilder =>
            {
                policyBuilder
                    .AllowAnyHeader()
                    .AllowAnyMethod()
                    .AllowCredentials()
                    .WithExposedHeaders("content-disposition", "etag")
                    .SetIsOriginAllowed(_ => true); // Allow any origin
            });
        });

        services.AddFastEndpoints();
        services.AddFastEndpointsApiExplorer();
        services.AddScoped<ISqlRunnerService, SqlRunnerService>();
        services.AddScoped<IDatabaseProvisioningService, DatabaseProvisioningService>();
        services.AddScoped<IStorageContainerProvisioningService, StorageContainerProvisioningService>();
        services.AddScoped<IProvisionStatusService, ProvisionStatusService>();
        services.AddScoped<ICompanyAccessService, CompanyAccessService>();
        services.AddScoped<ICompanyValidationService, CompanyValidationService>();
        services.AddScoped<ITaxValidationService, TaxValidationService>();
        services.AddScoped<ICompanyRepository, CompanyRepository>();
        services.AddScoped<IResourceNameGeneratorService, ResourceNameGeneratorService>();
        services.AddScoped<IBlobServiceClientFactory, BlobServiceClientFactory>();

        services.AddSwaggerGen(c =>
        {
            c.SwaggerDoc("v1",
                new OpenApiInfo { Title = "PakEnergy.Services.CompanyService.Domain.Api", Version = "v1" });
            c.EnableAnnotations();
            c.OperationFilter<FastEndpointsOperationFilter>();
            c.OperationFilter<SwaggerOperationNotRequiredParametersFilter>();
            c.DocumentFilter<SwaggerEnumDocumentFilter>();
            c.SchemaFilter<SwaggerFluentValidationRules>();
            // Include 'SecurityScheme' to use JWT Authentication
            var jwtSecurityScheme = new OpenApiSecurityScheme
            {
                BearerFormat = "JWT",
                Name = "JWT Authentication",
                In = ParameterLocation.Header,
                Type = SecuritySchemeType.Http,
                Scheme = JwtBearerDefaults.AuthenticationScheme,
                Description = "Put **_ONLY_** your JWT Bearer token on textbox below!",

                Reference = new OpenApiReference
                {
                    Id = JwtBearerDefaults.AuthenticationScheme,
                    Type = ReferenceType.SecurityScheme
                }
            };

            c.AddSecurityDefinition("Bearer", jwtSecurityScheme);

            c.AddSecurityRequirement(new OpenApiSecurityRequirement()
            {
                { jwtSecurityScheme, new List<string>() }
            });
        });

        services
            .AddAuthentication()
            .AddJwtBearer(options =>
            {
                options.Authority = builder.Configuration.GetValue<string>("Auth0:Authority");
                options.Audience = builder.Configuration.GetValue<string>("Auth0:Audience");
            });

        builder.Services.AddAuthorization(options =>
        {
            options.AddPolicy(GlobalPolicies.RequiresMatchingProductId, policy =>
            {
                policy.AddRequirements(new RequiresMatchingProductIdRequirement());
            });
            options.AddPolicy(GlobalPolicies.RequiresMatchingCompanyId, policy =>
            {
                policy.AddRequirements(new RequiresMatchingCompanyIdRequirement());
            });
        });

        // add list services for diagnostic purposes - see https://github.com/ardalis/AspNetCoreStartupServices
        services.Configure<ServiceConfig>(config =>
        {
            config.Services = new List<ServiceDescriptor>(services);
            config.Path = "listservices";
        });
    }
}

I read this document about dependency injection in FastEndpoint validators but it doesn't contain information for my user case

2

There are 2 answers

0
Jorge Zapata On BEST ANSWER

I found a more holistic solution which is using IServiceCollection extension method AddValidatorsFromAssemblyContaining<T>() of nuget package FluentValidation.DependencyInjectionExtensions where It adds all validators in the assembly of the type specified by the generic parameter.

0
Dĵ ΝιΓΞΗΛψΚ On

i'm not sure why you're using swashbuckle as fastendpoints only supports nswag for swagger generation. the nswag equivalent would be an ISchemaProcessor. also FE doesn't register validators in the DI container itself. so if you need to obtain a validator instance within a schema processor, you'll have to register the validator in DI yourself. here's an example how you'd achieve that:

var bld = WebApplication.CreateBuilder(args);
bld.Services
   .AddFastEndpoints()
   .AddSingleton<IValidator<MyRequest>, MyValidator>() //register the validator yourself
   .SwaggerDocument(
       o =>
       {
           o.DocumentSettings = s =>
           {
               s.SchemaProcessors.Add(new MySchemaProcessor(bld.Services)); //pass down the service collection to the schema processor
           };
       });

var app = bld.Build();
app.UseFastEndpoints();
app.UseSwaggerGen();
app.Run();

class MySchemaProcessor : ISchemaProcessor
{
    readonly IServiceProvider _serviceProvider;

    public MySchemaProcessor(IServiceCollection serviceCollection)
    {
        _serviceProvider = serviceCollection.BuildServiceProvider();
    }

    public void Process(SchemaProcessorContext ctx)
    {
        var validator = _serviceProvider.GetService(typeof(IValidator<>).MakeGenericType(ctx.ContextualType.Type));

        if (validator is not null)
        {
            // do whatever you want here
        }
    }
}

sealed class MyRequest
{
    public int Id { get; set; }
}

sealed class MyValidator : Validator<MyRequest>
{
    public MyValidator()
    {
        RuleFor(r => r.Id).NotEmpty();
    }
}

sealed class MyEndpoint : Endpoint<MyRequest>
{
    public override void Configure()
    {
        Post("test");
        AllowAnonymous();
    }

    public override async Task HandleAsync(MyRequest r, CancellationToken c)
    {
        await SendAsync(r);
    }
}