From 9b5e48c0931d6a802c7eb826454e965627d973cc Mon Sep 17 00:00:00 2001 From: rmorris Date: Thu, 25 Jun 2020 00:51:19 +0100 Subject: [PATCH] Improve polymorphism & inheritance behavior incl. more flexible config --- .gitignore | 1 + Swashbuckle.AspNetCore.sln | 15 + .../AnnotationsSwaggerGenOptionsExtensions.cs | 8 +- .../NewtonsoftDataContractResolver.cs | 82 ++--- .../ConfigureSchemaGeneratorOptions.cs | 3 +- .../SwaggerGenOptionsExtensions.cs | 96 +++-- .../SchemaGenerator/IDataContractResolver.cs | 85 +++-- .../JsonSerializerDataContractResolver.cs | 120 ++++--- .../SchemaGenerator/SchemaGenerator.cs | 327 +++++++++++------- .../SchemaGenerator/SchemaGeneratorOptions.cs | 11 +- .../SchemaGenerator/TypeExtensions.cs | 68 +--- .../SwaggerGenerator/SchemaRepository.cs | 55 ++- .../NewtonsoftSerializerTesting.cs | 11 +- .../NewtonsoftSchemaGeneratorTests.cs | 139 +++++--- .../Fixtures/FakeController.cs | 13 +- .../JsonSerializerTesting.cs | 30 +- .../JsonSerializerSchemaGeneratorTests.cs | 164 +++++++-- .../SwaggerGenerator/SwaggerGeneratorTests.cs | 32 -- .../Fixtures/ComplexType2.cs | 11 + .../ComplexTypeWithRestrictedProperties.cs | 9 + .../Fixtures/DataAnnotatedType.cs | 3 + .../Fixtures/DataAnnotatedViaMetadataType.cs | 8 +- test/WebSites/CliExample/CliExample.csproj | 1 - .../Controllers/AnimalsController.cs | 42 +++ .../NswagClientExample.csproj | 30 ++ test/WebSites/NswagClientExample/Program.cs | 26 ++ .../Properties/launchSettings.json | 30 ++ test/WebSites/NswagClientExample/Startup.cs | 54 +++ .../appsettings.Development.json | 9 + .../NswagClientExample/appsettings.json | 10 + test/WebSites/NswagClientExample/swagger.json | 135 ++++++++ version.props | 2 +- 32 files changed, 1140 insertions(+), 490 deletions(-) create mode 100644 test/Swashbuckle.AspNetCore.TestSupport/Fixtures/ComplexType2.cs create mode 100644 test/Swashbuckle.AspNetCore.TestSupport/Fixtures/ComplexTypeWithRestrictedProperties.cs create mode 100644 test/WebSites/NswagClientExample/Controllers/AnimalsController.cs create mode 100644 test/WebSites/NswagClientExample/NswagClientExample.csproj create mode 100644 test/WebSites/NswagClientExample/Program.cs create mode 100644 test/WebSites/NswagClientExample/Properties/launchSettings.json create mode 100644 test/WebSites/NswagClientExample/Startup.cs create mode 100644 test/WebSites/NswagClientExample/appsettings.Development.json create mode 100644 test/WebSites/NswagClientExample/appsettings.json create mode 100644 test/WebSites/NswagClientExample/swagger.json diff --git a/.gitignore b/.gitignore index 2ecf6da87..6eed56493 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ artifacts/ Thumbs.db test/WebSites/CliExample/wwwroot/api-docs/v1/*.json test/WebSites/CliExampleWithFactory/wwwroot/api-docs/v1/*.json +test/WebSites/NswagClientExample/NSwagClient/ *ncrunch* \ No newline at end of file diff --git a/Swashbuckle.AspNetCore.sln b/Swashbuckle.AspNetCore.sln index 8cb1f0dbc..5e8dd2520 100644 --- a/Swashbuckle.AspNetCore.sln +++ b/Swashbuckle.AspNetCore.sln @@ -89,6 +89,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CliExampleWithFactory", "te EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Swashbuckle.AspNetCore.TestSupport", "test\Swashbuckle.AspNetCore.TestSupport\Swashbuckle.AspNetCore.TestSupport.csproj", "{6E3C0128-931F-4438-AEDD-984E0E2B2C7B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NSwagClientExample", "test\WebSites\NswagClientExample\NSwagClientExample.csproj", "{9840E751-5845-431C-943B-C75CAE596A45}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -459,6 +461,18 @@ Global {6E3C0128-931F-4438-AEDD-984E0E2B2C7B}.Release|x64.Build.0 = Release|Any CPU {6E3C0128-931F-4438-AEDD-984E0E2B2C7B}.Release|x86.ActiveCfg = Release|Any CPU {6E3C0128-931F-4438-AEDD-984E0E2B2C7B}.Release|x86.Build.0 = Release|Any CPU + {9840E751-5845-431C-943B-C75CAE596A45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9840E751-5845-431C-943B-C75CAE596A45}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9840E751-5845-431C-943B-C75CAE596A45}.Debug|x64.ActiveCfg = Debug|Any CPU + {9840E751-5845-431C-943B-C75CAE596A45}.Debug|x64.Build.0 = Debug|Any CPU + {9840E751-5845-431C-943B-C75CAE596A45}.Debug|x86.ActiveCfg = Debug|Any CPU + {9840E751-5845-431C-943B-C75CAE596A45}.Debug|x86.Build.0 = Debug|Any CPU + {9840E751-5845-431C-943B-C75CAE596A45}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9840E751-5845-431C-943B-C75CAE596A45}.Release|Any CPU.Build.0 = Release|Any CPU + {9840E751-5845-431C-943B-C75CAE596A45}.Release|x64.ActiveCfg = Release|Any CPU + {9840E751-5845-431C-943B-C75CAE596A45}.Release|x64.Build.0 = Release|Any CPU + {9840E751-5845-431C-943B-C75CAE596A45}.Release|x86.ActiveCfg = Release|Any CPU + {9840E751-5845-431C-943B-C75CAE596A45}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -495,6 +509,7 @@ Global {605AA0D3-2AA7-46B4-811B-85DC1B1A3901} = {1669F896-133C-4996-B58C-E7CDA299ADFF} {322813C4-0458-4F98-965D-568AD2C819CE} = {245144DE-BC89-4822-B044-020458BFECC0} {6E3C0128-931F-4438-AEDD-984E0E2B2C7B} = {1669F896-133C-4996-B58C-E7CDA299ADFF} + {9840E751-5845-431C-943B-C75CAE596A45} = {245144DE-BC89-4822-B044-020458BFECC0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {36FC6A67-247D-4149-8EDD-79FFD1A75F51} diff --git a/src/Swashbuckle.AspNetCore.Annotations/AnnotationsSwaggerGenOptionsExtensions.cs b/src/Swashbuckle.AspNetCore.Annotations/AnnotationsSwaggerGenOptionsExtensions.cs index 8d93169cc..6c1cc8cfb 100644 --- a/src/Swashbuckle.AspNetCore.Annotations/AnnotationsSwaggerGenOptionsExtensions.cs +++ b/src/Swashbuckle.AspNetCore.Annotations/AnnotationsSwaggerGenOptionsExtensions.cs @@ -22,7 +22,11 @@ public static void EnableAnnotations(this SwaggerGenOptions options, bool enable options.DocumentFilter(); if (enableSubTypeAnnotations) - options.GeneratePolymorphicSchemas(AnnotationsSubTypeResolver, AnnotationsDiscriminatorSelector); + { + options.UseOneOfForPolymorphism(AnnotationsDiscriminatorSelector); + options.DetectSubTypesUsing(AnnotationsSubTypeResolver); + options.UseAllOfForInheritance(); + } } private static IEnumerable AnnotationsSubTypeResolver(Type type) @@ -42,7 +46,7 @@ private static string AnnotationsDiscriminatorSelector(Type type) { return type.GetCustomAttributes(false) .OfType() - .FirstOrDefault()?.Discriminator ?? "$type"; + .FirstOrDefault()?.Discriminator; } } } diff --git a/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs b/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs index a524a9a3d..3f6edb776 100644 --- a/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs +++ b/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs @@ -26,7 +26,12 @@ public NewtonsoftDataContractResolver(SchemaGeneratorOptions generatorOptions, J public DataContract GetDataContractForType(Type type) { - var jsonContract = _contractResolver.ResolveContract(type.IsNullable(out Type innerType) ? innerType : type); + if (type.IsAssignableTo(typeof(JToken))) + { + return DataContract.ForDynamic(underlyingType: type); + } + + var jsonContract = _contractResolver.ResolveContract(type); if (jsonContract is JsonPrimitiveContract && !jsonContract.UnderlyingType.IsEnum) { @@ -34,10 +39,10 @@ public DataContract GetDataContractForType(Type type) ? PrimitiveTypesAndFormats[jsonContract.UnderlyingType] : Tuple.Create(DataType.String, (string)null); - return new DataContract( + return DataContract.ForPrimitive( + underlyingType: jsonContract.UnderlyingType, dataType: primitiveTypeAndFormat.Item1, - format: primitiveTypeAndFormat.Item2, - underlyingType: jsonContract.UnderlyingType); + dataFormat: primitiveTypeAndFormat.Item2); } if (jsonContract is JsonPrimitiveContract && jsonContract.UnderlyingType.IsEnum) @@ -48,74 +53,52 @@ public DataContract GetDataContractForType(Type type) ? PrimitiveTypesAndFormats[typeof(string)] : PrimitiveTypesAndFormats[jsonContract.UnderlyingType.GetEnumUnderlyingType()]; - return new DataContract( - dataType: primitiveTypeAndFormat.Item1, - format: primitiveTypeAndFormat.Item2, + return DataContract.ForPrimitive( underlyingType: jsonContract.UnderlyingType, + dataType: primitiveTypeAndFormat.Item1, + dataFormat: primitiveTypeAndFormat.Item2, enumValues: enumValues); } + if (jsonContract is JsonArrayContract jsonArrayContract) + { + return DataContract.ForArray( + underlyingType: jsonArrayContract.UnderlyingType, + itemType: jsonArrayContract.CollectionItemType ?? typeof(object)); + } + if (jsonContract is JsonDictionaryContract jsonDictionaryContract) { var keyType = jsonDictionaryContract.DictionaryKeyType ?? typeof(object); var valueType = jsonDictionaryContract.DictionaryValueType ?? typeof(object); + IEnumerable keys = null; + if (keyType.IsEnum) { - // This is a special case where we can include named properties based on the enum values - var enumValues = GetDataContractForType(keyType).EnumValues; + // This is a special case where we know the possible key values + var enumValues = GetSerializedEnumValuesFor(_contractResolver.ResolveContract(keyType)); - var propertyNames = enumValues.Any(value => value is string) + keys = enumValues.Any(value => value is string) ? enumValues.Cast() : keyType.GetEnumNames(); - - return new DataContract( - dataType: DataType.Object, - underlyingType: jsonDictionaryContract.UnderlyingType, - properties: propertyNames.Select(name => new DataProperty(name, valueType))); } - return new DataContract( - dataType: DataType.Object, + return DataContract.ForDictionary( underlyingType: jsonDictionaryContract.UnderlyingType, - additionalPropertiesType: valueType); - } - - if (jsonContract is JsonArrayContract jsonArrayContract) - { - return new DataContract( - dataType: DataType.Array, - underlyingType: jsonArrayContract.UnderlyingType, - arrayItemType: jsonArrayContract.CollectionItemType ?? typeof(object)); + valueType: valueType, + keys: keys); } if (jsonContract is JsonObjectContract jsonObjectContract) { - return new DataContract( - dataType: DataType.Object, + return DataContract.ForObject( underlyingType: jsonObjectContract.UnderlyingType, properties: GetDataPropertiesFor(jsonObjectContract), - additionalPropertiesType: jsonObjectContract.ExtensionDataValueType); - } - - if (jsonContract.UnderlyingType == typeof(JArray)) - { - return new DataContract( - dataType: DataType.Array, - underlyingType: jsonContract.UnderlyingType, - arrayItemType: typeof(JToken)); - } - - if (jsonContract.UnderlyingType == typeof(JObject)) - { - return new DataContract( - dataType: DataType.Object, - underlyingType: jsonContract.UnderlyingType); + extensionDataType: jsonObjectContract.ExtensionDataValueType); } - return new DataContract( - dataType: DataType.Unknown, - underlyingType: jsonContract.UnderlyingType); + return DataContract.ForDynamic(underlyingType: type); } private IEnumerable GetSerializedEnumValuesFor(JsonContract jsonContract) @@ -165,11 +148,6 @@ private string GetConvertedEnumName(string enumName, bool hasSpecifiedName, Stri private IEnumerable GetDataPropertiesFor(JsonObjectContract jsonObjectContract) { - if (jsonObjectContract.UnderlyingType == typeof(object)) - { - return null; - } - var dataProperties = new List(); foreach (var jsonProperty in jsonObjectContract.Properties) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSchemaGeneratorOptions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSchemaGeneratorOptions.cs index 27f5d87d4..1a8d28ce3 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSchemaGeneratorOptions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/ConfigureSchemaGeneratorOptions.cs @@ -33,7 +33,8 @@ private void DeepCopy(SchemaGeneratorOptions source, SchemaGeneratorOptions targ target.CustomTypeMappings = new Dictionary>(source.CustomTypeMappings); target.SchemaIdSelector = source.SchemaIdSelector; target.IgnoreObsoleteProperties = source.IgnoreObsoleteProperties; - target.GeneratePolymorphicSchemas = source.GeneratePolymorphicSchemas; + target.UseOneOfForPolymorphism = source.UseOneOfForPolymorphism; + target.UseAllOfForInheritance = source.UseAllOfForInheritance; target.SubTypesResolver = source.SubTypesResolver; target.DiscriminatorSelector = source.DiscriminatorSelector; target.UseAllOfToExtendReferenceSchemas = source.UseAllOfToExtendReferenceSchemas; diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs index dca596046..66c5e886b 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenOptionsExtensions.cs @@ -57,18 +57,6 @@ public static void IgnoreObsoleteActions(this SwaggerGenOptions swaggerGenOption swaggerGenOptions.SwaggerGeneratorOptions.ConflictingActionsResolver = resolver; } - [Obsolete("If the serializer is configured for string enums (e.g. StringEnumConverter) Swashbuckle will reflect that automatically")] - public static void DescribeAllEnumsAsStrings(this SwaggerGenOptions swaggerGenOptions) - { - swaggerGenOptions.SchemaGeneratorOptions.DescribeAllEnumsAsStrings = true; - } - - [Obsolete("If the serializer is configured for (camel-cased) string enums (e.g. StringEnumConverter) Swashbuckle will reflect that automatically")] - public static void DescribeStringEnumsInCamelCase(this SwaggerGenOptions swaggerGenOptions) - { - swaggerGenOptions.SchemaGeneratorOptions.DescribeStringEnumsInCamelCase = true; - } - /// /// Provide a custom strategy for assigning "operationId" to operations /// @@ -79,6 +67,7 @@ public static void DescribeStringEnumsInCamelCase(this SwaggerGenOptions swagger swaggerGenOptions.SwaggerGeneratorOptions.OperationIdSelector = operationIdSelector; } + /// /// Provide a custom strategy for assigning a default "tag" to operations /// @@ -216,41 +205,62 @@ public static void IgnoreObsoleteProperties(this SwaggerGenOptions swaggerGenOpt } /// - /// Generate polymorphic schemas (i.e. "oneOf") based on discovered subtypes + /// Generate inline schema definitions (as opposed to referencing a shared definition) for enum parameters and properties + /// + /// + public static void UseInlineDefinitionsForEnums(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SchemaGeneratorOptions.UseInlineDefinitionsForEnums = true; + } + + /// + /// Extend reference schemas (using the allOf construct) so that contextual metadata can be applied to all parameter and property schemas + /// + /// + public static void UseAllOfToExtendReferenceSchemas(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SchemaGeneratorOptions.UseAllOfToExtendReferenceSchemas = true; + } + + /// + /// Enables polymorphic schema generation. If enabled, request and response schemas will contain the oneOf + /// construct to describe sub types as a set of alternative schemas. /// /// - /// /// - public static void GeneratePolymorphicSchemas( + public static void UseOneOfForPolymorphism( this SwaggerGenOptions swaggerGenOptions, - Func> subTypesResolver = null, Func discriminatorSelector = null) { - swaggerGenOptions.SchemaGeneratorOptions.GeneratePolymorphicSchemas = true; - - if (subTypesResolver != null) - swaggerGenOptions.SchemaGeneratorOptions.SubTypesResolver = subTypesResolver; + swaggerGenOptions.SchemaGeneratorOptions.UseOneOfForPolymorphism = true; if (discriminatorSelector != null) swaggerGenOptions.SchemaGeneratorOptions.DiscriminatorSelector = discriminatorSelector; } /// - /// Extend reference schemas (using the allOf construct) so that contextual metadata can be applied to all parameter and property schemas + /// Enables composite schema generation. If enabled, sub-class schemas will contain the allOf construct to + /// incorporate properties from the base class instead of defining those properties inline. /// /// - public static void UseAllOfToExtendReferenceSchemas(this SwaggerGenOptions swaggerGenOptions) + public static void UseAllOfForInheritance(this SwaggerGenOptions swaggerGenOptions) { - swaggerGenOptions.SchemaGeneratorOptions.UseAllOfToExtendReferenceSchemas = true; + swaggerGenOptions.SchemaGeneratorOptions.UseAllOfForInheritance = true; } /// - /// Generate inline schema definitions (as opposed to referencing a shared definition) for enum parameters and properties + /// To support polymorphism and inheritance behavior, Swashbuckle needs to detect the "known" sub types for a given base type. + /// That is, the sub types explicitly exposed by your API. By default, this will be any sub types in the same assembly as the base type. + /// To override this, you can provide a custom resolver function. This setting is only applicable when used in conjunction with + /// the UseOneOfForPolymorphism or UseAllOfForInheritance settings. /// /// - public static void UseInlineDefinitionsForEnums(this SwaggerGenOptions swaggerGenOptions) + /// + public static void DetectSubTypesUsing( + this SwaggerGenOptions swaggerGenOptions, + Func> resolver) { - swaggerGenOptions.SchemaGeneratorOptions.UseInlineDefinitionsForEnums = true; + swaggerGenOptions.SchemaGeneratorOptions.SubTypesResolver = resolver; } /// @@ -383,5 +393,39 @@ public static void UseInlineDefinitionsForEnums(this SwaggerGenOptions swaggerGe { swaggerGenOptions.IncludeXmlComments(() => new XPathDocument(filePath), includeControllerXmlComments); } + + + [Obsolete("If the serializer is configured for string enums (e.g. StringEnumConverter) Swashbuckle will reflect that automatically")] + public static void DescribeAllEnumsAsStrings(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SchemaGeneratorOptions.DescribeAllEnumsAsStrings = true; + } + + [Obsolete("If the serializer is configured for (camel-cased) string enums (e.g. StringEnumConverter) Swashbuckle will reflect that automatically")] + public static void DescribeStringEnumsInCamelCase(this SwaggerGenOptions swaggerGenOptions) + { + swaggerGenOptions.SchemaGeneratorOptions.DescribeStringEnumsInCamelCase = true; + } + + /// + /// Generate polymorphic schemas (i.e. "oneOf") based on discovered subtypes. + /// Deprecated: Use the \"UseOneOfForPolymorphism\" and \"UseAllOfForInheritance\" settings instead + /// + /// + /// + /// + [Obsolete("You can use \"UseOneOfForPolymorphism\", \"UseAllOfForInheritance\" and \"DetectSubTypesUsing\" to configure equivalant behavior")] + public static void GeneratePolymorphicSchemas( + this SwaggerGenOptions swaggerGenOptions, + Func> subTypesResolver = null, + Func discriminatorSelector = null) + { + swaggerGenOptions.UseOneOfForPolymorphism(discriminatorSelector); + + if (subTypesResolver != null) + swaggerGenOptions.DetectSubTypesUsing(subTypesResolver); + + swaggerGenOptions.UseAllOfForInheritance(); + } } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/IDataContractResolver.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/IDataContractResolver.cs index f5f8009f3..04648ed86 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/IDataContractResolver.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/IDataContractResolver.cs @@ -11,42 +11,89 @@ public interface IDataContractResolver public class DataContract { - public DataContract( - DataType dataType, + public static DataContract ForPrimitive(Type underlyingType, DataType dataType, string dataFormat, IEnumerable enumValues = null) + { + return new DataContract( + underlyingType: underlyingType, + dataType: dataType, + dataFormat: dataFormat, + enumValues: enumValues); + } + + public static DataContract ForArray(Type underlyingType, Type itemType) + { + return new DataContract( + underlyingType: underlyingType, + dataType: DataType.Array, + arrayItemType: itemType); + } + + public static DataContract ForDictionary(Type underlyingType, Type valueType, IEnumerable keys = null) + { + return new DataContract( + underlyingType: underlyingType, + dataType: DataType.Dictionary, + dictionaryValueType: valueType, + dictionaryKeys: keys); + } + + public static DataContract ForObject(Type underlyingType, IEnumerable properties, Type extensionDataType = null) + { + return new DataContract( + underlyingType: underlyingType, + dataType: DataType.Object, + objectProperties: properties, + objectExtensionDataType: extensionDataType); + } + + public static DataContract ForDynamic(Type underlyingType) + { + return new DataContract(underlyingType: underlyingType, dataType: DataType.Unknown); + } + + private DataContract( Type underlyingType, - string format = null, + DataType dataType, + string dataFormat = null, IEnumerable enumValues = null, - IEnumerable properties = null, - Type additionalPropertiesType = null, - Type arrayItemType = null) + Type arrayItemType = null, + Type dictionaryValueType = null, + IEnumerable dictionaryKeys = null, + IEnumerable objectProperties = null, + Type objectExtensionDataType = null) { + UnderlyingType = underlyingType; DataType = dataType; - Format = format; + DataFormat = dataFormat; EnumValues = enumValues; - Properties = properties; - UnderlyingType = underlyingType; - AdditionalPropertiesType = additionalPropertiesType; ArrayItemType = arrayItemType; + DictionaryValueType = dictionaryValueType; + DictionaryKeys = dictionaryKeys; + ObjectProperties = objectProperties; + ObjectExtensionDataType = objectExtensionDataType; } + public Type UnderlyingType { get; } public DataType DataType { get; } - public string Format { get; } + public string DataFormat { get; } public IEnumerable EnumValues { get; } - public IEnumerable Properties { get; } - public Type UnderlyingType { get; } - public Type AdditionalPropertiesType { get; } public Type ArrayItemType { get; } + public Type DictionaryValueType { get; } + public IEnumerable DictionaryKeys { get; } + public IEnumerable ObjectProperties { get; } + public Type ObjectExtensionDataType { get; } } public enum DataType { - Unknown, Boolean, Integer, Number, String, + Array, + Dictionary, Object, - Array + Unknown } public class DataProperty @@ -70,17 +117,11 @@ public class DataProperty } public string Name { get; } - public bool IsRequired { get; } - public bool IsNullable { get; } - public bool IsReadOnly { get; } - public bool IsWriteOnly { get; } - public Type MemberType { get; } - public MemberInfo MemberInfo { get; } } } \ No newline at end of file diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs index 6ae982025..a0bd21b22 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -18,65 +19,54 @@ public JsonSerializerDataContractResolver(JsonSerializerOptions serializerOption public DataContract GetDataContractForType(Type type) { - var underlyingType = type.IsNullable(out Type innerType) ? innerType : type; + if (type.IsOneOf(typeof(JsonDocument), typeof(JsonElement))) + { + return DataContract.ForDynamic(underlyingType: type); + } - if (PrimitiveTypesAndFormats.ContainsKey(underlyingType)) + if (PrimitiveTypesAndFormats.ContainsKey(type)) { - var primitiveTypeAndFormat = PrimitiveTypesAndFormats[underlyingType]; + var primitiveTypeAndFormat = PrimitiveTypesAndFormats[type]; - return new DataContract( + return DataContract.ForPrimitive( + underlyingType: type, dataType: primitiveTypeAndFormat.Item1, - format: primitiveTypeAndFormat.Item2, - underlyingType: underlyingType); + dataFormat: primitiveTypeAndFormat.Item2); } - if (underlyingType.IsEnum) + if (type.IsEnum) { - var enumValues = GetSerializedEnumValuesFor(underlyingType); + var enumValues = GetSerializedEnumValuesFor(type); var primitiveTypeAndFormat = (enumValues.Any(value => value is string)) ? PrimitiveTypesAndFormats[typeof(string)] - : PrimitiveTypesAndFormats[underlyingType.GetEnumUnderlyingType()]; + : PrimitiveTypesAndFormats[type.GetEnumUnderlyingType()]; - return new DataContract( + return DataContract.ForPrimitive( + underlyingType: type, dataType: primitiveTypeAndFormat.Item1, - format: primitiveTypeAndFormat.Item2, - underlyingType: underlyingType, + dataFormat: primitiveTypeAndFormat.Item2, enumValues: enumValues); } - if (underlyingType.IsDictionary(out Type keyType, out Type valueType)) - { - if (keyType.IsEnum) - throw new NotSupportedException( - $"Schema cannot be generated for type {underlyingType} as it's not supported by the System.Text.Json serializer"); - - return new DataContract( - dataType: DataType.Object, - underlyingType: underlyingType, - additionalPropertiesType: valueType); - } - - if (underlyingType.IsEnumerable(out Type itemType)) + if (IsSupportedDictionary(type, out Type keyType, out Type valueType)) { - return new DataContract( - dataType: DataType.Array, - underlyingType: underlyingType, - arrayItemType: itemType); + return DataContract.ForDictionary( + underlyingType: type, + valueType: valueType); } - if (underlyingType.IsOneOf(typeof(JsonDocument), typeof(JsonElement))) + if (IsSupportedCollection(type, out Type itemType)) { - return new DataContract( - dataType: DataType.Unknown, - underlyingType: underlyingType); + return DataContract.ForArray( + underlyingType: type, + itemType: itemType); } - return new DataContract( - dataType: DataType.Object, - underlyingType: underlyingType, - properties: GetDataPropertiesFor(underlyingType, out Type extensionDataValueType), - additionalPropertiesType: extensionDataValueType); + return DataContract.ForObject( + underlyingType: type, + properties: GetDataPropertiesFor(type, out Type extensionDataType), + extensionDataType: extensionDataType); } private IEnumerable GetSerializedEnumValuesFor(Type enumType) @@ -92,15 +82,56 @@ private IEnumerable GetSerializedEnumValuesFor(Type enumType) : underlyingValues; } - private IEnumerable GetDataPropertiesFor(Type objectType, out Type extensionDataValueType) + public bool IsSupportedDictionary(Type type, out Type keyType, out Type valueType) { - extensionDataValueType = null; + if (type.IsConstructedFrom(typeof(IDictionary<,>), out Type constructedType) + || type.IsConstructedFrom(typeof(IReadOnlyDictionary<,>), out constructedType)) + { + keyType = constructedType.GenericTypeArguments[0]; + valueType = constructedType.GenericTypeArguments[1]; + return true; + } - if (objectType == typeof(object)) + if (typeof(IDictionary).IsAssignableFrom(type)) { - return null; + keyType = valueType = typeof(object); + return true; } + keyType = valueType = null; + return false; + } + + public bool IsSupportedCollection(Type type, out Type itemType) + { + if (type.IsConstructedFrom(typeof(IEnumerable<>), out Type constructedType)) + { + itemType = constructedType.GenericTypeArguments[0]; + return true; + } + +#if NETCOREAPP3_0 + if (type.IsConstructedFrom(typeof(IAsyncEnumerable<>), out constructedType)) + { + itemType = constructedType.GenericTypeArguments[0]; + return true; + } +#endif + + if (typeof(IEnumerable).IsAssignableFrom(type)) + { + itemType = typeof(object); + return true; + } + + itemType = null; + return false; + } + + private IEnumerable GetDataPropertiesFor(Type objectType, out Type extensionDataType) + { + extensionDataType = null; + var applicableProperties = objectType.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .Where(property => { @@ -115,9 +146,10 @@ private IEnumerable GetDataPropertiesFor(Type objectType, out Type foreach (var propertyInfo in applicableProperties) { - if (propertyInfo.HasAttribute() && propertyInfo.PropertyType.IsDictionary(out Type _, out Type valueType)) + if (propertyInfo.HasAttribute() + && propertyInfo.PropertyType.IsConstructedFrom(typeof(IDictionary<,>), out Type constructedDictionary)) { - extensionDataValueType = valueType; + extensionDataType = constructedDictionary.GenericTypeArguments[1]; continue; } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs index 55f18000f..607488149 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs @@ -27,7 +27,9 @@ public SchemaGenerator(SchemaGeneratorOptions generatorOptions, IDataContractRes MemberInfo memberInfo = null, ParameterInfo parameterInfo = null) { - var schema = GenerateSchemaForType(type, schemaRepository); + var schema = TryGetCustomTypeMapping(type, out Func mapping) + ? mapping() + : GenerateSchemaForType(type, schemaRepository); if (memberInfo != null) { @@ -46,11 +48,27 @@ public SchemaGenerator(SchemaGeneratorOptions generatorOptions, IDataContractRes return schema; } + private bool TryGetCustomTypeMapping(Type type, out Func mapping) + { + if (_generatorOptions.CustomTypeMappings.TryGetValue(type, out mapping)) + { + return true; + } + + if (type.IsConstructedGenericType && + _generatorOptions.CustomTypeMappings.TryGetValue(type.GetGenericTypeDefinition(), out mapping)) + { + return true; + } + + return false; + } + private OpenApiSchema GenerateSchemaForType(Type type, SchemaRepository schemaRepository) { - if (TryGetCustomMapping(type, out var mapping)) + if (type.IsNullable(out Type innerType)) { - return mapping(); + return GenerateSchemaForType(innerType, schemaRepository); } if (type.IsAssignableToOneOf(typeof(IFormFile), typeof(FileResult))) @@ -58,84 +76,163 @@ private OpenApiSchema GenerateSchemaForType(Type type, SchemaRepository schemaRe return new OpenApiSchema { Type = "string", Format = "binary" }; } - if (_generatorOptions.GeneratePolymorphicSchemas) + if (type == typeof(object)) { - var knownSubTypes = _generatorOptions.SubTypesResolver(type); - if (knownSubTypes.Any()) - { - return GeneratePolymorphicSchema(knownSubTypes, schemaRepository); - } + return new OpenApiSchema { Type = "object" }; } + Func definitionFactory; + bool returnAsReference; + var dataContract = _dataContractResolver.GetDataContractForType(type); - var shouldBeReferenced = - // regular object - (dataContract.DataType == DataType.Object && dataContract.Properties != null && !dataContract.UnderlyingType.IsDictionary()) || - // dictionary-based AND self-referencing - (dataContract.DataType == DataType.Object && dataContract.AdditionalPropertiesType == dataContract.UnderlyingType) || - // array-based AND self-referencing - (dataContract.DataType == DataType.Array && dataContract.ArrayItemType == dataContract.UnderlyingType) || - // enum-based AND opted-out of inline - (dataContract.EnumValues != null && !_generatorOptions.UseInlineDefinitionsForEnums); - - return (shouldBeReferenced) - ? GenerateReferencedSchema(dataContract, schemaRepository) - : GenerateInlineSchema(dataContract, schemaRepository); + switch (dataContract.DataType) + { + case DataType.Boolean: + case DataType.Integer: + case DataType.Number: + case DataType.String: + { + definitionFactory = () => GeneratePrimitiveSchema(dataContract); + returnAsReference = type.IsEnum && !_generatorOptions.UseInlineDefinitionsForEnums; + break; + } + + case DataType.Array: + { + definitionFactory = () => GenerateArraySchema(dataContract, schemaRepository); + returnAsReference = type == dataContract.ArrayItemType; + break; + } + + case DataType.Dictionary: + { + definitionFactory = () => GenerateDictionarySchema(dataContract, schemaRepository); + returnAsReference = type == dataContract.DictionaryValueType; + break; + } + + case DataType.Object: + { + if (_generatorOptions.UseOneOfForPolymorphism && IsBaseTypeWithKnownSubTypes(type, out IEnumerable subTypes)) + { + definitionFactory = () => GeneratePolymorphicSchema(dataContract, schemaRepository, subTypes); + returnAsReference = false; + } + else + { + definitionFactory = () => GenerateObjectSchema(dataContract, schemaRepository); + returnAsReference = true; + } + + break; + } + + default: + { + definitionFactory = () => new OpenApiSchema(); + returnAsReference = false; + break; + } + } + + return returnAsReference + ? GenerateReferencedSchema(type, schemaRepository, definitionFactory) + : definitionFactory(); } - private bool TryGetCustomMapping(Type type, out Func mapping) + private OpenApiSchema GeneratePrimitiveSchema(DataContract dataContract) { - if (_generatorOptions.CustomTypeMappings.TryGetValue(type, out mapping)) + var schema = new OpenApiSchema { - return true; - } + Type = dataContract.DataType.ToString().ToLower(CultureInfo.InvariantCulture), + Format = dataContract.DataFormat + }; - if (type.IsGenericType && !type.IsGenericTypeDefinition && - _generatorOptions.CustomTypeMappings.TryGetValue(type.GetGenericTypeDefinition(), out mapping)) + if (dataContract.EnumValues != null) { - return true; + schema.Enum = dataContract.EnumValues + .Distinct() + .Select(value => OpenApiAnyFactory.CreateFor(schema, value)) + .ToList(); } - return false; + return schema; } - private OpenApiSchema GeneratePolymorphicSchema(IEnumerable knownSubTypes, SchemaRepository schemaRepository) + private OpenApiSchema GenerateArraySchema(DataContract dataContract, SchemaRepository schemaRepository) { return new OpenApiSchema { - OneOf = knownSubTypes - .Select(subType => GenerateSchema(subType, schemaRepository)) - .ToList() + Type = "array", + Items = GenerateSchema(dataContract.ArrayItemType, schemaRepository), + UniqueItems = dataContract.UnderlyingType.IsSet() ? (bool?)true : null }; } - private OpenApiSchema GenerateReferencedSchema(DataContract dataContract, SchemaRepository schemaRepository) + private OpenApiSchema GenerateDictionarySchema(DataContract dataContract, SchemaRepository schemaRepository) { - return schemaRepository.GetOrAdd( - dataContract.UnderlyingType, - _generatorOptions.SchemaIdSelector(dataContract.UnderlyingType), - () => + if (dataContract.DictionaryKeys != null) + { + return new OpenApiSchema { - var schema = GenerateInlineSchema(dataContract, schemaRepository); - ApplyFilters(schema, dataContract.UnderlyingType, schemaRepository); - return schema; - }); + Type = "object", + Properties = dataContract.DictionaryKeys.ToDictionary( + name => name, + name => GenerateSchema(dataContract.DictionaryValueType, schemaRepository)) + }; + } + else + { + return new OpenApiSchema + { + Type = "object", + AdditionalPropertiesAllowed = true, + AdditionalProperties = GenerateSchema(dataContract.DictionaryValueType, schemaRepository) + }; + } } - private OpenApiSchema GenerateInlineSchema(DataContract dataContract, SchemaRepository schemaRepository) + private bool IsBaseTypeWithKnownSubTypes(Type type, out IEnumerable subTypes) { - if (dataContract.DataType == DataType.Unknown) - return new OpenApiSchema(); + subTypes = _generatorOptions.SubTypesResolver(type); + + return subTypes.Any(); + } - if (dataContract.DataType == DataType.Object) - return GenerateObjectSchema(dataContract, schemaRepository); + private bool IsKnownSubType(Type type, out Type baseType) + { + if ((type.BaseType != null) && _generatorOptions.SubTypesResolver(type.BaseType).Contains(type)) + { + baseType = type.BaseType; + return true; + } - if (dataContract.DataType == DataType.Array) - return GenerateArraySchema(dataContract, schemaRepository); + baseType = null; + return false; + } - else - return GeneratePrimitiveSchema(dataContract); + private OpenApiSchema GeneratePolymorphicSchema(DataContract dataContract, SchemaRepository schemaRepository, IEnumerable subTypes) + { + var subTypeContracts = subTypes + .Select(subType => _dataContractResolver.GetDataContractForType(subType)); + + return new OpenApiSchema + { + OneOf = new[] { dataContract }.Union(subTypeContracts) + .Select(dc => GenerateReferencedSchema(dc.UnderlyingType, schemaRepository, () => GenerateObjectSchema(dc, schemaRepository))) + .ToList(), + + Discriminator = TryGetDiscriminatorFor(dataContract.UnderlyingType, out string discriminator) + ? new OpenApiDiscriminator { PropertyName = discriminator } + : null + }; + } + + private bool TryGetDiscriminatorFor(Type type, out string discriminator) + { + discriminator = _generatorOptions.DiscriminatorSelector(type); + return (discriminator != null); } private OpenApiSchema GenerateObjectSchema(DataContract dataContract, SchemaRepository schemaRepository) @@ -148,19 +245,52 @@ private OpenApiSchema GenerateObjectSchema(DataContract dataContract, SchemaRepo AdditionalPropertiesAllowed = false }; - // If it's a baseType with known subTypes, add the discriminator property - if (_generatorOptions.GeneratePolymorphicSchemas && _generatorOptions.SubTypesResolver(dataContract.UnderlyingType).Any()) + // By default, all properties will be defined in this schema + // However, if "Inheritance" behavior is enabled (see below), this set will be reduced to declared properties only + var applicableDataProperties = dataContract.ObjectProperties; + + if (_generatorOptions.UseOneOfForPolymorphism || _generatorOptions.UseAllOfForInheritance) { - var discriminatorName = _generatorOptions.DiscriminatorSelector(dataContract.UnderlyingType); + var isBaseType = IsBaseTypeWithKnownSubTypes(dataContract.UnderlyingType, out IEnumerable subTypes); + var isSubType = IsKnownSubType(dataContract.UnderlyingType, out Type baseType); + + if (isBaseType && _generatorOptions.UseOneOfForPolymorphism + && TryGetDiscriminatorFor(dataContract.UnderlyingType, out string discriminator)) + { + schema.Properties.Add(discriminator, new OpenApiSchema { Type = "string" }); + } + + if (isSubType && _generatorOptions.UseOneOfForPolymorphism + && !_generatorOptions.UseAllOfForInheritance && TryGetDiscriminatorFor(baseType, out discriminator)) + { + schema.Properties.Add(discriminator, new OpenApiSchema { Type = "string" }); + } - if (!schema.Properties.ContainsKey(discriminatorName)) - schema.Properties.Add(discriminatorName, new OpenApiSchema { Type = "string" }); + if (isBaseType && _generatorOptions.UseAllOfForInheritance) + { + // Ensure a schema for all known sub types is generated and added to the repository + foreach (var subType in subTypes) + { + var subTypeContract = _dataContractResolver.GetDataContractForType(subType); + GenerateReferencedSchema(subType, schemaRepository, () => GenerateObjectSchema(subTypeContract, schemaRepository)); + } + } - schema.Required.Add(discriminatorName); - schema.Discriminator = new OpenApiDiscriminator { PropertyName = discriminatorName }; + if (isSubType && _generatorOptions.UseAllOfForInheritance) + { + var baseTypeContract = _dataContractResolver.GetDataContractForType(baseType); + schema.AllOf = new List + { + GenerateReferencedSchema(baseType, schemaRepository, () => GenerateObjectSchema(baseTypeContract, schemaRepository)) + }; + + // Reduce the set of properties to be defined in this schema to declared properties only + applicableDataProperties = applicableDataProperties + .Where(dataProperty => dataProperty.MemberInfo?.DeclaringType == dataContract.UnderlyingType); + } } - foreach (var dataProperty in dataContract.Properties ?? Enumerable.Empty()) + foreach (var dataProperty in applicableDataProperties) { var customAttributes = dataProperty.MemberInfo?.GetInlineOrMetadataTypeAttributes() ?? Enumerable.Empty(); @@ -173,84 +303,45 @@ private OpenApiSchema GenerateObjectSchema(DataContract dataContract, SchemaRepo schema.Required.Add(dataProperty.Name); } - if (dataContract.AdditionalPropertiesType != null) + if (dataContract.ObjectExtensionDataType != null) { schema.AdditionalPropertiesAllowed = true; - schema.AdditionalProperties = GenerateSchema(dataContract.AdditionalPropertiesType, schemaRepository); - } - - // If it's a known subType, reference the baseType for inheritied properties - if ( - _generatorOptions.GeneratePolymorphicSchemas && - (dataContract.UnderlyingType.BaseType != null) && - _generatorOptions.SubTypesResolver(dataContract.UnderlyingType.BaseType).Contains(dataContract.UnderlyingType)) - { - var basedataContract = _dataContractResolver.GetDataContractForType(dataContract.UnderlyingType.BaseType); - var baseSchemaReference = GenerateReferencedSchema(basedataContract, schemaRepository); - - var baseSchema = schemaRepository.Schemas[baseSchemaReference.Reference.Id]; - foreach (var basePropertyName in baseSchema.Properties.Keys) - { - schema.Properties.Remove(basePropertyName); - } - - return new OpenApiSchema - { - AllOf = new List { baseSchemaReference, schema } - }; + schema.AdditionalProperties = GenerateSchema(dataContract.ObjectExtensionDataType, schemaRepository); } return schema; } - private OpenApiSchema GeneratePropertySchema(DataProperty serializerMember, SchemaRepository schemaRepository) + private OpenApiSchema GeneratePropertySchema(DataProperty dataProperty, SchemaRepository schemaRepository) { - var schema = GenerateSchemaForType(serializerMember.MemberType, schemaRepository); - - if (serializerMember.MemberInfo != null) - { - ApplyMemberMetadata(schema, serializerMember.MemberType, serializerMember.MemberInfo); - } + var schema = GenerateSchema(dataProperty.MemberType, schemaRepository, memberInfo: dataProperty.MemberInfo); if (schema.Reference == null) { - schema.Nullable = serializerMember.IsNullable && schema.Nullable; - schema.ReadOnly = serializerMember.IsReadOnly; - schema.WriteOnly = serializerMember.IsWriteOnly; - - ApplyFilters(schema, serializerMember.MemberType, schemaRepository, serializerMember.MemberInfo); + schema.Nullable = schema.Nullable && dataProperty.IsNullable; + schema.ReadOnly = dataProperty.IsReadOnly; + schema.WriteOnly = dataProperty.IsWriteOnly; } return schema; } - private OpenApiSchema GenerateArraySchema(DataContract dataContract, SchemaRepository schemaRepository) + private OpenApiSchema GenerateReferencedSchema( + Type type, + SchemaRepository schemaRepository, + Func definitionFactory) { - return new OpenApiSchema - { - Type = "array", - Items = GenerateSchema(dataContract.ArrayItemType, schemaRepository), - UniqueItems = dataContract.UnderlyingType.IsSet() ? (bool?)true : null - }; - } + if (schemaRepository.TryLookupByType(type, out OpenApiSchema referenceSchema)) + return referenceSchema; - private OpenApiSchema GeneratePrimitiveSchema(DataContract dataContract) - { - var schema = new OpenApiSchema - { - Type = dataContract.DataType.ToString().ToLower(CultureInfo.InvariantCulture), - Format = dataContract.Format - }; + var schemaId = _generatorOptions.SchemaIdSelector(type); - if (dataContract.EnumValues != null) - { - schema.Enum = dataContract.EnumValues - .Distinct() - .Select(value => OpenApiAnyFactory.CreateFor(schema, value)) - .ToList(); - } + schemaRepository.RegisterType(type, schemaId); - return schema; + var schema = definitionFactory(); + ApplyFilters(schema, type, schemaRepository); + + return schemaRepository.AddDefinition(schemaId, schema); } private void ApplyMemberMetadata(OpenApiSchema schema, Type type, MemberInfo memberInfo) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGeneratorOptions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGeneratorOptions.cs index 154f28aec..a0ae9549b 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGeneratorOptions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGeneratorOptions.cs @@ -18,19 +18,22 @@ public SchemaGeneratorOptions() public IDictionary> CustomTypeMappings { get; set; } + public bool UseInlineDefinitionsForEnums { get; set; } + public Func SchemaIdSelector { get; set; } public bool IgnoreObsoleteProperties { get; set; } - public bool GeneratePolymorphicSchemas { get; set; } + public bool UseAllOfToExtendReferenceSchemas { get; set; } + + public bool UseOneOfForPolymorphism { get; set; } public Func> SubTypesResolver { get; set; } public Func DiscriminatorSelector { get; set; } - public bool UseAllOfToExtendReferenceSchemas { get; set; } + public bool UseAllOfForInheritance { get; set; } - public bool UseInlineDefinitionsForEnums { get; set; } public IList SchemaFilters { get; set; } @@ -61,7 +64,7 @@ private IEnumerable DefaultSubTypesResolver(Type baseType) private string DefaultDiscriminatorSelector(Type baseType) { - return "$type"; + return null; } } } \ No newline at end of file diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/TypeExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/TypeExtensions.cs index a77472e5f..7accaa2dc 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/TypeExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/TypeExtensions.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; @@ -12,12 +11,17 @@ public static bool IsOneOf(this Type type, params Type[] possibleTypes) return possibleTypes.Any(possibleType => possibleType == type); } - public static bool IsAssignableToOneOf(this Type type, params Type[] possibleTypes) + public static bool IsAssignableTo(this Type type, Type baseType) { - return possibleTypes.Any(possibleType => possibleType.IsAssignableFrom(type)); + return baseType.IsAssignableFrom(type); } - private static bool IsConstructedFrom(this Type type, Type genericType, out Type constructedType) + public static bool IsAssignableToOneOf(this Type type, params Type[] possibleBaseTypes) + { + return possibleBaseTypes.Any(possibleBaseType => possibleBaseType.IsAssignableFrom(type)); + } + + public static bool IsConstructedFrom(this Type type, Type genericType, out Type constructedType) { constructedType = new[] { type } .Union(type.GetInterfaces()) @@ -40,60 +44,16 @@ public static bool IsReferenceOrNullableType(this Type type) return (!type.IsValueType || type.IsNullable(out Type _)); } - public static bool IsDictionary(this Type type, out Type keyType, out Type valueType) - { - if (type.IsConstructedFrom(typeof(IDictionary<,>), out Type constructedType) - || type.IsConstructedFrom(typeof(IReadOnlyDictionary<,>), out constructedType)) - { - keyType = constructedType.GenericTypeArguments[0]; - valueType = constructedType.GenericTypeArguments[1]; - return true; - } - - if (typeof(IDictionary).IsAssignableFrom(type)) - { - keyType = valueType = typeof(object); - return true; - } - - keyType = valueType = null; - return false; - } - - public static bool IsDictionary(this Type type) - { - return type.IsDictionary(out Type _, out Type _); - } - - public static bool IsEnumerable(this Type type, out Type itemType) + public static bool IsSet(this Type type) { - if (type.IsConstructedFrom(typeof(IEnumerable<>), out Type constructedType)) - { - itemType = constructedType.GenericTypeArguments[0]; - return true; - } - -#if NETCOREAPP3_0 - if (type.IsConstructedFrom(typeof(IAsyncEnumerable<>), out constructedType)) - { - itemType = constructedType.GenericTypeArguments[0]; - return true; - } -#endif - - if (typeof(IEnumerable).IsAssignableFrom(type)) - { - itemType = typeof(object); - return true; - } - - itemType = null; - return false; + return type.IsConstructedFrom(typeof(ISet<>), out Type _); } - public static bool IsSet(this Type type) + public static object GetDefaultValue(this Type type) { - return type.IsConstructedFrom(typeof(ISet<>), out Type _); + return type.IsValueType + ? Activator.CreateInstance(type) + : null; } } } diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SchemaRepository.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SchemaRepository.cs index 12e246c9b..9a9365c78 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SchemaRepository.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SwaggerGenerator/SchemaRepository.cs @@ -11,44 +11,43 @@ public class SchemaRepository public Dictionary Schemas { get; private set; } = new Dictionary(); - public OpenApiSchema GetOrAdd(Type type, string schemaId, Func factoryMethod) + public void RegisterType(Type type, string schemaId) { - if (!_reservedIds.TryGetValue(type, out string reservedId)) - { - // First invocation of the factoryMethod for this type - reserve the provided schemaId first, and then invoke the factory method. - // Reserving the id first ensures that the factoryMethod will only be invoked once for a given type, even in recurrsive scenarios. - // If subsequent calls are made for the same type, a simple reference will be created instead. - ReserveIdFor(type, schemaId); - Schemas.Add(schemaId, factoryMethod()); - } - else + if (_reservedIds.ContainsValue(schemaId)) { - schemaId = reservedId; + var conflictingType = _reservedIds.First(entry => entry.Value == schemaId).Key; + + throw new InvalidOperationException( + $"Can't use schemaId \"${schemaId}\" for type \"${type}\". " + + $"The same schemaId is already used for type \"${conflictingType}\""); } - return new OpenApiSchema - { - Reference = new OpenApiReference { Id = schemaId, Type = ReferenceType.Schema } - }; + _reservedIds.Add(type, schemaId); } - public bool TryGetIdFor(Type type, out string schemaId) - { - return _reservedIds.TryGetValue(type, out schemaId); - } - - public void ReserveIdFor(Type type, string schemaId) + public bool TryLookupByType(Type type, out OpenApiSchema referenceSchema) { - if (_reservedIds.ContainsValue(schemaId)) + if (_reservedIds.TryGetValue(type, out string schemaId)) { - var reservedForType = _reservedIds.First(entry => entry.Value == schemaId).Key; - - throw new InvalidOperationException( - $"Can't use schemaId \"${schemaId}\" for type \"${type}\". " + - $"The same schemaId is already used for type \"${reservedForType}\""); + referenceSchema = new OpenApiSchema + { + Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = schemaId } + }; + return true; } - _reservedIds.Add(type, schemaId); + referenceSchema = null; + return false; + } + + public OpenApiSchema AddDefinition(string schemaId, OpenApiSchema schema) + { + Schemas.Add(schemaId, schema); + + return new OpenApiSchema + { + Reference = new OpenApiReference { Type = ReferenceType.Schema, Id = schemaId } + }; } } } diff --git a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/NewtonsoftSerializerTesting.cs b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/NewtonsoftSerializerTesting.cs index e9914c195..fb4d3d291 100644 --- a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/NewtonsoftSerializerTesting.cs +++ b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/NewtonsoftSerializerTesting.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Newtonsoft.Json; using Swashbuckle.AspNetCore.TestSupport; using Xunit; @@ -13,15 +14,9 @@ public class NewtonsoftSerializerTesting [Fact] public void Serialize() { - var dto = new Dictionary - { - [IntEnum.Value2] = "foo", - [IntEnum.Value4] = "bar", - [IntEnum.Value8] = "blah", - }; + var dto = new Version(1, 1, 1); var jsonString = JsonConvert.SerializeObject(dto); - Assert.Equal("{\"Value2\":\"foo\",\"Value4\":\"bar\",\"Value8\":\"blah\"}", jsonString); } [Fact] diff --git a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs index ebbb7d6b4..57ae38184 100644 --- a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/SchemaGenerator/NewtonsoftSchemaGeneratorTests.cs @@ -178,7 +178,6 @@ public void GenerateSchema_GeneratesObjectSchema_IfObjectType() Assert.Equal("object", schema.Type); Assert.Empty(schema.Properties); - Assert.False(schema.AdditionalPropertiesAllowed); } [Theory] @@ -393,10 +392,10 @@ public void GenerateSchema_SupportsOption_SchemaIdSelector() } [Fact] - public void GenerateSchema_SupportsOption_GeneratePolymorphicSchemas() + public void GenerateSchema_SupportsOption_UseOneOfForPolymorphism() { var subject = Subject( - configureGenerator: c => c.GeneratePolymorphicSchemas = true + configureGenerator: c => c.UseOneOfForPolymorphism = true ); var schemaRepository = new SchemaRepository(); @@ -404,25 +403,94 @@ public void GenerateSchema_SupportsOption_GeneratePolymorphicSchemas() // The polymorphic schema Assert.NotNull(schema.OneOf); - Assert.Equal(2, schema.OneOf.Count); + Assert.Equal(3, schema.OneOf.Count); Assert.NotNull(schema.OneOf[0].Reference); - // The first sub schema - var subSchema1 = schemaRepository.Schemas[schema.OneOf[0].Reference.Id]; - Assert.NotNull(subSchema1.AllOf); - Assert.Equal(2, subSchema1.AllOf.Count); - Assert.Equal("PolymorphicType", subSchema1.AllOf[0].Reference.Id); - Assert.Equal(new[] { "Property1" }, subSchema1.AllOf[1].Properties.Keys); - // The second sub schema - var subSchema2 = schemaRepository.Schemas[schema.OneOf[1].Reference.Id]; - Assert.NotNull(subSchema2.AllOf); - Assert.Equal(2, subSchema2.AllOf.Count); - Assert.Equal("PolymorphicType", subSchema2.AllOf[0].Reference.Id); - Assert.Equal(new[] { "Property2" }, subSchema2.AllOf[1].Properties.Keys); - // The base schema - var baseSchema = schemaRepository.Schemas[subSchema1.AllOf[0].Reference.Id]; - Assert.Equal(new[] { "$type", "BaseProperty" }, baseSchema.Properties.Keys); - Assert.Equal(new[] { "$type" }, baseSchema.Required); - Assert.Equal("$type", baseSchema.Discriminator.PropertyName); + Assert.NotNull(schema.OneOf[1].Reference); + Assert.NotNull(schema.OneOf[2].Reference); + // The base type schema + var baseSchema = schemaRepository.Schemas[schema.OneOf[0].Reference.Id]; + Assert.Equal("object", baseSchema.Type); + Assert.Equal(new[] { "BaseProperty"}, baseSchema.Properties.Keys); + // The first sub type schema + var subType1Schema = schemaRepository.Schemas[schema.OneOf[1].Reference.Id]; + Assert.Equal("object", subType1Schema.Type); + Assert.Equal(new[] { "Property1", "BaseProperty" }, subType1Schema.Properties.Keys); + // The second sub type schema + var subType2Schema = schemaRepository.Schemas[schema.OneOf[2].Reference.Id]; + Assert.Equal("object", subType2Schema.Type); + Assert.Equal(new[] { "Property2", "BaseProperty" }, subType2Schema.Properties.Keys); + } + + [Fact] + public void GenerateSchema_SupportsOption_UseAllOfForInheritance() + { + var subject = Subject( + configureGenerator: c => c.UseAllOfForInheritance = true + ); + var schemaRepository = new SchemaRepository(); + + var referenceSchema = subject.GenerateSchema(typeof(PolymorphicType), schemaRepository); + + // The base type schema + var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; + Assert.Equal("object", schema.Type); + Assert.Equal(new[] { "BaseProperty" }, schema.Properties.Keys); + // The first sub type schema + var subType1Schema = schemaRepository.Schemas["SubType1"]; + Assert.Equal("object", subType1Schema.Type); + Assert.NotNull(subType1Schema.AllOf); + Assert.Equal(1, subType1Schema.AllOf.Count); + Assert.NotNull(subType1Schema.AllOf[0].Reference); + Assert.Equal(referenceSchema.Reference.Id, subType1Schema.AllOf[0].Reference.Id); + Assert.Equal(new[] { "Property1" }, subType1Schema.Properties.Keys); + // The second sub type schema + var subType2Schema = schemaRepository.Schemas["SubType2"]; + Assert.Equal("object", subType2Schema.Type); + Assert.NotNull(subType2Schema.AllOf); + Assert.Equal(1, subType2Schema.AllOf.Count); + Assert.NotNull(subType2Schema.AllOf[0].Reference); + Assert.Equal(referenceSchema.Reference.Id, subType2Schema.AllOf[0].Reference.Id); + Assert.Equal(new[] { "Property2" }, subType2Schema.Properties.Keys); + } + + [Fact] + public void GenerateSchema_SupportsOption_UseOneOfForPolymorphism_CombinedWith_UseAllOfForInheritance() + { + var subject = Subject(configureGenerator: c => + { + c.UseOneOfForPolymorphism = true; + c.UseAllOfForInheritance = true; + }); + var schemaRepository = new SchemaRepository(); + + var schema = subject.GenerateSchema(typeof(PolymorphicType), schemaRepository); + + // The polymorphic schema + Assert.NotNull(schema.OneOf); + Assert.Equal(3, schema.OneOf.Count); + Assert.NotNull(schema.OneOf[0].Reference); + Assert.NotNull(schema.OneOf[1].Reference); + Assert.NotNull(schema.OneOf[2].Reference); + // The base type schema + var baseSchema = schemaRepository.Schemas[schema.OneOf[0].Reference.Id]; + Assert.Equal("object", baseSchema.Type); + Assert.Equal(new[] { "BaseProperty"}, baseSchema.Properties.Keys); + // The first sub type schema + var subType1Schema = schemaRepository.Schemas[schema.OneOf[1].Reference.Id]; + Assert.Equal("object", subType1Schema.Type); + Assert.NotNull(subType1Schema.AllOf); + Assert.Equal(1, subType1Schema.AllOf.Count); + Assert.NotNull(subType1Schema.AllOf[0].Reference); + Assert.Equal(schema.OneOf[0].Reference.Id, subType1Schema.AllOf[0].Reference.Id); + Assert.Equal(new[] { "Property1" }, subType1Schema.Properties.Keys); + // The second sub type schema + var subType2Schema = schemaRepository.Schemas[schema.OneOf[2].Reference.Id]; + Assert.Equal("object", subType2Schema.Type); + Assert.NotNull(subType2Schema.AllOf); + Assert.Equal(1, subType2Schema.AllOf.Count); + Assert.NotNull(subType2Schema.AllOf[0].Reference); + Assert.Equal(schema.OneOf[0].Reference.Id, subType2Schema.AllOf[0].Reference.Id); + Assert.Equal(new[] { "Property2" }, subType2Schema.Properties.Keys); } [Fact] @@ -635,35 +703,18 @@ public void GenerateSchema_HonorsSerializerAttribute_JsonExtensionData() Assert.Equal("object", schema.AdditionalProperties.Type); } - [Fact] - public void GenerateSchema_GeneratesEmptySchema_IfJToken() + [Theory] + [InlineData(typeof(JToken))] + [InlineData(typeof(JObject))] + [InlineData(typeof(JArray))] + public void GenerateSchema_GeneratesEmptySchema_IfDynamicJsonType(Type type) { - var schema = Subject().GenerateSchema(typeof(JToken), new SchemaRepository()); + var schema = Subject().GenerateSchema(type, new SchemaRepository()); Assert.Null(schema.Reference); Assert.Null(schema.Type); } - [Fact] - public void GenerateSchema_GeneratesArraySchema_IfJArray() - - { - var schema = Subject().GenerateSchema(typeof(JArray), new SchemaRepository()); - - Assert.Equal("array", schema.Type); - Assert.NotNull(schema.Items); - Assert.Null(schema.Items.Type); - } - - [Fact] - public void GenerateSchema_GeneratesObjectSchema_IfJObject() - { - var schema = Subject().GenerateSchema(typeof(JObject), new SchemaRepository()); - - Assert.Equal("object", schema.Type); - Assert.Empty(schema.Properties); - } - private SchemaGenerator Subject( Action configureGenerator = null, Action configureSerializer = null) diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs index 363d204a5..8a709e02c 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/Fixtures/FakeController.cs @@ -34,10 +34,19 @@ public void ActionWithParameterWithRequiredAttribute([Required]string param) public void ActionWithParameterWithBindRequiredAttribute([BindRequired]string param) { } - public void ActionWithOptionalParameter(string param = "someDefaultValue") + public void ActionWithIntParameter(int param) { } - public void ActionWithParameterWithDefaultValueAttribute([DefaultValue("someDefaultValue")]string param) + public void ActionWithIntParameterWithRangeAttribute([Range(1, 12)]int param) + { } + + public void ActionWithIntParameterWithDefaultValue(int param = 1) + { } + + public void ActionWithIntParameterWithDefaultValueAttribute([DefaultValue(3)]int param) + { } + + public void ActionWithIntParameterWithRequiredAttribute([Required]int param) { } [Consumes("application/someMediaType")] diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/JsonSerializerTesting.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/JsonSerializerTesting.cs index 01ce82247..426661ee5 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/JsonSerializerTesting.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/JsonSerializerTesting.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text.Json; +using System.Text.Json; using Xunit; namespace Swashbuckle.AspNetCore.SwaggerGen.Test @@ -13,27 +11,23 @@ public class JsonSerializerTesting [Fact] public void Serialize() { - var dto = new Dictionary - { - [JsonConverterAnnotatedEnum.Value1] = "foo", - [JsonConverterAnnotatedEnum.Value2] = "bar", - [JsonConverterAnnotatedEnum.X] = "blah", - }; + var dto = new TestDto(); - Assert.Throws(() => - { - JsonSerializer.Serialize(dto); - }); + var json = JsonSerializer.Serialize(dto); + + //Assert.Equal("{\"Prop1\":null}", json); } [Fact] public void Deserialize() { - Assert.Throws(() => - { - var dto = JsonSerializer.Deserialize>( - "{ \"value1\": \"foo\", \"value2\": \"bar\" }"); - }); + var dto = JsonSerializer.Deserialize( + "{ \"Prop1\": 123 }"); } } + + public class TestDto + { + public int Prop1 { get; set; } + } } diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs index e856da4fb..5e8a00a9c 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs @@ -5,13 +5,13 @@ using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; +using System.Net; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; using Xunit; using Swashbuckle.AspNetCore.TestSupport; -using System.Net; namespace Swashbuckle.AspNetCore.SwaggerGen.Test { @@ -133,7 +133,6 @@ public void GenerateSchema_GeneratesReferencedDictionarySchema_IfSelfReferencing [Theory] [InlineData(typeof(int[]), "integer", "int32")] [InlineData(typeof(IEnumerable), "string", null)] - [InlineData(typeof(IAsyncEnumerable), "string", null)] [InlineData(typeof(DateTime?[]), "string", "date-time")] [InlineData(typeof(int[][]), "array", null)] [InlineData(typeof(IList), "object", null)] @@ -181,7 +180,6 @@ public void GenerateSchema_GeneratesObjectSchema_IfObjectType() Assert.Equal("object", schema.Type); Assert.Empty(schema.Properties); - Assert.False(schema.AdditionalPropertiesAllowed); } [Theory] @@ -396,36 +394,148 @@ public void GenerateSchema_SupportsOption_SchemaIdSelector() } [Fact] - public void GenerateSchema_SupportsOption_GeneratePolymorphicSchemas() + public void GenerateSchema_SupportsOption_UseOneOfForPolymorphism() { var subject = Subject( - configureGenerator: c => c.GeneratePolymorphicSchemas = true + configureGenerator: c => c.UseOneOfForPolymorphism = true ); var schemaRepository = new SchemaRepository(); var schema = subject.GenerateSchema(typeof(PolymorphicType), schemaRepository); + // The polymorphic schema + Assert.NotNull(schema.OneOf); + Assert.Equal(3, schema.OneOf.Count); + Assert.NotNull(schema.OneOf[0].Reference); + Assert.NotNull(schema.OneOf[1].Reference); + Assert.NotNull(schema.OneOf[2].Reference); + // The base type schema + var baseSchema = schemaRepository.Schemas[schema.OneOf[0].Reference.Id]; + Assert.Equal("object", baseSchema.Type); + Assert.Equal(new[] { "BaseProperty"}, baseSchema.Properties.Keys); + // The first sub type schema + var subType1Schema = schemaRepository.Schemas[schema.OneOf[1].Reference.Id]; + Assert.Equal("object", subType1Schema.Type); + Assert.Equal(new[] { "Property1", "BaseProperty" }, subType1Schema.Properties.Keys); + // The second sub type schema + var subType2Schema = schemaRepository.Schemas[schema.OneOf[2].Reference.Id]; + Assert.Equal("object", subType2Schema.Type); + Assert.Equal(new[] { "Property2", "BaseProperty" }, subType2Schema.Properties.Keys); + } + + [Fact] + public void GenerateSchema_SupportsOption_SubTypesResolver() + { + var subject = Subject(configureGenerator: c => + { + c.UseOneOfForPolymorphism = true; + c.SubTypesResolver = (type) => new[] { typeof(SubType1) }; + }); + + var schema = subject.GenerateSchema(typeof(PolymorphicType), new SchemaRepository()); + // The polymorphic schema Assert.NotNull(schema.OneOf); Assert.Equal(2, schema.OneOf.Count); + } + + [Fact] + public void GenerateSchema_SupportsOption_DiscriminatorSelector() + { + var subject = Subject(configureGenerator: c => + { + c.UseOneOfForPolymorphism = true; + c.DiscriminatorSelector = (type) => "$type"; + }); + var schemaRepository = new SchemaRepository(); + + var schema = subject.GenerateSchema(typeof(PolymorphicType), schemaRepository); + + // The polymorphic schema + Assert.NotNull(schema.OneOf); + Assert.NotNull(schema.Discriminator); + Assert.Equal("$type", schema.Discriminator.PropertyName); + // The base type schema + var baseSchema = schemaRepository.Schemas[schema.OneOf[0].Reference.Id]; + Assert.Contains("$type", baseSchema.Properties.Keys); + // The first sub type schema + var subType1Schema = schemaRepository.Schemas[schema.OneOf[1].Reference.Id]; + Assert.Contains("$type", subType1Schema.Properties.Keys); + // The second sub type schema + var subType2Schema = schemaRepository.Schemas[schema.OneOf[2].Reference.Id]; + Assert.Contains("$type", subType2Schema.Properties.Keys); + } + + [Fact] + public void GenerateSchema_SupportsOption_UseAllOfForInheritance() + { + var subject = Subject( + configureGenerator: c => c.UseAllOfForInheritance = true + ); + var schemaRepository = new SchemaRepository(); + + var referenceSchema = subject.GenerateSchema(typeof(PolymorphicType), schemaRepository); + + // The base type schema + var schema = schemaRepository.Schemas[referenceSchema.Reference.Id]; + Assert.Equal("object", schema.Type); + Assert.Equal(new[] { "BaseProperty" }, schema.Properties.Keys); + // The first sub type schema + var subType1Schema = schemaRepository.Schemas["SubType1"]; + Assert.Equal("object", subType1Schema.Type); + Assert.NotNull(subType1Schema.AllOf); + Assert.Equal(1, subType1Schema.AllOf.Count); + Assert.NotNull(subType1Schema.AllOf[0].Reference); + Assert.Equal(referenceSchema.Reference.Id, subType1Schema.AllOf[0].Reference.Id); + Assert.Equal(new[] { "Property1" }, subType1Schema.Properties.Keys); + // The second sub type schema + var subType2Schema = schemaRepository.Schemas["SubType2"]; + Assert.Equal("object", subType2Schema.Type); + Assert.NotNull(subType2Schema.AllOf); + Assert.Equal(1, subType2Schema.AllOf.Count); + Assert.NotNull(subType2Schema.AllOf[0].Reference); + Assert.Equal(referenceSchema.Reference.Id, subType2Schema.AllOf[0].Reference.Id); + Assert.Equal(new[] { "Property2" }, subType2Schema.Properties.Keys); + } + + [Fact] + public void GenerateSchema_SupportsOption_UseOneOfForPolymorphism_CombinedWith_UseAllOfForInheritance() + { + var subject = Subject(configureGenerator: c => + { + c.UseOneOfForPolymorphism = true; + c.UseAllOfForInheritance = true; + }); + var schemaRepository = new SchemaRepository(); + + var schema = subject.GenerateSchema(typeof(PolymorphicType), schemaRepository); + + // The polymorphic schema + Assert.NotNull(schema.OneOf); + Assert.Equal(3, schema.OneOf.Count); Assert.NotNull(schema.OneOf[0].Reference); - // The first sub schema - var subSchema1 = schemaRepository.Schemas[schema.OneOf[0].Reference.Id]; - Assert.NotNull(subSchema1.AllOf); - Assert.Equal(2, subSchema1.AllOf.Count); - Assert.Equal("PolymorphicType", subSchema1.AllOf[0].Reference.Id); - Assert.Equal(new[] { "Property1" }, subSchema1.AllOf[1].Properties.Keys); - // The second sub schema - var subSchema2 = schemaRepository.Schemas[schema.OneOf[1].Reference.Id]; - Assert.NotNull(subSchema2.AllOf); - Assert.Equal(2, subSchema2.AllOf.Count); - Assert.Equal("PolymorphicType", subSchema2.AllOf[0].Reference.Id); - Assert.Equal(new[] { "Property2" }, subSchema2.AllOf[1].Properties.Keys); - // The base schema - var baseSchema = schemaRepository.Schemas[subSchema1.AllOf[0].Reference.Id]; - Assert.Equal(new[] { "$type", "BaseProperty" }, baseSchema.Properties.Keys); - Assert.Equal(new[] { "$type" }, baseSchema.Required); - Assert.Equal("$type", baseSchema.Discriminator.PropertyName); + Assert.NotNull(schema.OneOf[1].Reference); + Assert.NotNull(schema.OneOf[2].Reference); + // The base type schema + var baseSchema = schemaRepository.Schemas[schema.OneOf[0].Reference.Id]; + Assert.Equal("object", baseSchema.Type); + Assert.Equal(new[] { "BaseProperty"}, baseSchema.Properties.Keys); + // The first sub type schema + var subType1Schema = schemaRepository.Schemas[schema.OneOf[1].Reference.Id]; + Assert.Equal("object", subType1Schema.Type); + Assert.NotNull(subType1Schema.AllOf); + Assert.Equal(1, subType1Schema.AllOf.Count); + Assert.NotNull(subType1Schema.AllOf[0].Reference); + Assert.Equal(schema.OneOf[0].Reference.Id, subType1Schema.AllOf[0].Reference.Id); + Assert.Equal(new[] { "Property1" }, subType1Schema.Properties.Keys); + // The second sub type schema + var subType2Schema = schemaRepository.Schemas[schema.OneOf[2].Reference.Id]; + Assert.Equal("object", subType2Schema.Type); + Assert.NotNull(subType2Schema.AllOf); + Assert.Equal(1, subType2Schema.AllOf.Count); + Assert.NotNull(subType2Schema.AllOf[0].Reference); + Assert.Equal(schema.OneOf[0].Reference.Id, subType2Schema.AllOf[0].Reference.Id); + Assert.Equal(new[] { "Property2" }, subType2Schema.Properties.Keys); } [Fact] @@ -496,16 +606,6 @@ public void GenerateSchema_Errors_IfTypesHaveConflictingSchemaIds() }); } - [Theory] - [InlineData(typeof(IDictionary))] - public void GenerateSchema_Errors_IfTypeIsUnsupportedBySerializer(Type type) - { - Assert.Throws(() => - { - Subject().GenerateSchema(type, new SchemaRepository()); - }); - } - [Fact] public void GenerateSchema_HonorsSerializerOption_IgnoreReadonlyProperties() { diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs index 810d37b28..5172a9aca 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenerator/SwaggerGeneratorTests.cs @@ -248,38 +248,6 @@ public void GetSwagger_SetsParameterTypeToString_IfApiParameterHasNoCorrespondin Assert.Equal("string", operation.Parameters.First().Schema.Type); } - [Theory] - [InlineData(nameof(FakeController.ActionWithOptionalParameter))] - [InlineData(nameof(FakeController.ActionWithParameterWithDefaultValueAttribute))] - public void GetSwagger_SetsParameterDefault_IfActionParameterIsOptionalOrHasDefaultValueAttribute( - string actionName) - { - var subject = Subject( - apiDescriptions: new[] - { - ApiDescriptionFactory.Create( - methodInfo: typeof(FakeController).GetMethod(actionName), - groupName: "v1", - httpMethod: "POST", - relativePath: "resource", - parameterDescriptions: new [] - { - new ApiParameterDescription - { - Name = "param", - Source = BindingSource.Query - } - }) - } - ); - - var document = subject.GetSwagger("v1"); - - var operation = document.Paths["/resource"].Operations[OperationType.Post]; - Assert.Equal(1, operation.Parameters.Count); - Assert.Equal("someDefaultValue", ((OpenApiString)operation.Parameters.First().Schema.Default).Value); - } - [Fact] public void GetSwagger_GeneratesRequestBody_ForFirstApiParameterThatIsBoundToBody() { diff --git a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/ComplexType2.cs b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/ComplexType2.cs new file mode 100644 index 000000000..a88dcde54 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/ComplexType2.cs @@ -0,0 +1,11 @@ +namespace Swashbuckle.AspNetCore.TestSupport +{ + public class ComplexType2 + { + public int IntProperty { get; set; } + + public string StringProperty { get; set; } + + public int? NullableIntProperty { get; set; } + } +} diff --git a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/ComplexTypeWithRestrictedProperties.cs b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/ComplexTypeWithRestrictedProperties.cs new file mode 100644 index 000000000..d7f72e083 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/ComplexTypeWithRestrictedProperties.cs @@ -0,0 +1,9 @@ +namespace Swashbuckle.AspNetCore.TestSupport +{ + public class ComplexTypeWithRestrictedProperties + { + public int ReadWriteProperty { get; set; } + public int ReadOnlyProperty { get; } + public int WriteOnlyProperty { set { } } + } +} diff --git a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/DataAnnotatedType.cs b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/DataAnnotatedType.cs index f55ec3ad2..7c66491ae 100644 --- a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/DataAnnotatedType.cs +++ b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/DataAnnotatedType.cs @@ -16,6 +16,9 @@ public class DataAnnotatedType [Range(1, 12)] public int IntWithRange { get; set; } + [DefaultValue(3)] + public int IntWithDefaultValue { get; set; } + [RegularExpression("^[3-6]?\\d{12,15}$")] public string StringWithRegularExpression { get; set; } diff --git a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/DataAnnotatedViaMetadataType.cs b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/DataAnnotatedViaMetadataType.cs index 6befc3d34..23b0ae718 100644 --- a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/DataAnnotatedViaMetadataType.cs +++ b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/DataAnnotatedViaMetadataType.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Mvc; namespace Swashbuckle.AspNetCore.TestSupport @@ -13,6 +14,8 @@ public class DataAnnotatedViaMetadataType public int IntWithRange { get; set; } public string StringWithRegularExpression { get; set; } + + public string StringWithMinMaxLength { get; set; } } public class MetadataType @@ -28,5 +31,8 @@ public class MetadataType [RegularExpression("^[3-6]?\\d{12,15}$")] public string StringWithRegularExpression { get; set; } + + [MinLength(1), MaxLength(3)] + public string StringWithMinMaxLength { get; set; } } } \ No newline at end of file diff --git a/test/WebSites/CliExample/CliExample.csproj b/test/WebSites/CliExample/CliExample.csproj index ec58b0282..da67b5ebb 100644 --- a/test/WebSites/CliExample/CliExample.csproj +++ b/test/WebSites/CliExample/CliExample.csproj @@ -8,7 +8,6 @@ -