Here are a few things you can do to make your life easier when starting a new ASP.NET Core project, or reducing duplicate code without using boilerplate.
Validate model state automatically
The usual way would be to have this in almost every action in all controllers:
if (!ModelState.IsValid)
{
// return bad request, add errors, or redirect somewhere
}
Instead, I like to have my model state automatically validated. One way to do that is to create a filter and apply it to your actions, or even better, apply it globally.
Here’s the filter I use for my APIs:
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState); // returns 400 with error
}
}
}
Then I just apply it globally:
// In Startup.cs ConfigureServices
services.AddMvc(opts =>
{
// apply [ValidateModel] globally
opts.Filters.Add(typeof(ValidateModelAttribute));
});
And ta-da! All of my models are now automatically validated with no duplicate code. Note that a similar feature is planned for ASP.NET Core 2.1.
Handle database exceptions globally
Another thing I find myself doing often is handling DbUpdateException
s. For instance, checking if an entity exists in the database before creating it (and returning a 409 if it does). A better approach would be to ask for forgiveness rather than permission. In other words, try creating the entity with no checks, and if a DbUpdateException
is raised, return an error.
To do that, I created a DbUpdateExceptionHandler
middleware that handles database-related exceptions globally (especially duplicate value inserts):
public class DbUpdateExceptionHandler
{
private readonly RequestDelegate _next;
public DbUpdateExceptionHandler(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
try
{
await _next(httpContext);
}
catch (DbUpdateException ex)
{
await HandleExceptionAsync(httpContext, ex);
}
}
private static async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
object result;
var code = 400;
if (exception.InnerException.Message.ToLower().Contains("duplicate")) // Might be different for different databases, this is just an example
{
code = 409;
result = new
{
Error = "Error updating database. Duplicate value."
};
}
else
{
result = new
{
Error = "Error updating database."
};
}
context.Response.ContentType = "application/json"; // Will vary
context.Response.StatusCode = code;
await context.Response.WriteAsync(JsonConvert.SerializeObject(result));
}
}
public static class HandleDbUpdateExceptionExtensions
{
public static IApplicationBuilder UseDbUpdateExceptionHandler(this IApplicationBuilder builder)
{
return builder.UseMiddleware<DbUpdateExceptionHandler>();
}
}
Then use the middleware:
// In Startup.cs Configure
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseDbUpdateExceptionHandler(); // handle DB exceptions globally
app.UseExceptionHandler();
}
Quickly access user claims
When using bearer authentication without Identity services, accessing user claims usually requires a bit of legwork. For example, to get the UserId:
var userId = User.FindFirst(ClaimTypes.NameIdentifier).Value;
or even
var userId = User?.Claims
.FirstOrDefault(c => c.Type == "sub")?.Value;
Usually I need to access user claims in many different locations, resulting in unnecessary duplicate code.
One solution would be to create extension methods that handle this kind of thing, for example:
public static class ClaimsPrincipalExtensions
{
public static string GetUserId(this ClaimsPrincipal user)
{
return user.Claims?.FirstOrDefault(c => c.Type == "sub")?.Value;
}
public static string GetUserName(this ClaimsPrincipal user)
{
return user.Claims?.FirstOrDefault(c => c.Type == "name")?.Value;
}
}
Now you can do this in your controllers:
var userId = User.GetUserId() // ta-da!
Sorting, filtering, and pagination
Another thing I find myself writing a lot of code for is sorting, filtering, and pagination. Usually that is done via repeated usage of LINQ’s Where
, OrderBy
, Skip
, and Take
. Sometimes you even end up writing a micro-DSL (ie. comma separated sort/filter values) for every project.
This is especially relevant in APIs where, for example, instead of having
GetPopularPosts
andGetNewPosts
, you just haveGetPosts
with a query parameter that specifies what kind of posts to get
Sadly OData for ASP.NET Core is still in early stages, being completely undocumented. To save myself the trouble of having to write shared sort/filter/paginate code for each project, I decided to package my solution into a nuget - Sieve. Sieve adds sorting filtering, and pagination functionality to your projects out of the box.
Here’s an example of using Sieve to allow sorting, filtering, and pagination of Post
entities in an API:
[HttpGet]
public JsonResult GetPosts(SieveModel sieveModel) // captures sort/filter/paginate
{
var result = _dbContext.Posts.AsNoTracking(); // Makes read-only queries faster
result = _sieveProcessor.ApplyAll(sieveModel, result); // Returns `result` after applying the sort/filter/page query in `SieveModel` to it
return Json(result.ToList());
}
Now you can send the request:
GET /GetPosts
?sorts= LikeCount,CommentCount,-created // sort by likes, then comments, then descendingly by date created
&filters= LikeCount>10, Title@=awesome title, // filter to posts with more than 10 likes, and a title that contains the phrase "awesome title"
&page= 1 // get the first page...
&pageSize= 10 // ...which contains 10 posts
Quick out-of-the-box authentication
When starting a new project without using the default authentication templates, it’s not always easy to test how your project would function with [Authorize]
attributes in a development environment.
I usually use my Demo Cierge Server to quickly add authentication to my projects. Cierge is an especially good solution for this kind of testing since it is really easy to self-host with no code modifications, uses no passwords, and is also written in ASP.NET Core. Plus, you can always use the freely available demo server for quick testing.
An example of using Cierge for an API (no additional work required):
// In Startup.cs ConfigureServices
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(opts =>
{
opts.Audience = "https://cierge.azurewebsites.net";
opts.Authority = "demo-resource-server";
opts.IncludeErrorDetails = true;
if (Env.IsDevelopment())
opts.RequireHttpsMetadata = false;
});
And ta-da! You got auth! Now you can obtain an authentication token from Cierge (via the OIDC implicit flow) and get testing!
(check out the demo resource server for an example API project)
You can also do the same to quickly add authentication to MVC projects that you’ve created without Individual User Accounts authentication - here’s an example project of just that.
To reiterate, the demo server should only be used for development, and will probably not be able to handle the load in production environments.
Quickly changing the authenticated user in development
When using token based auth, I find it helpful to be able to quickly change the authenticated user without having to go through actually authenticating as that user.
One way to do that would be to create middleware that allows you to specify the authenticated user (obviously should only be enabled in development). Here’s the one I use:
public class SetDevelopmentUser
{
private readonly RequestDelegate _next;
public SetDevelopmentUser(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext httpContext)
{
// get "dev_user_id" and "dev_user_name" from query strings and use them
var devUserId = httpContext.Request.Query["dev_user_id"].ToString();
var devUserName = httpContext.Request.Query["dev_user_name"].ToString();
devUserId = String.IsNullOrEmpty(devUserId) ? "test_user_id" : devUserId;
devUserName = String.IsNullOrEmpty(devUserName) ? "test_user_name" : devUserName;
var identity = new ClaimsIdentity("DevAuth");
identity.AddClaim(new Claim("sub", devUserId));
identity.AddClaim(new Claim("name", devUserName));
httpContext.User = new ClaimsPrincipal(identity);
await _next(httpContext);
}
}
public static class SetDevelopmentUserExtensions
{
public static IApplicationBuilder UseSetDevelopmentUser(this IApplicationBuilder builder)
{
return builder.UseMiddleware<SetDevelopmentUser>();
}
}
Then register the middleware:
// In Startup.cs Configure
if (env.IsDevelopment())
{
app.UseSetDevelopmentUser();
}
Now I can make requests while quickly specifying what user should be used through query strings. Note that the user with given Id still has to exist in your database if you’re using foreign keys and the like.
If you want something simpler, you can simply disable authentication for development scenarios:
// In Startup.cs ConfigureService
services.AddMvc(opts =>
{
if (Env.IsDevelopment())
opts.Filters.Add(new AllowAnonymousFilter());
});
Conclusion
If you have any more tips/errata feel free to contact me to have it included here (with due credit, of course).
Discussion
Find discussion for this article on Hacker News and Reddit.