Using a Dictionary<string, object> in a class that is to be stored in DynamoDB requires a custom property converter that is not very elegant
I have a very simple class in a shared library with looks roughly like this
public class ServiceInfo
{
public string Region { get; set; }
public string Name { get; set; }
public Dictionary<string, object> AdditionalProperties { get; set; }
}
The only way I can find of being able to use this object is by specifying a custom converter for the property. My issue with this is that my shared library now requires a dependency on AWS SDK which is not ideal and I have to use refection to find the existing converter for a type as there is no public DynamoDBEntryConversion.ConvertToEntry which takes the type as an argument
public class ServiceInfo
{
public string Region { get; set; }
public string Name { get; set; }
[DynamoDBProperty(typeof(DictionaryTypeConverter))]
public Dictionary<string, object> AdditionalProperties { get; set; }
}
class DictionaryTypeConverter : IPropertyConverter
{
private static readonly Func<object, DynamoDBEntry> Convert;
static DictionaryTypeConverter()
{
var method = typeof(DynamoDBEntryConversion).GetMethod(nameof(DynamoDBEntryConversion.ConvertToEntry));
Convert = o =>
{
var generic = method.MakeGenericMethod(o.GetType());
return (DynamoDBEntry)generic.Invoke(DynamoDBEntryConversion.V2, new[] { o });
};
}
public DynamoDBEntry ToEntry(object value)
{
var dictionary = value as IDictionary<string, object>;
if (dictionary == null)
throw new InvalidOperationException();
var d = new Document();
foreach (var keyValuePair in dictionary)
{
var key = keyValuePair.Key;
var obj = keyValuePair.Value;
var dynamoDbEntry = (DynamoDBEntry)null;
if (obj != null)
dynamoDbEntry = Convert(obj);
d[key] = dynamoDbEntry;
}
return d;
}
public object FromEntry(DynamoDBEntry entry)
{
var d = entry as Document;
if (d == null)
throw new InvalidOperationException();
var result = new Dictionary<string, object>();
foreach (var keyValuePair in d)
{
result.Add(keyValuePair.Key, ConvertValue(keyValuePair.Value));
}
return result;
}
private object ConvertValue(DynamoDBEntry value)
{
var primitive = value as Primitive;
if (primitive != null)
{
switch (primitive.Type)
{
case DynamoDBEntryType.String:
return value.AsString();
case DynamoDBEntryType.Numeric:
{
int integer;
return DynamoDBEntryConversion.V1.TryConvertFromEntry(value, out integer) ? integer : value.AsDouble();
}
case DynamoDBEntryType.Binary:
return value.AsByteArray();
}
}
else if (value is DynamoDBList)
{
var result = new List<object>();
var list = (DynamoDBList)value;
foreach (var item in list.Entries)
{
result.Add(ConvertValue(item));
}
return result;
}
else if (value is DynamoDBBool)
{
return value.AsBoolean();
}
else if (value is DynamoDBNull)
{
return null;
}
else if (value is Document)
{
return FromEntry(value);
}
throw new InvalidOperationException();
}
}
DictionaryConverterV1 to support TryFromDynamoDBEntryConversion (This is currently all internal)You can use the ServiceInfo object without annotating it with DynamoDBProperty or including a reference to the SDK in the project that defines ServiceInfo. See the below code sample showing how an un-annotated class can use a customer converter.
// Define converter for a property
var dataPropertyConfig = new Amazon.Util.PropertyConfig("AdditionalProperties ");
dataPropertyConfig.Converter = typeof(DictionaryTypeConverter);
// Define type-to-table mapping
var typeMapping = new Amazon.Util.TypeMapping(typeof(ServiceInfo), "MyAmazingTable");
// Add property that needs to be customized
typeMapping.AddProperty(dataPropertyConfig);
// Add global mapping
AWSConfigsDynamoDB.Context.AddMapping(typeMapping);
This information can also be specified inside an app.config/web.config file. You can see an example of that here.
Will that approach work for you?
That is a much nicer way of registering the converter I will give it a try, however this approach still won't fix the need for using reflection in the property converter
True.
Is there a reason to use the Object Persistence Model (DynamoDBContext) for the data in question? OPM is good for cases where all of the data is modeled, but AdditionalProperties clearly isn't.
A more appropriate DynamoDB API would be the Document Model. Since you are already dealing with DynamoDBEntry objects and already need to cast the data coming out of AdditionalProperties, loading a Document object instead of ServiceInfo may save you a lot of hassle.
I may use the Document object but I would still need to covert to a ServiceInfo instance as that's what my API requires
Closing for lack of activity
FWIW, I ran into this issue myself recently and it's quite the surprise. I would expect, of all things, Dictionary
Any pointers on how to convert Document to List<Dictionary<string, object>>?
SDK was able to convert List
Code that I'm working for reference looks something like this:
using (DynamoDBContext dynamoDBContext = new DynamoDBContext(_dynamoDBClient))
{
return await dynamoDBContext.QueryAsync<Myclass>(Parameter).GetRemainingAsync().ConfigureAwait(false);
}
Most helpful comment
FWIW, I ran into this issue myself recently and it's quite the surprise. I would expect, of all things, Dictionary to be supported as a M (map) property easily, and Dictionary seems just as straight forward. Kind of weak-sauce IMHO (i.e. arbitrary) to work with List<>, etc. and not dictionaries.