FS.AspNetCore.ResponseWrapper 9.1.0

dotnet add package FS.AspNetCore.ResponseWrapper --version 9.1.0
                    
NuGet\Install-Package FS.AspNetCore.ResponseWrapper -Version 9.1.0
                    
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="FS.AspNetCore.ResponseWrapper" Version="9.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FS.AspNetCore.ResponseWrapper" Version="9.1.0" />
                    
Directory.Packages.props
<PackageReference Include="FS.AspNetCore.ResponseWrapper" />
                    
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 FS.AspNetCore.ResponseWrapper --version 9.1.0
                    
#r "nuget: FS.AspNetCore.ResponseWrapper, 9.1.0"
                    
#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 FS.AspNetCore.ResponseWrapper@9.1.0
                    
#: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=FS.AspNetCore.ResponseWrapper&version=9.1.0
                    
Install as a Cake Addin
#tool nuget:?package=FS.AspNetCore.ResponseWrapper&version=9.1.0
                    
Install as a Cake Tool

FS.AspNetCore.ResponseWrapper

NuGet Version NuGet Downloads GitHub License GitHub Stars

Automatic API response wrapping with metadata injection for ASP.NET Core applications.

FS.AspNetCore.ResponseWrapper provides a consistent, standardized response format for your ASP.NET Core APIs with zero boilerplate code. Transform your raw controller responses into rich, metadata-enhanced API responses that include execution timing, pagination details, correlation IDs, status codes, and comprehensive error handling.

🎯 Why ResponseWrapper?

Building robust APIs means handling consistent response formats, error management, timing information, status codes, and pagination metadata. Without a standardized approach, you end up with:

  • Inconsistent Response Formats: Different endpoints returning data in different structures
  • Manual Error Handling: Writing repetitive error response logic in every controller
  • Missing Metadata: No execution timing, correlation IDs, or request tracking
  • Complex Pagination: Mixing business data with pagination information
  • Status Code Confusion: Mixing HTTP status codes with application-specific workflow states
  • Debugging Difficulties: Limited insight into request processing and performance

ResponseWrapper solves all these challenges by automatically wrapping your API responses with a consistent structure, comprehensive metadata, and intelligent error handling.

✨ Key Features

🔄 Automatic Response Wrapping

Transform any controller response into a standardized format without changing your existing code.

⏱️ Performance Monitoring

Built-in execution time tracking and database query statistics for performance optimization.

🔍 Request Tracing

Automatic correlation ID generation and tracking for distributed systems debugging.

📊 Application Status Codes

Intelligent status code extraction and promotion from response data, enabling complex workflow management and rich client-side conditional logic.

📄 Smart Pagination

Automatic detection and clean separation of pagination metadata from business data using duck typing.

🚨 Comprehensive Error Handling

Global exception handling with customizable error messages and consistent error response format.

🎛️ Flexible Configuration

Extensive configuration options for customizing behavior, excluding specific endpoints, and controlling metadata generation.

🦆 Duck Typing Support

Works with ANY pagination implementation - no need to change existing pagination interfaces.

📦 Installation

Install the package via NuGet Package Manager:

dotnet add package FS.AspNetCore.ResponseWrapper

Or via Package Manager Console:

Install-Package FS.AspNetCore.ResponseWrapper

Or add directly to your .csproj file:

<PackageReference Include="FS.AspNetCore.ResponseWrapper" Version="9.1.0" />

🚀 Quick Start

Getting started with ResponseWrapper is incredibly simple. Add it to your ASP.NET Core application in just two steps:

Step 1: Register ResponseWrapper Services

In your Program.cs file, add ResponseWrapper to your service collection:

using FS.AspNetCore.ResponseWrapper;

var builder = WebApplication.CreateBuilder(args);

// Add controllers
builder.Services.AddControllers();

// Add ResponseWrapper with default configuration
builder.Services.AddResponseWrapper();

var app = builder.Build();

// Configure the HTTP request pipeline
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

For comprehensive error handling, add the middleware:

var app = builder.Build();

// Add ResponseWrapper middleware for global exception handling
app.UseMiddleware<GlobalExceptionHandlingMiddleware>();

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

That's it! Your API responses are now automatically wrapped. Let's see what this means in practice.

📊 Before and After

