ASP.NET Core Request Post Processing
If you’ve worked with Asp.Net Core to create APIs then you have more than likely run into situations where you needed to return different sets of data for the same model. One way to accomplish this is request post processing using an ActionFilter. Lets start with a common scenario. We have an internal enterprise application and we have different types of users in the system. Users can call our API to get data on other users depending on their permission levels. We have three different types of users: Admin, HelpDesk, and Employee. Our class looks like this:
public class User { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string UserName { get; set; } public UserType UserType { get; set; } public string CreatedBy { get; set; } public DateTime? DateCreated { get; set; } public DateTime? DateUpdated { get; set; } public string UpdatedBy { get; set; } } public enum UserType { Admin = 1, HelpDesk, Employee }
I’ve also created a fake user to generate a User model populated with data using Bogus. If you aren’t familiar with Bogus, it’s a library to generate fake data using a fluent API.
public class FakeUser : Faker<User> { public FakeUser() { RuleFor(m => m.Id, r => r.Random.Int()); RuleFor(m => m.FirstName, r => r.Name.FirstName()); RuleFor(m => m.LastName, r => r.Name.LastName()); RuleFor(m => m.UserName, r => r.Person.UserName); RuleFor(m => m.UserType, r => UserType.Employee); RuleFor(m => m.CreatedBy, r => r.Person.UserName); RuleFor(m => m.DateUpdated, r => r.Date.Recent()); RuleFor(m => m.DateCreated, r => r.Date.Recent()); RuleFor(m => m.UpdatedBy, r => ""); } }
Setting Up The Requests
So, we have our User and UserType that we can now build our API for. We’re going to start off by installing RequestInjector and then wire our requests in the start up using the IRequest marker interface. From there we will create two requests: GetUsersRequest and GetUserRequest. The first one returns a list of users with fake generated data and the second returns a single user in the same manner.
public class GetUsersRequest : IRequest { public async Task<IActionResult> Handle() { public int Id { get; set; } return new OkObjectResult(new List<ApiRequestPostProcess.Core.Models.User> { new FakeUser().Generate(), new FakeUser().Generate() }) { DeclaredType = typeof(List<ApiRequestPostProcess.Core.Models.User>) }; } } public class GetUserRequest : IRequest { public int Id { get; set; } public async Task<IActionResult> Handle() { return new OkObjectResult(new FakeUser().Generate()) { DeclaredType = typeof(ApiRequestPostProcess.Core.Models.User) }; } }
You’ll notice in here that we are setting the DeclaredType. This is so that we can access this property later in the filter without having to do as much reflection and manipulation. Now that we have our models and requests, we are going to need to create a strategy pattern to handle each case. For HelpDesk and Admin UserTypes, we are going to return all properties since they have access to everything. For calls made by an Employee though, we are going to want to prevent DateUpdated, DateCreated, CreatedBy, and UpdatedBy from being returned.
Strategy Pattern For Post Processing
Let’s go ahead and implement the strategy pattern to handle our post processing implementation. Since the filters in ASP.NET Core are singletons, we are not going to be able to inject our strategy implementation. We wouldn’t be able to anyway since we don’t know the implementation until further down in the code. That means that we need later binding than what constructor Dependency Injection provides. One option is to use Service Location and get a named implementation from the container as needed. Another option is to create a factory and allow it to create the implementation based on the UserType, which is what I opted for in this case.
public interface IUserResponseStrategyFactory { IUserResponseStrategy Create(UserType userType); } public class UserResponseStrategyFactory : Dictionary<UserType, Func<IUserResponseStrategy>>, IUserResponseStrategyFactory { public UserResponseStrategyFactory() { Add(UserType.Admin, () => new AdminUserResponseStrategy()); Add(UserType.Employee, () => new EmployeeUserResponseStrategy()); Add(UserType.HelpDesk, () => new HelpDeskUserResponseStrategy()); } public IUserResponseStrategy Create(UserType userType) { return this[userType](); } }
We’ll pass the factory implementation to the filter when we wire it up in the start up. Next we’re going to need our strategy implementations. The HelpDesk and Admin implementations are essentially left blank since we aren’t currently filtering on them. You could skip these and do nothing when that UserType comes up in the filter, but usually requirements change over time and I create them so they are there for the future.
public class EmployeeUserResponseStrategy : IUserResponseStrategy { public void Execute(User user) { user.CreatedBy = null; user.DateCreated = null; user.DateUpdated = null; user.UpdatedBy = null; } public void Execute(List<User> users) { users.ForEach(m => { m.CreatedBy = null; m.DateCreated = null; m.DateUpdated = null; m.UpdatedBy = null; }); } } public class AdminUserResponseStrategy : IUserResponseStrategy { public void Execute(User user) { //Return everything } public void Execute(List<User> users) { //Return everything } } public class HelpDeskUserResponseStrategy : IUserResponseStrategy { public void Execute(User user) { //Return everything. } public void Execute(List<User> users) { //Return everything. } }
You’ll notice we are setting the properties we don’t want returned to null. Asp.Net Core allows you set a property on the SerializerSettings to ignore null values. Observe the following:
services.AddMvc(config => { config.ModelMetadataDetailsProviders.Add(new RequestInjectionMetadataProvider()); config.ModelBinderProviders.Insert(0, new QueryModelBinderProvider(provider)); config.Filters.Add(new ResponseFilter(new UserResponseStrategyFactory())); }) .AddJsonOptions(options => { options.SerializerSettings.Converters.Add(new RequestInjectionHandler<IRequest>(provider)); options.SerializerSettings.NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore; });
MVC ActionFilter OnActionExecuted
The next thing we’re going to write is a filter to process the request after it has been executed.
public class ResponseFilter : ActionFilterAttribute { IUserResponseStrategyFactory userResponseStrategyFactory; public ResponseFilter(IUserResponseStrategyFactory userResponseStrategyFactory) { this.userResponseStrategyFactory = userResponseStrategyFactory; } public override void OnActionExecuted(ActionExecutedContext context) { var userTypeHeader = context.HttpContext.Request.Headers.SingleOrDefault(m => m.Key == "UserType").Value.FirstOrDefault(); if (!Enum.TryParse(userTypeHeader, out UserType userType)) base.OnActionExecuted(context); if (context.Result is OkObjectResult) { var okResult = context.Result as OkObjectResult; dynamic userObject = null; if (okResult.DeclaredType == typeof(List <User >)) userObject = okResult.Value as List <User >; else if (okResult.DeclaredType == typeof(User)) userObject = okResult.Value as User; else base.OnActionExecuted(context); var strategy = userResponseStrategyFactory.Create(userType); strategy.Execute(userObject); } base.OnActionExecuted(context); } }
We start off by passing our factory implementation to the filter from start up. From there we grab the UserType header we are passing in, parse out the UserType, and then check to see that it was an OkObjectResult. If it is, we continue by casting it to that type to get access to the Value and DeclaredType properties. The DeclaredType property that we discussed earlier in the post is now coming in to play and it is how we differentiate between a list of Users and just a User. Once we have our object set up, we go ahead and grab the strategy implementation we want from the factory and execute it.
Testing From Swagger
I always use Swagger when I’m building APIs these days and I highly recommend it. In the image below you can see one of the test results. Notice that the 4 properties we nulled out are not being returned.
I’ve included tests for both the GetUser and GetUsers requests in the source code that can be accessed from the application and navigating to /swagger. As always, you can find the source code for everything discussed in this post on my GitHub, here. Thanks for reading.