From ddca05aa82f5a1f63c345669993117f83f1745c7 Mon Sep 17 00:00:00 2001 From: Ryan Heath Date: Tue, 10 Sep 2024 01:29:37 +0200 Subject: [PATCH 1/5] Added setting UseSystemTextJsonPolymorphicSerialization when true a CustomTemplateFactory is used that overrides the NSwag DefaultTemplateFactory behavior --- .../CSharpClientGeneratorFactory.cs | 99 +++++-- src/Refitter.Core/Refitter.Core.csproj | 8 + .../Settings/RefitGeneratorSettings.cs | 10 +- src/Refitter.Core/Templates/Class.liquid | 148 +++++++++++ .../Templates/JsonInheritanceAttribute.liquid | 0 .../Templates/JsonInheritanceConverter.liquid | 0 ...emTextJsonPolymorphicSerializationTests.cs | 247 ++++++++++++++++++ src/Refitter/GenerateCommand.cs | 3 +- src/Refitter/Settings.cs | 7 +- 9 files changed, 495 insertions(+), 27 deletions(-) create mode 100644 src/Refitter.Core/Templates/Class.liquid create mode 100644 src/Refitter.Core/Templates/JsonInheritanceAttribute.liquid create mode 100644 src/Refitter.Core/Templates/JsonInheritanceConverter.liquid create mode 100644 src/Refitter.Tests/Examples/UseSystemTextJsonPolymorphicSerializationTests.cs diff --git a/src/Refitter.Core/CSharpClientGeneratorFactory.cs b/src/Refitter.Core/CSharpClientGeneratorFactory.cs index 2983cf9e..6e394d63 100644 --- a/src/Refitter.Core/CSharpClientGeneratorFactory.cs +++ b/src/Refitter.Core/CSharpClientGeneratorFactory.cs @@ -1,3 +1,5 @@ +using System.Reflection; +using NJsonSchema.CodeGeneration; using NJsonSchema.CodeGeneration.CSharp; using NSwag; @@ -17,30 +19,43 @@ public CustomCSharpClientGenerator Create() } } + var csharpClientGeneratorSettings = new CSharpClientGeneratorSettings + { + GenerateClientClasses = false, + GenerateDtoTypes = true, + GenerateClientInterfaces = false, + GenerateExceptionClasses = false, + CodeGeneratorSettings = { PropertyNameGenerator = new CustomCSharpPropertyNameGenerator(), }, + CSharpGeneratorSettings = + { + Namespace = settings.ContractsNamespace ?? settings.Namespace, + JsonLibrary = CSharpJsonLibrary.SystemTextJson, + TypeAccessModifier = settings.TypeAccessibility.ToString().ToLowerInvariant(), + ClassStyle = + settings.ImmutableRecords || + settings.CodeGeneratorSettings?.GenerateNativeRecords is true + ? CSharpClassStyle.Record + : CSharpClassStyle.Poco, + GenerateNativeRecords = + settings.ImmutableRecords || + settings.CodeGeneratorSettings?.GenerateNativeRecords is true, + } + }; + + if (settings.UseSystemTextJsonPolymorphicSerialization) + { + csharpClientGeneratorSettings.CSharpGeneratorSettings.TemplateFactory = new CustomTemplateFactory( + csharpClientGeneratorSettings.CSharpGeneratorSettings, + [ + typeof(CSharpGenerator).Assembly, + typeof(CSharpGeneratorBaseSettings).Assembly, + typeof(CustomTemplateFactory).Assembly, + ]); + } + var generator = new CustomCSharpClientGenerator( document, - new CSharpClientGeneratorSettings - { - GenerateClientClasses = false, - GenerateDtoTypes = true, - GenerateClientInterfaces = false, - GenerateExceptionClasses = false, - CodeGeneratorSettings = { PropertyNameGenerator = new CustomCSharpPropertyNameGenerator(), }, - CSharpGeneratorSettings = - { - Namespace = settings.ContractsNamespace ?? settings.Namespace, - JsonLibrary = CSharpJsonLibrary.SystemTextJson, - TypeAccessModifier = settings.TypeAccessibility.ToString().ToLowerInvariant(), - ClassStyle = - settings.ImmutableRecords || - settings.CodeGeneratorSettings?.GenerateNativeRecords is true - ? CSharpClassStyle.Record - : CSharpClassStyle.Poco, - GenerateNativeRecords = - settings.ImmutableRecords || - settings.CodeGeneratorSettings?.GenerateNativeRecords is true, - } - }); + csharpClientGeneratorSettings); MapCSharpGeneratorSettings( settings.CodeGeneratorSettings, @@ -82,4 +97,42 @@ private static void MapCSharpGeneratorSettings( settingsProperty.SetValue(destination, value); } } -} \ No newline at end of file + + /// + /// custom template factory + /// solely for the purpose of supporting UseSystemTextJsonPolymorphicSerialization + /// This class and its templates should be removed when NSwag supports this feature. + /// + private class CustomTemplateFactory : NSwag.CodeGeneration.DefaultTemplateFactory + { + /// Initializes a new instance of the class. + /// The settings. + /// The assemblies. + public CustomTemplateFactory(CodeGeneratorSettingsBase settings, Assembly[] assemblies) + : base(settings, assemblies) + { + } + + /// Tries to load an embedded Liquid template. + /// The language. + /// The template name. + /// The template. + protected override string GetEmbeddedLiquidTemplate(string language, string template) + { + template = template.TrimEnd('!'); + var assembly = GetLiquidAssembly("Refitter.Core"); + var resourceName = $"Refitter.Core.Templates.{template}.liquid"; + + var resource = assembly.GetManifestResourceStream(resourceName); + if (resource != null) + { + using (var reader = new StreamReader(resource)) + { + return reader.ReadToEnd(); + } + } + + return base.GetEmbeddedLiquidTemplate(language, template); + } + } +} diff --git a/src/Refitter.Core/Refitter.Core.csproj b/src/Refitter.Core/Refitter.Core.csproj index f192d75e..31a93102 100644 --- a/src/Refitter.Core/Refitter.Core.csproj +++ b/src/Refitter.Core/Refitter.Core.csproj @@ -23,4 +23,12 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Refitter.Core/Settings/RefitGeneratorSettings.cs b/src/Refitter.Core/Settings/RefitGeneratorSettings.cs index 1b3e4fcf..b9904ebb 100644 --- a/src/Refitter.Core/Settings/RefitGeneratorSettings.cs +++ b/src/Refitter.Core/Settings/RefitGeneratorSettings.cs @@ -1,4 +1,4 @@ -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace Refitter.Core; @@ -216,4 +216,10 @@ public class RefitGeneratorSettings /// Dependency Injection is written to a file called DependencyInjection.cs /// public bool GenerateMultipleFiles { get; set; } -} \ No newline at end of file + + /// + /// Set to true to use System.Text.Json polymorphic serialization. Default is false + /// Gets a value indicating whether to use System.Text.Json polymorphic serialization + /// + public bool UseSystemTextJsonPolymorphicSerialization { get; set; } +} diff --git a/src/Refitter.Core/Templates/Class.liquid b/src/Refitter.Core/Templates/Class.liquid new file mode 100644 index 00000000..17be521b --- /dev/null +++ b/src/Refitter.Core/Templates/Class.liquid @@ -0,0 +1,148 @@ +{%- if HasDescription -%} +/// +/// {{ Description | csharpdocs }} +/// +{%- endif -%} +{%- if HasDiscriminator -%} +{%- if UseSystemTextJson -%} +[System.Text.Json.Serialization.JsonPolymorphic(TypeDiscriminatorPropertyName = "{{ Discriminator }}", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] +{%- else -%} +[Newtonsoft.Json.JsonConverter(typeof(JsonInheritanceConverter), "{{ Discriminator }}")] +{%- endif -%} +{%- for derivedClass in DerivedClasses -%} +{%- if derivedClass.IsAbstract != true -%} +{%- if UseSystemTextJson -%} +[System.Text.Json.Serialization.JsonDerivedType(typeof({{ derivedClass.ClassName }}), typeDiscriminator: "{{ derivedClass.Discriminator }}")] +{%- endif -%} +{%- endif -%} +{%- endfor -%} +{%- endif -%} +[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "{{ ToolchainVersion }}")] +{%- if InheritsExceptionSchema -%} +{%- if UseSystemTextJson -%} +// TODO(system.text.json): What to do here? +{%- else -%} +[Newtonsoft.Json.JsonObjectAttribute] +{%- endif -%} +{%- endif -%} +{%- if IsDeprecated -%} +[System.Obsolete{% if HasDeprecatedMessage %}({{ DeprecatedMessage | literal }}){% endif %}] +{% endif -%} +{%- template Class.Annotations -%} +{{ TypeAccessModifier }} {% if IsAbstract %}abstract {% endif %}partial {% if GenerateNativeRecords %}record{% else %}class{% endif %} {{ClassName}} {%- template Class.Inheritance %} +{ +{%- if IsTuple -%} + public {{ ClassName }}({%- for tupleType in TupleTypes %}{{ tupleType }} item{{ forloop.index }}{%- if forloop.last == false %}, {% endif %}{% endfor %}) : base({%- for tupleType in TupleTypes %}item{{ forloop.index }}{%- if forloop.last == false %}, {% endif %}{% endfor %}) + { + } + +{%- endif -%} +{%- if RenderInpc or RenderPrism -%} +{%- for property in Properties -%} + private {{ property.Type }} {{ property.FieldName }}{%- if property.HasDefaultValue %} = {{ property.DefaultValue }}{% elsif GenerateNullableReferenceTypes %} = default!{%- endif %}; +{%- endfor -%} + +{%- endif -%} + {%- template Class.Constructor -%} +{%- if RenderRecord -%} + {% template Class.Constructor.Record -%} +{%- endif -%} +{%- for property in Properties -%} +{%- if property.HasDescription -%} + /// + /// {{ property.Description | csharpdocs }} + /// +{%- endif -%} +{%- if UseSystemTextJson %} + [System.Text.Json.Serialization.JsonPropertyName("{{ property.Name }}")] +{%- if property.IsStringEnumArray %} + // TODO(system.text.json): Add string enum item converter +{%- endif -%} +{%- else -%} + [Newtonsoft.Json.JsonProperty("{{ property.Name }}", Required = {{ property.JsonPropertyRequiredCode }}{% if property.IsStringEnumArray %}, ItemConverterType = typeof(Newtonsoft.Json.Converters.StringEnumConverter){% endif %})] +{%- endif -%} +{%- if property.RenderRequiredAttribute -%} + [System.ComponentModel.DataAnnotations.Required{% if property.AllowEmptyStrings %}(AllowEmptyStrings = true){% endif %}] +{%- endif -%} +{%- if property.RenderRangeAttribute -%} + [System.ComponentModel.DataAnnotations.Range({{ property.RangeMinimumValue }}, {{ property.RangeMaximumValue }})] +{%- endif -%} +{%- if property.RenderStringLengthAttribute -%} + [System.ComponentModel.DataAnnotations.StringLength({{ property.StringLengthMaximumValue }}{% if property.StringLengthMinimumValue > 0 %}, MinimumLength = {{ property.StringLengthMinimumValue }}{% endif %})] +{%- endif -%} +{%- if property.RenderMinLengthAttribute -%} + [System.ComponentModel.DataAnnotations.MinLength({{ property.MinLengthAttribute }})] +{%- endif -%} +{%- if property.RenderMaxLengthAttribute -%} + [System.ComponentModel.DataAnnotations.MaxLength({{ property.MaxLengthAttribute }})] +{%- endif -%} +{%- if property.RenderRegularExpressionAttribute -%} + [System.ComponentModel.DataAnnotations.RegularExpression(@"{{ property.RegularExpressionValue }}")] +{%- endif -%} +{%- if property.IsStringEnum -%} +{%- if UseSystemTextJson -%} + [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] +{%- else -%} + [Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))] +{%- endif -%} +{%- endif -%} +{%- if property.IsDate and UseDateFormatConverter -%} +{%- if UseSystemTextJson -%} + [System.Text.Json.Serialization.JsonConverter(typeof(DateFormatConverter))] +{%- else -%} + [Newtonsoft.Json.JsonConverter(typeof(DateFormatConverter))] +{%- endif -%} +{%- endif -%} +{%- if property.IsDeprecated -%} + [System.Obsolete{% if property.HasDeprecatedMessage %}({{ property.DeprecatedMessage | literal }}){% endif %}] +{%- endif -%} + {%- template Class.Property.Annotations -%} + public {{ property.Type }} {{ property.PropertyName }}{% if RenderInpc == false and RenderPrism == false %} { get; {% if property.HasSetter and RenderRecord == false %}set; {% elsif RenderRecord and GenerateNativeRecords %}init; {% endif %}}{% if property.HasDefaultValue and RenderRecord == false %} = {{ property.DefaultValue }};{% elsif GenerateNullableReferenceTypes and RenderRecord == false %} = default!;{% endif %} +{% else %} + { + get { return {{ property.FieldName }}; } + +{%- if property.HasSetter -%} +{%- if RenderInpc -%} + {{PropertySetterAccessModifier}}set + { + if ({{ property.FieldName }} != value) + { + {{ property.FieldName }} = value; + RaisePropertyChanged(); + } + } +{%- else -%} + {{PropertySetterAccessModifier}}set { SetProperty(ref {{ property.FieldName }}, value); } +{%- endif -%} +{%- endif -%} + } +{%- endif %} +{%- endfor -%} + +{%- if GenerateAdditionalPropertiesProperty -%} + + private System.Collections.Generic.IDictionary{% if GenerateNullableReferenceTypes %}?{% endif %} _additionalProperties; + +{%- if UseSystemTextJson -%} + [System.Text.Json.Serialization.JsonExtensionData] +{%- else -%} + [Newtonsoft.Json.JsonExtensionData] +{%- endif -%} + public System.Collections.Generic.IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new System.Collections.Generic.Dictionary()); } + {{PropertySetterAccessModifier}}set { _additionalProperties = value; } + } + +{%- endif -%} +{%- if GenerateJsonMethods -%} + {% template Class.ToJson %} + {% template Class.FromJson %} + +{%- endif -%} +{%- if RenderInpc -%} + {% template Class.Inpc %} +{%- endif -%} + {% template Class.Body %} +} diff --git a/src/Refitter.Core/Templates/JsonInheritanceAttribute.liquid b/src/Refitter.Core/Templates/JsonInheritanceAttribute.liquid new file mode 100644 index 00000000..e69de29b diff --git a/src/Refitter.Core/Templates/JsonInheritanceConverter.liquid b/src/Refitter.Core/Templates/JsonInheritanceConverter.liquid new file mode 100644 index 00000000..e69de29b diff --git a/src/Refitter.Tests/Examples/UseSystemTextJsonPolymorphicSerializationTests.cs b/src/Refitter.Tests/Examples/UseSystemTextJsonPolymorphicSerializationTests.cs new file mode 100644 index 00000000..4b0fa4ac --- /dev/null +++ b/src/Refitter.Tests/Examples/UseSystemTextJsonPolymorphicSerializationTests.cs @@ -0,0 +1,247 @@ +using FluentAssertions; + +using Refitter.Core; +using Refitter.Tests.Build; + +using Xunit; + +namespace Refitter.Tests.Examples; + +public class UseSystemTextJsonPolymorphicSerializationTests +{ + private const string OpenApiSpec = @" +openapi: 3.0.1 +paths: + /v1/Warehouses: + post: + tags: + - Warehouses + operationId: CreateWarehouse + parameters: + - name: 'token' + in: 'query' + description: 'Some Token' + required: false + type: 'string' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Warehouse' + responses: + '201': + description: Created + headers: + X-Rate-Limit: + type: 'integer' + format: 'int32' + description: 'calls per hour allowed by the user' + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/WarehouseResponse' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + '500': + description: Server Error +components: + schemas: + Metadata: + type: object + properties: + createdAt: + type: string + format: date-time + createdBy: + type: string + nullable: true + lastModifiedAt: + type: string + format: date-time + lastModifiedBy: + type: string + nullable: true + additionalProperties: false + SomeComponent: + required: + - $type + type: object + allOf: + - $ref: '#/components/schemas/Component' + properties: + $type: + type: string + typeId: + type: integer + format: int64 + additionalProperties: false + discriminator: + propertyName: $type + SomeComponentState: + enum: + - Active + - Inactive + - Blocked + - Deleted + type: string + SomeComponentType: + type: object + allOf: + - $ref: '#/components/schemas/Component' + properties: + state: + $ref: '#/components/schemas/SomeComponentState' + isBaseRole: + type: boolean + name: + type: string + nullable: true + numberingId: + type: string + nullable: true + additionalProperties: false + Component: + type: object + properties: + id: + type: integer + format: int64 + metadata: + $ref: '#/components/schemas/Metadata' + additionalProperties: false + LoadingAddress: + type: object + allOf: + - $ref: '#/components/schemas/SomeComponent' + properties: + info: + type: string + nullable: true + additionalProperties: false + Warehouse: + type: object + allOf: + - $ref: '#/components/schemas/SomeComponent' + properties: + info: + type: string + nullable: true + additionalProperties: false + WarehouseResponse: + type: object + allOf: + - $ref: '#/components/schemas/SomeComponent' + properties: + info: + type: string + nullable: true + additionalProperties: false + UserComponent: + type: object + allOf: + - $ref: '#/components/schemas/SomeComponent' + properties: + info: + type: string + nullable: true + additionalProperties: false + UserComponent2: + type: object + allOf: + - $ref: '#/components/schemas/UserComponent' + properties: + info2: + type: string + nullable: true + additionalProperties: false + ProblemDetails: + required: + - $type + type: object + properties: + $type: + type: string + type: + type: string + nullable: true + title: + type: string + nullable: true + status: + type: integer + format: int32 + nullable: true + detail: + type: string + nullable: true + instance: + type: string + nullable: true + additionalProperties: { } + discriminator: + propertyName: $type +"; + + [Fact] + public async Task Can_Generate_Code() + { + string generateCode = await GenerateCode(); + generateCode.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task Use_SystemTextJson_Polymorphic_Serialization() + { + string generateCode = await GenerateCode(); + + generateCode.Should().NotContain("JsonInheritanceConverter"); + generateCode.Should().NotContain("JsonInheritanceAttribute"); + + generateCode.Should().Contain("[JsonPolymorphic(TypeDiscriminatorPropertyName = \"$type\", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]"); + generateCode.Should().Contain("[JsonDerivedType(typeof(Warehouse), typeDiscriminator: \"Warehouse\")]"); + generateCode.Should().Contain("[JsonDerivedType(typeof(WarehouseResponse), typeDiscriminator: \"WarehouseResponse\")]"); + generateCode.Should().Contain("[JsonDerivedType(typeof(LoadingAddress), typeDiscriminator: \"LoadingAddress\")]"); + generateCode.Should().Contain("[JsonDerivedType(typeof(UserComponent), typeDiscriminator: \"UserComponent\")]"); + generateCode.Should().Contain("[JsonDerivedType(typeof(UserComponent2), typeDiscriminator: \"UserComponent2\")]"); + } + + [Fact] + public async Task Can_Build_Generated_Code() + { + string generateCode = await GenerateCode(); + BuildHelper + .BuildCSharp(generateCode) + .Should() + .BeTrue(); + } + + private static async Task GenerateCode() + { + var swaggerFile = await CreateSwaggerFile(OpenApiSpec); + var settings = new RefitGeneratorSettings + { + OpenApiPath = swaggerFile, + UseSystemTextJsonPolymorphicSerialization = true + }; + + var sut = await RefitGenerator.CreateAsync(settings); + var generateCode = sut.Generate(); + return generateCode; + } + + private static async Task CreateSwaggerFile(string contents) + { + var filename = $"{Guid.NewGuid()}.yml"; + var folder = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(folder); + var swaggerFile = Path.Combine(folder, filename); + await File.WriteAllTextAsync(swaggerFile, contents); + return swaggerFile; + } +} diff --git a/src/Refitter/GenerateCommand.cs b/src/Refitter/GenerateCommand.cs index e8b0e5af..438cc1a1 100644 --- a/src/Refitter/GenerateCommand.cs +++ b/src/Refitter/GenerateCommand.cs @@ -131,7 +131,8 @@ private static RefitGeneratorSettings CreateRefitGeneratorSettings(Settings sett UseDynamicQuerystringParameters = settings.UseDynamicQuerystringParameters, GenerateMultipleFiles = settings.GenerateMultipleFiles || !string.IsNullOrWhiteSpace(settings.ContractsOutputPath), ContractsOutputFolder = settings.ContractsOutputPath ?? settings.OutputPath, - ContractsNamespace = settings.ContractsNamespace + ContractsNamespace = settings.ContractsNamespace, + UseSystemTextJsonPolymorphicSerialization = settings.UseSystemTextJsonPolymorphicSerialization, }; } diff --git a/src/Refitter/Settings.cs b/src/Refitter/Settings.cs index e32b0643..fba2badc 100644 --- a/src/Refitter/Settings.cs +++ b/src/Refitter/Settings.cs @@ -210,4 +210,9 @@ The NSwag IOperationNameGenerator implementation to use. [CommandOption("--use-dynamic-querystring-parameters")] [DefaultValue(false)] public bool UseDynamicQuerystringParameters { get; set; } -} \ No newline at end of file + + [Description("Set to true to use System.Text.Json polymorphic serialization.")] + [CommandOption("--use-system-text-json-polymorphic-serialization")] + [DefaultValue(false)] + public bool UseSystemTextJsonPolymorphicSerialization { get; set; } +} From cb410eb4c52d49ea888abddec03e3f53c6b4278a Mon Sep 17 00:00:00 2001 From: Ryan Heath Date: Tue, 10 Sep 2024 01:36:57 +0200 Subject: [PATCH 2/5] removed unneed file inclusion --- src/Refitter.Core/Refitter.Core.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Refitter.Core/Refitter.Core.csproj b/src/Refitter.Core/Refitter.Core.csproj index 31a93102..96c5f7cc 100644 --- a/src/Refitter.Core/Refitter.Core.csproj +++ b/src/Refitter.Core/Refitter.Core.csproj @@ -27,8 +27,4 @@ - - - - \ No newline at end of file From 1a77a023a0aadb26ee66de869d71f15e7f1f21b2 Mon Sep 17 00:00:00 2001 From: Ryan Heath Date: Tue, 10 Sep 2024 10:57:52 +0200 Subject: [PATCH 3/5] PR review --- src/Refitter.Core/CSharpClientGeneratorFactory.cs | 4 ++-- src/Refitter.Core/Refitter.Core.csproj | 2 +- src/Refitter.Core/Settings/RefitGeneratorSettings.cs | 2 +- .../Refitter.SourceGenerator.csproj | 4 ++++ ...lizationTests.cs => UsePolymorphicSerializationTests.cs} | 6 +++--- src/Refitter/GenerateCommand.cs | 2 +- src/Refitter/Settings.cs | 4 ++-- 7 files changed, 14 insertions(+), 10 deletions(-) rename src/Refitter.Tests/Examples/{UseSystemTextJsonPolymorphicSerializationTests.cs => UsePolymorphicSerializationTests.cs} (97%) diff --git a/src/Refitter.Core/CSharpClientGeneratorFactory.cs b/src/Refitter.Core/CSharpClientGeneratorFactory.cs index 6e394d63..a5fb0f14 100644 --- a/src/Refitter.Core/CSharpClientGeneratorFactory.cs +++ b/src/Refitter.Core/CSharpClientGeneratorFactory.cs @@ -42,7 +42,7 @@ public CustomCSharpClientGenerator Create() } }; - if (settings.UseSystemTextJsonPolymorphicSerialization) + if (settings.UsePolymorphicSerialization) { csharpClientGeneratorSettings.CSharpGeneratorSettings.TemplateFactory = new CustomTemplateFactory( csharpClientGeneratorSettings.CSharpGeneratorSettings, @@ -100,7 +100,7 @@ private static void MapCSharpGeneratorSettings( /// /// custom template factory - /// solely for the purpose of supporting UseSystemTextJsonPolymorphicSerialization + /// solely for the purpose of supporting UsePolymorphicSerialization /// This class and its templates should be removed when NSwag supports this feature. /// private class CustomTemplateFactory : NSwag.CodeGeneration.DefaultTemplateFactory diff --git a/src/Refitter.Core/Refitter.Core.csproj b/src/Refitter.Core/Refitter.Core.csproj index 96c5f7cc..d829dbd9 100644 --- a/src/Refitter.Core/Refitter.Core.csproj +++ b/src/Refitter.Core/Refitter.Core.csproj @@ -24,7 +24,7 @@ - + \ No newline at end of file diff --git a/src/Refitter.Core/Settings/RefitGeneratorSettings.cs b/src/Refitter.Core/Settings/RefitGeneratorSettings.cs index b9904ebb..12b5691d 100644 --- a/src/Refitter.Core/Settings/RefitGeneratorSettings.cs +++ b/src/Refitter.Core/Settings/RefitGeneratorSettings.cs @@ -221,5 +221,5 @@ public class RefitGeneratorSettings /// Set to true to use System.Text.Json polymorphic serialization. Default is false /// Gets a value indicating whether to use System.Text.Json polymorphic serialization /// - public bool UseSystemTextJsonPolymorphicSerialization { get; set; } + public bool UsePolymorphicSerialization { get; set; } } diff --git a/src/Refitter.SourceGenerator/Refitter.SourceGenerator.csproj b/src/Refitter.SourceGenerator/Refitter.SourceGenerator.csproj index d5f24fd8..d3260c84 100644 --- a/src/Refitter.SourceGenerator/Refitter.SourceGenerator.csproj +++ b/src/Refitter.SourceGenerator/Refitter.SourceGenerator.csproj @@ -29,4 +29,8 @@ + + + + \ No newline at end of file diff --git a/src/Refitter.Tests/Examples/UseSystemTextJsonPolymorphicSerializationTests.cs b/src/Refitter.Tests/Examples/UsePolymorphicSerializationTests.cs similarity index 97% rename from src/Refitter.Tests/Examples/UseSystemTextJsonPolymorphicSerializationTests.cs rename to src/Refitter.Tests/Examples/UsePolymorphicSerializationTests.cs index 4b0fa4ac..bca8c376 100644 --- a/src/Refitter.Tests/Examples/UseSystemTextJsonPolymorphicSerializationTests.cs +++ b/src/Refitter.Tests/Examples/UsePolymorphicSerializationTests.cs @@ -7,7 +7,7 @@ namespace Refitter.Tests.Examples; -public class UseSystemTextJsonPolymorphicSerializationTests +public class UsePolymorphicSerializationTests { private const string OpenApiSpec = @" openapi: 3.0.1 @@ -196,7 +196,7 @@ public async Task Can_Generate_Code() } [Fact] - public async Task Use_SystemTextJson_Polymorphic_Serialization() + public async Task Use_Polymorphic_Serialization() { string generateCode = await GenerateCode(); @@ -227,7 +227,7 @@ private static async Task GenerateCode() var settings = new RefitGeneratorSettings { OpenApiPath = swaggerFile, - UseSystemTextJsonPolymorphicSerialization = true + UsePolymorphicSerialization = true }; var sut = await RefitGenerator.CreateAsync(settings); diff --git a/src/Refitter/GenerateCommand.cs b/src/Refitter/GenerateCommand.cs index 438cc1a1..04475500 100644 --- a/src/Refitter/GenerateCommand.cs +++ b/src/Refitter/GenerateCommand.cs @@ -132,7 +132,7 @@ private static RefitGeneratorSettings CreateRefitGeneratorSettings(Settings sett GenerateMultipleFiles = settings.GenerateMultipleFiles || !string.IsNullOrWhiteSpace(settings.ContractsOutputPath), ContractsOutputFolder = settings.ContractsOutputPath ?? settings.OutputPath, ContractsNamespace = settings.ContractsNamespace, - UseSystemTextJsonPolymorphicSerialization = settings.UseSystemTextJsonPolymorphicSerialization, + UsePolymorphicSerialization = settings.UsePolymorphicSerialization, }; } diff --git a/src/Refitter/Settings.cs b/src/Refitter/Settings.cs index fba2badc..20f646e6 100644 --- a/src/Refitter/Settings.cs +++ b/src/Refitter/Settings.cs @@ -212,7 +212,7 @@ The NSwag IOperationNameGenerator implementation to use. public bool UseDynamicQuerystringParameters { get; set; } [Description("Set to true to use System.Text.Json polymorphic serialization.")] - [CommandOption("--use-system-text-json-polymorphic-serialization")] + [CommandOption("--use-polymorphic-serialization")] [DefaultValue(false)] - public bool UseSystemTextJsonPolymorphicSerialization { get; set; } + public bool UsePolymorphicSerialization { get; set; } } From d4460bca41eef395b0182b9f6d0e1e8f12509dd6 Mon Sep 17 00:00:00 2001 From: Ryan Heath Date: Tue, 10 Sep 2024 14:22:11 +0200 Subject: [PATCH 4/5] added support for SourceGenerator --- .../CSharpClientGeneratorFactory.cs | 4 +- .../UseJsonInheritanceConverter.g.cs | 375 ++++++++++++++++++ .../UsePolymorphicSerialization.g.cs | 375 ++++++++++++++++++ .../UseJsonInheritanceConverter.refitter | 5 + .../UsePolymorphicSerialization.refitter | 5 + .../Refitter.SourceGenerator.Tests.csproj | 1 + .../UsePolymorphicSerialization.yaml | 176 ++++++++ .../UseJsonInheritanceConverterTests.cs | 25 ++ .../UsePolymorphicSerializationTests.cs | 27 ++ .../Refitter.SourceGenerator.csproj | 2 +- 10 files changed, 992 insertions(+), 3 deletions(-) create mode 100644 src/Refitter.SourceGenerator.Tests/AdditionalFiles/Generated/UseJsonInheritanceConverter.g.cs create mode 100644 src/Refitter.SourceGenerator.Tests/AdditionalFiles/Generated/UsePolymorphicSerialization.g.cs create mode 100644 src/Refitter.SourceGenerator.Tests/AdditionalFiles/UseJsonInheritanceConverter.refitter create mode 100644 src/Refitter.SourceGenerator.Tests/AdditionalFiles/UsePolymorphicSerialization.refitter create mode 100644 src/Refitter.SourceGenerator.Tests/Resources/UsePolymorphicSerialization.yaml create mode 100644 src/Refitter.SourceGenerator.Tests/UseJsonInheritanceConverterTests.cs create mode 100644 src/Refitter.SourceGenerator.Tests/UsePolymorphicSerializationTests.cs diff --git a/src/Refitter.Core/CSharpClientGeneratorFactory.cs b/src/Refitter.Core/CSharpClientGeneratorFactory.cs index a5fb0f14..e798d48a 100644 --- a/src/Refitter.Core/CSharpClientGeneratorFactory.cs +++ b/src/Refitter.Core/CSharpClientGeneratorFactory.cs @@ -120,8 +120,8 @@ public CustomTemplateFactory(CodeGeneratorSettingsBase settings, Assembly[] asse protected override string GetEmbeddedLiquidTemplate(string language, string template) { template = template.TrimEnd('!'); - var assembly = GetLiquidAssembly("Refitter.Core"); - var resourceName = $"Refitter.Core.Templates.{template}.liquid"; + var assembly = Assembly.GetExecutingAssembly(); // this code is running in Refitter.Core and Refitter.SourceGenerator + var resourceName = $"{assembly.GetName().Name}.Templates.{template}.liquid"; var resource = assembly.GetManifestResourceStream(resourceName); if (resource != null) diff --git a/src/Refitter.SourceGenerator.Tests/AdditionalFiles/Generated/UseJsonInheritanceConverter.g.cs b/src/Refitter.SourceGenerator.Tests/AdditionalFiles/Generated/UseJsonInheritanceConverter.g.cs new file mode 100644 index 00000000..b6dbd2ce --- /dev/null +++ b/src/Refitter.SourceGenerator.Tests/AdditionalFiles/Generated/UseJsonInheritanceConverter.g.cs @@ -0,0 +1,375 @@ +// +// This code was generated by Refitter. +// + + +using Refit; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +#nullable enable annotations + +namespace Refitter.Tests.UseJsonInheritanceConverter +{ + [System.CodeDom.Compiler.GeneratedCode("Refitter", "1.0.0.0")] + public partial interface IApiClient + { + /// Some Token + /// Created + /// + /// Thrown when the request returns a non-success status code: + /// + /// + /// Status + /// Description + /// + /// + /// 400 + /// Bad Request + /// + /// + /// 500 + /// Server Error + /// + /// + /// + [Headers("Accept: application/json")] + [Post("/v1/Warehouses")] + Task> CreateWarehouse([Query] string token, [Body] Warehouse body); + + + } + +} + + +//---------------------- +// +// Generated using the NSwag toolchain v14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) +// +//---------------------- + +#pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended." +#pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword." +#pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?' +#pragma warning disable 612 // Disable "CS0612 '...' is obsolete" +#pragma warning disable 649 // Disable "CS0649 Field is never assigned to, and will always have its default value null" +#pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ... +#pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..." +#pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'" +#pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant" +#pragma warning disable 8603 // Disable "CS8603 Possible null reference return" +#pragma warning disable 8604 // Disable "CS8604 Possible null reference argument for parameter" +#pragma warning disable 8625 // Disable "CS8625 Cannot convert null literal to non-nullable reference type" +#pragma warning disable 8765 // Disable "CS8765 Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes)." + +namespace Refitter.Tests.UseJsonInheritanceConverter +{ + using System = global::System; + + + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Metadata + { + + [JsonPropertyName("createdAt")] + public System.DateTimeOffset CreatedAt { get; set; } + + [JsonPropertyName("createdBy")] + public string CreatedBy { get; set; } + + [JsonPropertyName("lastModifiedAt")] + public System.DateTimeOffset LastModifiedAt { get; set; } + + [JsonPropertyName("lastModifiedBy")] + public string LastModifiedBy { get; set; } + + } + + [JsonInheritanceConverter(typeof(SomeComponent), "$type")] + [JsonInheritanceAttribute("Warehouse", typeof(Warehouse))] + [JsonInheritanceAttribute("WarehouseResponse", typeof(WarehouseResponse))] + [JsonInheritanceAttribute("LoadingAddress", typeof(LoadingAddress))] + [JsonInheritanceAttribute("UserComponent", typeof(UserComponent))] + [JsonInheritanceAttribute("UserComponent2", typeof(UserComponent2))] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class SomeComponent : Component + { + + [JsonPropertyName("typeId")] + public long TypeId { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public enum SomeComponentState + { + + [System.Runtime.Serialization.EnumMember(Value = @"Active")] + Active = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"Inactive")] + Inactive = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"Blocked")] + Blocked = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"Deleted")] + Deleted = 3, + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class SomeComponentType : Component + { + + [JsonPropertyName("state")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public SomeComponentState State { get; set; } + + [JsonPropertyName("isBaseRole")] + public bool IsBaseRole { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("numberingId")] + public string NumberingId { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Component + { + + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("metadata")] + public Metadata Metadata { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class LoadingAddress : SomeComponent + { + + [JsonPropertyName("info")] + public string Info { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Warehouse : SomeComponent + { + + [JsonPropertyName("info")] + public string Info { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class WarehouseResponse : SomeComponent + { + + [JsonPropertyName("info")] + public string Info { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class UserComponent : SomeComponent + { + + [JsonPropertyName("info")] + public string Info { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class UserComponent2 : UserComponent + { + + [JsonPropertyName("info2")] + public string Info2 { get; set; } + + } + + [JsonInheritanceConverter(typeof(ProblemDetails), "$type")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ProblemDetails + { + + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("status")] + public int? Status { get; set; } + + [JsonPropertyName("detail")] + public string Detail { get; set; } + + [JsonPropertyName("instance")] + public string Instance { get; set; } + + private IDictionary _additionalProperties; + + [JsonExtensionData] + public IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Interface, AllowMultiple = true)] + internal class JsonInheritanceAttribute : System.Attribute + { + public JsonInheritanceAttribute(string key, System.Type type) + { + Key = key; + Type = type; + } + + public string Key { get; } + + public System.Type Type { get; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + internal class JsonInheritanceConverterAttribute : JsonConverterAttribute + { + public string DiscriminatorName { get; } + + public JsonInheritanceConverterAttribute(System.Type baseType, string discriminatorName = "discriminator") + : base(typeof(JsonInheritanceConverter<>).MakeGenericType(baseType)) + { + DiscriminatorName = discriminatorName; + } + } + + public class JsonInheritanceConverter : JsonConverter + { + private readonly string _discriminatorName; + + public JsonInheritanceConverter() + { + var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(typeof(TBase)); + _discriminatorName = attribute?.DiscriminatorName ?? "discriminator"; + } + + public JsonInheritanceConverter(string discriminatorName) + { + _discriminatorName = discriminatorName; + } + + public string DiscriminatorName { get { return _discriminatorName; } } + + public override TBase Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) + { + var document = System.Text.Json.JsonDocument.ParseValue(ref reader); + var hasDiscriminator = document.RootElement.TryGetProperty(_discriminatorName, out var discriminator); + var subtype = GetDiscriminatorType(document.RootElement, typeToConvert, hasDiscriminator ? discriminator.GetString() : null); + + var bufferWriter = new System.IO.MemoryStream(); + using (var writer = new System.Text.Json.Utf8JsonWriter(bufferWriter)) + { + document.RootElement.WriteTo(writer); + } + + return (TBase)System.Text.Json.JsonSerializer.Deserialize(bufferWriter.ToArray(), subtype, options); + } + + public override void Write(System.Text.Json.Utf8JsonWriter writer, TBase value, System.Text.Json.JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteString(_discriminatorName, GetDiscriminatorValue(value.GetType())); + + var bytes = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes((object)value, options); + var document = System.Text.Json.JsonDocument.Parse(bytes); + foreach (var property in document.RootElement.EnumerateObject()) + { + property.WriteTo(writer); + } + + writer.WriteEndObject(); + } + + public string GetDiscriminatorValue(System.Type type) + { + var jsonInheritanceAttributeDiscriminator = GetSubtypeDiscriminator(type); + if (jsonInheritanceAttributeDiscriminator != null) + { + return jsonInheritanceAttributeDiscriminator; + } + + return type.Name; + } + + protected System.Type GetDiscriminatorType(System.Text.Json.JsonElement jObject, System.Type objectType, string discriminatorValue) + { + var jsonInheritanceAttributeSubtype = GetObjectSubtype(objectType, discriminatorValue); + if (jsonInheritanceAttributeSubtype != null) + { + return jsonInheritanceAttributeSubtype; + } + + if (objectType.Name == discriminatorValue) + { + return objectType; + } + + var typeName = objectType.Namespace + "." + discriminatorValue; + var subtype = System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType).Assembly.GetType(typeName); + if (subtype != null) + { + return subtype; + } + + throw new System.InvalidOperationException("Could not find subtype of '" + objectType.Name + "' with discriminator '" + discriminatorValue + "'."); + } + + private System.Type GetObjectSubtype(System.Type objectType, string discriminator) + { + foreach (var attribute in System.Reflection.CustomAttributeExtensions.GetCustomAttributes(System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType), true)) + { + if (attribute.Key == discriminator) + return attribute.Type; + } + + return objectType; + } + + private string GetSubtypeDiscriminator(System.Type objectType) + { + foreach (var attribute in System.Reflection.CustomAttributeExtensions.GetCustomAttributes(System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType), true)) + { + if (attribute.Type == objectType) + return attribute.Key; + } + + return objectType.Name; + } + } + + +} + +#pragma warning restore 108 +#pragma warning restore 114 +#pragma warning restore 472 +#pragma warning restore 612 +#pragma warning restore 1573 +#pragma warning restore 1591 +#pragma warning restore 8073 +#pragma warning restore 3016 +#pragma warning restore 8603 +#pragma warning restore 8604 +#pragma warning restore 8625 \ No newline at end of file diff --git a/src/Refitter.SourceGenerator.Tests/AdditionalFiles/Generated/UsePolymorphicSerialization.g.cs b/src/Refitter.SourceGenerator.Tests/AdditionalFiles/Generated/UsePolymorphicSerialization.g.cs new file mode 100644 index 00000000..72a4e86f --- /dev/null +++ b/src/Refitter.SourceGenerator.Tests/AdditionalFiles/Generated/UsePolymorphicSerialization.g.cs @@ -0,0 +1,375 @@ +// +// This code was generated by Refitter. +// + + +using Refit; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +#nullable enable annotations + +namespace Refitter.Tests.UsePolymorphicSerialization +{ + [System.CodeDom.Compiler.GeneratedCode("Refitter", "1.0.0.0")] + public partial interface IApiClient + { + /// Some Token + /// Created + /// + /// Thrown when the request returns a non-success status code: + /// + /// + /// Status + /// Description + /// + /// + /// 400 + /// Bad Request + /// + /// + /// 500 + /// Server Error + /// + /// + /// + [Headers("Accept: application/json")] + [Post("/v1/Warehouses")] + Task> CreateWarehouse([Query] string token, [Body] Warehouse body); + + + } + +} + + +//---------------------- +// +// Generated using the NSwag toolchain v14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0)) (http://NSwag.org) +// +//---------------------- + +#pragma warning disable 108 // Disable "CS0108 '{derivedDto}.ToJson()' hides inherited member '{dtoBase}.ToJson()'. Use the new keyword if hiding was intended." +#pragma warning disable 114 // Disable "CS0114 '{derivedDto}.RaisePropertyChanged(String)' hides inherited member 'dtoBase.RaisePropertyChanged(String)'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword." +#pragma warning disable 472 // Disable "CS0472 The result of the expression is always 'false' since a value of type 'Int32' is never equal to 'null' of type 'Int32?' +#pragma warning disable 612 // Disable "CS0612 '...' is obsolete" +#pragma warning disable 649 // Disable "CS0649 Field is never assigned to, and will always have its default value null" +#pragma warning disable 1573 // Disable "CS1573 Parameter '...' has no matching param tag in the XML comment for ... +#pragma warning disable 1591 // Disable "CS1591 Missing XML comment for publicly visible type or member ..." +#pragma warning disable 8073 // Disable "CS8073 The result of the expression is always 'false' since a value of type 'T' is never equal to 'null' of type 'T?'" +#pragma warning disable 3016 // Disable "CS3016 Arrays as attribute arguments is not CLS-compliant" +#pragma warning disable 8603 // Disable "CS8603 Possible null reference return" +#pragma warning disable 8604 // Disable "CS8604 Possible null reference argument for parameter" +#pragma warning disable 8625 // Disable "CS8625 Cannot convert null literal to non-nullable reference type" +#pragma warning disable 8765 // Disable "CS8765 Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes)." + +namespace Refitter.Tests.UsePolymorphicSerialization +{ + using System = global::System; + + + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Metadata + { + + [JsonPropertyName("createdAt")] + public System.DateTimeOffset CreatedAt { get; set; } + + [JsonPropertyName("createdBy")] + public string CreatedBy { get; set; } + + [JsonPropertyName("lastModifiedAt")] + public System.DateTimeOffset LastModifiedAt { get; set; } + + [JsonPropertyName("lastModifiedBy")] + public string LastModifiedBy { get; set; } + + } + + [JsonInheritanceConverter(typeof(SomeComponent), "$type")] + [JsonInheritanceAttribute("Warehouse", typeof(Warehouse))] + [JsonInheritanceAttribute("WarehouseResponse", typeof(WarehouseResponse))] + [JsonInheritanceAttribute("LoadingAddress", typeof(LoadingAddress))] + [JsonInheritanceAttribute("UserComponent", typeof(UserComponent))] + [JsonInheritanceAttribute("UserComponent2", typeof(UserComponent2))] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class SomeComponent : Component + { + + [JsonPropertyName("typeId")] + public long TypeId { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public enum SomeComponentState + { + + [System.Runtime.Serialization.EnumMember(Value = @"Active")] + Active = 0, + + [System.Runtime.Serialization.EnumMember(Value = @"Inactive")] + Inactive = 1, + + [System.Runtime.Serialization.EnumMember(Value = @"Blocked")] + Blocked = 2, + + [System.Runtime.Serialization.EnumMember(Value = @"Deleted")] + Deleted = 3, + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class SomeComponentType : Component + { + + [JsonPropertyName("state")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public SomeComponentState State { get; set; } + + [JsonPropertyName("isBaseRole")] + public bool IsBaseRole { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("numberingId")] + public string NumberingId { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Component + { + + [JsonPropertyName("id")] + public long Id { get; set; } + + [JsonPropertyName("metadata")] + public Metadata Metadata { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class LoadingAddress : SomeComponent + { + + [JsonPropertyName("info")] + public string Info { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class Warehouse : SomeComponent + { + + [JsonPropertyName("info")] + public string Info { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class WarehouseResponse : SomeComponent + { + + [JsonPropertyName("info")] + public string Info { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class UserComponent : SomeComponent + { + + [JsonPropertyName("info")] + public string Info { get; set; } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class UserComponent2 : UserComponent + { + + [JsonPropertyName("info2")] + public string Info2 { get; set; } + + } + + [JsonInheritanceConverter(typeof(ProblemDetails), "$type")] + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + public partial class ProblemDetails + { + + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("title")] + public string Title { get; set; } + + [JsonPropertyName("status")] + public int? Status { get; set; } + + [JsonPropertyName("detail")] + public string Detail { get; set; } + + [JsonPropertyName("instance")] + public string Instance { get; set; } + + private IDictionary _additionalProperties; + + [JsonExtensionData] + public IDictionary AdditionalProperties + { + get { return _additionalProperties ?? (_additionalProperties = new Dictionary()); } + set { _additionalProperties = value; } + } + + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Interface, AllowMultiple = true)] + internal class JsonInheritanceAttribute : System.Attribute + { + public JsonInheritanceAttribute(string key, System.Type type) + { + Key = key; + Type = type; + } + + public string Key { get; } + + public System.Type Type { get; } + } + + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] + internal class JsonInheritanceConverterAttribute : JsonConverterAttribute + { + public string DiscriminatorName { get; } + + public JsonInheritanceConverterAttribute(System.Type baseType, string discriminatorName = "discriminator") + : base(typeof(JsonInheritanceConverter<>).MakeGenericType(baseType)) + { + DiscriminatorName = discriminatorName; + } + } + + public class JsonInheritanceConverter : JsonConverter + { + private readonly string _discriminatorName; + + public JsonInheritanceConverter() + { + var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(typeof(TBase)); + _discriminatorName = attribute?.DiscriminatorName ?? "discriminator"; + } + + public JsonInheritanceConverter(string discriminatorName) + { + _discriminatorName = discriminatorName; + } + + public string DiscriminatorName { get { return _discriminatorName; } } + + public override TBase Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) + { + var document = System.Text.Json.JsonDocument.ParseValue(ref reader); + var hasDiscriminator = document.RootElement.TryGetProperty(_discriminatorName, out var discriminator); + var subtype = GetDiscriminatorType(document.RootElement, typeToConvert, hasDiscriminator ? discriminator.GetString() : null); + + var bufferWriter = new System.IO.MemoryStream(); + using (var writer = new System.Text.Json.Utf8JsonWriter(bufferWriter)) + { + document.RootElement.WriteTo(writer); + } + + return (TBase)System.Text.Json.JsonSerializer.Deserialize(bufferWriter.ToArray(), subtype, options); + } + + public override void Write(System.Text.Json.Utf8JsonWriter writer, TBase value, System.Text.Json.JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteString(_discriminatorName, GetDiscriminatorValue(value.GetType())); + + var bytes = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes((object)value, options); + var document = System.Text.Json.JsonDocument.Parse(bytes); + foreach (var property in document.RootElement.EnumerateObject()) + { + property.WriteTo(writer); + } + + writer.WriteEndObject(); + } + + public string GetDiscriminatorValue(System.Type type) + { + var jsonInheritanceAttributeDiscriminator = GetSubtypeDiscriminator(type); + if (jsonInheritanceAttributeDiscriminator != null) + { + return jsonInheritanceAttributeDiscriminator; + } + + return type.Name; + } + + protected System.Type GetDiscriminatorType(System.Text.Json.JsonElement jObject, System.Type objectType, string discriminatorValue) + { + var jsonInheritanceAttributeSubtype = GetObjectSubtype(objectType, discriminatorValue); + if (jsonInheritanceAttributeSubtype != null) + { + return jsonInheritanceAttributeSubtype; + } + + if (objectType.Name == discriminatorValue) + { + return objectType; + } + + var typeName = objectType.Namespace + "." + discriminatorValue; + var subtype = System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType).Assembly.GetType(typeName); + if (subtype != null) + { + return subtype; + } + + throw new System.InvalidOperationException("Could not find subtype of '" + objectType.Name + "' with discriminator '" + discriminatorValue + "'."); + } + + private System.Type GetObjectSubtype(System.Type objectType, string discriminator) + { + foreach (var attribute in System.Reflection.CustomAttributeExtensions.GetCustomAttributes(System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType), true)) + { + if (attribute.Key == discriminator) + return attribute.Type; + } + + return objectType; + } + + private string GetSubtypeDiscriminator(System.Type objectType) + { + foreach (var attribute in System.Reflection.CustomAttributeExtensions.GetCustomAttributes(System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType), true)) + { + if (attribute.Type == objectType) + return attribute.Key; + } + + return objectType.Name; + } + } + + +} + +#pragma warning restore 108 +#pragma warning restore 114 +#pragma warning restore 472 +#pragma warning restore 612 +#pragma warning restore 1573 +#pragma warning restore 1591 +#pragma warning restore 8073 +#pragma warning restore 3016 +#pragma warning restore 8603 +#pragma warning restore 8604 +#pragma warning restore 8625 \ No newline at end of file diff --git a/src/Refitter.SourceGenerator.Tests/AdditionalFiles/UseJsonInheritanceConverter.refitter b/src/Refitter.SourceGenerator.Tests/AdditionalFiles/UseJsonInheritanceConverter.refitter new file mode 100644 index 00000000..aceab711 --- /dev/null +++ b/src/Refitter.SourceGenerator.Tests/AdditionalFiles/UseJsonInheritanceConverter.refitter @@ -0,0 +1,5 @@ +{ + "openApiPath": "../Resources/UsePolymorphicSerialization.yaml", + "namespace": "Refitter.Tests.UseJsonInheritanceConverter", + "usePolymorphicSerialization": false +} diff --git a/src/Refitter.SourceGenerator.Tests/AdditionalFiles/UsePolymorphicSerialization.refitter b/src/Refitter.SourceGenerator.Tests/AdditionalFiles/UsePolymorphicSerialization.refitter new file mode 100644 index 00000000..33a715a5 --- /dev/null +++ b/src/Refitter.SourceGenerator.Tests/AdditionalFiles/UsePolymorphicSerialization.refitter @@ -0,0 +1,5 @@ +{ + "openApiPath": "../Resources/UsePolymorphicSerialization.yaml", + "namespace": "Refitter.Tests.UsePolymorphicSerialization", + "usePolymorphicSerialization": true +} diff --git a/src/Refitter.SourceGenerator.Tests/Refitter.SourceGenerator.Tests.csproj b/src/Refitter.SourceGenerator.Tests/Refitter.SourceGenerator.Tests.csproj index 99ded392..3c342220 100644 --- a/src/Refitter.SourceGenerator.Tests/Refitter.SourceGenerator.Tests.csproj +++ b/src/Refitter.SourceGenerator.Tests/Refitter.SourceGenerator.Tests.csproj @@ -25,6 +25,7 @@ + diff --git a/src/Refitter.SourceGenerator.Tests/Resources/UsePolymorphicSerialization.yaml b/src/Refitter.SourceGenerator.Tests/Resources/UsePolymorphicSerialization.yaml new file mode 100644 index 00000000..6647cab6 --- /dev/null +++ b/src/Refitter.SourceGenerator.Tests/Resources/UsePolymorphicSerialization.yaml @@ -0,0 +1,176 @@ +openapi: 3.0.1 +paths: + /v1/Warehouses: + post: + tags: + - Warehouses + operationId: CreateWarehouse + parameters: + - name: 'token' + in: 'query' + description: 'Some Token' + required: false + type: 'string' + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Warehouse' + responses: + '201': + description: Created + headers: + X-Rate-Limit: + type: 'integer' + format: 'int32' + description: 'calls per hour allowed by the user' + content: + application/json: + schema: + type: 'array' + items: + $ref: '#/components/schemas/WarehouseResponse' + '400': + description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetails' + '500': + description: Server Error +components: + schemas: + Metadata: + type: object + properties: + createdAt: + type: string + format: date-time + createdBy: + type: string + nullable: true + lastModifiedAt: + type: string + format: date-time + lastModifiedBy: + type: string + nullable: true + additionalProperties: false + SomeComponent: + required: + - $type + type: object + allOf: + - $ref: '#/components/schemas/Component' + properties: + $type: + type: string + typeId: + type: integer + format: int64 + additionalProperties: false + discriminator: + propertyName: $type + SomeComponentState: + enum: + - Active + - Inactive + - Blocked + - Deleted + type: string + SomeComponentType: + type: object + allOf: + - $ref: '#/components/schemas/Component' + properties: + state: + $ref: '#/components/schemas/SomeComponentState' + isBaseRole: + type: boolean + name: + type: string + nullable: true + numberingId: + type: string + nullable: true + additionalProperties: false + Component: + type: object + properties: + id: + type: integer + format: int64 + metadata: + $ref: '#/components/schemas/Metadata' + additionalProperties: false + LoadingAddress: + type: object + allOf: + - $ref: '#/components/schemas/SomeComponent' + properties: + info: + type: string + nullable: true + additionalProperties: false + Warehouse: + type: object + allOf: + - $ref: '#/components/schemas/SomeComponent' + properties: + info: + type: string + nullable: true + additionalProperties: false + WarehouseResponse: + type: object + allOf: + - $ref: '#/components/schemas/SomeComponent' + properties: + info: + type: string + nullable: true + additionalProperties: false + UserComponent: + type: object + allOf: + - $ref: '#/components/schemas/SomeComponent' + properties: + info: + type: string + nullable: true + additionalProperties: false + UserComponent2: + type: object + allOf: + - $ref: '#/components/schemas/UserComponent' + properties: + info2: + type: string + nullable: true + additionalProperties: false + ProblemDetails: + required: + - $type + type: object + properties: + $type: + type: string + type: + type: string + nullable: true + title: + type: string + nullable: true + status: + type: integer + format: int32 + nullable: true + detail: + type: string + nullable: true + instance: + type: string + nullable: true + additionalProperties: { } + discriminator: + propertyName: $type diff --git a/src/Refitter.SourceGenerator.Tests/UseJsonInheritanceConverterTests.cs b/src/Refitter.SourceGenerator.Tests/UseJsonInheritanceConverterTests.cs new file mode 100644 index 00000000..3af2020b --- /dev/null +++ b/src/Refitter.SourceGenerator.Tests/UseJsonInheritanceConverterTests.cs @@ -0,0 +1,25 @@ +using FluentAssertions; + +using Refitter.Tests.UseJsonInheritanceConverter; +using Xunit; + +namespace Refitter.SourceGenerators.Tests; + +public class UseJsonInheritanceConverterTests +{ + [Theory] + [InlineData(typeof(SomeComponent))] + public void Should_Generate_JsonInheritanceConverter_Usage(Type type) => + type + .GetCustomAttributes(typeof(JsonInheritanceConverterAttribute), false) + .Should() + .HaveCount(1); + + [Theory] + [InlineData(typeof(SomeComponent))] + public void Should_Generate_JsonInheritanceAttribute_Usage(Type type) => + type + .GetCustomAttributes(typeof(JsonInheritanceAttribute), false) + .Should() + .HaveCount(5); +} diff --git a/src/Refitter.SourceGenerator.Tests/UsePolymorphicSerializationTests.cs b/src/Refitter.SourceGenerator.Tests/UsePolymorphicSerializationTests.cs new file mode 100644 index 00000000..e555ffbe --- /dev/null +++ b/src/Refitter.SourceGenerator.Tests/UsePolymorphicSerializationTests.cs @@ -0,0 +1,27 @@ +using System.Text.Json.Serialization; +using FluentAssertions; + +using Refitter.Tests.UsePolymorphicSerialization; + +using Xunit; + +namespace Refitter.SourceGenerators.Tests; + +public class UsePolymorphicSerializationTests +{ + [Theory] + [InlineData(typeof(SomeComponent))] + public void Should_Generate_JsonPolymorphicAttribute_Usage(Type type) => + type + .GetCustomAttributes(typeof(JsonPolymorphicAttribute), false) + .Should() + .HaveCount(1); + + [Theory] + [InlineData(typeof(SomeComponent))] + public void Should_Generate_JsonDerivedTypeAttribute_Usage(Type type) => + type + .GetCustomAttributes(typeof(JsonDerivedTypeAttribute), false) + .Should() + .HaveCount(5); +} diff --git a/src/Refitter.SourceGenerator/Refitter.SourceGenerator.csproj b/src/Refitter.SourceGenerator/Refitter.SourceGenerator.csproj index d3260c84..136c2794 100644 --- a/src/Refitter.SourceGenerator/Refitter.SourceGenerator.csproj +++ b/src/Refitter.SourceGenerator/Refitter.SourceGenerator.csproj @@ -30,7 +30,7 @@ - + \ No newline at end of file From d9f050ea8b6a959f6bcd0fbb54f2c7ae717fd2db Mon Sep 17 00:00:00 2001 From: Ryan Heath Date: Tue, 10 Sep 2024 23:32:35 +0200 Subject: [PATCH 5/5] extended CustomCSharpClientGenerator to pass usePolymorphicSerialization into custom template models to be used in the liquid template files ... --- .../CSharpClientGeneratorFactory.cs | 20 +- .../CustomCSharpClientGenerator.cs | 133 +++++++++- src/Refitter.Core/Templates/Class.liquid | 8 +- .../Templates/JsonInheritanceAttribute.liquid | 16 ++ .../Templates/JsonInheritanceConverter.liquid | 240 ++++++++++++++++++ .../UseJsonInheritanceConverter.g.cs | 1 + .../UsePolymorphicSerialization.g.cs | 145 +---------- 7 files changed, 408 insertions(+), 155 deletions(-) diff --git a/src/Refitter.Core/CSharpClientGeneratorFactory.cs b/src/Refitter.Core/CSharpClientGeneratorFactory.cs index e798d48a..37ab671d 100644 --- a/src/Refitter.Core/CSharpClientGeneratorFactory.cs +++ b/src/Refitter.Core/CSharpClientGeneratorFactory.cs @@ -42,20 +42,18 @@ public CustomCSharpClientGenerator Create() } }; - if (settings.UsePolymorphicSerialization) - { - csharpClientGeneratorSettings.CSharpGeneratorSettings.TemplateFactory = new CustomTemplateFactory( - csharpClientGeneratorSettings.CSharpGeneratorSettings, - [ - typeof(CSharpGenerator).Assembly, - typeof(CSharpGeneratorBaseSettings).Assembly, - typeof(CustomTemplateFactory).Assembly, - ]); - } + csharpClientGeneratorSettings.CSharpGeneratorSettings.TemplateFactory = new CustomTemplateFactory( + csharpClientGeneratorSettings.CSharpGeneratorSettings, + [ + typeof(CSharpGenerator).Assembly, + typeof(CSharpGeneratorBaseSettings).Assembly, + typeof(CustomTemplateFactory).Assembly, + ]); var generator = new CustomCSharpClientGenerator( document, - csharpClientGeneratorSettings); + csharpClientGeneratorSettings, + settings.UsePolymorphicSerialization); MapCSharpGeneratorSettings( settings.CodeGeneratorSettings, diff --git a/src/Refitter.Core/CustomCSharpClientGenerator.cs b/src/Refitter.Core/CustomCSharpClientGenerator.cs index fe0676df..94a3f20a 100644 --- a/src/Refitter.Core/CustomCSharpClientGenerator.cs +++ b/src/Refitter.Core/CustomCSharpClientGenerator.cs @@ -1,16 +1,139 @@ +using NJsonSchema; +using NJsonSchema.CodeGeneration; +using NJsonSchema.CodeGeneration.CSharp; +using NJsonSchema.CodeGeneration.CSharp.Models; using NSwag; using NSwag.CodeGeneration.CSharp; using NSwag.CodeGeneration.CSharp.Models; namespace Refitter.Core; -internal class CustomCSharpClientGenerator : CSharpClientGenerator +internal class CustomCSharpClientGenerator(OpenApiDocument document, CSharpClientGeneratorSettings settings, bool usePolymorphicSerialization) +#pragma warning disable CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well. + : CSharpClientGenerator(document, settings) +#pragma warning restore CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well. { - internal CustomCSharpClientGenerator(OpenApiDocument document, CSharpClientGeneratorSettings settings) - : base(document, settings) + internal CSharpOperationModel CreateOperationModel(OpenApiOperation operation) => + CreateOperationModel(operation, Settings); + + /// + /// override to generate DTO types with our custom CSharpGenerator + /// This code should be removed when NSwag supports STJ polymorphic serialization + /// + protected override IEnumerable GenerateDtoTypes() { + var generator = new CCustomSharpGenerator(document, Settings.CSharpGeneratorSettings, (CSharpTypeResolver)Resolver, usePolymorphicSerialization); + return generator.GenerateTypes(); } - internal CSharpOperationModel CreateOperationModel(OpenApiOperation operation) => - CreateOperationModel(operation, Settings); + private class CCustomSharpGenerator(OpenApiDocument document, CSharpGeneratorSettings settings, CSharpTypeResolver resolver, bool usePolymorphicSerialization) +#pragma warning disable CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well. + : CSharpGenerator(document, settings, resolver) +#pragma warning restore CS9107 // Parameter is captured into the state of the enclosing type and its value is also passed to the base constructor. The value might be captured by the base class as well. + { + /// + /// override to generate Class with our custom ClassTemplateModel + /// code is taken from NJsonSchema.CodeGeneration.CSharp.CSharpGenerator.GenerateType + /// + protected override CodeArtifact GenerateType(JsonSchema schema, string typeNameHint) + { + var typeName = resolver.GetOrGenerateTypeName(schema, typeNameHint); + + if (schema.IsEnumeration) + { + return base.GenerateType(schema, typeName); + } + else + { + return GenerateClass(schema, typeName); + } + } + + /// + /// override to generate JsonInheritanceAttribute, JsonInheritanceConverter with our custom template models + /// code is taken from NJsonSchema.CodeGeneration.CSharp.CSharpGenerator.GenerateTypes + /// + public override IEnumerable GenerateTypes() + { + var baseArtifacts = base.GenerateTypes(); + var artifacts = new List(); + + if (baseArtifacts.Any(r => r.Code.Contains("JsonInheritanceConverter"))) + { + if (Settings.ExcludedTypeNames?.Contains("JsonInheritanceAttribute") != true) + { + var template = Settings.TemplateFactory.CreateTemplate("CSharp", "JsonInheritanceAttribute", new CustomJsonInheritanceConverterTemplateModel(Settings, usePolymorphicSerialization)); + artifacts.Add(new CodeArtifact("JsonInheritanceAttribute", CodeArtifactType.Class, CodeArtifactLanguage.CSharp, CodeArtifactCategory.Utility, template)); + } + + if (Settings.ExcludedTypeNames?.Contains("JsonInheritanceConverter") != true) + { + var template = Settings.TemplateFactory.CreateTemplate("CSharp", "JsonInheritanceConverter", new CustomJsonInheritanceConverterTemplateModel(Settings, usePolymorphicSerialization)); + artifacts.Add(new CodeArtifact("JsonInheritanceConverter", CodeArtifactType.Class, CodeArtifactLanguage.CSharp, CodeArtifactCategory.Utility, template)); + } + } + + return baseArtifacts.Concat(artifacts); + } + + /// + /// Code is taken from NJsonSchema.CodeGeneration.CSharp.CSharpGenerator.GenerateClass + /// to instantiate our custom ClassTemplateModel + /// + private CodeArtifact GenerateClass(JsonSchema schema, string typeName) + { + var model = new CustomClassTemplateModel(typeName, Settings, resolver, schema, RootObject, usePolymorphicSerialization); + + RenamePropertyWithSameNameAsClass(typeName, model.Properties); + + var template = Settings.TemplateFactory.CreateTemplate("CSharp", "Class", model); + return new CodeArtifact(typeName, model.BaseClassName, CodeArtifactType.Class, CodeArtifactLanguage.CSharp, CodeArtifactCategory.Contract, template); + } + + /// + /// Code is taken from NJsonSchema.CodeGeneration.CSharp.CSharpGenerator.RenamePropertyWithSameNameAsClass + /// + private static void RenamePropertyWithSameNameAsClass(string typeName, IEnumerable properties) + { + var propertyModels = properties as PropertyModel[] ?? properties.ToArray(); + PropertyModel? propertyWithSameNameAsClass = null; + foreach (var p in propertyModels) + { + if (p.PropertyName == typeName) + { + propertyWithSameNameAsClass = p; + break; + } + } + + if (propertyWithSameNameAsClass != null) + { + var number = 1; + var candidate = typeName + number; + while (propertyModels.Any(p => p.PropertyName == candidate)) + { + number++; + } + + propertyWithSameNameAsClass.PropertyName = propertyWithSameNameAsClass.PropertyName + number; + } + } + + /// + /// finally, our custom ClassTemplateModel and CustomJsonInheritanceConverterTemplateModel + /// to have access to UsePolymorphicSerialization + /// This code should be removed when NSwag supports STJ polymorphic serialization + /// + private class CustomClassTemplateModel(string typeName, CSharpGeneratorSettings settings, CSharpTypeResolver resolver, JsonSchema schema, object rootObject, bool usePolymorphicSerialization) + : ClassTemplateModel(typeName, settings, resolver, schema, rootObject) + { + public bool UsePolymorphicSerialization => usePolymorphicSerialization; + } + + private class CustomJsonInheritanceConverterTemplateModel(CSharpGeneratorSettings settings, bool usePolymorphicSerialization) + : JsonInheritanceConverterTemplateModel(settings) + { + public bool UsePolymorphicSerialization => usePolymorphicSerialization; + } + } } diff --git a/src/Refitter.Core/Templates/Class.liquid b/src/Refitter.Core/Templates/Class.liquid index 17be521b..4017f52b 100644 --- a/src/Refitter.Core/Templates/Class.liquid +++ b/src/Refitter.Core/Templates/Class.liquid @@ -5,14 +5,20 @@ {%- endif -%} {%- if HasDiscriminator -%} {%- if UseSystemTextJson -%} +{%- if UsePolymorphicSerialization -%} [System.Text.Json.Serialization.JsonPolymorphic(TypeDiscriminatorPropertyName = "{{ Discriminator }}", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] {%- else -%} +[JsonInheritanceConverter(typeof({{ ClassName }}), "{{ Discriminator }}")] +{%- endif -%} +{%- else -%} [Newtonsoft.Json.JsonConverter(typeof(JsonInheritanceConverter), "{{ Discriminator }}")] {%- endif -%} {%- for derivedClass in DerivedClasses -%} {%- if derivedClass.IsAbstract != true -%} -{%- if UseSystemTextJson -%} +{%- if UseSystemTextJson and UsePolymorphicSerialization -%} [System.Text.Json.Serialization.JsonDerivedType(typeof({{ derivedClass.ClassName }}), typeDiscriminator: "{{ derivedClass.Discriminator }}")] +{%- else -%} +[JsonInheritanceAttribute("{{ derivedClass.Discriminator }}", typeof({{ derivedClass.ClassName }}))] {%- endif -%} {%- endif -%} {%- endfor -%} diff --git a/src/Refitter.Core/Templates/JsonInheritanceAttribute.liquid b/src/Refitter.Core/Templates/JsonInheritanceAttribute.liquid index e69de29b..1160e921 100644 --- a/src/Refitter.Core/Templates/JsonInheritanceAttribute.liquid +++ b/src/Refitter.Core/Templates/JsonInheritanceAttribute.liquid @@ -0,0 +1,16 @@ +{%- if UsePolymorphicSerialization == false -%} +[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "{{ ToolchainVersion }}")] +[System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Interface, AllowMultiple = true)] +internal class JsonInheritanceAttribute : System.Attribute +{ + public JsonInheritanceAttribute(string key, System.Type type) + { + Key = key; + Type = type; + } + + public string Key { get; } + + public System.Type Type { get; } +} +{%- endif -%} diff --git a/src/Refitter.Core/Templates/JsonInheritanceConverter.liquid b/src/Refitter.Core/Templates/JsonInheritanceConverter.liquid index e69de29b..dcd746f4 100644 --- a/src/Refitter.Core/Templates/JsonInheritanceConverter.liquid +++ b/src/Refitter.Core/Templates/JsonInheritanceConverter.liquid @@ -0,0 +1,240 @@ +{%- if UsePolymorphicSerialization == false -%} +[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "{{ ToolchainVersion }}")] +{%- if UseSystemTextJson -%} +internal class JsonInheritanceConverterAttribute : System.Text.Json.Serialization.JsonConverterAttribute +{ + public string DiscriminatorName { get; } + + public JsonInheritanceConverterAttribute(System.Type baseType, string discriminatorName = "discriminator") + : base(typeof(JsonInheritanceConverter<>).MakeGenericType(baseType)) + { + DiscriminatorName = discriminatorName; + } +} + +public class JsonInheritanceConverter : System.Text.Json.Serialization.JsonConverter +{ + private readonly string _discriminatorName; + + public JsonInheritanceConverter() + { + var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(typeof(TBase)); + _discriminatorName = attribute?.DiscriminatorName ?? "discriminator"; + } + + public JsonInheritanceConverter(string discriminatorName) + { + _discriminatorName = discriminatorName; + } + + public string DiscriminatorName { get { return _discriminatorName; } } + + public override TBase Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) + { + var document = System.Text.Json.JsonDocument.ParseValue(ref reader); + var hasDiscriminator = document.RootElement.TryGetProperty(_discriminatorName, out var discriminator); + var subtype = GetDiscriminatorType(document.RootElement, typeToConvert, hasDiscriminator ? discriminator.GetString() : null); + + var bufferWriter = new System.IO.MemoryStream(); + using (var writer = new System.Text.Json.Utf8JsonWriter(bufferWriter)) + { + document.RootElement.WriteTo(writer); + } + + return (TBase)System.Text.Json.JsonSerializer.Deserialize(bufferWriter.ToArray(), subtype, options); + } + + public override void Write(System.Text.Json.Utf8JsonWriter writer, TBase value, System.Text.Json.JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteString(_discriminatorName, GetDiscriminatorValue(value.GetType())); + + var bytes = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes((object)value, options); + var document = System.Text.Json.JsonDocument.Parse(bytes); + foreach (var property in document.RootElement.EnumerateObject()) + { + property.WriteTo(writer); + } + + writer.WriteEndObject(); + } + + public string GetDiscriminatorValue(System.Type type) + { + var jsonInheritanceAttributeDiscriminator = GetSubtypeDiscriminator(type); + if (jsonInheritanceAttributeDiscriminator != null) + { + return jsonInheritanceAttributeDiscriminator; + } + + return type.Name; + } + + protected System.Type GetDiscriminatorType(System.Text.Json.JsonElement jObject, System.Type objectType, string discriminatorValue) + { + var jsonInheritanceAttributeSubtype = GetObjectSubtype(objectType, discriminatorValue); + if (jsonInheritanceAttributeSubtype != null) + { + return jsonInheritanceAttributeSubtype; + } + + if (objectType.Name == discriminatorValue) + { + return objectType; + } + + var typeName = objectType.Namespace + "." + discriminatorValue; + var subtype = System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType).Assembly.GetType(typeName); + if (subtype != null) + { + return subtype; + } + + throw new System.InvalidOperationException("Could not find subtype of '" + objectType.Name + "' with discriminator '" + discriminatorValue + "'."); + } + + private System.Type GetObjectSubtype(System.Type objectType, string discriminator) + { + foreach (var attribute in System.Reflection.CustomAttributeExtensions.GetCustomAttributes(System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType), true)) + { + if (attribute.Key == discriminator) + return attribute.Type; + } + + return objectType; + } + + private string GetSubtypeDiscriminator(System.Type objectType) + { + foreach (var attribute in System.Reflection.CustomAttributeExtensions.GetCustomAttributes(System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType), true)) + { + if (attribute.Type == objectType) + return attribute.Key; + } + + return objectType.Name; + } +} +{%- else -%} +public class JsonInheritanceConverter : Newtonsoft.Json.JsonConverter +{ + internal static readonly string DefaultDiscriminatorName = "discriminator"; + + private readonly string _discriminatorName; + + [System.ThreadStatic] + private static bool _isReading; + + [System.ThreadStatic] + private static bool _isWriting; + + public JsonInheritanceConverter() + { + _discriminatorName = DefaultDiscriminatorName; + } + + public JsonInheritanceConverter(string discriminatorName) + { + _discriminatorName = discriminatorName; + } + + public string DiscriminatorName { get { return _discriminatorName; } } + + public override void WriteJson(Newtonsoft.Json.JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) + { + try + { + _isWriting = true; + + var jObject = Newtonsoft.Json.Linq.JObject.FromObject(value, serializer); + jObject.AddFirst(new Newtonsoft.Json.Linq.JProperty(_discriminatorName, GetSubtypeDiscriminator(value.GetType()))); + writer.WriteToken(jObject.CreateReader()); + } + finally + { + _isWriting = false; + } + } + + public override bool CanWrite + { + get + { + if (_isWriting) + { + _isWriting = false; + return false; + } + return true; + } + } + + public override bool CanRead + { + get + { + if (_isReading) + { + _isReading = false; + return false; + } + return true; + } + } + + public override bool CanConvert(System.Type objectType) + { + return true; + } + + public override object ReadJson(Newtonsoft.Json.JsonReader reader, System.Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer) + { + var jObject = serializer.Deserialize(reader); + if (jObject == null) + return null; + + var discriminatorValue = jObject.GetValue(_discriminatorName); + var discriminator = discriminatorValue != null ? Newtonsoft.Json.Linq.Extensions.Value(discriminatorValue) : null; + var subtype = GetObjectSubtype(objectType, discriminator); + + var objectContract = serializer.ContractResolver.ResolveContract(subtype) as Newtonsoft.Json.Serialization.JsonObjectContract; + if (objectContract == null || System.Linq.Enumerable.All(objectContract.Properties, p => p.PropertyName != _discriminatorName)) + { + jObject.Remove(_discriminatorName); + } + + try + { + _isReading = true; + return serializer.Deserialize(jObject.CreateReader(), subtype); + } + finally + { + _isReading = false; + } + } + + private System.Type GetObjectSubtype(System.Type objectType, string discriminator) + { + foreach (var attribute in System.Reflection.CustomAttributeExtensions.GetCustomAttributes(System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType), true)) + { + if (attribute.Key == discriminator) + return attribute.Type; + } + + return objectType; + } + + private string GetSubtypeDiscriminator(System.Type objectType) + { + foreach (var attribute in System.Reflection.CustomAttributeExtensions.GetCustomAttributes(System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType), true)) + { + if (attribute.Type == objectType) + return attribute.Key; + } + + return objectType.Name; + } +} +{%- endif -%} +{%- endif -%} diff --git a/src/Refitter.SourceGenerator.Tests/AdditionalFiles/Generated/UseJsonInheritanceConverter.g.cs b/src/Refitter.SourceGenerator.Tests/AdditionalFiles/Generated/UseJsonInheritanceConverter.g.cs index b6dbd2ce..4a2883f9 100644 --- a/src/Refitter.SourceGenerator.Tests/AdditionalFiles/Generated/UseJsonInheritanceConverter.g.cs +++ b/src/Refitter.SourceGenerator.Tests/AdditionalFiles/Generated/UseJsonInheritanceConverter.g.cs @@ -228,6 +228,7 @@ public IDictionary AdditionalProperties } + [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Interface, AllowMultiple = true)] internal class JsonInheritanceAttribute : System.Attribute diff --git a/src/Refitter.SourceGenerator.Tests/AdditionalFiles/Generated/UsePolymorphicSerialization.g.cs b/src/Refitter.SourceGenerator.Tests/AdditionalFiles/Generated/UsePolymorphicSerialization.g.cs index 72a4e86f..9ebee678 100644 --- a/src/Refitter.SourceGenerator.Tests/AdditionalFiles/Generated/UsePolymorphicSerialization.g.cs +++ b/src/Refitter.SourceGenerator.Tests/AdditionalFiles/Generated/UsePolymorphicSerialization.g.cs @@ -88,12 +88,12 @@ public partial class Metadata } - [JsonInheritanceConverter(typeof(SomeComponent), "$type")] - [JsonInheritanceAttribute("Warehouse", typeof(Warehouse))] - [JsonInheritanceAttribute("WarehouseResponse", typeof(WarehouseResponse))] - [JsonInheritanceAttribute("LoadingAddress", typeof(LoadingAddress))] - [JsonInheritanceAttribute("UserComponent", typeof(UserComponent))] - [JsonInheritanceAttribute("UserComponent2", typeof(UserComponent2))] + [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] + [JsonDerivedType(typeof(Warehouse), typeDiscriminator: "Warehouse")] + [JsonDerivedType(typeof(WarehouseResponse), typeDiscriminator: "WarehouseResponse")] + [JsonDerivedType(typeof(LoadingAddress), typeDiscriminator: "LoadingAddress")] + [JsonDerivedType(typeof(UserComponent), typeDiscriminator: "UserComponent")] + [JsonDerivedType(typeof(UserComponent2), typeDiscriminator: "UserComponent2")] [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class SomeComponent : Component { @@ -197,7 +197,7 @@ public partial class UserComponent2 : UserComponent } - [JsonInheritanceConverter(typeof(ProblemDetails), "$type")] + [JsonPolymorphic(TypeDiscriminatorPropertyName = "$type", UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)] [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] public partial class ProblemDetails { @@ -228,137 +228,6 @@ public IDictionary AdditionalProperties } - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] - [System.AttributeUsage(System.AttributeTargets.Class | System.AttributeTargets.Interface, AllowMultiple = true)] - internal class JsonInheritanceAttribute : System.Attribute - { - public JsonInheritanceAttribute(string key, System.Type type) - { - Key = key; - Type = type; - } - - public string Key { get; } - - public System.Type Type { get; } - } - - [System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.1.0.0 (NJsonSchema v11.0.2.0 (Newtonsoft.Json v13.0.0.0))")] - internal class JsonInheritanceConverterAttribute : JsonConverterAttribute - { - public string DiscriminatorName { get; } - - public JsonInheritanceConverterAttribute(System.Type baseType, string discriminatorName = "discriminator") - : base(typeof(JsonInheritanceConverter<>).MakeGenericType(baseType)) - { - DiscriminatorName = discriminatorName; - } - } - - public class JsonInheritanceConverter : JsonConverter - { - private readonly string _discriminatorName; - - public JsonInheritanceConverter() - { - var attribute = System.Reflection.CustomAttributeExtensions.GetCustomAttribute(typeof(TBase)); - _discriminatorName = attribute?.DiscriminatorName ?? "discriminator"; - } - - public JsonInheritanceConverter(string discriminatorName) - { - _discriminatorName = discriminatorName; - } - - public string DiscriminatorName { get { return _discriminatorName; } } - - public override TBase Read(ref System.Text.Json.Utf8JsonReader reader, System.Type typeToConvert, System.Text.Json.JsonSerializerOptions options) - { - var document = System.Text.Json.JsonDocument.ParseValue(ref reader); - var hasDiscriminator = document.RootElement.TryGetProperty(_discriminatorName, out var discriminator); - var subtype = GetDiscriminatorType(document.RootElement, typeToConvert, hasDiscriminator ? discriminator.GetString() : null); - - var bufferWriter = new System.IO.MemoryStream(); - using (var writer = new System.Text.Json.Utf8JsonWriter(bufferWriter)) - { - document.RootElement.WriteTo(writer); - } - - return (TBase)System.Text.Json.JsonSerializer.Deserialize(bufferWriter.ToArray(), subtype, options); - } - - public override void Write(System.Text.Json.Utf8JsonWriter writer, TBase value, System.Text.Json.JsonSerializerOptions options) - { - writer.WriteStartObject(); - writer.WriteString(_discriminatorName, GetDiscriminatorValue(value.GetType())); - - var bytes = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes((object)value, options); - var document = System.Text.Json.JsonDocument.Parse(bytes); - foreach (var property in document.RootElement.EnumerateObject()) - { - property.WriteTo(writer); - } - - writer.WriteEndObject(); - } - - public string GetDiscriminatorValue(System.Type type) - { - var jsonInheritanceAttributeDiscriminator = GetSubtypeDiscriminator(type); - if (jsonInheritanceAttributeDiscriminator != null) - { - return jsonInheritanceAttributeDiscriminator; - } - - return type.Name; - } - - protected System.Type GetDiscriminatorType(System.Text.Json.JsonElement jObject, System.Type objectType, string discriminatorValue) - { - var jsonInheritanceAttributeSubtype = GetObjectSubtype(objectType, discriminatorValue); - if (jsonInheritanceAttributeSubtype != null) - { - return jsonInheritanceAttributeSubtype; - } - - if (objectType.Name == discriminatorValue) - { - return objectType; - } - - var typeName = objectType.Namespace + "." + discriminatorValue; - var subtype = System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType).Assembly.GetType(typeName); - if (subtype != null) - { - return subtype; - } - - throw new System.InvalidOperationException("Could not find subtype of '" + objectType.Name + "' with discriminator '" + discriminatorValue + "'."); - } - - private System.Type GetObjectSubtype(System.Type objectType, string discriminator) - { - foreach (var attribute in System.Reflection.CustomAttributeExtensions.GetCustomAttributes(System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType), true)) - { - if (attribute.Key == discriminator) - return attribute.Type; - } - - return objectType; - } - - private string GetSubtypeDiscriminator(System.Type objectType) - { - foreach (var attribute in System.Reflection.CustomAttributeExtensions.GetCustomAttributes(System.Reflection.IntrospectionExtensions.GetTypeInfo(objectType), true)) - { - if (attribute.Type == objectType) - return attribute.Key; - } - - return objectType.Name; - } - } - }