Before: Raw Controller Response

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    [HttpGet]
    public async Task<List<User>> GetUsers()
    {
        return await _userService.GetUsersAsync();
    }
}

Raw Response:

[
  {"id": 1, "name": "John Doe", "email": "john@example.com"},
  {"id": 2, "name": "Jane Smith", "email": "jane@example.com"}
]

After: ResponseWrapper Enhanced

Same Controller Code - No changes needed!

Enhanced Response:

{
  "success": true,
  "data": [
    {"id": 1, "name": "John Doe", "email": "john@example.com"},
    {"id": 2, "name": "Jane Smith", "email": "jane@example.com"}
  ],
  "message": null,
  "statusCode": null,
  "errors": [],
  "metadata": {
    "requestId": "550e8400-e29b-41d4-a716-446655440000",
    "timestamp": "2024-01-15T10:30:45.123Z",
    "executionTimeMs": 42,
    "version": "1.0",
    "correlationId": "abc123",
    "path": "/api/users",
    "method": "GET",
    "additional": {
      "requestSizeBytes": 0,
      "clientIP": "192.168.1.1"
    }
  }
}

🎛️ Configuration Options

ResponseWrapper provides extensive configuration options to customize behavior according to your needs.

Basic Configuration

builder.Services.AddResponseWrapper(options =>
{
    // Enable/disable execution time tracking
    options.EnableExecutionTimeTracking = true;
    
    // Enable/disable pagination metadata extraction
    options.EnablePaginationMetadata = true;
    
    // Enable/disable correlation ID tracking
    options.EnableCorrelationId = true;
    
    // Enable/disable database query statistics (requires EF interceptors)
    options.EnableQueryStatistics = false;
    
    // Control which responses to wrap
    options.WrapSuccessResponses = true;
    options.WrapErrorResponses = true;
    
    // Exclude specific paths from wrapping
    options.ExcludedPaths = new[] { "/health", "/metrics", "/swagger" };
    
    // Exclude specific result types from wrapping
    options.ExcludedTypes = new[] { typeof(FileResult), typeof(RedirectResult) };
});

Advanced Configuration with Custom Error Messages

builder.Services.AddResponseWrapper(
    options =>
    {
        options.EnableExecutionTimeTracking = true;
        options.EnableQueryStatistics = true;
        options.ExcludedPaths = new[] { "/health", "/metrics" };
    },
    errorMessages =>
    {
        errorMessages.ValidationErrorMessage = "Please check your input and try again";
        errorMessages.NotFoundErrorMessage = "The requested item could not be found";
        errorMessages.UnauthorizedAccessMessage = "Please log in to access this resource";
        errorMessages.ForbiddenAccessMessage = "You don't have permission to access this resource";
        errorMessages.BusinessRuleViolationMessage = "This operation violates business rules";
        errorMessages.ApplicationErrorMessage = "We're experiencing technical difficulties";
        errorMessages.UnexpectedErrorMessage = "An unexpected error occurred. Our team has been notified";
    });

Expert Configuration with Custom Dependencies

builder.Services.AddResponseWrapper<CustomApiLogger>(
    dateTimeProvider: () => DateTime.UtcNow, // Custom time provider for testing
    configureOptions: options =>
    {
        options.EnableExecutionTimeTracking = true;
        options.EnableQueryStatistics = true;
    },
    configureErrorMessages: errorMessages =>
    {
        errorMessages.ValidationErrorMessage = "Custom validation message";
        errorMessages.NotFoundErrorMessage = "Custom not found message";
    });

🚨 Error Handling

ResponseWrapper provides comprehensive error handling that transforms exceptions into consistent API responses.

Built-in Exception Types

ResponseWrapper includes several exception types for common scenarios:

// For validation errors
throw new ValidationException(validationFailures);

// For missing resources
throw new NotFoundException("User", userId);

// For business rule violations
throw new BusinessException("Insufficient inventory for this order");

// For authorization failures
throw new ForbiddenAccessException("Access denied to this resource");

Exception Response Examples

Validation Error Response:

{
  "success": false,
  "data": null,
  "message": "Please check your input and try again",
  "statusCode": "VALIDATION_ERROR",
  "errors": [
    "Email is required",
    "Password must be at least 8 characters"
  ],
  "metadata": {
    "requestId": "550e8400-e29b-41d4-a716-446655440000",
    "timestamp": "2024-01-15T10:30:45.123Z",
    "executionTimeMs": 15,
    "path": "/api/users",
    "method": "POST"
  }
}

