ASP.NET Core middleware redirect to another route - asp.net-core

I have an ASP.NET 6 Web API controller and it has two methods.
This is my code:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger<WeatherForecastController> _logger;
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
[HttpGet]
[HttpPost]
[Route("Test")]
public IEnumerable<WeatherForecast> Test()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
[HttpGet]
[HttpPost]
[Route("User")]
public IEnumerable<WeatherForecast> UserInfo()
{
return Enumerable.Range(1, 2).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}
Then I wrote a middleware component:
public class RequestResponseMiddleware
{
private RequestDelegate _next;
public RequestResponseMiddleware(RequestDelegate next)
{
this._next = next;
}
public async Task Invoke(HttpContext context)
{
var req = context.Request;
context.Response.Redirect("/weatherforecast/User",true,true);
await _next(context);
}
}
I used
https://localhost:7290/weatherforecast/Test?t=1675343945003&m=&k=sdf
to test it.
In the middleware, response can redirect to /weatherforecast/User, but it will fire 5 times. It means that the method UserInfo() will be invoked 5 times.
What's wrong with my code? And how to fix the problem?
----update----
mapping example.
{
"Routes":[
{
"Controllor": "WeatherForecast",
"Metods":[
{
"Test": "User"
},
{
"Weather": "Weather"
}
]
}
]
}
---- update2 ----
context.Response.Redirect
will invoke WeatherForecast/ Test1 time, and invoke WeatherForecast/User4 times.
---- update 3 ----
code
code here

i know what's wrong with my code . There is not a exist condition before redirect.
a solution likes
if (context.Request.Path.HasValue && context.Request.Path == "/weatherforecast/Test")
{
context.Response.Redirect("/weatherforecast/User", false);
}
else
{
await _next.Invoke(context);
}

You have to branch the pineline,or the next time you already redirected to /weatherforecast/User the middleware would redirect you to the same uri again
Check this document
For example:
app.MapWhen(x => x.Request.Path.Value.Contains("Test"), app =>
{    
app.UseMiddleware<RequestResponseMiddleware>();
});
Create a Class to bind the configration file:
public class RouteMap
{
public string Controllor { get; set; }
public List<Dictionary<string,string>> Methods { get; set; }
}
In program.cs:
var routemap=new List<RouteMap>();
app.Configuration.GetSection("Routes").Bind(routemap);
app.MapWhen(x => x.Request.Path.Check(routemap), app =>
{
app.UseMiddleware<RequestResponseMiddleware>();
});
Complete the extension method:
public static class Extension
{
public static bool Check(this PathString pathString,List<RouteMap> routeMaps)
{
bool exist=false;
// you need to modify the logical here yourself to match the route
var controllername= pathString.Value?.Split('/')[1];
var methodname = pathString.Value?.Split('/')[2];
foreach (RouteMap routeMap in routeMaps)
{
if (routeMap.Controllor == controllername)
{
foreach(var methodmaps in routeMap.Methods)
{
if (methodmaps.ContainsKey(methodname))
{
exist =true;
break;
}
}
if (exist)
{
break;
}
}
}
return exist;
}
}
The result:

Related

Endpoint is null when accessed in middleware during asp.net core 3.1 integration test

