Azure Face API in ASP.NET Core is useful when an application needs face rectangles or landmarks without building and operating its own computer-vision model. The production boundary is straightforward: validate the image before upload, request only the detection data you need, pass cancellation through the HTTP call, and keep biometric data and credentials out of logs.

This guide builds that boundary with .NET 10 and the Azure Face REST API v1.2. It deliberately performs face detection—not identification or verification—and sets returnFaceId=false. The original working .NET 8 Blazor demo, its video, screenshots, and GitHub repository are preserved later as historical material.

Decide whether face detection fits the requirement

Detection answers a bounded question: where are the faces in this image, and which requested landmarks or supported attributes describe them? It can return a rectangle for each detected face and, when requested with a compatible detection model, landmark coordinates such as the nose tip and pupils.

Recognition is a different security and governance boundary. Identification, verification, and a returned face ID can connect detected features to a person or to stored face data. Microsoft limits access to several Face capabilities, and the organization using biometric data remains responsible for notice, consent, retention, and deletion requirements.

  • Use detection when the application needs locations, counts, landmarks, or permitted quality signals.
  • Do not request a face ID when no later recognition operation needs it.
  • Do not treat a detected face, an attribute, or a confidence value as an identity decision.
  • Keep authorization, retention, and human-review policies outside the generic HTTP client.

The implementation below requests rectangles and landmarks only. It uses detection_03, which Microsoft recommends over the default model for improved accuracy on smaller and rotated faces, and explicitly disables face-ID creation.

Provision Azure Face and protect the credential

Create a Face resource in the Azure portal, then copy its endpoint from the Keys and Endpoint page. Availability and approval requirements depend on the capability and account. Confirm access for the intended workload before designing a production dependency around the service.

Provisioning the original Azure Face resource in the Azure portal
The original portal flow recorded for the .NET 8 demo. Azure portal labels may change over time.
Azure Face resource endpoint and access-key page
The endpoint is configuration. The key is a secret and must not be committed or logged.

For local development, use user secrets or environment variables. In Azure, prefer a workload identity where the selected service and hosting model support Microsoft Entra authentication. If the application must use an API key, load it from a secret store and rotate it. The Azure Key Vault configuration guide covers the application boundary; never place a real key in source control or a public tutorial.

dotnet user-secrets init
dotnet user-secrets set "AzureFace:Endpoint" "https://YOUR-RESOURCE.cognitiveservices.azure.com/"
dotnet user-secrets set "AzureFace:ApiKey" "YOUR-DEVELOPMENT-KEY"

Configure the .NET 10 application

The current implementation targets net10.0. .NET 8 was correct for the original 2024 demo, but it reaches end of support on November 10, 2026. Existing applications can use the .NET 8/9 to .NET 10 production migration checklist to plan the runtime and package upgrade without changing the Face client contract at the same time.

<PropertyGroup>
  <TargetFramework>net10.0</TargetFramework>
  <Nullable>enable</Nullable>
  <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

Keep the options surface narrow and validate it during startup. That moves a missing endpoint or key from the first user request to application boot, where the failure is visible and deterministic.

public sealed class AzureFaceOptions
{
    public const string SectionName = "AzureFace";
    public required Uri Endpoint { get; init; }
    public required string ApiKey { get; init; }
}
builder.Services
    .AddOptions<AzureFaceOptions>()
    .Bind(builder.Configuration.GetSection(AzureFaceOptions.SectionName))
    .Validate(o => o.Endpoint.IsAbsoluteUri, "Azure Face endpoint must be absolute.")
    .Validate(o => !string.IsNullOrWhiteSpace(o.ApiKey), "Azure Face key is required.")
    .ValidateOnStart();
builder.Services.AddHttpClient<IFaceDetectionClient, AzureFaceClient>(
    (services, client) =>
    {
        var options = services
            .GetRequiredService<IOptions<AzureFaceOptions>>().Value;
        client.BaseAddress = options.Endpoint;
        client.Timeout = TimeSpan.FromSeconds(15);
        client.DefaultRequestHeaders.Add(
            "Ocp-Apim-Subscription-Key",
            options.ApiKey);
    });

A timeout is still necessary even when the caller supplies a cancellation token. Cancellation represents the caller abandoning the operation; the client timeout bounds a request whose caller remains connected but whose upstream dependency does not complete.

Build a cancellation-aware Face API client

The Face REST API accepts image bytes with application/octet-stream. Sending the upload as a stream avoids an unnecessary application-level byte-array copy. The response model includes only the fields this application owns.

