In an ASP.NET MVC application (C#) have a factory-like class that generates settings objects of ISettingType
for my application. However, some settings are very simple and a standard built-in such as a string or number type value can also suffice. I think that the way that I'm doing this is very much a code smell though.
These settings are persisted within my database in a table that's acting as a key/value store. The key is a NVARCHAR(50)
column being the name of the setting also acting as the primary key, and the value field is an XML
column. Manipulating the XML directly is unnecessary, and I have various classes that implement the ISettingType
interface, which includes methods for de/serialization from/to XML. As I mentioned some of these settings are very simple and only require a single string or number value.
The factory looks like this:
public static class SettingsFactory { private const string MODEL_NAMESPACE = "Company.Models.Administration.SettingTypes."; public static ISettingType Create(ApplicationSetting appSetting) { object setting = DeserializeValue(appSetting); var settingType = setting as ISettingType; if (settingType != null) return settingType; throw new InvalidOperationException("Setting is not of a complex type requiring an ISettingType"); } public static T Create<T>(ApplicationSetting appSetting) { if (typeof(ISettingType).IsAssignableFrom(typeof(T))) return (T)Create(appSetting); return (T) DeserializeValue(appSetting); } private static object DeserializeValue(ApplicationSetting appSetting) { XElement rootElement = XElement.Load(appSetting.SettingValue); if (rootElement.HasElements) { Type settingType = Type.GetType(MODEL_NAMESPACE + appSetting.SettingName); if (settingType != null) { using (XmlReader reader = rootElement.CreateReader()) { reader.MoveToContent(); XmlSerializer serializer = new XmlSerializer(settingType); return (ISettingType)serializer.Deserialize(reader); } } } var numeric = ConvertToNumber(rootElement.Value); return numeric ?? rootElement.Value; } private static object ConvertToNumber(string str) { decimal number; if (decimal.TryParse(str, NumberStyles.Any, NumberFormatInfo.InvariantInfo, out number)) { if (number % 1 == 0) { return (long)number; } return number; } return null; } }
To clarify one thing the class ApplicationSetting
is an EF entity storing the key/value pair within the database.
Having a factory that has two "get" (called Create()
in the above code) methods where one only works for a specific type of setting seems like a code smell. Additionally having a factory that creates both objects implementing the interface AND primitive types just feels "wrong," but I'm not really sure if wrapping these settings in tiny classes that have a single property so they can implement ISettingType
is really the best way to handle this either.
The ApplicationSetting
EF Entity class is just a simple POCO:
public class ApplicationSetting { public string SettingName { get; set; } public string SettingValue { get; set; } }
The ISettingType
interface:
public interface ISettingType { string SerializeValue(); ApplicationSetting ToApplicationSetting(); }
I have an abstract base class that implements this:
public abstract class BaseSetting : ISettingType { public virtual string SerializeValue() { StringBuilder sb = new StringBuilder(); using (var writer = new StringWriter(sb)) using (var xmlWriter = XmlWriter.Create(writer)) { XmlSerializer serializer = new XmlSerializer(this.GetType()); xmlWriter.WriteStartElement("Value"); serializer.Serialize(xmlWriter, this); xmlWriter.WriteEndElement(); xmlWriter.Flush(); } return sb.ToString(); } public virtual ApplicationSetting ToApplicationSetting() { var name = this.GetType().Name; var value = this.SerializeValue(); return new ApplicationSetting { SettingName = name, SettingValue = value }; } }
An example of a class derived from this is Smtp
:
public class Smtp : BaseSetting { public class SmtpServer { [XmlText] [Url] [Required] [Display(Name = "Host")] public string Host { get; set; } [XmlAttribute("port")] [Display(Name = "Port")] public int Port { get; set; } public SmtpServer() { Port = 25; } } [XmlElement("Server")] public SmtpServer Server { get; set; } [XmlElement("Username")] [Required] [Display(Name = "Username")] public string Username { get; set; } [XmlIgnore] [Display(Name = "Password")] public string Password { get; set; } [XmlElement("Password")] public string SerializedPassword { get { return Password.EncryptString(); } set { this.Password = value.DecryptString(); } } }
Smtp
is a view model and is used as below from controller methods:
public async Task<ActionResult> GetSmtpForm() { //db is my Entity Framework context var setting = await db.ApplicationSettings.SingleAsync(s => s.SettingName.Equals("Smtp")); var smtp = SettingFactory.Create<Smtp>(setting); return PartialView("SmtpForm", smtp); } [HttpPost] public async Task<ActionResult> UpdateSmtp(Smtp smtp) { //perform validation -- below is if passed var setting = smtp.ToApplicationSetting(); db.ApplicationSettings.Attach(setting); db.Entity(setting).State = EntityState.Modified; await db.SaveChangesAsync(); //Send data back to client }
NVARCHAR(MAX)
column with them? Literally, the only reason I went down the XML route was due to the built-in support on the database server to make it easy for migrations in the future if necessary.\$\endgroup\$