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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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


```csharp
// GenericType.cs
public class GenericType<T>
{
public T MyProperty { get; set; }
}

// Startup.cs
c.MapType(typeof(GenericType<>), mappingContext =>
{
// First, get the type of T:
var type = mappingContext.UnderlyingType.GenericTypeArguments[0];

// Now, autmatically generate a schema for T and return that:
return mappingContext.SchemaGenerator.GenerateSchema(type, mappingContext.SchemaRepository);
});
```

### Extend Generator with Operation, Schema & Document Filters ###

Swashbuckle exposes a filter pipeline that hooks into the generation process. Once generated, individual metadata objects are passed into the pipeline where they can be modified further. You can wire up custom filters to enrich the generated "Operations", "Schemas" and "Documents".
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public void Configure(SchemaGeneratorOptions options)

private void DeepCopy(SchemaGeneratorOptions source, SchemaGeneratorOptions target)
{
target.CustomTypeMappings = new Dictionary<Type, Func<OpenApiSchema>>(source.CustomTypeMappings);
target.CustomTypeMappings = new Dictionary<Type, Func<MappingContext, OpenApiSchema>>(source.CustomTypeMappings);
target.UseInlineDefinitionsForEnums = source.UseInlineDefinitionsForEnums;
target.SchemaIdSelector = source.SchemaIdSelector;
target.IgnoreObsoleteProperties = source.IgnoreObsoleteProperties;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ public static void AddServer(this SwaggerGenOptions swaggerGenOptions, OpenApiSe
Type type,
Func<OpenApiSchema> schemaFactory)
{
swaggerGenOptions.SchemaGeneratorOptions.CustomTypeMappings.Add(type, schemaFactory);
swaggerGenOptions.MapType(type, _ => schemaFactory());
}

/// <summary>
Expand All @@ -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>

public static void MapType(
this SwaggerGenOptions swaggerGenOptions,
Type type,
Func<IMappingContext, OpenApiSchema> schemaFactory)
{
swaggerGenOptions.SchemaGeneratorOptions.CustomTypeMappings.Add(type, 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.

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>
/// <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<IMappingContext, OpenApiSchema> schemaFactory)
{
swaggerGenOptions.MapType(typeof(T), schemaFactory);
}

/// <summary>
/// Generate inline schema definitions (as opposed to referencing a shared definition) for enum parameters and properties
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,9 +212,9 @@ private bool IsBaseTypeWithKnownTypesDefined(DataContract dataContract, out IEnu

private OpenApiSchema GenerateConcreteSchema(DataContract dataContract, SchemaRepository schemaRepository)
{
if (TryGetCustomTypeMapping(dataContract.UnderlyingType, out Func<OpenApiSchema> customSchemaFactory))
if (TryGetCustomTypeMapping(dataContract.UnderlyingType, out Func<MappingContext, OpenApiSchema> customSchemaFactory))
{
return customSchemaFactory();
return customSchemaFactory(new MappingContext(this, schemaRepository, dataContract.UnderlyingType));
}

if (dataContract.UnderlyingType.IsAssignableToOneOf(BinaryStringTypes))
Expand Down Expand Up @@ -271,7 +271,7 @@ private OpenApiSchema GenerateConcreteSchema(DataContract dataContract, SchemaRe
: schemaFactory();
}

private bool TryGetCustomTypeMapping(Type modelType, out Func<OpenApiSchema> schemaFactory)
private bool TryGetCustomTypeMapping(Type modelType, out Func<MappingContext, OpenApiSchema> schemaFactory)
{
return _generatorOptions.CustomTypeMappings.TryGetValue(modelType, out schemaFactory)
|| (modelType.IsConstructedGenericType && _generatorOptions.CustomTypeMappings.TryGetValue(modelType.GetGenericTypeDefinition(), out schemaFactory));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ public class SchemaGeneratorOptions
{
public SchemaGeneratorOptions()
{
CustomTypeMappings = new Dictionary<Type, Func<OpenApiSchema>>();
CustomTypeMappings = new Dictionary<Type, Func<MappingContext, OpenApiSchema>>();
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.


public bool UseInlineDefinitionsForEnums { get; set; }

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;

namespace Swashbuckle.AspNetCore.SwaggerGen;

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

/// <summary>
/// Actual runtime type that's being mapped.
/// </summary>
Type UnderlyingType { get;}
}

public class MappingContext(
ISchemaGenerator schemaGenerator,
SchemaRepository schemaRepository,
Type underlyingType)
: IMappingContext
{
public ISchemaGenerator SchemaGenerator { get; } = schemaGenerator;
public SchemaRepository SchemaRepository { get; } = schemaRepository;
public Type UnderlyingType { get; } = underlyingType;
}
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ public void GenerateSchema_DoesNotSetReadOnlyFlag_IfPropertyIsReadOnlyButCanBeSe
string expectedSchemaType)
{
var subject = Subject(
configureGenerator: c => c.CustomTypeMappings.Add(mappingType, () => new OpenApiSchema { Type = "string" })
configureGenerator: c => c.CustomTypeMappings.Add(mappingType, _ => new OpenApiSchema { Type = "string" })
);
var schema = subject.GenerateSchema(type, new SchemaRepository());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ public void GenerateSchema_SetsDefault_IfParameterHasDefaultValueAttribute()
string expectedSchemaType)
{
var subject = Subject(
configureGenerator: c => c.CustomTypeMappings.Add(mappingType, () => new OpenApiSchema { Type = "string" })
configureGenerator: c => c.CustomTypeMappings.Add(mappingType, _ => new OpenApiSchema { Type = "string" })
);
var schema = subject.GenerateSchema(type, new SchemaRepository());

Expand Down Expand Up @@ -979,6 +979,57 @@ public void GenerateSchema_GeneratesSchema_IfParameterHasTypeConstraints()
Assert.Equal("integer", schema.Type);
}

[Theory]
[InlineData(typeof(GenericType<string>), "string")]
[InlineData(typeof(GenericType<int>), "number")]
public void GenerateSchema_GeneratesSchemaWithCorrectType_IfTypeTakenFromUnderlyingType(Type genericType, string expectedType)
{
var subject = Subject(
configureGenerator: c => c.CustomTypeMappings.Add(genericType, mappingContext =>
{
Assert.Equal(genericType, mappingContext.UnderlyingType);
Assert.NotNull(mappingContext.SchemaGenerator);
Assert.NotNull(mappingContext.SchemaRepository);

var genericTypeArgument = mappingContext.UnderlyingType.GenericTypeArguments.First();

if (genericTypeArgument == typeof(string))
return new OpenApiSchema { Type = "string" };
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.

})
);
var schema = subject.GenerateSchema(genericType, new SchemaRepository());

Assert.Equal(expectedType, schema.Type);
Assert.Empty(schema.Properties);
}

[Fact]
public void GenerateSchema_GeneratesSchemaWithCorrectReference_IfSchemaIsGeneratedForGenericTypeArgument()
{
var genericType = typeof(GenericType<ComplexType>);

var subject = Subject(
configureGenerator: c => c.CustomTypeMappings.Add(genericType, mappingContext =>
{
Assert.Equal(genericType, mappingContext.UnderlyingType);
Assert.NotNull(mappingContext.SchemaGenerator);
Assert.NotNull(mappingContext.SchemaRepository);

var genericTypeArgument = mappingContext.UnderlyingType.GenericTypeArguments.First();

return mappingContext.SchemaGenerator.GenerateSchema(genericTypeArgument, mappingContext.SchemaRepository);
})
);
var schema = subject.GenerateSchema(genericType, new SchemaRepository());

Assert.Equal("#/components/schemas/ComplexType", schema.Reference.ReferenceV3);
Assert.Empty(schema.Properties);
}

private static SchemaGenerator Subject(
Action<SchemaGeneratorOptions> configureGenerator = null,
Action<JsonSerializerOptions> configureSerializer = null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
namespace Swashbuckle.AspNetCore.TestSupport
{
public class GenericType<T>
{
public T Property1 { get; set; }
}

public class GenericType<T,K>
{
public T Property1 { get; set; }
Expand Down
25 changes: 25 additions & 0 deletions test/WebSites/Basic/Controllers/GenericsController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using Microsoft.AspNetCore.Mvc;

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

[HttpPost("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.

Add a test for more complex custom objects such as:

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

7 changes: 7 additions & 0 deletions test/WebSites/Basic/GenericTypes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Basic
{
public class GenericType<T>
{
public T Property1 { get; set; }
}
}
8 changes: 8 additions & 0 deletions test/WebSites/Basic/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Microsoft.OpenApi.Models;
using Microsoft.AspNetCore.Localization;
using Basic.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace Basic
{
Expand Down Expand Up @@ -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?

{
var type = mappingContext.UnderlyingType.GenericTypeArguments[0];

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

Expand Down