public sealed record FacePoint(double X, double Y);
public sealed record FaceLandmarks(FacePoint NoseTip);
public sealed record FaceRectangle(
    int Left,
    int Top,
    int Width,
    int Height);
public sealed record DetectedFace(
    FaceRectangle FaceRectangle,
    FaceLandmarks? FaceLandmarks);
public interface IFaceDetectionClient
{
    Task<IReadOnlyList<DetectedFace>> DetectAsync(
        Stream image,
        CancellationToken cancellationToken);
}

The request pins API version v1.2 and the intended detection behavior. Explicit parameters are preferable to silently inheriting service defaults, especially when a default such as returnFaceId=true creates data the application does not need.

using System.Net;
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;
public sealed class AzureFaceClient(HttpClient httpClient)
    : IFaceDetectionClient
{
    private const string DetectPath =
        "face/v1.2/detect" +
        "?detectionModel=detection_03" +
        "&returnFaceId=false" +
        "&returnFaceLandmarks=true";
    public async Task<IReadOnlyList<DetectedFace>> DetectAsync(
        Stream image,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(image);
        using var request = new HttpRequestMessage(HttpMethod.Post, DetectPath);
        request.Content = new StreamContent(image);
        request.Content.Headers.ContentType =
            new MediaTypeHeaderValue("application/octet-stream");
        using var response = await httpClient.SendAsync(
            request,
            HttpCompletionOption.ResponseHeadersRead,
            cancellationToken);
        if (!response.IsSuccessStatusCode)
        {
            throw await FaceApiException.CreateAsync(
                response,
                cancellationToken);
        }
        var faces = await response.Content
            .ReadFromJsonAsync<List<DetectedFace>>(
                cancellationToken: cancellationToken);
        return faces ?? [];
    }
}
public sealed class FaceApiException(
    HttpStatusCode statusCode,
    string? serviceCode)
    : Exception($"Azure Face request failed with HTTP {(int)statusCode}.")
{
    public HttpStatusCode StatusCode { get; } = statusCode;
    public string? ServiceCode { get; } = serviceCode;
    public static async Task<FaceApiException> CreateAsync(
        HttpResponseMessage response,
        CancellationToken cancellationToken)
    {
        FaceErrorEnvelope? error = null;
        try
        {
            error = await response.Content
                .ReadFromJsonAsync<FaceErrorEnvelope>(
                    cancellationToken: cancellationToken);
        }
        catch (JsonException)
        {
            // Preserve the status without copying an untrusted body.
        }
        return new FaceApiException(
            response.StatusCode,
            error?.Error?.Code);
    }
    private sealed record FaceErrorEnvelope(FaceError? Error);
    private sealed record FaceError(string? Code);
}

The exception keeps the HTTP status and bounded service error code for application policy, but it does not copy the entire upstream response into a log message. Map known failures at the API boundary: invalid images are client errors, authentication failures are configuration or identity incidents, throttling needs backpressure, and upstream failures should normally become a controlled dependency error.

Validate images before sending them to Azure

The service accepts JPEG, PNG, GIF first frames, and BMP images between 1 KB and 6 MB. Reject unsupported input before paying for a network call. A browser-provided content type is useful as an early filter, but it is not proof of the file format; production upload pipelines should also inspect the file signature with a maintained image library.

private const long MinImageBytes = 1_024;
private const long MaxImageBytes = 6 * 1_024 * 1_024;
private static readonly HashSet<string> AllowedContentTypes =
    new(StringComparer.OrdinalIgnoreCase)
    {
        "image/jpeg",
        "image/png",
        "image/gif",
        "image/bmp"
    };
public async Task<IReadOnlyList<DetectedFace>> DetectUploadAsync(
    IBrowserFile file,
    CancellationToken cancellationToken)
{
    if (file.Size is < MinImageBytes or > MaxImageBytes)
    {
        throw new InvalidOperationException(
            "The image must be between 1 KB and 6 MB.");
    }
    if (!AllowedContentTypes.Contains(file.ContentType))
    {
        throw new InvalidOperationException("Unsupported image format.");
    }
    await using var image = file.OpenReadStream(
        MaxImageBytes,
        cancellationToken);
    return await faceClient.DetectAsync(image, cancellationToken);
}

This method sends the stream once and does not create a second base64 copy for preview. If the UI needs a preview, handle it as a separate presentation concern and revoke any browser object URL when it is no longer needed. Do not retain the uploaded image merely because detection succeeded.

Image dimensions also affect results. Microsoft documents a minimum detectable face size of 36×36 pixels for images no larger than 1920×1080; higher-resolution images require proportionally larger faces. An empty array therefore means “no face detected under this request and image,” not proof that the image contains no person.

