2
Vote

Implementing IDictionary results in not mapped exception

description

Example:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace dict.Models
{
    public class DictItem
    {
        [Key]
        public int Key { get; set; }
        public string Value { get; set; }
    }
    public class DictList
    {
        public int DictListId { get; set; }
        public List<DictItem> MyFakeDict { get; set; }
    }
    public class Dict : IDictionary<int, string>
    {
        public int DictId { get; set; }
        public List<DictItem> MyDict { get; set; }

        public void Add(int key, string value)
        {
            if (!ContainsKey(key))
                MyDict.Add(new DictItem { Key = key, Value = value });
            else
                throw new System.ArgumentException("An item with the same key has already been added.");
        }

        public bool ContainsKey(int key)
        {
            foreach (int Key in Keys)
                if (Key == key)
                    return true;

            return false;
        }

        public ICollection<int> Keys
        {
            get
            {
                List<int> languagelist = new List<int>();
                foreach (DictItem item in MyDict)
                    languagelist.Add(item.Key);

                return languagelist;
            }
        }

        public bool Remove(int key)
        {
            foreach (DictItem item in MyDict)
            {
                if (item.Key == key)
                    return MyDict.Remove(item);
            }
            return false;
        }

        public bool TryGetValue(int key, out string value)
        {
            foreach (DictItem item in MyDict)
            {
                if (item.Key == key)
                {
                    value = item.Value;
                    return true;
                }
            }
            value = default(string);
            return false;
        }

        public ICollection<string> Values
        {
            get
            {
                ICollection<string> itemlist = new List<string>();
                foreach (DictItem item in MyDict)
                    itemlist.Add(item.Value);

                return itemlist;
            }
        }

        public string this[int key]
        {
            get
            {
                foreach (DictItem item in MyDict)
                {
                    if (item.Key == key)
                        return item.Value;
                }
                return default(string);
            }
            set
            {
                if (!ContainsKey(key))
                    Add(key, value);
            }
        }

        public void Add(KeyValuePair<int, string> item)
        {
            Add(item.Key, item.Value);
        }

        public void Clear()
        {
            MyDict.Clear();
        }

        public bool Contains(KeyValuePair<int, string> item)
        {
            string founditem;
            if (TryGetValue(item.Key, out founditem))
                return founditem.Equals(item.Value);

            return false;
        }

        public void CopyTo(KeyValuePair<int, string>[] array, int arrayIndex)
        {
            if (array == null)
                throw new System.ArgumentNullException("array is null.");
            if (arrayIndex < 0)
                throw new System.ArgumentOutOfRangeException("index is less than zero.");
            if (array.Length - arrayIndex < this.Count)
                throw new System.ArgumentException("The number of elements in the source System.Collections.Generic.Dictionary<TKey,TValue>.KeyCollection is greater than the available space from index to the end of the destination array.");
            for (int i = 0; i < MyDict.Count; i++)
            {
                array[i + arrayIndex] = new KeyValuePair<int, string>(MyDict[i].Key, MyDict[i].Value);
            }
        }

        public int Count
        {
            get { return MyDict.Count; }
        }

        public bool IsReadOnly
        {
            get { return false; }
        }

        public bool Remove(KeyValuePair<int, string> item)
        {
            return Remove(item.Key);
        }

        public IEnumerator<KeyValuePair<int, string>> GetEnumerator()
        {
            throw new NotImplementedException();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            throw new NotImplementedException();
        }
    }
    public class MyClass
    {
        public int MyClassId { get; set; }
        public DictList MyDictList { get; set; }
        public Dict MyDict { get; set; }
    }
}

file attachments

comments

ajcvickers wrote Mar 2, 2013 at 10:18 PM

@cbke EF currently doesn't support mapping IDictionary properties and Code First will, by default, not map this property. If an attempt is then made to use the property in a query or similar then EF will generate a NotMapped exception.

We are considering adding support for more flexible mapping in a future release, but for now this is not supported.

Thanks,
Arthur

ajcvickers wrote Mar 6, 2013 at 11:27 PM

Rowan pointed out that the Dict class was probably intended to be an entity type (since it has a key) in which case the MyDict property should probably be treated as a reference navigation property. EF currently doesn't do this and if an attempt is made to map the navigation property explicitly (e.g. builder.Entity<MyClass>().HasOptional(e => e.MyDict).WithMany();) then an exception is thrown--see below. This is because MyDict implements ICollection, which confuses both the discovery and configuration APIs.

It would probably be hard and/or fragile to try to detect this as part of discovery, but it is not unreasonable to allow it to work when an explicit mapping is made.

Trace:
Unhandled Exception: System.InvalidOperationException: The navigation property 'MyDict' is not a declared property on ty
pe 'MyClass'. Verify that it has not been explicitly excluded from the model and that it is a valid navigation property.

   at System.Data.Entity.ModelConfiguration.Configuration.Types.EntityTypeConfiguration.ConfigureAssociations(EdmEntityT
ype entityType, EdmModel model)
   at System.Data.Entity.ModelConfiguration.Configuration.Types.EntityTypeConfiguration.Configure(EdmEntityType entityTy
pe, EdmModel model)
   at System.Data.Entity.ModelConfiguration.Configuration.ModelConfiguration.ConfigureEntities(EdmModel model)
   at System.Data.Entity.DbModelBuilder.Build(DbProviderManifest providerManifest, DbProviderInfo providerInfo)
   at System.Data.Entity.DbModelBuilder.Build(DbConnection providerConnection)
   at System.Data.Entity.Internal.LazyInternalContext.CreateModel(LazyInternalContext internalContext)
   at System.Data.Entity.Internal.RetryLazy`2.GetValue(TInput input)
   at System.Data.Entity.Internal.LazyInternalContext.InitializeContext()
   at System.Data.Entity.Database.Initialize(Boolean force)
   at Investigate664.Program.Main(String[] args) in c:\Stuff\Investigate664\Investigate664\Program.cs:line 201

RoMiller wrote Mar 7, 2013 at 11:40 PM

EF Team Triage: We agree that this would be a good scenario to enable. Taking into account where we are in the EF6 release along with the size and the impact of this feature our team is not planning to implement it in EF6. Therefore, we are moving it to the Future release to reconsider in the next release.

Marchy wrote Nov 6, 2013 at 6:05 PM

This would indeed be a good scenario to support.

It affects any situation where an entity has an association which is conceptually a simple map/dictionary. From on OO perspective you want to treat that association as an IDictionary, even if behind the scenes you have to model it as a full entity association as shown in the example above.

The workarounds/alternatives take us further away from the true OO design which code-first aims to allow us to achieve; and it leaks an unnecessary amount of underlying database implementation details up into the domain model which then gets used throughout the entire codebase.


I think the EF team should in general prioritize issues that impact the ability to model the natural OO design of entities as generally higher than other priorities - in other words give it a biased amount of priority with respect to the cost/impact at other initiatives at hand. The simple reasoning being that these architectural-level issues force necessary mutations of the OO design in the consuming codebases and this mid-design bubbles up to impact systems at upper layers (at the very least the consuming business layer, and at worst propagates across API boundaries to mobile/web clients as well). These design costs plague codebases for long times to come and thus their real costs are much higher than the initially-perceived cost of finding a workaround for the limitation at hand. Would love to know whether the EF team already has this view when it comes to framework priorities, or what the team's stance is on this point.


Cheers!