0

I've created a data class that I plan to use to send data to be persisted in the database and to return data from the database in a strongly typed way. In addition to its properties, the class contains a Dictionary that I populate in the constructor with the name of and reference to each property. This makes the properties enumerable and enables me to iterate through them using 'foreach'.

This works great when setting property values and sending the object to be persisted in the database. I can iterate through the Dictionary keys, get the value of each property, and add a SqlParameter for each property using the key as the parameter name and the property value as the parameter value.

However, going the other way doesn't work. I can iterate through the Dictionary keys and get the value of each column in each row of the SqlDataReader, but when I try to assign these values to my data object using the Dictionary's reference to the corresponding object property, a curious thing occurs. The assignments succeed, BUT the data object properties all retain their initial, default values. I can view the data object properties and see these initial, default values. I can also view the Dictionary entry values and see the updated values that were read and assigned from the SqlDataReader.

This makes no sense. The Dictionary is supposed to provide access to each property (the 'object' generic type) via its key (the 'string' generic type), but its acting like its maintaining a separate copy of each Dictionary 'KeyValuePair'.

What gives?

I'm doing all this in C# in the context of an ASP.NET Core 2.1.1 project running on macOS 10.13.6 High Sierra.

I've searched StackOverflow extensively, and I see lots of recommendations for using reflection to do this type of thing. I'll refactor my code to use reflection if necessary, but I'd really like to understand where and how my mental model for what's happening is off.

An explanation of what's happening and why would be MOST appreciated.

Example Data Class with Property Dictionary

using System;
using System.Collections.Generic;
using System.Text;
using System.Reflection;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
using Newtonsoft.Json;

namespace MyOrg.MyProj.Data
{
    [DataContract]
    public class DataObj
    {
        #region Attributes

        [Required]
        [DataMember(Name = "dataObjectId")]
        public Int64 DataObjectId { get; set; }

        [Required]
        [DataMember(Name = "guid")]
        public Guid Guid { get; set; }

        public virtual Dictionary<string, object> DataMembers { get; set; }     //NOTE: Implements the IEnumerable interface in order to support 'foreach' operations, etc on 'DataObj' class attributes

        #endregion Attributes

        #region Constructors

        public DataObj(Int64 dataObjectId, Guid guid)
        {
            try
            {
                DataObjectId = dataObjectId;
                Guid = guid;

                DataMembers = new Dictionary<string, object>
                {
                    { "DataObjectId", DataObjectId },
                    { "Guid", Guid }
                };
            }
            catch (Exception e)
            {
                Console.WriteLine($"RUNTIME EXCEPTION while INSTANTIATEing DataObj, " + e.Message + ", " + e.StackTrace);
            }
        }

        #endregion Constructors

        #region Methods

        /// <summary>
        /// Implements the IEnumerable interface in order to support 'foreach' operations, etc on 'DataObj' class attributes
        /// </summary>
        /// <returns>Enumerator</returns>
        public Dictionary<string, object>.Enumerator Enumerator()
        {
            return DataMembers.GetEnumerator();     //NOTE: Return the Dictionary object's IEnumerator rather than implementing IEnumerable for the 'DataObj' class itself
        }

        #endregion Methods

Example Data Access Class (excerpt)

                reader = command.ExecuteReader();

                dataObjList = new List<DataObj>();

