0

I'm trying to create a library that will pull from a web service and map the retrieved values to any class provided by the user using attributes to decorate the target class properties. This works fine for basic types, however, some "types" are a custom type (ConvertableDecimal, ...Int, ...Float) from another library that does unit conversions. The numeric values are stored in a common property of the type called "BaseValue".

Here is an example of how a property of a class implementing properties of these types would look:

[OEDProperty("Discharge Capacity Rated", "BaseValue")]
public ConvertableDecimal DischargeCapacity { get; set; } = new ConvertableDecimal(MeasureType.FlowRate);

"OEDProperty" is an attribute class I created to decorate the properties and it takes two inputs:

  1. The xml fieldname to be mapped (e.g. "Discharge Capacity Rated") and
  2. An optional parameter called "TargetMember", "BaseValue" in this case...

Here is the mapping method:

public static T Map<T> (OEDData OED, out string Errors)
{
    string mappingErrors = string.Empty;

    object retObj = Activator.CreateInstance (typeof (T)); //we'll reset this later if we need to, e.g. targeting a member
    PropertyInfo[] properties = retObj.GetType ().GetProperties();

    foreach (PropertyInfo pi in properties) 
    {
        OEDPropertyAttribute propAtt = (OEDPropertyAttribute) pi.GetCustomAttribute (typeof (OEDPropertyAttribute));
        if (propAtt != null) 
        {
            PropertyInfo piTargetMember = null;

            if (!string.IsNullOrEmpty (propAtt.TargetMember)) 
            {
                try 
                { 
                    piTargetMember = pi.PropertyType.GetProperty (propAtt.TargetMember); 
                }
                catch (Exception ex) 
                { 
                    mappingErrors += string.Format("Error locating target member \"{0}\" for type \"{1}\" when setting field \"{2}\".\r\nMake sure the target member name is spelled correctly. Target member names ARE case sensitive.\r\nError: {3}", 
                                                    propAtt.TargetMember, 
                                                    propAtt.GetType().Name, 
                                                    propAtt.Field.ToLower(), 
                                                    ex.Message); 
                }
            }

            if (propAtt.IsHeaderField) //header fields
            {
                /*snip*/
            } 
            else //fields
            {
                try 
                {
                    var fVal = OED.Fields.FirstOrDefault (f => f.FieldName.ToLower () == propAtt.Field.ToLower ()).Value;
                    var convertedFVal = (piTargetMember == null) ? ChangeType (fVal, pi.PropertyType) : ChangeType (fVal, piTargetMember.PropertyType);

                    if (piTargetMember == null) 
                    { 
                        pi.SetValue(retObj, convertedFVal); 
                    } 
                    else
                    {
                        pi.SetValue(retObj.GetType().GetProperty(propAtt.TargetMember), convertedFVal);
                        //error happens here
                        //error text: Non-static method requires a target
                    }
                }
                catch (Exception ex) 
                { 
                    mappingErrors += string.Format("Unable to map oed field value: \"{0}\".\r\nError: {1}", propAtt.Field.ToLower (), ex.Message); 
                }
            }
        }
    }

    Errors = mappingErrors;

    return (T) retObj;
}

The error text when trying to set the property value is: "Non-static method requires a target"

I undertstand from this post (Non-static method requires a target) that this is due to a null reference at runtime.

My question is, what options do I have for making this library work and be flexible with any user defined types that may occur in the future.

Any insights would be very much appreciated.

Etienne de Martel
  • 34,692
  • 8
  • 91
  • 111
