Search code examples
c#azure-functionsazure-durable-functionsjsonconverter

Azure Function Json Serialization issue


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?


Solution

  • 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.