DataMapper and the pain

DataMapper and the pain

Old forum URL: forums.lhotka.net/forums/t/3994.aspx


skaue posted on Thursday, December 06, 2007

I must be going about this the wrong way. I use the DataMapper to map my object with the FormView values, and most values work out ok. Decimals and integers don't. Ignoring the exception cripples my object since it inherits certain data from a baseclass.

I've seen some ideas  for altering the DataMapper, but that would mean I had to "remember to hack CSLA after updating it" :-/

Is there are any one out there who knows a better way to solve this?

RockfordLhotka replied on Thursday, December 06, 2007

I am in the middle of updating DataMapper (in various ways) for 3.5. One set of pretty major changes is around type coercion, especially when dealing with nullable<t> type values.

Can you give specific value examples of what's failing (what is the value from the FormView, and the exact data type of the property)?

skaue replied on Friday, December 07, 2007

I did a svn checkout on the trunk and noticed a lot of improvements on datamapper, but I don't suppose these improvements can be easily applied to 3.0.2, or?

I ended up with making my own datamapper using the code from this post:
http://forums.lhotka.net/forums/thread/4221.aspx

This solved saving decimal and some other convertion issues. I have yet to try a datetime field... looking forward to the agony of that.

Here's my current datamapper in c#:


using System;
using System.Collections.Generic;
using System.Reflection;
using System.ComponentModel;
using Csla;

namespace XXXX.BLL.Utils
{
    /// 
    /// Map data from a source into a target object
    /// by copying public property values.
    /// 
    /// 
    public static class DataMapper
    {

        internal static class Resources
        {
            internal static readonly string PropertyCopyFailed = "property map error";
        }

        #region Map from IDictionary

        /// 
        /// Copies values from the source into the
        /// properties of the target.
        /// 
        /// A name/value dictionary containing the source values.
        /// An object with properties to be set from the dictionary.
        /// 
        /// The key names in the dictionary must match the property names on the target
        /// object. Target properties may not be readonly or indexed.
        /// 
        public static void Map(System.Collections.IDictionary source, object target)
        {
            Map(source, target, false);
        }

        /// 
        /// Copies values from the source into the
        /// properties of the target.
        /// 
        /// A name/value dictionary containing the source values.
        /// An object with properties to be set from the dictionary.
        /// A list of property names to ignore.
        /// These properties will not be set on the target object.
        /// 
        /// The key names in the dictionary must match the property names on the target
        /// object. Target properties may not be readonly or indexed.
        /// 
        public static void Map(System.Collections.IDictionary source, object target, params string[] ignoreList)
        {
            Map(source, target, false, ignoreList);
        }

        /// 
        /// Copies values from the source into the
        /// properties of the target.
        /// 
        /// A name/value dictionary containing the source values.
        /// An object with properties to be set from the dictionary.
        /// A list of property names to ignore.
        /// These properties will not be set on the target object.
        /// If , any exceptions will be supressed.
        /// 
        /// The key names in the dictionary must match the property names on the target
        /// object. Target properties may not be readonly or indexed.
        /// 
        public static void Map(
          System.Collections.IDictionary source,
          object target, bool suppressExceptions,
          params string[] ignoreList)
        {
            List ignore = new List(ignoreList);
            foreach (string propertyName in source.Keys)
            {
                if (!ignore.Contains(propertyName))
                {
                    try
                    {
                        SetPropertyValue(target, propertyName, source[propertyName]);
                    }
                    catch (Exception ex)
                    {
                        if (!suppressExceptions)
                            throw new ArgumentException(
                              String.Format("{0} ({1})",
                              Resources.PropertyCopyFailed, propertyName), ex);
                    }
                }
            }
        }

        #endregion

        #region Map from Object

        /// 
        /// Copies values from the source into the
        /// properties of the target.
        /// 
        /// An object containing the source values.
        /// An object with properties to be set from the dictionary.
        /// 
        /// The property names and types of the source object must match the property names and types
        /// on the target object. Source properties may not be indexed.
        /// Target properties may not be readonly or indexed.
        /// 
        public static void Map(object source, object target)
        {
            Map(source, target, false);
        }

        /// 
        /// Copies values from the source into the
        /// properties of the target.
        /// 
        /// An object containing the source values.
        /// An object with properties to be set from the dictionary.
        /// A list of property names to ignore.
        /// These properties will not be set on the target object.
        /// 
        /// The property names and types of the source object must match the property names and types
        /// on the target object. Source properties may not be indexed.
        /// Target properties may not be readonly or indexed.
        /// 
        public static void Map(object source, object target, params string[] ignoreList)
        {
            Map(source, target, false, ignoreList);
        }

        /// 
        /// Copies values from the source into the
        /// properties of the target.
        /// 
        /// An object containing the source values.
        /// An object with properties to be set from the dictionary.
        /// A list of property names to ignore.
        /// These properties will not be set on the target object.
        /// If , any exceptions will be supressed.
        /// 
        /// 
        /// The property names and types of the source object must match the property names and types
        /// on the target object. Source properties may not be indexed.
        /// Target properties may not be readonly or indexed.
        /// 
        /// Properties to copy are determined based on the source object. Any properties
        /// on the source object marked with the  equal
        /// to false are ignored.
        /// 
        /// 
        public static void Map(
          object source, object target,
          bool suppressExceptions,
          params string[] ignoreList)
        {
            List ignore = new List(ignoreList);
            PropertyInfo[] sourceProperties =
              GetSourceProperties(source.GetType());
            foreach (PropertyInfo sourceProperty in sourceProperties)
            {
                string propertyName = sourceProperty.Name;
                if (!ignore.Contains(propertyName))
                {
                    try
                    {
                        SetPropertyValue(
                          target, propertyName,
                          sourceProperty.GetValue(source, null));
                    }
                    catch (Exception ex)
                    {
                        if (!suppressExceptions)
                            throw new ArgumentException(
                              String.Format("{0} ({1})",
                              Resources.PropertyCopyFailed, propertyName), ex);
                    }
                }
            }
        }

        private static PropertyInfo[] GetSourceProperties(Type sourceType)
        {
            List result = new List();
            PropertyDescriptorCollection props =
              TypeDescriptor.GetProperties(sourceType);
            foreach (PropertyDescriptor item in props)
                if (item.IsBrowsable)
                    result.Add(sourceType.GetProperty(item.Name));
            return result.ToArray();
        }

        #endregion

        /// 
        /// Sets an object's property with the specified value,
        /// coercing that value to the appropriate type if possible.
        /// 
        /// Object containing the property to set.
        /// Name of the property to set.
        /// Value to set into the property.
        public static void SetPropertyValue(object target, string propertyName, object value)
        {
            PropertyInfo propertyInfo = target.GetType().GetProperty(propertyName);
            if (value == null)
            {
                propertyInfo.SetValue(target, value, null);
            }
            else
            {
                Type pType = Utilities.GetPropertyType(propertyInfo.PropertyType);
                Type vType = Utilities.GetPropertyType(value.GetType());
                if (pType.Equals(vType))
                {
                    // types match, just copy value
                    propertyInfo.SetValue(target, value, null);
                }
                else
                {
                    // types don't match, try to coerce types
                    if (pType.Equals(typeof(Guid)))
                    {
                        propertyInfo.SetValue(target, new Guid(value.ToString()), null);
                    }
                    else if (pType.IsEnum && vType.Equals(typeof(string)))
                    {
                        propertyInfo.SetValue(target, System.Enum.Parse(pType, value.ToString()), null);
                    }
                    // '------Modified Part Start Here -----
                    else if (pType.FullName == "System.Int32" & vType.FullName == "System.String")
                    {
                        if (value.Equals(string.Empty))
                        {
                            value = "0";
                            propertyInfo.SetValue(target, Convert.ChangeType(value, pType), null);
                        }
                        else
                        {
                            propertyInfo.SetValue(target, Convert.ChangeType(value, pType), null);
                        }
                    }
                    else if (pType.FullName == "System.Decimal" & vType.FullName == "System.String")
                    {
                        if (value.Equals(string.Empty))
                        {
                            value = "0";
                            propertyInfo.SetValue(target, Convert.ChangeType(value, pType), null);
                        }
                        else
                        {
                            propertyInfo.SetValue(target, Convert.ChangeType(value, pType), null);
                        }
                    }
                    //-------------------------------------End of Modification---------------------------------------------------------
                    else
                    {
                        propertyInfo.SetValue(target, Convert.ChangeType(value, pType), null);
                    }
                }
            }
        }
    }
}

not sure if this post editor handles code correctly...

RockfordLhotka replied on Friday, December 07, 2007

I do think the CoerceValue() code can easily back-port to 3.0.3. So far I haven’t done anything in DataMapper that is .NET 3.5 or even .NET 3.0 specific.

 

Can you provide specific source/target examples that were failing so I can ensure they work in the new model in 3.5?

 

Rocky

 

From: skaue [mailto:cslanet@lhotka.net]
Sent: Friday, December 07, 2007 7:09 AM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] DataMapper and the pain

 

I did a svn checkout on the trunk and noticed a lot of improvements on datamapper, but I don't suppose these improvements can be easily applied to 3.0.2, or?

I ended up with making my own datamapper using the code from this post:
http://forums.lhotka.net/forums/thread/4221.aspx

This solved saving decimal and some other convertion issues. I have yet to try a datetime field... looking forward to the agony of that.
[…]

 

skaue replied on Friday, December 07, 2007

well, given that I wrote my own DataMapper and this one works, you can view for yourself what types the original didn't support that well:


// '------Modified Part Start Here -----
else if (pType.FullName == "System.Int32" & vType.FullName == "System.String")
{
if (value.Equals(string.Empty))
{
value = "0";
propertyInfo.SetValue(target, Convert.ChangeType(value, pType), null);
}
else
{
propertyInfo.SetValue(target, Convert.ChangeType(value, pType), null);
}
}
else if (pType.FullName == "System.Decimal" & vType.FullName == "System.String")
{
if (value.Equals(string.Empty))
{
value = "0";
propertyInfo.SetValue(target, Convert.ChangeType(value, pType), null);
}
else
{
propertyInfo.SetValue(target, Convert.ChangeType(value, pType), null);
}
}
//-------------------------------------End of Modification---------------------------------------------------------

Would it be possible to add your CoerceValue() to the 3.0.x branch, or is that branch "locked" ;-)

RockfordLhotka replied on Friday, December 07, 2007

OK, thanks for that. Numeric (primitive) types need to go to a 0 value from string.Empty or null.

 

My 3.5 code now does that (it didn’t).

 

The 3.0.x branch is not locked, but it is for bug fixes only. I’ll consider this a bug fix (though I think it is  a stretch). As a result, 3.0.x is now 3.0.4 and includes a back-port of the 3.5 code for type coercion.

 

Rocky

 

ajj3085 replied on Friday, December 07, 2007

Doh!  And I just finished upgrading to 3.0.3... ah well.  I'll wait until I it something that affects me for next upgrade :-)

RockfordLhotka replied on Friday, December 07, 2007

Well 3.0.4 is not a release – it is test mode stuff. And most likely 3.0.4 won’t “release” until 3.5 does – it’ll be the final vehicle for 3.0 bug fixes up to that point.

 

The problem I have is that 3.5 really is 3.5. I’m not sure I can pull off the compiler directive trick to make 3.5 work for .NET 2.0/3.0. Best case is maybe using compiler directives to get back to 2.0a and 3.0a, and even then I’m not sure.

 

So the 3.0.x branch will probably be alive for bug fixes for a long time :(

 

Rocky

 

 

From: ajj3085 [mailto:cslanet@lhotka.net]
Sent: Friday, December 07, 2007 12:01 PM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] RE: RE: DataMapper and the pain

 

Doh!  And I just finished upgrading to 3.0.3... ah well.  I'll wait until I it something that affects me for next upgrade :-)


ajj3085 replied on Friday, December 07, 2007

Ahh, ok.  I just like keeping up with the office releases.

It seems like even though the clr hasn't changed for 3.5, 3.5 is more than just additive like 3.0 was, because the compiler has changed.  Hopefully MS will slow down a bit, as it is I'm going to skip 3.0 entirely.

RockfordLhotka replied on Friday, December 07, 2007

Hehe.

 