Not Found Error Response:

{
  "success": false,
  "data": null,
  "message": "The requested item could not be found",
  "statusCode": "NOT_FOUND",
  "errors": ["User (123) was not found."],
  "metadata": {
    "requestId": "550e8400-e29b-41d4-a716-446655440001",
    "timestamp": "2024-01-15T10:32:15.456Z",
    "executionTimeMs": 8,
    "path": "/api/users/123",
    "method": "GET"
  }
}

Custom Error Messages

Customize error messages for different environments or languages:

// English messages
errorMessages.ValidationErrorMessage = "Please check your input and try again";
errorMessages.NotFoundErrorMessage = "The requested item could not be found";

// Turkish messages
errorMessages.ValidationErrorMessage = "Lütfen girdiğiniz bilgileri kontrol edin";
errorMessages.NotFoundErrorMessage = "Aradığınız öğe bulunamadı";

// Developer-friendly messages for development environment
if (environment.IsDevelopment())
{
    errorMessages.ValidationErrorMessage = "Validation failed - check detailed errors";
    errorMessages.ApplicationErrorMessage = "Application error - check logs for stack trace";
}

📊 Application Status Codes

One of ResponseWrapper's powerful features is its intelligent application status code handling, which enables complex workflow management beyond simple success/failure indicators.

The Problem with HTTP Status Codes Alone

HTTP status codes are great for transport-level communication, but modern applications often need richer status information:

{
  "success": true,
  "data": {"userId": 123, "email": "user@example.com"},
  "statusCode": "EMAIL_VERIFICATION_REQUIRED",
  "message": "Account created successfully. Please verify your email."
}

This enables sophisticated client-side logic based on application state rather than just HTTP semantics.

Automatic Status Code Extraction

ResponseWrapper automatically extracts status codes from your response data when they implement the IHasStatusCode interface:

// Your response DTO
public class UserRegistrationResult : IHasStatusCode
{
    public int UserId { get; set; }
    public string Email { get; set; }
    public string StatusCode { get; set; } = "EMAIL_VERIFICATION_REQUIRED";
    public string Message { get; set; } = "Please verify your email to complete registration";
}

// Your controller
[HttpPost("register")]
public async Task<UserRegistrationResult> RegisterUser(RegisterRequest request)
{
    var result = await _userService.RegisterAsync(request);
    return result; // StatusCode is automatically promoted to ApiResponse level
}

Resulting Response:

{
  "success": true,
  "data": {
    "userId": 123,
    "email": "user@example.com"
  },
  "statusCode": "EMAIL_VERIFICATION_REQUIRED",
  "message": "Please verify your email to complete registration",
  "metadata": { ... }
}

Notice how the status code and message are promoted to the top-level ApiResponse while the data remains clean and focused on business information.

Complex Workflow Examples

// Authentication workflow
public class LoginResult : IHasStatusCode
{
    public string Token { get; set; }
    public UserProfile User { get; set; }
    public string StatusCode { get; set; }
    public string Message { get; set; }
}

// Different status codes for different scenarios
switch (authResult.Status)
{
    case AuthStatus.Success:
        return new LoginResult 
        { 
            Token = token, 
            User = user, 
            StatusCode = "LOGIN_SUCCESS",
            Message = "Welcome back!"
        };
    
    case AuthStatus.RequiresTwoFactor:
        return new LoginResult 
        { 
            StatusCode = "TWO_FACTOR_REQUIRED",
            Message = "Please enter your authentication code"
        };
    
    case AuthStatus.PasswordExpired:
        return new LoginResult 
        { 
            StatusCode = "PASSWORD_EXPIRED",
            Message = "Your password has expired. Please update it."
        };
}

Client-Side Usage

The status codes enable sophisticated client-side logic:

const response = await api.post('/auth/login', credentials);

if (response.success) {
    switch (response.statusCode) {
        case 'LOGIN_SUCCESS':
            router.push('/dashboard');
            break;
        case 'TWO_FACTOR_REQUIRED':
            showTwoFactorDialog();
            break;
        case 'PASSWORD_EXPIRED':
            router.push('/change-password');
            break;
        case 'EMAIL_VERIFICATION_REQUIRED':
            showEmailVerificationPrompt();
            break;
    }
} else {
    handleErrors(response.errors);
}

