Skip to content

Instantly share code, notes, and snippets.

@mykeels
Last active April 13, 2024 07:34
Show Gist options
  • Save mykeels/054b88035bf1bb5be57be95fd176f939 to your computer and use it in GitHub Desktop.
Save mykeels/054b88035bf1bb5be57be95fd176f939 to your computer and use it in GitHub Desktop.
Code for extracting OpenAPI schema from ASP.NET Core projects
// Use a conditional, because You may not want to provide swagger documentation in public environments
if (_configuration.GetValue<bool>("SwaggerConfiguration:EnableSwagger"))
{
services.AddEndpointsApiExplorer();
services.AddSwaggerGen(options => {
options.AddCustomIds();
options.AddMetadata(typeof(Program));
options.SchemaFilter<NullableEnumSchemaFilter>();
options.SchemaFilter<RequiredPropertiesSchemaFilter>();
options.SchemaFilter<EnumDescriptionSchemaFilter>();
});
}
{
"version": 1,
"isRoot": true,
"tools": {
"swashbuckle.aspnetcore.cli": {
"version": "6.2.3",
"commands": [
"swagger"
]
}
}
}
using System;
using System.Linq;
using System.Collections.Generic;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
public class EnumDescriptionSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (context.Type.IsEnum)
{
var enumStringNames = Enum.GetNames(context.Type);
IEnumerable<long> enumStringValues;
try
{
enumStringValues = Enum.GetValues(context.Type).Cast<long>();
}
catch
{
enumStringValues = Enum.GetValues(context.Type).Cast<int>().Select(i => Convert.ToInt64(i));
}
var enumStringKeyValuePairs = enumStringNames.Zip(enumStringValues, (name, value) => $"{value} = {name}");
var enumStringNamesAsOpenApiArray = new OpenApiArray();
enumStringNamesAsOpenApiArray.AddRange(enumStringNames.Select(name => new OpenApiString(name)).ToArray());
schema.Description = string.Join("\n", enumStringKeyValuePairs);
schema.Extensions.Add("x-enum-varnames", enumStringNamesAsOpenApiArray);
schema.Extensions.Add("x-enumNames", enumStringNamesAsOpenApiArray);
}
}
}
using System.Reflection;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Linq;
using System.Collections.Generic;
using AutoMapper.Internal;
public class NullableEnumSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
var isReferenceType =
TypeHelper.IsReference(context.Type) &&
!TypeHelper.IsCLR(context.Type) &&
!TypeHelper.IsMicrosoft(context.Type);
if(!isReferenceType) { return; }
var bindingFlags = BindingFlags.Public | BindingFlags.Instance;
var members = context.Type.GetFields(bindingFlags).Cast<MemberInfo>()
.Concat(context.Type.GetProperties(bindingFlags))
.ToArray();
var hasNullableEnumMembers = members.Any(x => TypeHelper.IsNullableEnum(x.GetMemberType()));
if (!hasNullableEnumMembers) { return; }
schema.Properties.Where(x => !x.Value.Nullable).ToList().ForEach(property =>
{
var name = property.Key;
var possibleNames = new string[]
{
name,
TextCaseHelper.ToPascalCase(name),
}; // handle different cases
var sourceMember = possibleNames
.Select(n => context.Type.GetMember(n, bindingFlags).FirstOrDefault())
.Where(x => x != null)
.FirstOrDefault();
if (sourceMember == null) { return; }
var sourceMemberType = sourceMember.GetMemberType();
if (sourceMemberType == null || !TypeHelper.IsNullableEnum(sourceMemberType)) { return; }
// manual nullability fixes
if (property.Value.Reference != null)
{
// https://stackoverflow.com/a/48114924/5168794
property.Value.Nullable = true;
property.Value.AllOf = new List<OpenApiSchema>()
{
new OpenApiSchema
{
Reference = property.Value.Reference,
},
};
property.Value.Reference = null;
}
});
}
}
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Interfaces;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
public static class OpenApiExtensions
{
/// <summary>
/// Adds custom operation and schema ids to the Swagger document.
/// </summary>
/// <param name="options"></param>
/// <returns></returns>
public static SwaggerGenOptions AddCustomIds(this SwaggerGenOptions options)
{
options.CustomOperationIds(e => $"{e.ActionDescriptor.RouteValues["controller"]}_{e.ActionDescriptor.RouteValues["action"]}");
options.CustomSchemaIds(type => type.FullName?.Replace("+", "."));
return options;
}
/// <summary>
/// Adds metadata to the Swagger document, such as:
/// - Title
/// - Version e.g. v1
/// - API Version e.g. 1.0.0, derived from the AssemblyInformationalVersionAttribute
/// </summary>
/// <param name="options"></param>
/// <param name="type"></param>
/// <returns></returns>
public static SwaggerGenOptions AddMetadata(this SwaggerGenOptions options, Type type)
{
var entryAssembly = type
.GetTypeInfo()
.Assembly;
string projectVersion = entryAssembly
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
?.InformationalVersion ?? "1.0.0";
options.SwaggerDoc(
"v1",
new OpenApiInfo
{
Title = entryAssembly.GetName().Name,
Version = "v1",
Extensions = new Dictionary<string, IOpenApiExtension>
{
{ "apiVersion", new OpenApiString(projectVersion) }
}
}
);
return options;
}
}
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
public class RequiredPropertiesSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (schema.Type == "object")
{
foreach (var openApiSchema in schema.Properties)
{
if (openApiSchema.Value.Nullable == false)
{
schema.Required.Add(openApiSchema.Key);
}
}
}
}
}
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<Version>0.0.1</Version>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet tool restore" />
<Exec Command="dotnet swagger tofile --output obj/swagger.json $(OutputPath)$(AssemblyName).dll v1" EnvironmentVariables="ASPNETCORE_ENVIRONMENT=Development" />
</Target>
</Project>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment