Skip to content

Instantly share code, notes, and snippets.

@VincenzoCarlino
Last active March 29, 2022 19:07
Show Gist options
  • Save VincenzoCarlino/b7093d823a3d5f1e8795595c064a120f to your computer and use it in GitHub Desktop.
Save VincenzoCarlino/b7093d823a3d5f1e8795595c064a120f to your computer and use it in GitHub Desktop.
Schema validation
namespace TestApi;
using System.Reflection;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Mvc.Infrastructure;
/*
* FOR INFORMATION PURPOSE ONLY!!!
* THIS CODE IS NOT READY FOR PRODUCTION.
*/
public class SchemaValidator
{
private const int MIN_SUCCESS_STATUS_CODE = 200;
private const int MAX_SUCCESS_STATUS_CODE = 299;
private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionsProvider;
public SchemaValidator(
IApiDescriptionGroupCollectionProvider apiDescriptionsProvider
)
{
_apiDescriptionsProvider = apiDescriptionsProvider;
}
public void ValidateControllerResponses()
{
var invalidEndpoints = new List<string>();
var applicableApiDescriptions = _apiDescriptionsProvider.ApiDescriptionGroups.Items
.SelectMany(group => group.Items);
foreach (var apiDescription in applicableApiDescriptions)
{
if (apiDescription.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor)
{
var methodReturnType = controllerActionDescriptor.MethodInfo.ReturnParameter
.ParameterType;
// Check if it is task
if (
methodReturnType.IsGenericType &&
methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(Task<>) &&
methodReturnType.GetTypeInfo().GetGenericArguments().Length > 0
)
{
methodReturnType = methodReturnType.GetGenericArguments().First();
}
// Check if it is ActionResult
if (
methodReturnType.IsGenericType &&
methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(ActionResult<>)
)
{
methodReturnType = methodReturnType.GetGenericArguments().First();
}
// Check if it is StrictJsonResult
if (
methodReturnType.IsGenericType &&
methodReturnType.GetTypeInfo().GetGenericTypeDefinition() == typeof(StrictJsonResult<>)
)
{
methodReturnType = methodReturnType.GetGenericArguments().First();
}
// Get list of unsupported response type (e.g. From [ProducesResponseType])
var unsupportedSuccessResponseSystemTypes = GetSuccessSupportedResponsesTypes(
apiDescription
)
.Where(type => methodReturnType != type)
.ToList();
foreach (var unsupportedType in unsupportedSuccessResponseSystemTypes)
{
invalidEndpoints.Add($"[{apiDescription.HttpMethod} - {apiDescription.RelativePath}]. It should return: {unsupportedType} but instead it returns: {methodReturnType}");
}
}
}
if (invalidEndpoints.Any())
{
throw new Exception($"Following endpoints are invalid: {string.Join("\n", invalidEndpoints)}");
}
}
private IEnumerable<Type> GetSuccessSupportedResponsesTypes(ApiDescription apiDescription)
=> apiDescription.SupportedResponseTypes
.Where(supportedResponseType =>
CheckIfSuccessResponseType(supportedResponseType) &&
supportedResponseType.Type != typeof(void) // This annotation: [ProducesResponseType(200)] make supportedResponseType = System.Void
)
.Select(supportedResponseType => supportedResponseType.Type
.GetTypeInfo().ImplementedInterfaces.Contains(typeof(IConvertToActionResult))
? supportedResponseType.Type.GetGenericArguments().First()
: supportedResponseType.Type
)
.DefaultIfEmpty(typeof(ActionResult)) // if list is empty, add ActionResult as default value
.ToList();
// Check if response status is inside range of success statuses codes
private bool CheckIfSuccessResponseType(ApiResponseType responseType)
=> responseType.StatusCode is >= MIN_SUCCESS_STATUS_CODE and <= MAX_SUCCESS_STATUS_CODE;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment