European ASP.NET 4.5 Hosting BLOG

BLOG about ASP.NET 4, ASP.NET 4.5 Hosting and Its Technology - Dedicated to European Windows Hosting Customer

European ASP.NET Core 10.0 Hosting - HostForLIFE :: Using ASP.NET Core to Create a Production-Ready Image Analysis API

clock August 5, 2026 11:00 by author Peter

Images are being accepted as input by more and more modern online apps.

Users can provide a product photo and ask for structured characteristics, upload a snapshot and expect OCR text, or submit an interface capture and inquire about its contents.

The backend can initially seem straightforward:

  • Accept a picture.
  • Forward it to a vision model.
  • Give back the outcome that was produced.

This design works for a prototype, but it becomes unreliable when real users upload large files, corrupted images, unsupported formats, or several requests at the same time.

A production system also needs to handle:

  • Secure file validation
  • Private storage
  • Long-running tasks
  • External provider timeouts
  • Duplicate processing
  • Task recovery
  • Structured error states
  • Frontend status polling
  • Usage limits
  • Privacy and data retention

In this article, we will build a provider-independent image analysis API with ASP.NET Core.

The API will:

  • Validate uploaded images on the server
  • Generate safe internal file names
  • Store images outside the public web directory
  • Create asynchronous processing tasks
  • Queue tasks with Channel<T>
  • Run analysis with BackgroundService
  • Separate application code from the vision provider
  • Expose a task-status endpoint
  • Return completed and failed states clearly

Products such as Describe Image demonstrate the practical value of turning images into descriptions, OCR text, alt text, prompts, product information, and reusable notes.

The implementation below is an independent architecture example and does not describe the internal implementation of any specific product.

1. Project Architecture

The API should return quickly after an image upload is accepted.

The actual vision-model request should run outside the HTTP request lifecycle.

The workflow will look like this:
Client
    ↓
POST /api/image-analysis
    ↓
Upload validation
    ↓
Private file storage
    ↓
Task creation
    ↓
Background queue
    ↓
ImageAnalysisWorker
    ↓
Vision provider
    ↓
Task result storage
    ↓
GET /api/image-analysis/{taskId}
    ↓
Client receives the status and result


This architecture prevents an HTTP connection from remaining open while an external model processes an image.

It also makes retries, monitoring, and task recovery easier to implement.

2. Create the ASP.NET Core Project

Create a new ASP.NET Core Web API project:
dotnet new webapi -n ImageAnalysisApi
cd ImageAnalysisApi


The sample uses standard ASP.NET Core services and does not require a specific AI provider.

3. Define the Analysis Modes

Different visual tasks require different instructions and output formats. OCR should not use the same prompt as alt-text generation, product analysis, or a detailed image description.

Create a Models folder and add AnalysisMode.cs:
namespace ImageAnalysisApi.Models;

public enum AnalysisMode
{
    DetailedDescription,
    Ocr,
    AltText,
    ProductAnalysis
}

Using an enum prevents clients from submitting arbitrary mode names and gives the backend a stable list of supported operations.

4. Define the Task States
A Boolean property such as IsComplete is not enough for a long-running workflow. The client needs to know whether a task is waiting, actively processing, completed, or failed.

Create AnalysisTaskStatus.cs:
namespace ImageAnalysisApi.Models;

public enum AnalysisTaskStatus
{
    Queued,
    Processing,
    Completed,
    Failed
}


Now create ImageAnalysisTask.cs:
namespace ImageAnalysisApi.Models;

public sealed class ImageAnalysisTask
{
    public required Guid Id { get; init; }

    public required string StoredFilePath { get; init; }

    public required string ContentType { get; init; }

    public required AnalysisMode Mode { get; init; }

    public AnalysisTaskStatus Status { get; set; }

    public string? Result { get; set; }

    public string? ErrorCode { get; set; }

    public string? ErrorMessage { get; set; }

    public DateTimeOffset CreatedAt { get; init; }

    public DateTimeOffset UpdatedAt { get; set; }
}


Detailed states improve:

  • Frontend progress handling
  • Retry logic
  • Operational monitoring
  • Customer support
  • Failure diagnosis
  • Usage accounting

A production application should store these records in a durable database.

5. Create the Task Store
The task store separates persistence logic from controllers and background workers.

Create IAnalysisTaskStore.cs:
using ImageAnalysisApi.Models;

namespace ImageAnalysisApi.Services;

public interface IAnalysisTaskStore
{
    Task CreateAsync(
        ImageAnalysisTask task,
        CancellationToken cancellationToken);

    Task<ImageAnalysisTask?> GetAsync(
        Guid taskId,
        CancellationToken cancellationToken);

    Task UpdateAsync(
        ImageAnalysisTask task,
        CancellationToken cancellationToken);
}


For this tutorial, use a thread-safe in-memory implementation.

Create InMemoryAnalysisTaskStore.cs:
using System.Collections.Concurrent;
using ImageAnalysisApi.Models;

namespace ImageAnalysisApi.Services;

public sealed class InMemoryAnalysisTaskStore
    : IAnalysisTaskStore
{
    private readonly ConcurrentDictionary<Guid, ImageAnalysisTask>
        _tasks = new();

    public Task CreateAsync(
        ImageAnalysisTask task,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        if (!_tasks.TryAdd(task.Id, task))
        {
            throw new InvalidOperationException(
                $"Task {task.Id} already exists.");
        }

        return Task.CompletedTask;
    }

    public Task<ImageAnalysisTask?> GetAsync(
        Guid taskId,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        _tasks.TryGetValue(taskId, out var task);

        return Task.FromResult(task);
    }

    public Task UpdateAsync(
        ImageAnalysisTask task,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        _tasks[task.Id] = task;

        return Task.CompletedTask;
    }
}

Because this implementation stores everything in memory, all tasks disappear when the application restarts. That is acceptable for a tutorial, but not for a production deployment.

A real implementation can use:

  • SQL Server
  • PostgreSQL
  • MySQL
  • Redis
  • Azure Cosmos DB
  • Another durable task store

6. Validate Uploaded Files
Never trust the original file name, extension, or browser-provided content type by itself.

The server should verify:

  • A file was supplied
  • The file is not empty
  • The file does not exceed the size limit
  • The declared content type is supported
  • The file signature matches the expected image format

Create IImageUploadValidator.cs:
namespace ImageAnalysisApi.Services;

public interface IImageUploadValidator
{
    Task ValidateAsync(
        IFormFile file,
        CancellationToken cancellationToken);
}


Create ImageUploadValidator.cs:
namespace ImageAnalysisApi.Services;

public sealed class ImageUploadValidator
    : IImageUploadValidator
{
    private const long MaxFileSize =
        10 * 1024 * 1024;

    private static readonly HashSet<string>
        AllowedContentTypes =
        new(StringComparer.OrdinalIgnoreCase)
    {
        "image/jpeg",
        "image/png",
        "image/webp"
    };

    public async Task ValidateAsync(
        IFormFile file,
        CancellationToken cancellationToken)
    {
        ArgumentNullException.ThrowIfNull(file);

        if (file.Length == 0)
        {
            throw new InvalidDataException(
                "The uploaded file is empty.");
        }

        if (file.Length > MaxFileSize)
        {
            throw new InvalidDataException(
                "The uploaded file exceeds the 10 MB limit.");
        }

        if (!AllowedContentTypes.Contains(file.ContentType))
        {
            throw new InvalidDataException(
                "Only JPEG, PNG, and WebP images are supported.");
        }

        await using var stream =
            file.OpenReadStream();

        var header = new byte[12];

        var bytesRead = await stream.ReadAsync(
            header.AsMemory(0, header.Length),
            cancellationToken);

        if (!MatchesSupportedSignature(
            header.AsSpan(0, bytesRead)))
        {
            throw new InvalidDataException(
                "The file signature does not match a supported image.");
        }
    }

    private static bool MatchesSupportedSignature(
        ReadOnlySpan<byte> header)
    {
        return IsJpeg(header)
            || IsPng(header)
            || IsWebP(header);
    }

    private static bool IsJpeg(
        ReadOnlySpan<byte> header)
    {
        return header.Length >= 3
            && header[0] == 0xFF
            && header[1] == 0xD8
            && header[2] == 0xFF;
    }

    private static bool IsPng(
        ReadOnlySpan<byte> header)
    {
        byte[] signature =
        {
            0x89, 0x50, 0x4E, 0x47,
            0x0D, 0x0A, 0x1A, 0x0A
        };

        return header.Length >= signature.Length
            && header[..signature.Length]
                .SequenceEqual(signature);
    }

    private static bool IsWebP(
        ReadOnlySpan<byte> header)
    {
        byte[] riff =
        {
            0x52, 0x49, 0x46, 0x46
        };

        byte[] webp =
        {
            0x57, 0x45, 0x42, 0x50
        };

        return header.Length >= 12
            && header[..4].SequenceEqual(riff)
            && header.Slice(8, 4).SequenceEqual(webp);
    }
}


File-signature validation prevents obvious extension and MIME-type spoofing, but it is not a complete security solution.

A production system should also consider:

  • Decoding the image with a trusted image library
  • Rejecting excessive image dimensions
  • Limiting the total pixel count
  • Removing unnecessary metadata
  • Running malware scanning
  • Applying per-user rate limits
  • Restricting concurrent tasks

7. Store Files with Safe Names
Do not use the original upload name as the physical storage name.
The original name may contain:

  • Unsafe characters
  • Misleading extensions
  • Path-related values
  • Personally identifiable information
  • Duplicate names

Create IImageStorage.cs:
namespace ImageAnalysisApi.Services;

public interface IImageStorage
{
    Task<string> SaveAsync(
        IFormFile file,
        CancellationToken cancellationToken);

    Task<Stream> OpenReadAsync(
        string storedPath,
        CancellationToken cancellationToken);
}


Create LocalImageStorage.cs:
namespace ImageAnalysisApi.Services;

public sealed class LocalImageStorage
    : IImageStorage
{
    private readonly string _uploadDirectory;

    public LocalImageStorage(
        IWebHostEnvironment environment)
    {
        _uploadDirectory = Path.Combine(
            environment.ContentRootPath,
            "App_Data",
            "uploads");

        Directory.CreateDirectory(
            _uploadDirectory);
    }

    public async Task<string> SaveAsync(
        IFormFile file,
        CancellationToken cancellationToken)
    {
        var extension =
            GetSafeExtension(file.ContentType);

        var generatedName =
            $"{Guid.NewGuid():N}{extension}";

        var fullPath = Path.Combine(
            _uploadDirectory,
            generatedName);

        await using var destination =
            new FileStream(
                fullPath,
                FileMode.CreateNew,
                FileAccess.Write,
                FileShare.None,
                bufferSize: 81920,
                useAsync: true);

        await file.CopyToAsync(
            destination,
            cancellationToken);

        return fullPath;
    }

    public Task<Stream> OpenReadAsync(
        string storedPath,
        CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();

        Stream stream = new FileStream(
            storedPath,
            FileMode.Open,
            FileAccess.Read,
            FileShare.Read,
            bufferSize: 81920,
            useAsync: true);

        return Task.FromResult(stream);
    }

    private static string GetSafeExtension(
        string contentType)
    {
        return contentType.ToLowerInvariant() switch
        {
            "image/jpeg" => ".jpg",
            "image/png" => ".png",
            "image/webp" => ".webp",
            _ => throw new InvalidDataException(
                "Unsupported image type.")
        };
    }
}


The upload directory is placed under App_Data instead of wwwroot. This prevents uploaded files from automatically becoming public web assets. For distributed deployments, private object storage is usually more appropriate than local disk.