I run integration tests for my asp.net core application, the call passes from multiple middle-wares but stops at one of them which has the following line :
var endpoint = context.Features.Get<IEndpointFeature>()?.Endpoint;
var attribute = endpoint?.Metadata.GetMetadata<AllowAHeader>();
The endpoint is null.
public class CustomWebApplicationFactory<TStartup> : WebApplicationFactory<TStartup>
where TStartup : class
{
protected override IHostBuilder CreateHostBuilder()
{
var builder = Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(x =>
{
x.UseStartup<TStartup>().UseTestServer();
});
return builder;
}
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
builder.ConfigureTestServices(services =>
{
services.RemoveAll<DbContext>();
services.RemoveAll<DbContextOptions>();
foreach (var option in services.Where(s =>
s.ServiceType.BaseType ==
typeof(DbContextOptions)).ToList())
{
services.Remove(option);
}
services.AddDbContext<DbContext>(options =>
{
options.UseInMemoryDatabase("Testing");
});
});
}
}
Here is the test
public class ClientTests : IClassFixture<CustomWebApplicationFactory<TestStartup>>
{
private readonly HttpClient _client;
public ClientTests(CustomWebApplicationFactory<TestStartup> customWebApplicationFactory)
{
_client = customWebApplicationFactory.CreateClient();
}
[Fact]
public async Task GetClients()
{
_client.DefaultRequestHeaders.Add("X-Integration-Testing", "True");
_client.DefaultRequestHeaders.Add("X-Integration-Authroize", "Basic");
var result = await _client.PostAsync("v1/client", null);
}
}
The TestStartup :
public class TestStartup : Startup
{
public TestStartup(IConfiguration configuration)
: base(configuration)
{
}
protected override void ConfigureMiddlewareForIntegrationTest(IApplicationBuilder app)
{
app.UseMiddleware<AuthenticatedTestRequestMiddleware>();
}
}
public class AuthenticatedTestRequestMiddleware
{
public const string TestingHeader = "X-Integration-Testing";
public const string TestingHeaderAuthValueValue = "X-Integration-Authroize";
private readonly RequestDelegate _next;
public AuthenticatedTestRequestMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if (context.Request.Headers.Keys.Contains(TestingHeader))
{
if (context.Request.Headers.Keys.Contains(TestingHeaderAuthValueValue))
{
var encoded = "Basic " + System.Convert.ToBase64String(System.Text.Encoding.GetEncoding("ISO-8859-1").GetBytes("user" + ":" + "123456"));
context.Request.Headers.Add("Authorization", encoded);
}
}
}
}
In ConfigureWebHostDefaults add:
x.UseHttpSys(opt =>
opt.RequestQueueMode = RequestQueueMode.Create;
)
Have not figured out exactly why it's needed, but I'm guessing it's a bug being the value of RequestQueueMode is 0 by default, same as RequestQueueMode.Create's value.

FluentValidation failure not returning BadRequest

