ASP.NET MVC Optimization Series: Part 2 - Data Layer

July 8th, 2015

If we want our web application to act like a native app, we need some speedy access. Today, we'll go over how to optimize your data layer for the front-end.

In the last post, we set up our basic project to include all of the packages and starter code we need to begin the project. If we need additional packages, I'll address that in the post.

In this post, we'll lay the groundwork for our data layer. We will build a different kind of repository with a Unit Of Work and implement some caching features to deliver our data to the client just a little bit faster.

To review, I'm planning to write a web application in ASP.NET MVC and convert it to act and look like a native app. Two of the goals I'm focusing on for this project are speed and UI (User Interface).

But before we start, first, we need models.

No, no, not those models! DATA models.

Creating the Data Models

We need a way to translate our data into classes so we can work with the object instances on the client-side.

The best way to quickly generate our models is to use the XML from each service and let Visual Studio do the heavy lifting (also see bold note below).

  1. First, navigate a browser to the Sessions feed (https://cmprod-speakers.azurewebsites.net/api/SessionsData)
  2. Save the data locally.
  3. Using Notepad (or Notepad++), open the file, select all, and copy the XML code (Ctrl-C)
  4. In Visual Studio, create a blank class.
  5. In the Edit menu, select Paste Special -> Paste XML as Classes.
  6. You now have a complete hierarchy of your XML document that you can Serialize/Deserialize (see note below).
  7. Repeat steps 1-5 to perform the same task for your other feed, Speakers (https://cmprod-speakers.azurewebsites.net/api/SpeakersData)

You'll receive a full class hierarchy of classes representing the XML Nodes...your data models.

NOTE: Here are some reference material for some of the topics above:

There is a problem, though.

We need to make our data models a little more programmer-friendly instead of the long names that trace down through the XML nodes and aggregate into a classname.

Now that we have our data models, we can start building our data layer.

Repositories and Unit Of Work

Our repositories are not what you think. We'll retrieve data from a web service instead of a database. That means we'll structure our repository a little bit differently than a database repository.

I created a WebRepository that would accept a URL and return records from the web service.

Repository\WebRepository.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Net;
using CodemashApp.Interfaces;
 
namespace CodemashApp.Repository
{
    public class WebRepository<T>: IWebRepository<T> where T: class
    {
        public Uri Uri { getset; }
        
        public WebRepository(Uri url)
        {
            Uri = url;
        }
 
        public IEnumerable<T> GetAll()
        {
            var data = GetData(Uri);
            return GetRecords(data);
        }
 
        public virtual IEnumerable<T> GetRecords(string data)
        {
            return null;
        }
 
        private string GetData(Uri uri, string headerType = "xml")
        {
            using (var web = new WebClient())
            {
                web.Headers["Content-Type"] = string.Format("application/{0}", headerType);
                return web.DownloadString(uri);
            }
        }
 
        public IQueryable<T> Find(Expression<Func<T, bool>> predicate)
        {
            return null;
        }
 
        public T GetById(string id)
        {
            var newUri = new Uri(Uri+"/"+id);
            var data = GetData(newUri);
            return GetRecords(data).FirstOrDefault();
        }
 
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
 
        protected virtual void Dispose(bool disposing)
        {
            if (!disposing) return;
        }
    }
}

Notice the GetById? If we decide to pull only one session or speaker, we can easily include their Id and immediately get their information instead of the entire 500k download and then grab the specific speaker or session.

So now we create our SessionRepository and SpeakerRepository.

Repository\SessionRepository.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml.Serialization;
using CodemashApp.Models;
 
namespace CodemashApp.Repository
{
    public class SessionRepository : WebRepository<CodemashSession>
    {
        public SessionRepository(Uri url): base(url) { }
 
        public override IEnumerable<CodemashSession> GetRecords(string data)
        {
            var serializer = new XmlSerializer(typeof(ArrayOfPublicSessionDataModel));
            var buffer = Encoding.UTF8.GetBytes(data);
            using (var stream = new MemoryStream(buffer))
            {
                var sessions = (ArrayOfPublicSessionDataModel)serializer.Deserialize(stream);
                return sessions.Sessions;
            }
        }
    }
}

Repository\SpeakerRepository.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Xml.Serialization;
using CodemashApp.Models;
 
namespace CodemashApp.Repository
{
    public class SpeakerRepository : WebRepository<CodemashSpeaker>
    {
        public SpeakerRepository(Uri url) : base(url) { }
 
        public override IEnumerable<CodemashSpeaker> GetRecords(string data)
        {
            var serializer = new XmlSerializer(typeof(ArrayOfPublicSpeakerDataModel));
            var buffer = Encoding.UTF8.GetBytes(data);
            using (var stream = new MemoryStream(buffer))
            {
                var speakers = (ArrayOfPublicSpeakerDataModel)serializer.Deserialize(stream);
                return speakers.Speakers;
            }
        }
    }
}

We can now easily return data from the web service and return a record or many records.

Caching the Data

Since we want our speed out of this web app, we need speed! While it may be fast enough, we want it to perform like a native app.

So I built a cache layer on top of my repositories. As DBAs say, the best database call you can make is no call at all.

Repository\CacheSessionRepository.cs

using System;
using System.Collections.Generic;
using System.Web;
using CodemashApp.Models;
 
namespace CodemashApp.Repository
{
    public class CacheSessionRepository : SessionRepository
    {
        private static readonly object CacheLockObject = new object();
 
        public CacheSessionRepository(Uri url) : base(url) { }
 
        public override IEnumerable<CodemashSession> GetRecords(string data)
        {
            const string cacheKey = "CachedCodemashSessionRepository:GetRecords";
            IEnumerable<CodemashSession> result = HttpRuntime.Cache[cacheKey] as List<CodemashSession>;
            if (result != nullreturn result;
            
            lock (CacheLockObject)
            {
                result = HttpRuntime.Cache[cacheKey] as List<CodemashSession>;
                if (result != nullreturn result;
                    
                result = base.GetRecords(data);
                HttpRuntime.Cache.Insert(cacheKey, result, null,
                    DateTime.Now.AddMinutes(15), TimeSpan.Zero);
            }
            return result;
 
        }
    }
}

Repository\CacheSpeakerRepository.cs

using System;
using System.Collections.Generic;
using System.Web;
using CodemashApp.Models;
 
namespace CodemashApp.Repository
{
    public class CacheSpeakerRepository : SpeakerRepository
    {
        private static readonly object CacheLockObject = new object();
 
        public CacheSpeakerRepository(Uri url) : base(url) { }
 
        public override IEnumerable<CodemashSpeaker> GetRecords(string data)
        {
            const string cacheKey = "CachedCodemashSpeakerRepository:GetRecords";
            IEnumerable<CodemashSpeaker> result = HttpRuntime.Cache[cacheKey] as List<CodemashSpeaker>;
            if (result == null)
            {
                lock (CacheLockObject)
                {
                    result = HttpRuntime.Cache[cacheKey] as List<CodemashSpeaker>;
                    if (result == null)
                    {
                        result = base.GetRecords(data);
                        HttpRuntime.Cache.Insert(cacheKey, result, null,
                                                 DateTime.Now.AddMinutes(15), TimeSpan.Zero);
                    }
                }
            }
            return result;
        }
        
    }
}

With this caching in place, we can use either the regular SpeakerRepository to retrieve a non-cached version or our CacheSpeakerRepository to retrieve a cached version of the data.

The only thing missing is the Unit of work to bind it together.

Unit of Work

This Unit of Work we're using will not be similar to the other Unit Of Work patterns that we've done in the past. This one won't have a DbContext attached to it.

I have a simple CodemashUnitOfWork with two simple repositories. We'll use the cached implementations of the WebRepository for Sessions and Speakers.

When we need the data, we call the cached repository. If the data is already there, just return the data instead of making an expensive web service call. You know...with that network latency and all.

UnitOfWork\CodemashUnitOfWork.cs

using System;
using CodemashApp.Configuration;
using CodemashApp.Repository;
 
namespace CodemashApp.UnitOfWork
{
    public class CodemashUnitOfWork
    {
        private CacheSessionRepository _sessionRepository;
        private CacheSpeakerRepository _speakerRepository;
 
        #region Repositories
 
        public CacheSessionRepository SessionRepository
        {
            get
            {
                return _sessionRepository
                       ?? (_sessionRepository = 
                        new CacheSessionRepository(
                            new Uri(CodemashConfiguration.SessionUrl)));
            }
        }
 
        public CacheSpeakerRepository SpeakerRepository
        {
            get
            {
                return _speakerRepository
                       ?? (_speakerRepository =
                        new CacheSpeakerRepository(
                            new Uri(CodemashConfiguration.SpeakerUrl)));
            }
        }
 
        #endregion
 
    }
}

Again, pretty simple since we are just reading data and don't need Entity Framework. We don't need a DbContext, we just need a central entry point for wrangling our repositories.

Conclusion

Now that we have our data layer defined and ready to go, we can start looking at the client-side. The client-side will become more prominent in the next couple of posts.

It will also become the most critical part of this series.

See you soon!

Did you notice a better way to make the data layer even faster? Post a comment below.

ASP.NET MVC Optimization Series: