-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for System.Text.Json converter
This adds native support for asp.net core minimal API parameter binding and output rendering. We simplify the template-based approach since it's highly reusable as of now: both parsable and STJ generation is identical except for the text templates themselves.
- Loading branch information
Showing
14 changed files
with
215 additions
and
109 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,13 @@ | ||
using Microsoft.AspNetCore.Mvc; | ||
using Sample; | ||
|
||
var builder = WebApplication.CreateBuilder(args); | ||
var app = builder.Build(); | ||
|
||
app.MapGet("/{id}", (UserId id) => id); | ||
app.MapGet("/{id}", (UserId id) => new User(id, "kzu")); | ||
|
||
app.Run(); | ||
|
||
readonly partial record struct UserId : IStructId; | ||
readonly partial record struct UserId : IStructId<int>; | ||
|
||
record User(UserId id, string Alias); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
using Microsoft.CodeAnalysis; | ||
|
||
namespace StructId; | ||
|
||
[Generator(LanguageNames.CSharp)] | ||
public class JsonConverterGenerator() : TemplateGenerator( | ||
"System.IParsable`1", | ||
ThisAssembly.Resources.Templates.SJsonConverter.Text, | ||
ThisAssembly.Resources.Templates.TJsonConverter.Text); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
using System; | ||
using System.Linq; | ||
using System.Text; | ||
using Microsoft.CodeAnalysis; | ||
using Microsoft.CodeAnalysis.CSharp; | ||
using Microsoft.CodeAnalysis.CSharp.Syntax; | ||
using Microsoft.CodeAnalysis.Text; | ||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; | ||
|
||
namespace StructId; | ||
|
||
public abstract class TemplateGenerator(string valueInterface, string stringTemplate, string typeTemplate) : IIncrementalGenerator | ||
{ | ||
record struct TemplateArgs(string TargetNamespace, INamedTypeSymbol StructId, INamedTypeSymbol ValueType, INamedTypeSymbol InterfaceType, INamedTypeSymbol StringType); | ||
|
||
public void Initialize(IncrementalGeneratorInitializationContext context) | ||
{ | ||
var targetNamespace = context.AnalyzerConfigOptionsProvider | ||
.Select((x, _) => x.GlobalOptions.TryGetValue("build_property.StructIdNamespace", out var ns) ? ns : "StructId"); | ||
|
||
// Locate the required types | ||
var types = context.CompilationProvider | ||
.Select((x, _) => (InterfaceType: x.GetTypeByMetadataName(valueInterface), StringType: x.GetTypeByMetadataName("System.String"))); | ||
|
||
var ids = context.CompilationProvider | ||
.SelectMany((x, _) => x.Assembly.GetAllTypes().OfType<INamedTypeSymbol>()) | ||
.Where(x => x.IsStructId()) | ||
.Where(x => x.IsPartial()); | ||
|
||
var combined = ids.Combine(types) | ||
// NOTE: we never generate for compilations that don't have the specified value interface type | ||
.Where(x => x.Right.InterfaceType != null || x.Right.StringType == null) | ||
.Combine(targetNamespace) | ||
.Select((x, _) => | ||
{ | ||
var ((structId, (interfaceType, stringType)), targetNamespace) = x; | ||
|
||
// The value type is either a generic type argument for IStructId<T>, or the string type | ||
// for the non-generic IStructId | ||
var valueType = structId.AllInterfaces | ||
.First(x => x.Name == "IStructId") | ||
.TypeArguments.OfType<INamedTypeSymbol>().FirstOrDefault() ?? | ||
stringType!; | ||
|
||
return new TemplateArgs(targetNamespace, structId, valueType, interfaceType!, stringType!); | ||
}) | ||
.Where(x => x.ValueType.Is(x.InterfaceType)); | ||
|
||
context.RegisterImplementationSourceOutput(combined, GenerateCode); | ||
} | ||
|
||
void GenerateCode(SourceProductionContext context, TemplateArgs args) | ||
{ | ||
var ns = args.StructId.ContainingNamespace.Equals(args.StructId.ContainingModule.GlobalNamespace, SymbolEqualityComparer.Default) | ||
? null | ||
: args.StructId.ContainingNamespace.ToDisplayString(); | ||
|
||
var template = args.ValueType.Equals(args.StringType, SymbolEqualityComparer.Default) | ||
? stringTemplate : typeTemplate; | ||
|
||
// replace tokens in the template | ||
template = template | ||
// Adjust to current target namespace | ||
.Replace("namespace StructId;", $"namespace {args.TargetNamespace};") | ||
.Replace("using StructId;", $"using {args.TargetNamespace};") | ||
// Simple names suffices since we emit a partial in the same namespace | ||
.Replace("TStruct", args.StructId.Name) | ||
.Replace("SStruct", args.StructId.Name) | ||
.Replace("TValue", args.ValueType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); | ||
|
||
// parse template into a C# compilation unit | ||
var parseable = CSharpSyntaxTree.ParseText(template).GetCompilationUnitRoot(); | ||
|
||
// if we got a ns, move all members after a file-scoped namespace declaration | ||
if (ns != null) | ||
{ | ||
var members = parseable.Members; | ||
var fsns = FileScopedNamespaceDeclaration(ParseName(ns).WithLeadingTrivia(Whitespace(" "))) | ||
.WithLeadingTrivia(LineFeed) | ||
.WithTrailingTrivia(LineFeed) | ||
.WithMembers(members); | ||
parseable = parseable.WithMembers(SingletonList<MemberDeclarationSyntax>(fsns)); | ||
} | ||
|
||
context.AddSource($"{args.StructId.ToFileName()}.cs", SourceText.From(parseable.ToFullString(), Encoding.UTF8)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
// <auto-generated/> | ||
|
||
using System; | ||
using System.Globalization; | ||
using System.Text.Json; | ||
using System.Text.Json.Serialization; | ||
|
||
namespace StructId; | ||
|
||
public static partial class StructIdConverters | ||
{ | ||
#if NET7_0_OR_GREATER | ||
public class SystemTextJsonConverter<TStruct, TValue> : JsonConverter<TStruct> | ||
where TStruct : IStructId<TValue>, IParsable<TStruct> | ||
where TValue: struct | ||
{ | ||
public override TStruct Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | ||
{ | ||
return TStruct.Parse(reader.GetString(), CultureInfo.InvariantCulture); | ||
} | ||
|
||
public override void Write(Utf8JsonWriter writer, TStruct value, JsonSerializerOptions options) | ||
{ | ||
switch (value.Value) | ||
{ | ||
case Guid guid: | ||
writer.WriteStringValue(guid); | ||
break; | ||
case TValue inner: | ||
writer.WriteRawValue(inner.ToString()); | ||
break; | ||
default: | ||
throw new InvalidOperationException("Unsupported value type."); | ||
} | ||
} | ||
|
||
public override TStruct ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) | ||
=> TStruct.Parse(reader.GetString() ?? throw new FormatException("Unsupported null value for struct id."), CultureInfo.InvariantCulture); | ||
|
||
public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, TStruct value, global::System.Text.Json.JsonSerializerOptions options) | ||
=> writer.WritePropertyName(value.Value.ToString()); | ||
} | ||
|
||
public class SystemTextJsonConverter<TStruct> : JsonConverter<TStruct> | ||
where TStruct : IStructId, IParsable<TStruct> | ||
{ | ||
public override TStruct Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) | ||
{ | ||
return TStruct.Parse(reader.GetString() ?? throw new FormatException("Unsupported null value for struct id."), CultureInfo.InvariantCulture); | ||
} | ||
|
||
public override void Write(Utf8JsonWriter writer, TStruct value, JsonSerializerOptions options) | ||
{ | ||
writer.WriteStringValue(value.Value); | ||
} | ||
|
||
public override TStruct ReadAsPropertyName(ref global::System.Text.Json.Utf8JsonReader reader, global::System.Type typeToConvert, global::System.Text.Json.JsonSerializerOptions options) | ||
=> TStruct.Parse(reader.GetString() ?? throw new FormatException("Unsupported null value for struct id."), CultureInfo.InvariantCulture); | ||
|
||
public override void WriteAsPropertyName(global::System.Text.Json.Utf8JsonWriter writer, TStruct value, global::System.Text.Json.JsonSerializerOptions options) | ||
=> writer.WritePropertyName(value.Value); | ||
} | ||
#endif | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// <auto-generated/> | ||
#nullable enable | ||
|
||
using System.Text.Json.Serialization; | ||
using StructId; | ||
|
||
#if NET7_0_OR_GREATER | ||
[JsonConverter(typeof(StructIdConverters.SystemTextJsonConverter<SStruct>))] | ||
#endif | ||
readonly partial record struct SStruct | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// <auto-generated/> | ||
#nullable enable | ||
|
||
using System.Text.Json.Serialization; | ||
using StructId; | ||
|
||
#if NET7_0_OR_GREATER | ||
[JsonConverter(typeof(StructIdConverters.SystemTextJsonConverter<TStruct, TValue>))] | ||
#endif | ||
readonly partial record struct TStruct | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.