📄 Pagination Support

One of ResponseWrapper's most powerful features is its intelligent pagination handling using duck typing.

The Problem with Traditional Pagination

Most pagination libraries mix business data with pagination metadata:

{
  "items": [...],
  "page": 1,
  "pageSize": 10,
  "totalPages": 5,
  "totalItems": 47
}

This creates inconsistent API responses and makes client development more complex.

ResponseWrapper's Solution: Clean Separation

ResponseWrapper automatically detects pagination objects and separates business data from pagination metadata:

Clean Response with Separated Metadata:

{
  "success": true,
  "data": {
    "items": [
      {"id": 1, "name": "Product 1"},
      {"id": 2, "name": "Product 2"}
    ]
  },
  "metadata": {
    "pagination": {
      "page": 1,
      "pageSize": 10,
      "totalPages": 5,
      "totalItems": 47,
      "hasNextPage": true,
      "hasPreviousPage": false
    },
    "requestId": "...",
    "executionTimeMs": 25
  }
}

Duck Typing: Works with ANY Pagination Library

ResponseWrapper uses duck typing, which means it works with ANY pagination implementation that has the required properties. You don't need to change your existing code!

Works with your existing pagination classes:

// Your existing pagination class - no changes needed!
public class MyCustomPagedResult<T>
{
    public List<T> Items { get; set; }
    public int Page { get; set; }
    public int PageSize { get; set; }
    public int TotalPages { get; set; }
    public int TotalItems { get; set; }
    public bool HasNextPage { get; set; }
    public bool HasPreviousPage { get; set; }
}

// Your controller - no changes needed!
[HttpGet]
public async Task<MyCustomPagedResult<Product>> GetProducts()
{
    return await _productService.GetPagedProductsAsync();
}

Also works with third-party libraries:

// Works with EntityFramework extensions
public async Task<PagedList<User>> GetUsers()
{
    return await context.Users.ToPagedListAsync(page, pageSize);
}

// Works with any library that follows the pagination pattern
public async Task<PaginatedResult<Order>> GetOrders()
{
    return await _orderService.GetPaginatedOrdersAsync();
}

Supported Pagination Patterns

ResponseWrapper automatically detects any object with these properties:

  • Items (List<T>) - The business data
  • Page (int) - Current page number
  • PageSize (int) - Items per page
  • TotalPages (int) - Total number of pages
  • TotalItems (int) - Total number of items
  • HasNextPage (bool) - Whether next page exists
  • HasPreviousPage (bool) - Whether previous page exists

🎯 Real-World Usage Examples

E-Commerce API Example

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }

    // Simple product listing - automatically wrapped
    [HttpGet]
    public async Task<List<Product>> GetProducts()
    {
        return await _productService.GetActiveProductsAsync();
    }

    // Paginated products - pagination metadata automatically extracted
    [HttpGet("paged")]
    public async Task<PagedResult<Product>> GetPagedProducts(int page = 1, int pageSize = 10)
    {
        return await _productService.GetPagedProductsAsync(page, pageSize);
    }

    // Product creation with status codes - automatically wrapped with 201 status
    [HttpPost]
    public async Task<ProductCreationResult> CreateProduct(CreateProductRequest request)
    {
        // Validation happens automatically via ValidationException
        if (!ModelState.IsValid)
        {
            var failures = ModelState.Values
                .SelectMany(v => v.Errors)
                .Select(e => new ValidationFailure("", e.ErrorMessage));
            throw new ValidationException(failures);
        }

        return await _productService.CreateProductAsync(request);
    }

    // Product by ID - automatic 404 handling
    [HttpGet("{id}")]
    public async Task<Product> GetProduct(int id)
    {
        var product = await _productService.GetProductByIdAsync(id);
        
        // This automatically becomes a 404 with proper error structure
        if (product == null)
            throw new NotFoundException("Product", id);
            
        return product;
    }

    // File download - automatically excluded from wrapping
    [HttpGet("{id}/image")]
    public async Task<IActionResult> GetProductImage(int id)
    {
        var imageData = await _productService.GetProductImageAsync(id);
        return new CustomExportResult(imageData, "product.jpg", "image/jpeg");
    }

    // Exclude specific endpoint from wrapping
    [HttpGet("raw")]
    [SkipApiResponseWrapper("Legacy endpoint for backward compatibility")]
    public async Task<List<Product>> GetProductsRaw()
    {
        return await _productService.GetActiveProductsAsync();
    }
}

