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

Thursday, August 20, 2026

Use of AZURE_TOKEN_CREDENTIALS in Azure.Identity

In this post, let's have a look at the AZURE_TOKEN_CREDENTIALS environment variable in Azure.Identity. I only came across it recently, and it has removed something that used to annoy me every single day.

We all know why we should be authenticating to Azure services with Microsoft Entra ID rather than with keys, connection strings or passwords. 
TokenCredential credential = new DefaultAzureCredential();
When I am working on something locally, what I used to do is grant my own Azure account access to the development resource, sign in through Visual Studio or the Azure CLI, and let DefaultAzureCredential pick that identity up. 

But this can be a pain when running locally. DefaultAzureCredential is a chain. It attempts credentials one after the other, in order, and the first one that returns a token wins. The deployed service credentials (EnvironmentCredential, WorkloadIdentityCredential, ManagedIdentityCredential) come first and then the developer tool credentials like VisualStudioCredential, AzureCliCredential, AzureDeveloperCliCredential etc.

In our local machine, deployed credentials obviously won't work, but still it will get tried. ManagedIdentityCredential is the worst among them, because it actually goes out to the Azure Instance Metadata Service (IMDS) endpoint at 169.254.169.254 and only gives up once that socket fails or times out. So every run and every debug session pays for these guaranteed failures.

Let's look by an example. I am using latest Azure.Identity package as of today.
<PackageReference Include="Azure.Identity" Version="1.21.0" />
Here is the basic code.
using Azure.Core;
using Azure.Core.Diagnostics;
using Azure.Identity;
using System.Diagnostics.Tracing;

using AzureEventSourceListener listener = new((eventArgs, message) =>
{
    // Helper method to print out the provider
    PrintProvider(eventArgs, message);
}, EventLevel.Informational);

TokenCredential credential = new DefaultAzureCredential();

// Omitted: some call that exercises the credentials

Console.WriteLine("\nDone");
When I run this, I can see something like below:
Trying   : DefaultAzureCredential
Trying   : EnvironmentCredential
Trying   : WorkloadIdentityCredential
Trying   : ManagedIdentityCredential
Trying   : VisualStudioCredential
Selected : VisualStudioCredential

Done
You can see all the credentials it's trying and it takes unnecessary time. For a long time my workaround was to branch on the environment, and when running in Development, switch off the credentials that were never going to work anyway, using the Exclude prefixed properties on DefaultAzureCredentialOptions, something like below.
TokenCredential credential = new DefaultAzureCredential();

// Environment check, could be ASPNETCORE_ENVIRONMENT, AZURE_FUNCTIONS_ENVIRONMENT etc.
bool isDevelopment = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") == "Development";
if (isDevelopment)
{
    credential = new DefaultAzureCredential(new DefaultAzureCredentialOptions
    {
        ExcludeEnvironmentCredential = true,
        ExcludeWorkloadIdentityCredential = true,
        ExcludeManagedIdentityCredential = true,
    });
}
With this, it will print something like this.
Trying   : DefaultAzureCredential
Trying   : VisualStudioCredential
Selected : VisualStudioCredential

Done
It works, but it's a pain to do this in each project I am working on.

Since Azure.Identity 1.14.0, all of the above can be replaced with an environment variable. Set AZURE_TOKEN_CREDENTIALS to dev and the chain drops every deployed service credential, leaving only the developer tool ones.
{
  "profiles": {
    "ConsoleApp1": {
      "commandName": "Project",
      "environmentVariables": {
        "DOTNET_ENVIRONMENT": "Development",
        "AZURE_TOKEN_CREDENTIALS": "dev"
      }
    }
  }
}

And the code then goes back to being the single line.
TokenCredential credential = new DefaultAzureCredential();
Exactly the same outcome as the Exclude block, with no branching, nothing to maintain. It is purely configuration.

One thing to keep in mind is that if AZURE_TOKEN_CREDENTIALS isn't set at all, DefaultAzureCredential quietly falls back to the full chain. So if someone clones the repo without the launch profile, or the variable gets dropped somewhere along the way, you are silently back to where you started. If you would rather fail fast, since Azure.Identity 1.16.0 there is a constructor overload that takes the environment variable name and requires it to be set to a valid value.
TokenCredential credential =
    new DefaultAzureCredential(DefaultAzureCredential.DefaultEnvironmentVariableName);