3.5 is a FAR bigger release than 3.0 in terms of impacting existing functionality. This is partially because it requires 2.0 SP1 (aka 2.0a), which introduces a bunch of new features to 2.0 itself (and thus 3.0/3.5). The same is true for 3.0 SP1 (aka 3.0a)… And there are new compilers.

 

And new ASP.NET features/controls – including (I’m told) a major change to ASP.NET data binding.

 

3.0a has a whole bunch of enhancements to WPF/WCF/WF.

 

The net result is that skipping to 3.5 is wise (imo). It really represents a higher level of maturity of the 3.0 stuff, plus the new compilers.

 

Rocky

 

 

From: ajj3085 [mailto:cslanet@lhotka.net]
Sent: Friday, December 07, 2007 12:18 PM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] RE: RE: RE: DataMapper and the pain

 

Ahh, ok.  I just like keeping up with the office releases.

It seems like even though the clr hasn't changed for 3.5, 3.5 is more than just additive like 3.0 was, because the compiler has changed.  Hopefully MS will slow down a bit, as it is I'm going to skip 3.0 entirely.


tna55 replied on Friday, December 07, 2007

Hi,

I am working with .NET 2.0 and hence the same CSLA.NET framework. Should I jump directly to .NET 3.5 and CSLA.NET or is it worth working with .NET 3.0 before 3.5?

Our next project will start in Febuary so I was thinking to directly use 3.5.

Tahir

RockfordLhotka replied on Friday, December 07, 2007

http://www.lhotka.net/weblog/MicrosoftNETAndCSLANETVersionConfusion.aspx

 

 

 

From: tna55 [mailto:cslanet@lhotka.net]
Sent: Friday, December 07, 2007 2:03 PM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] RE: RE: RE: RE: DataMapper and the pain

 

Hi,

I am working with .NET 2.0 and hence the same CSLA.NET framework. Should I jump directly to .NET 3.5 and CSLA.NET or is it worth working with .NET 3.0 before 3.5?

Our next project will start in Febuary so I was thinking to directly use 3.5.

Tahir



ajj3085 replied on Friday, December 07, 2007

2.0 sp1 and 3.0 sp1 add new functionality?  I thought MS wasn't going to add functionality via service packs anymore... argh.

I'm glad you put a positive spin on skipping 3.0; my reason is simply I haven't had time to learn it and move to it (and lack of vs2008 didn't help either).

skaue replied on Friday, December 07, 2007

Ok. I will do an update on the svn checkout then to grab the changes. :-)

I see this thread went off topic along the way. I guess me and my colleague will "upgrade" to 3.5 and vs2008 some time in january, and then this nightmare will be all over.... :P

RockfordLhotka replied on Saturday, December 08, 2007

skaue:
Ok. I will do an update on the svn checkout then to grab the changes. :-)

I see this thread went off topic along the way. I guess me and my colleague will "upgrade" to 3.5 and vs2008 some time in january, and then this nightmare will be all over.... :P

Please let me know if you encounter any issues with the 3.0.4 code. Since that is also the intended 3.5 code, if there are issues I'd rather fix them now than later!

skaue replied on Sunday, December 09, 2007

Sure thing, Rocky. Thanks for the update. I'll know by monday if this works, but I recon your own tests worked as expected. :-)

skaue replied on Monday, December 10, 2007

So far, no problems with the new DataMapper. I have removed my own implementation and using the "proper" one ;-)

-Tommy

skaue replied on Tuesday, December 11, 2007

Ok. I think I have one problem that I hope is solvable without me readding my own DataMapper.

I have a property that is of type Decimal (in fact I have several). If someone tries to submit an empty string then the DataMapper throws. Seems the CoerceValue only attempts to fix empty strings for primitive values and Decimal isn't one.