User Management with Complex Workflows

// Response DTO with status codes
public class UserActivationResult : IHasStatusCode
{
    public User User { get; set; }
    public string StatusCode { get; set; }
    public string Message { get; set; }
}

[ApiController]
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    private readonly IUserService _userService;

    public UsersController(IUserService userService)
    {
        _userService = userService;
    }

    [HttpPost("{id}/activate")]
    public async Task<UserActivationResult> ActivateUser(int id)
    {
        var user = await _userService.GetUserByIdAsync(id);
        if (user == null)
            throw new NotFoundException("User", id);

        // Business rule validation
        if (user.IsActive)
        {
            return new UserActivationResult 
            { 
                User = user,
                StatusCode = "ALREADY_ACTIVE",
                Message = "User is already active"
            };
        }

        if (user.IsSuspended)
        {
            return new UserActivationResult 
            { 
                StatusCode = "ACCOUNT_SUSPENDED",
                Message = "Cannot activate suspended user. Please contact support."
            };
        }

        var activatedUser = await _userService.ActivateUserAsync(id);
        return new UserActivationResult 
        { 
            User = activatedUser,
            StatusCode = "ACTIVATION_SUCCESS",
            Message = "User activated successfully"
        };
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> DeleteUser(int id)
    {
        // Authorization check
        if (!User.IsInRole("Admin"))
            throw new ForbiddenAccessException("Only administrators can delete users");

        await _userService.DeleteUserAsync(id);
        
        // Empty successful response
        return Ok();
    }
}

🔧 Advanced Scenarios

Environment-Specific Configuration

public void ConfigureServices(IServiceCollection services)
{
    services.AddResponseWrapper(
        options =>
        {
            options.EnableExecutionTimeTracking = true;
            options.EnablePaginationMetadata = true;
            
            // Enable detailed query stats only in development
            options.EnableQueryStatistics = Environment.IsDevelopment();
            
            // Different excluded paths per environment
            if (Environment.IsProduction())
            {
                options.ExcludedPaths = new[] { "/health" };
            }
            else
            {
                options.ExcludedPaths = new[] { "/health", "/swagger", "/debug" };
            }
        },
        errorMessages =>
        {
            if (Environment.IsDevelopment())
            {
                // Detailed messages for development
                errorMessages.ValidationErrorMessage = "Validation failed - check detailed error list";
                errorMessages.ApplicationErrorMessage = "Application error - check logs for full stack trace";
            }
            else
            {
                // User-friendly messages for production
                errorMessages.ValidationErrorMessage = "Please check your information and try again";
                errorMessages.ApplicationErrorMessage = "We're experiencing technical difficulties";
            }
        });
}

Integration with Entity Framework for Query Statistics

// Create a custom interceptor for query statistics
public class QueryStatisticsInterceptor : DbConnectionInterceptor
{
    public override ValueTask<InterceptionResult<DbDataReader>> ReaderExecutingAsync(
        DbCommand command,
        CommandEventData eventData,
        InterceptionResult<DbDataReader> result,
        CancellationToken cancellationToken = default)
    {
        // Track query execution
        var httpContext = GetHttpContext();
        if (httpContext != null)
        {
            var queryStats = GetOrCreateQueryStats(httpContext);
            queryStats["QueriesCount"] = (int)queryStats.GetValueOrDefault("QueriesCount", 0) + 1;
            
            var executedQueries = (List<string>)queryStats.GetValueOrDefault("ExecutedQueries", new List<string>());
            executedQueries.Add(command.CommandText);
            queryStats["ExecutedQueries"] = executedQueries.ToArray();
        }

        return base.ReaderExecutingAsync(command, eventData, result, cancellationToken);
    }
}

// Register the interceptor
builder.Services.AddDbContext<AppDbContext>(options =>
{
    options.UseSqlServer(connectionString)
           .AddInterceptors(new QueryStatisticsInterceptor());
});

Custom Logger Implementation

public class CustomApiLogger : ILogger<ApiResponseWrapperFilter>
{
    private readonly ILogger<ApiResponseWrapperFilter> _innerLogger;
    private readonly IMetrics _metrics;

