I'm having issues with my custom serializer sometimes not working when passing information between Orchestration Functions and I don't know if this is because of how the object is nested / constructed or if this has something to do with durable functions and how I'm implementing the serializer. Mostly it seems to fails on a Activity call inside an Ochestration that's been called by a Durable Client.
Here is the details:
So I have a custom base class for what is essentially a string Enum (It is a compilation of ideas I found here on Stack Overflow)
public abstract class StringEnum<T>
where T : StringEnum<T>
{
public readonly string Value;
protected StringEnum(string value)
{
Value = value;
}
public override string ToString()
{
return Value;
}
public override bool Equals(object obj)
{
try
{
return (string)obj == Value;
}
catch
{
return false;
}
}
public override int GetHashCode()
{
return Value.GetHashCode();
}
public static IEnumerable<T> All
=> typeof(T).GetProperties()
.Where(p => p.PropertyType == typeof(T))
.Select(x => (T)x.GetValue(null, null));
public static implicit operator string(StringEnum<T> enumObject)
{
return enumObject?.Value;
}
public static implicit operator StringEnum<T>(string stringValue)
{
if (All.Any(x => x.Value == stringValue))
{
Type t = typeof(T);
ConstructorInfo ci = t.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic, null, new Type[] { typeof(string) }, null);
return (T)ci.Invoke(new object[] { stringValue });
}
return null;
}
public static bool operator ==(StringEnum<T> a, StringEnum<T> b)
{
return a.Value == b.Value;
}
public static bool operator !=(StringEnum<T> a, StringEnum<T> b)
{
return a.Value != b.Value;
}
}
I have two implementations of this:
public class ReportType : StringEnum<ReportType>, IReportType
{
private ReportType(string value): base(value) { }
public new string Value { get { return base.Value; } }
public static ReportType A_Orders => new ReportType("A_GET_ORDERS");
// ... more types
}
public class ReportStatus : StringEnum<ReportStatus>
{
private ReportStatus(string value): base(value) { }
public new string Value { get { return base.Value; } }
public static ReportStatus New => new ReportStatus("New");
public static ReportStatus Done => new ReportStatus("Done");
// ... more types
}
I wrote a custom JsonConverter to handle the JSON transitions for this class
public class StringEnumJsonConverter<T> : JsonConverter<T>
where T : StringEnum<T>
{
public override void WriteJson(JsonWriter writer, T value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString());
}
public override T ReadJson(JsonReader reader, Type objectType, T existingValue, bool hasExistingValue, JsonSerializer serializer)
{
string s = (string)reader.Value;
return (T)s;
}
}
I then implemented it in the function startup
[assembly: FunctionsStartup(typeof(Functions.Startup))]
namespace Functions
{
public class Startup : FunctionsStartup
{
public override void Configure(IFunctionsHostBuilder builder)
{
builder.Services.AddSingleton<IMessageSerializerSettingsFactory, StringEnumMessageSerializerSettingsFactory>();
}
internal class StringEnumMessageSerializerSettingsFactory : IMessageSerializerSettingsFactory
{
public JsonSerializerSettings CreateJsonSerializerSettings()
{
return new JsonSerializerSettings()
{
Converters = new List<JsonConverter>
{
new StringEnumJsonConverter<ReportType>(),
new StringEnumJsonConverter<ReportStatus>(),
},
ContractResolver = new StringEnumResolver()
};
}
}
internal class StringEnumResolver : DefaultContractResolver
{
protected override JsonContract CreateContract(Type objectType)
{
if (objectType == typeof(ReportType))
{
return GetContract(new StringEnumJsonConverter<ReportType>()), objectType);
}
else if (objectType == typeof(ReportStatus))
{
return GetContract(new StringEnumJsonConverter<ReportStatus>(), objectType);
}
return base.CreateContract(objectType);
}
private JsonContract GetContract(JsonConverter converter, Type objectType)
{
var contract = base.CreateObjectContract(objectType);
contract.Converter = converter;
return contract;
}
}
}
}
I have a class that uses the ReportType
public class ReportsRequestOptions
{
public List<ReportType> ReportTypes { get; set; }
public List<int> Ids { get; set; }
public DateTime From { get; set; }
public DateTime To { get; set; }
}
and a class that uses both ReportType and ReportStatus which is used in a list in another class
public class ReportRequest
{
public ReportType ReportName { get; }
public ReportStatus ReportStatus { get; set; }
// other fields that work
}
internal class ClientReportsRequest
{
public int Id {get; set; }
public List<ReportRequest> Requests { get; set; }
public DateTime To {get; set; }
public DateTime From {get; set; }
}
I use ReportsRequestOptions when I move data from my HttpTrigger to my main Orchestration function but when I then pass a ClientReportsRequest into a sub Orchestration the JsonConverter doesn't seem to work, the values are just Null instead of the strings they normally show as. I can put a break point in the converter and see that it is being called but for some reason the values don't appear in my locals so I can't inspect it to find out why this is happening.
Implementation:
[FunctionName(nameof(RunReportsAsync))]
public async Task<IActionResult> RunReportsAsync(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = null)] HttpRequest req,
[DurableClient] IDurableClient client
)
{
string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
ReportsRequestOptions requestOptions = JsonConvert.DeserializeObject<ReportsRequestOptions>(requestBody, new StringEnumJsonConverter<ReportType>());
// StringEnum data is correct at this point
if (!requestOptions.ReportTypes.Any())
requestOptions.ReportTypes.AddRange(ReportType.All);
var instanceId = await client.StartNewAsync(nameof(GetReports), requestOptions);
return new OkObjectResult(instanceId);
}
[FunctionName(nameof(GetReports))]
public async Task<RunLog> GetReports(
[OrchestrationTrigger] IDurableOrchestrationContext context
)
{
var requestOptions = context.GetInput<ReportsRequestOptions>();
// string enum data is correct at this point
var clientReportsRequests = GetClientInfo(storeIds)
.Select(x => new ClientReportsRequest()
{
ReportTypes = requestOptions.ReportTypes,
Id = x.Id,
From = requestOptions.From,
To = requestOptions.To
});
// ParallelForEach Async code shouldn't be the issue here.
// it's based on this article: https://dev.to/cgillum/scheduling-tons-of-orchestrator-functions-concurrently-in-c-1ih7
var results = (await clientReportsRequests.ParallelForEachAsync(MaxParallelStoreThreadCount, clientReportsRequest =>
{
return context.CallSubOrchestratorAsync<(int, List<ReportRequest>)>(nameof(GetReportsForClient), clientReportsRequest);
})).ToDictionary(x => x.Item1, x => x.Item2);
return new RunLog(requestOptions, results);
}
[FunctionName(nameof(GetReportsForClient))]
public async Task<(int, List<ReportRequest>)> GetReportsForClient(
[OrchestrationTrigger] IDurableOrchestrationContext context
)
{
var requestOptions = context.GetInput<ClientReportsRequest>();
var completedRequests = new List<ReportRequest>();
foreach (var request in requestOptions.Requests)
{
completedRequests.add(GetReport(request));
// GetReport code has been truncated for brevity but the issue is that neither field in the request
// has it's StringEnum data at this point
}
return (requestOptions.Id, completedRequests);
}
I've been beating my head against this for a couple of days and can't find an answer, anyone got any ideas? Is there a better way I should be serializing this?
Ugh, this was a non-issue. I was missing a public get on Requests field in the ClientReportsRequest sorry to have wasted anyone's time.
Related
i have a json string with duplicated property name with different data types. if i removed all duplicated other data types, it will work fine.
{
"data":[
{
"ctr":0,
"spend":11839.8600,
"clicks":6402
},
{
"ctr":0,
"spend":12320.5000,
"clicks":5789
},
{
"clicks":{
"value":13156.0,
"prior_year":0.0,
"prior_month":14122.0,
"prior_month_perc":0.0684039087947882736156351792,
"prior_year_perc":0.0
}
}
],
"timing":null,
"warnings":[
],
"success":true
}
here is the my model class
public class MyTestModel
{
public int? ctr { get; set; }
public decimal? spend { get; set; }
public int? clicks { get; set; }
}
if i removed this snipped json part, program will work.
{
"clicks":{
"value":13156.0,
"prior_year":0.0,
"prior_month":14122.0,
"prior_month_perc":0.0684039087947882736156351792,
"prior_year_perc":0.0
}
are there any method to stop binding unsupported types to model property.
You can use a JsonConverter to support either/both types in a 'union' class.
public partial class ClicksClass
{
[JsonProperty("value")]
public long Value { get; set; }
[JsonProperty("prior_year")]
public long PriorYear { get; set; }
[JsonProperty("prior_month")]
public long PriorMonth { get; set; }
[JsonProperty("prior_month_perc")]
public double PriorMonthPerc { get; set; }
[JsonProperty("prior_year_perc")]
public long PriorYearPerc { get; set; }
}
public partial struct ClicksUnion
{
public ClicksClass ClicksClass;
public long? Integer;
public static implicit operator ClicksUnion(ClicksClass ClicksClass) => new ClicksUnion { ClicksClass = ClicksClass };
public static implicit operator ClicksUnion(long Integer) => new ClicksUnion { Integer = Integer };
}
internal static class Converter
{
public static readonly JsonSerializerSettings Settings = new JsonSerializerSettings
{
MetadataPropertyHandling = MetadataPropertyHandling.Ignore,
DateParseHandling = DateParseHandling.None,
Converters =
{
ClicksUnionConverter.Singleton,
new IsoDateTimeConverter { DateTimeStyles = DateTimeStyles.AssumeUniversal }
},
};
}
internal class ClicksUnionConverter : JsonConverter
{
public override bool CanConvert(Type t) => t == typeof(ClicksUnion) || t == typeof(ClicksUnion?);
public override object ReadJson(JsonReader reader, Type t, object existingValue, JsonSerializer serializer)
{
switch (reader.TokenType)
{
case JsonToken.Integer:
var integerValue = serializer.Deserialize<long>(reader);
return new ClicksUnion { Integer = integerValue };
case JsonToken.StartObject:
var objectValue = serializer.Deserialize<ClicksClass>(reader);
return new ClicksUnion { ClicksClass = objectValue };
}
throw new Exception("Cannot unmarshal type ClicksUnion");
}
public override void WriteJson(JsonWriter writer, object untypedValue, JsonSerializer serializer)
{
var value = (ClicksUnion)untypedValue;
if (value.Integer != null)
{
serializer.Serialize(writer, value.Integer.Value);
return;
}
if (value.ClicksClass != null)
{
serializer.Serialize(writer, value.ClicksClass);
return;
}
throw new Exception("Cannot marshal type ClicksUnion");
}
public static readonly ClicksUnionConverter Singleton = new ClicksUnionConverter();
}
This means that, once parsed, whenever you get to the ClickUnion instances, you can check which field is not null, and that is the one you use. It depends on your usecase of course.
BTW, I used this web site to write the classes from the json
I assume you are using JsonConvert.DeserializeObject to deserialize your json string.
While doing so, the data type missmatch causes error and you can safely handle this error using JsonSerializerSettings as below
MyTestModel result = JsonConvert.DeserializeObject<MyTestModel>("{json string}",
new JsonSerializerSettings {
Error = MethodToHandleError
});
and then define MethodToHandleError something like below. This will be called whenever deserialization error occurs:
public void MethodToHandleError(object sender, ErrorEventArgs args)
{
// log error message -> args.ErrorContext.Error.Message
// you can also write your own logic here
args.ErrorContext.Handled = true;
}
I have a request to create a Web API that is able to accept a POST request and take different actions depending on the type of data (DataAvailableNotification vs ExpiredNotification) received in the parameters.
I've created an ApiController and exposed two methods:
[HttpPost]
public void DataAvailable(DataAvailableNotification dataAvailable,
[FromUri] string book, [FromUri] string riskType)
{
}
[HttpPost]
public void DataAvailable(ExpiredNotification dataAvailable,
[FromUri] string book, [FromUri] string riskType)
{
}
public class DataAvailableNotification
{
[JsonProperty(PropertyName = "$type")]
public string RdfType { get { return "App.RRSRC.Feeds.DataAvailable"; } }
public string SnapshotRevisionId { get; set; }
public string[] URLs { get; set; }
public string ConsumerId { get; set; }
public Guid ChannelId { get; set; }
}
public class ExpiredNotification
{
[JsonProperty(PropertyName = "$type")]
public string RdfType { get { return "Service.Feeds.Expired"; } }
public string ConsumerId { get; set; }
public Guid ChannelId { get; set; }
}
However, they don't get called at all.
If I comment out one of them the notification reaches the controller but I cannot handle the notification type correctly (given that both notifications will map to the same method).
Is there any way configuring Web API to look into the type of the POSTed value and call the best matching controller method?
PS: I cannot have 2 different of URLs to handle the different notifications. So please don't suggest this.
Use one action and filter based on the type.
I got around the same issue by using reflection and doing something like the following.
[HttpPost]
public void DataAvailable([FromBody]IDictionary<string, string> dataAvailable,
[FromUri] string book, [FromUri] string riskType) {
if(dataAvailable != null && dataAvailable.ContainsKey("$type") {
var type = dataAvaliable["$type"];
if(type == "App.RRSRC.Feeds.DataAvailable"){
DataAvailableNotification obj = createInstanceOf<DataAvailableNotification>(dataAvailable);
DataAvailable(obj,book,riskType);
} else if (type == "Service.Feeds.Expired") {
ExpiredNotification obj = createInstanceOf<ExpiredNotification>(dataAvailable);
DataAvailable(obj,book,riskType);
}
}
}
private void DataAvailable(DataAvailableNotification dataAvailable, string book, string riskType) {
}
private void DataAvailable(ExpiredNotification dataAvailable, string book, string riskType) {
}
private T createInstanceOf<T>(IDictionary<string, string> data) where T : class, new() {
var result = new T();
var type = typeof(T);
//map properties
foreach (var kvp in data) {
var propertyName = kvp.Key;
var rawValue = kvp.Value;
var property = type.GetProperty(propertyName);
if (property != null && property.CanWrite) {
property.SetValue(result, rawValue );
}
}
return result;
}
The solution I settled with is similar to what #Nikosi and #jpgrassi suggested.
In the controller I've created a single notification point:
[HttpPost]
public void Notify(BaseNotification notification,
[FromUri] string book, [FromUri] string riskType)
{
DataAvailableNotification dataAvailableNotification;
ExpiredNotification expiredNotification;
if ((dataAvailableNotification = notification as DataAvailableNotification) != null)
{
HandleDataAvailableNotification(dataAvailableNotification);
}
else if ((expiredNotification = notification as ExpiredNotification) != null)
{
HandleExpiredNotification(expiredNotification);
}
}
private void HandleDataAvailableNotification(DataAvailableNotification dataAvailableNotification)
{
}
private void HandleExpiredNotification(ExpiredNotification expiredNotification)
{
}
BaseNotification is the base class for all notifications:
public abstract class BaseNotification
{
[JsonProperty(PropertyName = "$type")]
public abstract string RdfType { get; }
public string ConsumerId { get; set; }
public Guid ChannelId { get; set; }
}
Created a JsonConverter:
public class RdfNotificationJsonConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotSupportedException();
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var resultJson = JObject.Load(reader);
var rdfType = resultJson["$type"].ToObject<string>();
BaseNotification result;
switch (rdfType)
{
case "App.RRSRC.Feeds.DataAvailable":
{
result = new DataAvailableNotification
{
SnapshotRevisionId = resultJson["SnapshotRevisionId"].ToObject<string>(),
URLs = resultJson["URLs"].ToObject<string[]>()
};
break;
}
case "Service.Feeds.Expired":
{
result = new ExpiredNotification();
break;
}
default:
{
throw new NotSupportedException();
}
}
result.ChannelId = resultJson["ChannelId"].ToObject<Guid>();
result.ConsumerId = resultJson["ConsumerId"].ToObject<string>();
return result;
}
public override bool CanConvert(Type objectType)
{
return objectType == typeof(BaseNotification);
}
}
And registered the new converter in the configuration:
public static void Configure(HttpSelfHostConfiguration config)
{
Throw.IfNull(config, "config");
config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new RdfNotificationJsonConverter());
}
I like this solution better because I have the actual type in the controller and the converter handles the ugly deserialization part (also more testable).
PS: I'll move the literal strings somewhere else so I don't specify them twice in the solution.
Situation:
Language: C# using the C# Driver
I have a model that contains a List as a property. That List can contain one of 3 different models that all inherit the BaseModelClass. To assist in serialization of this situation Mongo adds _t to identify which of the models is actually being used. For us this is a problem due to the amount of space that _t is taking up. I am a lowly dev, I have asked for more space and ram and they have told me to solve it without the additional space. So I sat down to writing a custom serializer that handled the different types without writing a _t to the BSONDocument. I thought all was great until I started doing my unit testing of the serialization. I started getting "ReadEndArray can only be called when ContextType is Array, not when ContextType is Document."
Any advice or suggestions are greatly appreciated.
Here is the code I have thus far...
<---------Collection Model--------------------->
[BsonCollectionName("calls")]
[BsonIgnoreExtraElements]
public class Call
{
[BsonId]
public CallId _id { get; set; }
[BsonElement("responses")]
[BsonIgnoreIfNull]
public IList<DataRecord> Responses { get; set; }
}
<----------Base Data Record------------------>
[BsonSerializer(typeof(DataRecordSerializer))]
public abstract class DataRecord
{
[BsonElement("key")]
public string Key { get; set; }
}
<-----------Examples of actual Data Records----------------->
[BsonSerializer(typeof(DataRecordSerializer))]
public class DataRecordInt : DataRecord
{
[BsonElement("value")]
public int Value { get; set; }
}
[BsonSerializer(typeof(DataRecordSerializer))]
public class DataRecordDateTime : DataRecord
{
[BsonElement("value")]
public DateTime? Value { get; set; }
}
<---------------Unit Test to trigger Deserializer----------------->
//Arrange
var bsonDocument = TestResources.SampleCallJson;
//Act
var result = BsonSerializer.Deserialize<Call>(bsonDocument);
//Assert
Assert.IsTrue(true);
<----------------Serializer----------------->
public class DataRecordSerializer : IBsonSerializer
{
public object Deserialize(BsonReader bsonReader, Type nominalType, IBsonSerializationOptions options)
{
//Entrance Criteria
if(nominalType != typeof(DataRecord)) throw new BsonSerializationException("Must be of base type DataRecord.");
if(bsonReader.GetCurrentBsonType() != BsonType.Document) throw new BsonSerializationException("Must be of type Document.");
bsonReader.ReadStartDocument();
var key = bsonReader.ReadString("key");
bsonReader.FindElement("value");
var bsonType = bsonReader.CurrentBsonType;
if (bsonType == BsonType.DateTime)
{
return DeserializeDataRecordDateTime(bsonReader, key);
}
return bsonType == BsonType.Int32 ? DeserializeDataRecordInt(bsonReader, key) : DeserializeDataRecordString(bsonReader, key);
}
public object Deserialize(BsonReader bsonReader, Type nominalType, Type actualType, IBsonSerializationOptions options)
{
//Entrance Criteria
if (nominalType != typeof (DataRecord)) throw new BsonSerializationException("Must be of base type DataRecord.");
if (bsonReader.GetCurrentBsonType() != BsonType.Document) throw new BsonSerializationException("Must be of type Document.");
bsonReader.ReadStartDocument(); // Starts Reading and is able to pull data fine through this and the next few lines of code.
var key = bsonReader.ReadString("key");
if (actualType == typeof(DataRecordDateTime))
{
return DeserializeDataRecordDateTime(bsonReader, key);
}
return actualType == typeof(DataRecordInt) ? DeserializeDataRecordInt(bsonReader, key) : DeserializeDataRecordString(bsonReader, key); // Once it tries to return I am getting the following Error: ReadEndArray can only be called when ContextType is Array, not when ContextType is Document.
}
public IBsonSerializationOptions GetDefaultSerializationOptions()
{
return new DocumentSerializationOptions
{
AllowDuplicateNames = false,
SerializeIdFirst = false
};
}
public void Serialize(BsonWriter bsonWriter, Type nominalType, object value, IBsonSerializationOptions options)
{
var currentType = value.GetType();
if (currentType == typeof (DataRecordInt))
{
SerializeDataRecordInt(bsonWriter, value);
return;
}
if (currentType == typeof(DataRecordDateTime))
{
SerializeDataRecordDateTime(bsonWriter, value);
return;
}
if (currentType == typeof(DataRecordString))
{
SerializeDataRecordString(bsonWriter, value);
}
}
private static object DeserializeDataRecordString(BsonReader bsonReader, string key)
{
var stringValue = bsonReader.ReadString();
var isCommentValue = false;
if (bsonReader.FindElement("iscomment"))
{
isCommentValue = bsonReader.ReadBoolean();
}
return new DataRecordString
{
Key = key,
Value = stringValue,
IsComment = isCommentValue
};
}
private static object DeserializeDataRecordInt(BsonReader bsonReader, string key)
{
var intValue = bsonReader.ReadInt32();
return new DataRecordInt
{
Key = key,
Value = intValue
};
}
private static object DeserializeDataRecordDateTime(BsonReader bsonReader, string key)
{
var dtValue = bsonReader.ReadDateTime();
var dateTimeValue = new BsonDateTime(dtValue).ToUniversalTime();
return new DataRecordDateTime
{
Key = key,
Value = dateTimeValue
};
}
private static void SerializeDataRecordString(BsonWriter bsonWriter, object value)
{
var stringRecord = (DataRecordString) value;
bsonWriter.WriteStartDocument();
var keyValue = stringRecord.Key;
bsonWriter.WriteString("key", string.IsNullOrEmpty(keyValue) ? string.Empty : keyValue);
var valueValue = stringRecord.Value;
bsonWriter.WriteString("value", string.IsNullOrEmpty(valueValue) ? string.Empty : valueValue);
bsonWriter.WriteBoolean("iscomment", stringRecord.IsComment);
bsonWriter.WriteEndDocument();
}
private static void SerializeDataRecordDateTime(BsonWriter bsonWriter, object value)
{
var dateRecord = (DataRecordDateTime) value;
var millisecondsSinceEpoch = dateRecord.Value.HasValue
? BsonUtils.ToMillisecondsSinceEpoch(new DateTime(dateRecord.Value.Value.Ticks, DateTimeKind.Utc))
: 0;
bsonWriter.WriteStartDocument();
var keyValue = dateRecord.Key;
bsonWriter.WriteString("key", string.IsNullOrEmpty(keyValue) ? string.Empty : keyValue);
if (millisecondsSinceEpoch != 0)
{
bsonWriter.WriteDateTime("value", millisecondsSinceEpoch);
}
else
{
bsonWriter.WriteString("value", string.Empty);
}
bsonWriter.WriteEndDocument();
}
private static void SerializeDataRecordInt(BsonWriter bsonWriter, object value)
{
var intRecord = (DataRecordInt) value;
bsonWriter.WriteStartDocument();
var keyValue = intRecord.Key;
bsonWriter.WriteString("key", string.IsNullOrEmpty(keyValue) ? string.Empty : keyValue);
bsonWriter.WriteInt32("value", intRecord.Value);
bsonWriter.WriteEndDocument();
}
}
Also asked here: https://groups.google.com/forum/#!topic/mongodb-user/iOeEXbUYbo4
I think your better bet in this situation is to use a custom discriminator convention. You can see an example of this here: https://github.com/mongodb/mongo-csharp-driver/blob/v1.x/MongoDB.DriverUnitTests/Samples/MagicDiscriminatorTests.cs. While this example is based on whether a field exists in the document, you could easily base it on what type the field is (BsonType.Int32, BsonType.Date, etc...).
Basing on #Craig Wilson answer, to get rid off all discriminators, you can:
public class NoDiscriminatorConvention : IDiscriminatorConvention
{
public string ElementName => null;
public Type GetActualType(IBsonReader bsonReader, Type nominalType) => nominalType;
public BsonValue GetDiscriminator(Type nominalType, Type actualType) => null;
}
and register it:
BsonSerializer.RegisterDiscriminatorConvention(typeof(BaseEntity), new NoDiscriminatorConvention());
This problem occurred in my case when I was adding a Dictionary<string, object> and List entities to database. The following link helped me in this regard: C# MongoDB complex class serialization. For your case, I would suggest, following the link given above, as follows:
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
[JsonConverter(typeof(DataRecordConverter))]
public abstract class DataRecord
{
public string Key { get; set; }
public string DataRecordType {get;}
}
public class DataRecordInt : DataRecord
{
public int Value { get; set; }
public override string DataRecordType => "Int"
}
public class DataRecordDateTime : DataRecord
{
public DateTime? Value { get; set; }
public override string DataRecordType => "DateTime"
}
public class DataRecordConverter: JsonConverter
{
public override bool CanWrite => false;
public override bool CanRead => true;
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DataRecord);
}
public override void WriteJson(
JsonWriter writer,
object value,
JsonSerializer serializer)
{}
public override object ReadJson(
JsonReader reader,
Type objectType,
object existingValue,
JsonSerializer serializer)
{
var jsonObject = JObject.Load(reader);
var dataRecord = default(DataRecord);
switch (jsonObject["DataRecordType"].Value<string>())
{
case "Int":
dataRecord = new DataRecordInt();
break;
case "DateTime":
dataRecord = new DataRecordDataTime();
break;
}
serializer.Populate(jsonObject.CreateReader, dataRecord);
return dataRecord;
}
}
[BsonCollectionName("calls")]
[BsonIgnoreExtraElements]
public class Call
{
[BsonId]
public CallId _id { get; set; }
[JsonIgnore]
[BsonElement("responses")]
[BsonIgnoreIfNull]
public BsonArray Responses { get; set; }
}
You can add populate the BsonArray using:
var jsonDoc = JsonConvert.SerializeObject(source);
var bsonArray = BsonSerializer.Deserialize<BsonArray>(jsonDoc);
Now, you can get deserialized List from Mongo using:
var bsonDoc = BsonExtensionMethods.ToJson(source);
var dataRecordList = JsonConvert.DeserializeObject<List<DataRecord>>(bsonDoc, new DataRecordConverter())
Hope this helps, again, thanks to C# MongoDB complex class serialization for this.
Ok I have WebApi application that is sending back name value pairs like so
{'FirstName':'SomeGuy'}
On the server the FirstName field is not just a string, it is a generic object that hold additional information about FirstName, and is not send back from the client.
Here is a outline of the classes
public abstract class Field
{
protected object _value;
......More Properties/Methods
public bool HasValue
{
get { return _hasValue; }
}
public object Value
{
get { return _hasValue ? _value : null; }
}
protected void SetValue(object value, bool clearHasValue = false)
{
_value = value;
_hasValue = clearHasValue ?
false :
value != null;
}
}
public class Field<T> : Field
{
..Constructors and methods
public new T Value
{
get { return _hasValue ? (T)_value : default(T); }
set { SetValue(value); }
}
}
So.. In theory I may be trying to bind to a model like
class FieldModel
{
public Field<string> FirstName { get; set; }
public Field<string> LastName { get; set; }
public Field<Decimal> Amount { get; set; }
public FieldModel()
{
FirstName = new Field<string>();
LastName = new Field<string>();
Amount = new Field<decimal>();
}
}
So here is the issue.. I want FirstName in my json object to deseralize to right property. Now if I modify the json package to {'FirstName.Value':'SomeGuy'} JSON.net works out of the box, but I really not to do that. I have been tying to make my own JsonConverter but have not been able to get that to work. So, I don't think this should be very hard, but I am a bit stuck.
EDIT
So.. I did come up with a solution that works, but I have to think there is a better way.. It uses dynamics and I have to think that I am missing an easy solution.
public class FieldConverter : JsonConverter
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
{
return null;
}
var internalVal = serializer.Deserialize(reader, objectType.GetGenericArguments().FirstOrDefault());
var retVal = existingValue as dynamic;
retVal.Value = internalVal as dynamic;
return retVal;
}
public override bool CanRead
{
get { return true; }
}
public override bool CanWrite
{
get { return false; }
}
public override bool CanConvert(Type objectType)
{
return objectType.IsSubclassOf(typeof(Field));
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
You can easily do this with JSON.NET's CustomCreationConverter. Here's an example:
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime BirthDate { get; set; }
}
public class Employee : Person
{
public string Department { get; set; }
public string JobTitle { get; set; }
}
public class PersonConverter : CustomCreationConverter<Person>
{
public override Person Create(Type objectType)
{
return new Employee();
}
}
And the usage:
string json = #"{
'Department': 'Furniture',
'JobTitle': 'Carpenter',
'FirstName': 'John',
'LastName': 'Joinery',
'BirthDate': '1983-02-02T00:00:00'
}";
Person person = JsonConvert.DeserializeObject<Person>(json, new PersonConverter());
Console.WriteLine(person.GetType().Name);
// Employee
Employee employee = (Employee)person;
Console.WriteLine(employee.JobTitle);
// Carpenter
I have a class that looks something like this:
public class MyClass
{
string _value;
public static implicit operator MyClass (string value)
{
return new MyClass(value);
}
MyClass(string value)
{
// Do something...
_value = value;
}
public override string ToString()
{
// Do something...
return _value;
}
}
Hence, I can use the class like this:
MyClass a = "Hello!";
But in Raven DB it will just be stored like
"SomeProperty": {}
since it has no public properties. And it is quite useless.
To solve this I would make the _value private member a public property instead, like this:
public string Value { get; set; }
and Raven DB will store
"SomeProperty": { "Value": "Hello!" }
and it will be deserializable.
But I don't want this public property. Can I somehow make Raven DB serialize and deserialize the class as was it would a string? Like:
"SomeProperty": "Hello!"
Hi I know this is old but I thought I would add some additions to Ayendes' reply to help people who like me had the same issue and spent hours looking on forums for an answer (of which there were a few but none had any example that you could follow), it's not hard to figure this out but with an example I could have solved this in 10 minutes as opposed to spending a few hours.
My problems was that we have custom value type structs in our application the example I will use is EmailAddress. Unfortunately in Ravendb we could not run queries against these types without defining a custom serialiser.
Our Value Type looked Like this:
[DataContract(Namespace = DataContractNamespaces.ValueTypes)]
public struct EmailAddress : IEquatable<EmailAddress>
{
private const char At = '#';
public EmailAddress(string value) : this()
{
if (value == null)
{
throw new ArgumentNullException("value");
}
this.Value = value;
}
public bool IsWellFormed
{
get
{
return Regex.IsMatch(this.Value, #"\w+([-+.']\w+)*#\w+([-.]\w+)*\.\w+([-.]\w+)*");
}
}
public string Domain
{
get
{
return this.Value.Split(At)[1];
}
}
[DataMember(Name = "Value")]
private string Value { get; set; }
public static bool operator ==(EmailAddress left, EmailAddress right)
{
return left.Equals(right);
}
public static bool operator !=(EmailAddress left, EmailAddress right)
{
return !left.Equals(right);
}
public override bool Equals(object obj)
{
if (obj == null)
{
return false;
}
return this.Equals(new EmailAddress(obj.ToString()));
}
public override int GetHashCode()
{
return this.Value.GetHashCode();
}
public override string ToString()
{
return this.Value;
}
public bool Equals(EmailAddress other)
{
return other != null && this.Value.Equals(other.ToString(), StringComparison.OrdinalIgnoreCase);
}
}
The type of document we wanted to save and query would look something like this
public class Customer
{
public Guid Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public EmailAddress Email { get; set; }
}
The custom serialiser to store our email as a raw string and then convert it back to its value type on retrieval looked like this:
public class EmailConverterTest : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(EmailAddress);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
EmailAddress actualAddress = new EmailAddress(reader.Value.ToString());
return actualAddress;
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
EmailAddress actualAddress = (EmailAddress)value;
string stringEmail = actualAddress.ToString();
writer.WriteValue(stringEmail);
}
}
Finally I wired it up and was able to query everything as follows:
public static void serializercustom(Newtonsoft.Json.JsonSerializer serialiser)
{
serialiser.Converters.Add(new EmailConverterTest());
}
public static void TestCustomer()
{
using (var documentStore = new DefaultDocumentStore())
{
documentStore.ConnectionStringName = Properties.Settings.Default.SandBoxConnection;
documentStore.Initialize();
documentStore.Conventions.CustomizeJsonSerializer = new Action<Newtonsoft.Json.JsonSerializer>(serializercustom);
var customer = new Customer
{
Id = Guid.NewGuid(),
FirstName = "TestFirstName",
LastName = "TestLastName",
Email = new EmailAddress("testemail#gmail.com")
};
// Save and retrieve the data
using (var session = documentStore.OpenSession())
{
session.Store(customer);
session.SaveChanges();
}
using (var session = documentStore.OpenSession())
{
var addressToQuery = customer.Email;
var result = session.Query<Customer>(typeof(CustomerEmailIndex).Name).Customize(p => p.WaitForNonStaleResults()).Where(p => p.Email == addressToQuery);
Console.WriteLine("Number of Results {0}", result.Count()); // This always seems to return the matching document
}
}
}
You can write a JsonConverter and teach RavenDB how you want to store the data.
After you write the converter, register it in the store.Conventions.CustomizeSerializer event.