using System.Diagnostics.Eventing.Reader; using System.Reflection; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Http.Metadata; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; namespace NejCommon.Controllers { public class TypedResultsMetadataProvider : IOperationFilter { private readonly Lazy _contentTypes; /// /// Constructor to inject services /// /// MVC options to define response content types public TypedResultsMetadataProvider(IOptions mvc) { _contentTypes = new Lazy(() => { var apiResponseTypes = new List(); if (mvc.Value == null) { apiResponseTypes.Add("application/json"); } else { var jsonApplicationType = mvc.Value.FormatterMappings.GetMediaTypeMappingForFormat("json"); if (jsonApplicationType != null) apiResponseTypes.Add(jsonApplicationType); var xmlApplicationType = mvc.Value.FormatterMappings.GetMediaTypeMappingForFormat("xml"); if (xmlApplicationType != null) apiResponseTypes.Add(xmlApplicationType); } return apiResponseTypes.ToArray(); }); } void IOperationFilter.Apply(OpenApiOperation operation, OperationFilterContext context) { if (!IsControllerAction(context)) return; var actionReturnType = UnwrapTask(context.MethodInfo.ReturnType); if (!IsHttpResults(actionReturnType)) return; if (typeof(IEndpointMetadataProvider).IsAssignableFrom(actionReturnType)) { var populateMetadataMethod = actionReturnType.GetMethod("Microsoft.AspNetCore.Http.Metadata.IEndpointMetadataProvider.PopulateMetadata", BindingFlags.Static | BindingFlags.NonPublic); if (populateMetadataMethod == null) return; var endpointBuilder = new MetadataEndpointBuilder(); populateMetadataMethod.Invoke(null, new object[] { context.MethodInfo, endpointBuilder }); var responseTypes = endpointBuilder.Metadata.Cast().ToList(); if (!responseTypes.Any()) return; operation.Responses.Clear(); foreach (var responseType in responseTypes) { var statusCode = responseType.StatusCode.ToString(); var oar = new OpenApiResponse { Description = GetResponseDescription(statusCode) }; if (responseType.Type != null && responseType.Type != typeof(void)) { var schema = context.SchemaGenerator.GenerateSchema(responseType.Type, context.SchemaRepository); foreach (var contentType in _contentTypes.Value) { oar.Content.Add(contentType, new OpenApiMediaType { Schema = schema }); } } operation.Responses.Add(statusCode, oar); } } else if (actionReturnType == typeof(UnauthorizedHttpResult)) { operation.Responses.Clear(); operation.Responses.Add("401", new OpenApiResponse { Description = ReasonPhrases.GetReasonPhrase(401) }); } var generics = actionReturnType.GetGenericArguments(); foreach (var generic in generics) { if (generic == typeof(FileStreamHttpResult)) { var statusCode = "200"; var oar = new OpenApiResponse { Description = GetResponseDescription(statusCode) }; oar.Content.Add( "application/octet-stream", new OpenApiMediaType { Schema = new OpenApiSchema { Type = "string", Format = "binary" } }); operation.Responses.Add(statusCode, oar); } } } private static bool IsControllerAction(OperationFilterContext context) => context.ApiDescription.ActionDescriptor is ControllerActionDescriptor; private static bool IsHttpResults(Type type) => type.Namespace == "Microsoft.AspNetCore.Http.HttpResults"; private static Type UnwrapTask(Type type) { if (type.IsGenericType) { var genericType = type.GetGenericTypeDefinition(); if (genericType == typeof(Task<>) || genericType == typeof(ValueTask<>)) { return type.GetGenericArguments()[0]; } } return type; } private static string? GetResponseDescription(string statusCode) => ResponseDescriptionMap .FirstOrDefault(entry => Regex.IsMatch(statusCode, entry.Key)) .Value; private static readonly IReadOnlyCollection> ResponseDescriptionMap = new[] { new KeyValuePair("1\\d{2}", "Information"), new KeyValuePair("201", "Created"), new KeyValuePair("202", "Accepted"), new KeyValuePair("204", "No Content"), new KeyValuePair("2\\d{2}", "Success"), new KeyValuePair("304", "Not Modified"), new KeyValuePair("3\\d{2}", "Redirect"), new KeyValuePair("400", "Bad Request"), new KeyValuePair("401", "Unauthorized"), new KeyValuePair("403", "Forbidden"), new KeyValuePair("404", "Not Found"), new KeyValuePair("405", "Method Not Allowed"), new KeyValuePair("406", "Not Acceptable"), new KeyValuePair("408", "Request Timeout"), new KeyValuePair("409", "Conflict"), new KeyValuePair("429", "Too Many Requests"), new KeyValuePair("4\\d{2}", "Client Error"), new KeyValuePair("5\\d{2}", "Server Error"), new KeyValuePair("default", "Error") }; private sealed class MetadataEndpointBuilder : EndpointBuilder { public override Endpoint Build() => throw new NotImplementedException(); } } }