Voyager.Common.Proxy.Server.AspNetCore 1.7.7

There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package Voyager.Common.Proxy.Server.AspNetCore --version 1.7.7
                    
NuGet\Install-Package Voyager.Common.Proxy.Server.AspNetCore -Version 1.7.7
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Voyager.Common.Proxy.Server.AspNetCore" Version="1.7.7" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Voyager.Common.Proxy.Server.AspNetCore" Version="1.7.7" />
                    
Directory.Packages.props
<PackageReference Include="Voyager.Common.Proxy.Server.AspNetCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Voyager.Common.Proxy.Server.AspNetCore --version 1.7.7
                    
#r "nuget: Voyager.Common.Proxy.Server.AspNetCore, 1.7.7"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Voyager.Common.Proxy.Server.AspNetCore@1.7.7
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Voyager.Common.Proxy.Server.AspNetCore&version=1.7.7
                    
Install as a Cake Addin
#tool nuget:?package=Voyager.Common.Proxy.Server.AspNetCore&version=1.7.7
                    
Install as a Cake Tool

Voyager.Common.Proxy.Server.AspNetCore

ASP.NET Core integration for Voyager.Common.Proxy.Server - automatically generates HTTP endpoints from service interfaces.

Installation

dotnet add package Voyager.Common.Proxy.Server.AspNetCore

Quick Start

// Define your service interface
public interface IUserService
{
    Task<Result<User>> GetUserAsync(int id, CancellationToken cancellationToken);
    Task<Result<User>> CreateUserAsync(CreateUserRequest request);
    Task<Result<IEnumerable<User>>> SearchUsersAsync(string? name, int? limit);
    Task<Result> DeleteUserAsync(int id);
}

// Register in Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IUserService, UserService>();

var app = builder.Build();

// Map service endpoints
app.MapServiceProxy<IUserService>();

app.Run();

This generates the following endpoints:

Method Route Parameters
GET /user-service/get-user id from query string
POST /user-service/create-user request from body
GET /user-service/search-users name, limit from query string
DELETE /user-service/delete-user id from query string

Features

  • Automatic endpoint generation from service interfaces
  • Convention-based routing matching Voyager.Common.Proxy.Client
  • Parameter binding from route, query string, and request body
  • Result<T> support - automatically unwraps successful results or returns appropriate error responses
  • CancellationToken injection - automatically passed from HttpContext.RequestAborted
  • Authorization support - attribute-based and configuration-based
  • Minimal API integration - uses ASP.NET Core's endpoint routing

Parameter Binding

The library automatically determines where to bind each parameter based on its type, HTTP method, and route template.

Binding Sources

Parameter Source When Used Example
Route Parameter name matches {placeholder} in route /users/{id}
Query Simple types (int, string, Guid, enum, etc.) ?name=John&limit=10
Body Complex types on POST, PUT, PATCH JSON request body
Route + Query Complex types on GET with route placeholders Mixed binding
Injected CancellationToken From HttpContext.RequestAborted

Simple Types from Query String

Task<Result<List<User>>> SearchUsersAsync(string? name, int? limit, bool? active);
// GET /user-service/search-users?name=John&limit=10&active=true

Route Parameters

When parameter name matches a route placeholder:

[HttpMethod(HttpMethod.Get, "{id}")]
Task<Result<User>> GetUserAsync(int id);
// GET /user-service/123  →  id = 123

Request Body (POST, PUT, PATCH)

Complex types are deserialized from JSON:

Task<Result<User>> CreateUserAsync(CreateUserRequest request);
// POST /user-service/create-user
// Content-Type: application/json
// Body: { "name": "John", "email": "john@example.com" }

Mixed Binding (Route + Query)

For GET requests with complex type parameters and route placeholders, properties are bound from both sources. Route values take precedence over query parameters.

public class PaymentsListRequest
{
    public int IdBusMapCoach_RNo { get; set; }  // Bound from route
    public string? Status { get; set; }         // Bound from query
    public int? Limit { get; set; }             // Bound from query
    public DateTime? FromDate { get; set; }     // Bound from query
}

[HttpMethod(HttpMethod.Get, "payments/{IdBusMapCoach_RNo}")]
Task<Result<PaymentsList>> GetPaymentsAsync(PaymentsListRequest request);

// Request: GET /service/payments/123?Status=Active&Limit=10&FromDate=2024-01-01
//
// Binding result:
//   request.IdBusMapCoach_RNo = 123       ← from route {IdBusMapCoach_RNo}
//   request.Status = "Active"              ← from query string
//   request.Limit = 10                     ← from query string
//   request.FromDate = 2024-01-01          ← from query string

This enables RESTful URL design while passing multiple filter parameters:

public class OrderFilterRequest
{
    public int CustomerId { get; set; }        // From route
    public string? Status { get; set; }        // From query
    public DateTime? Since { get; set; }       // From query
    public int Page { get; set; } = 1;         // From query (with default)
    public int PageSize { get; set; } = 20;    // From query (with default)
}

