We usually save most of application
setting under app.config or web.config with key and value pair under
appSetting element. The big problem with those are whenever you need
value, typecasting need to be done and also need to be called through
key. The other problem I find is with hierarchy of settings like
download related setting present should under download element for
easy accessing.
To solve strongly typed configuration
and hierarchy, ConfigurationManager
(http://msdn.microsoft.com/en-us/library/system.configuration.configurationmanager(v=vs.110).aspx)
class comes for rescue. You might have seen under web.config where we
have sectionGroup and related configuration under elements. To
implement it, lot of codes need to written to achieve simplification.
To reduce numbers of codes for
implementing XML configuration a very good and free tool is available
called "Configuration Section Designer" available on
http://csd.codeplex.com/.
There we do not have to write any line of code. All things could be
managed in UI level only.
Now a days most of the projects are
moving to cloud based and as a result we are loosing control to
manipulate configuration. For example, Azure resets deployed builds
after certain time. So, if you have changed anything in web.config it
will get reset to depolyed version. We need to have certain
permission to change configuration which creates problem on hosting
server.
In my current approach, I have tried to
check every drawbacks and simplify the items through database and T4
template. Strongly typed, hierarchy, zero maintenance and easy
accessibility through class would be taken care.
First we need to have self relation
table to create hierarchy which will result up in creation of classes
with its different members.
Mandatory columns for table:
Id: Primary key
ParentId: This is a reference of Id to
make relationship. Need to have 0 in case of root elements.
SettingName: Name of the setting.
Through this property and classes would be created but we can put
spaces between string.
SettingValue: Need to be NULL for
parent members/classes and also proper value need to be put for
others. Data type would be identified through this only.
That is all we need from table side to
populate class. Clean and neat with few rules.
Example for table:
Id|ParentId|SettingName|SettingValue
1|0|System|NULL [NOTE: 0 as root
element]
2|1|Application Name|Demo Setting
[NOTE: ParantId is set to 1 (System)]
3|0|Password Policy|NULL [NOTE: Another
root element]
4|3|Password Age In Days|3 [NOTE:
SettingValue as 3 for generating int property]
5|3|Password Constraint|NULL [NOTE:
Creating hierarchy under Password policy with NULL value under
SettingValue]
6|5|Complex Password|true| [NOTE:
SettingValue is set as true for bool]
7|5|Password Min Length|5
Result of above table into hierarchy
-- System [Root]
---- Application Name
-- Password Policy [Root]
---- Password Age In Days
---- Password Constraint [Root level 2]
-------- Complex Password
-------- Password Min Length
I am going to explain some key parts
from T4 template codes so that it would be easy to modify and at last
will put all codes from T4 code for implementation.
- Connection string: Modify according
to your database
var connectString = "datasource=.;initialcatalog=SettingDemo;integratedsecurity=True";
- DB query. I have not implemented
regular expression in this tutorial but we can take advantage to
generate data annotation for regular expression.
var queryString = "SELECT Id,ParentId, SettingName, 'RegularExpression', SettingValue from SystemSetting";
-
List<Tuple<int,int,string, string, string, string>> GetSettingsItems(string connectString)
through above function we will get
tuple as
Item1= Id
Item2 = ParentId
Item3 = Property/Class name
Item4 = Name or Description that could
be used for XML comment. [Not used in this example]
Item5 = Regular Expression reserved
[Not used in this example]
Item6 = Setting value
- Codes for T4
<#@ template debug="true" hostSpecific="true" #>
<#@ output extension=".cs" #>
<#@ Assembly Name="System" #>
<#@ Assembly Name="System.Globalization" #>
<#@ Assembly Name="System.Data" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Text.RegularExpressions" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ include file="EF.Utility.CS.ttinclude"#>
<#@ import namespace="System.Data.SqlClient" #>
<#
var code = new CodeGenerationTools(this);
var connectString = "data source=.;initial catalog=SettingDemo;integrated security=True";
var settings = GetSettingsItems(connectString);
#>
using System;
using System.Collections.Generic;
using System.Linq;
namespace <#= code.VsNamespaceSuggestion()#>
{
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated from a template and will be overwritten as soon
// as the template is executed.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
/// <summary>
/// Application settings
/// </summary>
public class Setting
{
<#
var settingCreateMethod = new StringBuilder("public static Setting CreateSetting"
+ "(Dictionary<string,string> settings)\n\t{\n\t var setting = new Setting();\n");
foreach (var setting in settings.Where(seting => seting.Item6 == null))
{#>
<#if(setting.Item2 == 0){
// method generation for root element
settingCreateMethod.Append("setting." + setting.Item3 + " = Setting"
+ setting.Item3 +".Create" + setting.Item3 + "(settings);\n");
#>
public Setting<#= setting.Item3 #> <#= setting.Item3 #> { get; set; }
<#
}#>
public class Setting<#= setting.Item3 #>
{
<#
// String builder for parameter
var childCtor = new StringBuilder();
// String builder to put contents under constructor
var childCtorProrInit = new StringBuilder();
// String builder to create method parameters to use dictionary
var mthodDictionary = new StringBuilder();
childCtor.Append("public Setting" + setting.Item3 + "(" );
mthodDictionary.Append("public static Setting" + setting.Item3 + " Create" + setting.Item3
+ "(Dictionary<string,string> settings)\n\t{\n\t"+
"return new Setting" + setting.Item3 + "(" );
#>
<#
foreach (var childSetting in settings
.Where(i=>i.Item2==setting.Item1)
.AsQueryable().OrderBy(value => value.Item3))
{
var dataType = GetType(childSetting.Item6);
// In case of class members
if(childSetting.Item6 != null)
{
childCtor.Append("string " + code.CamelCase(childSetting.Item3) + ", ");
if(dataType != "string")
{
childCtorProrInit.Append(childSetting.Item3 + " = " + dataType + ".Parse("+code.CamelCase(childSetting.Item3)+");\n\t");
}
else
{
childCtorProrInit.Append(childSetting.Item3 + " = "+code.CamelCase(childSetting.Item3)+";\n\t");
}
mthodDictionary.Append("GetValue(settings, \"" + childSetting.Item4 + "\"), \n");
#>
public <#= dataType #> <#= childSetting.Item3 #> { get; set; }
<# }else{
// If type is class itself
// CTOR for class
childCtor.Append("Setting" + childSetting.Item3 + " " + code.CamelCase(childSetting.Item3) + ", ");
childCtorProrInit.Append(childSetting.Item3 + " = "+code.CamelCase(childSetting.Item3)+";\n\t");
// Get all values for the child class and add in method parameter
mthodDictionary.Append("new Setting" + childSetting.Item3 + "\n(\n");
foreach (var paramChildSetting in settings
.Where(i => childSetting.Item1 == i.Item2)
.AsQueryable().OrderBy(value => value.Item3))
{
mthodDictionary.Append("GetValue(settings, \"" + paramChildSetting.Item4 + "\"), \n");
}
var strWhole = mthodDictionary.ToString().Remove(mthodDictionary.ToString().Length -3, 3);
mthodDictionary.Clear();
mthodDictionary.Append(strWhole);
mthodDictionary.Append("), \n");
#>
public Setting<#= childSetting.Item3 #> <#= childSetting.Item3 #> { get; set;}
<# } #>
<#
} ;#>
<#= childCtor.ToString().Remove(childCtor.ToString().Length -2, 2 ) + ")\n{\t" + childCtorProrInit + "}\n" #>
<#= mthodDictionary.ToString().Remove(mthodDictionary.ToString().Length -3, 3) + ");\n" #>
}
}
<# } #>
internal static string GetValue(Dictionary<string, string> settings, string settingKey)
{
return settings.FirstOrDefault(x => x.Key.ToLower() == settingKey.ToLower()).Value;
}
<#= settingCreateMethod.ToString() + "\nreturn setting;}\n"#>
}
/// <summary>
/// System settings key
/// </summary>
public enum SystemSettingKey
{
<#
foreach (var setting in settings)
{
#>
/// <summary>
/// <#= setting.Item4 #>
/// </summary>
<#= setting.Item3 #>,
<#
}
#>
}
}
<#+
private static List<Tuple<int,int,string, string, string, string>> GetSettingsItems(string connectString)
{
List<Tuple<int,int,string, string, string, string>> settings = new
List<Tuple<int,int,string, string, string, string>>();
var queryString = "SELECT Id,ParentId, SettingName, 'RegularExpression', SettingValue from SystemSetting";
using (SqlConnection connection = new SqlConnection(connectString))
{
var command = new SqlCommand(queryString, connection);
connection.Open();
var reader = command.ExecuteReader();
// Call Read before accessing data.
while (reader.Read())
{
settings.Add(new Tuple<int,int,string, string, string, string>
(int.Parse(reader[0].ToString()),int.Parse(reader[1].ToString()),
GetPascalCase(Convert.ToString(reader[2])),Convert.ToString(reader[2]),
Convert.ToString(reader[3]),
(Convert.IsDBNull(reader[4])?null:reader[4].ToString())));
}
reader.Close();
}
return settings.AsQueryable().OrderBy(value => value.Item3).ToList();
}
private string GetType(string value)
{
string settingType=null;
int i; bool b; decimal d;
if (int.TryParse(value, out i))
settingType = "int";
else if (bool.TryParse(value, out b))
settingType = "bool";
else if (decimal.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out d))
settingType = "decimal";
else
settingType = "string";
return settingType;
}
private static string GetPascalCase(string name)
{
return new CultureInfo("en").TextInfo.ToTitleCase(name.ToLower()).Replace(" ", "");
}
#>
The above code will generate classes
for setting. It is also having a static method to generate "Setting"
object based on dictionary value.
To generate setting object:
var settings = new Dictionary<string, string>();
var queryString = "SELECT SettingName, SettingValue from SystemSetting";
using (SqlConnection connection = new SqlConnection("data source=.;initial catalog=TestDB;integrated security=True"))
{
var command = new SqlCommand(queryString, connection);
connection.Open();
var reader = command.ExecuteReader();
// Call Read before accessing data.
while (reader.Read())
{
settings.Add(reader[0].ToString(), reader[1].ToString());
}
reader.Close();
}
// That is all needed to populate
// NOTE: Much easy if we are using ORM to populate dictionary object
Setting setting = Setting.CreateSetting(settings);
This approach is not just restricted to
generate settings only but to generate classes based on table with
different level of hierarchy.
NOTE: The generated code from T4
template will not be properly formatted.
Comments
Post a Comment