I have wired up FluentValidation as per instructions, and when debuging test I can see that model is invalid based on the test setup, but exception is not thrown, but rather method on the controller is being executed. This is on 3.1 with EndPoint routing enabled. Is there anything else one needs to do to get this to work and throw. What happens is that validation obviously runs; it shows as ModelState invalid and correct InstallmentId is invalid, but it keeps processing in Controller instead of throwing exception.
services.AddMvc(
options =>
{
options.EnableEndpointRouting = true;
//// options.Filters.Add<ExceptionFilter>();
//// options.Filters.Add<CustomerRequestFilter>();
})
.AddFluentValidation(
config =>
{
config.RegisterValidatorsFromAssemblyContaining<Startup>();
})
Command and Validator
public class ProcessManualPayment
{
public class Command
: CustomerRequest<Result?>
{
public Guid PaymentPlanId { get; set; }
public Guid InstallmentId { get; set; }
public Guid PaymentCardId { get; set; }
}
public class Validator : AbstractValidator<Command>
{
public Validator()
{
this.RuleFor(x => x.CustomerId)
.IsValidGuid();
this.RuleFor(x => x.PaymentPlanId)
.IsValidGuid();
this.RuleFor(x => x.InstallmentId)
.IsValidGuid();
this.RuleFor(x => x.PaymentCardId)
.IsValidGuid();
}
}
Controller
[Authorize]
[HttpPost]
[Route("payments")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
public async Task<IActionResult> ProcessManualPayment(
[FromBody]
ProcessManualPayment.Command command)
{
Test
[Fact]
public async Task When_Command_Has_Invalid_Payload_Should_Fail()
{
var client = this.factory.CreateClient();
// Arrange
var validCmd = new ProcessManualPayment.Command()
{
CustomerId = Guid.NewGuid(),
PaymentPlanId = Guid.NewGuid(),
InstallmentId = Guid.NewGuid(),
PaymentCardId = Guid.NewGuid(),
};
var validCmdJson = JsonConvert.SerializeObject(validCmd, Formatting.None);
var jObject = JObject.Parse(validCmdJson);
jObject["installmentId"] = "asdf";
var payload = jObject.ToString(Formatting.None);
// Act
var content = new StringContent(payload, Encoding.UTF8, MediaTypeNames.Application.Json);
var response = await client.PostAsync(MakePaymentUrl, content);
var returned = await response.Content.ReadAsStringAsync();
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
}
[Fact]
public async Task When_Payload_Is_Null_Should_Fail()
{
// Arrange
var client = this.factory.CreateClient();
// Act
var response = await client.PostAsJsonAsync(MakePaymentUrl, null);
// Assert
response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
}
GuidValidator
public class GuidValidator : PropertyValidator
{
public GuidValidator()
: base("'{PropertyName}' value {AttemptedValue} is not a valid Guid.")
{
}
protected override bool IsValid(PropertyValidatorContext context)
{
context.MessageFormatter.AppendArgument("AttemptedValue", context.PropertyValue ?? "'null'");
if (context.PropertyValue == null)
{
return false;
}
Guid.TryParse(context.PropertyValue.ToString(), out var value);
return IsValid(value);
}
private static bool IsValid(Guid? value) =>
value.HasValue
&& !value.Equals(Guid.Empty);
}
Mystery solved, I was missing [ApiController] attribute on the controller.

.NET Core 3 Service result to Controller to FE (data or error reason)

building new app on .net core 3 and Angular. Overall all works, but I want to add more intelligence to service/controller part. This is one of the api's, but this logic can be applied to others as as well.
Here's my Login Controller:
[HttpPost]
public async Task<IActionResult> Login([FromBody] UserLoginDto userLogin)
{
var token = await _userService.LoginAsync(userLogin);
if (token != null)
{
return Ok(token);
}
else
{
return BadRequest("Something went wrong");
}
}
And here's my userService:
public async Task<string> LoginAsync(UserLoginDto userLogin)
{
var user = await _userManager.FindByEmailAsync(userLogin.Email);
if (user != null)
{
var result = await _signInManager.PasswordSignInAsync(user, userLogin.Password, false, true);
if (result.Succeeded)
{
var roles = await _userManager.GetRolesAsync(user);
var tokenJson = _jwtManager.getJwtToken(user.Email, roles);
return tokenJson;
}
else
{
return null; // Return BadRequest and result reason (Failed, lockedout, etc)
}
}
else
{
return null; // User not found, return NotFound }
}
Here's my question - how should I return result from userService to Controller so, that I could respond to API call either with Ok(token) or BadRequest/NotFound with the reason.
If I keep all this LoginAsync code in controller, then it's easy, but I want to use service.
One option I was thinking was to introduce new class, something like:
public class BaseResult
{
public object Data { get; set; }
public long ResponseCode { get; set; }
public string ErrorMessage { get; set; }
}
then always return this class from service, but not fully like that idea either.
thanks!
Here is a working demo you could follow:
Model:
public class UserLoginDto
{
public string Email { get; set; }
public string Password { get; set; }
}
IUserService:
public interface IUserService
{
Task<IActionResult> LoginAsync(UserLoginDto userLogin);
}
UserService:
public class UserService: IUserService
{
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
private readonly IJwtManager _jwtManager;
public UserService(
UserManager<IdentityUser> userManager,
SignInManager<IdentityUser> signInManager,
IJwtManager jwtManager)
{
_userManager = userManager;
_signInManager = signInManager;
_jwtManager = jwtManager;
}
public async Task<IActionResult> LoginAsync(UserLoginDto userLogin)
{
var user = await _userManager.FindByEmailAsync(userLogin.Email);
if (user != null)
{
var result = await _signInManager.PasswordSignInAsync(user, userLogin.Password, false, true);
if (result.Succeeded)
{
var roles = await _userManager.GetRolesAsync(user);
var tokenJson = _jwtManager.getJwtToken(user.Email, roles);
return new OkObjectResult(tokenJson);
}
else
{
// Return BadRequest and result reason (Failed, lockedout, etc)
if (result.IsNotAllowed)
{
if (!await _userManager.IsEmailConfirmedAsync(user))
{
// Email isn't confirmed.
return new BadRequestObjectResult("Email isn't confirmed.");
}
if (!await _userManager.IsPhoneNumberConfirmedAsync(user))
{
// Phone Number isn't confirmed.
return new BadRequestObjectResult("Phone Number isn't confirmed.");
}
return new BadRequestObjectResult("Login IsNotAllowed");
}
else if (result.IsLockedOut)
{
// Account is locked out.
return new BadRequestObjectResult("Account is locked out.");
}
else if (result.RequiresTwoFactor)
{
// 2FA required.
return new BadRequestObjectResult("2FA required");
}
else
{
// Password is incorrect.
return new BadRequestObjectResult("Password is incorrect.");
}
}
}
else
{
return new NotFoundObjectResult("Username is incorrect"); // User not found, return NotFound }
}
}
}
Controller:
public class HomeController : Controller
{
private readonly IUserService _userService;
public HomeController(IUserService userService)
{
_userService = userService;
}
[HttpPost]
public async Task<IActionResult> Login([FromBody] UserLoginDto userLogin)
{
var result= await _userService.LoginAsync(userLogin);
return result;
}
}
Startup.cs:
Not sure what is _jwtManager.getJwtToken in your code,so I just guess it is an interface and owns a JwtManager class implemented this interface.And it contains a getJwtToken method which generated the token.
services.AddScoped<IUserService, UserService>();
services.AddScoped<IJwtManager, JwtManager>();

Modelbinding an optional array of a custom model bound type

I'm stuck with binding an optional array in an ASP.NET Core Controller. The array contains elements of a custom type. Single elements of this type are bound with a custom model binder and validated in it.
Sample repo here: https://github.com/MarcusKohnert/OptionalArrayModelBinding
I get only two tests out of three working in the sample test project:
https://github.com/MarcusKohnert/OptionalArrayModelBinding/blob/master/OptionalArrayModelBindingTest/TestOptionalArrayCustomModelBinder.cs
public class TestOptionalArrayCustomModelBinder
{
private readonly TestServer server;
private readonly HttpClient client;
public TestOptionalArrayCustomModelBinder()
{
server = new TestServer(new WebHostBuilder().UseStartup<Startup>());
client = server.CreateClient();
}
[Fact]
public async Task SuccessWithoutProvidingIds()
{
var response = await client.GetAsync("/api/values");
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task SuccessWithValidIds()
{
var response = await client.GetAsync("/api/values?ids=aaa001&ids=bbb002");
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task FailureWithOneInvalidId()
{
var response = await client.GetAsync("/api/values?ids=xaaa001&ids=bbb002");
Assert.Equal(System.Net.HttpStatusCode.BadRequest, response.StatusCode);
}
}
Controller:
[Route("api/[controller]")]
public class ValuesController : Controller
{
[HttpGet]
public IActionResult Get(CustomIdentifier[] ids)
{
if (this.ModelState.IsValid == false) return this.BadRequest();
return this.Ok(ids);
}
}
Startup:
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.ModelBinderProviders.Insert(0, new CutomIdentifierModelBinderProvider());
//options.ModelBinderProviders.Add(new CutomIdentifierModelBinderProvider());
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseMvc();
}
}
ModelBinder:
public class CutomIdentifierModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
//if (context.Metadata.ModelType.IsArray && context.Metadata.ModelType == typeof(CustomIdentifier[]))
//{
// return new ArrayModelBinder<CustomIdentifier>(new CustomIdentifierModelBinder());
//}
if (context.Metadata.ModelType == typeof(CustomIdentifier))
{
return new BinderTypeModelBinder(typeof(CustomIdentifierModelBinder));
}
return null;
}
}
public class CustomIdentifierModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var attemptedValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).ToString();
var parseResult = CustomIdentifier.TryParse(attemptedValue);
if (parseResult.Failed)
{
bindingContext.Result = ModelBindingResult.Failed();
bindingContext.ModelState.AddModelError(bindingContext.ModelName, parseResult.Message.Message);
}
else
{
bindingContext.Model = parseResult.Value;
bindingContext.Result = ModelBindingResult.Success(parseResult.Value);
}
return Task.CompletedTask;
}
}
The MVC default ArrayModelBinder of T binds optional arrays correctly and sets ModelState.IsValid to true. If I use my own CustomIdentifierModelBinder however ModelState.IsValid will be false. Empty arrays are not recognized as valid.
How can I solve this problem? Thanks in advance.
You are very close. Just customize behavior of built-in ArrayModelBinder for the case of missing parameter. If extracted value is an empty string just fill the model with an empty array. In all other cases you could call usual ArrayModelBinder.
Here is a working sample that passes all your 3 tests:
public class CutomIdentifierModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType.IsArray && context.Metadata.ModelType == typeof(CustomIdentifier[]))
{
return new CustomArrayModelBinder<CustomIdentifier>(new CustomIdentifierModelBinder());
}
return null;
}
}
public class CustomArrayModelBinder<T> : IModelBinder
{
private readonly ArrayModelBinder<T> innerModelBinder;
public CustomArrayModelBinder(IModelBinder elemeBinder)
{
innerModelBinder = new ArrayModelBinder<T>(elemeBinder);
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var attemptedValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).ToString();
if (String.IsNullOrEmpty(attemptedValue))
{
bindingContext.Model = new T[0];
bindingContext.Result = ModelBindingResult.Success(bindingContext.Model);
return Task.CompletedTask;
}
return innerModelBinder.BindModelAsync(bindingContext);
}
}
The solution is the following code change, reflected in this commit:
https://github.com/MarcusKohnert/OptionalArrayModelBinding/commit/552f4d35d8c33c002e1aa0c05acb407f1f962102
I've found the solution by inspecting MVC's source code again.
https://github.com/aspnet/Mvc/blob/35601f95b345d0ef938fb21ce1c51f5a67a1fb62/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/SimpleTypeModelBinder.cs#L37
You'll need to check the valueProviderResult for None. If it's none then there is no parameter given and the ModelBinder binds correctly.
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == ValueProviderResult.None)
And also you register the provided ArrayModelBinder of T with your custom ModelBinder:
if (context.Metadata.ModelType.IsArray && context.Metadata.ModelType == typeof(CustomIdentifier[]))
{
return new ArrayModelBinder<CustomIdentifier>(new CustomIdentifierModelBinder());
}

