Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 17 additions & 32 deletions src/Refitter.Core/CSharpClientGeneratorFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public CustomCSharpClientGenerator Create()
{
Namespace = settings.ContractsNamespace ?? settings.Namespace,
JsonLibrary = CSharpJsonLibrary.SystemTextJson,
JsonPolymorphicSerializationStyle = settings.UsePolymorphicSerialization ? CSharpJsonPolymorphicSerializationStyle.SystemTextJson : CSharpJsonPolymorphicSerializationStyle.NJsonSchema,
TypeAccessModifier = settings.TypeAccessibility.ToString().ToLowerInvariant(),
ClassStyle =
settings.ImmutableRecords ||
Expand All @@ -46,18 +47,11 @@ public CustomCSharpClientGenerator Create()
csharpClientGeneratorSettings.ParameterNameGenerator = settings.ParameterNameGenerator;
}

csharpClientGeneratorSettings.CSharpGeneratorSettings.TemplateFactory = new CustomTemplateFactory(
csharpClientGeneratorSettings.CSharpGeneratorSettings,
[
typeof(CSharpGenerator).Assembly,
typeof(CSharpGeneratorBaseSettings).Assembly,
typeof(CustomTemplateFactory).Assembly,
]);
csharpClientGeneratorSettings.CSharpGeneratorSettings.TemplateFactory = new CustomTemplateFactory(csharpClientGeneratorSettings.CSharpGeneratorSettings);

var generator = new CustomCSharpClientGenerator(
document,
csharpClientGeneratorSettings,
settings.UsePolymorphicSerialization);
csharpClientGeneratorSettings);

MapCSharpGeneratorSettings(
settings.CodeGeneratorSettings,
Expand Down Expand Up @@ -102,39 +96,30 @@ private static void MapCSharpGeneratorSettings(

/// <summary>
/// custom template factory
/// solely for the purpose of supporting UsePolymorphicSerialization
/// This class and its templates should be removed when NSwag supports this feature.
/// solely for the purpose of tweaking the JsonPolymorphic attribute with UnknownDerivedTypeHandling = FallBackToBaseType and IgnoreUnrecognizedTypeDiscriminators = true
/// This class should be removed if NSwag eventually supports setting UnknownDerivedTypeHandling and IgnoreUnrecognizedTypeDiscriminators.
/// </summary>
Comment on lines +99 to 101

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Doc comment intent is clear. Add a safety note about STJ version requirement.

Generated attributes with IgnoreUnrecognizedTypeDiscriminators may require STJ v8+. Consider documenting the minimum required STJ version to avoid consumer build breaks.


🌐 Web query:

Which minimum System.Text.Json package version exposes JsonPolymorphicAttribute.IgnoreUnrecognizedTypeDiscriminators?

💡 Result:

The property appears starting in System.Text.Json package version 9.0.0. [1][2]

Sources:
[1] Microsoft Docs — JsonPolymorphicAttribute.IgnoreUnrecognizedTypeDiscriminators (shows Package: System.Text.Json v9.0.0).
[2] Microsoft Docs — JsonPolymorphicAttribute class (includes the property).


Doc comment intent is clear. Document minimum STJ version. Generated attributes with IgnoreUnrecognizedTypeDiscriminators require System.Text.Json v9.0.0 or later; include this in the doc comment to prevent consumer build breaks. cite12

🤖 Prompt for AI Agents
In src/Refitter.Core/CSharpClientGeneratorFactory.cs around lines 99 to 101, the
XML doc comment describing why the helper class exists should also state the
minimum required System.Text.Json version; update the comment to mention that
generated attributes using IgnoreUnrecognizedTypeDiscriminators require
System.Text.Json v9.0.0 or later (or specify exact minimum supported version),
so consumers know to upgrade their STJ package to avoid build breaks.

private class CustomTemplateFactory : NSwag.CodeGeneration.DefaultTemplateFactory
{
/// <summary>Initializes a new instance of the <see cref="DefaultTemplateFactory" /> class.</summary>
/// <summary>Initializes a new instance of the <see cref="CustomTemplateFactory" /> class.</summary>
/// <param name="settings">The settings.</param>
/// <param name="assemblies">The assemblies.</param>
public CustomTemplateFactory(CodeGeneratorSettingsBase settings, Assembly[] assemblies)
: base(settings, assemblies)
public CustomTemplateFactory(CodeGeneratorSettingsBase settings)
: base(settings, [typeof(CSharpGenerator).Assembly, typeof(CSharpGeneratorBaseSettings).Assembly])
{
}

/// <summary>Tries to load an embedded Liquid template.</summary>
/// <param name="language">The language.</param>
/// <param name="template">The template name.</param>
/// <returns>The template.</returns>
/// <inheritdoc />
protected override string GetEmbeddedLiquidTemplate(string language, string template)
{
template = template.TrimEnd('!');
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)
var templateText = base.GetEmbeddedLiquidTemplate(language, template);
return template switch
{
using (var reader = new StreamReader(resource))
{
return reader.ReadToEnd();
}
}

return base.GetEmbeddedLiquidTemplate(language, template);
"Class" => templateText
.Replace(
"[System.Text.Json.Serialization.JsonPolymorphic(TypeDiscriminatorPropertyName = \"{{ Discriminator }}\")]",
"[System.Text.Json.Serialization.JsonPolymorphic(TypeDiscriminatorPropertyName = \"{{ Discriminator }}\", UnknownDerivedTypeHandling = System.Text.Json.Serialization.JsonUnknownDerivedTypeHandling.FallBackToBaseType, IgnoreUnrecognizedTypeDiscriminators = true)]"),
_ => templateText,
};
}
Comment on lines +111 to 123

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

Template patch is brittle to upstream formatting changes; make it resilient and idempotent.

Exact Replace(...) can silently fail if NSwag alters spacing/ordering, undermining the PR’s goal to ease future upgrades. Use a regex that:

  • Detects [JsonPolymorphic(...)] regardless of whitespace/arg order,
  • Adds the two arguments only if missing.
-        protected override string GetEmbeddedLiquidTemplate(string language, string template)
-        {
-            var templateText = base.GetEmbeddedLiquidTemplate(language, template);
-            return template switch
-            {
-                "Class" => templateText
-                    .Replace(
-                        "[System.Text.Json.Serialization.JsonPolymorphic(TypeDiscriminatorPropertyName = \"{{ Discriminator }}\")]",
-                        "[System.Text.Json.Serialization.JsonPolymorphic(TypeDiscriminatorPropertyName = \"{{ Discriminator }}\", UnknownDerivedTypeHandling = System.Text.Json.Serialization.JsonUnknownDerivedTypeHandling.FallBackToBaseType, IgnoreUnrecognizedTypeDiscriminators = true)]")
-                _ => templateText,
-            };
-        }
+        protected override string GetEmbeddedLiquidTemplate(string language, string template)
+        {
+            var text = base.GetEmbeddedLiquidTemplate(language, template);
+            if (!string.Equals(template, "Class", StringComparison.Ordinal))
+                return text;
+
+            // Patch [JsonPolymorphic(...)] robustly
+            const string pattern = @"\[\s*System\.Text\.Json\.Serialization\.JsonPolymorphic\s*\((?<args>[^)]*)\)\s*\]";
+            return System.Text.RegularExpressions.Regex.Replace(text, pattern, m =>
+            {
+                var args = m.Groups["args"].Value;
+                // Idempotent: if either arg already present, do nothing
+                if (args.Contains("UnknownDerivedTypeHandling") || args.Contains("IgnoreUnrecognizedTypeDiscriminators"))
+                    return m.Value;
+
+                var insert = string.IsNullOrWhiteSpace(args) ? string.Empty : args.Trim() + ", ";
+                insert += "UnknownDerivedTypeHandling = System.Text.Json.Serialization.JsonUnknownDerivedTypeHandling.FallBackToBaseType, IgnoreUnrecognizedTypeDiscriminators = true";
+                return $"[System.Text.Json.Serialization.JsonPolymorphic({insert})]";
+            });
+        }

Add once at the top of the file:

using System.Text.RegularExpressions;
  • Confirm the enum member spelling in STJ (Fallback vs. FallBack).

}
}
131 changes: 2 additions & 129 deletions src/Refitter.Core/CustomCSharpClientGenerator.cs
Original file line number Diff line number Diff line change
@@ -1,139 +1,12 @@
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(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.
internal class CustomCSharpClientGenerator(OpenApiDocument document, CSharpClientGeneratorSettings settings)
: 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 CSharpOperationModel CreateOperationModel(OpenApiOperation operation) =>
CreateOperationModel(operation, Settings);

/// <summary>
/// override to generate DTO types with our custom CSharpGenerator
/// This code should be removed when NSwag supports STJ polymorphic serialization
/// </summary>
protected override IEnumerable<CodeArtifact> GenerateDtoTypes()
{
var generator = new CCustomSharpGenerator(document, Settings.CSharpGeneratorSettings, (CSharpTypeResolver)Resolver, usePolymorphicSerialization);
return generator.GenerateTypes();
}

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.
{
/// <summary>
/// override to generate Class with our custom ClassTemplateModel
/// code is taken from NJsonSchema.CodeGeneration.CSharp.CSharpGenerator.GenerateType
/// </summary>
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);
}
}

