1

Background problem:

I have a document POCO that I would like to use with the CosmosDbOutput binding extension (latest, v4).

I prefer to have my POCO property names in PascaLCase, and my Cosmos / Json documents in camelCase respectively. However, I cannot find the correct way to modify the serialization options to get this to work.

using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using System.Net;
using FromBodyAttribute = Microsoft.Azure.Functions.Worker.Http.FromBodyAttribute;

namespace FunctionCrudSample;

// Function
    [Function(nameof(CreatePascalCase))]
    public async Task<CreateResponsePascalCase> CreatePascalCase(
        [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req,
        [FromBody] MyDocumentPascalCase myDocument,
        FunctionContext executionContext
        )
    {
        var logger = executionContext.GetLogger(nameof(Api));
        logger.LogInformation("C# HTTP trigger function processed a request.");
        var response = req.CreateResponse(HttpStatusCode.OK);
        await response.WriteAsJsonAsync(myDocument);
        return new() { Response = response, Document = myDocument };
    }

// Multi-Return type
    public class CreateResponsePascalCase
    {
        [HttpResult]
        public required HttpResponseData Response { get; set; }

        [CosmosDBOutput(cosmosDbName, cosmosContainerName, Connection = connectionString, CreateIfNotExists = true, PartitionKey = "/id")]
        public MyDocumentPascalCase? Document { get; set; }
    }

// POCO
    public class MyDocumentPascalCase
    {
        public string? Id { get; set; }
        public string? Message { get; set; }
    }

What I've tried

1. Modify the POCO to use camelCase property names

This works, but again. I would prefer to use PascalCase. Not to mention using lower case property names violates code style IDE1006.

public class MyDocumentCamelCase
{
    public string? id { get; set; }
    public string? message { get; set; }
}

2. Configuring IOptions<WorkerOptions> and ConfigureCosmosDBExtensionOptions()

I found no success with either, or both of these configuration options in the Program.cs

Not to mention they are undocumented. I spent a few hours in the dotnet worker runtime repository.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;

var host = new HostBuilder().ConfigureFunctionsWorkerDefaults(options =>
{
    options.Services.AddOptions<CosmosDBExtensionOptions>().Configure<IOptions<WorkerOptions>>((cosmos, worker) =>
    {
        cosmos.ClientOptions.SerializerOptions.PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase;
    });

    options.ConfigureCosmosDBExtensionOptions(options =>
    {
        options.ClientOptions.SerializerOptions.PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase;
    });

}).Build();

await host.RunAsync();

Error details

[2025-01-06T00:12:14.733Z] Executing 'Functions.CreatePascalCase' (Reason='This function was programmatically called via the host APIs.', Id=e3502c80-8f5d-45f8-8060-3f79526d959a)
[2025-01-06T00:12:14.835Z] C# HTTP trigger function processed a request.
[2025-01-06T00:12:15.542Z] Host lock lease acquired by instance ID '000000000000000000000000451B1ABA'.
[2025-01-06T00:12:22.899Z] Executed 'Functions.CreatePascalCase' (Failed, Id=e3502c80-8f5d-45f8-8060-3f79526d959a, Duration=8170ms)
[2025-01-06T00:12:22.900Z] System.Private.CoreLib: Exception while executing function: Functions.CreatePascalCase. Microsoft.Azure.Cosmos.Client: Response status code does not indicate success: BadRequest (400); Substatus: 0; ActivityId: 8ba46106-33ab-444c-9706-234f1c626bd4; Reason: ({"code":"BadRequest","message":"Message: {\"Errors\":[\"One of the specified inputs is invalid\"]}\r\nActivityId: 8ba46106-33ab-444c-9706-234f1c626bd4, Request URI: /apps/DocDbApp/services/DocDbServer1/partitions/a4cb494d-38c8-11e6-8106-8cdcd42c33be/replicas/1p/, RequestStats: \r\nRequestStartTime: 2025-01-06T00:12:22.8309413Z, RequestEndTime: 2025-01-06T00:12:22.8326386Z,  Number of regions attempted:1\r\n{\"systemHistory\":[{\"dateUtc\":\"2025-01-06T00:11:28.7225106Z\",\"cpu\":0.400,\"memory\":7185016.000,\"threadInfo\":{\"isThreadStarving\":\"False\",\"threadWaitIntervalInMs\":0.1436,\"availableThreads\":32766,\"minThreads\":16,\"maxThreads\":32767},\"numberOfOpenTcpConnection\":1},{\"dateUtc\":\"2025-01-06T00:11:38.7228784Z\",\"cpu\":0.303,\"memory\":7172964.000,\"threadInfo\":{\"isThreadStarving\":\"False\",\"threadWaitIntervalInMs\":0.1101,\"availableThreads\":32766,\"minThreads\":16,\"maxThreads\":32767},\"numberOfOpenTcpConnection\":1},{\"dateUtc\":\"2025-01-06T00:11:48.7368240Z\",\"cpu\":0.361,\"memory\":7234704.000,\"threadInfo\":{\"isThreadStarving\":\"False\",\"threadWaitIntervalInMs\":0.0365,\"availableThreads\":32766,\"minThreads\":16,\"maxThreads\":32767},\"numberOfOpenTcpConnection\":1},{\"dateUtc\":\"2025-01-06T00:11:58.7376400Z\",\"cpu\":0.557,\"memory\":7209968.000,\"threadInfo\":{\"isThreadStarving\":\"False\",\"threadWaitIntervalInMs\":0.1008,\"availableThreads\":32766,\"minThreads\":16,\"maxThreads\":32767},\"numberOfOpenTcpConnection\":1},{\"dateUtc\":\"2025-01-06T00:12:08.7520453Z\",\"cpu\":4.037,\"memory\":6609924.000,\"threadInfo\":{\"isThreadStarving\":\"False\",\"threadWaitIntervalInMs\":0.0644,\"availableThreads\":32766,\"minThreads\":16,\"maxThreads\":32767},\"numberOfOpenTcpConnection\":1},{\"dateUtc\":\"2025-01-06T00:12:18.7587838Z\",\"cpu\":3.106,\"memory\":6173820.000,\"threadInfo\":{\"isThreadStarving\":\"False\",\"threadWaitIntervalInMs\":0.0562,\"availableThreads\":32765,\"minThreads\":16,\"maxThreads\":32767},\"numberOfOpenTcpConnection\":1}]}\r\nRequestStart: 2025-01-06T00:12:22.8311613Z; ResponseTime: 2025-01-06T00:12:22.8326386Z; StoreResult: StorePhysicalAddress: rntbd://127.0.0.1:10253/apps/DocDbApp/services/DocDbServer1/partitions/a4cb494d-38c8-11e6-8106-8cdcd42c33be/replicas/1p/, LSN: 30, GlobalCommittedLsn: -1, PartitionKeyRangeId: 0, IsValid: True, StatusCode: 400, SubStatusCode: 0, RequestCharge: 0, ItemLSN: -1, SessionToken: -1#30, UsingLocalLSN: False, TransportException: null, BELatencyMs: 0.297, ActivityId: 8ba46106-33ab-444c-9706-234f1c626bd4, RetryAfterInMs: , ReplicaHealthStatuses: [(port: 10253 | status: Connected | lkt: 1/4/2025 6:59:48 PM)], TransportRequestTimeline: {\"requestTimeline\":[{\"event\": \"Created\", \"startTimeUtc\": \"2025-01-06T00:12:22.8311628Z\", \"durationInMs\": 0.0208},{\"event\": \"ChannelAcquisitionStarted\", \"startTimeUtc\": \"2025-01-06T00:12:22.8311836Z\", \"durationInMs\": 0.0031},{\"event\": \"Pipelined\", \"startTimeUtc\": \"2025-01-06T00:12:22.8311867Z\", \"durationInMs\": 0.1507},{\"event\": \"Transit Time\", \"startTimeUtc\": \"2025-01-06T00:12:22.8313374Z\", \"durationInMs\": 0.7515},{\"event\": \"Received\", \"startTimeUtc\": \"2025-01-06T00:12:22.8320889Z\", \"durationInMs\": 0.0735},{\"event\": \"Completed\", \"startTimeUtc\": \"2025-01-06T00:12:22.8321624Z\", \"durationInMs\": 0}],\"serviceEndpointStats\":{\"inflightRequests\":1,\"openConnections\":1},\"connectionStats\":{\"waitforConnectionInit\":\"False\",\"callsPendingReceive\":0,\"lastSendAttempt\":\"2025-01-06T00:12:22.7832281Z\",\"lastSend\":\"2025-01-06T00:12:22.7832850Z\",\"lastReceive\":\"2025-01-06T00:12:22.7847877Z\"},\"requestSizeInBytes\":545,\"requestBodySizeInBytes\":40,\"responseMetadataSizeInBytes\":183,\"responseBodySizeInBytes\":53};\r\n ResourceType: Document, OperationType: Upsert\r\n, SDK: Microsoft.Azure.Documents.Common/2.14.0"}

Reproducible sample

https://github.com/EntityAdam/FunctionCrudSample/tree/features/serialization-problem-repro

Related, but not relevant to v4 isolated functions

6
  • Why are you not using direct serilization? Commented Jan 6 at 3:32
  • What is direct serialization? Why should I use that, instead of what I'm doing now? Commented Jan 6 at 4:16
  • Use this var rith = new HostBuilder() .ConfigureFunctionsWebApplication() .ConfigureServices(cho => { cho.Configure<WorkerOptions>(ritcho => { ritcho.Serializer = new JsonObjectSerializer( new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }); }); }) .Build(); Commented Jan 6 at 4:24
  • Did it work @AdamVincent? Commented Jan 6 at 6:18
  • sure did, thanks friend. Commented Jan 6 at 12:31

1 Answer 1

2

I have a document POCO that I would like to use with the CosmosDbOutput binding extension (latest, v4).I prefer to have my POCO property names in PascaLCase, and my Cosmos / Json documents in camelCase respectively. However, I cannot find the correct way to modify the serialization options to get this to work.

Below code worked for me:

Program.cs:

using Microsoft.Extensions.Hosting;
using Microsoft.Azure.Functions.Worker;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Azure.Core.Serialization;

var rith = new HostBuilder()
    .ConfigureFunctionsWebApplication()
    .ConfigureServices(cho =>
    {
        cho.Configure<WorkerOptions>(bo =>
        {
            bo.Serializer = new JsonObjectSerializer(
                new JsonSerializerOptions
                {
                    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
                    DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
                });
        });
    })
    .Build();

rith.Run();

Function.cs:

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
using System.Net;

namespace FunctionApp20
{
    public class Function1
    {
        private readonly ILogger<Function1> ri_lg;

        public Function1(ILogger<Function1> lg)
        {
            ri_lg = lg;
        }

        [Function("Function1")]
        public async Task<RithOut> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "post")] HttpRequestData req,
            FunctionContext executionContext
        )
        {
            ri_lg.LogInformation("Hello Bojja");

            var myDocument = new RithPas
            {
                Id = "008",  
                Message = "Rithwik Bojja"
            };

            var response = req.CreateResponse(HttpStatusCode.OK);
            return new RithOut { Response = response, Document = myDocument };
        }
    }

    public class RithOut
    {
        [HttpResult]
        public required HttpResponseData Response { get; set; }

        [CosmosDBOutput("Testdb", "testcon", Connection = "test", CreateIfNotExists = true, PartitionKey = "/id")]
        public RithPas? Document { get; set; }
    }

    public class RithPas
    {
        public string? Id { get; set; }
        public string? Message { get; set; }
    }
}

Output: enter image description here

enter image description here

enter image description here

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.