WebApi
WebApi copied to clipboard
Serialize content of Dictionary<string, string>
Hi,
I have an entity with a property of type Dictionary<string, string>. Existing data in the dictionary is not serialized in responses. Now, I don't know if such dictionaries are intended to be supported at all, but the dictionary property shows up nicely in the EDM so I thought this could work, but it doesn't. This is not related to open types, I just need a string dictionary. Can someone clarify?
Scenario:
public partial class Zappa
{
public Zappa()
{
Header = new Dictionary<string, string>();
}
public Guid Id { get; set; }
public Dictionary<string, string> Header { get; set; }
}
<EntityType Name="Zappa" BaseType="Ga.Data.EntityBase">
-<Key>
<PropertyRef Name="Id"/>
</Key>
<Property Name="Id" Type="Edm.Guid"/>
<Property Name="Header" Type="Collection(System.Collections.Generic.KeyValuePair_2OfString_String)"/>
</EntityType>
In controller:
item.Header = new Dictionary<string, string>();
item.Header.Add("Hello", "Is this OK?");
Response data:
{
"@odata.context":"http://localhost:26162/odata/$metadata#Zappa/$entity",
"Id":"df45be0e-cab8-4f90-aeb3-7f9b5608aaff","Header":[
{
}]
}
I'm using: Microsoft.AspNet.OData 5.7.0-beta-150716-Nightly (and the latest nightly ODataLib packages) .NET 4.5.2 VS 2015 RC
Regards, Kasimier Buchcik
It's not supported so far. Can you create a complex type and mark the Header as the collection of such complex type. For example
public class MyKeyValuePair
{
public string Key {get;set;}
public string Value {get;set;}
}
<EntityType Name="Zappa" BaseType="Ga.Data.EntityBase">
-<Key>
<PropertyRef Name="Id"/>
</Key>
<Property Name="Id" Type="Edm.Guid"/>
<Property Name="Header" Type="Collection(Ga.Data.MyKeyValuePair)"/>
</EntityType>
<ComplexType Name="MyKeyValuePair">
<Property Name="Key" Type="Edm.String"/>
<Property Name="Value" Type="Edm.String"/>
</ComplexType>
@xuzhg: Thanks for pointing out a workaround.
My current workaround is to put everything into the open type dynamic properties container (IDictionary<string, object>
) and wait until the generic dictionary is supported, because:
- I can already now add the desired properties as non-data members, but let them operate on the dynamic properties container. This involves some conversion, but the code is generated for me anyway and I want the interface to my classes be already defined.
- I don't want to generate a key-value-pair class for every combination of string -> type
This works fine for the server and .NET clients, since I share this code on all .NET apps.
The only issue left is with web clients, but I think I can find a generic way to mimic the desired interface.
We need a design for this, if we want to support generic type, such as dictionary:
- what is the type name?
- what are the property names?
I don't know if the issue should be about all generic types or specifically Dictionary<TKey, TValue>
.
But yes, what is the type name? Currently it is possible to have duplicate types names, demonstrated by the following example. This might be a bug worthy of fixing, or not because it's also quite an esoteric case.
One gets (note the duplicate "Generic_1OfT"):
<ComplexType Name="Generic_1OfT"/>
<ComplexType Name="Generic_1OfT"/>
if one defines
public class Generic<T>
{ }
public class Generic_1OfT
{}
with
builder.AddComplexType(typeof(Generic<>));
builder.ComplexType<Generic_1OfT>();
If we put the Dictionary into focus, then I would differentiate between:
-
Property of type
Dictionary<string, object>
: This is handled as a open type dynamic properties container. There can only be one such property on a type. Effectively it's not treated as a property (especially on the JavaScript side), but just a container for overflow named values of any type. Handy for versioning, transport of related extra data and handy for JavaScript libraries which insist in sending you more data than you actually want. -
Property of type
Dictionary<string, SomeType>
: This is the form I'm interested in. A property defining named values of a specific type.
OData Web Api could implicitely add a generic KeyValuePair<string, SomeType> complex type to the model as long as SomeType is an Edm.PrimitiveType or a registered complex type.
JSON serialization would be similar to dynamic properties container serialization, except for:
one (maybe?) would have to be able to annotate the property with generic type arguments somehow. I don't see support for this in the OData spec. I.e.:
@odata.type = #Ns.Dictionary_OfYouTellMe
does exist, but
@odata.genericTypeArguments = [Edm.String, #Ga.Data.SomeType]
does not exist.
- Property of type
Dictionary<SomeType, SomeType>
: This is the tricky form. I don't have need for this. Handy for programming languages where this is supported, but makes less sense on e.g. JavaScript's side.
I just experimented a bit with dictionaries. I don't know yet what the outcome actually means for me, because it is getting late and I need some sleep.
Scenario type:
[DataContract]
public class Salutation
{
[DataMember]
public Dictionary<string, string> MyAttributes1 { get; set; } = new Dictionary<string, string>();
[DataMember]
public ODataNamedValueDictionary<string, string> MyAttributes2 { get; set; } = new ODataNamedValueDictionary<string>();
}
I also tried the following form, but this resulted in an error. Somehow OData Web Api fails to recognize that ODataNamedValueDictionary
[DataContract]
public class Salutation
{
[DataMember]
public IDictionary<string, string> MyAttributes1 { get; set; } = new Dictionary<string, string>();
[DataMember]
public IDictionary<string, string> MyAttributes2 { get; set; } = new ODataNamedValueDictionary<string>();
}
-
Dictionary<string, string>
: Surprise, this is already supported. One only needs to register the type explicitely: It would be great if OData Web Api could make this easier and one could avoid registering each generic form of this type.
builder.AddComplexType(typeof(Dictionary<string, string>));
The generated schema:
<Schema xmlns="http://docs.oasis-open.org/odata/ns/edm" Namespace="System.Collections.Generic">
<ComplexType Name="Dictionary_2OfString_String">
<Property Name="Keys" Type="Collection(Edm.String)"/>
<Property Name="Values" Type="Collection(Edm.String)"/>
</ComplexType>
</Schema>
The data being generated:
"MyAttributes1":{
"Keys":[
"Hello"
],"Values":[
"Is this OK?"
]
}
- I implemented IDictionary<string, TValue> on the type ODataNamedValueDictionary and used the open type dynamic property container as its underlying storage. Here I also must register the type, otherwise no data is serialized. It would be great if OData Web Api could make this easier and one could avoid registering each generic form of this type.
builder.AddComplexType(typeof(ODataNamedValueDictionary<string>));
I.e. just registering:
builder.AddComplexType(typeof(ODataNamedValueDictionary<>));
is not enough. I think it would be really great if OData Web Api could some day figure out which generic forms of this type are used and register them for us.
This produces the following data. That's exactly what I hoped for.
"MyAttributes2":{
"Hello":"Is this OK?"
}
ODataNamedValueDictionary. I also have to make the underlying container property public, which is not nice.
[DataContract]
public class ODataNamedValueDictionary<TValue> : IDictionary<string, TValue>
{
[DataMember]
// NOTE: Must be public, otherwise OData Web Api won't serialize anything.
public IDictionary<string, object> Items { get { return _items ?? (_items = new Dictionary<string, object>()); } set { _items = value; } }
IDictionary<string, object> _items;
public TValue this[string key]
{
get { return (TValue)Items[key]; }
set { Items[key] = value; }
}
public int Count
{
get { return Items.Count; }
}
public bool IsReadOnly
{
get { return Items.IsReadOnly; }
}
public ICollection<string> Keys
{
get { return Items.Keys; }
}
/// <summary>
/// NOTE: This method will create a new ReadOnlyCollection based on the
/// values of the underlying dictionary.
/// </summary>
public ICollection<TValue> Values
{
get { return new ReadOnlyCollection<TValue>(Items.Values.Cast<TValue>().ToList()); }
}
public void Add(KeyValuePair<string, TValue> item)
{
Items.Add(Convert(item));
}
public void Add(string key, TValue value)
{
Items.Add(key, value);
}
public void Clear()
{
Items.Clear();
}
public bool Contains(KeyValuePair<string, TValue> item)
{
return Items.Contains(Convert(item));
}
public bool ContainsKey(string key)
{
return Items.ContainsKey(key);
}
public void CopyTo(KeyValuePair<string, TValue>[] array, int arrayIndex)
{
if (array == null) throw new ArgumentNullException(nameof(array));
if (arrayIndex < 0) throw new ArgumentOutOfRangeException(nameof(arrayIndex));
if (Items.Count > array.Length - arrayIndex)
throw new ArgumentException("The number of elements in the source dictionary " +
"is greater than the available space from arrayIndex to the end of the destination array.",
nameof(arrayIndex));
var i = 0;
foreach (var item in Items)
{
array[i] = Convert(item);
i++;
}
}
public bool Remove(KeyValuePair<string, TValue> item)
{
return Items.Remove(Convert(item));
}
public bool Remove(string key)
{
return Items.Remove(key);
}
public bool TryGetValue(string key, out TValue value)
{
object obj;
if (Items.TryGetValue(key, out obj))
{
value = (TValue)obj;
return true;
}
value = default(TValue);
return false;
}
public IEnumerator<KeyValuePair<string, TValue>> GetEnumerator()
{
foreach (var item in Items)
yield return Convert(item);
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
KeyValuePair<string, object> Convert(KeyValuePair<string, TValue> item)
{
return new KeyValuePair<string, object>(item.Key, item.Value);
}
KeyValuePair<string, TValue> Convert(KeyValuePair<string, object> item)
{
return new KeyValuePair<string, TValue>(item.Key, (TValue)item.Value);
}
}
I tested your custom dictionary implementation with odata on asp.net core. It works!
Is there any way this PR above will be merged? The above suggested answer is not working for me... it may also be the lack of code...
The workaround of Casimodo72 still works in ASP.NET 5
Exact steps I followed were:
- Copying and adding the class
ODataNamedValueDictionary<TValue>
Note that the attributes[DataContract]
and[DataMember]
are required to hideKeys
andValues
- Registering it as a complex type of the model through
oDataModelBuilder.AddComplexType(typeof(ODataNamedValueDictionary<string>));
- Adding a property of type
ODataNamedValueDictionary<TValue>
that is to be published through oData
public class MyClass
{
public ODataNamedValueDictionary<string> MyAttributes { get; init; } = new();
}
Note that making it of type IDictionary<string, string>
will not work
Would be great if this gets supported!
@huesla I got the @Casimodo72's fix to work on .NET 5.0, but only when the generic type of the ODataNamedValueDictionary
is string
. Could you get it to work with other types?
In my case, I'm trying to retun an ODataNamedValueDictionary<ContactDto>
, but it fails with the following error:
System.Runtime.Serialization.SerializationException: ODataResourceSerializer cannot write an object of type 'Collection(System.Collections.Generic.KeyValuePair_2OfString_ContactDto)'.
I have public IDictionary<string, string> SomeSettings{ get; set; }
property and I'm getting error System.Runtime.Serialization.SerializationException: ODataResourceSerializer cannot write an object of type 'Collection(System.Collections.Generic.KeyValuePair_2OfString_String)'.
I cannot change Idictionary to other type. Is there any workaround?
Is there any updates about it?
It looks like it can be fixed this way, but it doesn't seem to be the best solution
builder.ComplexType<Dictionary<string, string>>();