Now if it's missing, you get a clear error instead of a silent fallback.
Unhandled exception. 
System.InvalidOperationException: Environment variable 'AZURE_TOKEN_CREDENTIALS' is not set or is empty.
And if you typo the value, it throws as well and lists every value it accepts, which is quite handy.

There is a prod value too, which does the opposite and keeps only the deployed service credentials. And from Azure.Identity 1.15.0 onwards you can go further and name a single credential, which reduces the chain to just that one. The comparison is case insensitive.
"AZURE_TOKEN_CREDENTIALS": "VisualStudioCredential"
A small feature, but if you have been quietly copying that Exclude block around for years like I have, it is a life saver.

Happy Coding.

Regards,
Jaliya

Sunday, August 9, 2026

Support for C# 15 Union Types in ASP.NET Core

In this post, let's have a look at how Union types in C# 15 flow through an ASP.NET Core minimal API and how they are described in OpenAPI. This is a follow up to my previous post C# 15: Brand New Union Types, so I will reuse the same Shape union.

Everything here runs on .NET 11 Preview 6 (latest as of today, it will change) and the Microsoft.AspNetCore.OpenApi package for the built in OpenAPI document generation.

Also using latest language features.
<LangVersion>preview</LangVersion>
Here is a single endpoint that returns a Shape.
using Microsoft.AspNetCore.Http.HttpResults;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();

WebApplication app = builder.Build();

app.MapOpenApi();

app
    .MapGet("/shapes/{id}", Results<Ok<Shape>, NotFound> (int id) =>
    {
        Dictionary<int, Shape> shapes = new()
        {
            { 1, new Circle(2) },
            { 2, new Rectangle(3, 4) },
            { 3, new Triangle(4, 5) }
        };

        return shapes.TryGetValue(id, out var shape)
            ? TypedResults.Ok(shape)
            : TypedResults.NotFound();
    })
    .WithName("GetShape");

app.Run();

public record class Circle(double Radius);

public record class Rectangle(double Width, double Height);

public record class Triangle(double Base, double Height);

public union Shape(Circle, Rectangle, Triangle);
With the union exposed, an endpoint that returns a union is described with an anyOf schema listing each case type. Here is the OpenAPI document.
{
  "paths": {
    "/shapes/{id}": {
      "get": {
        "operationId": "GetShape",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Shape" }
              }
            }
          },
          "404": { "description": "Not Found" }
        }
      }
    }
  },
  "components": {
    "schemas": {
      // The union is an "anyOf" over its case types.
      "Shape": {
        "type": "object",
        "anyOf": [
          { "$ref": "#/components/schemas/Circle" },
          { "$ref": "#/components/schemas/Rectangle" },
          { "$ref": "#/components/schemas/Triangle" }
        ]
      },
      // Each case reuses its own standalone component, no "$type" discriminator.
      "Circle": {
        "type": "object",
        "required": [ "radius" ],
        "properties": { 
"radius": { "type": "number", "format": "double" } } }, "Rectangle": { "type": "object", "required": [ "width", "height" ], "properties": { "width": { "type": "number", "format": "double" }, "height": { "type": "number", "format": "double" } } }, "Triangle": { "type": "object", "required": [ "base", "height" ], "properties": { "base": { "type": "number", "format": "double" }, "height": { "type": "number", "format": "double" } } } } } }
Note that unlike polymorphic types, union cases don't carry a $type discriminator.

When you run this, you can see something like this:
Union Types in ASP.NET Core
A few limits apply in this preview. Only JSON request bodies and responses are supported. Binding a union from the query string, route values, headers, or form fields is not yet available (dotnet/aspnetcore #66648).

More read:

Hope this helps.

Happy Coding.

Regards,
Jaliya

Friday, July 17, 2026

ASP.NET Core 11.0: Async Validation in Minimal APIs

In this post, let's have a look at asynchronous validation support for Minimal APIs, which is going to be available in ASP.NET Core 11.0.

Some time back, I blogged about Validation Support for Minimal APIs and Custom Validation in ASP.NET Core 10.0. This is a continuation of that, but this time the validators run asynchronously.

With the latest .NET 11 Preview 6, Minimal APIs now supports asynchronous validators end-to-end.

Let's look at the two ways to write asynchronous validators.

AsyncValidationAttribute

This is the asynchronous version of a custom ValidationAttribute. We inherit from AsyncValidationAttribute and override IsValidAsync. Here we can resolve the services from the ValidationContext and use those services for our validation logic.
public record Product(
    [Required, UniqueSku] string Sku,
    [Required] string Name);

internal sealed class UniqueSkuAttribute : AsyncValidationAttribute
{
    protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) =>
        throw new InvalidOperationException($"Validate this attribute with '{nameof(IsValidAsync)}'.");

    protected override async Task<ValidationResult?> IsValidAsync(object? value,
        ValidationContext validationContext,
        CancellationToken cancellationToken)
    {
        ICatalogService catalogService = validationContext.GetRequiredService<ICatalogService>();
        if (value is string sku && await catalogService.SkuExistsAsync(sku, cancellationToken))
        {
            return new ValidationResult("A product with that SKU already exists.");
        }

        return ValidationResult.Success;
    }
}
Note the UniqueSku attribute in Sku. That's the custom attribute we created that performs an asynchronous validation. Here since IsValid in ValidationAttribute is abstract, we have to implement it even though we validate asynchronously. Here it just throws. 

Something to remember: for minimal API validation using Microsoft.Extensions.Validation, the framework always calls the async path and never the sync path. So it's safe to throw.

Now say we have the following API.
using System.ComponentModel.DataAnnotations;
using System.Runtime.CompilerServices;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// Add validation services to the container
builder.Services.AddValidation();

builder.Services.AddSingleton<ICatalogService, DummyCatalogService>();

WebApplication app = builder.Build();

app.MapPost("/products", (Product product) =>
    Results.Ok(product));

app.MapPost("/orders", (OrderRequest orderRequest) =>
    Results.Ok(orderRequest));

app.Run();
Just like before, we enable validation by calling AddValidation(). 

Now let's create a Product with an existing SKU.
@WebApplication1_HostAddress = http://localhost:5016

POST {{WebApplication1_HostAddress}}/products
Content-Type: application/json

{
  "sku": "EXISTING_SKU",
  "name": "Some Product"
}
Throws 400 as expected.
400: Bad Request
IAsyncValidatableObject

This is the asynchronous version of IValidatableObject. This is useful when the validation needs more than one property. We implement ValidateAsync, which returns an IAsyncEnumerable<ValidationResult>.
public record OrderRequest(
    [Range(1, int.MaxValue)] int ProductId,
    [Range(1, int.MaxValue)] int Quantity,
    string PromoCode) : IAsyncValidatableObject
{
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) =>
        throw new InvalidOperationException($"Validate this type with '{nameof(ValidateAsync)}'.");

    public async IAsyncEnumerable<ValidationResult> ValidateAsync(
        ValidationContext validationContext,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        ICatalogService catalogService = validationContext.GetRequiredService<ICatalogService>();

        if (!await catalogService.HasStockAsync(ProductId, Quantity, cancellationToken))
        {
            yield return new ValidationResult(
                $"Product '{ProductId}' does not have '{Quantity}' unit(s) in stock.",
                [nameof(Quantity)]);
        }

        if (!await catalogService.IsPromoCodeValidAsync(PromoCode, cancellationToken))
        {
            yield return new ValidationResult(
                $"Promo code '{PromoCode}' is not valid.",
                [nameof(PromoCode)]);
        }
    }
}
IAsyncValidatableObject extends IValidatableObject, so the synchronous Validate still has to be there. Same as before, it just throws.

Ordering more than what we have in stock and also with an invalid Promo Code,
@WebApplication1_HostAddress = http://localhost:5016

POST {{WebApplication1_HostAddress}}/orders
Content-Type: application/json

{
  "productId": 1,
  "quantity": 100,
  "promoCode": "INVALID"
}
Throws 400 as expected.
400: Bad Request
Hope this helps.

