ASP.NET Core Integration

Integrate the .NET Custody SDK into your ASP.NET Core applications with controllers, minimal APIs, background services, and health checks.

Overview

All methods in the Custody SDK are asynchronous by default (async Task<T>), making them ideal for ASP.NET Core's async programming model. This page covers integration patterns for building production-ready APIs.

Controller-Based API

Integrate the Custody Client into MVC controllers for traditional API endpoints:

using CoxAuto.Vecu.CustodySdk.Client;
using CoxAuto.Vecu.CustodySdk.Exceptions;
using CoxAuto.Vecu.CustodySdk.Models.Requests;
using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/[controller]")]
public class AuthorizationsController : ControllerBase
{
    private readonly ICustodyServiceClient _custodyClient;
    private readonly ILogger<AuthorizationsController> _logger;

    public AuthorizationsController(
        ICustodyServiceClient custodyClient,
        ILogger<AuthorizationsController> logger)
    {
        _custodyClient = custodyClient;
        _logger = logger;
    }

    /// <summary>
    /// Create a new vehicle custody authorization
    /// </summary>
    [HttpPost]
    [ProducesResponseType(StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status409Conflict)]
    public async Task<IActionResult> CreateAuthorization(
        [FromBody] CreateAuthorizationRequest request,
        CancellationToken cancellationToken)
    {
        try
        {
            _logger.LogInformation("Creating authorization for VIN: {Vin}", request.Vin);

            var response = await _custodyClient.CreateAuthorizationAsync(
                request,
                cancellationToken
            );

            _logger.LogInformation(
                "Authorization created: {AuthorizationId}",
                response.AuthorizationId
            );

            return CreatedAtAction(
                nameof(GetAuthorization),
                new { id = response.AuthorizationId },
                response
            );
        }
        catch (ValidationException ex)
        {
            _logger.LogWarning("Validation error: {Message}", ex.Message);
            return BadRequest(new
            {
                error = "Validation failed",
                message = ex.Message,
                errors = ex.ValidationErrors
            });
        }
        catch (DuplicateAuthorizationException ex)
        {
            _logger.LogWarning(
                "Duplicate authorization: {ExistingId}",
                ex.ExistingAuthorizationId
            );
            return Conflict(new
            {
                error = "Duplicate authorization",
                existingAuthorizationId = ex.ExistingAuthorizationId
            });
        }
    }

    /// <summary>
    /// Get authorization details by ID
    /// </summary>
    [HttpGet("{id}")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetAuthorization(
        string id,
        CancellationToken cancellationToken)
    {
        try
        {
            var authorization = await _custodyClient.GetAuthorizationAsync(
                id,
                cancellationToken
            );
            return Ok(authorization);
        }
        catch (AuthorizationNotFoundException ex)
        {
            _logger.LogWarning(
                "Authorization not found: {AuthorizationId}",
                ex.AuthorizationId
            );
            return NotFound(new
            {
                error = "Authorization not found",
                authorizationId = id
            });
        }
    }

    /// <summary>
    /// Cancel an existing authorization
    /// </summary>
    [HttpPost("{id}/cancel")]
    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status422UnprocessableEntity)]
    public async Task<IActionResult> CancelAuthorization(
        string id,
        [FromBody] CancelAuthorizationRequest request,
        CancellationToken cancellationToken)
    {
        try
        {
            _logger.LogInformation("Cancelling authorization: {AuthorizationId}", id);

            var response = await _custodyClient.CancelAuthorizationAsync(
                id,
                request,
                cancellationToken
            );

            _logger.LogInformation("Authorization cancelled: {AuthorizationId}", id);

            return Ok(response);
        }
        catch (AuthorizationNotFoundException ex)
        {
            _logger.LogWarning(
                "Authorization not found: {AuthorizationId}",
                ex.AuthorizationId
            );
            return NotFound(new
            {
                error = "Authorization not found",
                authorizationId = id
            });
        }
        catch (ValidationException ex)
        {
            _logger.LogWarning("Cancellation validation error: {Message}", ex.Message);
            return UnprocessableEntity(new
            {
                error = "Cannot cancel authorization",
                message = ex.Message,
                errors = ex.ValidationErrors
            });
        }
    }
}

Minimal API Integration

For simpler applications, use .NET's minimal API approach:

using CoxAuto.Vecu.CustodySdk.Client;
using CoxAuto.Vecu.CustodySdk.Exceptions;
using CoxAuto.Vecu.CustodySdk.Models.Requests;
using CoxAuto.Vecu.CustodySdk.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