Handle failures without leaking data

  • 400: return a safe validation response; do not echo the image or unrestricted upstream body.
  • 401/403: treat the key, identity, role, region, or limited-access configuration as an operational incident.
  • 429: honor Retry-After, bound concurrency, and reject excess work before memory grows.
  • 5xx/timeouts: fail predictably and expose dependency-health metrics without logging biometric payloads.

Automatic retries require care because this is a streamed POST. A retry policy must be able to replay the request body, must remain within the request deadline, and must not amplify throttling. For interactive uploads, one controlled retry of a buffered, validated image may be reasonable; an unbounded retry pipeline is not.

Record bounded metrics such as status class, latency, detected-face count, throttling, and cancellation. Never use the image URL, filename, face ID, raw response, or landmark coordinates as metric labels. Those values create both privacy exposure and high-cardinality telemetry.

Verify the HTTP contract

A useful test verifies the behavior the application controls: method, API version, parameters, media type, response mapping, and cancellation. It should not claim to measure Azure model accuracy.

[Fact]
public async Task DetectAsync_sends_bounded_detection_request()
{
    HttpMethod? method = null;
    string? requestUri = null;
    string? contentType = null;
    var handler = new StubHandler(request =>
    {
        method = request.Method;
        requestUri = request.RequestUri!.ToString();
        contentType = request.Content!.Headers.ContentType!.MediaType;
        return Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = JsonContent.Create(new[]
            {
                new DetectedFace(
                    new FaceRectangle(10, 20, 100, 120),
                    new FaceLandmarks(new FacePoint(58.5, 71.0)))
            })
        });
    });
    using var httpClient = new HttpClient(handler)
    {
        BaseAddress = new Uri("https://example.cognitiveservices.azure.com/")
    };
    var client = new AzureFaceClient(httpClient);
    await using var image = new MemoryStream(new byte[1_024]);
    var faces = await client.DetectAsync(image, CancellationToken.None);
    Assert.Single(faces);
    Assert.Equal(HttpMethod.Post, method);
    Assert.Contains("face/v1.2/detect", requestUri);
    Assert.Contains("detectionModel=detection_03", requestUri);
    Assert.Contains("returnFaceId=false", requestUri);
    Assert.Equal("application/octet-stream", contentType);
}
private sealed class StubHandler(
    Func<HttpRequestMessage, Task<HttpResponseMessage>> send)
    : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken) => send(request);
}

Add separate tests for an empty successful response, malformed JSON, 400, 401, 403, 429, 5xx, timeout, and caller cancellation. Run a small integration test against the intended Azure resource before deployment, but keep real keys and test images outside the repository.

Historical .NET 8 Blazor demo

The original article was published in May 2024 with a .NET 8 Blazor Web App and a custom Azure Face API client. The demo was implemented, run, and tested successfully at that time. The video and screenshots below are direct evidence of that working result, and the original Azure Face API repository remains available.

The historical client used face/v1.0, Newtonsoft.Json, a byte array, detection_01, and returnFaceId=true. Its Blazor component then read the selected file again to create a base64 preview. Those choices are preserved in the repository as the original implementation; they are not the recommended production baseline above. In particular, the modern boundary avoids an unnecessary face ID, uses detection_03, streams the upload once, passes cancellation, and separates safe error details from the upstream response body.

The following code is retained exactly as historical material from the working 2024 demo. Read it together with the video and repository, but use the .NET 10 implementation earlier in this article for new production work.

Original face models and API client

