Skip to content

This library provides a generic response type compatible with IActionResult in Microsoft.AspNetCore.Mvc

License

Notifications You must be signed in to change notification settings

ogulcanturan/Ogu.Response

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

110 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Header Ogu.Response

.NET Core Desktop NuGet

Introduction

Provides a generic response type (IResponse) compatible with IActionResult in Microsoft.AspNetCore.Mvc. This library is designed to simplify API responses by offering a structured model that can contain data, errors, validation errors, and additional extensions.

Features

  • Generic Response: A flexible and generic response model that can hold various types of data, including single objects, collections, or custom data structures.
  • Error Handling: Easily handle errors and exceptions in API responses, providing consistent error formats.
  • Validation Errors: Supports validation errors, making it easy to report validation issues in the response.
  • Extensions: Includes support for adding custom extensions to the response for specific use cases.

Installation

You can install the library via NuGet Package Manager:

dotnet add package Ogu.AspNetCore.Response

Usage

example 1: Returning an Success Response

public IActionResult GetExample1()
{
    return HttpStatusCode.OK.ToSuccessResponse(new string[]{ "Freezing", "Bracing", "Chilly" }).ToAction();
}

output

{
  "success": true,
  "status": 200,
  "data": [
    "Freezing",
    "Bracing",
    "Chilly"
  ]
}

example 2: Returning an Error Response Using an Enum

public enum ErrorKind
{
    [Error("Example Error", "Don't worry, everything's gonna be alright", "", "https://google.com")]
    ExampleErrorOccurred = 1
}
public IActionResult GetExample2()
{
    return HttpStatusCode.OK.ToFailureResponse(ErrorKind.ExampleErrorOccurred).ToAction();
}

output

{
  "success": false,
  "status": 200,
  "errors": [
    {
      "title": "Example Error",
      "description": "Don't worry, everything's gonna be alright",
      "traces": "",
      "code": "1",
      "helpLink": "https://google.com",
      "type": 0 // 0: Custom, 1: Validation, 2: Exception
    }
  ]
}

example 3: Returning an Error Response via ModelState

public IActionResult GetExample10([FromBody] SampleModel sample)
{
    return ModelState.IsValid
        ? HttpStatusCode.OK.ToSuccessResponse().ToAction()
        : ModelState.ToAction(); 
}

output

{
  "success": false,
  "status": 400,
  "errors": [
    {
      "title": "Validation Error",
      "description": "One or more validation errors occurred.",
      "type": 1,
      "validationFailures": [
        {
          "propertyName": "Name",
          "message": "The field Name must be a string with a minimum length of 3 and a maximum length of 50.",
          "severity": 0
        }
      ]
    }
  ]
}

Tip

If controller attached with [ApiController] attribute, ASP.NET Core automatically returns a 400 Bad Request when ModelState is invalid — before your controller code is even reached. To take full control you need to suppress this behavior:
services.Configure<ApiBehaviorOptions>(options => options.SuppressModelStateInvalidFilter = true);

example 4: Including Additional Data in the Response

public IActionResult GetExample5()
{
    var samples = HttpStatusCode.OK.ToSuccessResponse(new string[]{ "Freezing", "Bracing", "Chilly" });
    
    samples.Extras["IsExample"] = true;

    return samples.ToAction();
}

output

{
  "success": true,
  "status": 200,
  "data": [
    "Freezing",
    "Bracing",
    "Chilly"
  ],
  "extras": {
    "isExample": true
  }
}

example 5: Returning an Error Response via an Occurred Exception

public IActionResult GetExample8()
{
    try
    {
        int x = 0;
        int y = 5 / x; // Will throw an exception

        return HttpStatusCode.OK.ToSuccessResponse().ToAction();
    }
    catch (Exception ex)
    {
        return ex.ToResponse().ToAction();
    }
}

output

{
  "success": false,
  "status": 500,
  "errors": [
    {
      "title": "Exception",
      "description": "Attempted to divide by zero.",
      "traces": "DivideByZeroException: Attempted to divide by zero.",
      "code": "-2147352558",
      "type": 2
    }
  ]
}

example 6: Returning an Error Response via an Occurred Exception

public IActionResult GetExamples14([FromQuery][Required] ExceptionTraceLevel traceLevel)
{
    try
    {
        var innerInnerEx = new InvalidOperationException("Operation isn't valid");

        var innerEx = new ApplicationException("Application caught an expected exception", innerInnerEx);

        throw new Exception("There are some exceptions", innerEx);
    }
    catch (Exception ex)
    {
        return ex.ToResponse(traceLevel).ToAction();
    }
}

output: when the traces level 2 ( Summary )

{
  "success": false,
  "status": 500,
  "errors": [
    {
      "title": "Exception",
      "description": "There are some exceptions",
      "traces": "Exception: There are some exceptions -> ApplicationException: Application caught an expected exception -> InvalidOperationException: Operation isn't valid",
      "code": "-2146233088",
      "type": 2
    }
  ]
}

example 7: Returning an Error Response via a Custom Validation Rule

public IActionResult GetExamples13([FromBody] string id)
{
    var idRule = ValidationRules.GreaterThanRule(nameof(id), id, 0);

    if (idRule.IsFailed())
    {
        return idRule.Failure.ToResponse().ToAction();
    }

    var storedIdValue = idRule.GetStoredValue<int>();

    return HttpStatusCode.OK.ToSuccessResponse(storedIdValue).ToAction();
}

output: When the requested id value is 0

{
  "success": false,
  "status": 400,
  "errors": [
    {
      "title": "Validation Error",
      "description": "One or more validation errors occurred.",
      "type": 1,
      "validationFailures": [
        {
          "propertyName": "id",
          "message": "id must be valid number and value must be greater than 0.",
          "attemptedValue": "0",
          "severity": 0
        }
      ]
    }
  ]
}

output: When the requested id value is greater than 1

{
  "success": true,
  "status": 200,
  "data": 1
}

Handling Exceptions

In ASP.NET Core, you can register the exception-handling middleware early in the pipeline to ensure consistent error responses using Response.

app.UseExceptionHandler(cfg =>
{
    cfg.Run(async (context) =>
    {
        var contextFeature = context.Features.Get<IExceptionHandlerFeature>();

        var response = contextFeature.Error.ToResponse(ExceptionTraceLevel.Basic); // Default uses ExceptionTraceLevel.Basic
        
        // (optional) Add extras into the response model. e.g., CorrelationId etc.
        response.Extras.Add(nameof(HeaderNames.RequestId), Activity.Current?.Id ?? context.TraceIdentifier);

        await response.ToAction().ExecuteResultAsync(context);
    });
});
[HttpGet("examples/15")]
public IActionResult GetExamples15()
{
    var innerInnerEx = new InvalidOperationException("Operation isn't valid");
    var innerEx = new ApplicationException("Application caught an expected exception", innerInnerEx);
    throw new Exception("There are some exceptions", innerEx);
}

output:

{
  "success": false,
  "status": 500,
  "errors": [
    {
      "title": "Exception",
      "description": "There are some exceptions",
      "traces": "Exception: There are some exceptions -> ApplicationException: Application caught an expected exception -> InvalidOperationException: Operation isn't valid",
      "code": "-2146233088",
      "type": 2
    }
  ],
  "extras": {
    "requestId": "00-127004cd9c8f88d3559496646d2624c8-4db22e889d3a2b52-00"
  }
}

Validating a Request Model with a Validator

  • Request model
public class GetExamplesSixteenRequest
{
    public string Ids { get; set; }
}
  • Validated model
public class ValidatedGetExamplesSixteenRequest(List<IValidationFailure> failures = null) : Validated(failures)
{
    public HashSet<int> ParsedIds { get; init; } = [];
}
  • Request Validator
public class GetExamplesSixteenRequestValidator : IValidator<GetExamplesSixteenRequest, ValidatedGetExamplesSixteenRequest>
{
    public ValueTask<ValidatedGetExamplesSixteenRequest> ValidateAsync(GetExamplesSixteenRequest request, CancellationToken cancellationToken = default)
    {
        var hashSetRule = ValidationRules.ValidHashSetRule<int>(nameof(request.Ids), request.Ids);

        if (hashSetRule.IsFailed())
        {
            return new ValueTask<ValidatedGetExamplesSixteenRequest>(new ValidatedGetExamplesSixteenRequest([hashSetRule.Failure]));
        }

        var storedIds = hashSetRule.GetStoredValue<HashSet<int>>();

        return new ValueTask<ValidatedGetExamplesSixteenRequest>(new ValidatedGetExamplesSixteenRequest
        {
            ParsedIds = storedIds
        });
    }
}
  • Registering services
builder.Services.AddValidator(); // Internally registers the IValidator interface
builder.Services.AddSingleton<IValidator<GetExamplesSixteenRequest, ValidatedGetExamplesSixteenRequest>, GetExamplesSixteenRequestValidator>();
  • Controller
[Route("api/[controller]")]
public class SamplesController(IValidator validator) : ControllerBase
{
    [HttpGet("examples/16")]
    public async Task<IActionResult> GetExamples16(GetExamplesSixteenRequest request)
    {
        var validated = await validator.ValidateAsync<GetExamplesSixteenRequest, ValidatedGetExamplesSixteenRequest>(request);
        
        return validated.HasFailed ? validated.Failures.ToResponse().ToActionDto() : HttpStatusCode.OK.ToSuccessResponse(validated.ParsedIds).ToActionDto();
    }
}