beeker
  • 780
  • 4
  • 17
  • Took me a while to spot because the formatting was so weird, but that error occurs because the first parameter you're passing to `SetValue` is null and you have a non static property. That being said, I find the whole `pi.SetValue(retObj.GetType().GetProperty(propAtt.TargetMember), convertedFVal)` thing suspicious, because I doubt you want to pass the property itself as the target, right? – Etienne de Martel Jun 15 '19 at 04:52
  • I think this is the thing I'm not understanding... For a simple type (string, decimal, int, float...) if I do: pi.SetValue(retObj, convertedFVal); Then, the value sets fine... ConvertableDecimal is a class I created in a unit conversion library and there is a property called "BaseValue" (float?) that gets set in a common agreed numerical unit set and then presented in the users selected units (e.g. inches/mm, gallons/liters... etc) So, I need to be able to populate the "BaseValue" property of the "ConvertableDecimal" class/type – beeker Jun 15 '19 at 04:59
  • `prop.SetValue(target, value)` is the reflection equivalent to `target.prop = value`. Hence why you need a target if the property isn't static.. `GetProperty()` gives you a property, not a value: you'll need to call `GetValue()` on that (probably also passing a valid target) to get its value. – Etienne de Martel Jun 15 '19 at 05:02
  • So, using your convention above; I think what I'm actually going for is "target.prop.BaseValue= value" It's trying to set the "BaseValue" property of type "ConvertableDecimal" (type) property of class "MyClass" that I'm stumped on. It's one level deeper. If "prop" was a string and I said "target.prop = "Hello"" then, it's working every time but, targeting a property-of-a-property is what I'm not getting. MyClass > MyProperty (typeof(ConvertableDecimal) > BaseValue = 123.456 – beeker Jun 15 '19 at 05:29

1 Answers1

0

For the line in question, the one with an error, I would suggest the following changes:

piTargetMember.SetValue(pi.GetValue(retObj), ChangeType(fVal, piTargetMember.PropertyType));

Firstly, I moved the PropertyInfo from the target parameter to the property whose value is being set. From the surrounding code, I believe this was the intended operation.

That leaves the object target that you want to change to be the property pi of the retObj that was created earlier for this exact purpose.

And then I moved the conversion ternary function from above into this line as the value, because there is no sense in asking whether piTargetMember == null twice.


Edit:

In the case that your constructor for T doesn't create the property instance, you may have to do that in your code as well, which changes that line to three lines:

object propInstance = Activator.CreateInstance(pi.PropertyType);
piTargetMember.SetValue(propInstance, ChangeType(fVal, piTargetMember.PropertyType));
pi.SetValue(retObj, propInstance);

Full Code

While parsing through your code, I simplified much of the formatting. So I'll post the whole code as a sample:

public static T Map<T>(OEDData OED, out string Errors)
{
    string mappingErrors = string.Empty;

    object retObj = Activator.CreateInstance(typeof(T)); //we'll reset this later if we need to, e.g. targeting a member

    foreach (PropertyInfo pi in typeof(T).GetProperties()) 
    {
        if (pi.GetCustomAttribute(typeof(OEDPropertyAttribute)) is OEDPropertyAttribute propAtt) 
        {
            PropertyInfo piTargetMember = null;

            if (!string.IsNullOrEmpty(propAtt.TargetMember)) 
            {
                try 
                { 
                    piTargetMember = pi.PropertyType.GetProperty(propAtt.TargetMember); 
                }
                catch (Exception ex) 
                { 
                    mappingErrors += $"Error locating target member \"{propAtt.TargetMember}\" for type \"{propAtt.GetType().Name}\" when setting field \"{propAtt.Field.ToLower()}\"." + 
                                     $"\r\nMake sure the target member name is spelled correctly. Target member names ARE case sensitive.\r\nError: {ex.Message}\r\n"; 
                }
            }

            if (propAtt.IsHeaderField) //header fields
            {
                /*snip*/
            } 
            else //fields
            {
                try 
                {
                    var fVal = OED.Fields.FirstOrDefault((f) => string.Equals(f.FieldName, propAtt.Field, StringComparison.CurrentCultureIgnoreCase))?.Value;

                    if (piTargetMember == null)
                    {
                        pi.SetValue(retObj, ChangeType(fVal, pi.PropertyType));
                    }
                    else
                    {
                        object propInstance = pi.GetValue(retObj);
                        if (propInstance == null)
                        {
                            // construct the value which is the property pointed to by 'pi'
                            propInstance = Activator.CreateInstance(pi.PropertyType);
                            pi.SetValue(retObj, propInstance);
                        }
                        piTargetMember.SetValue(propInstance, ChangeType(fVal, piTargetMember.PropertyType));
                    }
                }
                catch (Exception ex) 
                { 
                    mappingErrors += $"Unable to map oed field value: \"{propAtt.Field.ToLower()}\".\r\nError: {ex.Message}\r\n";
                }
            }
        }
    }

    Errors = mappingErrors;

    return (T) retObj;
}
Rhaokiel
  • 813
  • 6
  • 17