Alright - use the information gathered from this post at your own risk as I don't condone this solution as ironclad, but do find it interesting enough to post.
Recently I've been working on a module for an app that uses Linq to SQL within a WCF Service to update values in a database. The app itself in short, is a reporting app and my task is to take validated data from the client-side (another part of the project), and store it. To do this I've mapped the given database schema within a DBML and have provided a Complex Type Report Object definition via WSDL which is expected as the first argument in a majority of the service's exposed CRUD functions.
I could have just manually mapped Service Report Object values to those exposed via the DBML, but given my normal nature I've decided to take the entire setup a bit further and build a set of functions that allow for dynamic getting and setting of values without relying on manual mapping. To justify this - like all applications, especially those that may be used by multiple organizations, requirements undoubtedly change and I’d really rather not have go through an intricate and lengthy process to update service definitions/references, redeployment, etc.
To do this, I needed to find a way to dynamically read incoming properties within our dbml context, and our report service object. As well, I needed a way to dynamically build/represent our report definition.
Report Definition
In a table in SQL we store the properties/property types/indexes of our properties we want to expose via our report. An external app could be built to manage this (among other) stored complex type definitions. Note we could also simply store our wsdl xml in the database instead. A static Report object in our app WCF Service has single public property, a Dictionary of Key Value Pairs. These values will need to be populated within the consuming application with our SQL stored properties, and Keys with the property names. Note that DataType information is essential in our SQL definition when initializing our Value objects as we rely on Value Type defaults when actually interrogating/assigning values to our properties.
The Logic
After the consuming application has downloaded the WCF Service object definition and SQL Report object property data, and then populated/assigned these values, it passes the report as an argument to the WCF Service. For each CRUD function (Create/Update/Read but not Delete), we rely on a similar set of function calls. We loop through recursively each property in our report, interrogate the PropertyInfo for each and attempt to match to a recursive property search on our matching report object (If we’re updating we get values from our WCF Report object and match to our DBML context, and if we’re setting we reverse that order => DBML Context to WCF Report object.
A check on each DBML Context property for Identity/DBType/IsDBGenerated/Non Null allows our logic to correctly ignore/assign values/default values and prevent sql exceptions when we call our context.submit(). A couple example functions (set/get/default value methods):
private void SetPropertyValues<TLocal, TContext>(TLocal l, ref TContext c)
{
if (l != null && c != null)
{
foreach (PropertyInfo cp in c.GetType().GetProperties())
{
try
{
bool isIdentity = false;
bool isNonNull = false;
bool isDbGenerated = false;
foreach (CustomAttributeData attr in cp.GetCustomAttributesData())
{
foreach (CustomAttributeNamedArgument arg in attr.NamedArguments)
{
if (arg.TypedValue != null)
{
if (arg.TypedValue.Value.ToString().ToLower().Contains("not null"))
{
isNonNull = true;
}
if (arg.TypedValue.Value.ToString().ToLower().Contains("identity"))
{
isIdentity = true;
}
if (arg.MemberInfo.Name.ToLower().Contains("isdbgenerated"))
{
isDbGenerated = true;
}
if (isNonNull && isIdentity && isDbGenerated)
{
break;
}
}
}
}
if (!isDbGenerated)
{
PropertyInfo lp = l.GetType().GetProperty(cp.Name);
if (lp != null && lp.PropertyType == cp.PropertyType) cp.SetValue(c, lp.GetValue(GetDefaultValue(l, lp.PropertyType), null), null);
else
{
if (isNonNull)
{
cp.SetValue(c, GetDefaultValue(cp.GetValue(c, null), cp.PropertyType), null);
}
}
}
}
catch
{
//error handling
}
}
}
}
private void GetPropertyValues<TLocal, TContext>(ref TLocal l, TContext c)
{
if (l != null && c != null)
{
foreach (PropertyInfo cp in c.GetType().GetProperties())
{
try
{
PropertyInfo lp = l.GetType().GetProperty(cp.Name);
if (lp != null) lp.SetValue(l, GetDefaultValue(cp.GetValue(c, null), cp.PropertyType), null);
}
catch
{
//error handling
}
}
}
}
private T GetDefaultValue<T>(object value)
{
return value == DBNull.Value ? default(T) : (T)value;
}
private object GetDefaultValue(object value, Type targetType)
{
object defaultValue = targetType.IsValueType ? Activator.CreateInstance(targetType) : null;
#region Custom Defaults...
if (targetType == typeof(string)) defaultValue = string.Empty;
if (targetType == typeof(DateTime) && (DateTime)value == DateTime.MinValue) defaultValue = DateTime.Now;
#endregion
return GetDefaultValue<object>(value) ?? defaultValue;
}
Overlooked
Now one thing I’ve not yet figured out is how to dynamically update our DBML, which rather makes all of this moot now doesn’t it? Perhaps a better way of doing this is to instead of interrogating a DBML context object for matching properties is to use a generic collection of DataSet as our context.