Summary
In this blog post I’m going to demonstrate a simple .Net MVC project that uses MS SQL server to access data. Then I’m going to show how to use Redis caching to cache your results to reduce the amount of traffic hitting your database. Finally, I’m going to show how to use the AutoFac IOC container to tie it all together and how you can leverage inversion of control to to break dependencies and unit test your code.
AutoFac
The AutoFac IOC container can be added to any .Net project using the NuGet manager. For this project I created an empty MVC project and added a class called AutofacBootstrapper to the App_Start directory. The class contains one static method called Run() just to keep it simple. This class contains the container builder setup that is described in the instructions for AutoFac Quick Start: Quick Start.
Next, I added .Net library projects to my solution for the following purposes:
BusinessLogic – This will contain the business classes that will be unit tested. All other projects will be nothing more than wire-up logic.
DAC – Data-tier Application.
RedisCaching – Redis backed caching service.
StoreTests – Unit testing library
I’m going to intentionally keep this solution simple and not make an attempt to break dependencies between dlls. If you want to break dependencies between modules or dlls, you should create another project to contain your interfaces. For this blog post, I’m just going to use the IOC container to ensure that I don’t have any dependencies between objects so I can create unit tests. I’m also going to make this simple by only providing one controller, one business logic method and one unit test.
Each .Net project will contain one or more objects and each object that will be referenced in the IOC container must use an interface. So there will be the following interfaces:
IDatabaseContext – The Entity Framework database context object.
IRedisConnectionManager – The Redis connection manager provides a pooled connection to a redis server. I’ll describe how to install Redis for windows so you can use this.
IRedisCache – This is the cache object that will allow the program to perform caching without getting into the ugly details of reading and writing to Redis.
ISalesProducts – This is the business class that will contain one method for our controller to call.
Redis Cache
In the sample solution there is a project called RedisCaching. This contains two classes: RedisConnectionManager and RedisCache. The connection manager object will need to be setup in the IOC container first. That needs the Redis server IP address, which would normally be read from a config file. In the sample code, I fed the IP address into the constructor at the IOC container registration stage. The second part of the redis caching is the actual cache object. This uses the connection manager object and is setup in the IOC container next, using the previously registered connection manager as a paramter like this:
builder.Register(c => new RedisConnectionManager("127.0.0.1")) .As<IRedisConnectionManager>() .PropertiesAutowired() .SingleInstance(); builder.Register(c => new RedisCache(c.Resolve<IRedisConnectionManager>())) .As<IRedisCache>() .PropertiesAutowired() .SingleInstance();
In order to use the cache, just wrap your query with syntax like this:
return _cache.Get("ProductList", 60, () => { return (from p in _db.Products select p.Name); });
The code between the { and } represents the normal EF linq query. This must be returned to the anonymous function call: ( ) =>
The cache key name in the example above is “ProductList” and it will stay in the cache for 60 minutes. The _cache.Get() method will check the cache first, if the data is there, then it returns the data and moves on. If the data is not in the cache, then it calls the inner function, causing the EF query to be executed. The result of the query is then saved to the cache server and then the result is returned. This guarantees that the next query in less than 60 minutes will be in the cache for direct retrieval. If you dig into the Get() method code you’ll notice that there are multiple try/catch blocks that will error out if the Redis server is down. For a situation where the server is down, the inner query will be executed and the result will be returned. In a production situation your system would run a bit slower and you’ll notice your database is working harder, but the system keeps running.
A precompiled version of Redis for Windows can be downloaded from here: Service-Stack Redis. Download the files into a directory on your computer (I used C:\redis) then you can open a command window and navigate into your directory and use the following command to setup a windows service:
redis-server –-service-install
Please notice that there are two “-” in front of the “service-install” instruction. Once this is setup, then Redis will start every time you start your PC.
The Data-tier
The DAC project contains the POCOs, the fluent configurations and the context object. There is one interface for the context object and that’s for AutoFac’s use:
builder.Register(c => new DatabaseContext("Server=SQL_INSTANCE_NAME;Initial Catalog=DemoData;Integrated Security=True")) .As<IDatabaseContext>() .PropertiesAutowired() .InstancePerLifetimeScope();
The context string should be read from the configuration file before being injected into the constructor shown above, but I’m going to keep this simple and leave out the configuration pieces.
Business Logic
The business logic library is just one project that contains all the complex classes and methods that will be called by the API. In a large application you might have two or more business logic projects. Typically though, you’ll divide your application into independent APIs that will each have their own business logic project as well as all the other wire-up projects shown in this example. By dividing your application by function you’ll be able to scale your services according to which function uses the most resources. In summary, you’ll put all the complicated code inside this project and your goal is to apply unit tests to cover all combination of features that this business logic project will contain.
This project will be wired up by AutoFac as well and it needs the caching and the data tier to be established first:
builder.Register(c => new SalesProducts(c.Resolve<IDatabaseContext>(), c.Resolve<IRedisCache>())) .As<ISalesProducts>() .PropertiesAutowired() .InstancePerLifetimeScope();
As you can see the database context and the redis caching is injected into the constructor of the SalesProjects class. Typically, each class in your business logic project will be registered with AutoFac. That ensures that you can treat each object independent of each other for unit testing purposes.
Unit Tests
There is one sample unit test that performs a test on the SalesProducts.Top10ProductNames() method. This test only tests the instance where there are more than 10 products and the expected count is going to be 10. For effective testing, you should test less than 10, zero, and exactly 10. The database context is mocked using moq. The Redis caching system is faked using the interfaces supplied by StackExchange. I chose to setup a dictionary inside the fake object to simulate a cached data point. There is no check for cache expire, this is only used to fake out the caching. Technically, I could have mocked the caching and just made it return whatever went into it. The fake cache can be effective in testing edit scenarios to ensure that the cache is cleared when someone adds, deletes or edits a value. The business logic should handle cache clearing and a unit test should check for this case.
Other Tests
You can test to see if the real Redis cache is working by starting up SQL Server Management Studio and running the SQL Server Profiler. Clear the profiler, start the MVC application. You should see some activity:
Then stop the MVC program and start it again. There should be no change to the profiler because the data is coming out of the cache.
One thing to note, you cannot use IQueryable as a return type for your query. It must be a list because the data read from Redis is in JSON format and it’s de-serialized all at once. You can de-searialize and serialize into a List() object. I would recommend adding a logger to the cache object to catch errors like this (since there are try/catch blocks).
Another aspect of using an IOC container that you need to be conscious of is the scope. This can come into play when you are deploying your application to a production environment. Typically developers do not have the ability to easily test multi-user situations, so an object that has a scope that is too long can cause cross-over data. If, for instance, you set your business logic to have a scope of SingleInstance() and then you required your list to be special to each user accessing your system, then you’ll end up with the data of the first person who accessed the API. This can also happen if your API receives an ID to your data for each call. If the object only reads the data when the API first starts up, then you’ll have a problem. This sample is so simple that it only contains one segment of data (top 10 products). It doesn’t matter who calls the API, they are all requesting the same data.
Other Considerations
This project is very minimalist, therefore, the solution does not cover a lot of real-world scenarios.
- You should isolate your interfaces by creating a project just for all the interface classes. This will break dependencies between modules or dlls in your system.
- As I mentioned earlier, you will need to move all your configuration settings into the web.config file (or a corresponding config.json file).
- You should think in terms of two or more instances of this API running at once (behind a load-balancer). Will there be data contention?
- Make sure you check for any memory leaks. IOC containers can make your code logic less obvious.
- Be careful of initialization code in an object that is started by an IOC container. Your initialization might occur when you least expect it to.
Where to Get The Code
You can download the entire solution from my GitHub account by clicking here. You’ll need to change the database instance in the code and you’ll need to setup a redis server in order to use the caching feature. A sql server script is provided so you can create a blank test database for this project.