Showing posts with label ASP.NET Core. Show all posts
Showing posts with label ASP.NET Core. Show all posts

Friday, January 9, 2026

.NET Isolated Azure Functions: Multiple Output Bindings with HTTP Functions

In this post, let's look at how we can use multiple output bindings with HTTP-triggered Azure Functions running on the isolated worker model.

When working with HTTP-triggered functions, you often need to return an HTTP response to the caller while also sending data to other services like Azure Service Bus, Azure Queue Storage, etc. This is where multiple output bindings come in handy.

HttpRequestData vs HttpRequest

With Azure Functions isolated worker model, by default we have HttpRequestData and HttpResponseData types from the Microsoft.Azure.Functions.Worker.Extensions.Http package.

However, we can also use ASP.NET Core integration via the Microsoft.Azure.Functions.Worker.Extensions.Http.AspNetCore NuGet package. This enables using standard ASP.NET Core types including HttpRequestHttpResponse, and IActionResult in HTTP Triggers.

Let's see how multiple output bindings work with both approaches.

To have multiple outputs, we need to create a class that contains our output binding properties.

Using HttpRequestData

using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using System.Net;

namespace FunctionApp1;

public class HttpStartWithHttpRequestData
{
    [Function(nameof(HttpStartWithHttpRequestData))]
    public async Task<HttpStartOutputWithHttpResponseDataRun(
        [HttpTrigger(AuthorizationLevel.Function, "get""post")] HttpRequestData req)
    {
        HttpResponseData response = req.CreateResponse(HttpStatusCode.OK);
        await response.WriteStringAsync("Message Sent");

        return new HttpStartOutputWithHttpResponseData()
        {
            ServiceBusMessageContent = "Some Content",
            HttpResponse = response
        };
    }
}

public class HttpStartOutputWithHttpResponseData
{
    [ServiceBusOutput("sbt-test-topic"ServiceBusEntityType.Topic, Connection = "ServiceBusConnection")]
    public string? ServiceBusMessageContent { getset; }

    public HttpResponseData HttpResponse { getset; }
}

With HttpRequestData, the HttpResponseData property doesn't require any special attribute. The function runtime automatically recognizes it as the HTTP response.

Note: The ServiceBusMessageContent property is nullable. If the value isn't set (i.e., it's null), no message will be sent to the Service Bus. This allows you to conditionally send messages based on your business logic.

Using HttpRequest (ASP.NET Core Integration)

When using HttpRequest from the ASP.NET Core integration, things are slightly different. We need to use the [HttpResult] attribute to indicate which property is the HTTP response.

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

namespace FunctionApp1;

public class HttpStartWithHttpRequest
{
    [Function(nameof(HttpStartWithHttpRequest))]
    public HttpStartOutputWithHttpResponse Run(
        [HttpTrigger(AuthorizationLevel.Function, "get""post")] HttpRequest req)
    {
        return new HttpStartOutputWithHttpResponse()
        {
            ServiceBusMessageContent = "Some Content",
            HttpResponse = new ObjectResult("Message Sent")
            {
                StatusCode = (int)HttpStatusCode.OK
            }
        };
    }
}

public class HttpStartOutputWithHttpResponse
{
    [ServiceBusOutput("sbt-test-topic"ServiceBusEntityType.Topic, Connection = "ServiceBusConnection")]
    public string? ServiceBusMessageContent { getset; }

    [HttpResult]
    public IActionResult HttpResponse { getset; }
}

The [HttpResult] attribute is required here because without it, the runtime won't know which property represents the HTTP response.

Note: Similar to the previous example, the ServiceBusMessageContent property is nullable. If the value isn't set, no message will be sent to the Service Bus.

Limitations

There's a significant limitation when it comes to Service Bus output bindings in the isolated worker model. The content type has to be the message body itself - it can be a simple type (like string) or a complex type (a POCO).

You cannot use ServiceBusMessage from the Azure.Messaging.ServiceBus SDK as the output type. If you try to do so, the entire ServiceBusMessage object gets serialized as JSON, resulting in something like this:

{
  "Body""eyJDb250ZW50IjoiU29tZSBDb250ZW50In0=",
  "MessageId"null,
  "PartitionKey"null,
  "SessionId"null,
  "TimeToLive""10675199.02:48:05.4775807",
  "CorrelationId"null,
  "Subject"null,
  "ContentType"null,
  "ApplicationProperties": {}
}

