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\$