Possible services include:

  • Azure Blob Storage
  • Amazon S3
  • Google Cloud Storage
  • Cloudflare R2
  • MinIO

8. Create an Asynchronous Task Queue
Channel<T> can be used to create a bounded in-process queue.

A bounded queue provides backpressure and prevents unlimited memory growth.

Create IAnalysisTaskQueue.cs:
namespace ImageAnalysisApi.Services;

public interface IAnalysisTaskQueue
{
    ValueTask QueueAsync(
        Guid taskId,
        CancellationToken cancellationToken);

    ValueTask<Guid> DequeueAsync(
        CancellationToken cancellationToken);
}


Create AnalysisTaskQueue.cs:
using System.Threading.Channels;

namespace ImageAnalysisApi.Services;

public sealed class AnalysisTaskQueue
    : IAnalysisTaskQueue
{
    private readonly Channel<Guid> _channel;

    public AnalysisTaskQueue()
    {
        var options =
            new BoundedChannelOptions(100)
            {
                FullMode =
                    BoundedChannelFullMode.Wait,
                SingleReader = true,
                SingleWriter = false
            };

        _channel =
            Channel.CreateBounded<Guid>(options);
    }

    public ValueTask QueueAsync(
        Guid taskId,
        CancellationToken cancellationToken)
    {
        return _channel.Writer.WriteAsync(
            taskId,
            cancellationToken);
    }

    public ValueTask<Guid> DequeueAsync(
        CancellationToken cancellationToken)
    {
        return _channel.Reader.ReadAsync(
            cancellationToken);
    }
}


An in-process queue is appropriate for:

  • Tutorials
  • Small projects
  • Single-instance applications
  • Local development

For multiple application instances, use a durable message broker such as:

  • Azure Service Bus
  • RabbitMQ
  • Amazon SQS
  • Apache Kafka
  • Google Cloud Pub/Sub

An in-process queue loses pending items when the application restarts.

9. Abstract the Vision Provider
Vendor-specific model calls should not be placed directly inside the controller.
Create an interface so the provider can be changed without rewriting the rest of the application.

Create IVisionAnalysisProvider.cs:
using ImageAnalysisApi.Models;

namespace ImageAnalysisApi.Services;

public interface IVisionAnalysisProvider
{
    Task<string> AnalyzeAsync(
        Stream image,
        string contentType,
        AnalysisMode mode,
        CancellationToken cancellationToken);
}


For this article, create a simulated provider.

Create DemoVisionAnalysisProvider.cs:
using ImageAnalysisApi.Models;

namespace ImageAnalysisApi.Services;

public sealed class DemoVisionAnalysisProvider
    : IVisionAnalysisProvider
{
    public async Task<string> AnalyzeAsync(
        Stream image,
        string contentType,
        AnalysisMode mode,
        CancellationToken cancellationToken)
    {
        await Task.Delay(
            TimeSpan.FromSeconds(2),
            cancellationToken);

        return mode switch
        {
            AnalysisMode.Ocr =>
                "Demo OCR result: visible text would appear here.",

            AnalysisMode.AltText =>
                "A concise alt-text description of the uploaded image.",

            AnalysisMode.ProductAnalysis =>
                "A structured description of visible product attributes.",

            _ =>
                "A detailed description of the uploaded image."
        };
    }
}


A real implementation may call:

  • A cloud computer-vision API
  • A multimodal language model
  • A self-hosted vision model
  • An internal Python inference service
  • A separate microservice

The controller and worker do not need to know which provider is used.

They only depend on IVisionAnalysisProvider.

10. Process Tasks with BackgroundService

The background worker will:

  • Dequeue a task ID
  • Load the task record
  • Confirm that it is still queued
  • Mark it as processing
  • Open the stored image
  • Call the vision provider
  • Store the result
  • Mark the task as completed or failed

Create ImageAnalysisWorker.cs:
using ImageAnalysisApi.Models;

namespace ImageAnalysisApi.Services;

public sealed class ImageAnalysisWorker
    : BackgroundService
{
    private readonly IAnalysisTaskQueue _queue;
    private readonly IAnalysisTaskStore _taskStore;
    private readonly IImageStorage _storage;
    private readonly IVisionAnalysisProvider _provider;
    private readonly ILogger<ImageAnalysisWorker> _logger;

    public ImageAnalysisWorker(
        IAnalysisTaskQueue queue,
        IAnalysisTaskStore taskStore,
        IImageStorage storage,
        IVisionAnalysisProvider provider,
        ILogger<ImageAnalysisWorker> logger)
    {
        _queue = queue;
        _taskStore = taskStore;
        _storage = storage;
        _provider = provider;
        _logger = logger;
    }

    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var taskId =
                await _queue.DequeueAsync(
                    stoppingToken);

            try
            {
                await ProcessTaskAsync(
                    taskId,
                    stoppingToken);
            }
            catch (OperationCanceledException)
                when (stoppingToken.IsCancellationRequested)
            {
                break;
            }
            catch (Exception exception)
            {
                _logger.LogError(
                    exception,
                    "Unhandled error for task {TaskId}.",
                    taskId);
            }
        }
    }

    private async Task ProcessTaskAsync(
        Guid taskId,
        CancellationToken cancellationToken)
    {
        var task = await _taskStore.GetAsync(
            taskId,
            cancellationToken);

        if (task is null)
        {
            _logger.LogWarning(
                "Task {TaskId} was not found.",
                taskId);

            return;
        }

        if (task.Status != AnalysisTaskStatus.Queued)
        {
            _logger.LogInformation(
                "Task {TaskId} has status {Status} and will not be processed again.",
                task.Id,
                task.Status);

            return;
        }

        task.Status =
            AnalysisTaskStatus.Processing;

        task.UpdatedAt =
            DateTimeOffset.UtcNow;

        await _taskStore.UpdateAsync(
            task,
            cancellationToken);

        try
        {
            await using var imageStream =
                await _storage.OpenReadAsync(
                    task.StoredFilePath,
                    cancellationToken);

            var result =
                await _provider.AnalyzeAsync(
                    imageStream,
                    task.ContentType,
                    task.Mode,
                    cancellationToken);

            if (string.IsNullOrWhiteSpace(result))
            {
                throw new InvalidOperationException(
                    "The vision provider returned an empty result.");
            }

            task.Result = result;

            task.Status =
                AnalysisTaskStatus.Completed;

            task.ErrorCode = null;
            task.ErrorMessage = null;
        }
        catch (OperationCanceledException)
            when (cancellationToken.IsCancellationRequested)
        {
            throw;
        }
        catch (Exception exception)
        {
            task.Status =
                AnalysisTaskStatus.Failed;

            task.ErrorCode =
                "ANALYSIS_FAILED";

            task.ErrorMessage =
                "The image could not be analyzed.";

            _logger.LogError(
                exception,
                "Image analysis failed for task {TaskId}.",
                task.Id);
        }
        finally
        {
            task.UpdatedAt =
                DateTimeOffset.UtcNow;

            await _taskStore.UpdateAsync(
                task,
                CancellationToken.None);
        }
    }
}


The worker checks that the task is still queued before processing it.

This reduces accidental duplicate work.
A distributed production system should claim tasks through:

  • An atomic database update
  • A row lock
  • A distributed lock
  • A unique stage-execution record

Checking the task status in memory is not enough for strict distributed idempotency.

11. Create the API Controller

The POST endpoint will:

  • Validate the image
  • Store the file
  • Create a task
  • Add the task to the queue
  • Return HTTP 202 Accepted

The GET endpoint will return the current status and result.
Create ImageAnalysisController.cs:
using ImageAnalysisApi.Models;
using ImageAnalysisApi.Services;
using Microsoft.AspNetCore.Mvc;

namespace ImageAnalysisApi.Controllers;