This means you cannot set message properties like CorrelationIdSessionIdSubject, or ApplicationProperties using output bindings in the isolated worker model. If you need to set these properties, you'll have to use the ServiceBusClient directly.

This is a known limitation and has been discussed in several GitHub issues:

Hope this helps.

Happy Coding.

Regards,
Jaliya

Friday, January 2, 2026

Sending Emails using Azure Communication Services in .NET

In this post, let's see how to send emails using Azure Communication Services (ACS) in a .NET application. With SendGrid's free tier being discontinued, ACS has become an attractive alternative for sending transactional emails from Azure-hosted applications.

First, you need to create two resources in Azure:
  1. Email Communication Services - This handles email domain configuration
  2. Communication Services - This is the main resource your application connects to
Creating Email Communication Services

In the Azure Portal, search for Email Communication Services and create a new resource. Once created, you need to add a domain. You have two options:
  1. Azure Managed Domain: Quick setup, gives you a subdomain like DoNotReply@xxxxxxxx.azurecomm.net 
  2. Custom Domain: Use your own domain like noreply@yourdomain.com

For a custom domain, you'll need to add following DNS records for verification, Azure will provide the values.

  • TXT: Domain ownership verification
  • TXT: SPF (Sender Policy Framework)
  • CNAME: DKIM key 1
  • CNAME: DKIM key 2

You can do this by clicking on Provision domains -> Custom domain and following the steps.

Email Communication Services: Add Custom Domain

Note: If you have an existing SPF record (e.g. from email forwarding), you need to merge them. You can only have one SPF record per domain:
v=spf1 include:spf.protection.outlook.com include:other.service.com -all
After domain verification, add a MailFrom address under your domain. 
Email Communication Services Domain: MainFrom addresses

Linking Domain to Communication Services

Create a
Communication Services resource, then navigate to Email -> Domains -> Connect domains and link your verified domain.
Communication Services: Link Email Domain
Implementing in .NET

First we need install the following NuGet Package.
dotnet add package Azure.Communication.Email

Add the following to your appsettings.json:

{
  "Email": {
    "Endpoint""https://<your-acs-resource>.communication.azure.com",
    "SenderAddress""<configured_mailfrom_address>"   }
}

We can define an Options Class to map Email settings.

namespace YourApp.Options;

public record Email
{
    public required string Endpoint { getinit; }

    public required string SenderAddress { getinit; }
}

Now let's register the Email options, EmailClient and an EmailService.

// In Program.cs

builder.Services.AddOptions<Email>()
    .Bind(builder.Configuration.GetSection("Email"));

builder.Services.AddSingleton<EmailClient>(sp =>
{
    var emailOptions = sp.GetRequiredService<IOptions<Email>>();
    var credential = new DefaultAzureCredential();

    return new EmailClient(new Uri(emailOptions.Value.Endpoint), credential);
});

builder.Services.AddScoped<EmailService>();

Now let's create a simple EmailService.

using Azure.Communication.Email;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace YourApp.Services;

public class EmailService
{
    private readonly EmailClient _emailClient;
    private readonly Email _emailOptions;
    private readonly ILogger<EmailService> _logger;

    public EmailService(
        EmailClient emailClient,
        IOptions<EmailemailOptions,
        ILogger<EmailServicelogger)
    {
        _emailClient = emailClient;
        _emailOptions = emailOptions.Value;
        _logger = logger;
    }

    public async Task SendEmailAsync(
        string recipientEmail,
        string subject,
        string htmlContent,
        string plainTextContent,
        CancellationToken cancellationToken = default)
    {
        var emailMessage = new EmailMessage(
            senderAddress: _emailOptions.SenderAddress,
            recipientAddressrecipientEmail,
            contentnew EmailContent(subject)
            {
                Html = htmlContent,
                PlainText = plainTextContent
            });

        try
        {
            EmailSendOperation operation = await _emailClient.SendAsync(
                Azure.WaitUntil.Completed,
                emailMessage,
                cancellationToken);

            _logger.LogInformation(
                "Email sent successfully. MessageId: {MessageId}",
                operation.Id);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex"Failed to send email");
            throw;
        }
    }
}

Managed Identity RBAC Role

When using DefaultAzureCredential with an Managed Identity in Production, you must assign the Communication and Email Service Owner RBAC role to your identity on the Communication Services resource.

az role assignment create `
  --assignee <managed-identity-principal-id> `
  --role "Communication and Email Service Owner" `
  --scope /subscriptions/<subscriptionId>/resourceGroups/<resourceGroup>/providers/Microsoft.Communication/CommunicationServices/<acsName>

Once email is sent:

Received Email

Hope this helps.

More read:
   Azure Communication Services Email Overview
   Quickstart: Send Email
   Email Pricing

Happy Coding.

Regards,
Jaliya

Thursday, September 4, 2025

ASP.NET Core 10.0: Custom Validation Support for Minimal APIs

In a previous post, I wrote about ASP.NET Core 10.0: Validation Support for Minimal APIs. In this post, let's go a bit further and see how we can implement custom validations using both ValidationAttribute implementations and implementing the IValidatableObject interface.

ValidationAttribute 


With ValidationAttribute, we can create a Custom attribute with our own custom logic.
public class CustomEmptyValidationAttribute : ValidationAttribute
{
    protected override ValidationResultIsValid(objectvalueValidationContext _)
    {
        if (value is string str && string.IsNullOrEmpty(str))
        {
            return new ValidationResult("Value cannot be null or empty.");
        }

        return ValidationResult.Success;
    }
}
And then we can apply the attribute, something like below for an example.
internal record Employee([CustomEmptyValidation] string Name);

IValidatableObject 


A class/record can implement IValidatableObject and add the validation logic. The validation will kick in as part of model binding.
internal record Employee : IValidatableObject
{
    [Range(1, int.MaxValue)]
    public int Id { getset}

    public string Name { getset}

    public IEnumerable<ValidationResult> Validate(ValidationContext _)
    {
        if (string.IsNullOrEmpty(Name))
        {
            yield return new ValidationResult("Name cannot be null or empty."[nameof(Name)]);
        }
    }
}
Note: Currently there is a bug where IValidatableObject wouldn't trigger validation when there is no validation attribute on a property. (aspnetcore/issues/63394: ASP.NET Core 10.0: Built-in Validation with IValidatableObject)

Hope this helps.

Happy Coding.

Regards,
Jaliya

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, June 25, 2025

ASP.NET Core in .NET 10 Preview 4: JSON Patch with System.Text.Json

With .NET 10 Preview 4 onwards, you can now use JsonPatch with System.Text.Json in ASP.NET Web API. 

Currently if you need to use JsonPatch you need to rely on Newtonsoft.Json as described in this article: JsonPatch in ASP.NET Core web API

Note: this isn't a complete drop-in replacement for the existing Newtonsoft.Json based implementation. In particular, the new implementation doesn't support dynamic types (like ExpandoObject).

Now let's see how this works.

First you need to install a new package, Microsoft.AspNetCore.JsonPatch.SystemTextJson. Note: it's still prerelease.

dotnet add package Microsoft.AspNetCore.JsonPatch.SystemTextJson --prerelease

Next, we can use the JsonPatchDocument<TModel>, that is introduced in Microsoft.AspNetCore.JsonPatch.SystemTextJson.

using Microsoft.AspNetCore.JsonPatch.SystemTextJson;
using Microsoft.AspNetCore.Mvc;
namespace WebApplication1.Controllers;
[ApiController]
[Route("[controller]")]
public class EmployeesController : ControllerBase
{
    [HttpPatch]
    [Route("{employeeId}")]
    public Employee PatchEmployee([FromRoute] int employeeId
        JsonPatchDocument<Employee> patchDocument)
    {
        Employee employee = new()
        {
            Id = employeeId,
            FirstName = "John",
            LastName = "Doe",
            Address = new Employee.AddressDto
            {
                Street = "123 Main St",
                City = "Redmond",
                State = "WA",
                ZipCode = "12345"
            }
        };
        patchDocument.ApplyTo(employee);
        return employee;
    }
}

You don't have to do any changes to the Program.cs.

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

WebApplication app = builder.Build();

app.MapControllers();

app.Run();

And invoke the endpoint like follows.

@WebApplication1_HostAddress = https://localhost:7070
PATCH {{WebApplication1_HostAddress}}/employees/1
Content-Type: application/json
[
    {
        "op": "replace",
        "path": "/LastName",
        "value": "Bloggs"
    },
    {
        "op": "replace",
        "path": "/Address/ZipCode",
        "value": "90100"
    }
]

PATCH
Hope this helps.

Happy Coding.

Regards,
Jaliya

Tuesday, May 6, 2025

ASP.NET Core 9.x: Support for Polymorphic Types with [FromForm]

I recently wanted to have an ASP.NET Core Web API endpoint, that accepts data via [FromForm] and the payload contains a Polymorphic Type.

For an example consider the following POCO types that the endpoint expects via [FromForm].

[JsonDerivedType(typeof(ClassificationOptions)nameof(ClassificationOptions))]
[JsonDerivedType(typeof(ExtractionOptions)nameof(ExtractionOptions))]
public class AnalyzeOptions
{
    public string? CommonOption1 { getset} = null;
}

public class ClassificationOptions : AnalyzeOptions
{
    public stringClassificationOption1 { getset} = null;
}

public class ExtractionOptions : AnalyzeOptions
{
    public string? ExtractionOption1 { getset} = null;
}

public class AnalyzeRequestModel
{
    public IFormFileCollection Files { getset}

    public AnalyzeOptions Options { getset}
}

And the endpoint looks like below:

[ApiController]
[Route("[controller]")]
public class ValuesController : ControllerBase
{
    [HttpPost("FromForm", Name = "PostFromForm")]
    public IActionResult PostFromForm([FromForm] AnalyzeRequestModel analyzeRequestModel)
    {
        // TODO: Process the files and options

        return Ok(analyzeRequestModel);
    }
}

And I was sending data as below, hoping System.Text.Json will kick in and do the deserialization as expected.

Postman
Not deserialized

But it seems [FromForm] model binding isn't honoring System.Text.Json support for Polymorphic Types which is working perfectly fine with [FromBody].

So in order to get this working, we can write a custom model binder for AnalyzeOptions.

public class AnalyzeOptionsModelBinder : IModelBinder
{
    private Dictionary<Type(ModelMetadataIModelBinder)> binders;

    public AnalyzeOptionsModelBinder(Dictionary<Type(ModelMetadataIModelBinder)> binders)
    {
        this.binders = binders;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        string modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, "$type");
        stringmodelTypeValue = bindingContext.ValueProvider.GetValue(modelKindName).FirstValue;

        IModelBinder modelBinder;
        ModelMetadata modelMetadata;
        if (modelTypeValue == nameof(ClassificationOptions))
        {
            (modelMetadatamodelBinder) = binders[typeof(ClassificationOptions)];
        }
        else if (modelTypeValue == nameof(ExtractionOptions))
        {
            (modelMetadatamodelBinder) = binders[typeof(ExtractionOptions)];
        }
        else
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return;
        }

        ModelBindingContext newBindingContext = DefaultModelBindingContext.CreateBindingContext(
            bindingContext.ActionContext,
            bindingContext.ValueProvider,
            modelMetadata,
            bindingInfonull,
            bindingContext.ModelName);

        await modelBinder.BindModelAsync(newBindingContext);
        bindingContext.Result = newBindingContext.Result;

        if (newBindingContext.Result.IsModelSet)
        {
            // Setting the ValidationState ensures properties on derived types are correctly 
            bindingContext.ValidationState[newBindingContext.Result.Model] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }
}
public class AnalyzeOptionsModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType != typeof(AnalyzeOptions))
        {
            return null;
        }

        Type[] subclasses = [typeof(ClassificationOptions)typeof(ExtractionOptions),];

        var binders = new Dictionary<Type(ModelMetadataIModelBinder)>();
        foreach (Type type in subclasses)
        {
            ModelMetadata modelMetadata = context.MetadataProvider.GetMetadataForType(type);
            binders[type] = (modelMetadatacontext.CreateBinder(modelMetadata));
        }

        return new AnalyzeOptionsModelBinder(binders);
    }
}

And then register this custom ModelBinderProvider.

builder.Services.AddControllers(options =>
{
    // Add the custom model binder to support [FromForm] with Polymorphic types
    options.ModelBinderProviders.Insert(0, new AnalyzeOptionsModelBinderProvider());
});

And now if I execute the request again,

Custom Model Binder
And that works.

Raised an issue in ASP.NET Core repo,
   [FromForm]: Support for Polymorphic Types #61378

Hope this helps.

Happy Coding.

Regards,
Jaliya