Handling Model Validation Errors

In ASP.NET Core, you can customize how model validation errors are returned by configuring ApiBehaviorOptions. This allows you to return a consistent Response instead of the default BadRequestObjectResult.

services.Configure<ApiBehaviorOptions>(options =>
{
    // Override the default behavior to return Response for model validation errors
    options.InvalidModelStateResponseFactory = context => context.ModelState.ToAction();
});

Built-in Validation Rules

There are 9 built-in validation rules:

  • ValidationRules.GreaterThanRule: To check if a property value is greater than a specified threshold.
    Parsed value can be retrieved through the rule's GetStoredValue<T>() method.

  • ValidationRules.SmallerThanRule: To check if a property value is smaller than a specified threshold. Parsed value can be retrieved through the rule's GetStoredValue<T>() method.

  • ValidationRules.EqualToRule: To check if a property value is equal to a specified value. Parsed value can be retrieved through the rule's GetStoredValue<T>() method.

  • ValidationRules.NotNullRule: To check if a property value is not null.

  • ValidationRules.NotEmptyRule: To check if a property value is not empty.

  • ValidationRules.ValidBooleanRule: To check if a property value is a valid boolean string ("true" or "false").
    Parsed value can be retrieved through the rule's GetStoredValue<bool>() method.

  • ValidationRules.ValidEnumRule: To check if a property value is a valid enum value of the specified enum type.
    Parsed value can be retrieved through the rule's GetStoredValue<T>() method.

  • ValidationRules.ValidNumberRule: To check if a property value is a valid number (integer or floating-point).
    Parsed value can be retrieved through the rule's GetStoredValue<T>() method.

  • ValidationRules.ValidJsonRule: To check if a property value is a valid json string.
    Parsed value can be retrieved through the rule's GetStoredValue<JsonDocument>() method. After done with the JsonDocument do not forget the dispose it.

  • ValidationRules.ValidHashSetRule: To check if a property value is a valid json string.
    Parsed value can be retrieved through the rule's GetStoredValue<HashSet<T>>() method.

You can extend the rules above, just like the one below.

public static IValidationFailure InvalidBooleanFormat(string propertyName, object attemptedValue)
{
    return new ValidationFailure(
        propertyName,
        $"The value '{attemptedValue}' for field '{propertyName}' is not a valid boolean.",
        attemptedValue
    );
}
public static ValidationRule ValidBooleanRule(string propertyName, string propertyValue)
{
    return new ValidationRule(() => ValidationFailures.InvalidBooleanFormat(propertyName, propertyValue),
        (v) =>
        {
            if (!bool.TryParse(propertyValue, out var parsedValue))
            {
                return false; // Return false to indicate validation failure if parsing fails
            }

            v.Store(parsedValue); // Store the parsed boolean value 

            return true; // Return true indicating validation success
        });
}

Deserialization Process

When deserializing a Response, you cannot use JsonSerializer.Deserialize directly, as it does not support deserializing interfaces. If the response indicates a failure, the deserialization process will throw an exception.

To address this, you should create corresponding models instead.

public class ResponseDto : ResponseDto<object>
{
}

public class ResponseDto<T>
{
    public bool Success { get; init; }

