Saturday, August 30, 2025

ASP.NET Core 10.0: Validation Support for Minimal APIs

With ASP.NET Core 10.0, we now have built in validation support for Minimal APIs for request data in following.
  • Route parameters, Query Strings
  • Header
  • Request body
If any validation fails, the runtime returns a 400 Bad Request response with details of the validation errors.

Validations are defined using attributes in the System.ComponentModel.DataAnnotations namespace. We can even create our own validators using,
To register validation services and enable validation, we need to call the following method in the Program.cs.
builder.Services.AddValidation();
Now we can do something like below to validate route parameters.
app.MapGet("/employees/{employeeId}"([Range(1, int.MaxValue)] int employeeId) =>
{
    // Omitted
});
And if we try the endpoint with an incorrect route parameter, we will get an validation error.
GET {{WebApplication1_HostAddress}}/employees/0
Accept: application/json
Route parameter validation
We can use the similar concept with record types as well.

Say I have the following Employee record that has a annotated property.
internal record Employee([Required] string Name);
And now If I try to make a request to the following endpoint,
app.MapPost("/employees"(Employee employee) =>
{
    // Omitted
});
With a empty value for name,
POST {{WebApplication1_HostAddress}}/employees
Content-Type: application/json
{
    "name": ""
}
I am getting the following 400 Bad Request.
Request Body Validation
You can disable the validation at the endpoint by calling DisableValidation(), something like below:
app.MapPost("/employees"(Employee employee) =>
    {
        // Omitted
    })
    .DisableValidation();
Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, August 27, 2025

Azure Logic Apps (Consumption): HTTP Action to POST multipart/form-data with Files and Fields

In this post, let's see how we can POST multipart/form-data with files and fields using a HTTP action in a Consumption Azure Logic App.

I have the following Test API which I am going to call from the Logic App.
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

WebApplication app = builder.Build();

app.UseHttpsRedirection();

app.MapPost("/api/Files"async (IFormCollection formCollection) =>
{
    IFormFilefile = formCollection.Files.SingleOrDefault();

    if (file == null || file.Length == 0)
    {
        return Results.BadRequest("No file uploaded.");
    }

    string fileName = file.FileName;
// Save file and ensure file is good
    string filePath = Path.Combine(@"<some-location>"fileName);
    using FileStream stream = File.Create(filePath);
    await file.CopyToAsync(stream);

    return Results.Ok(new
    {
        fileName,
        fileSize = file.Length,
        someField = formCollection["someField"].ToString()
    });
})
.DisableAntiforgery();

app.Run();
In my Logic App, I have a variable called file  of type object and it's populated with data. 
{
  "fileName""<OMITTED>", // some-file.pdf
  "base64Content""<OMITTED>", // Base 64 encoded content
  "contentType""<OMITTED>" // application/pdf
}
And now let's add the HTTP action as follows:
HTTP Action
Code for Body is below.
{
  "$content-type""multipart/form-data",
  "$multipart": [
    {
      "headers": {
        "Content-Disposition""form-data; name=\"file\"; filename=\"@{variables('file')?['fileName']}\""
      },
      "body": {
        "$content""@{variables('file')?['base64Content']}",
        "$content-type""@{variables('file')?['contentType']}"
      }
    },
    {
      "headers": {
        "Content-Disposition""form-data; name=\"someField\""
      },
      "body""Hello World!"
    }
  ]
}
And now when the HTTP action is executed, I can see the values are getting passed correctly.
API Endpoint
Hope this helps.

Happy Coding.

Regards,
Jaliya

Wednesday, August 20, 2025

Azure Functions HTTP Orchestration Trigger with multipart/form-data

In this post, let's see have how we can invoke a HTTP Orchestration Trigger in an Azure Durable Functions with multipart/form-data. There can be scenarios where you want to pass multipart/form-data (mostly files) to HTTP Orchestration Trigger.

Note I am using Azure Functions Isolated model.

