I'm using ASP.NET 6 Core and writing a basic integration test for a controller that uses a mocked DBContext. Trying to base it on the Microsoft docs. But I can't build because I'm getting
CS0051 Inconsistent accessibility: parameter type 'CustomWebApplicationFactory<Program>' is less accessible than method 'ProjectsControllerTests.ProjectsControllerTests(CustomWebApplicationFactory<Program>)' DatabaseManagerService
But they are both public?!
ProjectControllersTests.cs
public class ProjectsControllerTests
{
private readonly CustomWebApplicationFactory<Program> _factory;
private const string s_apiBaseUri = "/api/v1";
public ProjectsControllerTests(CustomWebApplicationFactory<Program> factory)
{
// Arrange
_factory = factory;
}
[Fact]
public async Task Post_Responds201IfAuthenticatedAndRequiredFields_SuccessAsync()
{
// Arrange
HttpClient? client = _factory.CreateClient(new WebApplicationFactoryClientOptions()
{
AllowAutoRedirect = false
});
var project = new Project() { Name = "Test Project" };
var httpContent = new StringContent(JsonSerializer.Serialize(project));
// Act
var response = await client.PostAsync($"{s_apiBaseUri}/projects", httpContent);
// Assert
Assert.Equal(System.Net.HttpStatusCode.Created, response.StatusCode);
}
}
CustomWebApplicationFactory.cs
public class CustomWebApplicationFactory<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
var descriptor = services.SingleOrDefault(
d => d.ServiceType ==
typeof(DbContextOptions<myDbContext>));
if (descriptor != null)
services.Remove(descriptor);
services.AddDbContext<myDbContext>(options =>
{
options.UseInMemoryDatabase("InMemoryDbForTesting");
});
services.AddAuthentication("Test")
.AddScheme<AuthenticationSchemeOptions, MockAuthHandler>(
"Test", options => { });
var sp = services.BuildServiceProvider();
using (var scope = sp.CreateScope())
{
var scopedServices = scope.ServiceProvider;
var db = scopedServices.GetRequiredService<myDbContext>();
var logger = scopedServices
.GetRequiredService<ILogger<CustomWebApplicationFactory<Program>>>();
db.Database.EnsureCreated();
try
{
db.EndUsers.Add(new DataAccessLibrary.Models.EndUser() { ObjectIdentifier = "test-oid" });
db.SaveChanges();
}
catch (Exception ex)
{
logger.LogError(ex, "An error occurred seeding the " +
"database with test messages. Error: {Message}", ex.Message);
}
}
});
}
}
The Program class is not public in ASP.NET 6 Core. As per https://learn.microsoft.com/en-us/aspnet/core/migration/50-to-60-samples?view=aspnetcore-6.0#twa you can make Program public by appending the following to Program.cs:
public partial class Program { }
It should also be noted that the above test class must be defined as:
public class ProjectsControllerTests : IClassFixture<CustomWebApplicationFactory<Program>>
for DI to work.
Related
I try to do integrations tests with WebApplicationFactory, but I get error.
Part of Program.cs:
builder.AddNegotiate(options =>
{
var ldapConnectionsFactory = new LdapConnectionsFactory(domainConfiguration, loggerFactory.CreateLogger<LdapConnectionsFactory>());
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && ldapConnectionsFactory.TryCreate(out var ldapConnection))
{
options.EnableLdap(settings =>
{
settings.LdapConnection = ldapConnection;
settings.Domain = domainConfiguration.Domain;
});
}
});
This code add six services to services array. If I don't add builder.AddNegotiate, I don't have problem.
When I try to do test, I get error:
Negotiate authentication requires a server that supports IConnectionItemsFeature like Kestrel.
TestFile:
[TestFixture]
public class AuthControllerTests
{
readonly CustomWebApplicationFactory<Program> _factory;
private readonly HttpClient _client;
public AuthControllerTests()
{
_factory = new CustomWebApplicationFactory<Program>();
_client = _factory.CreateClient();
_client.BaseAddress = new Uri("http://localhost:8001/");
}
[Test]
public async Task CheckAdminLogIn_SendRequest_ShouldReturnOk()
{
// Arrange
var credentials = new Credentials() { Login = "admin", Password = "admin" };
var jsonSerializerOptions = new JsonSerializerOptions
{
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
PropertyNameCaseInsensitive = true
};
string jsonString = JsonSerializer.Serialize(credentials, jsonSerializerOptions);
StringContent httpContent = new StringContent(jsonString, System.Text.Encoding.UTF8, "application/json");
// Act
var response = await _client.PostAsync("api/Auth/login", httpContent);
// Assert
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
}
I try remove services in CustomWebApplicationFactory:
public class CustomWebApplicationFactory<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Here I am trying to find and delete Negotiate, but it deletes 4 out of 6 services
var negotiateHandler = services.SingleOrDefault(d => d.ServiceType == typeof(NegotiateHandler));
services.Remove(negotiateHandler);
List<ServiceDescriptor> servicesForRemove = services.Where(d => d.ServiceType.FullName.Contains("Negotiate")).ToList();
foreach (var s in servicesForRemove)
{
services.Remove(s);
}
});
}
}
It is not removed:
Is there another way to disable negotiate auth?
I removed all Microsoft.AspNetCore.Authentication, then added JwtBearer. It works for me
public class CustomWebApplicationFactory<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
List<ServiceDescriptor> servicesForRemove = services.Where(d => d.ServiceType.FullName.Contains("Microsoft.AspNetCore.Authentication")).ToList();
foreach (var s in servicesForRemove)
{
services.Remove(s);
}
var buildServiceProvider = services.BuildServiceProvider();
var loggerFactory = buildServiceProvider.GetService<ILoggerFactory>();
services.AddAuthentication().AddJwtBearer(options =>
{
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = JwtProvider.CreateTokenValidator(loggerFactory);
});
});
}
}
You can remove a specific scheme with:
internal class CustomWebApplicationFactory : WebApplicationFactory<Startup>
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureTestServices(services =>
{
services.Configure<AuthenticationOptions>(o =>
{
if (o.Schemes is List<AuthenticationSchemeBuilder> schemes)
{
schemes.RemoveAll(s => s.Name == NegotiateDefaults.AuthenticationScheme);
o.SchemeMap.Remove(NegotiateDefaults.AuthenticationScheme);
}
});
});
}
}
I have a test project that uses IClassFixture of a generic factory class.
For example
public class WeatherForecastAcceptanceTest
: IClassFixture<WebApi1ApplicationFactory<Startup>>, IDisposable
{
.....
}
after the Startup class executes ConfigureServices and Configure methods it executes the ConfigureWebHost where I remove the original DbContext and add a new one that runs in memory.
public class WebApi1ApplicationFactory<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
//remove the injected DbContext and inject in-memory
services.RemoveAll(typeof(DbContext));
var connection = new SqliteConnection("Data Source=:memory:");
services.AddDbContext<WebApi1DbContext>(
options => options.UseSqlite(connection));
var sp = services.BuildServiceProvider();
using (var scope = sp.CreateScope())
{
//ERROR HERE WITH THE RESOLVED DbContext
using (var dbContext = scope.ServiceProvider
.GetRequiredService<WebApi1DbContext>())
{
try
{
dbContext.Database.OpenConnection();
dbContext.Database.EnsureCreated();
}
catch (Exception ex)
{
throw;
}
}
}
});
}
}
The DbContext that I am resolving has the original connectionString instead of InMemory, as a result, all my testings are inserting content on my original database.
Here is how I am using my WebApplicationFactory
public class WeatherForecastAcceptanceTest : IClassFixture<WebApi1ApplicationFactory<Startup>>, IDisposable
{
private WebApi1ApplicationFactory<Startup> _factory;
private HttpClient _client;
public WeatherForecastAcceptanceTest(WebApi1ApplicationFactory<Startup> factory)
{
_factory = factory;
_client = factory.CreateClient();
}
[Fact]
public async Task GetAll_ReturnsElements_WhenPostWasExecutedSucessfully()
{
// Arrange
var weatherForecastForCreationDto = CreateRandomWeatherForecastForCreationDto();
var content = new JsonContent(weatherForecastForCreationDto);
//setting the content-type header
content.Headers.ContentType = new MediaTypeWithQualityHeaderValue(HttpMediaTypes.WeatherForecastType1);
//create an object
using (var client = _factory.CreateClient())
{
//act
var responsePost = await _client.PostAsync(ApiRoutes.WeatherForecast.CreateWeatherForecast, content);
//assert
responsePost.StatusCode.Should().Be(StatusCodes.Status201Created);
}
//check that the object was inserted
using (var client = _factory.CreateClient())
{
_client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue(HttpMediaTypes.WeatherForecastType1));
// Act
var response = await _client.GetAsync(ApiRoutes.WeatherForecast.GetWeatherForecast);
// Assert
response.StatusCode.Should().Be(StatusCodes.Status200OK);
var returnedGet = await response.Content.ReadAsAsync<WeatherForecastDto[]>();
returnedGet.Should().Contain(dto => dto.Summary == weatherForecastForCreationDto.Summary);
}
}
public void Dispose()
{
_client?.Dispose();
_factory?.Dispose();
}
}
How can I resolve the in-memory DbContext that was injected?
After dealing with this problem for a while, based on the documentation this is what they are suggesting
On your startup read a test environment variable, like you are doing with production or development, and inject the correct DbContext for that.
For example to inject an on memory DBContext with Sqlite.
if (_env.IsEnvironment("Test"))
{
var connection = new SqliteConnection(connectionDb);
services.AddDbContextPool<WebApi1DbContext>(options => options.UseSqlite(connection));
}
Instead of trying to remove the DbContext in that way, the idea is try to set the environment variable to the WebApplicationFactory because the AddDbContext sets more dependencies on the container that RemoveAll is not considering.
public class WebApi1ApplicationFactory<TStartup>
: WebApplicationFactory<TStartup> where TStartup : class
{
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment("Test");
builder.ConfigureServices(services =>
{
//DbContext in-memory is injected taking in consideration the Test environment.
var sp = services.BuildServiceProvider();
using (var scope = sp.CreateScope())
{
using (var dbContext = scope.ServiceProvider.GetRequiredService<WebApi1DbContext>())
{
try
{
dbContext.Database.OpenConnection();
dbContext.Database.EnsureCreated();
}
catch (Exception ex)
{
//Log errors or do anything you think it's needed
throw;
}
}
}
});
}
}
This action will allow you to use the DbContext to retrieve the DbContext you want for the test environment.
I am using nUnit Test and Signalr with .net framework 4.5 and I get error like :
System.Net.Http.HttpRequestException : An error occurred while sending the request.
----> System.Net.WebException : Unable to connect to the remote server
----> System.Net.Sockets.SocketException : No connection could be made because the target machine actively refused it 127.0.0.1:6790
Actually I need to test when hub.GetCallControlData(); being called then it should be invoked SetServer method
code :
[HubName("SignalRHub")]
public class SignalRHub1 : Hub
{
public override Task OnConnected()
{
return base.OnConnected();
}
public override Task OnDisconnected(bool stopCalled)
{
return base.OnDisconnected(stopCalled);
}
public void GetCallControlData()
{
Clients.Caller.SetServer("Server");
}
}
Test
[TestFixture]
public class SignalrHubTest1
{
public interface IClientContract
{
void SetServer(string s);
}
[Test]
public async Task MockSingalrHub()
{
var url = "http://localhost:6790";
var connectionId = Guid.NewGuid().ToString();
var mockRequest = new Mock<IRequest>();
var mockClients = new Mock<IHubCallerConnectionContext<dynamic>>();
var mockProxy = new Mock<IHubProxy>();
var _connection = new HubConnection(url);
var clientContract = new Mock<IClientContract>();
var mockHeaders = new Mock<INameValueCollection>();
mockHeaders.Setup(h => h["host"]).Returns(url);
mockRequest.Setup(r => r.Headers).Returns(mockHeaders.Object);
clientContract.Setup(_ => _.SetServer(It.IsAny<string>()));
mockClients.Setup(m => m.Caller).Returns(clientContract.Object);
var hub = new SignalRHub1()
{
Clients = mockClients.Object,
Context = new HubCallerContext(mockRequest.Object, connectionId)
};
var _hub = _connection.CreateHubProxy("SignalRHub");
mockProxy.Setup(x => x.Subscribe(It.IsAny<string>())).Returns(new Subscription());
_hub.On<string>("SetServer", x => SetServer(x));
await hub.OnConnected();
hub.GetCallControlData();
clientContract.VerifyAll();
await _connection.Start();
}
internal void SetServer(string s)
{
// throw new NotImplementedException();
}
}
Unit tests are meant to be isolated. There is no need to connect to an actual server in order to verify expected behavior.
Given the shown Hub,
[HubName("SignalRHub")]
public class SignalRHub1 : Hub {
public void GetCallControlData() {
Clients.Caller.SetServer("Server");
}
}
the following isolated unit test behaves as expected and verifies that the SetServer("Server") is invoked.
[TestClass]
public class SignalrHub1Tests {
public interface IClientContract {
void SetServer(string s);
}
[TestMethod]
public void GetCallControlData_Should_SetServer() {
//Arrange
var contract = new Mock<IClientContract>();
contract.Setup(_ => _.SetServer(It.IsAny<string>()));
var mockClients = new Mock<IHubCallerConnectionContext<dynamic>>();
mockClients.Setup(_ => _.Caller).Returns(contract.Object);
var hub = new SignalRHub1() {
Clients = mockClients.Object
};
//Act
hub.GetCallControlData();
//Assert
contract.Verify(_ => _.SetServer("Server"));
}
}
I'm using the Mediatr library to register and call my RequestHandlers.
Everything went fine until I started reading more about integrated tests.
PLEASE READ AFTER EDIT
I can't call my class which inherits from the RequesHandler.
My class looks like this:
public class MyRequestHandler : RequestHandler<MyRequest, MyResponse>
{
....
}
I'm not using the Meditr async and I'm using .net framework 4.7 instead of asp.net core, so, everything looks like returns me answers for asp.net core.
When I construct MyTestClass, to construct the RequestHandler I have to create a ServiceFactory and maybe this is the problem because I don't know how.
public MyClassTest()
{
ServiceFactory sv = null;
_mediator = new Mediator(sv);
}
EDIT
Providing more info
I have this Handler in my Application Layer
public class LogInUserByFormHandler : RequestHandler<LogInUserByFormRequest, LogInUserByFormResponse>
{
private readonly IValidator<LogInUserByFormRequest> _validator;
public LogInUserByFormHandler(IValidator<LogInUserByFormRequest> validator)
{
_validator = validator;
}
protected override LogInUserByFormResponse Handle(LogInUserByFormRequest request)
{
_validator.ValidateAndThrow(request);
var userInfo = GetUserInfo(request);
ValidateLogInUserByFormRules(userInfo);
var userLoginInfo = GetValidUserLoginInfo(request);
ValidateUserLoginInfoByFormRules(userLoginInfo);
var sessionKey = CreateUserSessionKey(userInfo);
var response = new LogInUserByFormResponse
{
UserName = request.UserName,
SessionKey = sessionKey,
UserId = userInfo.id_usuario
};
return response;
}
//A LOT OF CODE HERE, methods and etc
}
As it's possible to see, it implements the Mediatr.
On my Web Project on Presentation Layer, I used AutoFac to Inject the Handlers, so, any Request I do is always handled by the right method.
All I have to do is call, like this:
var logInByFormRequest = new LogInUserByFormRequest
{
UserName = viewModel.UserName,
Password = viewModel.Password
};
var response = _mediator.Send(logInByFormRequest).Result;
This works like a charm. The problem now is on the Test project. It references the Application as the Presentation Project does.
I don't know how to make the mediator.send find the right method.
EDIT²
Here comes my test code
[TestClass]
public class LogInUserByFormTest
{
private LogInUserByFormRequest CreateRequest(string userName, string password)
{
LogInUserByFormRequest request = new LogInUserByFormRequest
{
UserName = userName,
Password = password
};
return request;
}
[TestMethod]
[Description("")]
public void UserName_ShouldHave_Max_30Characters_Exception()
{
try
{
var request = CreateRequest("UserNameIsGreaterThanAllowed", "password");
var mediator = new Mock<IMediator>();
var response = mediator.Object.Send(request).Result;
}
catch (System.Exception ex)
{
throw;
}
}
}
The result (response) is always null and the mediator doesn't call the right handler.
EDIT3
Here is how I register the handlers and validators.
I use autofac. This class here is called on the global.asax
public class AutofacConfig
{
public static void ConfigureContainer()
{
var builder = new ContainerBuilder();
builder.RegisterControllers(Assembly.GetExecutingAssembly()).InstancePerRequest();
builder.RegisterType<Mediator>().As<IMediator>().InstancePerLifetimeScope();
builder.RegisterType<AutofacValidatorFactory>().As<IValidatorFactory>().SingleInstance();
builder.RegisterType<FluentValidationModelValidatorProvider>().As<ModelValidatorProvider>();
builder.RegisterType<RegistryManagerService>().As<IRegistryManagerService>().SingleInstance().WithParameter("appName", ConfigurationManager.AppSettings["APPNAME"]);
builder.Register<ServiceFactory>(context =>
{
var c = context.Resolve<IComponentContext>();
return t => c.Resolve(t);
});
builder.RegisterAssemblyTypes(Assembly.Load("Docspider.Application"))
.Where(x => x.Name.EndsWith("Handler"))
.AsImplementedInterfaces();
builder.RegisterAssemblyTypes(Assembly.Load("Docspider.Application"))
.Where(x => x.Name.EndsWith("Validator"))
.AsImplementedInterfaces()
.InstancePerLifetimeScope();
var container = builder.Build();
DependencyResolver.SetResolver(new AutofacDependencyResolver(container));
}
}
public class AutofacValidatorFactory : ValidatorFactoryBase
{
private readonly IComponentContext _context;
public AutofacValidatorFactory(IComponentContext context)
{
_context = context;
}
public override IValidator CreateInstance(Type validatorType)
{
if (_context.TryResolve(validatorType, out object instance))
{
var validator = instance as IValidator;
return validator;
}
return null;
}
}
For such an integration test you would need to configure the necessary dependencies. Since you have indicated that Autofac is being used then configure a container just as you would have in production. Use the container to get the mediator and perform the desired test.
For example.
[TestClass]
public class LogInUserByForm_IntegrartionTest {
private LogInUserByFormRequest CreateRequest(string userName, string password) {
LogInUserByFormRequest request = new LogInUserByFormRequest {
UserName = userName,
Password = password
};
return request;
}
IMediator BuildMediator() {
//AutoFac
var builder = new ContainerBuilder();
builder.RegisterAssemblyTypes(typeof(IMediator).GetTypeInfo().Assembly).AsImplementedInterfaces();
var mediatrOpenTypes = new[] {
typeof(IRequestHandler<,>)
};
foreach (var mediatrOpenType in mediatrOpenTypes) {
builder
.RegisterAssemblyTypes(typeof(LogInUserByFormRequest).GetTypeInfo().Assembly)
.AsClosedTypesOf(mediatrOpenType)
.AsImplementedInterfaces();
}
builder.Register<ServiceFactory>(ctx => {
var c = ctx.Resolve<IComponentContext>();
return t => c.Resolve(t);
});
//...all other needed dependencies.
//...
var container = builder.Build();
var mediator = container.Resolve<IMediator>();
return mediator;
}
[TestMethod]
[Description("")]
public async Task UserName_ShouldHave_Max_30Characters_Exception() {
try
{
//Arrange
var request = CreateRequest("UserNameIsGreaterThanAllowed", "password");
var mediator = BuildMediator();
//Act
var response = await mediator.Send(request);
//Assert
//...assert the expected values of response.
}
catch (System.Exception ex)
{
throw;
}
}
}
The above was modeled after the examples provided by MediatR.Examples.Autofac
I have an application in ASP.NET Core MVC (dnx46) RC1 with an AuthorizationHandler:
public class AppSumAuthAuthorizationHandler : AuthorizationHandler<AppSumAuthRequirement>
{
private readonly IUserRepository _userRepository;
private readonly IUserRoleRepository _userRoleRepository;
public AppSumAuthAuthorizationHandler(IUserRepository userRepository, IUserRoleRepository userRoleRepository)
{
_userRepository = userRepository;
_userRoleRepository = userRoleRepository;
}
protected override async void Handle(AuthorizationContext context, AppSumAuthRequirement requirement)
{
await HandleAsync(context,requirement);
}
protected override async Task HandleAsync(AuthorizationContext context, AppSumAuthRequirement requirement)
{
var currentUserName = context.User.Identity.Name;
var currentUser = await _userRepository.GetAsync(u => u.UserName == context.User.Identity.Name);
// Create user that does not yet exist
if(currentUser == null)
{
var user = new User(currentUserName);
/* Temporary add SysAdmin role */
using(new CreatedBySystemProvider(_userRepository))
{
_userRepository.Add(user);
await _userRepository.SaveChangesAsync();
if (string.Equals(currentUserName, #"BIJTJES\NilsG", StringComparison.CurrentCultureIgnoreCase))
{
user.AddRole(1);
}
currentUser = await _userRepository.GetAsync(u => u.Id == user.Id);
}
}
var resource = (Microsoft.AspNet.Mvc.Filters.AuthorizationContext) context.Resource;
var controllerActionDescriptor = resource.ActionDescriptor as ControllerActionDescriptor;
var controllerName = controllerActionDescriptor.ControllerName;
var actionName = controllerActionDescriptor.Name;
string moduleName;
try
{
// Get the name of the module
moduleName = ((ModuleAttribute)controllerActionDescriptor.ControllerTypeInfo.GetCustomAttributes(false).First(a => a.GetType().Name == "ModuleAttribute")).ModuleName;
}
catch(InvalidOperationException ex)
{
context.Fail();
throw new InvalidOperationException($"The Module Attribute is required on basecontroller {controllerName}.", ex);
}
var access = new Access(moduleName, controllerName, actionName);
if (await currentUser.HasPermissionTo(UrlAccessLevel.Access).OnAsync(access))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
}
The requirement class is empty:
public interface IAppSumAuthRequirement : IAuthorizationRequirement
{
}
public class AppSumAuthRequirement : IAppSumAuthRequirement
{
}
The Module attribute is also nothing special:
public class ModuleAttribute : Attribute
{
public string ModuleName { get; private set; }
public ModuleAttribute(string moduleName)
{
ModuleName = moduleName;
}
public override string ToString()
{
return ModuleName;
}
}
The exception filter:
public class JsonExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(ExceptionContext context)
{
var exception = context.Exception;
context.HttpContext.Response.StatusCode = 500;
context.Result = new JsonResult(new Error
{
Message = exception.Message,
InnerException = exception.InnerException?.InnerException?.Message,
Data = exception.Data,
ErrorCode = exception.HResult,
Source = exception.Source,
Stacktrace = exception.StackTrace,
ErrorType = exception.GetType().ToString()
});
}
}
and policy are configured in my Startup.cs:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
services.AddMvc(options =>
{
options.Filters.Add(new JsonExceptionFilterAttribute());
options.ModelBinders.Insert(0, new NullableIntModelBinder());
}).AddJsonOptions(options => {
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
options.SerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver();
});
// Security
services.AddAuthorization(options =>
{
options.AddPolicy("AppSumAuth",
policy => policy.Requirements.Add(new AppSumAuthRequirement()));
});
}
and the policy is set on all controllers, by inheriting BaseController:
[Authorize(Policy = "AppSumAuth")]
public class BaseController : Controller
{
public BaseController()
{
}
}
So, in my handler, I get the controllername, actionname and modulename (from the attribute set on the controllers):
[Module("Main")]
When this attribute is not set on a controller, I would like to catch the exception and report this back to the developer calling the controller and deny access. To do this, I've added:
catch(InvalidOperationException ex)
{
context.Fail();
throw new InvalidOperationException($"The Module Attribute is required on basecontroller {controllerName}.", ex);
}
The JsonExceptionFilter is called perfectly when there is an exception in the controllers. It is however not called when there is an error in the AuthorizationHandler.
So the question:
How can I get the Exceptions to be caught by the JsonExceptionFilter?
What am I doing wrong?
Solution:
Startup.cs:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// For Windows Auth!
app.UseIISPlatformHandler();
app.UseStaticFiles();
app.UseExceptionHandler(AppSumExceptionMiddleware.JsonHandler());
app.UseMvc();
}
And my middleware:
public class AppSumExceptionMiddleware
{
public static Action<IApplicationBuilder> JsonHandler()
{
return errorApp =>
{
errorApp.Run(async context =>
{
var exception = context.Features.Get<IExceptionHandlerFeature>();
if (exception != null)
{
var exceptionJson = Encoding.UTF8.GetBytes(
JsonConvert.SerializeObject(new AppSumException(exception.Error),
new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver()
})
);
context.Response.ContentType = "application/json";
await context.Response.Body.WriteAsync(exceptionJson, 0, exceptionJson.Length);
}
});
};
}
}
Action filter can be used as a method filter, controller filter, or global filter only for MVC HTTP requests. In your case you need to use a middleware, as
Middleware is component that "sit" on the HTTP pipeline and examine
all requests and responses.
As you want to works with exception, you may use ready-to-use ExceptionHandler middleware:
app.UseExceptionHandler(errorApp =>
{
errorApp.Run(async context =>
{
context.Response.StatusCode = 500; // for example
var error = context.Features.Get<IExceptionHandlerFeature>();
if (error != null)
{
var ex = error.Error;
// custom logic
}
});
});