    public HttpStatusCode Status { get; init; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public T Data { get; init; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public ErrorDto[] Errors { get; init; } = [];

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public Dictionary<string, object> Extras { get; init; } = [];
}
public class ErrorDto
{
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string Title { get; init; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string Description { get; init; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string Traces { get; init; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string Code { get; init; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string HelpLink { get; init; }

    public ErrorType Type { get; init; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public ValidationFailureDto[] ValidationFailures { get; init; } = [];
}
public class ValidationFailureDto
{
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string PropertyName { get; init; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string Message { get; init; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public object AttemptedValue { get; init; }

    public Severity Severity { get; init; }

    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
    public string Code { get; init; }
}
public static class DtoExtensions
{
    public static IResponse ToResponse(this ResponseDto responseDto)
    {
        return new Response(responseDto.Data, responseDto.Success, responseDto.Status, responseDto.Extras,
            responseDto.Errors?.Select(IError (e) => e.ToError()).ToList());
    }

    public static IResponse<TData> ToResponseOf<TData>(this ResponseDto<TData> responseDto)
    {
        return new Response<TData>(responseDto.Data, responseDto.Success, responseDto.Status, responseDto.Extras,
            responseDto.Errors?.Select(IError (e) => e.ToError()).ToList());
    }

    public static IResponse<TData> ToResponseOf<TData>(this ResponseDto responseDto)
    {
        var data = responseDto.Data switch
        {
            null => default,
            TData tData => tData,
            _ => (TData)Convert.ChangeType(responseDto.Data, typeof(TData))
        };

        return new Response<TData>(data, responseDto.Success, responseDto.Status, responseDto.Extras, responseDto.Errors?.Select(IError (e) => e.ToError()).ToList());
    }

    public static IActionResult ToActionDto(this ModelStateDictionary modelState)
    {
        return modelState.ToResponse().ToActionDto();
    }

    public static IActionResult ToActionDto<T>(this ModelStateDictionary modelState)
    {
        return modelState.ToResponse().ToActionDto();
    }

    public static IActionResult ToAction(this ResponseDto responseDto)
    {
        return InternalToAction((int)responseDto.Status, responseDto);
    }

    public static IActionResult ToAction<T>(this ResponseDto<T> responseDto)
    {
        return InternalToAction((int)responseDto.Status, responseDto);
    }

    private static IActionResult InternalToAction(int statusCode, object response)
    {
        return ResponseDefaults.NoResponseStatusCodes.Contains(statusCode)
            ? (IActionResult)new StatusCodeResult(statusCode)
            : new ObjectResult(response) { StatusCode = statusCode };
    }

    public static IActionResult ToActionDto(this IResponse response)
    {
        var responseDto = new ResponseDto
        {
            Success = response.Success,
            Status = response.Status,
            Data = response.Data,
            Errors = response.Errors.Count == 0
                ? null
                : response.Errors.Select(e => e.ToErrorDto()).ToArray(),
            Extras = response.Extras.Count == 0 ? null : response.Extras as Dictionary<string, object> ?? response.Extras.ToDictionary()
        };

        return responseDto.ToAction();
    }

    public static IActionResult ToActionDto<T>(this IResponse<T> response)
    {
        var responseDto = new ResponseDto<T>
        {
            Success = response.Success,
            Status = response.Status,
            Data = response.Data,
            Errors = response.Errors.Count == 0
                ? null
                : response.Errors.Select(e => e.ToErrorDto()).ToArray(),
            Extras = response.Extras.Count == 0 ? null : response.Extras as Dictionary<string, object> ?? response.Extras.ToDictionary()
        };

        return responseDto.ToAction();
    }

    private static Error ToError(this ErrorDto errorDto)
    {
        return new Error(errorDto.Title, errorDto.Description, errorDto.Traces, errorDto.Code, errorDto.HelpLink,
            errorDto.ValidationFailures?.Select(IValidationFailure (vf) => vf.ToValidationFailure()).ToList(), errorDto.Type);
    }

    private static ValidationFailure ToValidationFailure(this ValidationFailureDto vfDto)
    {
        return new ValidationFailure(vfDto.PropertyName, vfDto.Message, vfDto.AttemptedValue,
            vfDto.Severity, vfDto.Code);
    }

    private static ErrorDto ToErrorDto(this IError error)
    {
        return new ErrorDto
        {
            Title = error.Title,
            Description = error.Description,
            Traces = error.Traces,
            Code = error.Code,
            HelpLink = error.HelpLink,
            Type = error.Type,
            ValidationFailures = error.ValidationFailures.Count == 0
                ? null
                : error.ValidationFailures.Select(vf => vf.ToValidationFailureDto()).ToArray()
        };
    }

    private static ValidationFailureDto ToValidationFailureDto(this IValidationFailure vf)
    {
        return new ValidationFailureDto
        {
            PropertyName = vf.PropertyName,
            Message = vf.Message,
            AttemptedValue = vf.AttemptedValue,
            Severity = vf.Severity,
            Code = vf.Code
        };
    }
}

If you're using HttpClient to retrieve data of type Response, you can create extensions as shown below:

public static class HttpContentResponseExtensions
{
    public static async Task<IResponse> ToResponseAsync(this HttpContent content, JsonSerializerOptions serializerOptions = null, CancellationToken cancellationToken = default)
    {
        var responseDto = await content.ReadFromJsonAsync<ResponseDto>(serializerOptions, cancellationToken);

        return responseDto.ToResponse();
    }

    public static async Task<IResponse<T>> ToResponseAsync<T>(this HttpContent content, JsonSerializerOptions serializerOptions = null, CancellationToken cancellationToken = default)
    {
        var responseDto = await content.ReadFromJsonAsync<ResponseDto<T>>(serializerOptions, cancellationToken);

        return responseDto.ToResponse();
    }
}

Usage

using (var response = await _httpClient.GetAsync(relativeUri, cancellationToken
{
    return await response.Content.ToResponseAsync(cancellationToken: cancellationToken);
}

using (var response = await _httpClient.GetAsync(relativeUri, cancellationToken
{
    return await response.Content.ToResponseAsync<T>(cancellationToken: cancellationToken); // For generic type
}

Sample Application

A sample application demonstrating the usage of Ogu.Response can be found here.

About

This library provides a generic response type compatible with IActionResult in Microsoft.AspNetCore.Mvc

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages