Referencing this SO question about custom serialization of strings to enums and vice versa in Json.NET, decorating the enum members using the EnumMember attribute - is there a way to get MongoDB to perform the same feat?
I have just refactored some previously string fields to enums and was wondering if there is any way to instruct Mongo to also read the EnumMember values when (de-)serializing and avoid me having to go through the database and update all the current text values.
i´m using package: PackageReference Include="MongoDB.Bson" Version="2.12.1"
my map class:
public class OfferMap
{
public static void Configure()
{
BsonClassMap.RegisterClassMap<Offer>(map => //Offer is a class
{
map.AutoMap();
map.SetIgnoreExtraElements(true);
map
.SetIsRootClass(true);
map
.MapMember(x => x.OfferType)
.SetSerializer(new EnumSerializer<OfferType>(MongoDB.Bson.BsonType.String)) // OfferType is an Enum
.SetElementName("offerType")
.SetIgnoreIfNull(false)
.SetIsRequired(true);
I used a CustomEnumSerializer to handle the EnumMember attribute
public class CustomEnumSerializer<TEnum> : StructSerializerBase<TEnum>, IRepresentationConfigurable<CustomEnumSerializer<TEnum>> where TEnum : struct
{
private static readonly Dictionary<Type, Dictionary<string, object>> _fromValueMap = new Dictionary<Type, Dictionary<string, object>>(); // string representation to Enum value map
private static readonly Dictionary<Type, Dictionary<object, string>> _toValueMap = new Dictionary<Type, Dictionary<object, string>>(); // Enum value to string map
// private fields
private readonly BsonType _representation;
// constructors
/// <summary>
/// Initializes a new instance of the <see cref="EnumSerializer{TEnum}"/> class.
/// </summary>
public CustomEnumSerializer()
: this((BsonType)0) // 0 means use underlying type
{
}
/// <summary>
/// Initializes a new instance of the <see cref="EnumSerializer{TEnum}"/> class.
/// </summary>
/// <param name="representation">The representation.</param>
public CustomEnumSerializer(BsonType representation)
{
switch (representation)
{
case 0:
case BsonType.Int32:
case BsonType.Int64:
case BsonType.String:
break;
default:
var message = string.Format("{0} is not a valid representation for an EnumSerializer.", representation);
throw new ArgumentException(message);
}
// don't know of a way to enforce this at compile time
var enumTypeInfo = typeof(TEnum).GetTypeInfo();
if (!enumTypeInfo.IsEnum)
{
var message = string.Format("{0} is not an enum type.", typeof(TEnum).FullName);
throw new BsonSerializationException(message);
}
_representation = representation;
if (representation == BsonType.String)
{
var enumType = typeof(TEnum);
if (!_fromValueMap.ContainsKey(enumType))
{
Dictionary<string, object> fromMap = new Dictionary<string, object>(StringComparer.InvariantCultureIgnoreCase);
Dictionary<object, string> toMap = new Dictionary<object, string>();
FieldInfo[] fields = enumType.GetFields(BindingFlags.Static | BindingFlags.Public);
foreach (FieldInfo field in fields)
{
string name = field.Name;
object enumValue = Enum.Parse(enumType, name);
// use EnumMember attribute if exists
EnumMemberAttribute enumMemberAttrbiute = field.GetCustomAttribute<EnumMemberAttribute>();
if (enumMemberAttrbiute != null)
{
string enumMemberValue = enumMemberAttrbiute.Value;
fromMap[enumMemberValue] = enumValue;
toMap[enumValue] = enumMemberValue;
}
else
{
toMap[enumValue] = name;
}
fromMap[name] = enumValue;
}
_fromValueMap[enumType] = fromMap;
_toValueMap[enumType] = toMap;
}
}
}
// public properties
/// <summary>
/// Gets the representation.
/// </summary>
/// <value>
/// The representation.
/// </value>
public BsonType Representation
{
get { return _representation; }
}
// public methods
/// <summary>
/// Deserializes a value.
/// </summary>
/// <param name="context">The deserialization context.</param>
/// <param name="args">The deserialization args.</param>
/// <returns>A deserialized value.</returns>
public override TEnum Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var bsonReader = context.Reader;
var bsonType = bsonReader.GetCurrentBsonType();
switch (bsonType)
{
case BsonType.Int32: return (TEnum)Enum.ToObject(typeof(TEnum), bsonReader.ReadInt32());
case BsonType.Int64: return (TEnum)Enum.ToObject(typeof(TEnum), bsonReader.ReadInt64());
case BsonType.Double: return (TEnum)Enum.ToObject(typeof(TEnum), (long)bsonReader.ReadDouble());
case BsonType.String:
var fromValue = FromValue(typeof(TEnum), bsonReader.ReadString());
return (TEnum)Enum.Parse(typeof(TEnum), fromValue.ToString());
default:
throw CreateCannotDeserializeFromBsonTypeException(bsonType);
}
}
/// <summary>
/// Serializes a value.
/// </summary>
/// <param name="context">The serialization context.</param>
/// <param name="args">The serialization args.</param>
/// <param name="value">The object.</param>
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, TEnum value)
{
var bsonWriter = context.Writer;
switch (_representation)
{
case 0:
var underlyingTypeCode = Type.GetTypeCode(Enum.GetUnderlyingType(typeof(TEnum)));
if (underlyingTypeCode == TypeCode.Int64 || underlyingTypeCode == TypeCode.UInt64)
{
goto case BsonType.Int64;
}
else
{
goto case BsonType.Int32;
}
case BsonType.Int32:
bsonWriter.WriteInt32(Convert.ToInt32(value));
break;
case BsonType.Int64:
bsonWriter.WriteInt64(Convert.ToInt64(value));
break;
case BsonType.String:
var val = ToValue(typeof(TEnum), value);
bsonWriter.WriteString(val);
break;
default:
throw new BsonInternalException("Unexpected EnumRepresentation.");
}
}
private string ToValue(Type enumType, object obj)
{
Dictionary<object, string> map = _toValueMap[enumType];
return map[obj];
}
private object FromValue(Type enumType, string value)
{
Dictionary<string, object> map = _fromValueMap[enumType];
if (!map.ContainsKey(value))
return value;
return map[value];
}
/// <summary>
/// Returns a serializer that has been reconfigured with the specified representation.
/// </summary>
/// <param name="representation">The representation.</param>
/// <returns>The reconfigured serializer.</returns>
public CustomEnumSerializer<TEnum> WithRepresentation(BsonType representation)
{
if (representation == _representation)
{
return this;
}
else
{
return new CustomEnumSerializer<TEnum>(representation);
}
}
// explicit interface implementations
IBsonSerializer IRepresentationConfigurable.WithRepresentation(BsonType representation)
{
return WithRepresentation(representation);
}
}
I needed a custom deserializer that would return the default value when encountering an unexpected value in the data, rather than the default behavior of throwing a deserialization exception.
public class CustomEnumSerializer<TEnum>: MongoDB.Bson.Serialization.Serializers.EnumSerializer<TEnum>
where TEnum : struct, IComparable, IFormattable, IConvertible
{
public override TEnum Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var bsonReader = context.Reader;
var bsonType = bsonReader.GetCurrentBsonType();
var val = "";
switch (bsonType)
{
case BsonType.String:
val = bsonReader.ReadString() ?? "";
break;
case BsonType.Int32:
val = bsonReader.ReadInt32().ToString();
break;
case BsonType.Int64:
val = bsonReader.ReadInt64().ToString();
break;
case BsonType.Null:
return default(TEnum);
default:
return base.Deserialize(context, args);
}
if(Enum.TryParse(val, true, out TEnum result)){
return result;
}
return default(TEnum);
}
}
To implement it in your repository:
static MyRepository()
{
BsonClassMap.RegisterClassMap<MyDataType>(ms =>
{
ms.AutoMap();
ms.GetMemberMap(i => i.MyEnum)
.SetSerializer(new CustomEnumSerializer<MyEnumType>());
});
}
Related
I need translation for programmatically changed label text.
Therefor I built a List with translations. The list conpect looks like this:
translation["de"]["label1"] = "german text";
translation["en"]["label1"] = "english text";
Here is my actual code to build the list:
public List<KeyValuePair<string, ListDictionary>> translations = new List<KeyValuePair<string, ListDictionary>>();
ListDictionary tDE = new ListDictionary();
ListDictionary tEN = new ListDictionary();
tDE.Add("label1", "german text");
tEN.Add("label1", "english text");
translations.Add(new KeyValuePair<string, ListDictionary>("de", tDE));
translations.Add(new KeyValuePair<string, ListDictionary>("en", tEN));
How can I now get a value of a translation?
My approach is like this:
public string getLocStr(string lang, string key)
{
string str = "";
foreach (var trans in translations)
{
// how to get a List<string,string> to retrieve the $value from by $key?
// eg: str = trans[$lang][$key]
}
return str;
}
use this sample and using linq to do it :
class Program
{
public static List<Dictionary<string, Dictionary<string, string>>> translations = new List<Dictionary<string, Dictionary<string, string>>>();
public static Dictionary<string, Dictionary<string, string>> dic = new Dictionary<string, Dictionary<string, string>>();
static void Main(string[] args)
{
Dictionary<string,string> tDE = new Dictionary<string, string>();
Dictionary<string, string> tEN = new Dictionary<string, string>();
tEN.Add("label1", "english text");
tDE.Add("label1", "german text");
dic.Add("en", tEN);
dic.Add("de", tDE);
translations.Add(dic);
Console.WriteLine(getLocStr("de", "label1"));
Console.Read();
}
public static string getLocStr(string lang, string key)
{
string str = "";
foreach (var trans in translations)
{
var langDic = trans.FirstOrDefault(c => c.Key == lang);
if(langDic.Value!=null)
{
str = langDic.Value.FirstOrDefault(c => c.Key == key).Value;
}
// how to get a List<string,string> to retrieve the $value from by $key?
// eg: str = trans[$lang][$key]
}
return str;
}
}
Having such an enum:
public enum Language
{
None,
EN,
FR
}
Using:
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.Serialization;
using System.Text;
using EnumsNET;
And having that dictionary:
[Serializable]
class TranslationsDictionary : NullSafeOfStringDictionary<Language>
{
public TranslationsDictionary() : base()
{
}
protected TranslationsDictionary(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
From this one:
[Serializable]
public class NullSafeOfStringDictionary<T> : Dictionary<T, string>
{
public NullSafeOfStringDictionary()
{
}
public NullSafeOfStringDictionary(int capacity) : base(capacity)
{
}
public NullSafeOfStringDictionary(IEqualityComparer<T> comparer) : base(comparer)
{
}
public NullSafeOfStringDictionary(IDictionary<T, string> dictionary) : base(dictionary)
{
}
public NullSafeOfStringDictionary(int capacity, IEqualityComparer<T> comparer) : base(capacity, comparer)
{
}
public NullSafeOfStringDictionary(IDictionary<T, string> dictionary, IEqualityComparer<T> comparer) : base(dictionary, comparer)
{
}
protected NullSafeOfStringDictionary(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
public new string this[T key]
{
get
{
return ContainsKey(key) ? base[key] : null;
}
set
{
if ( ContainsKey(key) )
base[key] = value;
else
Add(key, value);
}
}
}
You can use this class:
static class Localizer
{
private const string ERR = "<Not translated>";
/// <summary>
/// Get the string translation.
/// </summary>
/// <param name="values">The dictionary containing lang>translation.</param>
static public string GetLang(this TranslationsDictionary values)
{
return values?[Languages.Current] ?? values?[Languages.Default] ?? ERR;
}
/// <summary>
/// Get the string translation.
/// </summary>
/// <param name="values">The dictionary containing lang>translation.</param>
/// <param name="parameters">Parameters for the translated string.</param>
static public string GetLang(this TranslationsDictionary values, params object[] parameters)
{
return string.Format(values?.GetLang(), parameters) ?? ERR + " " + string.Join(",", parameters);
}
/// <summary>
/// Get the string translation.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="values">The dictionary containing value>lang>translation.</param>
/// <param name="value">The value to translate.</param>
/// <param name="forceEnglish">True to force get in english.</param>
static public string GetLang<T>(this NullSafeDictionary<T, TranslationsDictionary> values, T value, bool forceEnglish = false)
{
var lang = forceEnglish ? Language.EN : Languages.Current;
return values?[value]?[lang] ?? values?[value]?[Languages.Default] ?? ERR;
}
/// <summary>
/// Get the list translation.
/// </summary>
/// <param name="values">The dictionary containing lang>list.</param>
static public NullSafeStringList GetLang(this NullSafeDictionary<Language, NullSafeStringList> values)
{
return values?[Languages.Current] ?? values?[Languages.Default] ?? new NullSafeStringList();
}
/// <summary>
/// Get the list translation.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="values">The dictionary containing lang>list.</param>
static public NullSafeList<T> GetLang<T>(this NullSafeDictionary<Language, NullSafeList<T>> values)
where T : class
{
return values?[Languages.Current] ?? values?[Languages.Default] ?? new NullSafeList<T>();
}
/// <summary>
/// Get the string list translation.
/// </summary>
/// <param name="values">The dictionary containing lang>translations.</param>
static public string[] GetLang(this NullSafeDictionary<Language, string[]> values)
{
return values?[Languages.Current] ?? values?[Languages.Default] ?? new string[1] { ERR };
}
/// <summary>
/// Get the string translation.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="values">The dictionary containing lang>value>translation.</param>
/// <param name="value">The value to translate.</param>
static public string GetLang<T>(this NullSafeDictionary<Language, NullSafeOfStringDictionary<T>> values, T value)
where T : Enum
{
return values?[Languages.Current]?[value] ?? values?[Languages.Default]?[value] ?? ERR;
}
/// <summary>
/// Remove diacritics signs.
/// </summary>
public static string RemoveDiacritics(this string str)
{
if ( str.IsNullOrEmpty() ) return str;
var normalized = str.Normalize(NormalizationForm.FormD);
var builder = new StringBuilder();
foreach ( var c in normalized )
if ( CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark )
builder.Append(c);
return builder.ToString().Normalize(NormalizationForm.FormC);
}
}
Having also:
[Serializable]
public class NullSafeList<T> : List<T>
where T : class
{
public NullSafeList()
{
}
public NullSafeList(int capacity) : base(capacity)
{
}
public NullSafeList(IEnumerable<T> collection) : base(collection)
{
}
public new T this[int index]
{
get
{
CheckIndex(index);
return index < Count ? base[index] : null;
}
set
{
CheckIndex(index);
if ( index < Count )
base[index] = value;
else
CreateOutOfRange(index, value);
}
}
private void CheckIndex(int index)
{
if ( index >= 0 ) return;
throw new IndexOutOfRangeException(SysTranslations.IndexCantBeNegative.GetLang(nameof(NullSafeStringList), index));
}
private void CreateOutOfRange(int index, T value)
{
Capacity = index + 1;
int count = index + 1 - Count;
for ( int i = 0; i < count; i++ )
Add(null);
base[index] = value;
}
}
And the following class:
static class Languages
{
/// <summary>
/// Indicate language codes.
/// </summary>
static public readonly NullSafeOfEnumDictionary<string, Language> Values;
/// <summary>
/// Indicate language codes.
/// </summary>
static public readonly NullSafeOfStringDictionary<Language> Codes;
/// <summary>
/// Indicate managed languages.
/// </summary>
static public readonly Language[] Managed;
/// <summary>
/// Indicate default language.
/// </summary>
static public readonly Language Default = Language.EN;
/// <summary>
/// Indicate current language code.
/// </summary>
static public string CurrentCode => Codes[Current];
/// <summary>
/// Indicate current language.
/// </summary>
static public Language Current
{
get
{
string lang = CultureInfo.CurrentCulture.TwoLetterISOLanguageName;
var result = Values[lang];
if ( !Managed.Contains(result) ) result = Default;
return result;
}
}
/// <summary>
/// Static constructor.
/// </summary>
static Languages()
{
try
{
Managed = Enums.GetValues<Language>().Skip(1).ToArray();
Codes = new NullSafeOfStringDictionary<Language>(Managed.ToDictionary(v => v, v => v.ToString().ToLower()));
Values = new NullSafeOfEnumDictionary<string, Language>(Codes.ToDictionary(v => v.Value, v => v.Key));
}
catch ( Exception ex )
{
MessageBox.Show(ex.message);
}
}
}
You can simplify and remove everything not needed.
[Serializable]
public class NullSafeOfEnumDictionary<TKey, TValue> : Dictionary<TKey, TValue>
where TValue : Enum
{
public NullSafeOfEnumDictionary()
{
}
public NullSafeOfEnumDictionary(int capacity) : base(capacity)
{
}
public NullSafeOfEnumDictionary(IEqualityComparer<TKey> comparer) : base(comparer)
{
}
public NullSafeOfEnumDictionary(IDictionary<TKey, TValue> dictionary) : base(dictionary)
{
}
public NullSafeOfEnumDictionary(int capacity, IEqualityComparer<TKey> comparer) : base(capacity, comparer)
{
}
public NullSafeOfEnumDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer) : base(dictionary, comparer)
{
}
protected NullSafeOfEnumDictionary(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
public new TValue this[TKey key]
{
get
{
return ContainsKey(key) ? base[key] : default;
}
set
{
if ( ContainsKey(key) )
base[key] = value;
else
Add(key, value);
}
}
}
Usage:
static class SysTranslations
{
static public readonly TranslationsDictionary NotImplemented
= new TranslationsDictionary
{
[Language.EN] = "Not implemented: {0}",
[Language.FR] = "Non implémenté : {0}",
};
}
string msg = SysTranslations.NotImplemented.GetLang("...");
Load/Write
static class NullSafeOfStringDictionaryHelper
{
static public bool LoadKeyValuePairs(this NullSafeOfStringDictionary<string> list,
string filePath,
string separator,
bool showError = true)
{
try
{
list.Clear();
foreach ( string line in File.ReadAllLines(filePath) )
if ( !line.StartsWith(";") && !line.StartsWith("//") )
{
var parts = line.SplitNoEmptyLines(separator);
if ( parts.Length == 1 )
list.Add(parts[0].Trim(), string.Empty);
else
if ( parts.Length == 2 )
list.Add(parts[0].Trim(), parts[1].Trim());
else
if ( parts.Length > 2 )
list.Add(parts[0].Trim(), string.Join(separator, parts.Skip(1)));
}
return true;
}
catch ( Exception ex )
{
if ( showError )
MessageBox.Show(SysTranslations.LoadFileError.GetLang(filePath, ex.Message),
Globals.AssemblyTitle,
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
return false;
}
}
static public bool SaveKeyValuePairs(this NullSafeOfStringDictionary<string> list,
string filePath,
string separator,
bool showError = true)
{
using ( var stream = File.CreateText(filePath) )
try
{
foreach ( var item in list )
if ( !item.Key.StartsWith(";") && !item.Key.StartsWith("//") )
stream.WriteLine(item.Key + separator + item.Value);
else
stream.WriteLine(item.Key);
stream.Close();
return true;
}
catch ( Exception ex )
{
if ( showError )
MessageBox.Show(SysTranslations.LoadFileError.GetLang(filePath, ex.Message),
Globals.AssemblyTitle,
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
return false;
}
}
}
This last is is even more of a rough draft than the above.
Working with VS 2017 on console application. When I use Dictionary<string, Guid> all works fine, however, when I use Dictionary<Guid, Guid> it doesn't map to AppSettings object (dict count is 0). Is there any way around it, rather then using string and converting it to Guid in the application?
appsetting.json
{
"AppSettings": {
"DictionaryTest": {
"55789653-86A3-485C-95E8-5E23C3219130": "74F87895-4965-447A-B07F-F702573218B7",
"FB1E7891-B3C2-4E83-A6BF-7F321C11BFFA": "FA7892A9-7925-4150-82A7-5DD3213D1242"
}
}
}
in Program.cs
var appPath = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().CodeBase).Replace(#"file:\", "");
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile($#"{appPath}\appsettings.json");
Configuration = builder.Build();
appSettings = new AppSettings();
Configuration.GetSection("AppSettings").Bind(appSettings);
When I use Dictionary<string, Guid> scenario there are 2 items in appSettings.DictionaryTest variable.
But when I use Dictionary<Guid, Guid> scenario there are 0 items in appSettings.DictionaryTest variable.
Dictionary<string, Guid> scenario:
AppSettings.cs
public class AppSettings
{
public Dictionary<string, Guid> DictionaryTest { get; set; }
}
Dictionary<Guid, Guid> scenario:
AppSettings.cs
public class AppSettings
{
public Dictionary<Guid, Guid> DictionaryTest { get; set; }
}
At the moment, it appears that the Configuration Binder doesn't support Guid as a key type. The only supported types are string and Enum types.
This is actually stated in the source code:
if (keyType != typeof(string) && !keyTypeIsEnum)
{
// We only support string and enum keys
return;
}
But I don't think it is documented anywhere else.
While Kevin Eaton is correct and the official package doesn't support this, nothing stops you from shamelessly copying that file with the Extension methods and with just a couple of new lines of code, implement this yourself
The below works
//Install Nuget Package Microsoft.Extensions.Configuration.Binder
//Install Nuget Package Microsoft.Extensions.Configuration.Json
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
namespace ConsoleApp4
{
class Program
{
static void Main(string[] args)
{
var builder = new ConfigurationBuilder().AddJsonFile($"appsettings.json", true, true);
var configuration = builder.Build();
var appSettings = new AppSettings();
var configurationSection = configuration.GetSection("AppSettings");
configurationSection.BindWhichSupportsGuidAsKey(appSettings);
Console.WriteLine("Finished!");
}
}
public class AppSettings
{
public Dictionary<Guid, Guid> DictionaryTest { get; set; }
}
}
All I did was
Renamed the class to CustomConfigurationBinderExtesions so that the methods don't clash
Renamed the Bind method to BindWhichSupportsGuidAsKey
Change if (keyType != typeof(string) && !keyTypeIsEnum) to
if (keyType != typeof(string) && keyType != typeof(Guid) && !keyTypeIsEnum)
Added this
else if (keyType == typeof(Guid))
{
Guid key = Guid.Parse(child.Key);
setter.SetValue(dictionary, item, new object[] { key });
}
Below is the full file(The original is here)
(You can remove most of the code here and keep only what you need but I'm too lazy to do that :))
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
namespace Microsoft.Extensions.Configuration
{
/// <summary>
/// Static helper class that allows binding strongly typed objects to configuration values.
/// </summary>
public static class CustomConfigurationBinderExtesions
{
private const BindingFlags DeclaredOnlyLookup = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
/// <summary>
/// Attempts to bind the configuration instance to a new instance of type T.
/// If this configuration section has a value, that will be used.
/// Otherwise binding by matching property names against configuration keys recursively.
/// </summary>
/// <typeparam name="T">The type of the new instance to bind.</typeparam>
/// <param name="configuration">The configuration instance to bind.</param>
/// <returns>The new instance of T if successful, default(T) otherwise.</returns>
//public static T Get<T>(this IConfiguration configuration)
// => configuration.Get<T>(_ => { });
/// <summary>
/// Attempts to bind the configuration instance to a new instance of type T.
/// If this configuration section has a value, that will be used.
/// Otherwise binding by matching property names against configuration keys recursively.
/// </summary>
/// <typeparam name="T">The type of the new instance to bind.</typeparam>
/// <param name="configuration">The configuration instance to bind.</param>
/// <param name="configureOptions">Configures the binder options.</param>
/// <returns>The new instance of T if successful, default(T) otherwise.</returns>
//public static T Get<T>(this IConfiguration configuration, Action<BinderOptions> configureOptions)
//{
// if (configuration == null)
// {
// throw new ArgumentNullException(nameof(configuration));
// }
// object result = configuration.Get(typeof(T), configureOptions);
// if (result == null)
// {
// return default(T);
// }
// return (T)result;
//}
/// <summary>
/// Attempts to bind the configuration instance to a new instance of type T.
/// If this configuration section has a value, that will be used.
/// Otherwise binding by matching property names against configuration keys recursively.
/// </summary>
/// <param name="configuration">The configuration instance to bind.</param>
/// <param name="type">The type of the new instance to bind.</param>
/// <returns>The new instance if successful, null otherwise.</returns>
//public static object Get(this IConfiguration configuration, Type type)
// => configuration.Get(type, _ => { });
/// <summary>
/// Attempts to bind the configuration instance to a new instance of type T.
/// If this configuration section has a value, that will be used.
/// Otherwise binding by matching property names against configuration keys recursively.
/// </summary>
/// <param name="configuration">The configuration instance to bind.</param>
/// <param name="type">The type of the new instance to bind.</param>
/// <param name="configureOptions">Configures the binder options.</param>
/// <returns>The new instance if successful, null otherwise.</returns>
public static object Get(this IConfiguration configuration, Type type, Action<BinderOptions> configureOptions)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
var options = new BinderOptions();
configureOptions?.Invoke(options);
return BindInstance(type, instance: null, config: configuration, options: options);
}
/// <summary>
/// Attempts to bind the given object instance to the configuration section specified by the key by matching property names against configuration keys recursively.
/// </summary>
/// <param name="configuration">The configuration instance to bind.</param>
/// <param name="key">The key of the configuration section to bind.</param>
/// <param name="instance">The object to bind.</param>
public static void BindWhichSupportsGuidAsKey(this IConfiguration configuration, string key, object instance)
=> configuration.GetSection(key).BindWhichSupportsGuidAsKey(instance);
/// <summary>
/// Attempts to bind the given object instance to configuration values by matching property names against configuration keys recursively.
/// </summary>
/// <param name="configuration">The configuration instance to bind.</param>
/// <param name="instance">The object to bind.</param>
public static void BindWhichSupportsGuidAsKey(this IConfiguration configuration, object instance)
=> configuration.BindWhichSupportsGuidAsKey(instance, o => { });
/// <summary>
/// Attempts to bind the given object instance to configuration values by matching property names against configuration keys recursively.
/// </summary>
/// <param name="configuration">The configuration instance to bind.</param>
/// <param name="instance">The object to bind.</param>
/// <param name="configureOptions">Configures the binder options.</param>
public static void BindWhichSupportsGuidAsKey(this IConfiguration configuration, object instance, Action<BinderOptions> configureOptions)
{
if (configuration == null)
{
throw new ArgumentNullException(nameof(configuration));
}
if (instance != null)
{
var options = new BinderOptions();
configureOptions?.Invoke(options);
BindInstance(instance.GetType(), instance, configuration, options);
}
}
/// <summary>
/// Extracts the value with the specified key and converts it to type T.
/// </summary>
/// <typeparam name="T">The type to convert the value to.</typeparam>
/// <param name="configuration">The configuration.</param>
/// <param name="key">The key of the configuration section's value to convert.</param>
/// <returns>The converted value.</returns>
public static T GetValue<T>(this IConfiguration configuration, string key)
{
return GetValue(configuration, key, default(T));
}
/// <summary>
/// Extracts the value with the specified key and converts it to type T.
/// </summary>
/// <typeparam name="T">The type to convert the value to.</typeparam>
/// <param name="configuration">The configuration.</param>
/// <param name="key">The key of the configuration section's value to convert.</param>
/// <param name="defaultValue">The default value to use if no value is found.</param>
/// <returns>The converted value.</returns>
public static T GetValue<T>(this IConfiguration configuration, string key, T defaultValue)
{
return (T)GetValue(configuration, typeof(T), key, defaultValue);
}
/// <summary>
/// Extracts the value with the specified key and converts it to the specified type.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="type">The type to convert the value to.</param>
/// <param name="key">The key of the configuration section's value to convert.</param>
/// <returns>The converted value.</returns>
public static object GetValue(this IConfiguration configuration, Type type, string key)
{
return GetValue(configuration, type, key, defaultValue: null);
}
/// <summary>
/// Extracts the value with the specified key and converts it to the specified type.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="type">The type to convert the value to.</param>
/// <param name="key">The key of the configuration section's value to convert.</param>
/// <param name="defaultValue">The default value to use if no value is found.</param>
/// <returns>The converted value.</returns>
public static object GetValue(this IConfiguration configuration, Type type, string key, object defaultValue)
{
IConfigurationSection section = configuration.GetSection(key);
string value = section.Value;
if (value != null)
{
return ConvertValue(type, value, section.Path);
}
return defaultValue;
}
private static void BindNonScalar(this IConfiguration configuration, object instance, BinderOptions options)
{
if (instance != null)
{
foreach (PropertyInfo property in GetAllProperties(instance.GetType()))
{
BindProperty(property, instance, configuration, options);
}
}
}
private static void BindProperty(PropertyInfo property, object instance, IConfiguration config, BinderOptions options)
{
// We don't support set only, non public, or indexer properties
if (property.GetMethod == null ||
(!options.BindNonPublicProperties && !property.GetMethod.IsPublic) ||
property.GetMethod.GetParameters().Length > 0)
{
return;
}
object propertyValue = property.GetValue(instance);
bool hasSetter = property.SetMethod != null && (property.SetMethod.IsPublic || options.BindNonPublicProperties);
if (propertyValue == null && !hasSetter)
{
// Property doesn't have a value and we cannot set it so there is no
// point in going further down the graph
return;
}
propertyValue = BindInstance(property.PropertyType, propertyValue, config.GetSection(property.Name), options);
if (propertyValue != null && hasSetter)
{
property.SetValue(instance, propertyValue);
}
}
private static object BindToCollection(Type type, IConfiguration config, BinderOptions options)
{
Type genericType = typeof(List<>).MakeGenericType(type.GenericTypeArguments[0]);
object instance = Activator.CreateInstance(genericType);
BindCollection(instance, genericType, config, options);
return instance;
}
// Try to create an array/dictionary instance to back various collection interfaces
private static object AttemptBindToCollectionInterfaces(Type type, IConfiguration config, BinderOptions options)
{
if (!type.IsInterface)
{
return null;
}
Type collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyList<>), type);
if (collectionInterface != null)
{
// IEnumerable<T> is guaranteed to have exactly one parameter
return BindToCollection(type, config, options);
}
collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyDictionary<,>), type);
if (collectionInterface != null)
{
Type dictionaryType = typeof(Dictionary<,>).MakeGenericType(type.GenericTypeArguments[0], type.GenericTypeArguments[1]);
object instance = Activator.CreateInstance(dictionaryType);
BindDictionary(instance, dictionaryType, config, options);
return instance;
}
collectionInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type);
if (collectionInterface != null)
{
object instance = Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(type.GenericTypeArguments[0], type.GenericTypeArguments[1]));
BindDictionary(instance, collectionInterface, config, options);
return instance;
}
collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyCollection<>), type);
if (collectionInterface != null)
{
// IReadOnlyCollection<T> is guaranteed to have exactly one parameter
return BindToCollection(type, config, options);
}
collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type);
if (collectionInterface != null)
{
// ICollection<T> is guaranteed to have exactly one parameter
return BindToCollection(type, config, options);
}
collectionInterface = FindOpenGenericInterface(typeof(IEnumerable<>), type);
if (collectionInterface != null)
{
// IEnumerable<T> is guaranteed to have exactly one parameter
return BindToCollection(type, config, options);
}
return null;
}
private static object BindInstance(Type type, object instance, IConfiguration config, BinderOptions options)
{
// if binding IConfigurationSection, break early
if (type == typeof(IConfigurationSection))
{
return config;
}
var section = config as IConfigurationSection;
string configValue = section?.Value;
object convertedValue;
Exception error;
if (configValue != null && TryConvertValue(type, configValue, section.Path, out convertedValue, out error))
{
if (error != null)
{
throw error;
}
// Leaf nodes are always reinitialized
return convertedValue;
}
if (config != null && config.GetChildren().Any())
{
// If we don't have an instance, try to create one
if (instance == null)
{
// We are already done if binding to a new collection instance worked
instance = AttemptBindToCollectionInterfaces(type, config, options);
if (instance != null)
{
return instance;
}
instance = CreateInstance(type);
}
// See if its a Dictionary
Type collectionInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type);
if (collectionInterface != null)
{
BindDictionary(instance, collectionInterface, config, options);
}
else if (type.IsArray)
{
instance = BindArray((Array)instance, config, options);
}
else
{
// See if its an ICollection
collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type);
if (collectionInterface != null)
{
BindCollection(instance, collectionInterface, config, options);
}
// Something else
else
{
BindNonScalar(config, instance, options);
}
}
}
return instance;
}
private static object CreateInstance(Type type)
{
if (type.IsInterface || type.IsAbstract)
{
throw new InvalidOperationException("SR.Format(SR.Error_CannotActivateAbstractOrInterface, type)");
}
if (type.IsArray)
{
if (type.GetArrayRank() > 1)
{
throw new InvalidOperationException("SR.Format(SR.Error_UnsupportedMultidimensionalArray, type)");
}
return Array.CreateInstance(type.GetElementType(), 0);
}
if (!type.IsValueType)
{
bool hasDefaultConstructor = type.GetConstructors(DeclaredOnlyLookup).Any(ctor => ctor.IsPublic && ctor.GetParameters().Length == 0);
if (!hasDefaultConstructor)
{
throw new InvalidOperationException("SR.Format(SR.Error_MissingParameterlessConstructor, type)");
}
}
try
{
return Activator.CreateInstance(type);
}
catch (Exception ex)
{
throw new InvalidOperationException("SR.Format(SR.Error_FailedToActivate, type), ex");
}
}
private static void BindDictionary(object dictionary, Type dictionaryType, IConfiguration config, BinderOptions options)
{
// IDictionary<K,V> is guaranteed to have exactly two parameters
Type keyType = dictionaryType.GenericTypeArguments[0];
Type valueType = dictionaryType.GenericTypeArguments[1];
bool keyTypeIsEnum = keyType.IsEnum;
if (keyType != typeof(string) && keyType != typeof(Guid) && !keyTypeIsEnum)
{
// We only support string and enum keys
return;
}
PropertyInfo setter = dictionaryType.GetProperty("Item", DeclaredOnlyLookup);
foreach (IConfigurationSection child in config.GetChildren())
{
object item = BindInstance(
type: valueType,
instance: null,
config: child,
options: options);
if (item != null)
{
if (keyType == typeof(string))
{
string key = child.Key;
setter.SetValue(dictionary, item, new object[] { key });
}
else if (keyType == typeof(Guid))
{
Guid key = Guid.Parse(child.Key);
setter.SetValue(dictionary, item, new object[] { key });
}
else if (keyTypeIsEnum)
{
object key = Enum.Parse(keyType, child.Key);
setter.SetValue(dictionary, item, new object[] { key });
}
}
}
}
private static void BindCollection(object collection, Type collectionType, IConfiguration config, BinderOptions options)
{
// ICollection<T> is guaranteed to have exactly one parameter
Type itemType = collectionType.GenericTypeArguments[0];
MethodInfo addMethod = collectionType.GetMethod("Add", DeclaredOnlyLookup);
foreach (IConfigurationSection section in config.GetChildren())
{
try
{
object item = BindInstance(
type: itemType,
instance: null,
config: section,
options: options);
if (item != null)
{
addMethod.Invoke(collection, new[] { item });
}
}
catch
{
}
}
}
private static Array BindArray(Array source, IConfiguration config, BinderOptions options)
{
IConfigurationSection[] children = config.GetChildren().ToArray();
int arrayLength = source.Length;
Type elementType = source.GetType().GetElementType();
var newArray = Array.CreateInstance(elementType, arrayLength + children.Length);
// binding to array has to preserve already initialized arrays with values
if (arrayLength > 0)
{
Array.Copy(source, newArray, arrayLength);
}
for (int i = 0; i < children.Length; i++)
{
try
{
object item = BindInstance(
type: elementType,
instance: null,
config: children[i],
options: options);
if (item != null)
{
newArray.SetValue(item, arrayLength + i);
}
}
catch
{
}
}
return newArray;
}
private static bool TryConvertValue(Type type, string value, string path, out object result, out Exception error)
{
error = null;
result = null;
if (type == typeof(object))
{
result = value;
return true;
}
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
if (string.IsNullOrEmpty(value))
{
return true;
}
return TryConvertValue(Nullable.GetUnderlyingType(type), value, path, out result, out error);
}
TypeConverter converter = TypeDescriptor.GetConverter(type);
if (converter.CanConvertFrom(typeof(string)))
{
try
{
result = converter.ConvertFromInvariantString(value);
}
catch (Exception ex)
{
error = new InvalidOperationException("SR.Format(SR.Error_FailedBinding, path, type), ex");
}
return true;
}
if (type == typeof(byte[]))
{
try
{
result = Convert.FromBase64String(value);
}
catch (FormatException ex)
{
error = new InvalidOperationException("SR.Format(SR.Error_FailedBinding, path, type), ex");
}
return true;
}
return false;
}
private static object ConvertValue(Type type, string value, string path)
{
object result;
Exception error;
TryConvertValue(type, value, path, out result, out error);
if (error != null)
{
throw error;
}
return result;
}
private static Type FindOpenGenericInterface(Type expected, Type actual)
{
if (actual.IsGenericType &&
actual.GetGenericTypeDefinition() == expected)
{
return actual;
}
Type[] interfaces = actual.GetInterfaces();
foreach (Type interfaceType in interfaces)
{
if (interfaceType.IsGenericType &&
interfaceType.GetGenericTypeDefinition() == expected)
{
return interfaceType;
}
}
return null;
}
private static IEnumerable<PropertyInfo> GetAllProperties(Type type)
{
var allProperties = new List<PropertyInfo>();
do
{
allProperties.AddRange(type.GetProperties(DeclaredOnlyLookup));
type = type.BaseType;
}
while (type != typeof(object));
return allProperties;
}
}
}
Try getting DictionaryTest as JSON and converting it to a Dictionary<Guid, Guid> manually with a JSON library. I wouldn't doubt if the conversion from appSettings isn't designed to handle that type of cast out of the box. Newtonsoft I know for sure can convert from JSON to a dictionary with a Guid key though. Right now I'm thinking something like this (using newtonsoft):
public class AppSettings
{
public JObject DictionaryTest { get; set; }
public Dictionary<Guid, Guid> DictionaryTestConvert
{
get { return JObject.toObject<Dictionary<Guid, Guid>>(DictionaryTestJson); }
}
}
I have a working PATCH for my user class with Delta in Web API 2. By using the .patch method I can easily detect only the changes that were sent over and then update accordingly, rather than have to receive the entire user!
The problem is there are several fields that I want to protect so they are never updated.
I saw one example on SO but it didn't leverage Delta rather seemed to be slightly more dated and practically wrote all of the patch code by hand. Is there not a way to easily tell OData's patch to skip over properties you designate (maybe I need to override patch and tell it to avoid some properties)?
How would I even begin to go about doing this (or what should I search for / research to get started)? Do action filters / validation have a role here? Do I look into model binding? Is it overriding patch?
Thanks!
Depending on what you want to do if someone tries to update protected fields you can either:
Update only fields that can be modified. For this you can construct new Delta with only these fields like this:
Delta<User> filteredDelta = new Delta<User>();
if (originalDelta.GetChangedPropertyNames().Contains("FirstName"))
{
filteredDelta.TrySetPropertyValue("FirstName", originalDelta.GetEntity().FirstName);
}
if (originalDelta.GetChangedPropertyNames().Contains("LastName"))
{
filteredDelta.TrySetPropertyValue("LastName", originalDelta.GetEntity().LastName);
}
filteredDelta.Patch(selectedUser);
Fail the PATCH request (I would say this is preferred and least surprising way to deal with such requests). This would be even simpler:
if (originalDelta.GetChangedPropertyNames().Contains("ModifiedDate"))
{
return InternalServerError(new ArgumentException("Attribue is read-only", "ModifiedDate"));
}
There's a couple of possibilities, depending on you use case...
You want to exclude the changes if they are supplied
You want to throw an error if non-editable fields are updated.
Start with an attribute to mark appropriate properties
/// <summary>
/// Marks a property as non-editable.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class NonEditableAttribute : Attribute
{
}
Then we can write some extensions against Delta to take advantage of this
public static class PatchExtensions
{
/// <summary>
/// Get the properties of a type that are non-editable.
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
public static IList<string> NonEditableProperties(this Type type)
{
return type.GetProperties().Where(x => Attribute.IsDefined(x, typeof(NonEditableAttribute))).Select(prop => prop.Name).ToList();
}
/// <summary>
/// Get this list of non-editable changes in a <see cref="Delta{T}"/>.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="delta"></param>
/// <returns></returns>
public static IList<string> NonEditableChanges<T>(this Delta<T> delta)
where T : class
{
var nec = new List<string>();
var excluded = typeof(T).NonEditableProperties();
nec.AddRange(delta.GetChangedPropertyNames().Where(x => excluded.Contains(x)));
return nec;
}
/// <summary>
/// Exclude changes from a <see cref="Delta{T}"/> based on a list of property names
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="delta"></param>
/// <param name="excluded"></param>
/// <returns></returns>
public static Delta<T> Exclude<T>(this Delta<T> delta, IList<string> excluded)
where T : class
{
var changed = new Delta<T>();
foreach (var prop in delta.GetChangedPropertyNames().Where(x => !excluded.Contains(x)))
{
object value;
if (delta.TryGetPropertyValue(prop, out value))
{
changed.TrySetPropertyValue(prop, value);
}
}
return changed;
}
/// <summary>
/// Exclude changes from a <see cref="Delta{T}"/> where the properties are marked with <see cref="NonEditableAttribute"/>
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="delta"></param>
/// <returns></returns>
public static Delta<T> ExcludeNonEditable<T>(this Delta<T> delta)
where T : class
{
var excluded = typeof(T).NonEditableProperties();
return delta.Exclude(excluded);
}
}
And a domain class
public class Customer
{
public int Id { get; set; }
public string Name { get; set; }
[NonEditable]
public string SecurityId { get; set; }
}
Finally your controller can then take advantage of this in the Patch method
public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Customer> delta)
{
var patch = delta.ExcludeNonEditable();
// TODO: Your patching action here
}
or
public async Task<IHttpActionResult> Patch([FromODataUri] int key, Delta<Customer> delta)
{
var nonEditable = delta.NonEditableChanges();
if (nonEditable.Count > 0)
{
throw new HttpException(409, "Cannot update as non-editable fields included");
}
// TODO: Your patching action here
}
I had the same need and I ended up writing an extension method to Delta that accepts additional parameters to limit which fields to update (similar to TryUpDateModel)
I know there must be a better way to do this, but for now this works.
I had to recreate some of the Delta private methods and classes. Most of the code is from https://github.com/mono/aspnetwebstack/blob/master/src/System.Web.Http.OData/OData/Delta.cs, https://github.com/ASP-NET-MVC/aspnetwebstack/blob/master/OData/src/System.Web.Http.OData/OData/PropertyAccessor.cs and https://github.com/mono/aspnetwebstack/blob/master/src/System.Web.Http.OData/OData/CompiledPropertyAccessor.cs (or similar, these are not the exact url's I copied from)
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Web;
using System.Reflection;
using System.Linq.Expressions;
namespace MyProject.ODataExtensions
{
public static class ODataExtensions
{
public static void Patch<TEntityType>(this System.Web.OData.Delta<TEntityType> d, TEntityType original, String[] includedProperties, String[] excludedProperties) where TEntityType : class
{
Dictionary<string, PropertyAccessor<TEntityType>> _propertiesThatExist = InitializePropertiesThatExist<TEntityType>();
var changedProperties = d.GetChangedPropertyNames();
if (includedProperties != null) changedProperties = changedProperties.Intersect(includedProperties);
if (excludedProperties != null) changedProperties = changedProperties.Except(excludedProperties);
PropertyAccessor<TEntityType>[] array = (
from s in changedProperties
select _propertiesThatExist[s]).ToArray();
var array2 = array;
for (int i = 0; i < array2.Length; i++)
{
PropertyAccessor<TEntityType> propertyAccessor = array2[i];
propertyAccessor.Copy(d.GetEntity(), original);
}
}
private static Dictionary<string, PropertyAccessor<T>> InitializePropertiesThatExist<T>() where T : class
{
Type backingType = typeof(T);
return backingType.GetProperties()
.Where(p => p.GetSetMethod() != null && p.GetGetMethod() != null)
.Select<PropertyInfo, PropertyAccessor<T>>(p => new CompiledPropertyAccessor<T>(p))
.ToDictionary(p => p.Property.Name);
}
internal abstract class PropertyAccessor<TEntityType> where TEntityType : class
{
protected PropertyAccessor(PropertyInfo property)
{
if (property == null)
{
throw new System.ArgumentException("Property cannot be null","property");
}
Property = property;
if (Property.GetGetMethod() == null || Property.GetSetMethod() == null)
{
throw new System.ArgumentException("Property must have public setter and getter", "property");
}
}
public PropertyInfo Property
{
get;
private set;
}
public void Copy(TEntityType from, TEntityType to)
{
if (from == null)
{
throw new System.ArgumentException("Argument cannot be null", "from");
}
if (to == null)
{
throw new System.ArgumentException("Argument cannot be null", "to");
}
SetValue(to, GetValue(from));
}
public abstract object GetValue(TEntityType entity);
public abstract void SetValue(TEntityType entity, object value);
}
internal class CompiledPropertyAccessor<TEntityType> : PropertyAccessor<TEntityType> where TEntityType : class
{
private Action<TEntityType, object> _setter;
private Func<TEntityType, object> _getter;
public CompiledPropertyAccessor(PropertyInfo property)
: base(property)
{
_setter = MakeSetter(Property);
_getter = MakeGetter(Property);
}
public override object GetValue(TEntityType entity)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
return _getter(entity);
}
public override void SetValue(TEntityType entity, object value)
{
if (entity == null)
{
throw new ArgumentNullException("entity");
}
_setter(entity, value);
}
private static Action<TEntityType, object> MakeSetter(PropertyInfo property)
{
Type type = typeof(TEntityType);
ParameterExpression entityParameter = Expression.Parameter(type);
ParameterExpression objectParameter = Expression.Parameter(typeof(Object));
MemberExpression toProperty = Expression.Property(Expression.TypeAs(entityParameter, property.DeclaringType), property);
UnaryExpression fromValue = Expression.Convert(objectParameter, property.PropertyType);
BinaryExpression assignment = Expression.Assign(toProperty, fromValue);
Expression<Action<TEntityType, object>> lambda = Expression.Lambda<Action<TEntityType, object>>(assignment, entityParameter, objectParameter);
return lambda.Compile();
}
private static Func<TEntityType, object> MakeGetter(PropertyInfo property)
{
Type type = typeof(TEntityType);
ParameterExpression entityParameter = Expression.Parameter(type);
MemberExpression fromProperty = Expression.Property(Expression.TypeAs(entityParameter, property.DeclaringType), property);
UnaryExpression convert = Expression.Convert(fromProperty, typeof(Object));
Expression<Func<TEntityType, object>> lambda = Expression.Lambda<Func<TEntityType, object>>(convert, entityParameter);
return lambda.Compile();
}
}
}
}
I have the following app that shows that the key part of a Dictionary is not sent to JsonConverter, but it is called ToString() on. This is an issue for me as I can't deserialize my Json string .
Any ideas?
class Program
{
static void Main(string[] args)
{
var coll = new Dictionary<Tuple<string,string>, string>();
coll.Add(Tuple.Create("key1", "KEY1"), "Value1");
coll.Add(Tuple.Create("key2", "KEY2"), "Value2");
string json = JsonConvert.SerializeObject(coll);
Dictionary<Tuple<string, string>, string> coll2;
Console.WriteLine(json);
//coll2 = JsonConvert.DeserializeObject<Dictionary<Tuple<string, string>, string>>(json);
// It throws an exception here
//foreach (var k in coll2)
//{
// Console.WriteLine("<{0}|{1}>",k.Key, k.Value);
//}
var t = Tuple.Create("key1", "key2");
Console.WriteLine(t.ToString());
string json2 = JsonConvert.SerializeObject(t);
Console.WriteLine(json2);
}
}
Output :
{"(key1, KEY1)":"Value1","(key2, KEY2)":"Value2"} (key1, key2)
{"Item1":"key1","Item2":"key2"}
Press any key to continue . . .
I also had the same problem with Deserializing a Dictionary with Tuple as key. JSON converts the tuple into a mere string. But in my case, i cannot avoid using Tuple as key in the dictionary. So i made a custom JSON convertor to Deserialize the Dictionary with Tuple as key and it worked well.
I have modified the same as per your code. Hope it will work fine and can give you an idea about JSON CustomConverter. Also explained better with comments.
public class TupleKeyConverter : JsonConverter
{
/// <summary>
/// Override ReadJson to read the dictionary key and value
/// </summary>
/// <param name="reader"></param>
/// <param name="objectType"></param>
/// <param name="existingValue"></param>
/// <param name="serializer"></param>
/// <returns></returns>
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
Tuple<string, string> _tuple = null;
string _value = null;
var _dict = new Dictionary<Tuple<string, string>, string>();
//loop through the JSON string reader
while (reader.Read())
{
// check whether it is a property
if (reader.TokenType == JsonToken.PropertyName)
{
string readerValue = reader.Value.ToString();
if (reader.Read())
{
// check if the property is tuple (Dictionary key)
if (readerValue.Contains('(') && readerValue.Contains(')'))
{
string[] result = ConvertTuple(readerValue);
if (result == null)
continue;
// Custom Deserialize the Dictionary key (Tuple)
_tuple = Tuple.Create<string, string>(result[0].Trim(), result[1].Trim());
// Custom Deserialize the Dictionary value
_value = (string)serializer.Deserialize(reader, _value.GetType());
_dict.Add(_tuple, _value);
}
else
{
// Deserialize the remaining data from the reader
serializer.Deserialize(reader);
break;
}
}
}
}
return _dict;
}
/// <summary>
/// To convert Tuple
/// </summary>
/// <param name="_string"></param>
/// <returns></returns>
public string[] ConvertTuple(string _string)
{
string tempStr = null;
// remove the first character which is a brace '('
if (_string.Contains('('))
tempStr = _string.Remove(0, 1);
// remove the last character which is a brace ')'
if (_string.Contains(')'))
tempStr = tempStr.Remove(tempStr.Length - 1, 1);
// seperate the Item1 and Item2
if (_string.Contains(','))
return tempStr.Split(',');
return null;
}
/// <summary>
/// WriteJson needs to be implemented since it is an abstract function.
/// </summary>
/// <param name="writer"></param>
/// <param name="value"></param>
/// <param name="serializer"></param>
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
serializer.Serialize(writer, value);
}
/// <summary>
/// Check whether to convert or not
/// </summary>
/// <param name="objectType"></param>
/// <returns></returns>
public override bool CanConvert(Type objectType)
{
return true;
}
}
Now declare a property as follows. JsonConvertor Property is important.
[JsonConverter(typeof(TupleKeyConverter))]
public Dictionary<Tuple<int,string>,string> MyDict {get; set;}
Or you could try this to replace this in your code. though i never tested.
coll2 = JsonConvert.DeserializeObject<Dictionary<Tuple<string, string>, string>>("", new TupleKeyConverter());
Based on the information you have provided, I would suggest that instead of using a Tuple as your key, use a custom struct or object and override the ToString method. Then you can serialize/deserialize as you wish.
I have my enumHelper class that contains these:
public static IList<T> GetValues()
{
IList<T> list = new List<T>();
foreach (object value in Enum.GetValues(typeof(T)))
{
list.Add((T)value);
}
return list;
}
and
public static string Description(Enum value)
{
Attribute DescAttribute = LMIGHelper.GetAttribute(value, typeof(DescriptionAttribute));
if (DescAttribute == null)
return value.ToString();
else
return ((DescriptionAttribute)DescAttribute).Description;
}
my enum is something like:
public enum OutputType
{
File,
[Description("Data Table")]
DataTable
}
So far so good. All the previous work fine.
Now I want to add a new helper to return BindingList>, so I can link any enum to any combo using
BindingList<KeyValuePair<OutputType, string>> list = Enum<OutputType>.GetBindableList();
cbo.datasource=list;
cbo.DisplayMember="Value";
cbo.ValueMember="Key";
For that I added:
public static BindingList<KeyValuePair<T, string>> GetBindingList()
{
BindingList<KeyValuePair<T, string>> list = new BindingList<KeyValuePair<T, string>>();
foreach (T value in Enum<T>.GetValues())
{
string Desc = Enum<T>.Description(value);
list.Add(new KeyValuePair<T, string>(value, Desc));
}
return list;
}
But "Enum.Description(value)" is not even compiling:
Argument '1': cannot convert from 'T' to 'System.Enum'
How can I do that? Is that even possible?
Thank you.
Take a look at this article. You can do this using the System.ComponentModel.DescriptionAttribute or creating your own attribute:
/// <summary>
/// Provides a description for an enumerated type.
/// </summary>
[AttributeUsage(AttributeTargets.Enum | AttributeTargets.Field,
AllowMultiple = false)]
public sealed class EnumDescriptionAttribute : Attribute
{
private string description;
/// <summary>
/// Gets the description stored in this attribute.
/// </summary>
/// <value>The description stored in the attribute.</value>
public string Description
{
get
{
return this.description;
}
}
/// <summary>
/// Initializes a new instance of the
/// <see cref="EnumDescriptionAttribute"/> class.
/// </summary>
/// <param name="description">The description to store in this attribute.
/// </param>
public EnumDescriptionAttribute(string description)
: base()
{
this.description = description;
}
}
You then need to decorate the enum values with this new attribute:
public enum SimpleEnum
{
[EnumDescription("Today")]
Today,
[EnumDescription("Last 7 days")]
Last7,
[EnumDescription("Last 14 days")]
Last14,
[EnumDescription("Last 30 days")]
Last30,
[EnumDescription("All")]
All
}
All of the "magic" takes place in the following extension methods:
/// <summary>
/// Provides a static utility object of methods and properties to interact
/// with enumerated types.
/// </summary>
public static class EnumHelper
{
/// <summary>
/// Gets the <see cref="DescriptionAttribute" /> of an <see cref="Enum" />
/// type value.
/// </summary>
/// <param name="value">The <see cref="Enum" /> type value.</param>
/// <returns>A string containing the text of the
/// <see cref="DescriptionAttribute"/>.</returns>
public static string GetDescription(this Enum value)
{
if (value == null)
{
throw new ArgumentNullException("value");
}
string description = value.ToString();
FieldInfo fieldInfo = value.GetType().GetField(description);
EnumDescriptionAttribute[] attributes =
(EnumDescriptionAttribute[])
fieldInfo.GetCustomAttributes(typeof(EnumDescriptionAttribute), false);
if (attributes != null && attributes.Length > 0)
{
description = attributes[0].Description;
}
return description;
}
/// <summary>
/// Converts the <see cref="Enum" /> type to an <see cref="IList" />
/// compatible object.
/// </summary>
/// <param name="type">The <see cref="Enum"/> type.</param>
/// <returns>An <see cref="IList"/> containing the enumerated
/// type value and description.</returns>
public static IList ToList(this Type type)
{
if (type == null)
{
throw new ArgumentNullException("type");
}
ArrayList list = new ArrayList();
Array enumValues = Enum.GetValues(type);
foreach (Enum value in enumValues)
{
list.Add(new KeyValuePair<Enum, string>(value, GetDescription(value)));
}
return list;
}
}
Finally, you can then simply bind the combobox:
combo.DataSource = typeof(SimpleEnum).ToList();
You should change:
public static string Description(Enum value)
{
...
}
to
public static string Description(T value)
{
...
}
so it accepts a value of the enumeration. Now here is where it gets tricky: you have a value, but attributes decorate the field which holds the value.
You actually need to reflect over the enumeration's fields and check the value of each against the value you've been given (results should be cached for performance):
foreach(var field in typeof(T).GetFields())
{
T fieldValue;
try
{
fieldValue = (T) field.GetRawConstantValue();
}
catch(InvalidOperationException)
{
// For some reason, one of the fields returned is {Int32 value__},
// which throws an InvalidOperationException if you try and retrieve
// its constant value.
//
// I am unsure how to check for this state before
// attempting GetRawConstantValue().
continue;
}
if(fieldValue == value)
{
var attribute = LMIGHelper.GetAttribute(field, typeof(DescriptionAttribute)) as DescriptionAttribute;
return attribute == null ? value.ToString() : attribute.Description;
}
}
Edit addressing the follow-up question
The FillComboFromEnum method is missing the type parameter for the enum. Try this:
public static void FillComboFromEnum<T>(ComboBox Cbo, BindingList<KeyValuePair<T, string>> List) where T : struct
Notice I constrained the type to be a struct. It's not a full enumeration constraint, but it's closer than nothing.
Enum doesn't have a Description() method. The best you could do is have your enum implement an interface that has the Description() method. If you do that, then you can have
public static BindingList<KeyValuePair<T extends _interface_, String>> getBindingList()
and then inside of that you can refer to
T foo = ...?
foo.Description(...);