Wiring up validation in MediatR and ASP.NET Core using autofac

I've just started to use MediatR in an asp.net core project and am struggling to wire up validation ...
Here's my controller:
public class PersonController : Controller
{
IMediator mediator;
public PersonController(IMediator mediator)
{
this.mediator = mediator;
}
[HttpPost]
public async Task<ActionResult> Post([FromBody]CreatePerson model)
{
var success = await mediator.Send(model);
if (success)
{
return Ok();
}
else
{
return BadRequest();
}
}
}
... and the CreatePerson command, validation (via FluentValidation) and request handler:
public class CreatePerson : IRequest<bool>
{
public string Title { get; set; }
public string FirstName { get; set; }
public string Surname { get; set; }
}
public class CreatePersonValidator : AbstractValidator<CreatePerson>
{
public CreatePersonValidator()
{
RuleFor(m => m.FirstName).NotEmpty().Length(1, 50);
RuleFor(m => m.Surname).NotEmpty().Length(3, 50);
}
}
public class CreatePersonHandler : IRequestHandler<CreatePerson, bool>
{
public CreatePersonHandler()
{
}
public bool Handle(CreatePerson message)
{
// do some stuff
return true;
}
}
I have this generic validation handler:
public class ValidatorHandler<TRequest, TResponse> : IRequestHandler<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
private readonly IRequestHandler<TRequest, TResponse> inner;
private readonly IValidator<TRequest>[] validators;
public ValidatorHandler(IRequestHandler<TRequest, TResponse> inner, IValidator<TRequest>[] validators)
{
this.inner = inner;
this.validators = validators;
}
public TResponse Handle(TRequest message)
{
var context = new ValidationContext(message);
var failures = validators
.Select(v => v.Validate(context))
.SelectMany(result => result.Errors)
.Where(f => f != null)
.ToList();
if (failures.Any())
throw new ValidationException(failures);
return inner.Handle(message);
}
}
... but I'm struggling to wire the validation up correctly in Startup.ConfigureServices using autofac:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
var builder = new ContainerBuilder();
builder.Register<SingleInstanceFactory>(ctx =>
{
var c = ctx.Resolve<IComponentContext>();
return t => c.Resolve(t);
});
builder.Register<MultiInstanceFactory>(ctx =>
{
var c = ctx.Resolve<IComponentContext>();
return t => (IEnumerable<object>)c.Resolve(typeof(IEnumerable<>).MakeGenericType(t));
});
builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly).AsImplementedInterfaces();
builder.RegisterAssemblyTypes(typeof(CreatePersonHandler).GetTypeInfo().Assembly).AsClosedTypesOf(typeof(IRequestHandler<,>));
builder.RegisterGenericDecorator(typeof(ValidatorHandler<,>), typeof(IRequestHandler<,>), "Validator").InstancePerLifetimeScope();
builder.Populate(services);
var container = builder.Build();
return container.Resolve<IServiceProvider>();
}
When I run the app and POST /api/person
{
"title": "Mr",
"firstName": "Paul",
"surname": ""
}
I get a 200.
CreatePersonHandler.Handle() was called but CreatePersonValidator() is never called.
Am i missing something in Startup.ConfigureServices()?
I suggest that you read the official documentation on how to wire up decorators in Autofac.
Decorators use named services to resolve the decorated services.
For example, in your piece of code:
builder.RegisterGenericDecorator(
typeof(ValidatorHandler<,>),
typeof(IRequestHandler<,>),
"Validator").InstancePerLifetimeScope();
you're instructing Autofac to use ValidationHandler<,> as a decorator to IRequestHandler<,> services that have been registered with the Validator name, which is probably not what you want.
Here's how you could get it working:
// Register the request handlers as named services
builder
.RegisterAssemblyTypes(typeof(CreatePersonHandler).GetTypeInfo().Assembly)
.AsClosedTypesOf(typeof(IRequestHandler<,>))
.Named("BaseImplementation");
// Register the decorators on top of your request handlers
builder.RegisterGenericDecorator(
typeof(ValidatorHandler<,>),
typeof(IRequestHandler<,>),
fromKey: "BaseImplementation").InstancePerLifetimeScope();
I find specifying the name of the fromKey parameter helps in understanding how decorators work with Autofac.