                if (reader.HasRows)
                {
                    while (reader.Read())
                    {
                        tempDataObj = new DataObj(-1, new Guid("00000000-0000-0000-0000-000000000000"));

                        keys = new List<String>(tempDataObj.DataMembers.Keys);       //NOTE: Can't modify a Dictionary while iterating through it.  See the 'Why This Error?' section of https://stackoverflow.com/questions/604831/collection-was-modified-enumeration-operation-may-not-execute

                        foreach (String key in keys)
                        {
                            tempDataObj.DataMembers[key] = reader[key];
                        }

                        dataObjList.Add(tempDataObj);

For 'key' = 'DataObjectId', 'Guid', etc, I expect the value of tempDataObj.DataObjectId, tempDataObj.Guid, etc to be set to the value returned from the database in 'reader[key]'.

Instead, it retains its initial, default value as set in the constructor, i.e. '-1'. This is true for both value and reference data types.

However, when I inspect tempDataObj.DataMembers["DataObjectId"], it has been set to the value returned from the database in 'reader[key]'.

Inspecting the Object Property and Dictionary Values

tempDataObj.DataMembers["DataObjectId"] should be referencing the tempDataObj.DataObjectId property, etc, but the Dictionary appears to be maintaining its own value rather than providing an object reference to the 'DataObjectId' property.

What's going on here? Thank you!

Doug Wilson
  • 343
  • 3
  • 8
  • `Int64` alias `long` is a value type and therefore passed by value, not by reference. – sticky bit Apr 10 '19 at 23:37
  • Ints are not passed by reference. Read about the ref keyword. – MineR Apr 10 '19 at 23:42
  • Side note... to make it work, in the getter and setter of DataObjectId, directly call the dictionary - then it will work. – MineR Apr 10 '19 at 23:44
  • You're setting the `Dictionary`'s key value in your `while` loop. Why do you think your `DataObjectId` **property** will change? Your `Dictionary` with key `DataObjectId` has the actual value of your property but then you override the value in your `Dictionary`, your property won't change. – JohanP Apr 10 '19 at 23:46
  • @alexei-levenkov, the answers to question 5057267 in no way address the question I'm asking here, e.g. accessing object **property** values via a Dictionary object reference. – Doug Wilson Apr 11 '19 at 16:42
  • @stickybit, I've updated my example code to include a Guid data type, but the issue is the same regardless of data types. It also affects Strings, DateTimes, etc. – Doug Wilson Apr 11 '19 at 16:43
  • @MineR, I've updated my example code to include a Guid data type, but the issue is the same regardless of data types. It also affects Strings, DateTimes, etc. – Doug Wilson Apr 11 '19 at 16:43
  • @MineR, I've tried updating the Dictionary in the property setter, and it results in a null pointer exception. – Doug Wilson Apr 11 '19 at 16:43
  • @JohanP, I'm setting the Dictionary's **object** value in my `while` loop, not its key value. The successful results of this can be seen clearly in the screen shot I provided. – Doug Wilson Apr 11 '19 at 16:44
  • Thank you all for your thoughts, but none of this actually explains why the Dictionary maintains its own set of values while the data object's properties remain unaffected, regardless of data type. – Doug Wilson Apr 11 '19 at 16:44
  • @DougWilson: `Guid` is also a value type. – sticky bit Apr 11 '19 at 16:54
  • @DougWilson you are right - it looked like it was the only part you don't understand, but apparently your confusion is broader - maybe https://stackoverflow.com/questions/8708632/passing-objects-by-reference-or-value-in-c-sharp could help a bit. You may want to experiment with much simpler code (ditch dictionary and just use local variable to save value of the property and than update that local variable. Stare at it for some time and explain yourself why value of the property should change) – Alexei Levenkov Apr 11 '19 at 18:40
  • (Good thing that you already know solution - reflection - to actual task you trying to solve, so at least you are not blocked on implementing whatever you are doing) – Alexei Levenkov Apr 11 '19 at 18:43
  • OK, @stickybit. How about strings? They don't work either. _"the issue is the same regardless of data types. It also affects Strings, DateTimes, etc"_ – Doug Wilson Apr 11 '19 at 19:41
  • Thanks for your patience, @AlexeiLevenkov. I'm staring. I've even drawn this out on a whiteboard. vmg's example answer to stackoverflow.com/questions/8708632/ makes complete sense ... in the context of passing a parameter to a method. But this doesn't seem to apply to object properties, i.e. there's no way to specify that a setter pass a parameter by reference. Any suggestions on articles I could stare at that would explain why reflection gets around this unwanted (by me) behavior? – Doug Wilson Apr 11 '19 at 20:07
  • @DougWilson `int value; … // after some code value == 2` - how would you know if that 2 come from a property or expression or just literal constant? With reflection you are forced to do correct thing - "set property with *this name* on *that object* to *the value*" - storing the value in a variable loses both "this name" and "that object" parts... – Alexei Levenkov Apr 11 '19 at 20:35
  • @DougWilson If you got a nullrefexception, it's because you need to initialize the Dictionary first. – MineR Apr 12 '19 at 02:35
  • Apologies, @AlexeiLevenkov, but I'm not following your `int value; … // after some code value == 2` scenario. The way I'm trying to set property values currently is by direct reference to each property to my 'DataMembers' Dictionary. This provides both the _property with this name_ and _on this object_ information needed to set the intended property's value. As for how I would know the source of the value, in this particular context I can see it being set from the SqlDataReader's result set in `tempDataObj.DataMembers[key] = reader[key];`. – Doug Wilson Apr 12 '19 at 16:19
  • Good point, @MineR. I'll try moving the Dictionary initialization up in the constructor's "batting order". Thanks! – Doug Wilson Apr 12 '19 at 16:21
  • @DougWilson I think I got completely confused on what you want - title, body, and code sample talks about somewhat different things (the way I read it) and mentioning of non-existent "direct reference to each property" in comments clearly makes my comments unrelated. Sorry about that, I'm glad enough people actually figured out what you want. – Alexei Levenkov Apr 12 '19 at 18:24
  • @AlexeiLevenkov, I'm so grateful that you took the time to read my question and to try to answer it. You raised several issues that I'm certain will help improve my understanding of how all this fits together. My sincere thanks. – Doug Wilson Apr 12 '19 at 21:16

2 Answers2

0

I see two (main) routes to do what you want. In both cases you should implement a custom indexer.

  1. In the indexer explicitly check the name given to it and get or set the field or property accordingly.

  2. Use reflection, i.e. GetField() or GetProperty(), to get the field or property and GetValue() or SetValue() to get or set the values.

Below is a demonstration where ExposeByExplicitIndexer0 and its descendants use way 1 and ExposeByIndexerUsingReflection0 and its descendants use way 2.

public class ExposeByExplicitIndexer0
{
    public int Int0 = 1;
    public string String0 = "A";

    public virtual object this[string name]
    {
        get
        {
            switch (name)
            {
                case "Int0":
                    return this.Int0;
                case "String0":
                    return this.String0;
                default:
                    throw new IndexOutOfRangeException();
            }
        }
        set
        {
            switch (name)
            {
                case "Int0":
                    this.Int0 = (int)value;
                    break;
                case "String0":
                    this.String0 = (string)value;
                    break;
                default:
                    throw new IndexOutOfRangeException();
            }
        }
    }
}

public class ExposeByExplicitIndexer1 : ExposeByExplicitIndexer0
{
    protected Guid _Guid1 = Guid.Empty;
    public Guid Guid1
    {
        get
        {
            return this._Guid1;
        }
        set
        {
            this._Guid1 = value;
        }
    }

    public override object this[string name]
    {
        get
        {
            switch (name)
            {
                case "Guid1":
                    return this.Guid1;
                default:
                    return base[name];
            }
        }
        set
        {
            switch (name)
            {
                case "Guid1":
                    this.Guid1 = (Guid)value;
                    break;
                default:
                    base[name] = value;
                    break;
            }
        }
    }
}

public class ExposeByIndexerUsingReflection0
{
    public object this[string name]
    {
        get
        {
            FieldInfo fieldInfo;
            if ((fieldInfo = this.GetType().GetField(name)) != null)
            {
                return fieldInfo.GetValue(this);
            }
            PropertyInfo propertyInfo;
            if ((propertyInfo = this.GetType().GetProperty(name)) != null)
            {
                return propertyInfo.GetValue(this);
            }
            throw new IndexOutOfRangeException();
        }
        set
        {
            FieldInfo fieldInfo;
            if ((fieldInfo = this.GetType().GetField(name)) != null)
            {
                fieldInfo.SetValue(this, value);
                return;
            }
            PropertyInfo propertyInfo;
            if ((propertyInfo = this.GetType().GetProperty(name)) != null)
            {
                propertyInfo.SetValue(this, value);
                return;
            }
            throw new IndexOutOfRangeException();
        }
    }
}

public class ExposeByIndexerUsingReflection1 : ExposeByIndexerUsingReflection0
{
    public int Int1 = 1;
    public string String1 = "A";
}

public class ExposeByIndexerUsingReflection2 : ExposeByIndexerUsingReflection1
{
    protected Guid _Guid2 = Guid.Empty;
    public Guid Guid2
    {
        get
        {
            return this._Guid2;
        }
        set
        {
            this._Guid2 = value;
        }
    }
}

public class Program
{
    static void Main(string[] args)
    {
        Guid newGuid = Guid.NewGuid();

        Console.WriteLine("Expose by explicit indexer:");
        ExposeByExplicitIndexer1 exposeByExplicitIndexer1 = new ExposeByExplicitIndexer1();
        exposeByExplicitIndexer1["Int0"] = 10;
        exposeByExplicitIndexer1["String0"] = "AAA";
        exposeByExplicitIndexer1["Guid1"] = newGuid;
        Console.WriteLine("output via indexer:");
        Console.WriteLine(exposeByExplicitIndexer1["Int0"]);
        Console.WriteLine(exposeByExplicitIndexer1["String0"]);
        Console.WriteLine(exposeByExplicitIndexer1["Guid1"]);
        Console.WriteLine("output via fields or properties:");
        Console.WriteLine(exposeByExplicitIndexer1.Int0);
        Console.WriteLine(exposeByExplicitIndexer1.String0);
        Console.WriteLine(exposeByExplicitIndexer1.Guid1);

        Console.WriteLine("Expose by indexer using reflection:");
        ExposeByIndexerUsingReflection2 exposeByIndexerUsingReflection2 = new ExposeByIndexerUsingReflection2();
        exposeByIndexerUsingReflection2["Int1"] = 10;
        exposeByIndexerUsingReflection2["String1"] = "AAA";
        exposeByIndexerUsingReflection2["Guid2"] = newGuid;
        Console.WriteLine("output via indexer:");
        Console.WriteLine(exposeByIndexerUsingReflection2["Int1"]);
        Console.WriteLine(exposeByIndexerUsingReflection2["String1"]);
        Console.WriteLine(exposeByIndexerUsingReflection2["Guid2"]);
        Console.WriteLine("output via fields or properties:");
        Console.WriteLine(exposeByIndexerUsingReflection2.Int1);
        Console.WriteLine(exposeByIndexerUsingReflection2.String1);
        Console.WriteLine(exposeByIndexerUsingReflection2.Guid2);

        Console.Read();
    }
}

In way 1 every descendant that adds new fields or properties has to extend the indexer. That's more work in general but also offers an easy way of flexibility i.e. for adding some casts or expose some field or property via an alias, etc.

Way 2 needs less effort in the descendants. But being as flexible as in way 1 may become more difficult in turn. Maybe some mixed solution is also possible overriding the indexer in some descendant to inject special logic.

sticky bit
  • 36,626
  • 12
  • 31
  • 42
  • Thank you **very** much, @stickybit! I've been considering a custom indexer, and your helpful examples have really got my wheels turning. Many, many thanks! – Doug Wilson Apr 12 '19 at 16:24
0

You're storing the data twice - once in a Dictionary, and a second time in a field. There's no need to store it twice. Just do this:

[DataContract]
public class DataObj
{
    [Required]
    [DataMember(Name = "dataObjectId")]
    public Int64 DataObjectId
    {
        get => (long)DataMembers[nameof(DataObjectId)];
        set => DataMembers[nameof(DataObjectId)] = value;
    }

    [Required]
    [DataMember(Name = "guid")]
    public Guid Guid
    {
        get => (Guid)DataMembers[nameof(Guid)];
        set => DataMembers[nameof(Guid)] = value;
    }

    public Dictionary<string, object> DataMembers { get; } = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);

    public DataObj(Int64 dataObjectId, Guid guid)
    {
        DataObjectId = dataObjectId;
        Guid = guid;
    }

    public Dictionary<string, object>.Enumerator Enumerator()
    {
        return DataMembers.GetEnumerator();
    }
}

FYI, you can also look at using an ExpandoObject, which lets you access something in a way that looks like a class, but is really just a Dictionary. https://learn.microsoft.com/en-us/dotnet/api/system.dynamic.expandoobject?view=netframework-4.7.2

I have never used an ExpandoObject and I think the whole idea is as perverse as VBA's default of option explicit being off and On Error Resume Next. On the other hand, I don't deal with databases much.

MineR
  • 2,144
  • 12
  • 18
  • Oh, man! @MineR, you are sneaking up on answer to my "Why Can't .NET Object Property Values Be Set Via Dictionary Keys and Object References?" question. I love your idea/suggestion, and I am ever so grateful for the code example. Thank you! I still can't quite get my head around Alexei's explanation of "why", but I'm going to mark your pragmatic suggestion as my accepted answer and wait for enlightenment to strike on why the way I've been trying to do it results in two copies of the data rather than one with references in the Dictionary as I think it "should" work. Thanks again! – Doug Wilson Apr 12 '19 at 16:33
  • I've refactored a couple of data object classes, and your suggested answer seems to be working great. Thanks so much, @MineR! – Doug Wilson Apr 12 '19 at 21:18