In this tutorial, we will create a strongly typed Settings API that uses Lazy loading, Cache, Reflection and Entity Framework to manage application settings in ASP.NET MVC. I will explain why its better to store settings in a database, how using reflection can make it easier to add new settings, and how to group the settings into a wrapper that uses Lazy loading and cache.
The Settings API in Action
Before we start building the Settings API, lets take a look of what it looks like in action:
public SettingsController(ISettings settings)
{
// example of saving
_settings.General.SiteName = "Talk Sharp";
_settings.Seo.HomeMetaTitle = "Talk Sharp";
_settings.Seo.HomeMetaKeywords = "ASP.NET MVC";
_settings.Seo.HomeMetaDescription = "Welcome to Talk Sharp";
_settings.Save();
}
Notice there are two properties in the code above named General
and Seo
. These are read only properties that contain custom settings which are derived from a SettingsBase
type. There are two reasons for doing this which are:
- So that settings are grouped together and easier to find when accessing them.
- So that only the settings in the group being accessed are loaded from the database (this is done by using lazy loaded properties).
Why Store Settings in a Database?
You might be thinking. Why store the settings in a database when you can use the Web.config file and the .NET ConfigurationManager. The reason is because changes to the Web.config file will restart the application. There’s also the added issue of making sure Web.config files are kept in-sync when your application runs on multiple servers.
To avoid these issues, it’s best to store settings that need to be modified at runtime inside a database. In this article we will use the Entity Framework ORM to handle the storage and retrieval of the settings.
How to Create the Settings API?
There are quite a few steps involved so I have put together an example project you can download.
Here is an overview of what’s to come:
- Create the ASP.NET Web Application
- Create the Setting model and the Entity Framework DbContext
- Create the SettingsBase class for loading and saving settings with reflection
- Create the GeneralSettings and SeoSettings classes and make them inherit SettingsBase
- Create the Settings class (A simple entry point to manage all types)
1. Create Web Application
To get started we need to create a new ASP.NET Web Application and then add Entity Framework to the project using NuGet. This example is using Visual Studio 2013 with .NET Framework 4.5.1 and no authentication. Once you have added Entity Framework you will need to add a connection string to the Web.config file.
<connectionStrings>
<add name="MvcSettings" connectionString="Server=(localdb)\v11.0;Initial Catalog=MvcSettings;AttachDbFileName=|DataDirectory|\MvcSettings.mdf;Trusted_Connection=True" providerName="System.Data.SqlClient" />
</connectionStrings>
2. Entity Framework Model
The first thing we need to do is create a model that Entity Framework can use to store each setting. Create a new class called Setting
and add it to the Models folder.
public class Setting
{
public string Name { get; set; }
public string Type { get; set; }
public string Value { get; set; }
}
The Setting
model is very simple, it contains three public properties that will be used to store each settings name, value and type it belongs to. The type is the name of each class which inherits the SettingsBase
class. We will see how this works in step 5. The following screenshot shows how the settings for this example will look.
Figure 1: The generated settings table with multiple saved settings.
3. Create UnitOfWork (DbContext)
Create a new folder called Services at the root of the project and add a new class called UnitOfWork
. Here’s what the UnitOfWork
class should look like.
public interface IUnitOfWork
{
DbSet<Setting> Settings { get; set; }
int SaveChanges();
}
public class UnitOfWork : DbContext, IUnitOfWork
{
public UnitOfWork() : base("MvcSettings") { }
public DbSet<Setting> Settings { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Setting>()
.HasKey(x => new { x.Name, x.Type });
modelBuilder.Entity<Setting>()
.Property(x => x.Value)
.IsOptional();
base.OnModelCreating(modelBuilder);
}
}
There’s not much going on here, we are overriding the OnModelCreating
method so that we can specify a composite key of Name and Type. We are also making the Value field optional, and we are implementing an IUnitOfWork
interface that contains a call to SaveChanges
. Notice the string passed into the base constructor matches the name of the connection string inside the Web.config file.
4. Create SettingsBase
The SettingsBase
class is what contains the reflection magic. Whenever we need to create a new group of settings, we do so by inheriting this class. Create a new class called SettingsBase
and add it to the Services folder.
public abstract class SettingsBase
{
// 1 name and properties cached in readonly fields
private readonly string _name;
private readonly PropertyInfo[] _properties;
public SettingsBase()
{
var type = this.GetType();
_name = type.Name;
// 2
_properties = type.GetProperties();
}
public virtual void Load(IUnitOfWork unitOfWork)
{
// ARGUMENT CHECKING SKIPPED FOR BREVITY
// 3 get settings for this type name
var settings = unitOfWork.Settings.Where(w => w.Type == _name).ToList();
foreach (var propertyInfo in _properties)
{
// get the setting from the settings list
var setting = settings.SingleOrDefault(s => s.Name == propertyInfo.Name);
if (setting != null)
{
// 4 assign the setting values to the properties in the type inheriting this class
propertyInfo.SetValue(this, Convert.ChangeType(setting.Value, propertyInfo.PropertyType));
}
}
}
public virtual void Save(IUnitOfWork unitOfWork)
{
// 5 load existing settings for this type
var settings = unitOfWork.Settings.Where(w => w.Type == _name).ToList();
foreach (var propertyInfo in _properties)
{
object propertyValue = propertyInfo.GetValue(this, null);
string value = (propertyValue == null) ? null : propertyValue.ToString();
var setting = settings.SingleOrDefault(s => s.Name == propertyInfo.Name);
if (setting != null)
{
// 6 update existing value
setting.Value = value;
}
else
{
// 7 create new setting
var newSetting = new Setting()
{
Name = propertyInfo.Name,
Type = _name,
Value = value,
};
unitOfWork.Settings.Add(newSetting);
}
}
}
}
The class name and array of property info is stored in readonly instance fields (1). The reason for this is because the call to GetProperties
(2) is a slow operation so we don’t want to do it more than once. The Load
method takes an IUnitOfWork instance as a parameter and is used to load the group of settings in the derived type from the database (3). Each property that exists in the derived type is assigned a value from the database and is converted to the appropriate type (4).
The Save
method also takes an IUnitOfWork instance as a parameter and is used to save the group of settings in the derived type to the database. This is done by first loading the existing settings belonging to the derived type (5) then looping through the properties array and either updating the existing setting (6) or adding a new one (7).
The reason for passing the UnitOfWork into both methods rather than the constructor is because the UnitOfWork might need to be disposed between calls (cached objects).
5. GeneralSettings and SeoSettings
Create a new class called GeneralSettings
and a new class called SeoSettings
.
public class GeneralSettings : SettingsBase
{
public string SiteName { get; set; }
public string AdminEmail { get; set; }
}
public class SeoSettings : SettingsBase
{
public string HomeMetaTitle { get; set; }
public string HomeMetaDescription { get; set; }
}
The GeneralSettings
and SeoSettings
classes both inherit the SettingsBase
class and therefore contain Load
and Save
methods. You can add as many properties to these classes as you like and they will automatically be populated from the database when the Load
method is called.
6. Settings
The Settings
class is used to group all the settings types together into a wrapper. It provides the entry point to the Settings API. Create a new class called Settings
with the following code.
public interface ISettings
{
GeneralSettings General { get; }
SeoSettings Seo { get; }
void Save();
}
public class Settings : ISettings
{
// 1
private readonly Lazy<GeneralSettings> _generalSettings;
// 2
public GeneralSettings General { get { return _generalSettings.Value; } }
private readonly Lazy<SeoSettings> _seoSettings;
public SeoSettings Seo { get { return _seoSettings.Value; } }
private readonly IUnitOfWork _unitOfWork;
public Settings(IUnitOfWork unitOfWork)
{
// ARGUMENT CHECKING SKIPPED FOR BREVITY
_unitOfWork = unitOfWork;
// 3
_generalSettings = new Lazy<GeneralSettings>(CreateSettings<GeneralSettings>);
_seoSettings = new Lazy<SeoSettings>(CreateSettings<SeoSettings>);
}
public void Save()
{
// only save changes to settings that have been loaded
if (_generalSettings.IsValueCreated)
_generalSettings.Value.Save(_unitOfWork);
if (_seoSettings.IsValueCreated)
_seoSettings.Value.Save(_unitOfWork);
_unitOfWork.SaveChanges();
}
// 4
private T CreateSettings<T>() where T : SettingsBase, new()
{
var settings = new T();
settings.Load(_unitOfWork);
return settings;
}
}
First of all the interface defines what Settings types will be available and a Save
method to update the settings. Each Settings class that inherits SettingsBase
is stored in a readonly field using Lazy<T>
(1) so that the settings class is only constructed when the property is first accessed (2).
The readonly fields are assigned a new Lazy<T>
instance in the constructor (3) and passed the CreateSettings<T>
factory method. This factory method is called when the property is first accessed. The CreateSettings<T>
method uses generics and type constraints to make sure only types that inherit SettingsBase
can be constructed.
You can find a more detailed explanation of generics and type constraints in the book C# In Depth by Jon Skeet.
Where’s the Cache?
The Settings
class is usable in it’s current state but you may wish to add caching so that the application does not slow down due to reflection. If you want to add cache to the Settings
class you will need to add a new constructor and a new factory method that first checks the cache before creating the settings objects.
private readonly ICache _cache;
public Settings(IUnitOfWork unitOfWork, ICache cache)
{
// ARGUMENT CHECKING SKIPPED FOR BREVITY
_unitOfWork = unitOfWork;
_cache = cache;
_generalSettings = new Lazy<GeneralSettings>(CreateSettingsWithCache<GeneralSettings>);
_seoSettings = new Lazy<SeoSettings>(CreateSettingsWithCache<SeoSettings>);
}
private T CreateSettingsWithCache<T>() where T : SettingsBase, new()
{
// this is where you would implement loading from ICache
throw new NotImplementedException();
}
This article is pretty long already so I’ve skipped out the impementation details of loading the settings types from cache. I will be writing an article on implementing ICache with an Autofac module in the future.
Example Usage
The following example shows how to use the Settings API by creating a new action in the HomeController:
public ActionResult Save()
{
using (var uow = new UnitOfWork())
{
var settings = new Settings(uow);
settings.General.SiteName = "Talk Sharp";
settings.Seo.HomeMetaDescription = "Welcome to Talk Sharp";
settings.Save();
var settings2 = new Settings(uow, null);
string output = string.Format("SiteName: {0} HomeMetaDescription: {1}",
settings2.General.SiteName,
settings2.Seo.HomeMetaDescription
);
return Content(output);
}
}
Now if you run the application and browse to /home/save you should see the following output:
SiteName: Talk Sharp HomeMetaDescription: Welcome to Talk Sharp
You should also see the settings saved in the Settings table as they appear in Figure 1.
Final Thoughts
It’s been a lengthy process creating this Settings API for managing settings. In my opinion its worth the initial effort because adding new settings is very easy. The settings classes can be expanded by adding new properties because reflection takes care of mapping them to the settings records in the database. I think this is better than the alternative of writing out the retrieval and conversion for each property manually.