Trying to set-up unit / integration tests for some extensions I am writing for the OdataQueryOptions class. I am using .net core 3.1.
In order to create the SUT instance - I need a HttpRequest. Which I creating using the WebApplicationFactory
public class TestingWebApplicationFactoryFixture : WebApplicationFactory<TestStartUp>
{
protected override IHostBuilder CreateHostBuilder()
{
var builder = Host.CreateDefaultBuilder();
builder.ConfigureWebHost(hostBuilder =>
{
hostBuilder.ConfigureServices(services =>
{
services.AddMvc(options => options.EnableEndpointRouting = false);
services.AddOData();
}).Configure(app =>
{
app.UseMvc(routeBuilder =>
{
routeBuilder.EnableDependencyInjection();
routeBuilder.Select().Expand().OrderBy().Filter().MaxTop(int.MaxValue);
});
});
});
return builder;
}
I arrange the test to use the TestServer to produce the HttpContext. The OdataQueryContext and HttpRequest is then used to instantiate the OdataQueryOptions object.
const string path = "/?$filter=SalesOrderID eq 43659";
var httpContext = await _testingWebApplicationFactoryFixture.Server.SendAsync(context =>
{
context.Request.Method = HttpMethods.Get;
context.Request.Path = path;
});
var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.AddEntityType(typeof(Customer));
var model = modelBuilder.GetEdmModel();
var odataQueryContext = new ODataQueryContext(model, typeof(Customer), new ODataPath());
var sut = new ODataQueryOptions<Customer>(odataQueryContext, httpContext.Request);
I am getting an exception during the instantiation of the object:
System.ArgumentNullException
Value cannot be null. (Parameter 'provider')
at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T]
(IServiceProvider provider)
at Microsoft.AspNet.OData.Extensions.HttpRequestExtensions.CreateRequestScope(HttpRequest request,
String routeName)
at Microsoft.AspNet.OData.Extensions.HttpRequestExtensions.CreateRequestContainer(HttpRequest
request, String routeName)
at Microsoft.AspNet.OData.Extensions.HttpRequestExtensions.GetRequestContainer(HttpRequest request)
at Microsoft.AspNet.OData.Query.ODataQueryOptions..ctor(ODataQueryContext context, HttpRequest
request)
at Microsoft.AspNet.OData.Query.ODataQueryOptions`1..ctor(ODataQueryContext context, HttpRequest
request)
Digging into the actual method that is throwing - it is because the IServiceProvider is null. Shouldn't this be handled by the host?
UPDATE:
I modified the test method a bit so that I eliminate the WebApplicationFactory class.
Instead I create a TestServer with an IWebHostBuilder:
private IWebHostBuilder GetBuilder()
{
var webHostBuilder = new WebHostBuilder();
webHostBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.AddMvc(options => options.EnableEndpointRouting = false);
services.AddOData();
}).Configure(app =>
{
app.UseMvc(routeBuilder =>
{
routeBuilder.EnableDependencyInjection();
routeBuilder.Select().Expand().OrderBy().Filter().MaxTop(int.MaxValue);
});
});
return webHostBuilder;
}
And then create the TestServer:
[Fact]
public async Task QueryGenerator_Generate_SomeExpress_ShouldProduce()
{
const string path = "/?$filter=SalesOrderID eq 43659";
var testServer = new TestServer(GetBuilder());
var httpContext = await testServer.SendAsync(context =>
{
context.Request.Method = HttpMethods.Get;
context.Request.Path = path;
});
var modelBuilder = new ODataConventionModelBuilder();
modelBuilder.AddEntityType(typeof(Customer));
var model = modelBuilder.GetEdmModel();
var odataQueryContext = new ODataQueryContext(model, typeof(Customer), new ODataPath());
var sut = new ODataQueryOptions<Customer>(odataQueryContext, httpContext.Request);
}
I get the same exception. Why is the IServiceProvider null?
Never got a solution to using TestServer, but I found a work around. At the end of the day I needed the OdataQueryOptions class generated by the framework. So I created an IClassFixture<> in Xunit to manually get it created.
public class OdataQueryOptionFixture
{
public IServiceProvider Provider { get; private set; }
private IEdmModel _edmModel;
public OdataQueryOptionFixture()
{
SetupFixture();
}
public ODataQueryOptions<T> CreateODataQueryOptions<T>(HttpRequest request)
where T : class
{
var odataQueryContext = CreateOdataQueryContext<T>();
var odataQueryOptions = new ODataQueryOptions<T>(odataQueryContext, request);
return odataQueryOptions;
}
private ODataQueryContext CreateOdataQueryContext<T>()
where T : class
{
var odataQueryContext = new ODataQueryContext(_edmModel, typeof(T), new ODataPath());
return odataQueryContext;
}
private void SetupFixture()
{
var collection = new ServiceCollection();
collection.AddOData();
collection.AddTransient<ODataUriResolver>();
collection.AddTransient<ODataQueryValidator>();
Provider = collection.BuildServiceProvider();
ConfigureRoutes();
BuildModel();
}
private void ConfigureRoutes()
{
var routeBuilder = new RouteBuilder(Mock.Of<IApplicationBuilder>(x => x.ApplicationServices == Provider));
routeBuilder.Select().Expand().OrderBy().Filter().MaxTop(int.MaxValue).Count();
routeBuilder.EnableDependencyInjection();
}
private void BuildModel()
{
var edmContext = new AdventureWorksEdmContext();
_edmModel = edmContext.BuildModel();
}
Using the class fixture in a test class to construct the OdataQueryOptions
private QueryOptionsBuilder<Customer> GetSut(HttpRequest request)
{
var odataQueryOptions = _odataQueryOptionFixture.CreateODataQueryOptions<Customer>(request);
var odataQuerySettings = new ODataQuerySettings();
var odataValidationSettings = new ODataValidationSettings();
var customerExpandBinder = new CustomerExpandBinder(odataValidationSettings, odataQueryOptions.SelectExpand);
var customerOrderByBinder = new CustomerOrderByBinder(odataValidationSettings, odataQueryOptions.OrderBy);
var customerSelectBinder = new CustomerSelectBinder(odataValidationSettings, odataQueryOptions.SelectExpand);
var customerCompositeBinder = new CustomerCompositeBinder(customerExpandBinder, customerOrderByBinder, customerSelectBinder);
return new QueryOptionsBuilder<Customer>(customerCompositeBinder, odataQuerySettings);
}
TestServer would have been easier - but this gets the job done.
Related
I am trying to get groups from AzureAD by calling graph in the hub in the OnConnectedAsync() to add the current user to groups.
When calling my graph service from a controller it works just fine and returns me the data I want, but when calling it from inside the hub I get an error:
IDW10502: An MsalUiRequiredException was thrown due to a challenge for the user. See https://aka.ms/ms-id-web/ca_incremental-consent. No account or login hint was passed to the AcquireTokenSilent call.
Any idea?
Here is my graph service :
public class GraphService : IGraphService
{
private readonly ITokenAcquisition _tokenAcquisition;
private readonly IHttpClientFactory _httpClientFactory;
public GraphService(ITokenAcquisition tokenAcquisition,
IHttpClientFactory clientFactory)
{
_httpClientFactory = clientFactory;
_tokenAcquisition = tokenAcquisition;
}
public async Task<IEnumerable<Group>> GetUserGroupsAsync()
{
var graphClient = await GetGraphClient();
var memberShipCollection = await graphClient
.Me
.MemberOf
.Request()
.GetAsync();
return memberShipCollection
.OfType<Group>();
}
private async Task<GraphServiceClient> GetGraphClient()
{
var token = await _tokenAcquisition
.GetAccessTokenForUserAsync(new string[] { "https://graph.microsoft.com/.default" });
var client = _httpClientFactory.CreateClient();
client.BaseAddress = new Uri("https://graph.microsoft.com/v1.0");
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var graphClient = new GraphServiceClient(client)
{
AuthenticationProvider = new DelegateAuthenticationProvider(async (requestMessage) =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", token);
})
};
graphClient.BaseUrl = "https://graph.microsoft.com/v1.0";
return graphClient;
}
}
The hub :
[Authorize]
public class NotificationHub : Hub
{
ILogger<NotificationHub> _logger;
private readonly IGraphService _graphService;
public NotificationHub(ILogger<NotificationHub> logger,
IGraphService graphService)
{
_logger = logger;
_graphService = graphService;
}
public override async Task OnConnectedAsync()
{
await base.OnConnectedAsync();
var userDynamicGroups = await GetUserDynamicGroupsAsync();
//foreach (var group in userDynamicGroups)
// await Groups.AddToGroupAsync(Context.ConnectionId, group.DisplayName);
}
public override async Task OnDisconnectedAsync(Exception exception)
{
await base.OnDisconnectedAsync(exception);
//var userDynamicGroups = await GetUserDynamicGroupsAsync();
//foreach (var group in userDynamicGroups)
// await Groups.RemoveFromGroupAsync(Context.ConnectionId, group.DisplayName);
}
private async Task<IEnumerable<Group>> GetUserDynamicGroupsAsync()
{
var AllUserGroups = await _graphService.GetUserGroupsAsync();
return AllUserGroups.Where(g => g.DisplayName.Contains("ONE_"));
}
}
The part related to auth in my startup:
public static IServiceCollection AddInternalAuthentification(this IServiceCollection services, IConfiguration configuration)
{
services.AddMicrosoftIdentityWebApiAuthentication(configuration, "AzureAd")
.EnableTokenAcquisitionToCallDownstreamApi()
.AddMicrosoftGraph(configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
services.Configure<JwtBearerOptions>(JwtBearerDefaults.AuthenticationScheme, options =>
{
Func<MessageReceivedContext, Task> existingOnMessageReceivedHandler = options.Events.OnMessageReceived;
options.Events.OnMessageReceived = async context =>
{
await existingOnMessageReceivedHandler(context);
StringValues accessToken = context.Request.Query["access_token"];
PathString path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/Notify"))
context.Token = accessToken;
};
});
return services;
}
I have a method to get header value using IHttpContextAccessor
public class HeaderConfiguration : IHeaderConfiguration
{
public HeaderConfiguration()
{
}
public string GetTenantId(IHttpContextAccessor httpContextAccessor)
{
return httpContextAccessor.HttpContext.Request.Headers["Tenant-ID"].ToString();
}
}
I am testing GetBookByBookId method
Let's say the method looks like this:
public class Book
{
private readonly IHttpContextAccessor _httpContextAccessor;
private IHeaderConfiguration _headerConfiguration;
private string _tenantID;
public Book(IHeaderConfiguration headerConfiguration, IHttpContextAccessor httpContextAccessor){
var headerConfig = new HeaderConfiguration();
_httpContextAccessor = httpContextAccessor;
_tenantID = headerConfig.GetTenantId(_httpContextAccessor);
}
public Task<List<BookModel>> GetBookByBookId(string id){
//do something with the _tenantId
//...
}
}
Here's my unit test for GetBookByBookId method
[Fact]
public void test_GetBookByBookId()
{
//Arrange
//Mock IHttpContextAccessor
var mockHttpContextAccessor = new Mock<IHttpContextAccessor>();
mockHttpContextAccessor.Setup(req => req.HttpContext.Request.Headers["Tenant-ID"].ToString()).Returns(It.IsAny<string>());
//Mock HeaderConfiguration
var mockHeaderConfiguration = new Mock<IHeaderConfiguration>();
mockHeaderConfiguration.Setup(x => x.GetTenantId(mockHttpContextAccessor.Object)).Returns(It.IsAny<string>());
var book = new Book( mockHttpContextAccessor.Object, mockHeaderConfiguration.Object);
var bookId = "100";
//Act
var result = book.GetBookByBookId(bookId);
//Assert
result.Result.Should().NotBeNull().And.
BeOfType<List<BookModel>>();
}
But for this line:
mockHttpContextAccessor.Setup(req => req.HttpContext.Request.Headers["Tenant-ID"].ToString()).Returns(It.IsAny<string>());
It says
System.NotSupportedException: 'Type to mock must be an interface or an abstract or non-sealed class. '
I was wondering what's the proper way to mock IHttpContextAccessor with header value?
You can use the DefaultHttpContext as a backing for the IHttpContextAccessor.HttpContext. Saves you having to set-up too many things
Next you cannot use It.IsAny<string>() as a Returns result. They were meant to be used in the set up expressions alone.
Check the refactor
[Fact]
public async Task test_GetBookByBookId() {
//Arrange
//Mock IHttpContextAccessor
var mockHttpContextAccessor = new Mock<IHttpContextAccessor>();
var context = new DefaultHttpContext();
var fakeTenantId = "abcd";
context.Request.Headers["Tenant-ID"] = fakeTenantId;
mockHttpContextAccessor.Setup(_ => _.HttpContext).Returns(context);
//Mock HeaderConfiguration
var mockHeaderConfiguration = new Mock<IHeaderConfiguration>();
mockHeaderConfiguration
.Setup(_ => _.GetTenantId(It.IsAny<IHttpContextAccessor>()))
.Returns(fakeTenantId);
var book = new Book(mockHttpContextAccessor.Object, mockHeaderConfiguration.Object);
var bookId = "100";
//Act
var result = await book.GetBookByBookId(bookId);
//Assert
result.Should().NotBeNull().And.
BeOfType<List<BookModel>>();
}
There may also be an issue with the Class Under Test as it is manually initializing the HeaderConfiguration when it should actually be explicitly injected.
public Book(IHeaderConfiguration headerConfiguration, IHttpContextAccessor httpContextAccessor) {
_httpContextAccessor = httpContextAccessor;
_tenantID = headerConfiguration.GetTenantId(_httpContextAccessor);
}
In my scenario I had to mock IHttpContextAccessor and access the inner request url bits.
I'm sharing it here because I spent a decent amount of time figuring this out and hopefully it'll help someone.
readonly Mock<IHttpContextAccessor> _HttpContextAccessor =
new Mock<IHttpContextAccessor>(MockBehavior.Strict);
void SetupHttpContextAccessorWithUrl(string currentUrl)
{
var httpContext = new DefaultHttpContext();
setRequestUrl(httpContext.Request, currentUrl);
_HttpContextAccessor
.SetupGet(accessor => accessor.HttpContext)
.Returns(httpContext);
static void setRequestUrl(HttpRequest httpRequest, string url)
{
UriHelper
.FromAbsolute(url, out var scheme, out var host, out var path, out var query,
fragment: out var _);
httpRequest.Scheme = scheme;
httpRequest.Host = host;
httpRequest.Path = path;
httpRequest.QueryString = query;
}
}
If you are making use of the wonderful NSubstitute package for NUnit, you can do this...
var mockHttpAccessor = Substitute.For<IHttpContextAccessor>();
var context = new DefaultHttpContext
{
Connection =
{
Id = Guid.NewGuid().ToString()
}
};
mockHttpAccessor.HttpContext.Returns(context);
// usage...
I have repository structure... IUnitOfWork unitOfWork,
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies
// is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
options.CheckConsentNeeded
I wanna set it from the database
var culture = Thread.CurrentThread.CurrentCulture.ToString();
var checkConsentNeededCookieettings = _unitOfWork.Settings.GetAll().Where(i => i.Language.Culture == culture).FirstOrDefault().CheckConsentNeededCookie;
you know that these are services
how to configure it and where it is ?
di ? middleware or override ?
can you give me advice about that? and example
I would use IPostConfigureOptions for this. Create a class that implements IPostConfigureOptions<CookiePolicyOptions>:
internal class CookiePolicyPostConfigureOptions : IPostConfigureOptions<CookiePolicyOptions>
{
private readonly IUnitOfWork _unitOfWork;
public CookiePolicyPostConfigureOptions(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public void PostConfigure(string name, CookiePolicyOptions options)
{
// do something with _unitOfWork and set values on `options`
}
}
Then in Startup:
services.ConfigureOptions<CookiePolicyPostConfigureOptions>();
If I use same way for CustomIdentityOptions, there is a error that :
InvalidOperationException: ValueFactory attempted to access the Value property of this instance.
System.Lazy.ViaFactory(LazyThreadSafetyMode mode)
what is the different between IdentityOptions to CookiePolicyOptions
services.ConfigureOptions<CustomIdentityOptions>();
internal class CustomIdentityOptions : IPostConfigureOptions<IdentityOptions>
{
private readonly IUnitOfWork _unitOfWork;
private readonly ILogger<CustomIdentityOptions> _logger;
public CustomIdentityOptions(IUnitOfWork unitOfWork, ILogger<CustomIdentityOptions> logger)
{
_unitOfWork = unitOfWork;
_logger = logger;
}
public void PostConfigure(string name, IdentityOptions options)
{
var staticSettings = _unitOfWork.StaticSettings.GetAll().OrderBy(i => i.StaticSettingId).FirstOrDefault();
options.Lockout = new LockoutOptions
{
AllowedForNewUsers = staticSettings.AllowedForNewUsers,
DefaultLockoutTimeSpan = TimeSpan.FromMinutes(staticSettings.DefaultLockoutTimeSpanFromMinutes),
MaxFailedAccessAttempts = staticSettings.MaxFailedAccessAttempts
};
options.Password = new PasswordOptions
{
RequireDigit = staticSettings.RequireDigit,
RequiredLength = staticSettings.RequiredLength,
RequiredUniqueChars = staticSettings.RequiredUniqueChars,
RequireLowercase = staticSettings.RequireLowercase,
RequireNonAlphanumeric = staticSettings.RequireNonAlphanumeric,
RequireUppercase = staticSettings.RequireUppercase
};
options.SignIn = new SignInOptions
{
RequireConfirmedEmail = staticSettings.RequireConfirmedEmail,
RequireConfirmedPhoneNumber = staticSettings.RequireConfirmedPhoneNumber
};
}
}
I'm using .NET5, and if your database/repository is injected using the out of the box DI, than you can get the service right from the httpContext, so just use a lambda function like this:
services.Configure<CookiePolicyOptions>( (options) => {
options.CheckConsentNeeded = (context) => {
// get repo from DI
var repo = context.RequestServices.GetService<IRepository>();
// .. Determine result
return repo.GetAll().Where(i => i.Language.Culture == culture).FirstOrDefault().CheckConsentNeededCookie;
};
});
Is is possible to initialize HubConnection from Microsoft.AspNetCore.TestHost.TestServer?
The example below throws HttpRequestException(Not Found) exception at await hubConnection.StartAsync();
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.AspNetCore.TestHost;
using Xunit;
namespace FunctionalTests
{
public class PubSubScenarios
{
[Fact]
public async Task SignalRHubTest_Foo()
{
var webHostBuilder = WebHost.CreateDefaultBuilder().UseStartup<Startup>();
using (var testServer = new TestServer(webHostBuilder))
{
var hubConnection = await StartConnectionAsync(testServer.BaseAddress);
}
}
private static async Task<HubConnection> StartConnectionAsync(Uri baseUri)
{
var hubConnection = new HubConnectionBuilder()
.WithUrl($"http://{baseUri.Host}/fooHub")
.WithConsoleLogger()
.Build();
await hubConnection.StartAsync();
return hubConnection;
}
}
}
You need to call testServer.CreateHandler() and pass the HttpMessageHandler to WithMessageHandler:
[Fact]
public async Task SignalRHubTest_Foo()
{
var webHostBuilder = WebHost.CreateDefaultBuilder().UseStartup<Startup>();
using (var testServer = new TestServer(webHostBuilder))
{
var hubConnection = await StartConnectionAsync(testServer.CreateHandler());
}
}
private static async Task<HubConnection> StartConnectionAsync(HttpMessageHandler handler)
{
var hubConnection = new HubConnectionBuilder()
.WithUrl($"http://test/fooHub", options =>
{
options.Transports = HttpTransportType.LongPolling;
options.HttpMessageHandlerFactory = _ => handler;
})
.Build();
await hubConnection.StartAsync();
return hubConnection;
}
This won't work for websockets though (I opened an issue for this here https://github.com/aspnet/SignalR/issues/1595
I'm using Web Api 2, Autofac, and MediatR (CQRS). I have a mediator pipeline in place that has pre/post request handlers. That all works fine. I'm trying to hook up Validation now and decorate the pipeline with it.
Here is my Autofac DI code:
public void Configuration(IAppBuilder app)
{
var config = new HttpConfiguration();
FluentValidationModelValidatorProvider.Configure(config);
ConfigureDependencyInjection(app, config);
WebApiConfig.Register(config);
app.UseWebApi(config);
}
private static void ConfigureDependencyInjection(IAppBuilder app, HttpConfiguration config)
{
var builder = new ContainerBuilder();
builder.RegisterSource(new ContravariantRegistrationSource());
builder.RegisterAssemblyTypes(typeof(IMediator).Assembly).AsImplementedInterfaces();
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));
});
//register all pre handlers
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.As(type => type.GetInterfaces()
.Where(interfacetype => interfacetype.IsClosedTypeOf(typeof(IAsyncPreRequestHandler<>))))
.InstancePerLifetimeScope();
//register all post handlers
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.As(type => type.GetInterfaces()
.Where(interfacetype => interfacetype.IsClosedTypeOf(typeof(IAsyncPostRequestHandler<,>))))
.InstancePerLifetimeScope();
//register all async handlers
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.As(type => type.GetInterfaces()
.Where(interfaceType => interfaceType.IsClosedTypeOf(typeof(IAsyncRequestHandler<,>)))
.Select(interfaceType => new KeyedService("asyncRequestHandler", interfaceType)))
.InstancePerLifetimeScope();
//register pipeline decorator
builder.RegisterGenericDecorator(
typeof(AsyncMediatorPipeline<,>),
typeof(IAsyncRequestHandler<,>),
"asyncRequestHandler")
.Keyed("asyncMediatorPipeline", typeof(IAsyncRequestHandler<,>))
.InstancePerLifetimeScope();
//register validator decorator
builder.RegisterGenericDecorator(
typeof(ValidatorHandler<,>),
typeof(IAsyncRequestHandler<,>),
"asyncMediatorPipeline")
.InstancePerLifetimeScope();
// Register Web API controller in executing assembly.
builder.RegisterApiControllers(Assembly.GetExecutingAssembly()).InstancePerRequest();
//register RedStripeDbContext
builder.RegisterType<RedStripeDbContext>().As<IRedStripeDbContext>().InstancePerRequest();
builder.RegisterType<AutofacServiceLocator>().AsImplementedInterfaces();
var container = builder.Build();
config.DependencyResolver = new AutofacWebApiDependencyResolver(container);
// This should be the first middleware added to the IAppBuilder.
app.UseAutofacMiddleware(container);
// Make sure the Autofac lifetime scope is passed to Web API.
app.UseAutofacWebApi(config);
}
Here is the ValidatorHandler:
public class ValidatorHandler<TRequest, TResponse> : IAsyncRequestHandler<TRequest, TResponse> where TRequest : IAsyncRequest<TResponse>
{
private readonly IAsyncRequestHandler<TRequest, TResponse> _inner;
private readonly IValidator<TRequest>[] _validators;
public ValidatorHandler(
IAsyncRequestHandler<TRequest, TResponse> inner,
IValidator<TRequest>[] validators)
{
_inner = inner;
_validators = validators;
}
public async Task<TResponse> Handle(TRequest request)
{
var context = new ValidationContext(request);
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 await _inner.Handle(request);
}
}
Here is a sample query:
[Validator(typeof(GetAccountRequestValidationHandler))]
public class GetAccountRequest : IAsyncRequest<GetAccountResponse>
{
public int Id { get; set; }
}
Here is the fluent validation handler:
public class GetAccountRequestValidationHandler : AbstractValidator<GetAccountRequest>
{
public GetAccountRequestValidationHandler()
{
RuleFor(m => m.Id).GreaterThan(0).WithMessage("Please specify an id.");
}
public Task Handle(GetAccountRequest request)
{
Debug.WriteLine("GetAccountPreProcessor Handler");
return Task.FromResult(true);
}
}
Here is the request handler:
public class GetAccountRequestHandler : IAsyncRequestHandler<GetAccountRequest, GetAccountResponse>
{
private readonly IRedStripeDbContext _dbContext;
public GetAccountRequestHandler(IRedStripeDbContext redStripeDbContext)
{
_dbContext = redStripeDbContext;
}
public async Task<GetAccountResponse> Handle(GetAccountRequest message)
{
return await _dbContext.Accounts.Where(a => a.AccountId == message.Id)
.ProjectToSingleOrDefaultAsync<GetAccountResponse>();
}
}
Finally here is the Web Api 2 HttpGet method:
[Route("{id:int}")]
[HttpGet]
public async Task<IHttpActionResult> GetById([FromUri] GetAccountRequest request)
{
var model = await _mediator.SendAsync<GetAccountResponse>(request);
return Ok(model);
}
I put breakpoints all over the place and when I hit this endpoint, the first thing I get into is the GetAccountRequestValidationHandler. Then I get into the ValidatorHandler's constructor. The problem is, the IValidator[] validators parameter to the constructor is always null.
I must be missing something with fluent validation and its registration via Autofac? Any help is much appreciated.
The validator types must be registered in the IoC. Adding the below to your ConfigureDependencyInjection method should do it.
builder.RegisterAssemblyTypes(Assembly.GetExecutingAssembly())
.Where(t => t.Name.EndsWith("ValidationHandler"))
.AsImplementedInterfaces()
.InstancePerLifetimeScope();