I recently read a great post from Mike Webb over at BizStream about how to quickly integrate API features into our Kentico 12 MVC applications. ๐
If you haven't worked with Web API 2, ASP.NET's API framework, I definitely recommend going and giving that post a read to get started. ๐๐
What I'd like to do here is introduce some best practices and conventions I've discovered while working with Web API 2 in Kentico over the past several years (WiredViews has been integrating Web API 2 into Kentico since Kentico 8!).
Routing ๐บ
Attribute Routing
ASP.NET MVC allows developers to customize routing through the RouteCollection.MapRoute()
method using a convention-over-configuration approach.
Web API 2 offers a similar approach, using Convention Based Routing, with HttpConfiguration.Routes.MapHttpRoute()
, but I'd recommend using Attribute Routing.
Attribute Routing allows for more easily customized and hierarchical / resource-oriented URL structures.
Since APIs are usually discovered by URL patterns and not by following links, being able to identify what URL pattern matches each method of an ApiController
is very helpful.
Route Providers
If you want to centralize some route configuration, there are hook points within the framework that help with this.
A custom DefaultDirectRouteProvider
class can be used to insert a consistent route prefix in one location.
An implementation might look like this:
protected override string GetRoutePrefix(HttpControllerDescriptor controllerDescriptor)
{
string controllerPrefix = base.GetRoutePrefix(controllerDescriptor);
return controllerPrefix is null
? "api"
: string.Format("{0}/{1}", "api", controllerPrefix);
}
This can be a nice alternative to adding [RoutePrefix("api/...")]
on all of your ApiController
s. ๐
I also recommend you prefix your API routes, to keep them separate from your MVC routes, since you'll be hosting both frameworks in the same application.
API Documentation
Swagger
While we're looking at URLs and discoverabilty, there's an important tool we can easily add to our application to expose those API endpoints (and even test them out!)
If you haven't heard of Swagger, it's a set of tools for displaying interactive dashboards backed by custom built APIs. Using Swagger in a project can be very helpful when the developers consuming an API (ex: front-end) are not the developers building the API (ex: back-end).
Users can view the entire API of an application and test it out by filling in some fields and clicking "Try it out".
If you are still not sure what I'm talking about, check out this live demo
Swagger-Net
Swagger, however, is not specific to a language or framework, so we need a way to integrate it into our ASP.NET application.
This is where Swagger-Net comes in. Swagger-Net is a wrapper around Swagger that consumes XML comments on our ApiController
classes and then auto-generates the correct Swagger UI for our API.
There is another project called Swashbuckle which I used for years and provides similar functionality, but the maintainer's focus has shifted to ASP.NET Core.
I now recommend using Swagger-Net for your ASP.NET based Kentico 12 MVC applications.
Here are example User
and ApiController
classes with some XML doc comments:
namespace Kentico12MVC
{
/// <summary>
/// A User in the application
/// </summary>
public class User
{
/// <summary>
/// The User's id
/// </summary>
public int Id { get; set; }
/// <summary>
/// The User's full name
/// </summary>
public string Name { get; set; }
}
/// <summary>
/// Provides access to Kentico users
/// </summary>
[RoutePrefix("user")]
public class UserApiController : ApiController
{
/// <summary>
/// Returns all users
/// </summary>
[Route("")]
[ResponseType(typeof(IEnumerable<User>))]
public IHttpActionResult GetUsers()
{
var users = UserInfoProvider
.GetUsers()
.AsEnumerable()
.Select(u => new User { Id = u.UserID, Name = u.UserName });
return Ok(users);
}
/// <summary>
/// Returns the user with the Id specified in the route parameter
/// </summary>
/// <param name="id"></param>
[Route("{id:int}")]
[ResponseType(typeof(User))]
public IHttpActionResult GetUser(int id)
{
var user = UserInfoProvider.GetUserInfo(id);
return Ok(new User { Id = user.UserID, Name = user.UserName });
}
}
}
The above will produce the following Swagger UI in your site:
Pretty cool! ๐๐๐
I created a GitHub Gist which details some of the custom configuration I've done to integrate Swagger-Net into Web API 2.
Error Handling
IExceptionHandler
When exceptions happen in ASP.NET MVC we have several ways to prevent those from bubbling up to the framework and crashing the application.
No one wants their users to see the ugly yellow screen of death. ๐จ
To handle errors globally, in ASP.NET, we typically add an event handler in our Global.asax.cs
file as follows:
public void Application_Error(object sender, EventArgs e)
{
ApplicationErrorLogger.LogLastApplicationError();
}
In Web API 2, the best approach is to create and register a custom IExceptionHandler
.
This type has one public method, HandleAsync
, which is called whenever an unhandled exception is generated in the request/response pipeline.
You can use this method to assign different IHttpActionResult
instances to the ExceptionHandlerContext.Result
property. This result will then be sent back to the requester.
Custom Exception Types
I always create custom exception types in my applications that represent un-recoverable errors (ex: NotAuthenticatedException
, NotFoundException
, BadRequestException
).
Using some reflection we can determine if the exception caught by the IExceptionHandler
was one of these custom types and then return the correct HttpStatusCode
and error message:
private (HttpStatusCode code, ApiError error) BuildError(Exception exception)
{
var buildApiError = CaptureErrorId(Guid.NewGuid());
switch (exception)
{
case HttpException httpEx:
return ((HttpStatusCode)httpEx.GetHttpCode(), buildApiError("HTTP_EXCEPTION", "Server error"));
case NotAuthenticatedException notAuthNEx:
return (HttpStatusCode.Unauthorized, buildApiError("NOT_AUTHENTICATED", "This endpoint requires authentication"));
case NotAuthorizedException notAuthZEx:
return (HttpStatusCode.Forbidden, buildApiError("NOT_AUTHORIZED", "The request was not authorized to perform that action"));
case BadRequestException badRequestEx:
return (HttpStatusCode.BadRequest, buildApiError("BAD_REQUEST", "The request was malformed"));
case NotFoundException notFoundEx:
return (HttpStatusCode.NotFound, buildApiError("NOT_FOUND", "The requested data could not be found"));
default:
return (HttpStatusCode.InternalServerError, buildApiError("APPLICATION_EXCEPTION", "Server error"));
}
}
This is also a great place to centralize logging, using Kentico's EventLogProvider.LogException()
method or something like Serilog. ๐
I have an example of the code I use for custom error handling in this GitHub Gist
Serialization
Centrally Stored Serialization Configuration
When compared to MVC features in your application, API features tend to rely more on serialization. This is primarily due to the fact that all data going into and out of your API will likely be in a JSON format.
If you start making custom IHttpActionResult
types you will need to create API responses using a JsonMediaTypeFormatter
if you want your custom result to be more easily testable.
The easiest way to centralize JsonMediaTypeFormatter
configuration is to add it to your IoC container and then inject it as a dependency where you need it.
See the above Gist for custom error handling to see an example of this.
This means when I configure the formatting for my API, I resolve it from my container:
// In my Web Api configuration class
var config = new HttpConfiguration();
// ...
config.Formatters.Add(container.Resolve<JsonMediaTypeFormatter>());
Serialization and Database Date Precision
In addition to centrally configuring your serialization in your DI container, you'll want to double check that your serialization settings are configured to match the requirements for your database.
Mine usually look something like this:
builder.Register(c => new JsonSerializerSettings
{
Formatting = Formatting.None,
// UTC Date serialization configuration
DateFormatHandling = DateFormatHandling.IsoDateFormat,
DateParseHandling = DateParseHandling.DateTimeOffset,
DateTimeZoneHandling = DateTimeZoneHandling.Utc,
// Use X digits of precision (fffffff) to match data store datetimeoffset(X)
DateFormatString = "yyyy-MM-ddTHH:mm:ss.fffK",
ContractResolver = new CamelCasePropertyNamesContractResolver()
})
If you are using a specific datetimeoffset
data type in your Page Type or Custom Module Class fields, you'll want to ensure your JSON serialization matches that precision, otherwise you could lose data during serialization/de-serialization. ๐ฏ
This issue often comes into play if you are using a "last modified date" for Optimistic Concurrency Control - you need that de-serialized date from the client to match what's in the database exactly.
You might also want to occasionally serialize/de-serialize JSON in your MVC code.
If that is a requirement and you want your JsonSerializerSettings
to match what you have configured in your API pipeline, then use a custom ValueProviderFactory
that handles application/json
requests.
An example of how to do this can be found in this GitHub Gist
Define your API Contracts
Request and Response Types
When writing ApiControllers
to expose data from Kentico, our first inclination might be to diretly return the *Info
and custom PageType objects we query from *Provider
classes.
There are two problems I see with this approach.
- โ It exposes system internals (much of which is related to data persistence in Kentico) to the outside world. This can lead to tight coupling and leaky abstractions.
- โ The Kentico types coming from the database have a lot of fields and values, most of which are not relevant to the consumers of an API. This leads to overly heavy payloads and confusing API responses.
In the same way that Kentico recommends creating View Model classes to transport data to our views, we should also use custom model types at the API presentation layer (our ApiController
classes).
I follow the convention of naming these types *Request
and *Response
classes. It makes it easier to talk about them, identify their purpose, and avoid the two issues above.
Request Validation โ
There is a great library to help validate that the request class instances are valid.
I recommending using FluentValidation to make flexible, readable, and testable validation requirements.
Here is an example request class and its associated validator class:
public class CustomerCreateRequest
{
public int Id { get; set; }
public string FirstName { get; set; }
}
public class CustomerRequestValidator :
AbstractValidator<CustomerCreateRequest> {
public CustomerRequestValidator() {
RuleFor(c => c.Id).GreaterThan(0);
RuleFor(c => c.FirstName).NotEmpty();
}
}
I prefer this type of validation to the attribute validation, mostly because this is much more powerful.
I also recommend making your endpoints granular in functionality. The more an endpoint does, the larger the opportunity for something to go wrong which means handling rolling back transactions to ensure consistency.
The less that needs to be validated per-request, the less that can be invalid.
If each endpoint is considered a transaction by a consumer (a reasonable expectation!) and that "request transaction" maps to at most one or two database / system transactions, your system state will be easier to manage and harder to put into an invalid or incomplete state. ๐
Cross-Cutting concerns via Aspect Oriented Programming (AOP)
Logging
In addition to adding logging to your custom IExceptionHandler
, you will possibly want to add logging to other parts of your application - places where requests are being handled successfully.
My recommendation is, since you're going to be using an IoC container anyway, to apply logging through AOP - more specifically with decoration.
Note: all these patterns apply just as well to your MVC code.
Assume you have a IUserService
that returns users as follows:
public interface IUserService
{
UserInfo GetUser(int userId);
}
Let's also assume you have an implementation that looks as follows:
public class KenticoUserService : IUserService
{
public UserInfo GetUser(int userId) =>
UserInfoProvider.GetUserInfo(userId);
}
Now, if you want to log each time a user is successfully found and also if a user isn't found, where would you put these log statements? ๐ค
I would argue you would put them nowhere in the code we see above. ๐ฆ
Instead we should decorate the IUserService
with another implementation that only does one thing - logging. It then delegates the process of retrieving the user to the KenticoUserService
. ๐ง
Here is an example implementation:
public class LogUserServiceDecorator : IUserService
{
private readonly IUserService original;
public LogUserServiceDecorator(IUserService original)
{
Guard.Against.Null(original, nameof(original));
this.original = original;
}
public UserInfo GetUser(int userId)
{
var user = original.GetUser(userId);
if (user is null)
{
Log.Debug("User with {userId} not found", userId);
}
else
{
Log.Debug("User with {userId} found!", userId);
}
return user;
}
}
The original
class field above is the "decorated" type. The LogUserServiceDecorator
type fulfills the IUserService
contract and will be the type actually provided in our application whenever we ask for an IUserService
to be injected in a constructor.
LogUserServiceDecorator
holds within it the "original" KenticoUserService
(as IUserService
), which is called the userId
parameter to do the actual work of getting the user.
Our decorating service then logs information about result returned by the original
service.
Finally, the decorating service returns the user
, fulfilling the IUserService
contract while the rest of the application, and the "original" implementation, are none-the-wiser. ๐คฏ
Each class serves one purpose and cross-cutting concerns (like logging) don't increase the length or complexity of our business logic. ๐
FYI, I'm using
original
above as a demonstrative name - there's no convention when doing AOP to name this way.
Caching ๐ธ
You're probably using output caching for your MVC application, especially given how data heavy a complex Kentico site can be.
You should probably also use caching in your API layer.
Kentico provides the CacheHelper.Cache()
method that calls the delegate you pass to it and caches the result of that delegate before returning the value to the caller.
All you need to do is set the cache keys that your cached item depends on (what data changes should invalidate the cache), and how long you want your data cached.
Here's a nice little helper class to make caching easier:
public static class CacheSettingsExtensions
{
/// <summary>
/// Assigns an array of cache dependency keys to the given CacheSettings
/// and a function which returns a result to be cached
/// </summary>
public static T SetCacheDependency<T>(
this CacheSettings cs,
string[] cacheDependencyKeys,
Func<T> getResult)
{
cs.CacheDependency = cs.Cached
? CacheHelper.GetCacheDependency(cacheDependencyKeys)
: cs.CacheDependency;
return getResult();
}
}
You would use the above class as follows:
var cacheDependency = new string[] { "cms.user|all" };
IEnumerable<UserInfo> users = CacheHelper
.Cache(cs => cs.SetCacheDependency(
cacheDependency,
() => UserInfoProvider.GetUsers().AsEnumerable()),
new CacheSettings(120, "api.user.all");
return users;
I bet you already know how I'd recommend integrating caching into your API layer! That's right - AOP! ๐
To mimic the previous LogUserServiceDecorator
example, we could create a CacheUsersServiceDecorator
and replace UserInfoProvider.GetUsers().AsEnumerable()
with the call to your "original" service.
Understand OWIN vs ASP.NET pipelines
If you are confused about why you have a Startup.cs
class when you already have Global.asax.cs
, what OWIN is, and why Web API 2 (if you squint real hard) looks like ASP.NET Core, then understanding how it's different from ASP.NET MVC can be helpful. ๐คจ
It can also be helpful to keep the following concepts in mind when designing or integrating code that works with either Web API 2 or MVC but not both.
Startup.cs
The Startup.cs
comes from a re-thinking of what it means for an ASP.NET app to run without IIS and the ever-growing System.Web
library.
It's a new type of ASP.NET entry-point that defines a series of "middleware".
Middleware are pieces of code linked as a pipeline that can operate on the HTTP request, in the order they are defined, and a generated response, in the reverse order.
OWIN and Katana
So what are OWIN and Katana and how do they tie into Web API 2?
Here's some definitions
Web API 2: Microsoft's solution for an MVC-like pattern that returns XML/JSON natively, instead of server rendered HTML views, which works on the Project Katana infrastructure.
Project Katana: Microsoft's implementation of the OWIN contract (a dictionary) stating where HTTP request and response data is stored as it travels through a processing pipeline (a bunch of delegates/middleware).
OWIN: Open Web Interface for .NET was an attempt to define a way to separate .NET web services and the servers they run on. OWIN applications should be able to run outside of IIS,
System.Web
, and potentially on an operating system other than Windows. (Sounds a lot like ASP.NET Core!!)
You can get a ton of great historical information about OWIN and Katana in Microsoft's docs. I read this stuff for fun in the evening with a glass of cold beer.
Here's a video presentation from Brock Allen (of Identity Server fame), detailing what makes OWIN different from the classic ASP.NET architecture, way back in early 2014.
Knowledge
Let's review the many points we've covered:
- Routing
- Use Attribute Routing to best align with how an API wants to be designed.
- Centralize route configuration using the hooks the framework provides.
- API Documentation
- Swagger is a great UI for documenting and exploring APIs.
- Swagger-Net provides an easy way to integrate Swagger in our ASP.NET apps.
- Error Handling
- Create your own custom error handler to catch and log unhandled exceptions.
- Use custom exception types to return helpful HTTP status codes to clients.
- Serialization
- Put your serialization configuration in an IoC container for serialization consistency.
- Make sure your date serialization matches your database types.
- Define Your API Contracts
- Use Request and Reponse models at the edge of your API to abstract away Kentico implementation details.
- FluentValidation is a great way to declaratively validate your request models.
- Use Aspect Oriented Programming (AOP)
- Logging is a cross-cutting concern and should be applied through decoration, not inline in your business logic.
- Kentico provides helpful cache methods which can also be used with decoration.
- OWIN and ASP.NET
- The
Startup.cs
is a new entry / integration point for ASP.NET. - OWIN and Katana are precursors to ASP.NET Core and are the foundation for Web API 2.
- The
If you've stuck with me this far then you're a pretty dedicated developer who values knowledge or maybe you're just a little bored. ๐คฃ
Either way, I hope these thoughts of mine on how to use ASP.NET Web API 2 in your Kentico 12 MVC projects was helpful.
If you are looking for additional Kentico content, checkout the Kentico tag here on DEV:
or my Kentico 12: Design Patterns series.
Top comments (0)