Skip to content

Commit

Permalink
Merge pull request #694 from SteveDunn/improve-swashbuckle-mapping
Browse files Browse the repository at this point in the history
Improve Swashbuckle generated code
  • Loading branch information
SteveDunn authored Oct 31, 2024
2 parents 303644d + c8f8a8b commit 9aaaea1
Show file tree
Hide file tree
Showing 27 changed files with 241 additions and 752 deletions.
6 changes: 6 additions & 0 deletions Consumers.sln
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Infra", "samples\Onion\Infr
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrleansExample", "samples\OrleansExample\OrleansExample.csproj", "{2F496E59-0462-431A-A70B-266B58A9A672}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApplication.Shared", "samples\WebApplication.Shared\WebApplication.Shared.csproj", "{111EE1B7-E2F0-45EF-9D2D-86B5EF9EE899}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -72,6 +74,10 @@ Global
{2F496E59-0462-431A-A70B-266B58A9A672}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2F496E59-0462-431A-A70B-266B58A9A672}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2F496E59-0462-431A-A70B-266B58A9A672}.Release|Any CPU.Build.0 = Release|Any CPU
{111EE1B7-E2F0-45EF-9D2D-86B5EF9EE899}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{111EE1B7-E2F0-45EF-9D2D-86B5EF9EE899}.Debug|Any CPU.Build.0 = Debug|Any CPU
{111EE1B7-E2F0-45EF-9D2D-86B5EF9EE899}.Release|Any CPU.ActiveCfg = Release|Any CPU
{111EE1B7-E2F0-45EF-9D2D-86B5EF9EE899}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
15 changes: 13 additions & 2 deletions docs/site/Writerside/topics/how-to/Use-in-Swagger.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Both ways include setting a parameter in Vogen's global config:

```C#
[assembly: VogenDefaults(
openApiSchemaCustomizations: OpenApiSchemaCustomizations.[choices])]
openApiSchemaCustomizations: OpenApiSchemaCustomizations.[choices])]
```

The choices are: `GenerateSwashbuckleMappingExtensionMethod` or `GenerateSwashbuckleSchemaFilter`
Expand All @@ -33,7 +33,7 @@ The extension method mechanism is preferable to the schema filter mechanism, as

The extension method that is generated looks like this:
```C#
public static SwaggerGenOptions MapVogenTypes(this SwaggerGenOptions o)
static SwaggerGenOptions MapVogenTypes(this SwaggerGenOptions o)
{
o.MapType<Celcius>(() => new OpenApiSchema { Type = "number" });
o.MapType<City>(() => new OpenApiSchema { Type = "string" });
Expand All @@ -46,6 +46,17 @@ You register it like this:
builder.Services.AddSwaggerGen(opt => opt.MapVogenTypes());
```

<note>
If your value objects are defined in another project, that project will also need assembly-level Vogen configuration.
Your other project will then emit a source generated extension method named `MapVogenTypes`.
Because there are now two extension methods with the same signature, you'll need to call them explicitly, e.g.

```c#
WebApp.VogenSwashbuckleExtensions.MapVogenTypes(opt);
WebApp.Shared.VogenSwashbuckleExtensions.MapVogenTypes(opt);
```
</note>

If you decide to use the schema filter, it looks like this:

```C#
Expand Down
11 changes: 11 additions & 0 deletions samples/WebApplication.Shared/Class1.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using Vogen;

[assembly: VogenDefaults(
openApiSchemaCustomizations: OpenApiSchemaCustomizations.GenerateSwashbuckleMappingExtensionMethod)]

namespace WebApplication.Shared;


[ValueObject]
public partial struct SharedStruct;

36 changes: 36 additions & 0 deletions samples/WebApplication.Shared/WebApplication.Shared.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OpenApiMode>Swashbuckle</OpenApiMode>
<UseLocallyBuiltPackage>true</UseLocallyBuiltPackage>
</PropertyGroup>

<PropertyGroup Condition=" '$(OpenApiMode)' == 'MicrosoftAndScalar'">
<TargetFramework>net9.0</TargetFramework>
<DefineConstants>USE_MICROSOFT_OPENAPI_AND_SCALAR</DefineConstants>
</PropertyGroup>

<ItemGroup Condition=" '$(OpenApiMode)' == 'MicrosoftAndScalar'">
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0-preview.4.24267.6"/>
<PackageReference Include="Scalar.AspNetCore" Version="1.1.1"/>
</ItemGroup>

<ItemGroup Condition=" '$(OpenApiMode)' == 'Swashbuckle'">
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.2"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0"/>
</ItemGroup>

<ItemGroup Condition=" '$(UseLocallyBuiltPackage)' != ''">
<PackageReference Include="Vogen" Version="999.9.*"/>
</ItemGroup>

<ItemGroup Condition=" '$(UseLocallyBuiltPackage)' == ''">
<PackageReference Include="Vogen" Version="999.9.10219943"/>
</ItemGroup>


</Project>
5 changes: 4 additions & 1 deletion samples/WebApplication/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@
builder.Services.AddSwaggerGen(opt =>
{
// the following extension method is available if you specify `GenerateSwashbuckleMappingExtensionMethod` - as shown above
opt.MapVogenTypes();

//opt.MapVogenTypes();
WebApplication.VogenSwashbuckleExtensions.MapVogenTypes(opt);
WebApplication.Shared.VogenSwashbuckleExtensions.MapVogenTypes(opt);

// the following schema filter is generated if you specify GenerateSwashbuckleSchemaFilter as shown above
// opt.SchemaFilter<MyVogenSchemaFilter>();
Expand Down
4 changes: 4 additions & 0 deletions samples/WebApplication/WebApplication.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,9 @@
<PackageReference Include="Vogen" Version="999.9.10219943"/>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\WebApplication.Shared\WebApplication.Shared.csproj" />
</ItemGroup>


</Project>
90 changes: 64 additions & 26 deletions src/Vogen/GenerateCodeForOpenApiSchemaCustomization.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;

Expand All @@ -9,39 +10,46 @@ internal class GenerateCodeForOpenApiSchemaCustomization
public static void WriteIfNeeded(VogenConfiguration? globalConfig,
SourceProductionContext context,
List<VoWorkItem> workItems,
VogenKnownSymbols knownSymbols)
VogenKnownSymbols knownSymbols,
Compilation compilation)
{
var c = globalConfig?.OpenApiSchemaCustomizations ?? VogenConfiguration.DefaultInstance.OpenApiSchemaCustomizations;

var fullNamespace = compilation.Assembly.Name;

var theNamespace = string.IsNullOrEmpty(fullNamespace) ? string.Empty : $"namespace {fullNamespace};";

if (c.HasFlag(OpenApiSchemaCustomizations.GenerateSwashbuckleSchemaFilter))
{
WriteSchemaFilter(context, knownSymbols);
WriteSchemaFilter(context, knownSymbols, theNamespace);
}

if (c.HasFlag(OpenApiSchemaCustomizations.GenerateSwashbuckleMappingExtensionMethod))
{
WriteExtensionMethodMapping(context, workItems, knownSymbols);
WriteExtensionMethodMapping(context, workItems, knownSymbols, theNamespace);
}
}

private static void WriteSchemaFilter(SourceProductionContext context, VogenKnownSymbols knownSymbols)
private static void WriteSchemaFilter(SourceProductionContext context, VogenKnownSymbols knownSymbols, string theNamespace)
{
if (!IsSwashbuckleReferenced(knownSymbols))
{
return;
}

string source =
$$"""
{{GeneratedCodeSegments.Preamble}}
{{theNamespace}}
using System.Reflection;
public class VogenSchemaFilter : global::Swashbuckle.AspNetCore.SwaggerGen.ISchemaFilter
{
private const BindingFlags _flags = BindingFlags.Public | BindingFlags.Instance;
public void Apply(global::Microsoft.OpenApi.Models.OpenApiSchema schema, global::Swashbuckle.AspNetCore.SwaggerGen.SchemaFilterContext context)
{
if (context.Type.GetCustomAttribute<Vogen.ValueObjectAttribute>() is not { } attribute)
Expand Down Expand Up @@ -98,18 +106,21 @@ private static void TryCopyPublicProperties<T>(T oldObject, T newObject) where T

private static void WriteExtensionMethodMapping(SourceProductionContext context,
List<VoWorkItem> workItems,
VogenKnownSymbols knownSymbols)
VogenKnownSymbols knownSymbols,
string theNamespace)
{
if (!IsSwashbuckleReferenced(knownSymbols))
{
return;
}

string source =
$$"""
{{GeneratedCodeSegments.Preamble}}
{{theNamespace}}
public static class VogenSwashbuckleExtensions
{
public static global::Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenOptions MapVogenTypes(this global::Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenOptions o)
Expand All @@ -128,40 +139,67 @@ public static class VogenSwashbuckleExtensions

private static string MapWorkItems(List<VoWorkItem> workItems)
{
var workItemCode = new StringBuilder();
var sb = new StringBuilder();

// map everything an non-nullable
MapWorkItems(workItems, sb, false);

// map value types again as nullable, see https://github.com/SteveDunn/Vogen/issues/693
var valueTypes = workItems.Where(i => i.IsTheWrapperAValueType);
MapWorkItems(valueTypes, sb, true);

return sb.ToString();
}

private static string MapWorkItems(IEnumerable<VoWorkItem> workItems, StringBuilder sb, bool nullable)
{
foreach (var workItem in workItems)
{
string voTypeName = workItem.VoTypeName;

var fqn = string.IsNullOrEmpty(workItem.FullNamespace)
? $"{workItem.VoTypeName}"
: $"{workItem.FullNamespace}.{workItem.VoTypeName}";
? $"{voTypeName}"
: $"{workItem.FullNamespace}.{voTypeName}";

if (nullable)
{
fqn = $"global::System.Nullable<{fqn}>";
}

TypeAndFormat typeAndPossibleFormat = MapUnderlyingTypeToJsonSchema(workItem);
string typeText = $"Type = \"{typeAndPossibleFormat.Type}\"";
string formatText = typeAndPossibleFormat.Format.Length == 0 ? "" : $", Format = \"{typeAndPossibleFormat.Format}\"";

workItemCode.AppendLine(
$$"""global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<{{fqn}}>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { {{typeText}}{{formatText}} });""");
string nullableText = $", Nullable = {nullable.ToString().ToLower()}";

sb.AppendLine(
$$"""global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<{{fqn}}>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { {{typeText}}{{formatText}}{{nullableText}} });""");
}

return workItemCode.ToString();
return sb.ToString();
}

record struct TypeAndFormat(string Type, string Format = "");
private record struct TypeAndFormat(string Type, string Format);

// see https://spec.openapis.org/oas/v3.0.0.html#data-types
private static TypeAndFormat MapUnderlyingTypeToJsonSchema(VoWorkItem workItem)
{
var primitiveType = workItem.UnderlyingTypeFullName;

TypeAndFormat jsonType = primitiveType switch
{
"System.Int32" => new("integer"),
"System.Single" => new("number"),
"System.Decimal" =>new( "number"),
"System.Double" => new("number"),
"System.String" => new("string"),
"System.Boolean" =>new( "boolean"),
"System.Guid" =>new( "string", "uuid"),
_ => new(TryMapComplexPrimitive(workItem))
"System.Int32" => new("integer", "int32"),
"System.Int64" => new("integer", "int64"),
"System.Single" => new("number", ""),
"System.Decimal" => new("number", "double"),
"System.Double" => new("number", "double"),
"System.String" => new("string", ""),
"System.Boolean" => new("boolean", ""),
"System.DateOnly" => new("string", "date"),
"System.DateTime" => new("string", "date-time"),
"System.DateTimeOffset" => new("string", "date-time"),
"System.Guid" => new("string", "uuid"),
"System.Byte" => new("string", "byte"),
_ => new(TryMapComplexPrimitive(workItem), "")
};

return jsonType;
Expand Down
2 changes: 1 addition & 1 deletion src/Vogen/ValueObjectGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ private static void Execute(
// get all the ValueObject types found.
List<VoWorkItem> workItems = GetWorkItems(targets, spc, globalConfig, csharpCompilation.LanguageVersion, vogenKnownSymbols, compilation).ToList();

GenerateCodeForOpenApiSchemaCustomization.WriteIfNeeded(globalConfig, spc, workItems, vogenKnownSymbols);
GenerateCodeForOpenApiSchemaCustomization.WriteIfNeeded(globalConfig, spc, workItems, vogenKnownSymbols, compilation);

GenerateCodeForEfCoreSpecs.WriteIfNeeded(spc, compilation, efCoreConverterSpecs);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@
// Suppress warnings about CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member'
#pragma warning disable CS1591

namespace generator;

public static class VogenSwashbuckleExtensions
{
public static global::Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenOptions MapVogenTypes(this global::Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenOptions o)
{
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<MyLongNamespace.Vo>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "integer" });
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<MyLongNamespace.Vo>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "integer", Format = "int32", Nullable = false });
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<global::System.Nullable<MyLongNamespace.Vo>>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "integer", Format = "int32", Nullable = true });


return o;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@
// Suppress warnings about CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member'
#pragma warning disable CS1591

namespace generator;

public static class VogenSwashbuckleExtensions
{
public static global::Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenOptions MapVogenTypes(this global::Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenOptions o)
{
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<MyLongNamespace.Vo>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "string", Format = "uuid" });
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<MyLongNamespace.Vo>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "string", Format = "uuid", Nullable = false });
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<global::System.Nullable<MyLongNamespace.Vo>>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "string", Format = "uuid", Nullable = true });


return o;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#pragma warning disable CS8669, CS8632
// Suppress warnings about CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member'
#pragma warning disable CS1591
namespace generator;
using System.Reflection;

public class VogenSchemaFilter : global::Swashbuckle.AspNetCore.SwaggerGen.ISchemaFilter
Expand Down Expand Up @@ -91,11 +92,14 @@ public class VogenSchemaFilter : global::Swashbuckle.AspNetCore.SwaggerGen.ISche
// Suppress warnings about CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member'
#pragma warning disable CS1591

namespace generator;

public static class VogenSwashbuckleExtensions
{
public static global::Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenOptions MapVogenTypes(this global::Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenOptions o)
{
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<MyLongNamespace.Vo>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "integer" });
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<MyLongNamespace.Vo>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "integer", Format = "int32", Nullable = false });
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<global::System.Nullable<MyLongNamespace.Vo>>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "integer", Format = "int32", Nullable = true });


return o;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#pragma warning disable CS8669, CS8632
// Suppress warnings about CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member'
#pragma warning disable CS1591
namespace generator;
using System.Reflection;

public class VogenSchemaFilter : global::Swashbuckle.AspNetCore.SwaggerGen.ISchemaFilter
Expand Down Expand Up @@ -91,16 +92,18 @@ public class VogenSchemaFilter : global::Swashbuckle.AspNetCore.SwaggerGen.ISche
// Suppress warnings about CS1591: Missing XML comment for publicly visible type or member 'Type_or_Member'
#pragma warning disable CS1591

namespace generator;

public static class VogenSwashbuckleExtensions
{
public static global::Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenOptions MapVogenTypes(this global::Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenOptions o)
{
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<MyVoInt>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "integer" });
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<MyVoFloat>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "number" });
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<MyVoDecimal>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "number" });
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<MyVoDouble>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "number" });
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<MyVoString>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "string" });
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<MyVoBool>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "boolean" });
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<MyVoInt>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "integer", Format = "int32", Nullable = false });
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<MyVoFloat>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "number", Nullable = false });
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<MyVoDecimal>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "number", Format = "double", Nullable = false });
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<MyVoDouble>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "number", Format = "double", Nullable = false });
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<MyVoString>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "string", Nullable = false });
global::Microsoft.Extensions.DependencyInjection.SwaggerGenOptionsExtensions.MapType<MyVoBool>(o, () => new global::Microsoft.OpenApi.Models.OpenApiSchema { Type = "boolean", Nullable = false });


return o;
Expand Down
Loading

0 comments on commit 9aaaea1

Please sign in to comment.