    public CustomApiLogger(ILogger<ApiResponseWrapperFilter> innerLogger, IMetrics metrics)
    {
        _innerLogger = innerLogger;
        _metrics = metrics;
    }

    public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
    {
        // Custom logging logic
        _innerLogger.Log(logLevel, eventId, state, exception, formatter);
        
        // Send metrics
        if (logLevel >= LogLevel.Warning)
        {
            _metrics.Counter("api_errors").Increment();
        }
    }

    // Implement other ILogger methods...
}

// Register with ResponseWrapper
services.AddResponseWrapper<CustomApiLogger>(
    () => DateTime.UtcNow,
    options => options.EnableExecutionTimeTracking = true);

📋 Best Practices

1. Configure for Your Environment

// Development: Enable everything for debugging
if (env.IsDevelopment())
{
    services.AddResponseWrapper(options =>
    {
        options.EnableExecutionTimeTracking = true;
        options.EnableQueryStatistics = true;
        options.EnablePaginationMetadata = true;
        options.EnableCorrelationId = true;
    });
}

// Production: Optimize for performance
if (env.IsProduction())
{
    services.AddResponseWrapper(options =>
    {
        options.EnableExecutionTimeTracking = true;
        options.EnableQueryStatistics = false; // Disable if not needed
        options.EnablePaginationMetadata = true;
        options.EnableCorrelationId = true;
        options.ExcludedPaths = new[] { "/health", "/metrics" };
    });
}

2. Use Specific Exception Types

// Good: Specific exception types
if (user == null)
    throw new NotFoundException("User", id);

if (!user.IsActive)
    throw new BusinessException("User account is not active");

if (!User.IsInRole("Admin"))
    throw new ForbiddenAccessException("Administrator access required");

// Avoid: Generic exceptions
// throw new Exception("Something went wrong");

3. Leverage Application Status Codes

// Good: Rich status information
public class PaymentResult : IHasStatusCode
{
    public string TransactionId { get; set; }
    public decimal Amount { get; set; }
    public string StatusCode { get; set; }
    public string Message { get; set; }
}

// Different status codes for different outcomes
switch (paymentResponse.Status)
{
    case PaymentStatus.Success:
        return new PaymentResult { StatusCode = "PAYMENT_SUCCESS", ... };
    case PaymentStatus.InsufficientFunds:
        return new PaymentResult { StatusCode = "INSUFFICIENT_FUNDS", ... };
    case PaymentStatus.CardExpired:
        return new PaymentResult { StatusCode = "CARD_EXPIRED", ... };
}

4. Leverage Custom Error Messages

// Customize messages for better UX
errorMessages.ValidationErrorMessage = "Please review the highlighted fields and try again";
errorMessages.NotFoundErrorMessage = "We couldn't find what you're looking for";
errorMessages.UnauthorizedAccessMessage = "Please sign in to continue";

5. Exclude Appropriate Endpoints

options.ExcludedPaths = new[]
{
    "/health",        // Health checks
    "/metrics",       // Metrics endpoints
    "/swagger",       // API documentation
    "/api/files",     // File download endpoints
    "/webhooks"       // Webhook endpoints
};

6. Monitor Performance Impact

// Enable detailed monitoring in development
options.EnableQueryStatistics = Environment.IsDevelopment();

// Log execution times for performance monitoring
if (options.EnableExecutionTimeTracking)
{
    // Monitor slow requests
    if (executionTimeMs > 1000)
    {
        logger.LogWarning("Slow request detected: {RequestId} took {ExecutionTime}ms", 
            requestId, executionTimeMs);
    }
}

🐛 Troubleshooting

Common Issues and Solutions

1. Responses Not Being Wrapped

Problem: Some controller responses are not wrapped.

Solutions:

  • Ensure controllers have the [ApiController] attribute
  • Check that the endpoint is not in ExcludedPaths
  • Verify the result type is not in ExcludedTypes
  • Make sure WrapSuccessResponses is enabled
[ApiController] // Required for automatic wrapping
[Route("api/[controller]")]
public class UsersController : ControllerBase
{
    // This will be wrapped
    [HttpGet]
    public async Task<List<User>> GetUsers() { ... }
}
2. Pagination Not Detected

Problem: Pagination metadata is not extracted from custom pagination classes.

Solution: Ensure your pagination class has all required properties:

public class MyPagedResult<T>
{
    // All these properties are required
    public List<T> Items { get; set; }     // Required
    public int Page { get; set; }          // Required
    public int PageSize { get; set; }      // Required
    public int TotalPages { get; set; }    // Required
    public int TotalItems { get; set; }    // Required
    public bool HasNextPage { get; set; }  // Required
    public bool HasPreviousPage { get; set; } // Required
}
3. Status Codes Not Being Extracted

Problem: Application status codes are not appearing in responses.

Solution: Implement the IHasStatusCode interface on your response DTOs:

public class MyResponse : IHasStatusCode
{
    public string Data { get; set; }
    public string StatusCode { get; set; } // This will be promoted to ApiResponse level
    public string Message { get; set; }    // This will also be promoted
}
4. Error Messages Not Showing

Problem: Custom error messages are not displayed.

Solutions:

  • Ensure GlobalExceptionHandlingMiddleware is registered
  • Add the middleware early in the pipeline
  • Check that WrapErrorResponses is enabled
var app = builder.Build();

// Add this EARLY in the pipeline
app.UseMiddleware<GlobalExceptionHandlingMiddleware>();

app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
5. Performance Issues

Problem: API responses are slower after adding ResponseWrapper.

Solutions:

  • Disable query statistics if not needed: options.EnableQueryStatistics = false
  • Exclude high-frequency endpoints: options.ExcludedPaths = new[] { "/api/high-frequency" }
  • Disable execution time tracking for production: options.EnableExecutionTimeTracking = false
6. File Downloads Being Wrapped

Problem: File download endpoints are returning JSON instead of files.

Solutions:

  • Use ISpecialResult interface for custom file results
  • Add file result types to ExcludedTypes
  • Use the [SkipApiResponseWrapper] attribute
// Option 1: Use ISpecialResult
public class FileDownloadResult : ActionResult, ISpecialResult { ... }

// Option 2: Exclude file types
options.ExcludedTypes = new[] { typeof(FileResult), typeof(FileStreamResult) };

// Option 3: Skip specific endpoints
[SkipApiResponseWrapper("File download endpoint")]
public async Task<IActionResult> DownloadFile(int id) { ... }

🤝 Contributing

We welcome contributions to FS.AspNetCore.ResponseWrapper! Here's how you can help:

Reporting Issues

  1. Check existing issues to avoid duplicates
  2. Provide detailed reproduction steps
  3. Include relevant code examples
  4. Specify your .NET and ASP.NET Core versions

Submitting Pull Requests

  1. Fork the repository
  2. Create a feature branch (git checkout -b feature/amazing-feature)
  3. Make your changes with tests
  4. Ensure all tests pass
  5. Update documentation if needed
  6. Commit your changes (git commit -m 'Add amazing feature')
  7. Push to the branch (git push origin feature/amazing-feature)
  8. Open a Pull Request

Development Setup

# Clone the repository
git clone https://github.com/furkansarikaya/FS.AspNetCore.ResponseWrapper.git

# Navigate to the project directory
cd FS.AspNetCore.ResponseWrapper

# Restore dependencies
dotnet restore

# Build the project
dotnet build

# Run tests
dotnet test

Coding Standards

  • Follow Microsoft's C# coding conventions
  • Add comprehensive XML documentation for public APIs
  • Write unit tests for new features
  • Ensure backward compatibility when possible

📜 License

This project is licensed under the MIT License - see the LICENSE file for details.

🙏 Acknowledgments

  • Microsoft for the excellent ASP.NET Core framework
  • The open-source community for inspiration and feedback
  • All contributors who help make this project better

📞 Support


Made with ❤️ by Furkan Sarıkaya

Transform your ASP.NET Core APIs with consistent, metadata-rich responses and intelligent status code management. Install FS.AspNetCore.ResponseWrapper today and experience the difference!

Product Compatible and additional computed target framework versions.
.NET net9.0 is compatible.  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
9.1.0 97 7/17/2025
9.0.0 136 6/25/2025

Version 9.1.0: Added intelligent application status code extraction and promotion feature. ResponseWrapper now automatically detects and promotes status codes from response data that implements IHasStatusCode interface, enabling complex workflow management and rich client-side conditional logic. Enhanced error handling with automatic error code extraction from all exception types.