// Register Custody Client
builder.Services.AddCustodyClient(
    builder.Configuration,
    tokenProvider: async (ct) => await myAuthClient.GetTokenAsync(ct));

var app = builder.Build();

// Create authorization endpoint
app.MapPost("/api/authorizations", async (
    CreateAuthorizationRequest request,
    ICustodyServiceClient custodyClient,
    ILogger<Program> logger,
    CancellationToken cancellationToken) =>
{
    try
    {
        logger.LogInformation("Creating authorization for VIN: {Vin}", request.Vin);

        var response = await custodyClient.CreateAuthorizationAsync(
            request,
            cancellationToken
        );

        return Results.Created(
            $"/api/authorizations/{response.AuthorizationId}",
            response
        );
    }
    catch (ValidationException ex)
    {
        return Results.BadRequest(new
        {
            error = "Validation failed",
            message = ex.Message,
            errors = ex.ValidationErrors
        });
    }
    catch (DuplicateAuthorizationException ex)
    {
        return Results.Conflict(new
        {
            error = "Duplicate authorization",
            existingAuthorizationId = ex.ExistingAuthorizationId
        });
    }
})
.WithName("CreateAuthorization")
.WithOpenApi();

// Get authorization endpoint
app.MapGet("/api/authorizations/{id}", async (
    string id,
    ICustodyServiceClient custodyClient,
    CancellationToken cancellationToken) =>
{
    try
    {
        var authorization = await custodyClient.GetAuthorizationAsync(
            id,
            cancellationToken
        );
        return Results.Ok(authorization);
    }
    catch (AuthorizationNotFoundException)
    {
        return Results.NotFound(new
        {
            error = "Authorization not found",
            authorizationId = id
        });
    }
})
.WithName("GetAuthorization")
.WithOpenApi();

app.Run();

Background Services

Use IHostedService for background tasks like monitoring authorization statuses:

using CoxAuto.Vecu.CustodySdk.Client;
using CoxAuto.Vecu.CustodySdk.Models.Enums;

public class AuthorizationMonitorService : BackgroundService
{
    private readonly ILogger<AuthorizationMonitorService> _logger;
    private readonly IServiceProvider _serviceProvider;
    private readonly TimeSpan _pollInterval = TimeSpan.FromMinutes(1);

    public AuthorizationMonitorService(
        ILogger<AuthorizationMonitorService> logger,
        IServiceProvider serviceProvider)
    {
        _logger = logger;
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogInformation("Authorization Monitor Service started");

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                await MonitorAuthorizationsAsync(stoppingToken);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error monitoring authorizations");
            }

            await Task.Delay(_pollInterval, stoppingToken);
        }

        _logger.LogInformation("Authorization Monitor Service stopped");
    }

    private async Task MonitorAuthorizationsAsync(CancellationToken cancellationToken)
    {
        // Create a new scope to resolve scoped services
        using var scope = _serviceProvider.CreateScope();
        var custodyClient = scope.ServiceProvider
            .GetRequiredService<ICustodyServiceClient>();

        // Query database for pending authorizations
        var pendingAuthIds = await GetPendingAuthorizationIdsFromDatabase();

        foreach (var authId in pendingAuthIds)
        {
            try
            {
                var auth = await custodyClient.GetAuthorizationAsync(
                    authId,
                    cancellationToken
                );

                // Check for status changes
                if (auth.Status == AuthorizationStatus.RELEASED)
                {
                    _logger.LogInformation(
                        "Authorization {AuthId} released",
                        authId
                    );

                    await HandleCompletedAuthorizationAsync(auth);
                }
                else if (auth.Status == AuthorizationStatus.EXPIRED)
                {
                    _logger.LogWarning(
                        "Authorization {AuthId} expired",
                        authId
                    );

                    await HandleExpiredAuthorizationAsync(auth);
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(
                    ex,
                    "Error checking authorization {AuthId}",
                    authId
                );
            }
        }
    }

    private async Task<List<string>> GetPendingAuthorizationIdsFromDatabase()
    {
        // Query your database for pending authorization IDs
        // Example: return await dbContext.Authorizations
        //     .Where(a => a.Status == "Pending")
        //     .Select(a => a.AuthorizationId)
        //     .ToListAsync();
        return new List<string>();
    }

    private async Task HandleCompletedAuthorizationAsync(AuthorizationDetails auth)
    {
        // Send notification, update database, trigger workflow, etc.
        await Task.CompletedTask;
    }

    private async Task HandleExpiredAuthorizationAsync(AuthorizationDetails auth)
    {
        // Handle expiration: notify, log, retry, etc.
        await Task.CompletedTask;
    }
}

Register the background service in Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCustodyClient(
    builder.Configuration,
    tokenProvider: async (ct) => await myAuthClient.GetTokenAsync(ct));
builder.Services.AddHostedService<AuthorizationMonitorService>();

var app = builder.Build();
app.Run();

Background services are singleton by default. Use IServiceProvider.CreateScope() to resolve scoped services like ICustodyServiceClient.

Health Checks

Add health checks to monitor the Custody API connectivity:

using Microsoft.Extensions.Diagnostics.HealthChecks;
using CoxAuto.Vecu.CustodySdk.Client;

public class CustodyServiceHealthCheck : IHealthCheck
{
    private readonly ICustodyServiceClient _custodyClient;
    private readonly ILogger<CustodyServiceHealthCheck> _logger;

    public CustodyServiceHealthCheck(
        ICustodyServiceClient custodyClient,
        ILogger<CustodyServiceHealthCheck> logger)
    {
        _custodyClient = custodyClient;
        _logger = logger;
    }

    public async Task<HealthCheckResult> CheckHealthAsync(
        HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        try
        {
            // Attempt a simple releasability check with a test VIN
            await _custodyClient.GetReleasabilityAsync(
                "1TEST5678HEALTH99",
                "test-origin",
                cancellationToken);

            return HealthCheckResult.Healthy("Custody Service is accessible");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Custody Service health check failed");

            return HealthCheckResult.Unhealthy(
                "Custody Service is not accessible",
                ex
            );
        }
    }
}

Register health checks in Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCustodyClient(
    builder.Configuration,
    tokenProvider: async (ct) => await myAuthClient.GetTokenAsync(ct));
builder.Services.AddHealthChecks()
    .AddCheck<CustodyServiceHealthCheck>("custody_service");

var app = builder.Build();

app.MapHealthChecks("/health");

app.Run();

Logging Integration

The SDK integrates with ASP.NET Core's ILogger for structured logging:

var builder = WebApplication.CreateBuilder(args);

// Configure logging
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();

// Enable detailed SDK logging (development only)
if (builder.Environment.IsDevelopment())
{
    builder.Services.AddCustodyClient(options =>
    {
        options.Environment = CustodyEnvironment.Sandbox;
        options.TokenProvider = async (ct) => await myAuthClient.GetTokenAsync(ct);
        options.EnableDetailedLogging = true; // Logs HTTP requests/responses
    });
}
else
{
    builder.Services.AddCustodyClient(
    builder.Configuration,
    tokenProvider: async (ct) => await myAuthClient.GetTokenAsync(ct));
}

var app = builder.Build();

Structured Logging with Correlation IDs

Use CorrelationId from exceptions for distributed tracing:

public async Task<IActionResult> CreateAuthorization(
    CreateAuthorizationRequest request,
    CancellationToken cancellationToken)
{
    try
    {
        var response = await _custodyClient.CreateAuthorizationAsync(
            request,
            cancellationToken
        );

        _logger.LogInformation(
            "Authorization created: {AuthorizationId}",
            response.AuthorizationId
        );

        return CreatedAtAction(nameof(GetAuthorization), new { id = response.AuthorizationId }, response);
    }
    catch (CustodyException ex)
    {
        _logger.LogError(
            ex,
            "Custody API error: {ErrorCode}, CorrelationId: {CorrelationId}, StatusCode: {StatusCode}",
            ex.ErrorCode,
            ex.CorrelationId, // Correlation ID for support tickets
            ex.StatusCode
        );

        return StatusCode(500, new
        {
            error = "An error occurred while creating authorization",
            correlationId = ex.CorrelationId // Return correlation ID to client
        });
    }
}

Environment-Specific Configuration

Use different configurations per environment with appsettings.{Environment}.json:

Program.cs

var builder = WebApplication.CreateBuilder(args);

// Configuration automatically loads from:
// - appsettings.json
// - appsettings.{Environment}.json
// - Environment variables
// - User secrets (development)

builder.Services.AddCustodyClient(
    builder.Configuration,
    tokenProvider: async (ct) =>
    {
        var token = await myAuthClient.GetTokenAsync(ct);
        return token.AccessToken;
    });

var app = builder.Build();

appsettings.Development.json

{
  "CustodyClient": {
    "Environment": "Sandbox",
    "TimeoutSeconds": 30,
    "EnableDetailedLogging": true
  }
}

appsettings.Production.json

{
  "CustodyClient": {
    "Environment": "Production",
    "TimeoutSeconds": 60,
    "MaxRetries": 5,
    "EnableDetailedLogging": false
  }
}

Never commit secrets to source control. Use Azure Key Vault, AWS Secrets Manager, or environment variables for sensitive configuration.

Exception Middleware

Create global exception handling middleware for consistent error responses:

using CoxAuto.Vecu.CustodySdk.Exceptions;

public class CustodyExceptionMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<CustodyExceptionMiddleware> _logger;

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

    public async Task InvokeAsync(HttpContext context)
    {
        try
        {
            await _next(context);
        }
        catch (ValidationException ex)
        {
            await HandleValidationExceptionAsync(context, ex);
        }
        catch (AuthorizationNotFoundException ex)
        {
            await HandleNotFoundExceptionAsync(context, ex);
        }
        catch (RateLimitException ex)
        {
            await HandleRateLimitExceptionAsync(context, ex);
        }
        catch (CustodyException ex)
        {
            await HandleCustodyExceptionAsync(context, ex);
        }
    }

    private async Task HandleValidationExceptionAsync(
        HttpContext context,
        ValidationException ex)
    {
        _logger.LogWarning(
            "Validation error: {Message}, CorrelationId: {CorrelationId}",
            ex.Message,
            ex.CorrelationId
        );

        context.Response.StatusCode = 400;
        context.Response.ContentType = "application/json";

        await context.Response.WriteAsJsonAsync(new
        {
            error = "Validation failed",
            message = ex.Message,
            errors = ex.ValidationErrors,
            correlationId = ex.CorrelationId
        });
    }

    private async Task HandleNotFoundExceptionAsync(
        HttpContext context,
        AuthorizationNotFoundException ex)
    {
        context.Response.StatusCode = 404;
        context.Response.ContentType = "application/json";

        await context.Response.WriteAsJsonAsync(new
        {
            error = "Not found",
            message = ex.Message,
            authorizationId = ex.AuthorizationId,
            correlationId = ex.CorrelationId
        });
    }

    private async Task HandleRateLimitExceptionAsync(
        HttpContext context,
        RateLimitException ex)
    {
        _logger.LogWarning(
            "Rate limit exceeded, RetryAfter: {RetryAfter}",
            ex.RetryAfter
        );

        context.Response.StatusCode = 429;
        context.Response.Headers.Add("Retry-After", ex.RetryAfter?.TotalSeconds.ToString());
        context.Response.ContentType = "application/json";

        await context.Response.WriteAsJsonAsync(new
        {
            error = "Rate limit exceeded",
            message = ex.Message,
            retryAfter = ex.RetryAfter?.TotalSeconds,
            correlationId = ex.CorrelationId
        });
    }

    private async Task HandleCustodyExceptionAsync(
        HttpContext context,
        CustodyException ex)
    {
        _logger.LogError(
            ex,
            "Custody API error: {ErrorCode}, CorrelationId: {CorrelationId}",
            ex.ErrorCode,
            ex.CorrelationId
        );

        context.Response.StatusCode = (int)(ex.StatusCode ?? System.Net.HttpStatusCode.InternalServerError);
        context.Response.ContentType = "application/json";

        await context.Response.WriteAsJsonAsync(new
        {
            error = "Custody service error",
            message = ex.Message,
            errorCode = ex.ErrorCode,
            correlationId = ex.CorrelationId
        });
    }
}

// Register middleware
app.UseMiddleware<CustodyExceptionMiddleware>();

Complete Example

Full Program.cs with all integration patterns:

using CoxAuto.Vecu.CustodySdk.DependencyInjection;
using CoxAuto.Vecu.CustodySdk.Examples.AspNetCoreIntegration.Services;

var builder = WebApplication.CreateBuilder(args);

// Add services
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
    c.SwaggerDoc("v1", new() { Title = "VECU Custody API", Version = "v1" });
});

// Add Custody Client
builder.Services.AddCustodyClient(
    builder.Configuration,
    tokenProvider: async (ct) => await myAuthClient.GetTokenAsync(ct));

// Add background services
builder.Services.AddHostedService<AuthorizationMonitorService>();

// Add health checks
builder.Services.AddHealthChecks()
    .AddCheck<CustodyServiceHealthCheck>("custody_service");

var app = builder.Build();

// Configure middleware pipeline
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseMiddleware<CustodyExceptionMiddleware>(); // Global exception handling
app.UseAuthorization();
app.MapControllers();
app.MapHealthChecks("/health");

app.Run();

Next Steps