private static object CoerceValue(Type propertyType, Type valueType, object value)
{
if (propertyType.Equals(valueType))
{
// types match, just return value
return value;
}
else
{
if (propertyType.IsGenericType)
{
if (propertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
if (value == null)
return null;
else if (valueType.Equals(typeof(string)) && (string)value == string.Empty)
return null;
}
propertyType = Utilities.GetPropertyType(propertyType);
}

if (propertyType.IsEnum && valueType.Equals(typeof(string)))
return Enum.Parse(propertyType, value.ToString());

if (propertyType.IsPrimitive && valueType.Equals(typeof(string)) && string.IsNullOrEmpty((string)value))
value = 0;

try
{
return Convert.ChangeType(value, Utilities.GetPropertyType(propertyType));
}
catch
{
TypeConverter cnv = TypeDescriptor.GetConverter(Utilities.GetPropertyType(propertyType));
if (cnv != null && cnv.CanConvertFrom(value.GetType()))
return cnv.ConvertFrom(value);
else
throw;
}
}
}
Any suggestions for a fix?

edit: A short and evil fix is changing
propertyType.IsPrimitive
to
propertyType.IsPrimitive || System.Decimal

RockfordLhotka replied on Tuesday, December 11, 2007

That fix is too evil :)

In the wish list I had the idea of calling a delegate converter - so you could provide conversions that would effectively compliment this default implementation. I think I'll do that after all (I though I had it solved with these changes, but perhaps not).

I may still handle the Decimal case as you suggest, but the delegate would allow resolving any other failed type conversions without requiring modifications to DataMapper each time.

skaue replied on Tuesday, December 11, 2007

One thing I haven't tried yet is to check out the "SmartDecimal" from the contrib on Codeplex. I think it works similar to SmartDate (as a string).

btw, Rocky; have you seen Trac for SVN? A great web interface for surfing you SVN repos.

jrosenthal replied on Friday, May 01, 2009

These posts are about 18 mo old - Has anything been done with this is 3.6.2 because I believe I am having similar issues with in class containing an int. 

RockfordLhotka replied on Friday, May 01, 2009

What is your specific issue?

 

From: jrosenthal [mailto:cslanet@lhotka.net]
Sent: Friday, May 01, 2009 9:05 PM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] RE: DataMapper and the pain

 

These posts are about 18 mo old - Has anything been done with this is 3.6.2 because I believe I am having similar issues with in class containing an int. 


jrosenthal replied on Saturday, May 02, 2009

At this point I am simply trying to create communication thru several layers.  I am using DalLinq with a business layer on top of that.  Then I created a service with its own interface.  When I call into the service from the client I am doing this:
private void button1_Click(object sender, EventArgs e)
        {
            SOATPostingService.PostingServiceClient psvc = new SOATPostingService.PostingServiceClient();
            SOATPostingService.PostingListRequest lreq = new DatabaseTest.SOATPostingService.PostingListRequest();
            SOATPostingService.PostingData pdata = new DatabaseTest.SOATPostingService.PostingData();
            List<SOATPostingService.PostingData> list = new List<DatabaseTest.SOATPostingService.PostingData>();

            SOATPostingService.PostingRequest postingreq = new DatabaseTest.SOATPostingService.PostingRequest();
            postingreq.PostingId = new Guid("D166F769-04EB-41D2-A607-6225A8AED9A4");
            pdata = psvc.GetPosting(postingreq);


        }


The GetPosting call throws a FaultException (property copy failed (Number)).  Number is a field in my business layer (and also that DAL) and is just an int. 

The fault occurs when GetCachedProperty() gets called for the int and info (the PropertyInfo) is null as a result of the PropertyInfo info = objectType.GetProperty(propertyName, propertyFlags);
            if (info == null)
              throw new InvalidOperationException(

This is in line 157 of MethodCaller.cs

I don't know if I am doing something wrong or if there is an actual bug.  I am trying to pattern my code after what I read in the book and in the PTTracker sample.

Thanks for your time on this.

Jeff

jrosenthal replied on Saturday, May 02, 2009

I believe the fault is my own - That field is not a field in my client so when I put it in the ignorelist it works.  Is there a way to automatically have fields that are not found simply ignored instead of having to use an ignorelist or is this a disaster waiting to happen?

Thanks and sorry for my misunderstanding.

RockfordLhotka replied on Saturday, May 02, 2009

In 3.6 you have two choices. Either use the ignore list, or create a DataMap to explicitly list the fields/properties that are being mapped to and from.

 

Rocky

Copyright (c) Marimer LLC