[ApiController]
[Route("api/image-analysis")]
public sealed class ImageAnalysisController
    : ControllerBase
{
    private readonly IImageUploadValidator _validator;
    private readonly IImageStorage _storage;
    private readonly IAnalysisTaskStore _taskStore;
    private readonly IAnalysisTaskQueue _taskQueue;

    public ImageAnalysisController(
        IImageUploadValidator validator,
        IImageStorage storage,
        IAnalysisTaskStore taskStore,
        IAnalysisTaskQueue taskQueue)
    {
        _validator = validator;
        _storage = storage;
        _taskStore = taskStore;
        _taskQueue = taskQueue;
    }

    [HttpPost]
    [Consumes("multipart/form-data")]
    [ProducesResponseType(
        StatusCodes.Status202Accepted)]
    [ProducesResponseType(
        StatusCodes.Status400BadRequest)]
    public async Task<IActionResult> CreateAsync(
        [FromForm] IFormFile image,
        [FromForm] AnalysisMode mode,
        CancellationToken cancellationToken)
    {
        try
        {
            await _validator.ValidateAsync(
                image,
                cancellationToken);
        }
        catch (InvalidDataException exception)
        {
            return BadRequest(new
            {
                error = exception.Message
            });
        }

        var storedPath =
            await _storage.SaveAsync(
                image,
                cancellationToken);

        var now =
            DateTimeOffset.UtcNow;

        var task =
            new ImageAnalysisTask
            {
                Id = Guid.NewGuid(),
                StoredFilePath = storedPath,
                ContentType = image.ContentType,
                Mode = mode,
                Status =
                    AnalysisTaskStatus.Queued,
                CreatedAt = now,
                UpdatedAt = now
            };

        await _taskStore.CreateAsync(
            task,
            cancellationToken);

        await _taskQueue.QueueAsync(
            task.Id,
            cancellationToken);

        return AcceptedAtAction(
            nameof(GetAsync),
            new
            {
                taskId = task.Id
            },
            new
            {
                taskId = task.Id,
                status =
                    task.Status.ToString(),
                statusUrl =
                    Url.ActionLink(
                        nameof(GetAsync),
                        values: new
                        {
                            taskId = task.Id
                        })
            });
    }

    [HttpGet("{taskId:guid}")]
    [ProducesResponseType(
        StatusCodes.Status200OK)]
    [ProducesResponseType(
        StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetAsync(
        Guid taskId,
        CancellationToken cancellationToken)
    {
        var task =
            await _taskStore.GetAsync(
                taskId,
                cancellationToken);

        if (task is null)
        {
            return NotFound(new
            {
                error = "Task not found."
            });
        }

        return Ok(new
        {
            taskId = task.Id,
            mode =
                task.Mode.ToString(),
            status =
                task.Status.ToString(),
            result =
                task.Status ==
                AnalysisTaskStatus.Completed
                    ? task.Result
                    : null,
            errorCode =
                task.ErrorCode,
            errorMessage =
                task.Status ==
                AnalysisTaskStatus.Failed
                    ? task.ErrorMessage
                    : null,
            createdAt =
                task.CreatedAt,
            updatedAt =
                task.UpdatedAt
        });
    }
}


Returning HTTP 202 Accepted tells the client that the request has been accepted, but processing is not complete.

The client can poll the status endpoint until the task becomes Completed or Failed.

12. Register the Services

Update Program.cs:
using ImageAnalysisApi.Services;

var builder =
    WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddSingleton<
    IAnalysisTaskStore,
    InMemoryAnalysisTaskStore>();

builder.Services.AddSingleton<
    IAnalysisTaskQueue,
    AnalysisTaskQueue>();

builder.Services.AddSingleton<
    IImageUploadValidator,
    ImageUploadValidator>();

builder.Services.AddSingleton<
    IImageStorage,
    LocalImageStorage>();

builder.Services.AddSingleton<
    IVisionAnalysisProvider,
    DemoVisionAnalysisProvider>();

builder.Services.AddHostedService<
    ImageAnalysisWorker>();

var app =
    builder.Build();

app.UseHttpsRedirection();

app.MapControllers();

app.Run();


The task store, queue, validator, storage service, and demo provider are singletons in this simplified example. If a future implementation uses Entity Framework Core, resolve scoped services inside a dependency-injection scope created by the worker.

13. Test the API

Start the application:
dotnet run

Submit an image with cURL:
curl -X POST "https://localhost:7001/api/image-analysis" \
-F "[email protected];type=image/png" \
-F "mode=DetailedDescription"

A successful response resembles:
{
  "taskId": "4af8f71b-f93f-44c2-9358-472e8f52497d",
  "status": "Queued",
  "statusUrl": "https://localhost:7001/api/image-analysis/4af8f71b-f93f-44c2-9358-472e8f52497d"
}


Poll the status endpoint:
curl "https://localhost:7001/api/image-analysis/4af8f71b-f93f-44c2-9358-472e8f52497d"


While processing:
{
  "taskId": "4af8f71b-f93f-44c2-9358-472e8f52497d",
  "mode": "DetailedDescription",
  "status": "Processing",
  "result": null,
  "errorCode": null,
  "errorMessage": null,
  "createdAt": "2026-08-03T07:00:00+00:00",
  "updatedAt": "2026-08-03T07:00:01+00:00"
}


After completion:
{
  "taskId": "4af8f71b-f93f-44c2-9358-472e8f52497d",
  "mode": "DetailedDescription",
  "status": "Completed",
  "result": "A detailed description of the uploaded image.",
  "errorCode": null,
  "errorMessage": null,
  "createdAt": "2026-08-03T07:00:00+00:00",
  "updatedAt": "2026-08-03T07:00:03+00:00"
}


14. Return Structured Results
A plain string is enough for a basic prototype, but structured output is easier to validate and consume.

For example:
namespace ImageAnalysisApi.Models;

public sealed record ImageAnalysisResult(
    string Summary,
    IReadOnlyList<string> VisibleText,
    IReadOnlyList<DetectedObject> Objects,
    IReadOnlyList<string> Warnings);

public sealed record DetectedObject(
    string Name,
    double? Confidence);


The provider interface can then return ImageAnalysisResult instead of string:
using ImageAnalysisApi.Models;

namespace ImageAnalysisApi.Services;

public interface IStructuredVisionAnalysisProvider
{
    Task<ImageAnalysisResult> AnalyzeAsync(
        Stream image,
        string contentType,
        AnalysisMode mode,
        CancellationToken cancellationToken);
}


Structured output is useful because it allows the frontend to display:

  • A short summary
  • Extracted OCR text
  • Detected objects
  • Confidence values
  • Warnings
  • Unknown or unverified information

Do not assume that an external model will always return valid JSON.

Validate provider output before saving it or returning it to the client.

15. Separate Visible Facts from Inference
Image-analysis models may generate plausible assumptions that are not directly supported by the image.
For product analysis, the output should distinguish between:

  • Visible attributes
  • Possible interpretations
  • Unknown information

A structured model might look like this:
public sealed record ProductImageAnalysis(
    IReadOnlyDictionary<string, string> VisibleAttributes,
    IReadOnlyDictionary<string, string> PossibleAttributes,
    IReadOnlyList<string> UnknownAttributes);

For example:
{
  "visibleAttributes": {
    "color": "black",
    "closure": "zipper",
    "surface": "matte"
  },
  "possibleAttributes": {
    "material": "possibly synthetic fabric"
  },
  "unknownAttributes": [
    "exact dimensions",
    "weight",
    "manufacturer",
    "water resistance"
  ]
}


This prevents the application from presenting guesses as verified specifications.

16. Add Explicit Provider Timeouts

An unavailable external provider should not block the worker indefinitely.

A provider implementation can use HttpClient with an explicit timeout:
builder.Services.AddHttpClient(
    "VisionProvider",
    client =>
    {
        client.Timeout =
            TimeSpan.FromSeconds(60);
    });


The provider should also respect the CancellationToken passed by the worker. Timeouts should be classified separately from permanent failures so that transient errors can be retried safely.

17. Retry Only Transient Errors
Not every failure should be retried.

Possible failures include:

  • Invalid input
  • Unsupported image format
  • Provider timeout
  • Rate limiting
  • Invalid provider output
  • Moderation rejection
  • Storage failure
  • Network interruption

A retry policy might behave like this:

  • Invalid input: do not retry
  • Unsupported format: do not retry
  • Timeout: retry with exponential backoff
  • Rate limit: retry after the provider delay
  • Invalid output: retry once with a repair request
  • Storage failure: retry storage without repeating analysis
  • Moderation rejection: do not retry automatically

Repeating the complete pipeline can create duplicate provider charges.

Each expensive stage should be independently recoverable where possible.

18. Delete Temporary Files

The sample keeps uploaded files on disk.
A production application needs a retention policy.

Possible approaches include:

  • Delete the original after successful analysis
  • Keep it for a limited number of hours
  • Keep it only for authenticated users
  • Allow users to delete it manually
  • Store results longer than source images
  • Run scheduled cleanup jobs

Do not rely only on a privacy-policy statement.

Retention rules should be enforced by code.

19. Add Authentication and Rate Limiting

Image analysis can be expensive.

Without limits, an anonymous user may upload many large files and create excessive provider costs.

A production API should consider:

  • User authentication
  • API keys
  • Per-user daily limits
  • Per-IP limits
  • Maximum concurrent tasks
  • Maximum storage usage
  • Mode-specific credit costs
  • Request deduplication

ASP.NET Core rate limiting can protect the upload endpoint before a task reaches the background queue.

20. Improve Observability

Do not log raw images, full OCR results, or private storage URLs by default.
Instead, log safe operational information:

  • Task ID
  • User ID when appropriate
  • Analysis mode
  • File size
  • Image dimensions
  • Processing duration
  • Provider latency
  • Retry count
  • Failure code
  • Queue waiting time
  • Final task status

This provides enough information for monitoring without turning application logs into a database of sensitive image contents.

21. Buffered Uploads Versus Streaming

IFormFile uses buffered upload handling and is convenient for smaller files.
For very large images, high concurrency, or video uploads, consider multipart streaming.
Streaming prevents the application from buffering excessive content in memory or temporary disk space.

The correct choice depends on:

  1. Expected file size
  2. Number of simultaneous uploads
  3. Available memory
  4. Temporary storage performance
  5. Deployment architecture
  6. Whether uploads are sent directly to object storage

22. Why This Architecture Is Maintainable
The design separates responsibilities:

  • The controller handles HTTP behavior
  • The validator handles upload rules
  • The storage service handles files
  • The queue controls asynchronous work
  • The worker coordinates processing
  • The provider handles model integration
  • The task store tracks state

This separation makes the system easier to test and modify.
You can replace the demo provider without changing the API contract.
You can move from local disk to object storage without rewriting the worker.
You can replace the in-memory task store with a database without changing the controller.
You can introduce a durable queue without changing the vision provider.

Conclusion

Building a reliable image analysis API requires more than sending an image to an AI model. A production-oriented system must validate uploads, generate safe file names, control resource usage, store files securely, process long-running tasks asynchronously, expose clear task states, validate model output, and recover from failures without duplicating expensive work. ASP.NET Core provides the core components required for this architecture, including dependency injection, controllers, hosted background services, asynchronous file operations, and structured configuration.

The provider-independent design can be extended for:

  • OCR
  • Alt text
  • Screenshot understanding
  • Product image analysis
  • Document-image extraction
  • Prompt generation
  • Visual question answering
  • Chart interpretation
  • Interface analysis

The most important principle is to treat the vision model as one component inside a larger application boundary.

The model generates an answer, but the surrounding application determines whether that answer is secure, traceable, recoverable, and useful.

Summary

This post showed how to use asynchronous processing, secure file validation, private storage, background workers, task queues, and a pluggable vision provider architecture to create an ASP.NET Core provider-independent image analysis API. The application becomes more scalable, durable, and maintainable for real-world deployments by integrating production-oriented principles including retry methods, rate limitation, observability, and organized task states.

HostForLIFE.eu ASP.NET Core 10.0 Hosting
European best, cheap and reliable ASP.NET hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.



European ASP.NET Core 10.0 Hosting - HostForLIFE :: Using ASP.NET Core's Outbox Pattern for Dependable Event Publishing

clock August 3, 2026 10:51 by author Peter

Current apps frequently have to broadcast an event to a message broker and update a database. An e-commerce program may, for instance, broadcast an OrderCreated event to RabbitMQ, Azure Service Bus, or Kafka after saving an order to SQL Server.

Making sure both operations are successful together is the difficult part. The event is never received by downstream systems if the database transaction commits but event publishing is unsuccessful. On the other hand, customers receive an event for nonexistent data if the event is published but the database transaction fails. By storing events in the same database transaction as the business data and publishing them asynchronously thereafter, the Outbox Pattern resolves this issue.

In this article, you'll build a production-ready Outbox implementation in ASP.NET Core, understand how it works, and learn best practices for reliable event publishing.

    Note: This article focuses on architecture and implementation. Performance depends on workload, database design, polling intervals, and messaging infrastructure.

What Is the Outbox Pattern?

Instead of publishing an event directly after saving data, the application stores the event in an Outbox table within the same database transaction.

A background service later reads pending events and publishes them to the message broker.
Client Request
      │
      ▼
Business Logic
      │
      ▼
Database Transaction
           │
 ┌────┴─────────┐
 ▼                           ▼
Business Data  Outbox Event
      │
      ▼
 Transaction Commit
      │
      ▼
Background Publisher
      │
      ▼
Message Broker

This guarantees that if the business transaction succeeds, the event is never lost.

Why Use the Outbox Pattern?
Without the Outbox Pattern:

  • Database commit succeeds.
  • Message broker becomes unavailable.
  • Event is lost.
  • Downstream services never process the operation.

With the Outbox Pattern:

  • Business data and event are committed together.
  • Event publishing can be retried safely.
  • Temporary messaging failures don't lose events.

Create the Outbox Entity
Create an entity to store pending events.
public class OutboxMessage
{
    public Guid Id { get; set; }

    public string EventType { get; set; } = "";

    public string Payload { get; set; } = "";

    public DateTime CreatedAt { get; set; }

    public DateTime? ProcessedAt { get; set; }
}

Each row represents one event waiting to be published.

Configure EF Core
Register the Outbox table.
public class AppDbContext : DbContext
{
    public DbSet<Order> Orders => Set<Order>();

    public DbSet<OutboxMessage> Outbox =>
        Set<OutboxMessage>();

    public AppDbContext(
        DbContextOptions<AppDbContext> options)
        : base(options)
    {
    }
}


Run the migration to create the table.
dotnet ef migrations add AddOutbox

dotnet ef database update

Save Business Data and Event Together
Suppose an order is created.
public async Task CreateOrderAsync(Order order)
{
    _context.Orders.Add(order);

    var message = new OutboxMessage
    {
        Id = Guid.NewGuid(),
        EventType = "OrderCreated",
        Payload = JsonSerializer.Serialize(order),
        CreatedAt = DateTime.UtcNow
    };

    _context.Outbox.Add(message);

    await _context.SaveChangesAsync();
}

Both the order and the Outbox record are committed in a single database transaction.

Build the Background Publisher
Create a hosted service that processes pending events.
public class OutboxProcessor
    : BackgroundService
{
    private readonly IServiceProvider _provider;

    public OutboxProcessor(
        IServiceProvider provider)
    {
        _provider = provider;
    }

    protected override async Task ExecuteAsync(
        CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using var scope =
                _provider.CreateScope();

            var db = scope.ServiceProvider
                .GetRequiredService<AppDbContext>();

            var events = await db.Outbox
                .Where(x => x.ProcessedAt == null)
                .ToListAsync(stoppingToken);

            foreach (var message in events)
            {
                // Publish to broker

                message.ProcessedAt =
                    DateTime.UtcNow;
            }

            await db.SaveChangesAsync(
                stoppingToken);

            await Task.Delay(
                TimeSpan.FromSeconds(5),
                stoppingToken);
        }
    }
}


The service periodically publishes pending events and marks them as processed.

Register the Hosted Service

builder.Services.AddHostedService<
    OutboxProcessor>();


The publisher starts automatically with the application.

Publishing to a Message Broker

The publishing logic depends on the messaging platform.
await publisher.PublishAsync(
    message.EventType,
    message.Payload);


The Outbox Pattern works with:

  • RabbitMQ
  • Azure Service Bus
  • Apache Kafka
  • Amazon SQS
  • NATS
  • Redis Streams

The persistence strategy remains the same regardless of the broker.

Handling Failures
If publishing fails:

  • Keep the Outbox row unchanged.
  • Retry during the next polling cycle.
  • Mark the event as processed only after successful publishing.

Avoid deleting events before confirming successful delivery.

Prevent Duplicate Processing
A consumer may receive the same event more than once if publishing succeeds but the application crashes before updating ProcessedAt.

Consumers should therefore be idempotent.

Example strategies include:

  • Event IDs
  • Message deduplication
  • Processed message table
  • Unique business keys

Idempotency is a critical part of any reliable messaging architecture.

End-to-End Workflow

A complete request follows these steps:

  • Client submits an order.
  • Order is saved.
  • Outbox event is stored.
  • Database transaction commits.
  • Background service reads pending events.
  • Event is published.
  • Event is marked as processed.
  • Consumers process the event.

This ensures reliable event delivery even during temporary messaging outages.

Outbox Pattern vs Direct Publishing

FeatureDirect PublishOutbox Pattern

Atomic database update

No

Yes

Reliable delivery

Limited

Yes

Retry support

Manual

Built-in

Event persistence

No

Yes

Temporary broker outage handling

Poor

Excellent

Suitable for microservices

Moderate

Excellent

Performance Evaluation Methodology

The research brief references reliability and scalability but does not include benchmark results. Instead of presenting unsupported figures, evaluate your implementation using the following methodology.

Test Environment
Keep these variables consistent:

  • .NET SDK version
  • Database engine
  • Message broker
  • Hardware
  • Network conditions
  • Polling interval

Test Scenarios

Compare:

  • Direct publishing
  • Outbox implementation
  • Broker outage
  • Database failure
  • High event throughput
  • Multiple publisher instances
  • Metrics to Measure

Collect:

  1. Events published per second
  2. Publish latency
  3. Failed publishes
  4. Retry count
  5. Queue backlog
  6. Database growth
  7. CPU utilization
  8. Memory usage

Useful Tools
Useful tools include:

  • BenchmarkDotNet (for application components)
  • dotnet-counters
  • dotnet-trace
  • SQL Server Query Store
  • RabbitMQ Management UI
  • Azure Service Bus metrics
  • Kafka monitoring tools

Validate behavior under production-like failure conditions rather than relying only on successful execution paths.

Best Practices

  • Store Outbox records in the same transaction as business data.
  • Keep event payloads immutable.
  • Include unique event identifiers.
  • Build idempotent consumers.
  • Monitor Outbox table growth.
  • Archive or remove processed events periodically.
  • Implement retry with exponential backoff if appropriate.

Log publishing failures with sufficient context.

Common Mistakes

MistakeImpact

Publishing before committing the database

Lost consistency

Deleting events before successful publish

Event loss

No retry mechanism

Failed events remain unpublished

Non-idempotent consumers

Duplicate processing

Never cleaning the Outbox table

Unbounded database growth

Long polling intervals

Increased event latency

Troubleshooting
Events Remain in the Outbox Table

Verify:

  • Background service is running.
  • Message broker is available.
  • Publishing logic is not throwing exceptions.
  • Database updates are being committed.

Duplicate Events
Review:

  • Consumer idempotency.
  • Event identifiers.
  • Retry logic.
  • Publisher crash recovery.

Outbox Table Grows Continuously
Check:

  • Cleanup strategy.
  • Processing failures.
  • Polling frequency.
  • Broker health.

Implement scheduled archival or deletion of successfully processed events.

FAQs
Why not publish directly after SaveChanges()?

If publishing fails after the database transaction succeeds, the event is permanently lost. The Outbox Pattern eliminates this inconsistency.

Does the Outbox Pattern guarantee exactly-once delivery?
No. It typically guarantees at-least-once delivery. Consumers should be designed to handle duplicate events safely.

Can the Outbox Pattern work with any message broker?
Yes. It is independent of the messaging platform and can be used with RabbitMQ, Azure Service Bus, Kafka, and other brokers.

How often should the background service poll the Outbox table?
The interval depends on latency requirements and workload. Shorter intervals reduce event latency but increase database activity.

Should processed Outbox records be deleted?
Yes. Archive or delete processed records periodically to prevent the Outbox table from growing indefinitely.

Conclusion
A tried-and-true method for dependable event publication in distributed systems is the Outbox Pattern. It removes the possibility of losing events as a result of transient communications failures by storing business data and integration events within the same database transaction. The Outbox Pattern offers a strong basis for event-driven ASP.NET Core applications and microservice architectures when combined with background processing, retries, idempotent consumers, and routine cleanup.

HostForLIFE.eu ASP.NET Core 10.0 Hosting
European best, cheap and reliable ASP.NET hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.



European ASP.NET Core 10.0 Hosting - HostForLIFE :: Handling Global Exceptions in ASP.NET Fundamentals: Creating Secure and Consistent Error Responses

clock July 31, 2026 13:05 by author Peter

In production applications, unhandled exceptions are unavoidable. Unexpected null values occur, database connections break, external APIs stop working, and custom exceptions may be thrown by business rules. These mistakes frequently result in inconsistent answers, reveal private implementation details, and complicate debugging in the absence of centralized exception handling.

Applications may offer consistent error answers while reporting failures for diagnostics thanks to ASP.NET Core's built-in middleware for handling exceptions globally. In addition to improving API accessibility and maintainability, a centralized error-handling approach keeps internal information hidden from clients.

This article describes how to create production-ready global exception handling in ASP.NET Core instead of enclosing each controller action in try-catch blocks.

Note: Without disclosing stack traces, connection strings, or other private implementation information, error replies should give API users enough information to understand what went wrong.

Why Global Exception Handling Matters?
Without centralized exception handling, applications often suffer from:

  • Inconsistent error responses
  • Duplicate try-catch blocks
  • Exposed stack traces
  • Difficult debugging
  • Poor client experience
  • Missing production logs

A single exception handling pipeline keeps error handling consistent across the application.

Common Exception Types

Production applications frequently encounter:

  • Validation exceptions
  • Authentication failures
  • Authorization failures
  • Database exceptions
  • External API failures
  • File system errors
  • Timeout exceptions
  • Business rule violations

Different exceptions should return different HTTP status codes while following the same response format.

Exception Handling Flow

flowchart LR

A[Client Request]
B[ASP.NET Core Middleware]
C[Controller / Service]
D{Exception?}
E[Global Exception Handler]
F[ProblemDetails Response]


A --> B
B --> C
C --> D
D -->|No| A
D -->|Yes| E
E --> F
F --> A


Every unhandled exception flows through the global exception handler before a response is returned to the client.

Using the Built-in Exception Handler

Configure the exception handling middleware.
var app = builder.Build();

app.UseExceptionHandler("/error");

app.MapControllers();

app.Run();

This middleware intercepts unhandled exceptions before they reach the client.

Creating an Error Endpoint

Create a centralized endpoint for handling exceptions.

[ApiExplorerSettings(IgnoreApi = true)]
[Route("/error")]
public class ErrorController : ControllerBase
{
    public IActionResult HandleError()
    {
        return Problem(
            title: "An unexpected error occurred.",
            statusCode: 500);
    }
}


Returning a standardized response makes client-side error handling much simpler.

Using ProblemDetails
ASP.NET Core supports the RFC 7807 Problem Details format.

Example response:
{
  "type": "about:blank",
  "title": "Resource not found.",
  "status": 404,
  "detail": "The requested product does not exist."
}

Using ProblemDetails creates consistent error responses across the API.

Handling Custom Exceptions

Applications often define business-specific exceptions.
public class ProductNotFoundException
    : Exception
{
    public ProductNotFoundException(int id)
        : base($"Product {id} was not found.")
    {
    }
}


Custom exceptions make application logic easier to understand and maintain.

Mapping Exceptions to Status Codes
Different exception types should produce appropriate HTTP responses.

ExceptionHTTP Status
ValidationException 400 Bad Request
UnauthorizedAccessException 401 Unauthorized
ProductNotFoundException 404 Not Found
ConflictException 409 Conflict
TimeoutException 408 Request Timeout
Exception 500 Internal Server Error

Returning meaningful status codes improves API usability and debugging.

Logging Exceptions
Always log unexpected exceptions.
try
{
    await service.ProcessAsync();
}
catch (Exception ex)
{
    logger.LogError(
        ex,
        "Unexpected error while processing request.");

    throw;
}


Structured logging makes production troubleshooting significantly easier.

Returning Validation Errors
Validation failures should return a 400 Bad Request.
if (!ModelState.IsValid)
{
    return ValidationProblem(ModelState);
}

This provides clients with detailed validation information without exposing internal implementation details.

Common Production Mistakes

ProblemRoot Cause
Stack traces returned to clients Developer exception page enabled in production
Inconsistent responses Local try-catch blocks everywhere
Missing logs Exceptions swallowed silently
Incorrect status codes Every exception returns HTTP 500
Difficult debugging No correlation IDs in logs
Sensitive information exposed Internal exception messages returned directly

Most exception handling issues stem from inconsistent implementation rather than framework limitations.

Best Practices

  • Use centralized exception handling middleware.
  • Return consistent ProblemDetails responses.
  • Log every unexpected exception.
  • Map business exceptions to appropriate HTTP status codes.
  • Include correlation IDs in logs.
  • Hide sensitive implementation details from clients.
  • Monitor exception rates using your observability platform.

Common Anti-Patterns
Avoid these common mistakes:

  • Wrapping every controller action in try-catch.
  • Returning stack traces in production.
  • Swallowing exceptions without logging.
  • Returning HTTP 200 for failed operations.
  • Using generic HTTP 500 responses for validation errors.
  • Exposing database or server details in error messages.

FAQ
Should every controller use try-catch?

No. Most unhandled exceptions should be processed by centralized exception handling middleware. Use try-catch only when you can recover from a specific exception locally.

What is ProblemDetails?
ProblemDetails is a standardized error response format defined by RFC 7807. It helps APIs return consistent and machine-readable error information.

Should exception details be returned to clients?
Only when they are safe and useful. Avoid exposing stack traces, SQL queries, connection strings, or other internal implementation details.

How should exceptions be monitored in production?
Use structured logging together with Application Insights, OpenTelemetry, Seq, Elasticsearch, or another observability platform to monitor exception frequency, trends, and root causes.

Conclusion
A key component of creating dependable ASP.NET Core apps is global exception handling. You may increase application security and developer efficiency by centralizing error handling, providing uniform ProblemDetails replies, and regularly reporting unexpected failures. Create a single, well-defined exception handling pipeline that generates consistent replies, safeguards sensitive data, and makes production troubleshooting easier rather than dispersing exception handling functionality throughout your codebase.

HostForLIFE.eu ASP.NET Core 10.0 Hosting
European best, cheap and reliable ASP.NET hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.



European ASP.NET Core 10.0 Hosting - HostForLIFE :: Common Errors and Real-World MediatR Patterns in ASP.NET Core

clock July 24, 2026 11:42 by author Peter

Controllers frequently take on too many responsibilities as ASP.NET Core applications expand, including managing HTTP requests, verifying input, carrying out business logic, and communicating with the data layer. Applications are more difficult to test, maintain, and expand due to this close dependency.

By directing requests through specialized handlers, the well-known library MediatR helps developers separate application components by implementing the Mediator paradigm. Applications that adhere to CQRS and Clean Architecture principles make extensive use of it.

But not all issues can be resolved with MediatR. Although it makes code more organized, when used improperly, it can add needless complexity. We'll look at frequent errors, useful MediatR patterns, and when it makes sense to utilize it in this post.

What Is MediatR?
MediatR is an in-process messaging library that enables communication between different parts of an application without creating direct dependencies.

Instead of a controller calling a service directly:
Controller
    │
ProductService
    │
Repository

The request flows through MediatR:

Controller
    │
MediatR
    │
Request Handler
    │
Repository


The controller only knows about MediatR, while the business logic resides inside dedicated request handlers.

Request and Response Pattern
The most common MediatR pattern is the Request/Response model.

First, define a request:
using MediatR;

public record GetProductQuery(int Id) : IRequest<Product>;


Next, implement the handler:
public class GetProductHandler
    : IRequestHandler<GetProductQuery, Product>
{
    private readonly IProductRepository _repository;

    public GetProductHandler(IProductRepository repository)
    {
        _repository = repository;
    }

    public async Task<Product> Handle(
        GetProductQuery request,
        CancellationToken cancellationToken)
    {
        return await _repository.GetByIdAsync(request.Id);
    }
}


Finally, send the request from a controller:
[HttpGet("{id}")]
public async Task<IActionResult> Get(
    int id,
    IMediator mediator)
{
    var product = await mediator.Send(new GetProductQuery(id));

    return product is null
        ? NotFound()
        : Ok(product);
}


This approach keeps controllers thin and delegates business logic to handlers.

Using Commands for Data Modification

Queries retrieve data, while commands modify it.

Example command:
public record CreateProductCommand(
    string Name,
    decimal Price) : IRequest<int>;


The corresponding handler performs validation, business logic, and persistence before returning the new product ID.

Separating commands and queries improves readability and aligns well with the Command Query Responsibility Segregation (CQRS) pattern.

Notifications for Multiple Actions
Sometimes a single event should trigger multiple independent actions.

For example:

  • Send an email
  • Update inventory
  • Write an audit log
  • Publish an integration event

Instead of placing all logic inside one handler, use notifications.
public record ProductCreatedNotification(int ProductId)
: INotification;


Each notification handler executes independently, making the application easier to extend without modifying existing code.

Pipeline Behaviors

One of MediatR's most powerful features is Pipeline Behaviors.

They allow cross-cutting concerns to execute before or after request handlers.

Common uses include:

  • Validation
  • Logging
  • Performance monitoring
  • Authorization
  • Exception handling

Instead of duplicating logic across handlers, pipeline behaviors centralize these concerns, resulting in cleaner and more maintainable code.

When MediatR Works Best

MediatR provides the greatest value in applications that have:

  • Complex business workflows
  • Multiple use cases
  • Clean Architecture
  • CQRS implementation
  • Large development teams
  • Extensive testing requirements

For these applications, separating requests into dedicated handlers improves maintainability and reduces coupling.

When MediatR May Be Unnecessary

Not every application benefits from MediatR.

For a simple CRUD API with only a few endpoints, adding requests, handlers, and pipeline behaviors may increase complexity without delivering significant value.

A straightforward service layer is often sufficient for:

  • Small internal tools
  • Prototype applications
  • Basic CRUD services
  • Lightweight APIs

Choose MediatR when it solves an architectural problem—not simply because it's popular.

Common Mistakes
Creating a Handler for Every Tiny Operation
Some developers create handlers for trivial methods that simply forward calls to a repository.

For example:

Controller
    │
Handler
    │
Service
    │
Repository


If the handler contains no business logic, MediatR adds an extra layer without improving maintainability.

Putting Business Logic in Controllers

Even when using MediatR, controllers should remain lightweight.

Avoid:

  • Validation logic
  • Business rules
  • Database access

Controllers should receive requests, send them through MediatR, and return responses.

Overusing Notifications

Notifications are excellent for independent actions, but they should not be used when execution order or transactional consistency is critical.

If one operation depends on another, a command handler is usually a better choice.

Ignoring Pipeline Behaviors

Many teams adopt MediatR but continue duplicating validation and logging inside handlers.
Pipeline behaviors provide a cleaner and more reusable solution for cross-cutting concerns.

Best Practices
Keep handlers focused on a single responsibility.
Use commands for writes and queries for reads.

  • Keep controllers thin.
  • Use pipeline behaviors for validation, logging, and exception handling.
  • Inject only the dependencies required by each handler.
  • Avoid creating handlers that simply wrap repository methods.
  • Group requests by feature to improve project organization.
  • Unit test handlers independently from controllers.

HostForLIFE.eu ASP.NET Core 10.0 Hosting
European best, cheap and reliable ASP.NET hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.



European ASP.NET Core 10.0 Hosting - HostForLIFE :: Explaining the CQRS Pattern: Developing Maintainable and Scalable Applications

clock July 20, 2026 12:17 by author Peter

Managing data operations effectively gets more difficult as applications become more sophisticated. Conventional designs frequently read and write data using the same paradigm. Although this strategy is effective for smaller applications, as company needs change, it may become challenging to expand and sustain.

Imagine an online store that simultaneously handles millions of product searches and thousands of orders per minute. Writing order data and accessing product information can have somewhat distinct needs. Performance bottlenecks and increased application complexity may result from using the same model for both processes.

The CQRS (Command Query Responsibility Segregation) paradigm is useful in this situation. By separating read and write processes, CQRS enables independent optimization of each side.

This article will teach you about the architecture, advantages, difficulties, implementation techniques, and recommended practices for contemporary applications of CQRS.

What Is CQRS?
CQRS stands for:
Command Query Responsibility Segregation

The pattern separates:

Commands

Operations that modify data.

Examples:

  • Create Order
  • Update Product
  • Delete User
  • Process Payment

Queries
Operations that retrieve data.

Examples:

  • Get Order Details
  • Search Products
  • View Dashboard
  • Generate Reports

Instead of using a single model for both operations, CQRS creates separate models for reading and writing.

Traditional CRUD Architecture

Most applications start with a CRUD approach.

Application
      ↓
Single Model
      ↓
Database


The same model handles:

  • Create
  • Read
  • Update
  • Delete

While simple, this approach can become difficult to scale in complex systems.

CQRS Architecture

With CQRS:
Application
      ↓
Commands
      ↓
Write Model
      ↓
Database

Queries
      ↓
Read Model
      ↓
Read Database

Read and write operations are separated.

This allows each side to evolve independently.
Why Use CQRS?
CQRS addresses several common application challenges.

Independent Scaling
Read workloads often exceed write workloads.

Example:
10000 Reads

500 Writes

CQRS allows read systems to scale independently.

Improved Performance

Read models can be optimized specifically for queries.

Better Maintainability
Business logic becomes easier to organize.

Flexible Data Models

Read models and write models can have different structures.

Easier Integration with Event-Driven Systems
CQRS works naturally with event sourcing and messaging systems.

Understanding Commands

Commands represent actions that change system state.

Example:
CreateCustomer

A command contains:

  • Intent
  • Input data
  • Validation rules

Example:
public record CreateCustomerCommand(
    string Name,
    string Email
);


Commands do not return data.

They indicate that something should happen.

Command Handlers

Command handlers process commands.

Example:
public class CreateCustomerHandler
{
    public async Task Handle(
        CreateCustomerCommand command)
    {
        // Save customer
    }
}

Responsibilities include:

  • Validation
  • Business rules
  • Data persistence

Handlers focus exclusively on write operations.

Understanding Queries

Queries retrieve information without changing data.

Example:
GetCustomerById


Query example:
public record GetCustomerQuery(
    int CustomerId
);


Queries should never modify application state.

Query Handlers
Query handlers process read requests.

Example:
public class GetCustomerHandler
{
    public async Task<CustomerDto>
    Handle(
        GetCustomerQuery query)
    {
        return customer;
    }
}


Query handlers focus on data retrieval and presentation.

Read Models vs Write Models

One of CQRS's biggest advantages is model separation.

Write Model
Optimized for:

  • Business rules
  • Validation
  • Transactions

Example:
Customer Entity

Read Model
Optimized for:

  • Fast retrieval
  • Reporting
  • Search operations

Example:
Customer Dashboard View

Different models serve different purposes.

CQRS Workflow

A typical workflow:

Command Flow

User Request
      ↓
Command
      ↓
Command Handler
      ↓
Database


Query Flow
User Request
      ↓
Query
      ↓
Query Handler
      ↓
Read Database

The two paths remain independent.

CQRS with MediatR in ASP.NET Core

MediatR is commonly used to implement CQRS.

Install package:
dotnet add package MediatR

Register MediatR:
builder.Services
.AddMediatR(cfg =>
{
    cfg.RegisterServicesFromAssembly(
        typeof(Program).Assembly);
});


MediatR simplifies command and query handling.

Command Example

Command:
public record CreateOrderCommand(
    string Product,
    decimal Price
);


Handler:
public class CreateOrderHandler
{
    public async Task Handle(
        CreateOrderCommand command,
        CancellationToken token)
    {
        // Save order
    }
}


This represents the write side.

Query Example
Query:
public record GetOrderQuery(
    int Id
);


Handler:
public class GetOrderHandler
{
    public async Task<OrderDto>
    Handle(
        GetOrderQuery query,
        CancellationToken token)
    {
        return order;
    }
}


This represents the read side.

CQRS and Event Sourcing
CQRS is frequently combined with Event Sourcing.

Instead of storing current state:

Order Status:
Shipped


Store events:
Order Created

Order Paid

Order Shipped

Benefits include:

  • Complete audit trail
  • Historical reconstruction
  • Improved traceability

Many event-driven systems use both patterns together.

CQRS in Microservices
CQRS works well in microservice architectures.

Example:
Order Service
      ↓
Events
      ↓
Read Models

Reporting Service


Analytics Service

Each service can maintain its own optimized read model.
This improves scalability and autonomy.

Practical Example
Consider an online store.

Write operation:
Place Order

Command handler:
Validate Payment
      ↓
Create Order
      ↓
Save Database


Read operation:
View Order History

Query handler:
Retrieve Read Model
      ↓
Return Results

Each workflow is optimized independently.

Benefits of CQRS
Better Scalability
Read and write workloads scale separately.

Improved Performance
Optimized read models improve query speed.

Clear Separation of Responsibilities
Business logic becomes easier to maintain.

Flexible Data Structures
Different models for different needs.

Easier Integration

Works naturally with event-driven architectures.

These benefits become increasingly valuable in large applications.

Challenges of CQRS

CQRS is not without trade-offs.

Increased Complexity
Additional models and handlers are required.

More Infrastructure

Separate read and write paths must be maintained.

Eventual Consistency
Read models may not update instantly.

Higher Learning Curve
Teams must understand additional patterns and concepts.
For small applications, CQRS may introduce unnecessary complexity.

When Should You Use CQRS?

CQRS is a strong choice when:

  • Read and write workloads differ significantly.
  • Business logic is complex.
  • Scalability is important.
  • Event-driven architecture is planned.
  • Multiple read models are required.

Avoid CQRS when:

  • Applications are small.
  • Requirements are simple.
  • CRUD operations dominate.

Complexity should be justified by business needs.

Best Practices
When implementing CQRS:

  • Keep commands focused.
  • Separate read and write models clearly.
  • Avoid sharing entities between sides.
  • Use DTOs for queries.
  • Implement validation at the command level.
  • Monitor eventual consistency.
  • Consider MediatR for ASP.NET Core projects.
  • Introduce CQRS gradually when possible.

These practices improve maintainability and reduce complexity.

Common Mistakes to Avoid
Avoid these common issues:

  • Applying CQRS to simple CRUD systems.
  • Mixing query logic into command handlers.
  • Sharing models unnecessarily.
  • Ignoring eventual consistency.
  • Creating overly complex architectures.
  • Using CQRS without clear business justification.

The pattern should solve a real problem, not create one.

HostForLIFE.eu ASP.NET Core 10.0 Hosting
European best, cheap and reliable ASP.NET hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.



European ASP.NET Core 10.0 Hosting - HostForLIFE :: How to Use AI-Powered Code Analysis to Update Outdated.NET Applications?

clock June 26, 2026 06:58 by author Peter

Legacy.NET apps that have long supported vital business operations are still used by many enterprises. Even though these apps are frequently reliable, when business needs change, they may become expensive to update, difficult to scale, and difficult to manage. Developers must manually examine thousands of lines of code, find technical debt, comprehend antiquated architectures, and plan migrations as part of traditional modernization initiatives. This procedure takes a long time and frequently poses needless hazards.

The way development teams approach modernization is evolving thanks to AI-powered code inspection. Developers can modernize systems more quickly and with less human labor by using AI to scan source code, find trends, uncover security flaws, and suggest fixes. This post will explain the operation of AI-powered code analysis, its advantages for legacy.NET apps, and useful methods for integrating AI into your modernization plan.

AI-Powered Code Analysis: What Is It?
Large language models (LLMs) and machine learning methods are used by AI-powered code analysis to comprehend software projects beyond the capabilities of conventional static analyzers. AI is able to do more than only verify syntax or coding principles.

Instead of checking only syntax or coding rules, AI can:

  • Explain complex legacy code
  • Identify outdated frameworks and APIs
  • Detect architectural issues
  • Recommend code refactoring
  • Generate documentation
  • Suggest performance improvements
  • Find security vulnerabilities
  • Assist with framework migration

Rather than replacing developers, AI acts as an intelligent assistant that accelerates the modernization process.

Challenges with Legacy .NET Applications

Modernizing older applications involves several common challenges.

Limited Documentation

Many legacy systems have little or no documentation, making it difficult for new developers to understand the application.

Large Codebases

Enterprise applications often contain hundreds of projects and thousands of source files that require analysis.

Outdated Technologies
Legacy applications may still use older versions of ASP.NET, Web Forms, WCF, or deprecated libraries that need replacement.

Technical Debt
Years of incremental changes often result in duplicated code, tightly coupled components, and inconsistent coding standards.

Migration Risks

Without understanding application dependencies, modernization efforts can unintentionally introduce breaking changes.

How AI Helps Modernize Legacy Applications?

AI significantly reduces the effort required to understand and improve existing applications.

Code Explanation

AI can summarize complex methods and classes in plain language.
For example, instead of manually tracing multiple function calls, developers can quickly understand what a method is doing.

Dependency Analysis

AI helps identify relationships between projects, services, APIs, and databases, making migration planning easier.

Refactoring Suggestions

AI recommends cleaner implementations by simplifying lengthy methods, removing duplicate logic, and improving readability.

Security Recommendations

AI can identify insecure coding patterns such as hardcoded secrets, weak authentication, or unsafe SQL queries.

Migration Guidance

AI can recommend modern alternatives for obsolete APIs and libraries, helping developers transition to newer .NET technologies.
Example: Simplifying Legacy Code

Consider the following legacy method.
public string GetStatus(int status)
{
    if(status == 1)
        return "Active";
    else if(status == 2)
        return "Inactive";
    else if(status == 3)
        return "Pending";
    else
        return "Unknown";
}


An AI assistant may recommend replacing multiple conditional statements with a switch expression.
public string GetStatus(int status)
{
    return status switch
    {
        1 => "Active",
        2 => "Inactive",
        3 => "Pending",
        _ => "Unknown"
    };
}

The updated version is easier to read, maintain, and extend.

AI-Assisted Documentation Generation

One of the biggest obstacles in modernization is missing documentation.
AI can generate summaries for classes and methods automatically.

Example:
public class CustomerService
{
    public Customer GetCustomer(int id)
    {
        // Business logic
    }
}


AI-generated documentation might describe it as:
Retrieves customer information based on the provided customer ID and returns the corresponding customer object.

This helps teams quickly understand unfamiliar codebases.

AI for Performance Optimization

Legacy applications often contain inefficient queries or redundant processing.

AI can recommend improvements such as:

  1. Optimizing LINQ queries
  2. Reducing unnecessary object creation
  3. Eliminating duplicate database calls
  4. Improving asynchronous programming
  5. Replacing blocking operations with async methods

Example:
var customer = await repository.GetCustomerAsync(id);

Using asynchronous methods improves application responsiveness and scalability.

Best Practices for AI-Assisted Modernization
Start with Code Analysis

Before making changes, use AI to analyze the overall architecture and identify high-priority modernization areas.

Validate AI Recommendations

AI-generated suggestions should always be reviewed by experienced developers before implementation.

Modernize Incrementally

Avoid rewriting the entire application at once. Update individual modules, services, or APIs in manageable phases.

Combine AI with Automated Testing
Maintain comprehensive unit and integration tests to ensure functionality remains intact after refactoring.

Focus on High-Impact Areas

Prioritize components that have the greatest effect on maintainability, performance, or security.

A Typical AI Modernization Workflow
A structured modernization process might include:

  • Analyze the codebase using AI.
  • Identify obsolete frameworks and dependencies.
  • Generate documentation for undocumented components.
  • Detect technical debt and security issues.
  • Refactor high-priority modules.
  • Validate changes using automated testing.
  • Deploy modernized components incrementally.

This approach minimizes risks while allowing teams to deliver continuous improvements.

Benefits of AI-Powered Code Analysis

Organizations adopting AI-assisted modernization can experience several advantages.

  • Faster code reviews
  • Reduced manual analysis
  • Better understanding of legacy systems
  • Improved code quality
  • Lower modernization costs
  • Enhanced security
  • Easier onboarding for new developers
  • Faster migration to modern .NET platforms

These benefits allow development teams to focus on solving business problems rather than spending excessive time understanding outdated code.

Conclusion
Long-term migration efforts and manual code reviews are no longer the only ways to modernize legacy.NET applications. Developers may better understand current systems, find technical debt, create documentation, suggest refactoring possibilities, and enhance overall program quality with AI-powered code analysis.

Organizations may minimize risks and upgrade systems more effectively by combining developer knowledge with AI insights. AI is a potent productivity tool that speeds up modernization efforts and assists teams in creating scalable, secure, and maintainable.NET apps for the future rather than taking the place of seasoned workers.

HostForLIFE.eu ASP.NET Core 10.0 Hosting
European best, cheap and reliable ASP.NET hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.



European ASP.NET Core 10.0 Hosting - HostForLIFE :: Using ASP.NET Core to Create AI Governance Platforms

clock June 23, 2026 07:32 by author Peter

Organizations are facing an increasing difficulty as AI is fully incorporated into enterprise applications: how to effectively regulate AI systems. AI models are impacting business decisions across industries, producing content, automating workflows, and providing suggestions. These capabilities add substantial commercial value, but they also bring with them issues pertaining to operational control, responsibility, security, transparency, and compliance.

Software, infrastructure, and data governance frameworks are found in many businesses. However, previous systems were not intended to handle the novel governance requirements brought about by AI.

Questions such as the following are becoming increasingly common:

  • Which AI models are currently deployed?
  • Who approved a model for production use?
  • What data was used to train the model?
  • Why did the AI generate a particular response?
  • Is the model compliant with company policies?
  • When should a model be retired?

To answer these questions, organizations are building AI Governance Platforms. In this article, we will explore how to design and build AI governance platforms using ASP.NET Core and modern enterprise architecture principles.

What Is an AI Governance Platform?

An AI Governance Platform is a centralized system that manages the lifecycle, compliance, monitoring, and oversight of AI assets across an organization.
The platform helps organizations control:

  • AI models
  • Prompts
  • Agents
  • Knowledge repositories
  • AI workflows
  • Data sources
  • AI policies
  • Compliance requirements

The objective is to ensure AI systems remain trustworthy, transparent, secure, and aligned with business goals.

Why AI Governance Matters?

Without governance, AI adoption can create significant operational and compliance challenges.

Common risks include:
Unapproved Models
Teams may deploy models without formal review processes.

Data Privacy Violations
AI systems may access sensitive information improperly.

Inconsistent Behavior

Different teams may implement AI solutions differently.

Lack of Auditability

Organizations may struggle to explain AI-generated decisions.

Compliance Challenges

Regulated industries often require strict oversight of automated decision-making systems.

Governance provides structure and accountability.
Core Components of an AI Governance Platform
A modern governance platform typically includes several layers.

AI Asset Registry

Maintains a catalog of all AI assets.

Policy Management

Stores governance policies and compliance rules.

Approval Workflow Engine

Controls deployment and change approval processes.

Monitoring and Audit Layer

Tracks AI activity and operational behavior.

Risk Assessment Engine

Identifies governance and compliance risks.

Reporting Dashboard

Provides visibility into governance metrics.

Designing an AI Asset Model
Let's begin by defining a simple model.
public class AiAsset
{
    public Guid Id { get; set; }

    public string Name { get; set; }

    public string AssetType
    {
        get; set;
    }

    public string Owner
    {
        get; set;
    }

    public string Status
    {
        get; set;
    }
}


Examples of assets include:

  • Chatbots
  • Recommendation engines
  • AI agents
  • Knowledge systems
  • Machine learning models

This inventory forms the foundation of governance.

Creating a Governance Policy Model

Policies define organizational requirements.

public class GovernancePolicy
{
    public string PolicyName
    {
        get; set;
    }

    public string Description
    {
        get; set;
    }

    public bool IsMandatory
    {
        get; set;
    }
}


Examples include:

  • Data privacy policies
  • Security requirements
  • Model approval rules
  • Audit requirements

Building an Approval Workflow
Before deployment, AI systems should pass through governance reviews.

Example workflow:

AI Development
       ↓
Risk Assessment
       ↓
Compliance Review
       ↓
Security Approval
       ↓
Production Deployment


This ensures proper oversight before AI systems become operational.

Practical Example

Imagine a team building a customer support assistant.

Before deployment, the governance platform evaluates:

Model:
Customer Support Assistant

Risk Level:
Medium

Compliance Status:
Passed

Security Review:
Approved

Deployment Status:
Authorized


The platform records all approvals and decisions for future audits.

Monitoring AI Usage
Governance does not end after deployment.
Organizations should monitor:

  • Model usage
  • Prompt execution
  • User interactions
  • Data access patterns
  • Response quality

Example metrics:
Daily Requests:
18,000

Average Response Time:
2.1 Seconds

Policy Violations:
0

Compliance Score:
98%


These insights support ongoing governance efforts.

Managing AI Risk

Risk management is a critical governance capability.

Common risk categories include:

Security Risks
Unauthorized access or misuse.

Compliance Risks

Regulatory violations.

Operational Risks
System failures and service disruptions.

Data Risks
Exposure of sensitive information.

Model Risks
Incorrect or biased outputs.
AI governance platforms should continuously assess and report these risks.

Supporting Audit Requirements

Many organizations must demonstrate how AI systems operate.

Audit records should include:

  • Model versions
  • Approval history
  • Policy compliance status
  • Deployment dates
  • User interactions
  • Configuration changes

Example:
Asset:
Sales Recommendation Model

Version:
2.1

Approved By:
AI Governance Board

Deployment Date:
March 12

Status:
Active

This improves accountability and transparency.

Integrating with ASP.NET Core Applications

Governance services can be integrated into existing applications.

Example architecture:
ASP.NET Core Application
          ↓
Governance API
          ↓
Policy Engine
          ↓
Compliance Validation
          ↓
Approval Decision

This allows governance checks to occur automatically during deployment and runtime operations.

Common Use Cases

AI governance platforms support many scenarios.

Enterprise AI Programs

Manage large AI portfolios.

Financial Services
Govern decision-making models.

Healthcare Systems

Monitor clinical AI solutions.

Government Applications

Ensure transparency and compliance.

Customer Experience Platforms

Control AI-powered support services.

Best Practices

Maintain a Complete AI Inventory
Track all AI assets across the organization.

Automate Governance Checks
Reduce manual effort where possible.

Define Clear Ownership

Assign responsibility for every AI asset.

Monitor Continuously

Governance should extend beyond deployment.

Maintain Audit Trails

Record important decisions and activities.

Review Policies Regularly

Governance requirements evolve over time.

Balance Innovation and Control

Enable responsible AI adoption without slowing development.

Challenges to Consider
Organizations should prepare for several challenges.

Rapid AI Evolution

Technology changes faster than governance frameworks.

Distributed Ownership

AI systems may be managed by multiple teams.

Regulatory Complexity

Requirements vary across industries and regions.

Scalability

  • Governance processes must scale as AI adoption grows.
  • Addressing these challenges helps build sustainable governance programs.

Conclusion
Organizations are facing an increasing difficulty as AI is fully incorporated into enterprise applications: how to effectively regulate AI systems. AI models are impacting business decisions across industries, producing content, automating workflows, and providing suggestions. These capabilities add substantial commercial value, but they also bring with them issues pertaining to operational control, responsibility, security, transparency, and compliance. Software, infrastructure, and data governance frameworks are found in many businesses. However, previous systems were not intended to handle the novel governance requirements brought about by AI.

HostForLIFE.eu ASP.NET Core 10.0 Hosting
European best, cheap and reliable ASP.NET hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.



European ASP.NET Core 10.0 Hosting - HostForLIFE :: The Complete Guide to ASP.NET Core Observability and Resilience

clock June 15, 2026 07:09 by author Peter

Developing a local application is just the first step. What occurs in production is the real litmus test for software engineering. How fast can you identify the underlying problem when a null reference exception ends a user session, a third-party API rate-limits your server, or a database query hangs?

Diagnosing problems in a distributed system is like trying to identify a needle in a haystack if you rely on simple text files and dispersed try/catch sections. Three pillars must be mastered in order to create an enterprise-grade ASP.NET Core application that is really resilient: Centralized Log Aggregation, the Middleware Pipeline, and Structured Logging. Let's investigate how to put this architecture into practice.

Phase 1: The Foundation for Observability (Structured Logging)
Treating logging as a means of writing text to a console is the most common error made by developers.
The String Interpolation Anti-Pattern:

_logger.LogInformation($"User {userId} successfully authenticated at {DateTime.Now}");

This generates a flat string. When you have millions of logs, you cannot easily query your database for "all logs where the UserId was 123".

The Best Practice (Structured Logging):
_logger.LogInformation("User {UserId} successfully authenticated.", userId);

By using message templates ({UserId}), ASP.NET Core preserves the property names and their values. Your logging provider stores UserId as an indexed, queryable column.
Applying Structured Logging Layer-by-Layer

To make an application truly observable, logging must be treated as a first-class citizen across all layers:

  • Controllers (The Entry Point): Log who is making the request and what the outcome is. If an Admin locks a user's account, capture the Admin's ID, not just the target user's ID, to create a secure audit trail.
  • Services (The Business Logic): Log the intent and outcome of external integrations. When calling third-party gateways, catch specific exceptions (like HttpRequestException) and log them, so you don't mistake a firewall block for a bug in your own code. Never log secrets, API keys, or passwords.
  • Repositories (The Database Frontier): Log the execution of queries. During logging audits, you will often find performance anti-patterns. For example, replacing a synchronous .FirstOrDefault() with an asynchronous .FirstOrDefaultAsync() prevents catastrophic thread-pool starvation under heavy load. Always log the Exception object in your catch blocks so stack traces aren't swallowed.

Phase 2: Mastering the Control Flow (Middleware)
Before an HTTP request ever reaches your Controller, it travels through the Middleware Pipeline. Think of middleware like a series of water filters. Each component can examine the request, modify it, pass it to the next component, or "short-circuit" and immediately return a response.

Because each middleware wraps the next one, the order in which you register them in Program.cs is critical.

Global Exception Handling (Must be first to catch errors from everything below it)

  • HTTPS Redirection & Static Files
  • Routing (Figure out where the request is going)
  • Authentication (Who are you?)
  • Authorization (Are you allowed to be here?)
  • Custom Middleware (e.g., API Key Validation)
  • Controllers / Endpoints


Phase 3: The Ultimate Safety Net (Global Exception Handling)
Sprinkling try/catch blocks inside every single Controller action makes your code messy and prone to data leakage. Instead, we use the Middleware Pipeline to build a Global Exception Handler.

By placing this middleware at the absolute top of the pipeline, it wraps the entire application in a try/catch. If any code throws an unhandled exception, it bubbles up to this middleware, which logs the exact error and returns a clean, standardized JSON response to the client—without leaking sensitive stack traces.
public class GlobalExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<GlobalExceptionMiddleware> _logger;

    public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context); // Pass request down the pipeline
        }
        catch (Exception ex)
        {
            // The request blew up somewhere, and the error bubbled back up to here!
            context.Response.ContentType = "application/json";
            context.Response.StatusCode = 500; // Default to Internal Server Error

            // Map specific exceptions to HTTP Status Codes
            if (ex is UnauthorizedAccessException) context.Response.StatusCode = 401;
            if (ex is KeyNotFoundException) context.Response.StatusCode = 404;

            // Log the structured error
            _logger.LogError(ex, "Unhandled exception on {RequestPath}. Method: {RequestMethod}", context.Request.Path, context.Request.Method);

            // Return a safe, standard JSON response
            var errorResponse = JsonSerializer.Serialize(new {
                Status = context.Response.StatusCode,
                Message = "An unexpected error occurred."
            });
            await context.Response.WriteAsync(errorResponse);
        }
    }
}


To see exactly how a request flows through the pipeline and how exceptions bubble up to be caught by this middleware, you can run the interactive simulation below.

Phase 4: The Centralized Brain (Serilog)
Now that your application is emitting beautiful structured logs and gracefully catching every exception, you need a place to view them. Local text files are useless in a multi-server production environment.

We integrate Serilog to aggregate these logs and ship them to a centralized platform (like Seq, Datadog, or Elasticsearch). Because we used standard ILogger everywhere, we don't need to change our business logic; we just wire Serilog into Program.cs using the Two-Stage Initialization pattern.

  • Install Packages: Serilog.AspNetCore, Serilog.Sinks.Console, Serilog.Sinks.Seq (or File).
  • Configure appsettings.json: Define your log levels and routing destinations without hardcoding them.
  • Bootstrap in Program.cs: Catch startup errors and replace noisy default HTTP logging with clean, single-line logs.

using Serilog;
// 1. Catch startup errors before the app even builds
Log.Logger = new LoggerConfiguration().WriteTo.Console().CreateBootstrapLogger();

try
{
    var builder = WebApplication.CreateBuilder(args);

    // 2. Wire Serilog into the ASP.NET Core host
    builder.Host.UseSerilog((context, services, configuration) => configuration
        .ReadFrom.Configuration(context.Configuration)
        .Enrich.FromLogContext());

    var app = builder.Build();

    // 3. Register our safety net FIRST
    app.UseMiddleware<GlobalExceptionMiddleware>();

    // 4. Clean, single-line HTTP Request Logging
    app.UseSerilogRequestLogging();

    app.UseRouting();
    app.MapControllers();
    app.Run();
}
catch (Exception ex)
{
    Log.Fatal(ex, "Host terminated unexpectedly");
}
finally
{
    Log.CloseAndFlush();
}


Conclusion

You can turn your application from a brittle black box into a highly observable, robust enterprise system by integrating Structured Logging, a thorough grasp of the Middleware Pipeline, a Global Exception Handler, and Serilog. You won't be speculating when the unavoidable production problem arises. Your consolidated dashboard will receive precise, queryable context, enabling you to find and fix the underlying issue in a matter of minutes.

HostForLIFE.eu ASP.NET Core 10.0 Hosting
European best, cheap and reliable ASP.NET hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.



European ASP.NET Core 10.0 Hosting - HostForLIFE :: OpenTelemetry in.NET: Complete Observability for Contemporary Uses

clock June 10, 2026 07:23 by author Peter

Applications in the modern era are no longer straightforward monolithic systems. Microservices, APIs, cloud resources, databases, message queues, and third-party integrations are frequently used in today's software to provide business functionality. Although this architecture increases flexibility and scalability, troubleshooting becomes much more difficult.

Developers need to see what's going on throughout the system when an application starts to lag or malfunction. Because it is difficult to see how requests flow between services or pinpoint performance bottlenecks, traditional logging by itself is frequently insufficient.

OpenTelemetry can help with this. With a uniform approach to gathering telemetry data, including logs, metrics, and traces, OpenTelemetry has emerged as the industry standard for observability. OpenTelemetry helps teams diagnose problems more quickly and increase system reliability by giving developers in the.NET ecosystem end-to-end visibility into application activity.