namespace Dnc.Services.FaceDetection.Models
{
    public class Face
    {
        public string FaceId { get; set; }
        public FaceRectangle FaceRectangle { get; set; }
        public FaceLandmarks FaceLandmarks { get; set; }
    }
    public class FaceLandmarks
    {
        public NoseTip NoseTip { get; set; }
    }
    public class NoseTip
    {
        public double X { get; set; }
        public double Y { get; set; }
    }
}
namespace Dnc.Services.FaceDetection.Clients
{
    public interface IAzureFaceDetectionClient
    {
        Task<IEnumerable<Face>> DetectFacesInBinaryImage(byte[] imageBytes);
        Task<IEnumerable<Face>> DetectFacesWithImageUrl(string imageUrl);
    }
}
namespace Dnc.Services.FaceDetection.Clients
{
    public class AzureFaceDetectionClient : IAzureFaceDetectionClient
    {
        private readonly HttpClient httpClient;
        public AzureFaceDetectionClient(HttpClient httpClient)
        {
            this.httpClient = httpClient;
        }
        public async Task<IEnumerable<Face>> DetectFacesInBinaryImage(byte[] imageBytes)
        {
            var request = new HttpRequestMessage(HttpMethod.Post, "face/v1.0/detect?returnFaceId=true&returnFaceLandmarks=true&detectionModel=detection_01");
            using var content = new ByteArrayContent(imageBytes);
            content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
            request.Content = content;
            var response = await httpClient.SendAsync(request);
            var responseBody = await response.Content.ReadAsStringAsync();
            if (response.IsSuccessStatusCode)
            {
                return JsonConvert.DeserializeObject<IEnumerable<Face>>(responseBody);
            }
            else
            {
                throw new Exception(responseBody);
            }
        }
        public async Task<IEnumerable<Face>> DetectFacesWithImageUrl(string imageUrl)
        {
            var request = new HttpRequestMessage(HttpMethod.Get, $"face/v1.0/detect?url={imageUrl}");
            var response = await httpClient.SendAsync(request);
            var responseBody = await response.Content.ReadAsStringAsync();
            if (response.IsSuccessStatusCode)
            {
                return JsonConvert.DeserializeObject<IEnumerable<Face>>(responseBody, new JsonSerializerSettings { Culture = CultureInfo.InvariantCulture });
            }
            else
            {
                throw new Exception(responseBody);
            }
        }
    }
}

Original service boundary

namespace Dnc.Services.FaceDetection
{
    public class BoundingFace
    {
        public string Id { get; set; }
        public int Top { get; set; }
        public int Left { get; set; } 
        public int Width { get; set; }
        public int Height { get; set; } 
        public double NoseTipX { get; set; } 
        public double NoseTipY { get; set; } 
    }
}
namespace Dnc.Services.FaceDetection
{
    public interface IAzureFaceDetectionService
    {
        Task<IEnumerable<BoundingFace>> DetectFacesInBinaryImage(byte[] imageData);
        Task<IEnumerable<BoundingFace>> DetectFacesWithImageUrl(string imageUrl);
    }
}
namespace Dnc.Services.FaceDetection
{
    public class AzureFaceDetectionService : IAzureFaceDetectionService
    {
        private readonly IAzureFaceDetectionClient azureFaceDetectionClient; 
        public AzureFaceDetectionService(IAzureFaceDetectionClient azureFaceDetectionClient)
        {
            this.azureFaceDetectionClient = azureFaceDetectionClient; 
        }
        public async Task<IEnumerable<BoundingFace>> DetectFacesInBinaryImage(byte[] imageData)
        {
            var faces = await azureFaceDetectionClient.DetectFacesInBinaryImage(imageData);
            return faces.Select(face => MapToBoundingFace(face));
        }
        public async Task<IEnumerable<BoundingFace>> DetectFacesWithImageUrl(string imageUrl)
        {
            var faces = await azureFaceDetectionClient.DetectFacesWithImageUrl(imageUrl);
            return faces.Select(face => MapToBoundingFace(face));
        }
        readonly Func<Face, BoundingFace> MapToBoundingFace = face => new BoundingFace
        {
            Id = face.FaceId,
            Top = face.FaceRectangle.Top,
            Left = face.FaceRectangle.Left,
            Width = face.FaceRectangle.Width,
            Height = face.FaceRectangle.Height,
            NoseTipX = face.FaceLandmarks.NoseTip.X,
            NoseTipY = face.FaceLandmarks.NoseTip.Y
        };
    }
}

Original Blazor registration and component

{
  "Endpoint": "YOUR ENDPOINT",
  "SubscriptionKey": "YOUR KEY"
}
builder.Services.AddHttpClient<IAzureFaceDetectionClient, AzureFaceDetectionClient>(httpClient =>
            {
                 httpClient.BaseAddress = new Uri(builder.Configuration.GetValue<string>("Endpoint"));
                 httpClient.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", builder.Configuration.GetValue<string>("SubscriptionKey"));
            });
@page "/"
@using Dnc.Services.FaceDetection

<PageTitle>Home</PageTitle>

