I have the API project on ASP.NET Core, and it's quite annoying always write methods like this:
[HttpGet("{libraryId:int}")] public async Task<IActionResult> Get(int libraryId) { try { var libraryInfo = await _librariesService .GetLibraryInfo(libraryId); if (libraryInfo is null) { return NotFound(); } return Ok(libraryInfo); } catch (Exception) { return StatusCode(StatusCodes.Status500InternalServerError); } }
So, the flow is:
- Validate arguments
- (optional) Pre process data
- Process data
- (optional) Post process data
- Return processed data with appropriate status code
Here my classes, which implement that flow:
public class ApiResponse { public int StatusCode { get; set; } public List<CommonError> Errors { get; } = new List<CommonError>(); public bool HasErrors => Errors.Count > 0; public bool ShouldSerializeErrors() => HasErrors; public ApiResponse AddErrors(IEnumerable<CommonError> errors) { Errors.AddRange(errors); return this; } public static ApiResponse Make() => new ApiResponse(); } public class ApiResponse<T> : ApiResponse { [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public T Data { get; set; } public static new ApiResponse<T> Make() => new ApiResponse<T>(); } public class CommonError { public string Key { get; private set; } public string Description { get; private set; } public CommonError(string key, string description) { Key = key; Description = description; } } public class CommonResult { public IList<CommonError> Errors { get; } = new List<CommonError>(); public bool HasErrors => Errors.Count > 0; public bool Success => !HasErrors; public CommonResult AddError(CommonError error) { Errors.Add(error); return this; } } public class CommonResult<T> : CommonResult { public T Data { get; set; } } public static class ApiResponseExtensions { public static ApiResponse Validate<T>(this ApiResponse apiResponse, T argument, Func<T, CommonResult> validator) { var result = validator(argument); if (result.HasErrors) { apiResponse.StatusCode = StatusCodes.Status400BadRequest; apiResponse.AddErrors(result.Errors); } return apiResponse; } public static ApiResponse<T> Validate<A, T>(this ApiResponse<T> apiResponse, A argument, Func<A, CommonResult> validator) { var result = validator(argument); if (result.HasErrors) { apiResponse.StatusCode = StatusCodes.Status400BadRequest; apiResponse.AddErrors(result.Errors); } return apiResponse; } public static async Task<ApiResponse<T>> Process<T>(this ApiResponse<T> apiResponse, Func<Task<CommonResult<T>>> func) { if (apiResponse.HasErrors) { return apiResponse; } var processed = await func(); if (processed.HasErrors) { apiResponse.StatusCode = StatusCodes.Status500InternalServerError; apiResponse.AddErrors(processed.Errors); } else { apiResponse.StatusCode = StatusCodes.Status200OK; apiResponse.Data = processed.Data; } return apiResponse; } public static async Task<ApiResponse<T>> SideProcesses<T>(this Task<ApiResponse<T>> apiResponse, Func<Task<CommonResult>>[] funcs) { var apiResponseAwaited = await apiResponse; if (apiResponseAwaited.HasErrors) { return apiResponseAwaited; } foreach (var func in funcs) { var processed = await func(); if (processed.HasErrors) { apiResponseAwaited.AddErrors(processed.Errors); } } return apiResponseAwaited; } public static async Task<ApiResponse<T>> SideProcesses<T>( this Task<ApiResponse<T>> apiResponse, Func<T, Task<CommonResult>>[] funcs) { var apiResponseAwaited = await apiResponse; if (apiResponseAwaited.HasErrors) { return apiResponseAwaited; } foreach (var func in funcs) { var processed = await func(apiResponseAwaited.Data); if (processed.HasErrors) { apiResponseAwaited.AddErrors(processed.Errors); } } return apiResponseAwaited; } public static async Task<IActionResult> ToResult<T>(this Task<ApiResponse<T>> apiResponse) { var apiResponseAwaited = await apiResponse; var result = new ObjectResult(apiResponseAwaited) { StatusCode = apiResponseAwaited.StatusCode }; return result; } }
Rewriting the GET
library info method:
[HttpGet("{libraryId:int}")] public async Task<IActionResult> Get(int libraryId) { return await ApiResponse<LibraryInfo>.Make() .Validate(libraryId, IdValidator), .Process(() => _librariesService.GetLibraryInfo(libraryId)) .ToResult(); }
This solution isn't perfect, so I have a couple of questions, how to best implement it.
Firstly, I don't like that the call order is not defined, and I can call Process
before Validate
, which is incorrect.
Secondly, where do I need to store already validated function arguments? Maybe have Dictionary<string, object> ValidatedArguments
inside ApiResponse
? Some better way?
Finally, what should I do with the status code? Functions in Services
mustn't return it, because they don't know anything about an environment where they are used. Or maybe returning always 200 is enough, when there are special flag and list of errors inside response?