[HttpMethod(HttpMethod.Get, "customers/{CustomerId}/orders")]
Task<Result<PagedList<Order>>> GetCustomerOrdersAsync(OrderFilterRequest filter);

// GET /service/customers/42/orders?Status=Pending&Page=2&PageSize=50

Supported Types for Route/Query Binding

Type Example Values
int, long, short, byte 123, -456
float, double, decimal 19.99, 3.14159
bool true, false
string hello, John%20Doe (URL encoded)
Guid 550e8400-e29b-41d4-a716-446655440000
DateTime 2024-01-15, 2024-01-15T10:30:00
DateTimeOffset 2024-01-15T10:30:00+02:00
TimeSpan 01:30:00, 1.12:00:00
Enums Active, PENDING (case-insensitive)
Nullable versions int?, bool?, DateTime?, etc.

Authorization

Option 1: Attribute on Interface (all endpoints)

using Voyager.Common.Proxy.Abstractions;

[RequireAuthorization]
public interface IUserService
{
    Task<Result<User>> GetUserAsync(int id);
    Task<Result<User>> CreateUserAsync(CreateUserRequest request);
}

Option 2: Attribute on Methods (specific endpoints)

public interface IProductService
{
    // Public endpoint
    Task<Result<Product>> GetProductAsync(int id);

    // Requires authentication
    [RequireAuthorization]
    Task<Result<Product>> CreateProductAsync(CreateProductRequest request);

    // Requires specific policy
    [RequireAuthorization("AdminPolicy")]
    Task<Result> DeleteProductAsync(int id);
}

Option 3: Mixed (Interface + AllowAnonymous)

[RequireAuthorization]
public interface IOrderService
{
    // Requires authentication (inherited)
    Task<Result<Order>> CreateOrderAsync(CreateOrderRequest request);

    // Public endpoint (override)
    [AllowAnonymous]
    Task<Result<OrderStatus>> GetOrderStatusAsync(int id);

    // Requires admin role
    [RequireAuthorization("AdminPolicy")]
    Task<Result> CancelOrderAsync(int id);
}

Option 4: Configuration at Mapping

// Apply to all endpoints in service
app.MapServiceProxy<IUserService>(e => e.RequireAuthorization());

// Apply specific policy
app.MapServiceProxy<IAdminService>(e => e.RequireAuthorization("AdminPolicy"));

// Combine with other conventions
app.MapServiceProxy<IOrderService>(e => e
    .RequireAuthorization()
    .RequireCors("AllowAll"));

Authorization Attributes

Attribute Target Description
[RequireAuthorization] Interface, Method Requires authenticated user
[RequireAuthorization("Policy")] Interface, Method Requires specific policy
[AllowAnonymous] Method Allows anonymous access (overrides interface)

You can also use ASP.NET Core's [Authorize] attribute - both are supported.

Setting up Authorization

var builder = WebApplication.CreateBuilder(args);

// Add authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options => { /* ... */ });

// Add authorization policies
builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminPolicy", policy =>
        policy.RequireRole("Admin"));
});

builder.Services.AddScoped<IUserService, UserService>();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

app.MapServiceProxy<IUserService>();

app.Run();

Service Resolution

Services are resolved from IServiceProvider for each request:

// With service provider (default)
app.MapServiceProxy<IUserService>();

Custom Permission Checking

For fine-grained access control beyond role-based authorization, use the permission checker:

Inline Permission Checker

app.MapServiceProxy<IVIPService>(options =>
{
    options.PermissionChecker = async ctx =>
    {
        // Check authentication
        if (ctx.User?.Identity?.IsAuthenticated != true)
            return PermissionResult.Unauthenticated();

        // Access HttpContext for services
        var httpContext = (HttpContext)ctx.RawContext;
        var checker = httpContext.RequestServices.GetRequiredService<IVIPPermissionChecker>();

        // Check permission based on method and parameters
        return await checker.CheckAsync(
            ctx.User,
            ctx.Method.Name,
            ctx.Parameters);
    };
});

Permission Checker with Context-Aware Factory

app.MapServiceProxy<IVIPService>(options =>
{
    // Create service with per-request identity
    options.ContextAwareFactory = httpContext =>
    {
        var identity = PilotIdentityFactory.Create(httpContext.User);
        var actionModule = httpContext.RequestServices.GetRequiredService<ActionModule>();
        return new VIPService(identity, actionModule);
    };

    // Check permissions before service creation
    options.PermissionChecker = async ctx =>
    {
        if (ctx.User?.Identity?.IsAuthenticated != true)
            return PermissionResult.Unauthenticated();

        // Method-level checks
        if (ctx.Method.Name == "DeleteAsync" && !ctx.User.IsInRole("Admin"))
            return PermissionResult.Denied("Admin role required for delete operations");

        // Parameter-level checks
        if (ctx.Parameters.TryGetValue("id", out var idObj) && idObj is int id)
        {
            var httpContext = (HttpContext)ctx.RawContext;
            var ownershipChecker = httpContext.RequestServices.GetRequiredService<IOwnershipChecker>();

            if (!await ownershipChecker.CanAccessAsync(ctx.User, id))
                return PermissionResult.Denied("You don't have access to this resource");
        }

        return PermissionResult.Granted();
    };
});

Typed Permission Checker (Reusable)

For complex permission logic, implement IServicePermissionChecker<TService>:

public class VIPServicePermissionChecker : IServicePermissionChecker<IVIPService>
{
    private readonly IOwnershipService _ownershipService;

    public VIPServicePermissionChecker(IOwnershipService ownershipService)
    {
        _ownershipService = ownershipService;
    }

    public async Task<PermissionResult> CheckPermissionAsync(PermissionContext context)
    {
        if (context.User?.Identity?.IsAuthenticated != true)
            return PermissionResult.Unauthenticated();

        // Different rules for different methods
        return context.Method.Name switch
        {
            "GetAsync" => PermissionResult.Granted(),
            "CreateAsync" => CheckCreatePermission(context),
            "DeleteAsync" => await CheckDeletePermissionAsync(context),
            _ => PermissionResult.Granted()
        };
    }

    private PermissionResult CheckCreatePermission(PermissionContext context)
    {
        if (!context.User!.IsInRole("Creator"))
            return PermissionResult.Denied("Creator role required");
        return PermissionResult.Granted();
    }

    private async Task<PermissionResult> CheckDeletePermissionAsync(PermissionContext context)
    {
        if (context.Parameters.TryGetValue("id", out var idObj) && idObj is int id)
        {
            var canDelete = await _ownershipService.CanDeleteAsync(context.User!, id);
            if (!canDelete)
                return PermissionResult.Denied("Cannot delete this resource");
        }
        return PermissionResult.Granted();
    }
}

// Registration
builder.Services.AddScoped<VIPServicePermissionChecker>();

app.MapServiceProxy<IVIPService>(options =>
{
    options.PermissionCheckerInstance = app.Services
        .GetRequiredService<VIPServicePermissionChecker>();
});

PermissionContext Properties

Property Type Description
User IPrincipal? The authenticated user (null for anonymous)
ServiceType Type The service interface type
Method MethodInfo The method being called
Endpoint EndpointDescriptor Route and parameter info
Parameters IReadOnlyDictionary<string, object?> Deserialized request parameters
RawContext object The HttpContext (cast to access)

PermissionResult Factory Methods

Method HTTP Status Use Case
PermissionResult.Granted() - Allow access
PermissionResult.Denied(reason) 403 Forbidden User authenticated but not allowed
PermissionResult.Unauthenticated() 401 Unauthorized User not authenticated

Diagnostics and Observability

The server automatically emits diagnostic events for all incoming requests. Events include request timing, success/failure status, and user context.

Basic Setup

Diagnostics handlers are automatically resolved from DI:

// Install: dotnet add package Voyager.Common.Proxy.Diagnostics

using Voyager.Common.Proxy.Diagnostics;

var builder = WebApplication.CreateBuilder(args);

// Register logging
builder.Services.AddLogging(b => b.AddConsole());

// Register diagnostics handler
builder.Services.AddProxyLoggingDiagnostics();

// Register your service
builder.Services.AddScoped<IUserService, UserService>();

var app = builder.Build();

// Map service - diagnostics are automatically active
app.MapServiceProxy<IUserService>();

app.Run();

Adding User Context

To include user information in diagnostic events:

public class HttpContextRequestContext : IProxyRequestContext
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public HttpContextRequestContext(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public string? UserLogin => _httpContextAccessor.HttpContext?.User?.Identity?.Name;
    public string? UnitId => _httpContextAccessor.HttpContext?.User?.FindFirst("unit_id")?.Value;
    public string? UnitType => "Agent";
    public IReadOnlyDictionary<string, string>? CustomProperties => null;
}

// Register
builder.Services.AddHttpContextAccessor();
builder.Services.AddProxyRequestContext<HttpContextRequestContext>();

Custom Diagnostics Handler

public class ServerMetricsDiagnostics : ProxyDiagnosticsHandler
{
    private readonly IMetricsService _metrics;

    public ServerMetricsDiagnostics(IMetricsService metrics) => _metrics = metrics;

    public override void OnRequestCompleted(RequestCompletedEvent e)
    {
        _metrics.RecordHistogram("server_request_duration_ms", e.Duration.TotalMilliseconds,
            new[] { ("service", e.ServiceName), ("method", e.MethodName), ("success", e.IsSuccess.ToString()) });
    }
}

// Register
builder.Services.AddProxyDiagnostics<ServerMetricsDiagnostics>();

Server-Side Events

Event When Emitted
OnRequestStarting When request is received
OnRequestCompleted After response is sent (success or business error)
OnRequestFailed When exception occurs during processing

Note: Server-side does not emit OnRetryAttempt or OnCircuitBreakerStateChanged - these are client-side patterns.

Target Frameworks

  • net6.0
  • net8.0
Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.8.0-preview 0 2/10/2026
1.7.8-preview.1 35 2/9/2026
1.7.7 47 2/6/2026
1.7.7-preview.1.1 34 2/6/2026
1.7.7-preview 0 2/10/2026