I use Dependency Injection (DI) quite a bit in my ASP.NET projects, particularly in Web API and MVC web applications. Recently, I had a need to implement a caching layer in one of my MVC apps, and such a layer would be best used if it could be injected into my clients layer (e.g. the layer that called an API and handled responses from the same). The solution I came up with seemed to be pretty simple, so I wanted to share it here.
In this post, we're going to set up a simple MVC project that consumes a Web API and implements a caching layer.
Requirements
For this project, I'm using two of my favorite NuGet packages:
- StructureMap.MVC5, which provides Dependency Injection capabilities.
- RestSharp, which enables easy consumption of web APIs.
The sample project, which is over on GitHub, is a fully-functional implementation of the strategy described in this post. Feel free to check it out, branch it, download it, whatever!
Setting Up the API
First, let's take a look at our sample API. Here's the class DateNumberObject
, which represents a response returned from the API to the web project:
public class DateNumberObject
{
public DateTime CurrentDate { get; set; }
public int RandomNumber { get; set; }
public DateNumberObject()
{
CurrentDate = DateTime.Now;
Random rand = new Random();
RandomNumber = rand.Next(1, 200);
}
}
As you can see, all this class does is return the current date and time and a random number.
We can now build our API, which is also rather simple. Here's a snippet from the API's controller:
[RoutePrefix("samples")]
public class SampleController : ApiController
{
[HttpGet]
[Route("date")]
public IHttpActionResult GetDateAndNumber()
{
return Ok(new DateNumberObject());
}
}
That's all our API will do: return the current date and a random number. Since there's no caching taking place at the service layer, we will need to implement caching at the web layer.
Speaking of the web layer, in our sample solution it is an MVC5 project, and we will be using my favorite Dependency Injection tool, StructureMap.
Setting up StructureMap
If you've never set up StructureMap in your MVC projects, you'll want to read this section; otherwise, skip to the next section.
The first thing we need to do is download the StructureMap.MVC5 NuGet package, which will add a DependencyInjection folder and a StructuremapMvc file to our app:
Inside the Dependency Resolution folder will be a file called DefaultRegistry.cs, which will initially look something like this:
namespace WebApiCacheDemo.Mvc.DependencyResolution {
using Caching;
using StructureMap.Configuration.DSL;
using StructureMap.Graph;
public class DefaultRegistry : Registry {
#region Constructors and Destructors
public DefaultRegistry() {
Scan(
scan => {
scan.TheCallingAssembly();
scan.WithDefaultConventions();
scan.With(new ControllerConvention());
});
}
#endregion
}
}
The way StructureMap works is that it keeps instances of classes that need to be "injected" into other classes in a Container class, and uses these instances any time a particular interface is called for in another class. You'll see exactly what this means in the next section.
The Uncached Example
Let's build the uncached example first, and we can begin by setting up the MVC client. For this project we'll be using RestSharp (which I have also written about before) to consume the API responses, so be sure to download the NuGet package.
Because we're using Dependency Injection, we need our clients to be injectable into our controllers. For this reason, we will make both an interface and a class for our client, like so:
public interface ISampleClient : IRestClient
{
DateNumberObject GetSampleDateAndNumberUncached();
}
public class SampleClient : RestClient, ISampleClient
{
public SampleClient()
{
BaseUrl = new Uri("http://localhost:58566/");
}
public DateNumberObject GetSampleDateAndNumberUncached()
{
RestRequest request = new RestRequest("samples/date", Method.GET);
var response = Execute<DateNumberObject>(request);
return response.Data;
}
}
Note that we don't need to register this client with StructureMap because the naming follows the standard conventions (ISampleClient
maps to SampleClient
).
Now we need our controller...
[RoutePrefix("Home")]
public class HomeController : Controller
{
private ISampleClient _sampleClient;
public HomeController(ISampleClient sampleClient)
{
_sampleClient = sampleClient;
}
[HttpGet]
[Route("Uncached")]
[Route("")]
[Route("~/")]
public ActionResult Uncached()
{
var model = _sampleClient.GetSampleDateAndNumberUncached();
return View(model);
}
}
...which returns a view:
@model WebApiCacheDemo.Contracts.Samples.DateNumberObject
@{
ViewBag.Title = "Uncached Date Sample";
}
<h2>Uncached Results</h2>
<div class="row">
<span><strong>Current Date:</strong></span>
@Model.CurrentDate
</div>
<div class="row">
<span><strong>Random Number:</strong></span>
@Model.RandomNumber
</div>
When we run this, we can see that the view returns the newest date and time, as shown in this gif:
That's exactly what we would expect to see, of course. Now, we can get down to implementing a cache at the web layer for this.
The Cached Example
The first thing we need is our CacheService interface and implementation, which I totally stole from this StackOverflow answer:
public class InMemoryCache : ICacheService
{
public T GetOrSet<T>(string cacheKey, Func<T> getItemCallback) where T : class
{
T item = MemoryCache.Default.Get(cacheKey) as T;
if (item == null)
{
item = getItemCallback();
MemoryCache.Default.Add(cacheKey, item, DateTime.Now.AddMinutes(30));
}
return item;
}
}
public interface ICacheService
{
T GetOrSet<T>(string cacheKey, Func<T> getItemCallback) where T : class;
}
We also need our SampleClient
class updated to use this cache service, like so:
public class SampleClient : RestClient, ISampleClient
{
private ICacheService _cache;
public SampleClient(ICacheService cache)
{
_cache = cache;
BaseUrl = new Uri("http://localhost:58566/");
}
public DateNumberObject GetSampleDateAndNumber()
{
return _cache.GetOrSet("SampleDateAndNumber", () => GetSampleDateAndNumberUncached());
}
public DateNumberObject GetSampleDateAndNumberUncached()
{
RestRequest request = new RestRequest("samples/date", Method.GET);
var response = Execute<DateNumberObject>(request);
return response.Data;
}
}
NOTE: In my structure, I am intentionally implementing the cached and uncached operations as separate methods in the same client, so that either one can be used. This may or may not be the correct usage in your application.
Further, we need to update our controller:
[RoutePrefix("Home")]
public class HomeController : Controller
{
...
[HttpGet]
[Route("Cached")]
public ActionResult Cached()
{
var model = _sampleClient.GetSampleDateAndNumber();
return View(model);
}
}
The last step is to register the ICacheService
implementing in StructureMap's DefaultRegistry.cs class:
namespace WebApiCacheDemo.Mvc.DependencyResolution {
...
public class DefaultRegistry : Registry {
public DefaultRegistry() {
...
var inMemoryCache = new InMemoryCache();
For<ICacheService>().Use(inMemoryCache);
}
}
}
Now, when we run this sample, we will see the cached values for the date and number, as shown in the following gif:
Now we've successfully implemented our cache AND used Dependency Injection in the process!
Summary
With this structure, we've successfully implemented a caching layer into our MVC application, all while using StructureMap to provide Dependency Injection. This structure allows us to potentially swap cache providers (in case we, say, move to a database-located cache for server farms) without too much trouble, as well as injecting the cache service into our existing clients.
Don't forget to check out the sample project over on GitHub, and feel free to point out how you've used this implemention or where it can be improved in the comments.
Happy Coding!