In this article, you'll learn what OpenTelemetry is, why observability matters, how OpenTelemetry works in .NET applications, and best practices for implementing effective observability.
What Is OpenTelemetry?
OpenTelemetry is an open-source observability framework that provides standardized APIs, SDKs, and tools for collecting telemetry data.

It helps developers capture three key observability signals:

  • Traces
  • Metrics
  • Logs

These signals work together to provide a complete view of application health and performance.

Instead of relying on vendor-specific monitoring solutions, OpenTelemetry offers a vendor-neutral approach that can integrate with various monitoring platforms.

The basic workflow looks like this:
Application
      ↓
OpenTelemetry SDK
      ↓
Telemetry Data
      ↓
Monitoring Platform


This standardized approach simplifies observability across different environments and technologies.

Understanding the Three Pillars of Observability
Traces

Tracing follows a request as it moves through multiple services.

Consider an e-commerce application:
User Request
      ↓
API
      ↓
Order Service
      ↓
Database
      ↓
Payment Service

Distributed tracing allows developers to see exactly where time is being spent and identify failures across service boundaries.

Metrics
Metrics provide numerical measurements about application behavior.

Examples include:

  • Request count
  • Response time
  • Error rate
  • CPU usage
  • Memory consumption

Metrics help teams monitor trends and detect potential problems before users are affected.

Logs
Logs provide detailed records of application events.

Examples include:

  • Exceptions
  • Warnings
  • Authentication failures
  • Business events

While traces show request flow and metrics show trends, logs provide detailed context.

Together, these three signals create a comprehensive observability strategy.

Why OpenTelemetry Matters for .NET Applications

Many organizations operate distributed systems built with:

  • ASP.NET Core APIs
  • Microservices
  • Azure services
  • Background workers
  • Containerized workloads

Without observability, diagnosing issues becomes difficult.

Common challenges include:

  • Slow API responses
  • Database bottlenecks
  • Service communication failures
  • Unexpected exceptions
  • Resource consumption spikes

OpenTelemetry helps developers quickly identify root causes by providing visibility across the entire application ecosystem.

Installing OpenTelemetry Packages
To get started, install the required packages.
dotnet add package OpenTelemetry.Extensions.Hosting

dotnet add package OpenTelemetry.Instrumentation.AspNetCore


dotnet add package OpenTelemetry.Instrumentation.Http

dotnet add package OpenTelemetry.Exporter.Console

These packages enable telemetry collection and export capabilities.

Configuring OpenTelemetry in ASP.NET Core
Register OpenTelemetry during application startup.

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing =>
    {
        tracing
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddConsoleExporter();
    });


This configuration automatically captures:

  • Incoming HTTP requests
  • Outgoing HTTP requests
  • Request durations
  • Trace information

Developers immediately gain visibility into application activity.

Understanding Distributed Tracing
Distributed tracing is one of OpenTelemetry's most valuable capabilities.

Imagine a request flowing through multiple services.
Client
   ↓
API Gateway
   ↓
Product Service
   ↓
Inventory Service
   ↓
Database


Without tracing, identifying performance issues requires examining logs across multiple systems.

With distributed tracing:
Trace ID
   ↓
Entire Request Journey

Developers can follow a request from start to finish and quickly locate bottlenecks.

This is especially useful in microservices architectures.

Collecting Metrics

Metrics help monitor system health over time.

Add metrics support:
builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics =>
    {
        metrics
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddRuntimeInstrumentation()
            .AddConsoleExporter();
    });


Common metrics include:

  • Request duration
  • Throughput
  • Memory usage
  • Garbage collection activity

These measurements help teams understand application performance trends.

Custom Tracing

In addition to automatic instrumentation, developers can create custom traces.

Example:
using System.Diagnostics;

var activitySource =
    new ActivitySource("OrderProcessing");

using var activity =
    activitySource.StartActivity("CreateOrder");

activity?.SetTag("OrderId", 1001);

Custom tracing is useful for:

  • Business workflows
  • Order processing
  • Payment operations
  • Inventory updates

This provides visibility into application-specific processes.

Exporting Telemetry Data
OpenTelemetry collects telemetry data, but organizations typically send it to monitoring platforms.

The workflow becomes:

Application
      ↓
OpenTelemetry
      ↓
Collector
      ↓
Monitoring Platform


This flexibility is one of OpenTelemetry's biggest advantages.

Teams can change monitoring vendors without rewriting application instrumentation.

OpenTelemetry and Microservices

Observability becomes increasingly important as systems grow.
Consider a microservices environment:

API Gateway
     ↓
User Service
     ↓
Order Service
     ↓
Payment Service
     ↓
Notification Service

A failure in any component can affect the user experience.

OpenTelemetry helps answer questions such as:

  • Which service is slow?
  • Where did the error occur?
  • How long did each operation take?
  • Which dependency is causing issues?

Without observability, answering these questions can take hours.

With OpenTelemetry, answers are often available within minutes.

Best Practices
Instrument Early

Add observability during development rather than after deployment.

Retrofitting telemetry later is often more difficult.

Use Automatic Instrumentation

Leverage built-in instrumentation whenever possible.
This reduces implementation effort and ensures consistency.

Add Business-Level Traces
Technical telemetry is important, but business workflows should also be traced.

Examples include:

  • Order creation
  • Payment processing
  • User registration

These traces provide valuable operational insights.

Monitor Critical Metrics

Focus on metrics that directly impact users:

  • Latency
  • Error rates
  • Throughput
  • Availability

Avoid collecting unnecessary data.

Standardize Naming

Use consistent names for:

  • Services
  • Activities
  • Metrics
  • Tags

Consistency improves troubleshooting and reporting.

Protect Sensitive Information
Never expose:

  • Passwords
  • Access tokens
  • Personal information

through telemetry data.

Observability should enhance visibility without creating security risks.

HostForLIFE.eu ASP.NET Core 10.0 Hosting
European best, cheap and reliable ASP.NET hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.



European ASP.NET Core 10.0 Hosting - HostForLIFE :: How Claude AI Can Be Integrated with .NET Applications?

clock June 8, 2026 07:14 by author Peter

Artificial intelligence is quickly taking center stage in contemporary software applications. .NET developers can now create intelligent chatbots, document processing systems, AI assistants, code generation tools, and enterprise automation solutions with Anthropic's Claude API. The entire process of incorporating Claude AI into an ASP.NET Core application using C# will be covered in this tutorial. You will discover how to set up the API, develop reusable services, manage requests, and make AI features available via REST endpoints.

Creating a New ASP.NET Core Project
Create a new Web API project:
dotnet new webapi -n ClaudeAIIntegration
cd ClaudeAIIntegration

Run the project:
dotnet run

Install Required Packages
Add the following packages:
dotnet add package Microsoft.Extensions.Http
dotnet add package Newtonsoft.Json


Configure Claude API Settings
Add configuration to appsettings.json:
{
  "ClaudeAI": {
    "ApiKey": "YOUR_API_KEY",
    "BaseUrl": "https://api.anthropic.com/v1/messages",
    "Model": "claude-sonnet-4-0"
  }
}


Create a configuration model:
public class ClaudeSettings
{
    public string ApiKey { get; set; }
    public string BaseUrl { get; set; }
    public string Model { get; set; }
}


Create Request Models
ClaudeRequest.cs
public class ClaudeRequest
{
    public string Prompt { get; set; }
}


ClaudeResponse.cs
public class ClaudeResponse
{
    public string Content { get; set; }
}


Build Claude AI Service
Create Services/ClaudeService.cs
using System.Text;
using Newtonsoft.Json;

public class ClaudeService
{
    private readonly HttpClient _httpClient;
    private readonly IConfiguration _configuration;

    public ClaudeService(
        HttpClient httpClient,
        IConfiguration configuration)
    {
        _httpClient = httpClient;
        _configuration = configuration;
    }

    public async Task<string> GetResponseAsync(string prompt)
    {
        var apiKey = _configuration["ClaudeAI:ApiKey"];
        var model = _configuration["ClaudeAI:Model"];
        var endpoint = _configuration["ClaudeAI:BaseUrl"];

        _httpClient.DefaultRequestHeaders.Clear();

        _httpClient.DefaultRequestHeaders.Add(
            "x-api-key",
            apiKey);

        _httpClient.DefaultRequestHeaders.Add(
            "anthropic-version",
            "2023-06-01");

        var requestBody = new
        {
            model = model,
            max_tokens = 1024,
            messages = new[]
            {
                new
                {
                    role = "user",
                    content = prompt
                }
            }
        };

        var json =
            JsonConvert.SerializeObject(requestBody);

        var content =
            new StringContent(
                json,
                Encoding.UTF8,
                "application/json");

        var response =
            await _httpClient.PostAsync(
                endpoint,
                content);

        response.EnsureSuccessStatusCode();

        return await response.Content.ReadAsStringAsync();
    }
}

Register Services
Update Program.cs:
builder.Services.AddHttpClient();
builder.Services.AddScoped<ClaudeService>();


Create API Controller
Controllers/ClaudeController.cs

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class ClaudeController : ControllerBase
{
    private readonly ClaudeService _claudeService;

    public ClaudeController(
        ClaudeService claudeService)
    {
        _claudeService = claudeService;
    }

    [HttpPost]
    public async Task<IActionResult> Ask(
        ClaudeRequest request)
    {
        var result =
            await _claudeService
                .GetResponseAsync(
                    request.Prompt);

        return Ok(result);
    }
}

Test the Endpoint
POST Request:

POST /api/claude


Request Body:
{
  "prompt": "Explain dependency injection in .NET"
}


Response:
{
  "content": "Dependency Injection is a design pattern..."
}


Implement Error Handling
Add try-catch blocks for production readiness:
try
{
    var result =
        await _claudeService
            .GetResponseAsync(prompt);

    return result;
}
catch(Exception ex)
{
    _logger.LogError(ex.Message);
    throw;
}


Add Dependency Injection Pattern

Define interface:
public interface IClaudeService
{
    Task<string> GetResponseAsync(
        string prompt);
}

Register:
builder.Services.AddScoped<
    IClaudeService,
    ClaudeService>();


Implement Streaming Responses
For real-time chat applications, use Claude's streaming API to deliver token-by-token responses to the frontend.

Benefits:

  • Lower perceived latency
  • Better user experience
  • Improved chatbot interactions
  • Real-time AI assistants

Rate Limiting

Protect API endpoints:
builder.Services.AddRateLimiter(options =>
{
    options.AddFixedWindowLimiter(
        "ClaudeLimiter",
        config =>
        {
            config.PermitLimit = 20;
            config.Window =
                TimeSpan.FromMinutes(1);
        });
});


Logging and Monitoring

Implement:

  • Serilog
  • Application Insights
  • OpenTelemetry

Response Caching

Reduce API costs by caching repeated prompts.

Common Use Cases
1. AI Chatbots
Customer support and virtual assistants.

2. Document Analysis
Process contracts, invoices, and reports.

3. Knowledge Base Search
Combine Claude with vector databases and RAG architecture.

4. Content Generation
Generate technical documentation and reports.

5. Internal Enterprise Assistants
Provide intelligent access to company knowledge.

Conclusion

Integrating Claude AI with .NET enables developers to build sophisticated AI-powered applications with minimal effort. By leveraging ASP.NET Core, HttpClient, dependency injection, and secure configuration practices, teams can rapidly deploy production-ready solutions powered by Anthropic's advanced language models.

Whether you're building chatbots, document intelligence systems, AI copilots, or enterprise automation platforms, Claude AI and .NET provide a scalable foundation for modern intelligent applications.

HostForLIFE.eu ASP.NET Core 10.0 Hosting
European best, cheap and reliable ASP.NET hosting with instant activation. HostForLIFE.eu is #1 Recommended Windows and ASP.NET hosting in European Continent. With 99.99% Uptime Guaranteed of Relibility, Stability and Performace. HostForLIFE.eu security team is constantly monitoring the entire network for unusual behaviour. We deliver hosting solution including Shared hosting, Cloud hosting, Reseller hosting, Dedicated Servers, and IT as Service for companies of all size.



About HostForLIFE.eu

HostForLIFE.eu is European Windows Hosting Provider which focuses on Windows Platform only. We deliver on-demand hosting solutions including Shared hosting, Reseller Hosting, Cloud Hosting, Dedicated Servers, and IT as a Service for companies of all sizes.

We have offered the latest Windows 2016 Hosting, ASP.NET Core 2.2.1 Hosting, ASP.NET MVC 6 Hosting and SQL 2017 Hosting.


Month List

Tag cloud

Sign in