Assume we have a multi-tenant blog application. Each user of the application may have a number of blogs hosted by the service.
Our API allows for both reading and writing of blog posts. In some cases specifying a BlogId is optional, for example, getting all posts tagged with ASP.NET:
/api/posts?tags=aspnet
If we wanted to view all posts tagged with ASP.NET on a specific blog, we could request:
/api/posts?blogId=10&tags=aspnet
Some API methods require a valid BlogId, such as when creating a new blog post:
POST: /api/posts
{
"blogid" : "10",
"title" : "This is a blog post."
}
The BlogId needs to be validated on the server to ensure it belongs to the current (authenticated) user. I would also like to infer the user's default blogId if it is not specified in the request (for simplicity you can assume that default is the user's first blog).
We have an IAccountContext
object that contains information about the current user. This can be injected if necessary.
{
bool ValidateBlogId(int blogId);
string GetDefaultBlog();
}
In ASP.NET Web API what would be the recommended approach to:
- If the BlogId is specified either in the message body or uri, validate it to ensure it belongs to the current user. Throw a 400 error if not.
- If the BlogId is not specified in the request, retrieve the default BlogId from
IAccountContext
and make it available to the controller action. I don't want the controller to be aware of this logic which is why I don't want to callIAccountContext
directly from my action.
[Update]
Following discussions on Twitter and taking into account @Aliostad's advice, I decided to treat the Blog as a resource and make it part of my Uri template (so it is always required) i.e.
GET api/blog/1/posts -- get all posts for blog 1
PUT api/blog/1/posts/5 -- update post 5 in blog 1
My query logic for loading single items was updated to load by Post id and blog id (to avoid tenants loading/updating other peoples posts).
The only thing left to do was validate the BlogId. It is a shame that we can't use validation attributes on Uri parameters otherwise @alexanderb's recommendation would have worked. Instead I opted to use an ActionFilter:
public class ValidateBlogAttribute : ActionFilterAttribute
{
public IBlogValidator Validator { get; set; }
public ValidateBlogAttribute()
{
// set up a fake validator for now
Validator = new FakeBlogValidator();
}
public override void OnActionExecuting(HttpActionContext actionContext)
{
var blogId = actionContext.ActionArguments["blogId"] as int?;
if (blogId.HasValue && !Validator.IsValidBlog(blogId.Value))
{
var message = new HttpResponseMessage(HttpStatusCode.BadRequest);
message.ReasonPhrase = "Blog {0} does not belong to you.".FormatWith(blogId);
throw new HttpResponseException(message);
}
base.OnActionExecuting(actionContext);
}
}
public class FakeBlogValidator : IBlogValidator
{
public bool IsValidBlog(int blogId)
{
return blogId != 999; // so we have something to test
}
}
Validating the blogId is now simply a case of decorating my controller/action with [ValidateBlog]
.
Virtually everyone's answers helped in the solution but I have marked as @alexanderb's as the answer since it did not couple the validation logic inside my controller.