Happy Coding.

Regards,
Jaliya

Friday, July 3, 2026

.NET Options Validation in Application Startup

In this post, let's have a look at how we can validate options in application startup in a .NET application.

The Options pattern lets us bind a configuration section to a strongly-typed class. On top of that, we can validate the bound values so that a missing/incorrect configuration fails fast at application startup rather than blowing up at some random point at runtime when the options are first used. All of this lives in Microsoft.Extensions.Options, so it works the same in Console apps, Worker Services, ASP.NET Core and any other .NET host. 

Let's have a look at a simple example.

Say we have the following appsettings.json.
{
  "WeatherApi": {
    "BaseUrl": "Something",
    "TimeoutSeconds": 300,
    "Cache": {
      "DurationSeconds": 7200
    }
  }
}
And the following options classes and registration.
using Microsoft.Extensions.Options;
using System.ComponentModel.DataAnnotations;

WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

builder.Services.AddOptions<WeatherApiOptions>()
    .Bind(builder.Configuration.GetSection(WeatherApiOptions.SectionName))
    .ValidateDataAnnotations()
    .ValidateOnStart();

// Omitted for brevity

WebApplication app = builder.Build();

// Omitted for brevity

app.Run();

public class WeatherApiOptions
{
    public const string SectionName = "WeatherApi";

    [Required]
    [Url]
    public string BaseUrl { get; set; } = string.Empty;

    [Range(1, 60)]
    public int TimeoutSeconds { get; set; }

    [Required]
    [ValidateObjectMembers]
    public CacheOptions Cache { get; set; } = new();
}

public class CacheOptions
{
    [Range(30, 3600)]
    public int DurationSeconds { get; set; }
}
Here ValidateDataAnnotations() validates the DataAnnotation attributes on our options type. By default though, that validation is lazy, it only runs the first time someone accesses IOptions<WeatherApiOptions>.Value. That means a misconfigured application would happily start up and only fail later at runtime when the options are first used. ValidateOnStart() fixes that by forcing the validation to run eagerly at application startup, so we fail fast. (This kicks in as long as something actually starts the host, e.g. app.Run().)

Something to note is, the ValidateDataAnnotations() only validates the top-level options type. It does not recurse into nested objects (or into items of a collection). So the DataAnnotation attributes on CacheOptions are silently ignored. And for that, from .NET 8, onwards, two new attributes have been added to Microsoft.Extensions.Options:
  • [ValidateObjectMembers] - recursively validates the DataAnnotation attributes on a nested object.
  • [ValidateEnumeratedItems] - recursively validates the DataAnnotation attributes on each item of a collection.
The application now fails at startup, and notice that all the validation failures, including the ones on the nested object are reported at once.
Microsoft.Extensions.Options.OptionsValidationException: 

DataAnnotation validation failed for 'WeatherApiOptions' members: 
'BaseUrl' with the error: 'The BaseUrl field is not a valid fully-qualified http, https, or ftp URL.'.;
DataAnnotation validation failed for 'WeatherApiOptions' members:
'TimeoutSeconds' with the error: 'The field TimeoutSeconds must be between 1 and 60.'.;
DataAnnotation validation failed for 'WeatherApiOptions.Cache' members:
'DurationSeconds' with the error: 'The field DurationSeconds must be between 30 and 3600.'.
Hope this helps.

Happy Coding.

Regards,
Jaliya

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 HttpRequest, HttpResponse, 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<HttpStartOutputWithHttpResponseData> Run(
        [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 { get; set; }

    public HttpResponseData HttpResponse { get; set; }
}

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 { get; set; }

    [HttpResult]
    public IActionResult HttpResponse { get; set; }
}

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 CorrelationId, SessionId, Subject, 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 { get; init; }

    public required string SenderAddress { get; init; }
}

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<Email> emailOptions,
        ILogger<EmailService> logger)
    {
        _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,
            recipientAddress: recipientEmail,
            content: new 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 ValidationResult? IsValid(object? value, ValidationContext _)
    {
        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 { get; set; }

    public string Name { get; set; }

    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