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

WIP: Better support for open generic when mapping types #2704

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from

Conversation

harvzor
Copy link

@harvzor harvzor commented Sep 8, 2023

Issues

This helps with these issues:

The problem

The issue I'm trying to solve is that there's no way to generically solve all mappings for a type that uses open generics. We can currently do something like this:

services.AddSwaggerGen(c =>
{
    c.MapType(typeof(GenericType<string>), () => new OpenApiSchema { Type = "string" });
    c.MapType(typeof(GenericType<int>), () => new OpenApiSchema { Type = "number" });
    // and handle every other type that GenericType could be...
}

But writing an exhaustive list is impossible, especially when you need to support custom objects.

The solution

Provide the user with a MappingContext so they can dynamically handle different types:

c.MapType(typeof(GenericType<>), mappingContext =>
{
    var type = mappingContext.UnderlyingType.GenericTypeArguments[0];

    return mappingContext.SchemaGenerator.GenerateSchema(type, mappingContext.SchemaRepository);
});

The MappingContext contains these useful properties:

public interface IMappingContext
{
    ISchemaGenerator SchemaGenerator { get; }
    SchemaRepository SchemaRepository { get; }
    Type UnderlyingType { get; }
}

@harvzor harvzor changed the title Better support for open generic when mapping types WIP: Better support for open generic when mapping types Sep 8, 2023
Comment on lines 198 to 196
/// <summary>
/// Provide a custom mapping, for a given type, to the Swagger-flavored JSONSchema
/// </summary>
/// <param name="swaggerGenOptions"></param>
/// <param name="type">System type</param>
/// <param name="schemaFactory">A factory method that generates Schema's for the provided type</param>
public static void MapType(
this SwaggerGenOptions swaggerGenOptions,
Type type,
Func<MappingContext, OpenApiSchema> schemaFactory)
{
swaggerGenOptions.SchemaGeneratorOptions.CustomTypeMappings.Add(type, schemaFactory);
}

/// <summary>
/// Provide a custom mapping, for a given type, to the Swagger-flavored JSONSchema
/// </summary>
/// <typeparam name="T">System type</typeparam>
/// <param name="swaggerGenOptions"></param>
/// <param name="schemaFactory">A factory method that generates Schema's for the provided type</param>
public static void MapType<T>(
this SwaggerGenOptions swaggerGenOptions,
Func<MappingContext, OpenApiSchema> schemaFactory)
{
swaggerGenOptions.MapType(typeof(T), schemaFactory);
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've kept the old MapType extension methods so that existing usages of this method won't become broken as I'm now providing the MappingContext.

However, if anyone is using swaggerGenOptions.SchemaGeneratorOptions.CustomTypeMappings directly instead of these helper methods, they will need to update their code.

Comment on lines 4 to 25
namespace Basic.Controllers
{
/// <summary>
/// Summary for GenericsController
/// </summary>
[Route("/generics")]
[Produces("application/json")]
public class GenericsController
{
[HttpPost(Name = "CreateString")]
public string CreateString([FromBody] GenericType<string> genericString)
{
throw new NotImplementedException();
}

[HttpPost(Name = "CreateDateTime")]
public string CreateDateTime([FromBody] GenericType<DateTime> genericObject)
{
throw new NotImplementedException();
}
}
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is the best place to put this

But it at least needs to be integration tested

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as a test would fail if this functionality got broken, it should be fine to live here. New projects are typically added when specific configurations are needed for the generation or for different types of apps.

throw new NotImplementedException();
}
}
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a test for more complex custom objects such as:

public class Foo
{
    public string Bar { get; set; }
}

@martincostello
Copy link
Collaborator

Thanks for contributing - if you'd like to continue with this pull request, please rebase against the default branch to pick up our new CI.

@harvzor harvzor reopened this May 17, 2024
@@ -839,6 +839,26 @@ services.AddSwaggerGen(c =>
};
```

As of version v6.6.x, you can easily map generic types dynamically by accessing the underlying type at runtime. In this example, I want to treat `GenericType` as if it was the generic type `T`:
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to make sure the version is correct when this is released

@@ -195,6 +195,33 @@ public static void AddServer(this SwaggerGenOptions swaggerGenOptions, OpenApiSe
swaggerGenOptions.MapType(typeof(T), schemaFactory);
}

/// <summary>
/// Provide a custom mapping, for a given type, to the Swagger-flavored JSONSchema
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless "JSONSchema" is a specific term I've not heard before:

Suggested change
/// Provide a custom mapping, for a given type, to the Swagger-flavored JSONSchema
/// Provides a custom mapping, for a given type, to the Swagger-flavored JSON schema

/// </summary>
/// <param name="swaggerGenOptions"></param>
/// <param name="type">System type</param>
/// <param name="schemaFactory">A factory method that generates Schema's for the provided type</param>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// <param name="schemaFactory">A factory method that generates Schema's for the provided type</param>
/// <param name="schemaFactory">A factory method that generates schemas for the provided type</param>

}

/// <summary>
/// Provide a custom mapping, for a given type, to the Swagger-flavored JSONSchema
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Provide a custom mapping, for a given type, to the Swagger-flavored JSONSchema
/// Provides a custom mapping, for a given type, to the Swagger-flavored JSON schema

SchemaIdSelector = DefaultSchemaIdSelector;
SubTypesSelector = DefaultSubTypesSelector;
DiscriminatorNameSelector = DefaultDiscriminatorNameSelector;
DiscriminatorValueSelector = DefaultDiscriminatorValueSelector;
SchemaFilters = new List<ISchemaFilter>();
}

public IDictionary<Type, Func<OpenApiSchema>> CustomTypeMappings { get; set; }
public IDictionary<Type, Func<MappingContext, OpenApiSchema>> CustomTypeMappings { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a binary-breaking change, so if we took it as-is this would have to be part of v7.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would get released sooner if there were a way to implement this without breaking changes.

UnderlyingType = underlyingType;
}

public readonly ISchemaGenerator SchemaGenerator;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use properties, not fields.


namespace Swashbuckle.AspNetCore.SwaggerGen
{
public class MappingContext
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be sealed.


namespace Swashbuckle.AspNetCore.SwaggerGen
{
public class MappingContext
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please /// document all the members as this is a new public-facing type.

if (genericTypeArgument == typeof(int))
return new OpenApiSchema { Type = "number" };

throw new NotImplementedException();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to fail with an assertion related to what unexpected value was received.

Comment on lines 4 to 25
namespace Basic.Controllers
{
/// <summary>
/// Summary for GenericsController
/// </summary>
[Route("/generics")]
[Produces("application/json")]
public class GenericsController
{
[HttpPost(Name = "CreateString")]
public string CreateString([FromBody] GenericType<string> genericString)
{
throw new NotImplementedException();
}

[HttpPost(Name = "CreateDateTime")]
public string CreateDateTime([FromBody] GenericType<DateTime> genericObject)
{
throw new NotImplementedException();
}
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As long as a test would fail if this functionality got broken, it should be fine to live here. New projects are typically added when specific configurations are needed for the generation or for different types of apps.

@@ -55,6 +56,13 @@ public void ConfigureServices(IServiceCollection services)
c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "Basic.xml"));

c.EnableAnnotations();

c.MapType(typeof(GenericType<>), mappingContext =>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test a generic type with more than one type parameter?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants