Skip to content

Instantly share code, notes, and snippets.

@dotnet22
Forked from mykeels/0_Program.cs
Created November 22, 2024 07:29
Show Gist options
  • Select an option

  • Save dotnet22/e0db3ab78bdc907897a3aa8a1949a5ef to your computer and use it in GitHub Desktop.

Select an option

Save dotnet22/e0db3ab78bdc907897a3aa8a1949a5ef to your computer and use it in GitHub Desktop.

Revisions

  1. @mykeels mykeels revised this gist Apr 13, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion 0_Program.cs
    Original file line number Diff line number Diff line change
    @@ -4,11 +4,11 @@
    services.AddEndpointsApiExplorer();
    services.AddSwaggerGen(options => {
    options.AddCustomIds();
    options.AddMetadata(typeof(Program));

    options.SchemaFilter<NullableEnumSchemaFilter>();
    options.SchemaFilter<RequiredPropertiesSchemaFilter>();
    options.SchemaFilter<EnumDescriptionSchemaFilter>();

    options.AddMetadata(typeof(Program));
    });
    }
  2. @mykeels mykeels renamed this gist Apr 13, 2024. 1 changed file with 0 additions and 0 deletions.
    File renamed without changes.
  3. @mykeels mykeels created this gist Apr 13, 2024.
    32 changes: 32 additions & 0 deletions EnumDescriptionSchemaFilter.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,32 @@
    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);
    }
    }
    }
    59 changes: 59 additions & 0 deletions NullableEnumSchemaFilter.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,59 @@
    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;
    }
    });
    }
    }
    53 changes: 53 additions & 0 deletions OpenApiExtensions.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,53 @@
    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;
    }
    }
    14 changes: 14 additions & 0 deletions Program.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,14 @@
    // 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.SchemaFilter<NullableEnumSchemaFilter>();
    options.SchemaFilter<RequiredPropertiesSchemaFilter>();
    options.SchemaFilter<EnumDescriptionSchemaFilter>();

    options.AddMetadata(typeof(Program));
    });
    }
    19 changes: 19 additions & 0 deletions RequiredPropertiesSchemaFilter.cs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,19 @@
    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);
    }
    }
    }
    }
    }
    12 changes: 12 additions & 0 deletions dotnet-tools.json
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,12 @@
    {
    "version": 1,
    "isRoot": true,
    "tools": {
    "swashbuckle.aspnetcore.cli": {
    "version": "6.2.3",
    "commands": [
    "swagger"
    ]
    }
    }
    }
    15 changes: 15 additions & 0 deletions sample.csproj
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,15 @@
    <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>