Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trying to fix the output SwaggerGen types generically. #3

Merged
merged 42 commits into from
Feb 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
c101d3a
Trying to fix the output SwaggerGen types generically.
harvzor Jul 25, 2023
aecf1e5
Extended model to have Optional object.
harvzor Jul 25, 2023
7d2de35
Tried generating maps using reflection but it doesn't seem to work wi…
harvzor Jul 25, 2023
8cdb47c
Revert "Tried generating maps using reflection but it doesn't seem to…
harvzor Jul 25, 2023
28e8702
This appears to work but I think it's too hacky.
harvzor Jul 28, 2023
d2106cd
This method seems to work with less bugs maybe, but I need to generic…
harvzor Jul 30, 2023
cf4f958
Ensures that if a type is added twice, it won't error on the second try.
harvzor Jul 30, 2023
6d2c210
Thought I had a solution but searching through the assembly doesn't f…
harvzor Jul 30, 2023
55fb89e
Moved Swashbuckle stuff to own project. Honestly, the mapping code se…
harvzor Jul 31, 2023
ccf928e
Reasonably happy with implementation. Need to fix some todos.
harvzor Jul 31, 2023
e645976
More readme fixes.
harvzor Jul 31, 2023
cff4037
Added support for single level arrays.
harvzor Jul 31, 2023
58fe7a1
Better docs.
harvzor Jul 31, 2023
13a1d6a
Implement support for arrays of arrays.
harvzor Jul 31, 2023
b135fac
Fix publishing Swashbuckle project.
harvzor Aug 1, 2023
80009ac
Type walker no longer looks through all types but only types used in …
harvzor Aug 2, 2023
47dc0a3
Added return type of custom methods too.
harvzor Aug 7, 2023
061bed3
Also check [ProducesResponseType] property.
harvzor Aug 7, 2023
7b12238
Added another way to map types.
harvzor Aug 7, 2023
99abe48
Fix issue with property walker walking through non-optional types and…
harvzor Aug 7, 2023
c9356d1
Simpler way to avoid throwing.
harvzor Aug 7, 2023
2d837a1
Merge branch 'master' into fix-swagger-gen
harvzor Aug 7, 2023
0b1b723
Merge branch 'master' into fix-swagger-gen
harvzor Aug 9, 2023
523e34f
Merge branch 'master' into fix-swagger-gen
harvzor Aug 9, 2023
f7976a2
Started adding tests.
harvzor Aug 10, 2023
1d3fd3c
Allowed injecting of specific controller. Cleaned up tests. Found bug…
harvzor Aug 16, 2023
2c205a1
Switch to dynamically created controllers.
harvzor Aug 17, 2023
70fa282
More tests.
harvzor Aug 17, 2023
bbfa269
Test that `[ProducesResponseType]` attribute is used.
harvzor Aug 17, 2023
817251f
Check that arrays and nested arrays work.
harvzor Aug 17, 2023
561950d
Check exception is thrown when now assembly is provided.
harvzor Sep 4, 2023
d2c00c6
Added object mapping test.
harvzor Sep 4, 2023
75a253f
Fix an issue with finding types on controller where the type is defin…
harvzor Sep 6, 2023
b94ef51
Tried adding extra test for nullable objects but it just doesn't work.
harvzor Sep 6, 2023
119f190
Realised that only structs will be generically Nullable<T>.
harvzor Sep 6, 2023
ac3478b
Remove unused param.
harvzor Sep 6, 2023
d89cb3a
Fix docs.
harvzor Sep 6, 2023
1f3daeb
Better format for known caveats
harvzor Feb 23, 2024
57ac087
Make sure to support all default types that Swashbuckle does.
harvzor Feb 23, 2024
e2ace96
Simplify example.
harvzor Feb 23, 2024
3d4da13
Better examples.
harvzor Feb 23, 2024
34ef523
Remove todo as this is indirectly tested.
harvzor Feb 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Binary file added .github/docs/broken-swagger-docs.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/docs/fixed-swagger-docs.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions Harvzor.Optional.sln
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Harvzor.Optional.SystemText
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Harvzor.Optional.JsonConverter.BaseTests", "tests\Harvzor.Optional.JsonConverter.BaseTests\Harvzor.Optional.JsonConverter.BaseTests.csproj", "{4257F779-067E-4EF8-9B56-DACCBA996357}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Harvzor.Optional.Swashbuckle", "src\Harvzor.Optional.Swashbuckle\Harvzor.Optional.Swashbuckle.csproj", "{3BFA9880-A29C-444D-9F94-633FE0901CFE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Harvzor.Optional.Swashbuckle.Tests", "tests\Harvzor.Optional.Swashbuckle.Tests\Harvzor.Optional.Swashbuckle.Tests.csproj", "{171F8BB6-1167-4553-9E4B-BEC33E75DC4F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -84,6 +88,14 @@ Global
{4257F779-067E-4EF8-9B56-DACCBA996357}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4257F779-067E-4EF8-9B56-DACCBA996357}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4257F779-067E-4EF8-9B56-DACCBA996357}.Release|Any CPU.Build.0 = Release|Any CPU
{3BFA9880-A29C-444D-9F94-633FE0901CFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3BFA9880-A29C-444D-9F94-633FE0901CFE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3BFA9880-A29C-444D-9F94-633FE0901CFE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3BFA9880-A29C-444D-9F94-633FE0901CFE}.Release|Any CPU.Build.0 = Release|Any CPU
{171F8BB6-1167-4553-9E4B-BEC33E75DC4F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{171F8BB6-1167-4553-9E4B-BEC33E75DC4F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{171F8BB6-1167-4553-9E4B-BEC33E75DC4F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{171F8BB6-1167-4553-9E4B-BEC33E75DC4F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{9ED9BB37-3250-4DF0-8224-FE86BAA2859A} = {0DC0E672-7784-4D81-8A18-C415472AC595}
Expand All @@ -98,5 +110,7 @@ Global
{22F65313-6A49-408E-8603-DE267698FCC2} = {A8A8C7B4-D30A-49F8-8F0F-D2CDB88EDE53}
{990ADC1B-BAAB-4170-B17F-BBED94FDD6DB} = {A8A8C7B4-D30A-49F8-8F0F-D2CDB88EDE53}
{4257F779-067E-4EF8-9B56-DACCBA996357} = {A8A8C7B4-D30A-49F8-8F0F-D2CDB88EDE53}
{3BFA9880-A29C-444D-9F94-633FE0901CFE} = {0DC0E672-7784-4D81-8A18-C415472AC595}
{171F8BB6-1167-4553-9E4B-BEC33E75DC4F} = {A8A8C7B4-D30A-49F8-8F0F-D2CDB88EDE53}
EndGlobalSection
EndGlobal
148 changes: 142 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
| Harvzor.Optional | [![NuGet](https://img.shields.io/nuget/v/Harvzor.Optional)](https://www.nuget.org/packages/Harvzor.Optional/) |
| Harvzor.Optional.SystemTextJson | [![NuGet](https://img.shields.io/nuget/v/Harvzor.Optional.SystemTextJson)](https://www.nuget.org/packages/Harvzor.Optional.SystemTextJson/) |
| Harvzor.Optional.NewtonsoftJson | [![NuGet](https://img.shields.io/nuget/v/Harvzor.Optional.NewtonsoftJson)](https://www.nuget.org/packages/Harvzor.Optional.NewtonsoftJson/) |
| Harvzor.Optional.Swashbuckle | [![NuGet](https://img.shields.io/nuget/v/Harvzor.Optional.Swashbuckle)](https://www.nuget.org/packages/Harvzor.Optional.Swashbuckle/) |

## The problem

Expand Down Expand Up @@ -135,9 +136,97 @@ services
});
```

#### Swagger support
### Swagger support

If you're using Swashbuckle Swagger, you'll also need to tell it how your types should look:
#### Harvzor.Optional.Swashbuckle

> **Warning**
> This package is experimental.

Swashbuckle SwaggerGen doesn't know how to handle `Optional<T>` and will attempt to generate complicated objects to express all the properties, for a simple class like:

```csharp
public class Foo
{
public Optional<int> OptionalInt { get; set; }
}
```

This ends up being generated like:

![broken-swagger-docs.png](.github/docs/broken-swagger-docs.png)

Instead we want SwaggerGen to treat `Optional<T>` as the generic type `T`. To handle doing this, add this:

```csharp
using Harvzor.Optional.Swashbuckle;

services
.AddSwaggerGen(options =>
{
// The assembly you pass in should include your controllers and perhaps even your DTOs.
options.FixOptionalMappings(Assembly.GetExecutingAssembly());
});
```

This results in the correct OpenAPI spec:

![fixed-swagger-docs.png](.github/docs/fixed-swagger-docs.png)

This will:

- ensure that all basic types like `Optional<string>` are mapped to the `string` type in the OpenAPI schema
- try to treat complex objects such as `Optional<MyType>` as the underlying generic type `MyType`

This doesn't work in all cases though, for example, with `Optional<Version>`, we want it to be treated as a `string` type and not as a `Version`, so this must be added:

```csharp
using Harvzor.Optional.Swashbuckle;

// Add your custom mappings first:
options.MapType<Optional<Version>>(() => new OpenApiSchema()
{
Type = "string"
});

options.FixOptionalMappings(Assembly.GetExecutingAssembly());
```

Alternatively, if you don't want to call `FixOptionalMappings(params Assembly[] assemblies)` which automagically finds any references to `Optional<T>` in your assembly, you can just directly feed it `Optional<T>` types that you know are used in your controllers:

```csharp
using Harvzor.Optional.Swashbuckle;

options
.FixOptionalMappingForType<Optional<Foo>>()
.FixOptionalMappingForType<Optional<Bar>>()
.FixOptionalMappingForType<Optional<int>>();
```

##### Known caveats

- `FixOptionalMappings(params Assembly[] assemblies)` does not work with minimal APIs as it searches for `Optional<T>` references on parameters and properties of any classes that implement controller methods, and then maps those `Optional<T>` types to their generic type `T`
- `Optional<T>` doesn't work with query parameters
- You can't have the following:
```csharp
[HttpGet]
public string Get([FromQuery] Optional<string> foo)
{
return foo;
}
```
- This is because of the following issue: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2226

##### Improvements

This package could be improved if these issues are ever resolved:

- https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1810
- https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2359

#### Manual Swagger support

If you're using `Swashbuckle.AspNetCore.SwaggerGen` but don't want to use `Harvzor.Optional.Swashbuckle`, you can also manually tell it how your types should look. Here are some basic types mapped:

```csharp
services.AddSwaggerGen(options =>
Expand All @@ -149,30 +238,75 @@ services.AddSwaggerGen(options =>

options.MapType<Optional<int>>(() => new OpenApiSchema
{
Type = "integer"
Type = "integer",
Format = "int32"
});

options.MapType<Optional<float>>(() => new OpenApiSchema
{
Type = "number"
Type = "number",
Format = "float"
});

options.MapType<Optional<double>>(() => new OpenApiSchema
{
Type = "number"
Type = "number",
Format = "double"
});

options.MapType<Optional<bool>>(() => new OpenApiSchema
{
Type = "boolean"
});

// todo: array, object?
options.MapType<Optional<DateTime>>(() => new OpenApiSchema
{
Type = "string",
Format = "date-time"
});

// IEnumerables:
options.MapType<Optional<IEnumerable<int>>>(() => new OpenApiSchema
{
Type = "array",
Items = new OpenApiSchema
{
Type = "integer",
Format = "int32"
}
});
});
```

You can see what basic types are available here: https://swagger.io/docs/specification/data-models/data-types/

However, handling custom objects such as `Optional<MyObject>` is quite complicated and not recommended, however, here's how to do it anyway:

```csharp
// Rewrite the mapping so it's an object reference:
options.MapType<Optional<MyObject>>(() => new OpenApiSchema
{
Type = "object",
Format = format,
Reference = new OpenApiReference
{
Id = nameof(MyObject),
Type = ReferenceType.Schema,
}
});

// Now add `MyObject` to the schema repisitory so the mapping actually points somewhere:
options.DocumentFilter<GenerateSchemaFor<MyObject>>();

private class GenerateSchemaFor<T> : IDocumentFilter where T : class
{
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
context.SchemaGenerator.GenerateSchema(typeof(T), context.SchemaRepository);
}
}
```

## Use case: JSON Merge PATCH

... need docs ...
Expand All @@ -198,3 +332,5 @@ docker-compose run --rm push-nuget --api-key {key}

- https://stackoverflow.com/questions/63418549/custom-json-serializer-for-optional-property-with-system-text-json
- https://stackoverflow.com/questions/12522000/optionally-serialize-a-property-based-on-its-runtime-value
- Optional in Swagger definition and how to handle generic types: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2359
- ISchemaGenerator: https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/2333#issuecomment-1035695675
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

<ItemGroup>
<ProjectReference Include="..\..\src\Harvzor.Optional.NewtonsoftJson\Harvzor.Optional.NewtonsoftJson.csproj" />
<ProjectReference Include="..\..\src\Harvzor.Optional.Swashbuckle\Harvzor.Optional.Swashbuckle.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
35 changes: 23 additions & 12 deletions examples/Harvzor.Optional.NewtonsoftJson.WebExample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
using System.Reflection;
using Harvzor.Optional;
using Harvzor.Optional.NewtonsoftJson;
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models;
using Harvzor.Optional.Swashbuckle;

var builder = WebApplication.CreateBuilder(args);

builder.Services
.AddEndpointsApiExplorer()
.AddSwaggerGen(options =>
{
options.MapType<Optional<string>>(() => new OpenApiSchema
{
Type = "string"
});
// Map types manually without Harvzor.Optional.Swashbuckle:
// options.MapType<Optional<string?>>(() => new OpenApiSchema
// {
// Type = "string"
// });

// Auto fixes mappings:
options.FixOptionalMappings(Assembly.GetExecutingAssembly());

// Alternatively, specify specific types that should be fixed:
// options
// .FixOptionalMappingForType<Optional<string>>();
})
.AddSwaggerGenNewtonsoftSupport();

Expand Down Expand Up @@ -50,16 +58,19 @@ public string Get()
}

[HttpPost]
public string Post(Foo foo)
public Foo Post(Foo foo)
{
if (foo.OptionalString.IsDefined)
return $"You're value is \"{foo.OptionalString.Value ?? "null"}\".";

return "You sent nothing.";
Console.WriteLine(
foo.OptionalString.IsDefined
? $"You sent: {(foo.OptionalString.Value == null ? "null" : $"\"{foo.OptionalString.Value}\"")}"
: "You sent nothing!"
);

return foo;
}
}

public record Foo
public class Foo
{
public Optional<string?> OptionalString { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Harvzor.Optional.Swashbuckle\Harvzor.Optional.Swashbuckle.csproj" />
<ProjectReference Include="..\..\src\Harvzor.Optional.SystemTextJson\Harvzor.Optional.SystemTextJson.csproj" />
</ItemGroup>

Expand Down