Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WriteAsJsonAsync throws exception when using NewtonsoftJson serializer #2735

Closed
CSharpBender opened this issue Sep 25, 2024 · 11 comments
Closed
Labels
potential-bug Items opened using the bug report template, not yet triaged and confirmed as a bug

Comments

@CSharpBender
Copy link

Description

In Azure Functions isolated model an exception is thrown when overriding the serializer from ConfigureFunctionsWebApplication() and response.WriteAsJsonAsync() is used: System.InvalidOperationException: Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead.
I followed the Microsoft guide for migrating an in-process Azure function to the isolated model which clearly states that I need to override the Json serializer in ConfigureFunctionsWebApplication and use services.AddMvc().AddNewtonsoftJson().
While searching for a fix to my issue I found issue1 and issue2

Steps to reproduce

  1. Create an Azure Function
 [Function("MyIsolatedFunction")]
        public async Task<HttpResponseData> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequestData request)
        { 
            var response = request.CreateResponse(HttpStatusCode.OK);
            await response.WriteAsJsonAsync(new { Name="Error"});
            return response;
 }
  1. Override Json serializer in Program.cs
public static class Program
{
    public static void Main(string[] args)
    {
        var host = new HostBuilder()
                .ConfigureFunctionsWebApplication((IFunctionsWorkerApplicationBuilder builder) =>
                {
                    builder.Services.Configure<WorkerOptions>(workerOptions =>
                    {
                        //<PackageReference Include="Microsoft.Azure.Core.NewtonsoftJson" Version="1.0.0" />
                        var settings = NewtonsoftJsonObjectSerializer.CreateJsonSerializerSettings();
                        settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
                        settings.NullValueHandling = NullValueHandling.Ignore;

                        workerOptions.Serializer = new NewtonsoftJsonObjectSerializer(settings);
                    });
                })
                .ConfigureServices(services =>
                {
                    services.AddApplicationInsightsTelemetryWorkerService();
                    services.ConfigureFunctionsApplicationInsights();
                    services.ConfigureSerializer();
                })
                .Build();

        host.Run();
    }
    public static IServiceCollection ConfigureSerializer(this IServiceCollection services)
    {
        services.AddMvc().AddNewtonsoftJson();
        return services;
    }
}
  1. Exception is thrown: System.InvalidOperationException: Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead.

  2. If I remove this line the exception disappears but the serialization is wrong because I'm using NewtonsoftJson attributes.
    workerOptions.Serializer = new NewtonsoftJsonObjectSerializer(settings); also this breaks the swagger definition.
    Another workaround is to serialize the object myself and use the WriteAsync method instead of WriteAsJsonAsync

@CSharpBender CSharpBender added the potential-bug Items opened using the bug report template, not yet triaged and confirmed as a bug label Sep 25, 2024
@snerte
Copy link

snerte commented Oct 1, 2024

I'm having thesame exception when using:

        public async Task<HttpResponseData> HttpStart(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequest req,
        HttpRequestData reqData,
        [DurableClient] DurableTaskClient client)
        {
            var request = await reqData.ReadFromJsonAsync<MyModel>();

A bug for sure.

@liliankasem
Copy link
Member

Hi, I wasn't able to repro the issue you reported. Could you try again with all the latest packages?

Here is what I tried which ended up working without issues (v1 of the worker):

csproj (functions version v4 | .net8)

  <ItemGroup>
    <PackageReference Include="Microsoft.Azure.Core.NewtonsoftJson" Version="2.0.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="1.23.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.2.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="1.18.1" />
  </ItemGroup>

Program.cs

    public static void Main(string[] args)
    {
        var host = new HostBuilder()
                .ConfigureFunctionsWorkerDefaults((IFunctionsWorkerApplicationBuilder builder) =>
                {
                    builder.Services.Configure<WorkerOptions>(workerOptions =>
                    {
                        var settings = NewtonsoftJsonObjectSerializer.CreateJsonSerializerSettings();
                        settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
                        settings.NullValueHandling = NullValueHandling.Ignore;

                        workerOptions.Serializer = new NewtonsoftJsonObjectSerializer(settings);
                    });
                })
                .Build();

        host.Run();
    }

MyHttpFunction.cs

        [Function("MyIsolatedFunction")]
        public async Task<HttpResponseData> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequestData request)
        {
            var response = request.CreateResponse(HttpStatusCode.OK);
            await response.WriteAsJsonAsync(new { Name="Error"});
            return response;
        }

Core Tools

Azure Functions Core Tools
Core Tools Version:       4.0.6610 Commit hash: N/A +0d55b5d7efe83d85d2b5c6e0b0a9c1b213e96256 (64-bit)
Function Runtime Version: 4.1036.1.23224

[2024-11-14T22:17:04.539Z] Found /Users/likasem/source/repros/dni-newtonsoft-exception-repro/dni-newtonsoft-exception-repro.csproj. Using for user secrets file configuration.
[2024-11-14T22:17:08.219Z] Worker process started and initialized.

Functions:

        MyIsolatedFunction: [GET,POST] http://localhost:7071/api/MyIsolatedFunction

For detailed output, run func with --verbose flag.
[2024-11-14T22:17:11.409Z] Executing 'Functions.MyIsolatedFunction' (Reason='This function was programmatically called via the host APIs.', Id=7cf54d0c-a0a9-4599-a546-7cde2d241c0c)
[2024-11-14T22:17:11.934Z] Executed 'Functions.MyIsolatedFunction' (Succeeded, Id=7cf54d0c-a0a9-4599-a546-7cde2d241c0c, Duration=551ms)
[2024-11-14T22:17:13.356Z] Host lock lease acquired by instance ID '000000000000000000000000303B82C0'.

New Model

It's also worth noting we have a new model we suggest folks update to:

csproj (functions version v4 | .net8)

  <ItemGroup>
    <FrameworkReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.Azure.Core.NewtonsoftJson" Version="2.0.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.0.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http" Version="3.2.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.0.0" />
    <PackageReference Include="Microsoft.Azure.Functions.Worker.Sdk" Version="2.0.0" />
  </ItemGroup>

Program.cs

using Azure.Core.Serialization;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;

var builder = FunctionsApplication.CreateBuilder(args);

builder.ConfigureFunctionsWebApplication();

builder.Services.Configure<WorkerOptions>(workerOptions =>
{
    var settings = NewtonsoftJsonObjectSerializer.CreateJsonSerializerSettings();
    settings.ContractResolver = new CamelCasePropertyNamesContractResolver();
    settings.NullValueHandling = NullValueHandling.Ignore;

    workerOptions.Serializer = new NewtonsoftJsonObjectSerializer(settings);
});

builder.Build().Run();

MyHttpFunction.cs

        [Function("MyHttpFunction")]
        public IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req)
        {
            _logger.LogInformation("C# HTTP trigger function processed a request.");
            return new OkObjectResult(new { Name="Error"});
        }

@CSharpBender
Copy link
Author

@liliankasem thank you for the response, unfortunately it doesn't work for me.
First version, doesn't even compile because there is a missing package: HostBuilder doesn't contain a definition for ConfigureFunctionsWorkerDefaults.
<PackageReference Include="Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore" Version="2.0.0" />

If I use the packages from the New Model then I get the exception: System.InvalidOperationException: Synchronous operations are disallowed. Call WriteAsync or set AllowSynchronousIO to true instead.
Can you attach the full project?

The New Model version doesn't use the Newtonsoft.Json serializer.
If I override the property name from the response it's ignored: name instead of responseType

      public class ResponseDto
      {
          [JsonProperty("responseType")]
          public string Name { get; set; }
      }
        [Function("MyHttpFunction")]
        public IActionResult Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req)
        {
            _logger.LogInformation("C# HTTP trigger function processed a request.");
            return new OkObjectResult(new ResponseDto { Name = "Error" });
        }

What should I change?

@liliankasem
Copy link
Member

Can you compare your project with this sample and see what is different?

https://github.com/liliankasem/dni-function-newtonsoft-sample/tree/main

@CSharpBender
Copy link
Author

@liliankasem your sample project proves that my previous statement was right.

I've made a few changes to your project: https://github.com/CSharpBender/dni-function-newtonsoft-sample

  1. Added a function that uses HttpRequestData and WriteAsJsonAsync (throws exception)
  2. Fixed Newtonsoft.Json serializer by adding services.AddMvc().AddNewtonsoftJson(); in Program.cs

@jviau
Copy link
Contributor

jviau commented Nov 23, 2024

When using AspNetCore integration, the default is to disallow synchronous IO. This default comes from AspNetCore, not functions. It also so happens the NewtonsoftJsonObjectSerializer is completely synchronous, meaning these are not compatible by default.

To enable synchronous IO, please add the following to your startup. This will enable Newtonsoft.Json usage:

services.Configure<KestrelServerOptions>(options => options.AllowSynchronousIO = true);

@CSharpBender
Copy link
Author

@jviau I'm not satisfied with your response because this is not mentioned in the Microsoft Guide
I appreciate your suggestion, but this approach is widely discussed online, and I was already aware of it. However, it didn't feel right to me, perhaps because I had too much faith in Microsoft to document such a significant issue properly.

@jviau
Copy link
Contributor

jviau commented Nov 25, 2024

@CSharpBender thank you pointing out the gap in the docs, we will add this information soon.

@ewillman
Copy link

ewillman commented Nov 27, 2024

Wait, how is this considered closed? the method is literally called WriteAsJsonAsync, and is essentially exactly what the error message is saying to do to resolve itself, but the solution to making WriteAsJsonAsync work without errors is to AllowSynchronousIO? That may be considered a workaround, but is certainly not a resolution.

@jviau
Copy link
Contributor

jviau commented Nov 27, 2024

@ewillman we do not own the NewtonsoftJsonObjectSerializer - if you have an issue with it writing synchronously even when calling async APIs (which we are), please open an issue with team/repo that owns it: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/src/NewtonsoftJsonObjectSerializer.cs.

But the reasoning is also simple: Newtonsoft.Json does not support asynchronous serialization/deserialization, so the implementation falls back to synchronous.

@jviau
Copy link
Contributor

jviau commented Nov 27, 2024

The synchronous issue also only applies to HttpRequestData and HttpResponseData. If using AspNetCore and sticking to aspnetcore bindings (using IActionResult and [FromBody] attributes, etc), then you can get Newtonsoft in an asynchronous compatible fashion via:

// need <PackageReference Include="Microsoft.Azure.Core.NewtonsoftJson" Version="2.0.0" /> for this
services.AddMvc().AddNewtonsoftJson();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
potential-bug Items opened using the bug report template, not yet triaged and confirmed as a bug
Projects
None yet
Development

No branches or pull requests

5 participants