Contents
This article describes how to setup default values of the value type fields in the .NET class using the Custom
Attribute from the Application Assembly or Config file. The concept is based on attributing the fields with a specified default value. These default values are persisted in the assembly metadata during the compiling time. Using the "generic loader" function during the run-time, the attributed fields can be initialized by these Default Values. The best understanding this concept is to show a usage of the FieldAttribute
in the example:
The following code snippet shows a usage of the FieldAttribute
for different types of the fields in the class:
namespace ClassSamples
{
public enum Day { Sunday, Monday, Tuesday, Wednesday, Thursday,
Friday, Saturday }
public class ClassA
{
[Field(DefaultValue = 1.23E+28)]
public decimal d;
[Field("This is a string A")]
public string s;
[Field(777.789)]
public double db;
[Field("F6B67807-2AC7-4ba6-BCA7-75D4B7668EC4")]
public Guid guid;
[Field(Day.Monday)]
public Day day;
[Field("ClassA")]
public object obj;
public ClassA()
{
}
}
public class ClassB : ClassA
{
[Field(DefaultValue = 123456789)]
public int i;
[Field("This is a string B")]
public new string s;
[Field(10, Desc="Create a string array[10]")]
public string[] sarr;
[Field(0x0a)]
public byte hex;
[Field(false)]
public bool b;
[Field]
public new Guid guid;
public ClassB()
{
}
}
public class ClassC : ClassB
{
[Field("This is a string C")]
public new string s;
[Field(1)]
public sbyte sb;
[Field(1234.5E+20)]
public float f;
public ClassC()
{
DefaultValues.Set(this);
}
}
}
Each public field of the class can be attributed using the custom attribute FieldAttribute
with the specified value and/or description. Note that the value has to be in the format of the field type, for instance; the Guid
type field is using a string value type, the array field is using this integer value as a size of the array and so on. In the case of the wrong (or missing) value specified by this attribute, the original value of the field will not be changed.
The value of the FieldAttribute
are persisted into the assembly metadata. During the run-time, we need a some "custom attribute loader" function to retrieve for particular attributed field its value from the storage (either from the assembly metadata or config file). This function is shown in the above samples as DefaultValues.Set(this)
. - see more details at Class DefaultValues. Usually place for this function is in the class ctor, but it can be used any place within the same assembly. The following code snippet shows this implementation:
namespace DefaultValuesConsole
{
class Class1
{
static void Main(string[] args)
{ int tc = 0;
ClassA a = new ClassA();
ClassB b = new ClassB();
ClassC c = new ClassC();
DefaultValues.Dump(c, "c", DumpOption.Console);
Console.WriteLine("press any key to continue this test");
Console.ReadLine();
b = DefaultValues.Set(new ClassB(),
DefaultValuesOption.FromAssembly) as ClassB;
DefaultValues.Dump(b, "b", DumpOption.Console);
Console.WriteLine("press any key to continue this test");
Console.ReadLine();
while(true)
{
tc = Environment.TickCount;
DefaultValues.Set(c, DefaultValuesOption.FromConfigFile);
tc = Environment.TickCount - tc;
DefaultValues.Dump(c, "c", DumpOption.Console);
Console.WriteLine("time={0}[ms], press any key to continue " +
"this test", tc);
Console.ReadLine();
}
}
}
}
The above code snippet is a test console program where classes A, B and C have been initiated and setup their fields from the assembly default values respectively from the config default values. The program shows a different ways how to do it - inside or outside of the class ctor. The function DefaultValues.Set
has a option to select a source of the default values between the assembly or config file. The default option is the assembly metadata source.
Using the config file to store the default values of the class fields needs to follow the format as it shown in the following code snippet. Its configSection
node contains a DefaultValues
section group with a specified sections of the classes. Each class section includes a name of the field and its default value.
<configuration>
<configSections>
<sectionGroup name="DefaultValues">
<section name="ClassSamples.ClassA"
type="System.Configuration.NameValueSectionHandler,System" />
<section name="ClassSamples.ClassB"
type="System.Configuration.NameValueSectionHandler,System" />
<section name="ClassSamples.ClassC"
type="System.Configuration.NameValueSectionHandler,System" />
</sectionGroup>
</configSections>
<DefaultValues>
<ClassSamples.ClassA>
<add key="d" value="199998888888999999.23456789" />
<add key="s" value="This is a string A - Config" />
<add key="db" value="1111.2222" />
<add key="guid" value="00000000-2AC7-4ba6-BCA7-75D4B7668EC4" />
<add key="day" value="Tuesday" />
<add key="obj" value="ClassB" />
</ClassSamples.ClassA>
<ClassSamples.ClassB>
<add key="i" value="1" />
<add key="sarr" value="1000" />
<add key="hex" value="128" />
<add key="guid" value="11111111-2222-3333-4444-555555555555" />
<add key="s" value="This is a string B - Config" />
<add key="b" value="true" />
</ClassSamples.ClassB>
<ClassSamples.ClassC>
<add key="s" value="This is a string C - Config" />
<add key="sb" value="100" />
<add key="f" value="1234567890.0123456789" />
</ClassSamples.ClassC>
</DefaultValues>
</configuration>
The solution has been divided into two parts:
- FieldAttribute
- DefaultValues
Let's look at about them for more details:
This is the Custom Attribute class for the class field usage. As you can see in the following code snippet its implementation is very simply and is based on the values _DefaultValue
and _Desc
. Both are smart fields and they can be initialized using the set or ctor arguments.
[AttributeUsage(AttributeTargets.Field)]
public class FieldAttribute : Attribute
{
private object _DefaultValue;
private string _Desc = string.Empty;
public object DefaultValue
{
get{ return _DefaultValue; }
set{ _DefaultValue = value; }
}
public string Desc
{
get{ return _Desc; }
set{ _Desc = value; }
}
public FieldAttribute() {}
public FieldAttribute(object defaultValue)
{
_DefaultValue = defaultValue;
}
public FieldAttribute(object defaultValue, string desc)
{
_DefaultValue = defaultValue;
_Desc = desc;
}
}
This "magic" class has been designed and implemented for the FieldAttribute
purpose only. Its responsibility is to initialize attributed field in the class with the Default value from the specified storage such as an application assembly or config file. Both functions have a generic design pattern based on using the Reflection technique, which is a integral part of the .NET Framework, to retrieve a specified type from the assembly metadata.
The FieldAttribute
ctor is called by invoking the GetCustomAttributes(true)
for each attributed field. During this time its fields such as _DefaultValue
and _Desc
are set up. Selecting a properly target type conversion, the attributed field can be updated by the Reflection.FieldInfo.SetValue
method.
There is a function Dump in this class to display values of the all fields for a specified class on the console or trace output stream. This function has a test purpose only.
public enum DefaultValuesOption { FromAssembly, FromConfigFile }
public enum DumpOption { Console, Trace }
public class DefaultValues
{
static public object Set(object parent)
{
return Set(parent, DefaultValuesOption.FromAssembly);
}
static public object Set(object parent, DefaultValuesOption option)
{
if(parent == null)
return null;
Type type = parent.GetType();
foreach(FieldInfo fi in type.GetFields())
{
foreach(Attribute attr in fi.GetCustomAttributes(true))
{
if(attr is FieldAttribute)
{
object DefaultValue = null;
if(option == DefaultValuesOption.FromConfigFile)
{
string cfgSectionName = "DefaultValues/"
+ fi.DeclaringType.FullName;
NameValueCollection cfgClass =
(NameValueCollection)ConfigurationSettings.GetConfig(
cfgSectionName);
DefaultValue = cfgClass[fi.Name];
}
else
{
DefaultValue = (attr as FieldAttribute).DefaultValue;
}
try
{
if(fi.FieldType == typeof(System.Guid))
{
fi.SetValue(parent, new Guid(DefaultValue.ToString()));
}
else
if(fi.FieldType == typeof(decimal))
{
if(DefaultValue.GetType() == typeof(string))
fi.SetValue(parent,
decimal.Parse(DefaultValue.ToString()));
else
fi.SetValue(parent, Convert.ToDecimal(DefaultValue));
}
else
if(fi.FieldType.IsEnum == true)
{
object en = Enum.Parse(fi.FieldType,
DefaultValue.ToString());
fi.SetValue(parent, en);
}
else
if(fi.FieldType.IsArray == true)
{
object arr = Activator.CreateInstance(fi.FieldType,
new object[]{Convert.ToInt32(DefaultValue)});
fi.SetValue(parent, arr );
}
else
if(fi.FieldType != DefaultValue.GetType())
{
fi.SetValue(parent, Convert.ChangeType(DefaultValue,
fi.FieldType));
}
else
{
fi.SetValue(parent, DefaultValue);
}
}
catch(Exception ex)
{
Trace.WriteLine(string.Format(
"Message:{0} [{1}.{2} = {3}]",
ex.Message, type.FullName, fi.Name, DefaultValue));
}
}
}
}
return parent;
}
static public void Dump(object parent, string prompt)
{
Dump(parent, prompt, DumpOption.Trace);
}
static public void Dump(object parent, string prompt, DumpOption option)
{
if(parent == null)
return;
Type type = parent.GetType();
string strFieldInfo = string.Empty;
foreach(FieldInfo fi in type.GetFields())
{
string strClassName = fi.DeclaringType.FullName;
if(fi.FieldType.IsArray == true)
{
string size = (fi.GetValue(parent) == null) ? "null" :
((Array)fi.GetValue(parent)).Length.ToString();
strFieldInfo = string.Format("{0}.[{1}.{2} = {3}].{4}.size={5}",
prompt, strClassName, fi.Name, fi.GetValue(parent),
fi.FieldType.FullName, size);
}
else
{
strFieldInfo = string.Format("{0}.[{1}.{2} = {3}].{4}",
prompt, strClassName, fi.Name, fi.GetValue(parent),
fi.FieldType.FullName);
}
if(option == DumpOption.Console)
System.Console.WriteLine(strFieldInfo);
else
System.Diagnostics.Trace.WriteLine(strFieldInfo);
}
}
}
The FieldAttribute
can be tested using the console program DefaultValuesConsole included this package. This is a very simple program to initiate few classes with the default values and then displaying their state. The following screen snap shows the first step of this test.