I am trying to use a durable entity in my Azure Function to cache some data. However, when I try to retrieve the entity (state) for the first time, I get an exception indicating an issue during the entity deserialization.
Here is my entity class and related code
[JsonObject(MemberSerialization.OptIn)]
public class ActionTargetIdCache : IActionTargetIdCache
{
[JsonProperty("cache")]
public Dictionary<string, ActionTargetIdsCacheItemInfo> Cache { get; set; } = new Dictionary<string, ActionTargetIdsCacheItemInfo>();
public void CacheCleanup(DateTime currentUtcTime)
{
foreach (string officeHolderId in Cache.Keys)
{
TimeSpan cacheItemAge = currentUtcTime - Cache[officeHolderId].lastUpdatedTimeStamp;
if (cacheItemAge > TimeSpan.FromMinutes(2))
{
Cache.Remove(officeHolderId);
}
}
}
public void DeleteActionTargetIds(string officeHolderId)
{
if (this.Cache.ContainsKey(officeHolderId))
{
this.Cache.Remove(officeHolderId);
}
}
public void DeleteState()
{
Entity.Current.DeleteState();
}
public void SetActionTargetIds(ActionTargetIdsCacheEntry entry)
{
this.Cache[entry.Key] = entry.Value;
}
public Task<ActionTargetIdsCacheItemInfo> GetActionTargetIdsAsync(string officeHolderId)
{
if (this.Cache.ContainsKey(officeHolderId))
{
return Task.FromResult(Cache[officeHolderId]);
}
else
{
return Task.FromResult(new ActionTargetIdsCacheItemInfo());
}
}
// public void Reset() => this.CurrentValue = 0;
// public int Get() => this.CurrentValue;
[FunctionName(nameof(ActionTargetIdCache))]
public static Task Run([EntityTrigger] IDurableEntityContext ctx)
=> ctx.DispatchAsync<ActionTargetIdCache>();
}
public class ActionTargetIdsCacheEntry
{
// officeHolderId
public string Key { get; set; } = string.Empty;
public ActionTargetIdsCacheItemInfo Value { get; set; } = new ActionTargetIdsCacheItemInfo();
}
[JsonObject(MemberSerialization.OptIn)]
public class ActionTargetIdsCacheItemInfo : ISerializable
{
public ActionTargetIdsCacheItemInfo()
{
lastUpdatedTimeStamp = DateTime.UtcNow;
actionTargetIds = new List<string>();
}
public ActionTargetIdsCacheItemInfo(SerializationInfo info, StreamingContext context)
{
lastUpdatedTimeStamp = info.GetDateTime("lastUpdated");
actionTargetIds = (List<string>)info.GetValue("actionTargetIds", typeof(List<string>));
}
[JsonProperty]
public DateTimeOffset lastUpdatedTimeStamp { get; set; } = DateTimeOffset.UtcNow;
[JsonProperty]
public List<string> actionTargetIds { get; set; } = new List<string>();
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("lastUpdated", lastUpdatedTimeStamp);
info.AddValue("actionTargetIds", actionTargetIds);
}
}
public interface IActionTargetIdCache
{
void CacheCleanup(DateTime currentUtcTime);
void DeleteActionTargetIds(string officeHolderId);
void DeleteState();
void SetActionTargetIds(ActionTargetIdsCacheEntry item);
// Task Reset();
Task<ActionTargetIdsCacheItemInfo> GetActionTargetIdsAsync(string officeHolderId);
// void Delete();
}
Here is the exception I get during the first attempt to access the state from an orchestration using the GetActionTargetIdsAsync method:
Exception has occurred: CLR/Microsoft.Azure.WebJobs.Extensions.DurableTask.EntitySchedulerException
Exception thrown: 'Microsoft.Azure.WebJobs.Extensions.DurableTask.EntitySchedulerException' in System.Private.CoreLib.dll: 'Failed to populate entity state from JSON: Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'PolTrack.CdbGetFunctionApp.ActionTargetIdsCacheItemInfo' because the type requires a JSON object (e.g. {"name":"value"}) to deserialize correctly.
To fix this error either change the JSON to a JSON object (e.g. {"name":"value"}) or change the deserialized type to an array or a type that implements a collection interface (e.g. ICollection, IList) like List<T> that can be deserialized from a JSON array. JsonArrayAttribute can also be added to the type to force it to deserialize from a JSON array.
Path 'cache.officeHolderId1', line 1, position 29.'
Inner exceptions found, see $exception in variables window for more details.
Innermost exception Newtonsoft.Json.JsonSerializationException : Cannot deserialize the current JSON array (e.g. [1,2,3]) into type 'PolTrack.CdbGetFunctionApp.ActionTargetIdsCacheItemInfo' because the type requires a JSON object (e.g. {"name":"value"}) to deserialize correctly.
To fix this error either change the JSON to a JSON object (e.g. {"name":"value"}) or change the deserialized type to an array or a type that implements a collection interface (e.g. ICollection, IList) like List<T> that can be deserialized from a JSON array. JsonArrayAttribute can also be added to the type to force it to deserialize from a JSON array.
Path 'cache.officeHolderId1', line 1, position 29.
Could someone with the sufficient SO privileges please add the tag azure-durable-entities.
I did manage to get around this by following #silent suggestion. I re-designed the entity class to only use CLR types. In my case, that meant replacing Dictionary<string, ActionTargetIdsCacheItemInfo> with two dictionaries Dictionary<string, List<string>> and Dictionary<string, DateTimeOffset>.
Related
We used the Masstransit Mediator to write request/response "Consumers" called from API controllers. Before the consumer is taken action, some ConsumeFilters take place: Logging, Validation and DBTransaction. Next I would like to implement a Cache Filter using simple Microsoft In-Memory Cache. The filter should check if the request object is already in cache, if not the consumer pipe is called and the cache object is added, else the cached object should be returned immediatly.
I could not figure out how write such a filter. Do I need two filters? If I call RespondAsync from ConsumeContext how can a use a generic response type?
Has someone done it before, or should I do I directly in consumer?
Seems like something that should be in the consumer itself. The cache itself could be a dependency of the consumer, which is a single instance and injected into the consumer via the constructor. That way, it would be able to check if the results are in the cache before calling the backing service with the request detail.
Hiding that in a filter seems a little specific to the message type, so within the consumer will likely be easier for developers to understand later.
I figured out a solution to integrate in-memory caching as mass transit scope filter. Currently it is only used in a mediator. Some prequists:
All messages are records (with value bases equal methods)
The query request object inherits from ApplicationQueryRequest (a record)
The query response objects inherits from ApplicationResponse (a record)
The request object has an attribute named Cached Attribute:
[AttributeUsage(AttributeTargets.Class)]
public sealed class CacheAttribute : Attribute
{
public CacheAttribute(int slidingExpireSecs = 30, int absoluteExpireSecs = 100)
{
SlidingExpireSecs = slidingExpireSecs;
AbsoluteExpireSecs = absoluteExpireSecs;
}
public int SlidingExpireSecs { get; }
public int AbsoluteExpireSecs { get; }
}
Therefore each request object can have a cache attribute like:
[Cache]
public record FooRequest
{
}
Target is that the filter automatically fetches data from the cache and stores data in it.
First initialize the mediator with all consumer and send fiters, in our case only one scope filter exists but must be added for send and consume:
services.AddMediator(
configurator =>
{
(context, cfg) =>
{
cfg.UseSendFilter(typeof(CacheScopeFilter<>), context);
cfg.UseConsumeFilter(typeof(CacheScopeFilter<>), context);
Additionally the ICacheScope must be of
services.AddScoped...
THe scope filter looks like this:
public class CacheScopeFilter<T> :
IFilter<SendContext<T>>,
IFilter<ConsumeContext<T>> where T : class
{
private readonly ILogger<T> logger;
private readonly IMemoryCache memoryCache;
private readonly ICacheScope cacheScope;
private CacheOptions cacheOptions;
public CacheScopeFilter(ILogger<T> logger, IOptionsMonitor<CacheOptions> options, IMemoryCache memoryCache, ICacheScope cacheScope)
{
this.logger = logger;
cacheOptions = options.CurrentValue;
options.OnChange(
opts =>
{
logger.LogInformation($"Set Memory Cache enabled: {opts.EnableMemoryCache}");
cacheOptions = opts;
});
this.memoryCache = memoryCache;
this.cacheScope = cacheScope;
}
public async Task Send(ConsumeContext<T> context, IPipe<ConsumeContext<T>> next)
{
var requestName = typeof(T).Name;
logger.LogInformation($"----- Start check cache application query request {requestName} {context.Message}");
cacheScope.RequestKey = null;
if (context.TryGetMessage<ApplicationQueryRequest>(out var requestContext))
{
if(!cacheOptions.EnableMemoryCache)
{
logger.LogInformation("Cache is disabled");
await next.Send(context);
return;
}
var cacheAttribute = (CacheAttribute)Attribute.GetCustomAttribute(
requestContext.Message.GetType(),
typeof(CacheAttribute));
if (cacheAttribute == null)
{
await next.Send(context);
return;
}
cacheScope.RequestKey = typeof(T).FullName + ";" + JsonConvert.SerializeObject(context.Message);
cacheScope.SlidingExpireSecs = cacheAttribute.SlidingExpireSecs;
cacheScope.AbsoluteExpireSecs = cacheAttribute.AbsoluteExpireSecs;
if (memoryCache.TryGetValue(cacheScope.RequestKey, out ApplicationResponse cacheResponse))
{
logger.LogInformation($"Take data from cache {requestName} {context.Message}, CacheKey: {cacheScope.RequestKey}");
await context.RespondAsync(cacheResponse);
return;
}
logger.LogInformation($"Data not in cache, fetching data {requestName} {context.Message}");
}
await next.Send(context);
logger.LogInformation($"----- Finish check cache application query request {requestName} {context.Message}");
}
public async Task Send(SendContext<T> context, IPipe<SendContext<T>> next)
{
var requestName = typeof(T).Name;
logger.LogInformation($"----- Start handling cache application query response {requestName} {context.Message}");
var isCachedSet = context.TryGetPayload<CacheDoneMarker>(out _);
if (context.Message is ApplicationResponse && (cacheScope.RequestKey != null) && !isCachedSet)
{
logger.LogInformation($"Cache data {requestName} {context.Message}, CacheKey: {cacheScope.RequestKey}");
var cacheEntryOptions = new MemoryCacheEntryOptions().
SetSlidingExpiration(TimeSpan.FromSeconds(cacheScope.SlidingExpireSecs)).
SetAbsoluteExpiration(TimeSpan.FromSeconds(cacheScope.AbsoluteExpireSecs));
memoryCache.Set(cacheScope.RequestKey, context.Message, cacheEntryOptions);
context.GetOrAddPayload(() => new CacheDoneMarker());
}
await next.Send(context);
logger.LogInformation($"----- Finish handling cache application query response {requestName} {context.Message}");
}
public void Probe(ProbeContext context)
{
context.CreateFilterScope("cache");
}
}
public class CacheScope : ICacheScope
{
public string RequestKey { get; set; }
public int SlidingExpireSecs { get; set; }
public int AbsoluteExpireSecs { get; set; }
}
// Scope injected !
public interface ICacheScope
{
public string RequestKey { get; set; }
public int SlidingExpireSecs { get; set; }
public int AbsoluteExpireSecs { get; set; }
}
This should also work for distributed cache, but not added yet.
I have an ASP.NET Core Web API end point which takes (FromBody) The Search object defined below
public class Search {
public int PageSize {get;set;}
public Expression Query{get;set;}
}
public class Expression {
public string Type {get;set;}
}
public class AndExpression {
public IList<Expression> Expressions {get;set;}
}
public class MatchesExpression {
public string FieldId {get;set;}
public string Value {get;set;}
public string Operator {get;set;}
}
So... if I post the following JSON to my endpoint
{ "pageSize":10, "query": { "fieldId": "body", "value": "cake", "operator": "matches" } }
I successfully get a Search Object, but the Query property is of type Expression, not MatchesExpression.
This is clearly a polymorphic issue.
This article (towards the end) gives a good example of a how to deal with this issue when your entire model is polymorphic.
https://learn.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-5.0
In my case, the property of my Model "Query" is polymorphic, so Im unsure how to build a ModelBinder for my Search object that will allow me to handle the Query Property
I Imagine, I need to write a model binder to construct the search object and then follow the pattern described for the property, however I cannot locate any examples of how to implement a model binder that isnt utterly trivial.
Any suggestions on how to achieve this? Good sources of information?
So.. I gave up with ModelBInders (because Im using the FromBody attribute which isnt compatible with my aims).
Instead I wrote a System.Text.Json JsonConvertor to handle the polymorphism (see shonky code below)
using Searchy.Models;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Searchy
{
public class ExpressionJsonConverter : JsonConverter<Expression>
{
public override Expression Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Utf8JsonReader readerClone = reader;
using (var jsonDocument = JsonDocument.ParseValue(ref readerClone))
{
if (!jsonDocument.RootElement.TryGetProperty("type", out var typeProperty))
{
throw new JsonException();
}
switch (typeProperty.GetString())
{
case "comparison":
return JsonSerializer.Deserialize<Comparison>(ref reader, options);
case "and":
return JsonSerializer.Deserialize<And>(ref reader, options);
}
}
return null;
}
public override void Write(
Utf8JsonWriter writer,
Expression expression,
JsonSerializerOptions options)
{
}
}
}
My Expression class also had the following attribue
[JsonConverter(typeof(ExpressionJsonConverter))]
I recently ran into the same issue, but was using Newtonsoft.Json, here is iasksillyquestions' solution using Newtonsoft.Json:
public class ConnectionJsonConverter : JsonConverter<Connection>
{
public override Connection? ReadJson(JsonReader reader, Type objectType, Connection? existingValue, bool hasExistingValue, JsonSerializer serializer)
{
// if this is the base type, deserialize it into the child type
if (objectType == typeof(Connection))
{
JObject parsedObject = JObject.Load(reader);
if (!parsedObject.TryGetValue("connectionType", out var connectionType))
{
throw new Exception("Unable to parse ConnectionType");
}
switch (connectionType.ToObject<ConnectionType>())
{
case ConnectionType.Database:
return parsedObject.ToObject<DatabaseConnection>();
case ConnectionType.LDAP:
return parsedObject.ToObject<LDAPConnection>();
default:
throw new Exception("Unrecognised ConnectionType");
}
}
// if it is a child type, just deserialize it normally
else
{
serializer.ContractResolver.ResolveContract(objectType).Converter = null;
return serializer.Deserialize(reader, objectType) as Connection;
}
}
public override void WriteJson(JsonWriter writer, Connection? value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
In my case I had an enum ConnectionType to specify what derived type the object was.
Connection.cs:
[JsonConverter(typeof(ConnectionJsonConverter))]
public abstract class Connection : Entity
{
#region Properties
// what type of system are we connecting to? database / api / ldap ...
public ConnectionType ConnectionType { get; set; }
// an optional description to explain where the connection is going
public string Description { get; set; } = "";
#endregion
#region Helper Methods
// test if the connection is good. Will throw an error if the connection is not good.
public abstract Task Test();
#endregion
}
LDAPConnection.cs:
public class LDAPConnection : Connection
{
#region Properties
public string Endpoint { get; set; }
public string? Username { get; set; }
public string? Password { get; set; }
#endregion
#region Helper Methods
public override async Task Test()
{
DirectoryEntry directoryEntry = new DirectoryEntry(this.Endpoint, this.Username, this.Password);
// check if the native object exists. If it does, then we are connected
if (directoryEntry.NativeObject == null)
{
throw new Exception("Unable to bind to server");
}
// assume that if we got here without any exceptions, we are connected.
}
#endregion
}
Then you can have one controller for all derived types:
// POST: /api/connections/create
[HttpPost, ActionName("Create")]
public async Task<IActionResult> Create([Bind] Connection connection)
{
connection.Test();
}
given the following json
{
"$$href": "http://localhost:8080/url1",
"name": "Sebastian Slutzky"
}
I'd like to deserialize it into an object like this one
public class DomainObject
{
[JsonProperty("$$href")]
public string href { get; set; }
public JObject this[string key] => throw new NotImplementedException();
}
so that arbitrary properties (like name can be accessed dynamically)
var href = domainObject.href;
var name = domainObject["name"] as string;
My current implementation is by passing the JObject to the constructor of my object, and decorate it (i.e. composition). Is there a way of solving this by inheritance instead (i.e. by extending JObject?
Any other solution?
You could make use of JsonExtensionData. For example
public class DomainObject
{
[JsonProperty("$$href")]
public string href { get; set; }
[JsonExtensionData]
private IDictionary<string, JToken> UnknownTypes;
public JToken this[string key] => UnknownTypes[key];
}
The Indexer now allows you to retrieve the values of dynamic properties with key as the following.
var result = JsonConvert.DeserializeObject<DomainObject>(json);
var name = result["name"].Value<string>();
I'm looking for a reliable solution to log details of requests and responses made to and from our controllers. However, some of the data passing through contains sensitive information that should not be written to a log.
In the controller, the inbound request is bound to a single model from the request body, and as the request is answered, a single model is passed to the Ok() result like this (very simplified):
[HttpGet]
[Route("Some/Route")]
public IHttpActionResult SomeController([FromBody] RequestType requestObj)
{
ResponseType responseObj = GetResponse(requestObj)
return this.Ok(responseObj);
}
Now my goal is to somehow log the contents of the request and response object at the beginning and end of the controller, respectively. What I would like to do is bind the models first, then log out their attributes. An example of the RequestType is something like:
public class RequestType
{
public string SomeAttribute { get; set; }
public string AnotherAttribute { get; set; }
public string Password{ get; set; }
}
And the log would look something like:
[date-time] Request to SomeController:
SomeAttribute: "value_from_request"
AnotherAttribute: "another_value"
Password: "supersecret123"
Now clearly we don't want the password to be logged. So I would like to create a custom data annotation that would not log certain fields. Its use would look like this (updated RequestType):
public class RequestType
{
public string SomeAttribute { get; set; }
public string AnotherAttribute { get; set; }
[SensitiveData]
public string Password{ get; set; }
}
Where would I start with this? I'm not incredibly familliar with .NET, but know that there are many sort of magic classes that can be subclassed to override some of their functionality. Is there any such class that can help here? Even better, is there any way to do this during the model binding? So we could catch errors that occur during model binding as well?
We should be able to achieve what you're looking for with an ActionFilterAttribute.
Capture Requests Attribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public sealed class CaptureRequestsAttribute : ActionFilterAttribute // *IMPORTANT* This is in the System.Web.Http.Filters namespace, not System.Web.Mvc
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
var messages = actionContext.ActionArguments.Select(arg => GetLogMessage(arg.Value));
var logMessage = $"[{DateTime.Now}] Request to " +
$"{actionContext.ControllerContext.Controller}]:\n{string.Join("\n", messages)}";
WriteToLog(logMessage);
base.OnActionExecuting(actionContext);
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
var result = actionExecutedContext.Response.Content as ObjectContent;
var message = GetLogMessage(result?.Value);
var logMessage = $"[{DateTime.Now}] Response from " +
$"{actionExecutedContext.ActionContext.ControllerContext.Controller}:\n{message}";
WriteToLog(logMessage);
base.OnActionExecuted(actionExecutedContext);
}
private static void WriteToLog(string message)
{
// todo: write you logging stuff here
}
private static string GetLogMessage(object objectToLog)
{
if (objectToLog == null)
{
return string.Empty;
}
var type = objectToLog.GetType();
var properties = type.GetProperties();
if (properties.Length == 0)
{
return $"{type}: {objectToLog}";
}
else
{
var nonSensitiveProperties = type
.GetProperties()
.Where(IsNotSensitiveData)
.Select(property => $"{property.Name}: {property.GetValue(objectToLog)}");
return string.Join("\n", nonSensitiveProperties);
}
}
private static bool IsNotSensitiveData(PropertyInfo property) =>
property.GetCustomAttributes<SensitiveDataAttribute>().Count() == 0;
}
Sensitive Data Attribute
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class SensitiveDataAttribute : Attribute
{
}
Then, you can just add it to your WebApi controller (or a specific method in it):
[CaptureRequests]
public class ValuesController : ApiController
{
// .. methods
}
And finally your models can just add the SensitiveDataAttribute:
public class TestModel
{
public string Username { get; set; }
[SensitiveData]
public string Password { get; set; }
}
This does not make use of DataAnnotations,however, One way that comes to mind would be to use the serialization. If your payload is within a reasonable size you could serialize and deserialize your RequestType class when reading and writing to/from a log. This would require a custom serialization format or making use of the default, xml.
[Seriliazeble()]
public class RequestType
{
public string SomeAttribute { get; set; }
public string AnotherAttribute { get; set; }
[NonSerialized()]
public string Password{ get; set; }
}
Using the above attribute will omit Password from serialization. Then you copuld proceed to Logger.Log(MySerializer.Serialize(MyRequest)); and your sensitive data will be omitted.
This link describes the approach in detail.
For xml serialization, simply use the XmlSerializer class.
public class MySerializationService
{
public string SerializeObject(object item)
{
XmlSerializer serializer = new XmlSerializer(item.GetType());
System.IO.MemoryStream aMemStr = new System.IO.MemoryStream();
System.Xml.XmlTextWriter writer = new System.Xml.XmlTextWriter(aMemStr, null);
serializer.Serialize(writer, item);
string strXml = System.Text.Encoding.UTF8.GetString(aMemStr.ToArray());
return strXml;
}
public object DeSerializeObject(Type objectType, string objectString)
{
object obj = null;
XmlSerializer xs = new XmlSerializer(objectType);
obj = xs.Deserialize(new StringReader(objectString));
return obj;
}
}
Then using the above or similar methods you can read and write in a custom format.
Write :
string logData=new MySerializationService().SerializeObject(myRequest);
Read :
RequestType loggedRequest= (RequestType)new MySerializationService().DeSerializeObject(new RequestType().GetType(), logData);
I have a situation where an API I'm using is returning inconsistent JSON, which I want to deserialize using JSON.NET. In one case, it returns an object that contains objects (note that the outer "1" can be any number):
{
"1":{
"0":{
"db_id":"12835424",
"title":"XXX"
},
"1":{
"db_id":"12768978",
"title":"YYY"
},
"2":{
"db_id":"12768980",
"title":"ZZZ"
},
"3":{
"db_id":"12768981",
"title":"PPP"
}
}
}
And in another case, it returns an array of objects:
{
"3":[
{
"db_id":"12769199",
"title":"XXX"
},
{
"db_id":"12769200",
"title":"YYY"
},
{
"db_id":"12769202",
"title":"ZZZ"
},
{
"db_id":"12769243",
"title":"PPP"
}
]
}
I have no idea why this inconsistency exists, but this is the format I'm working with. What would be the correct way to deserialize both formats with the JsonConvert.DeserializeObject method?
With the current version of Json.NET (Json.NET 4.5 Release 11), here is a CustomCreationConverter that will handle Json that deserializes sometimes as an object and sometimes as an array.
public class ObjectToArrayConverter<T> : CustomCreationConverter<List<T>> where T : new()
{
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
List<T> target = new List<T>();
try
{
// Load JObject from stream
JArray jArray = JArray.Load(reader);
// Populate the object properties
serializer.Populate(jArray.CreateReader(), target);
}
catch (JsonReaderException)
{
// Handle case when object is not an array...
// Load JObject from stream
JObject jObject = JObject.Load(reader);
// Create target object based on JObject
T t = new T();
// Populate the object properties
serializer.Populate(jObject.CreateReader(), t);
target.Add(t);
}
return target;
}
public override List<T> Create(Type objectType)
{
return new List<T>();
}
}
Example Usage:
[JsonObject]
public class Project
{
[JsonProperty]
public string id { get; set; }
// The Json for this property sometimes comes in as an array of task objects,
// and sometimes it is just a single task object.
[JsonProperty]
[JsonConverter(typeof(ObjectToArrayConverter<Task>))]
public List<Task> tasks{ get; set; }
}
[JsonObject]
public class Task
{
[JsonProperty]
public string name { get; set; }
[JsonProperty]
public DateTime due { get; set; }
}
I think this is something that should be possible by creating a JsonCreationConverter. This article can probably help out: http://dotnetbyexample.blogspot.nl/2012/02/json-deserialization-with-jsonnet-class.html