/// <summary>
/// override to generate JsonInheritanceAttribute, JsonInheritanceConverter with our custom template models
/// code is taken from NJsonSchema.CodeGeneration.CSharp.CSharpGenerator.GenerateTypes
/// </summary>
public override IEnumerable<CodeArtifact> GenerateTypes()
{
var baseArtifacts = base.GenerateTypes();
var artifacts = new List<CodeArtifact>();

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);
}

/// <summary>
/// Code is taken from NJsonSchema.CodeGeneration.CSharp.CSharpGenerator.GenerateClass
/// to instantiate our custom ClassTemplateModel
/// </summary>
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);
}

/// <summary>
/// Code is taken from NJsonSchema.CodeGeneration.CSharp.CSharpGenerator.RenamePropertyWithSameNameAsClass
/// </summary>
private static void RenamePropertyWithSameNameAsClass(string typeName, IEnumerable<PropertyModel> 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;
}
}

/// <summary>
/// finally, our custom ClassTemplateModel and CustomJsonInheritanceConverterTemplateModel
/// to have access to UsePolymorphicSerialization
/// This code should be removed when NSwag supports STJ polymorphic serialization
/// </summary>
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;
}
}
base.CreateOperationModel(operation, Settings);
}
8 changes: 2 additions & 6 deletions src/Refitter.Core/Refitter.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All" />
<PackageReference Include="NSwag.CodeGeneration.CSharp" Version="14.4.0" />
<PackageReference Include="NSwag.Core.Yaml" Version="14.4.0" />
<PackageReference Include="NSwag.CodeGeneration.CSharp" Version="14.6.0" />
<PackageReference Include="NSwag.Core.Yaml" Version="14.6.0" />
<PackageReference Include="OasReader" Version="1.6.16.16" />
</ItemGroup>

Expand All @@ -23,8 +23,4 @@
</AssemblyAttribute>
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="Templates/*.liquid" />
</ItemGroup>

</Project>
154 changes: 0 additions & 154 deletions src/Refitter.Core/Templates/Class.liquid

This file was deleted.

Loading
Loading