fluentvalidationswashbuckleswagger-codegenswashbuckle.aspnetcorefast-endpoints

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


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


Solution

  • 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.