From 635dee0afda0354943a075a4b951e3a134f2a502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Tue, 14 Jul 2026 19:25:49 +0200 Subject: [PATCH 1/3] Isolate Dock JSON context generation --- .../DockJsonSourceGenerator.cs | 3 ++ .../SourceGeneratedSerializerTests.cs | 54 +++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/src/Dock.Serializer.SystemTextJson.Generators/DockJsonSourceGenerator.cs b/src/Dock.Serializer.SystemTextJson.Generators/DockJsonSourceGenerator.cs index 4204d056c..53add4e60 100644 --- a/src/Dock.Serializer.SystemTextJson.Generators/DockJsonSourceGenerator.cs +++ b/src/Dock.Serializer.SystemTextJson.Generators/DockJsonSourceGenerator.cs @@ -1096,6 +1096,8 @@ private static string FormatPublicKeyToken(ImmutableArray publicKeyToken) private static class SystemTextJsonContextGenerator { + private const string DockContextHintNamePrefix = "DockSystemTextJsonContext."; + public static ImmutableArray Generate( Compilation compilation, string contextSource, @@ -1124,6 +1126,7 @@ public static ImmutableArray Generate( } return runResult.Results[0].GeneratedSources + .Where(static x => x.HintName.StartsWith(DockContextHintNamePrefix, StringComparison.Ordinal)) .Select(static x => new GeneratedSourceArtifact("SystemTextJson." + x.HintName, x.SourceText.ToString())) .ToImmutableArray(); } diff --git a/tests/Dock.Serializer.SystemTextJson.SourceGenTests/SourceGeneratedSerializerTests.cs b/tests/Dock.Serializer.SystemTextJson.SourceGenTests/SourceGeneratedSerializerTests.cs index 9712f279d..45a831062 100644 --- a/tests/Dock.Serializer.SystemTextJson.SourceGenTests/SourceGeneratedSerializerTests.cs +++ b/tests/Dock.Serializer.SystemTextJson.SourceGenTests/SourceGeneratedSerializerTests.cs @@ -29,6 +29,31 @@ namespace Dock.Serializer.SystemTextJson.SourceGenTests; public class SourceGeneratedSerializerTests { + [Fact] + public void DockGenerator_CoexistsWithApplicationJsonContexts() + { + var first = new WeatherForecast + { + Date = new DateTime(2026, 7, 14), + Summary = "Sunny" + }; + var second = new WeatherForecast2 + { + TemperatureCelsius = 21, + Summary = "Clear" + }; + + string firstJson = JsonSerializer.Serialize( + first, + ApplicationJsonContexts.WeatherForecastContext.Default.WeatherForecast); + string secondJson = JsonSerializer.Serialize( + second, + ApplicationJsonContexts.WeatherForecast2Context.Default.WeatherForecast2); + + Assert.Contains("Sunny", firstJson, StringComparison.Ordinal); + Assert.Contains("21", secondJson, StringComparison.Ordinal); + } + [Fact] public void GeneratedSerializer_Roundtrip_CustomDockTypes_Works() { @@ -465,6 +490,35 @@ public sealed class UnregisteredPayload public string? Name { get; set; } } +public sealed class WeatherForecast +{ + public DateTime Date { get; set; } + + public string? Summary { get; set; } +} + +public sealed class WeatherForecast2 +{ + public int TemperatureCelsius { get; set; } + + public string? Summary { get; set; } +} + +public static partial class ApplicationJsonContexts +{ + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(WeatherForecast))] + internal partial class WeatherForecastContext : JsonSerializerContext + { + } + + [JsonSourceGenerationOptions(WriteIndented = true)] + [JsonSerializable(typeof(WeatherForecast2))] + internal partial class WeatherForecast2Context : JsonSerializerContext + { + } +} + public class CustomRootDock : RootDock { public string? RootTag { get; set; } From 3b1ba704b349eed9cc21986dd414fd83772fc261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 15 Jul 2026 06:59:11 +0200 Subject: [PATCH 2/3] Disambiguate generated JSON context --- .../DockJsonSourceGenerator.cs | 12 ++++++--- .../DockJsonSourceGeneratorTests.cs | 27 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/Dock.Serializer.SystemTextJson.Generators/DockJsonSourceGenerator.cs b/src/Dock.Serializer.SystemTextJson.Generators/DockJsonSourceGenerator.cs index 53add4e60..47f811b85 100644 --- a/src/Dock.Serializer.SystemTextJson.Generators/DockJsonSourceGenerator.cs +++ b/src/Dock.Serializer.SystemTextJson.Generators/DockJsonSourceGenerator.cs @@ -17,6 +17,8 @@ namespace Dock.Serializer.SystemTextJson.Generators; [Generator] public sealed class DockJsonSourceGenerator : IIncrementalGenerator { + private const string GeneratedContextTypeName = "DockSerializerGeneratedJsonContext"; + public void Initialize(IncrementalGeneratorInitializationContext context) { IncrementalValueProvider modelProvider = @@ -1096,7 +1098,7 @@ private static string FormatPublicKeyToken(ImmutableArray publicKeyToken) private static class SystemTextJsonContextGenerator { - private const string DockContextHintNamePrefix = "DockSystemTextJsonContext."; + private const string DockContextHintNamePrefix = GeneratedContextTypeName + "."; public static ImmutableArray Generate( Compilation compilation, @@ -1223,7 +1225,9 @@ public static string EmitContext(ImmutableArray contextTypes) builder.AppendLine("))]"); } - builder.AppendLine("internal sealed partial class DockSystemTextJsonContext : global::System.Text.Json.Serialization.JsonSerializerContext"); + builder.Append("internal sealed partial class "); + builder.Append(GeneratedContextTypeName); + builder.AppendLine(" : global::System.Text.Json.Serialization.JsonSerializerContext"); builder.AppendLine("{"); builder.AppendLine("}"); return builder.ToString(); @@ -1239,7 +1243,9 @@ public static string EmitGenerated(GenerationModel model) builder.AppendLine(); builder.AppendLine("internal sealed class DockSystemTextJsonResolver : global::System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver"); builder.AppendLine("{"); - builder.AppendLine(" private static readonly global::System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver s_resolver = global::System.Text.Json.Serialization.Metadata.JsonTypeInfoResolver.WithAddedModifier(DockSystemTextJsonContext.Default, ModifyTypeInfo);"); + builder.Append(" private static readonly global::System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver s_resolver = global::System.Text.Json.Serialization.Metadata.JsonTypeInfoResolver.WithAddedModifier("); + builder.Append(GeneratedContextTypeName); + builder.AppendLine(".Default, ModifyTypeInfo);"); builder.AppendLine(" private static readonly global::System.Collections.Generic.IReadOnlyDictionary> s_ignoredMembers = CreateIgnoredMembers();"); builder.AppendLine(" private static readonly global::System.Collections.Generic.IReadOnlyDictionary s_objectPayloadDiscriminators = CreateObjectPayloadDiscriminators();"); builder.AppendLine(" private static readonly global::System.Collections.Generic.IReadOnlyDictionary s_objectPayloadTypes = CreateObjectPayloadTypes();"); diff --git a/tests/Dock.Serializer.SystemTextJson.Generators.UnitTests/DockJsonSourceGeneratorTests.cs b/tests/Dock.Serializer.SystemTextJson.Generators.UnitTests/DockJsonSourceGeneratorTests.cs index 88133e969..d640d6b16 100644 --- a/tests/Dock.Serializer.SystemTextJson.Generators.UnitTests/DockJsonSourceGeneratorTests.cs +++ b/tests/Dock.Serializer.SystemTextJson.Generators.UnitTests/DockJsonSourceGeneratorTests.cs @@ -158,6 +158,33 @@ public sealed class Payload Assert.Contains("Payload", generatedSource); } + [Fact] + public void ApplicationContextWithDockSimpleName_DoesNotCollideWithGeneratedContext() + { + const string source = """ + using Dock.Serializer.SystemTextJson; + using System.Text.Json.Serialization; + + [assembly: DockJsonSourceGeneration] + + namespace Example; + + [JsonSerializable(typeof(string))] + internal sealed partial class DockSystemTextJsonContext : JsonSerializerContext + { + } + """; + + CompilationRun run = Run(source); + GeneratorRunResult result = Assert.Single(run.RunResult.Results); + string contextSource = GetGeneratedSource(run, "DockSystemTextJsonContext.g.cs"); + string generatedSource = GetGeneratedSource(run, "DockSystemTextJsonGenerated.g.cs"); + + Assert.DoesNotContain(result.Diagnostics, x => x.Severity == DiagnosticSeverity.Error); + Assert.Contains("DockSerializerGeneratedJsonContext", contextSource); + Assert.Contains("DockSerializerGeneratedJsonContext.Default", generatedSource); + } + [Fact] public void AutoDiscovery_IncludesProtectedInternalNestedDockTypes() { From d25852654b6e18dd38e2c8a4939890f635cd0f16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Wed, 15 Jul 2026 07:25:25 +0200 Subject: [PATCH 3/3] Qualify generated JSON context ownership --- .../DockJsonSourceGenerator.cs | 73 ++++++++++++++++--- .../DockJsonSourceGeneratorTests.cs | 11 +-- .../SourceGeneratedSerializerTests.cs | 9 +++ 3 files changed, 78 insertions(+), 15 deletions(-) diff --git a/src/Dock.Serializer.SystemTextJson.Generators/DockJsonSourceGenerator.cs b/src/Dock.Serializer.SystemTextJson.Generators/DockJsonSourceGenerator.cs index 47f811b85..53da24de7 100644 --- a/src/Dock.Serializer.SystemTextJson.Generators/DockJsonSourceGenerator.cs +++ b/src/Dock.Serializer.SystemTextJson.Generators/DockJsonSourceGenerator.cs @@ -10,6 +10,7 @@ using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; namespace Dock.Serializer.SystemTextJson.Generators; @@ -17,7 +18,8 @@ namespace Dock.Serializer.SystemTextJson.Generators; [Generator] public sealed class DockJsonSourceGenerator : IIncrementalGenerator { - private const string GeneratedContextTypeName = "DockSerializerGeneratedJsonContext"; + private const string GeneratedContextTypeNameBase = "DockSerializerGeneratedJsonContext"; + private const string GeneratedContextNamespace = "Dock.Serializer.SystemTextJson"; public void Initialize(IncrementalGeneratorInitializationContext context) { @@ -168,6 +170,7 @@ private sealed record GeneratedSourceArtifact(string HintName, string SourceText private sealed record GenerationModel( bool ShouldGenerate, ImmutableArray Diagnostics, + string ContextTypeName, string ContextSource, ImmutableArray AdditionalSources, ImmutableArray ContextTypes, @@ -179,6 +182,7 @@ public static GenerationModel Empty(ImmutableArray diagnostics) return new GenerationModel( ShouldGenerate: false, Diagnostics: diagnostics, + ContextTypeName: string.Empty, ContextSource: string.Empty, AdditionalSources: ImmutableArray.Empty, ContextTypes: ImmutableArray.Empty, @@ -234,13 +238,15 @@ public static GenerationModel Build(Compilation compilation, System.Threading.Ca ImmutableArray contextTypes = BuildContextTypes(serializableTypes, dockSymbols!); - string contextSource = SourceEmitter.EmitContext(contextTypes); + string contextTypeName = GetUniqueContextTypeName(compilation, cancellationToken); + string contextSource = SourceEmitter.EmitContext(contextTypes, contextTypeName); ImmutableArray additionalSources = - SystemTextJsonContextGenerator.Generate(compilation, contextSource, cancellationToken); + SystemTextJsonContextGenerator.Generate(compilation, contextSource, contextTypeName, cancellationToken); return new GenerationModel( ShouldGenerate: true, Diagnostics: diagnostics.ToImmutable(), + ContextTypeName: contextTypeName, ContextSource: contextSource, AdditionalSources: additionalSources, ContextTypes: contextTypes, @@ -248,6 +254,22 @@ public static GenerationModel Build(Compilation compilation, System.Threading.Ca IgnoredMembers: ignoredMembers); } + private static string GetUniqueContextTypeName( + Compilation compilation, + System.Threading.CancellationToken cancellationToken) + { + var suffix = 0; + var candidate = GeneratedContextTypeNameBase; + + while (compilation.GetSymbolsWithName(candidate, SymbolFilter.Type, cancellationToken).Any()) + { + suffix++; + candidate = GeneratedContextTypeNameBase + "_" + suffix.ToString(CultureInfo.InvariantCulture); + } + + return candidate; + } + private static ImmutableArray GetRegisteredTypes( Compilation compilation, ImmutableArray.Builder diagnostics, @@ -1098,11 +1120,10 @@ private static string FormatPublicKeyToken(ImmutableArray publicKeyToken) private static class SystemTextJsonContextGenerator { - private const string DockContextHintNamePrefix = GeneratedContextTypeName + "."; - public static ImmutableArray Generate( Compilation compilation, string contextSource, + string contextTypeName, System.Threading.CancellationToken cancellationToken) { ISourceGenerator? generator = CreateGenerator(compilation); @@ -1119,7 +1140,11 @@ public static ImmutableArray Generate( GeneratorDriver driver = CSharpGeneratorDriver.Create( generators: new[] { generator }, parseOptions: parseOptions); - driver = driver.RunGenerators(augmentedCompilation, cancellationToken); + driver = driver.RunGeneratorsAndUpdateCompilation( + augmentedCompilation, + out Compilation outputCompilation, + out _, + cancellationToken); GeneratorDriverRunResult runResult = driver.GetRunResult(); if (runResult.Results.Length == 0) @@ -1127,12 +1152,40 @@ public static ImmutableArray Generate( return ImmutableArray.Empty; } + INamedTypeSymbol? contextSymbol = outputCompilation.GetTypeByMetadataName( + GeneratedContextNamespace + "." + contextTypeName); + if (contextSymbol is null) + { + return ImmutableArray.Empty; + } + return runResult.Results[0].GeneratedSources - .Where(static x => x.HintName.StartsWith(DockContextHintNamePrefix, StringComparison.Ordinal)) + .Where(x => IsContextArtifact(x, outputCompilation, contextSymbol, cancellationToken)) .Select(static x => new GeneratedSourceArtifact("SystemTextJson." + x.HintName, x.SourceText.ToString())) .ToImmutableArray(); } + private static bool IsContextArtifact( + GeneratedSourceResult source, + Compilation outputCompilation, + INamedTypeSymbol contextSymbol, + System.Threading.CancellationToken cancellationToken) + { + SemanticModel semanticModel = outputCompilation.GetSemanticModel(source.SyntaxTree); + SyntaxNode root = source.SyntaxTree.GetRoot(cancellationToken); + + foreach (ClassDeclarationSyntax declaration in root.DescendantNodes().OfType()) + { + if (semanticModel.GetDeclaredSymbol(declaration, cancellationToken) is INamedTypeSymbol declaredSymbol + && SymbolEqualityComparer.Default.Equals(declaredSymbol, contextSymbol)) + { + return true; + } + } + + return false; + } + private static ISourceGenerator? CreateGenerator(Compilation compilation) { Assembly? assembly = AppDomain.CurrentDomain.GetAssemblies() @@ -1208,7 +1261,7 @@ x is not null private static class SourceEmitter { - public static string EmitContext(ImmutableArray contextTypes) + public static string EmitContext(ImmutableArray contextTypes, string contextTypeName) { var builder = new StringBuilder(); builder.AppendLine("// "); @@ -1226,7 +1279,7 @@ public static string EmitContext(ImmutableArray contextTypes) } builder.Append("internal sealed partial class "); - builder.Append(GeneratedContextTypeName); + builder.Append(contextTypeName); builder.AppendLine(" : global::System.Text.Json.Serialization.JsonSerializerContext"); builder.AppendLine("{"); builder.AppendLine("}"); @@ -1244,7 +1297,7 @@ public static string EmitGenerated(GenerationModel model) builder.AppendLine("internal sealed class DockSystemTextJsonResolver : global::System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver"); builder.AppendLine("{"); builder.Append(" private static readonly global::System.Text.Json.Serialization.Metadata.IJsonTypeInfoResolver s_resolver = global::System.Text.Json.Serialization.Metadata.JsonTypeInfoResolver.WithAddedModifier("); - builder.Append(GeneratedContextTypeName); + builder.Append(model.ContextTypeName); builder.AppendLine(".Default, ModifyTypeInfo);"); builder.AppendLine(" private static readonly global::System.Collections.Generic.IReadOnlyDictionary> s_ignoredMembers = CreateIgnoredMembers();"); builder.AppendLine(" private static readonly global::System.Collections.Generic.IReadOnlyDictionary s_objectPayloadDiscriminators = CreateObjectPayloadDiscriminators();"); diff --git a/tests/Dock.Serializer.SystemTextJson.Generators.UnitTests/DockJsonSourceGeneratorTests.cs b/tests/Dock.Serializer.SystemTextJson.Generators.UnitTests/DockJsonSourceGeneratorTests.cs index d640d6b16..c0cc0a758 100644 --- a/tests/Dock.Serializer.SystemTextJson.Generators.UnitTests/DockJsonSourceGeneratorTests.cs +++ b/tests/Dock.Serializer.SystemTextJson.Generators.UnitTests/DockJsonSourceGeneratorTests.cs @@ -159,7 +159,7 @@ public sealed class Payload } [Fact] - public void ApplicationContextWithDockSimpleName_DoesNotCollideWithGeneratedContext() + public void ApplicationContextWithGeneratedSimpleName_DoesNotCollideWithDockContext() { const string source = """ using Dock.Serializer.SystemTextJson; @@ -170,19 +170,20 @@ public void ApplicationContextWithDockSimpleName_DoesNotCollideWithGeneratedCont namespace Example; [JsonSerializable(typeof(string))] - internal sealed partial class DockSystemTextJsonContext : JsonSerializerContext + internal sealed partial class DockSerializerGeneratedJsonContext : JsonSerializerContext { } """; CompilationRun run = Run(source); GeneratorRunResult result = Assert.Single(run.RunResult.Results); + Assert.DoesNotContain(run.RunResult.Diagnostics, x => x.Severity == DiagnosticSeverity.Error); + Assert.DoesNotContain(result.Diagnostics, x => x.Severity == DiagnosticSeverity.Error); string contextSource = GetGeneratedSource(run, "DockSystemTextJsonContext.g.cs"); string generatedSource = GetGeneratedSource(run, "DockSystemTextJsonGenerated.g.cs"); - Assert.DoesNotContain(result.Diagnostics, x => x.Severity == DiagnosticSeverity.Error); - Assert.Contains("DockSerializerGeneratedJsonContext", contextSource); - Assert.Contains("DockSerializerGeneratedJsonContext.Default", generatedSource); + Assert.Contains("DockSerializerGeneratedJsonContext_1", contextSource); + Assert.Contains("DockSerializerGeneratedJsonContext_1.Default", generatedSource); } [Fact] diff --git a/tests/Dock.Serializer.SystemTextJson.SourceGenTests/SourceGeneratedSerializerTests.cs b/tests/Dock.Serializer.SystemTextJson.SourceGenTests/SourceGeneratedSerializerTests.cs index 45a831062..792654920 100644 --- a/tests/Dock.Serializer.SystemTextJson.SourceGenTests/SourceGeneratedSerializerTests.cs +++ b/tests/Dock.Serializer.SystemTextJson.SourceGenTests/SourceGeneratedSerializerTests.cs @@ -49,9 +49,13 @@ public void DockGenerator_CoexistsWithApplicationJsonContexts() string secondJson = JsonSerializer.Serialize( second, ApplicationJsonContexts.WeatherForecast2Context.Default.WeatherForecast2); + string collidingNameJson = JsonSerializer.Serialize( + first, + DockSerializerGeneratedJsonContext.Default.WeatherForecast); Assert.Contains("Sunny", firstJson, StringComparison.Ordinal); Assert.Contains("21", secondJson, StringComparison.Ordinal); + Assert.Contains("Sunny", collidingNameJson, StringComparison.Ordinal); } [Fact] @@ -519,6 +523,11 @@ internal partial class WeatherForecast2Context : JsonSerializerContext } } +[JsonSerializable(typeof(WeatherForecast))] +internal partial class DockSerializerGeneratedJsonContext : JsonSerializerContext +{ +} + public class CustomRootDock : RootDock { public string? RootTag { get; set; }