<div class="container">
    <div class="row">
        <h3 class="my-5">Face services Api : Face Detection</h3>
        @if (!Loading)
        {
            <div class="col-6">
                @if (Image != null)
                {
                    <div>
                        <img src="@Image">
                    </div>
                }
                else
                {
                    <div class="image-empty"></div>
                }
            </div>
            <div class="col-6">
                @if (boundingFaces == null)
                {
                    <div class="error-message">
                        No faces detected on the image
                    </div>
                }
                @if (boundingFaces != null && boundingFaces.Count() > 0)
                {
                    <div class="error-message">
                        Faces detected in the image : @boundingFaces.Count() (face/faces)
                    </div>
                    var x = 1;
                    @foreach (var face in boundingFaces)
                    {
                        <span style="color:#0f8c98">Face (@x)</span>
                        <table>
                            <tr>
                                <td>Face ID:</td>
                                <td>@face.Id</td>
                            </tr>
                            <tr>
                                <td>Top:</td>
                                <td>@face.Top</td>
                            </tr>
                            <tr>
                                <td>Left:</td>
                                <td>@face.Left</td>
                            </tr>
                            <tr>
                                <td>Width:</td>
                                <td>@face.Width</td>
                            </tr>
                            <tr>
                                <td>Height:</td>
                                <td>@face.Height</td>
                            </tr>
                            <tr>
                                <td>Nose tip X: </td>
                                <td>@face.NoseTipX</td>
                            </tr>
                            <tr>
                                <td>Nose tip Y: </td>
                                <td>@face.NoseTipY</td>
                            </tr>
                        </table>
                        x++;
                    }
                }
            </div>
        }
        else
        {
            <div class="fa-x3 d-flex justify-content-center align-items-center">
                <div>
                    <i class="fas fa-circle-notch fa-spin"></i>
                    <div>
                        <span>Loading...</span>
                    </div>
                </div>

            </div>
        }
    </div>
    <div class="my-3">
        <label for="upload">
            <span style="cursor:pointer;color:#2c52fd;text-decoration:underline;text-transform:uppercase" aria-hidden="true">Upload</span>
            <InputFile type="file" id="upload" OnChange="@UploadPhoto" style="display:none" />
        </label>
    </div>
</div>



@code{

    protected string Image {get;set;}
    protected bool Loading {get;set;}

    protected IEnumerable<BoundingFace> boundingFaces{get;set;}
    protected BoundingFace BoundingFace { get; set; }

    [Inject]
    public IAzureFaceDetectionService AzureFaceDetectionService { get; set; }

    public async Task UploadPhoto(InputFileChangeEventArgs e)
    {
        Loading = true;
        var file = e?.File;

        try
        {
            if (file != null)
            {
                var imageBytes = await ConvertFileToByte(file);
                boundingFaces = await AzureFaceDetectionService.DetectFacesInBinaryImage(imageBytes);

                var base64String = await ConvertToBase64StringAsync(file);
                Image = string.Format("data:image/jpeg;base64,{0}", base64String);

            }
        }catch(Exception ex)
        {
            Console.WriteLine(ex.Message);
        }
        finally
        {
            Loading = false;
        }
    }

    private static async Task<byte[]> ConvertFileToByte(IBrowserFile file)
    {
        var buffer = new byte[file.Size];
        using (var stream = file.OpenReadStream())
        {
            await stream.ReadAsync(buffer, 0, (int)file.Size);
        }
        return buffer;
    }
    private async Task<string> ConvertToBase64StringAsync(IBrowserFile file)
    {
        using (var memoryStream = new MemoryStream())
        {
            await file.OpenReadStream().CopyToAsync(memoryStream);
            byte[] fileBytes = memoryStream.ToArray();
            return Convert.ToBase64String(fileBytes);
        }
    }
}
Original .NET 8 Blazor Web App project used by the Azure Face demo
Original Azure Face demo detecting one face and displaying its rectangle and nose landmark
Original Azure Face demo detecting two faces in one image

Production checklist

  • Confirm that Face service access and the intended capability are available for the Azure account and region.
  • Request only rectangles, landmarks, or attributes the application genuinely uses.
  • Keep returnFaceId=false unless an approved recognition workflow requires it.
  • Validate file size, supported format, signature, and application-specific dimensions before upload.
  • Use user secrets locally and workload identity or Key Vault-backed secrets in production.
  • Pass cancellation, configure a bounded timeout, and apply replay-safe retry behavior only where justified.
  • Map upstream errors without returning or logging biometric payloads.
  • Define notice, consent, retention, deletion, and access-control policies before processing real users’ images.
  • Test the HTTP contract and the deployed resource separately.

A production-ready Face client is not defined by how many response properties it exposes. It is defined by a narrow purpose, explicit data collection, bounded uploads, reliable cancellation, safe failure behavior, and a privacy policy that matches the actual workload.

References

Found this useful? Support more practical developer content.

Author

Practical .NET, Angular, Azure, Blazor, and AI engineering for real-world development.

Ads Blocker Image Powered by Code Help Pro

Ads Blocker Detected!!!

We have detected that you are using extensions to block ads. Please support us by disabling these ads blocker.

Powered By
Best Wordpress Adblock Detecting Plugin | CHP Adblock