If you scaffold a a Durable Function in Visual Studio, you will see it's HTTP Trigger function to be something like below.
[Function("Function_HttpStart")]
public static async Task<HttpResponseData> HttpStart(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get""post")] HttpRequestData req,
    [DurableClient] DurableTaskClient client,
    FunctionContext executionContext)
{
    string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(Function));
        
    // Omitted for brevity.
        
    return await client.CreateCheckStatusResponseAsync(reqinstanceId);
}
Notice that it uses HttpRequestData for request and for HttpResponseData. which is available via,
Microsoft.Azure.Functions.Worker.Extensions.Http
Instead of these, we can make use of Azure Functions ASP.NET Core integration and start using ASP.NET Core Request/Response types including HttpRequestHttpResponse and IActionResult in HTTP Triggers.

Azure Functions ASP.NET Core integration is available via following NuGet package.
Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore
And then in the Program.cs,
FunctionsApplicationBuilder builder = FunctionsApplication.CreateBuilder(args);

// Enable ASP.NET Core features in Azure Functions
builder.ConfigureFunctionsWebApplication();
// Omitted for brevity
And now we can change the HTTP trigger as follows.
[Function("Function_HttpStart")]
public static async Task<IActionResult> HttpStart(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get""post")] HttpRequest req,
    [DurableClient] DurableTaskClient client,
    FunctionContext executionContext)
{
    string instanceId = await client.ScheduleNewOrchestrationInstanceAsync(nameof(Function));

    if (req.HasFormContentType)
    {
        IFormCollection formCollection = await req.ReadFormAsync();
        // TODO: Process form data as needed.
    }

    // Omitted for brevity.

    HttpManagementPayload httpManagementPayload = client.CreateHttpManagementPayload(instanceId);
    return new ObjectResult(httpManagementPayload)
    {
        StatusCode = (int)HttpStatusCode.Accepted
    };
}
By the way, if you want to use multipart/form-data in any of the Azure Functions HTTP Triggers, Azure Functions ASP.NET Core integration is the way. 

Hope this helps.

Happy Coding.

Regards,
Jaliya

Saturday, August 9, 2025

Azure Automation Runbooks and Azure Cosmos DB for MongoDB

I recently wanted to run a daily job on an Azure Cosmos DB for MongoDB. I thought Logic Apps would be a good fit, but surprisingly there is still a no connector that supports Azure Cosmos DB for MongoDB (currently only supports Azure Cosmos DB for NoSQL), and that's a bummer.

But there are of course other approaches we can take, like Azure Automation Runbooks. 

In this post, let's see how we can create an Azure Automation Python Runbook to query Azure Cosmos DB for MongoDB.

I have an Azure Automation account created and I have created a Python 3.10 Runbook. Now in order to connect to MongoDB, I am going to use pymongo package. 

First let's add the package to Automation Account. Note: For Python 3.10 packages, only .whl files targeting cp310 Linux OS are currently supported.

I am downloading the pymongo package to my local computer.

pip download pymongo `
    --platform manylinux2014_x86_64 `
    --only-binary=:all: `
    --python-version 3.10

Upon completion of above command, I can see 2 .whl files, pymongo and a dependency.

.whl files
Then I am uploading both these .whl files to Python packages under Automation Account.

Add Python packages

Now I can run some python code to query my Azure Cosmos DB for MongoDB.

from pymongo import MongoClient

MONGODB_CONNECTIONSTRING = "mongodb://..."
DATABASE_NAME = "<Database_Name>"
COLLECTION_NAME = "<Collection_Name>"

client = MongoClient(MONGODB_CONNECTIONSTRING, ssl=True)
db = client[DATABASE_NAME]
collection = db[COLLECTION_NAME]

documents = collection.find(
    {
        # query to filter documents
    })

documents = list(documents)

client.close()
# TODO: Work with documents

Hope this helps.

Happy Coding.

Regards,
Jaliya