Skip to content

Commit c526032

Browse files
committed
Generalize value templates too, automatically adding Ulid to Dapper
A new templatized value feature allows built-in compiled template support for Ulid as a value type for struct ids. The template follows the same pattern as the struct id template (pending analyzer/codefix) and the dapper generator has been extended to consider them when emitting the registration code. We keep the original custom handler support too, which would still win over templatized one if users provide a type handler in the compilation themselves (as shown in the functional tests via the `StringUlidHandler` type.
1 parent 5513fac commit c526032

23 files changed

Lines changed: 789 additions & 169 deletions

readme.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,11 @@ connection.UseStructId();
101101
connection.Open();
102102
```
103103

104-
The supported types are `Guid`, `int`, `long` and `string` for now.
104+
The value types `Guid`, `int`, `long` and `string` have built-in support, as well as
105+
any other types that implement `IParsable<T>` and `IFormattable` (by persisting them
106+
as strings). This means that you can, for example, use [Ulid](https://github.com/Cysharp/Ulid)
107+
out of the box without any further configuration or customization (since it implements
108+
both interfaces).
105109

106110
## Customization via Templates
107111

src/StructId.Analyzer/AnalysisExtensions.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ public static class AnalysisExtensions
2525

2626
public static string ToFullName(this ISymbol symbol) => symbol.ToDisplayString(FullNameNullable);
2727

28+
public static CSharpParseOptions GetParseOptions(this Compilation compilation)
29+
=> (CSharpParseOptions?)compilation.SyntaxTrees.FirstOrDefault()?.Options ??
30+
CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest);
31+
2832
/// <summary>
2933
/// Checks whether the <paramref name="this"/> type inherits or implements the
3034
/// <paramref name="baseTypeOrInterface"/> type, even if it's a generic type.
@@ -152,6 +156,13 @@ public static string ToFileName(this ITypeSymbol type)
152156

153157
public static bool IsStructId(this ITypeSymbol type) => type.AllInterfaces.Any(x => x.Name == "IStructId");
154158

159+
public static bool IsValueTemplate(this AttributeData attribute)
160+
=> attribute.AttributeClass?.Name == "TValue" ||
161+
attribute.AttributeClass?.Name == "TValueAttribute";
162+
163+
public static bool IsValueTemplate(this AttributeSyntax attribute)
164+
=> attribute.Name.ToString() == "TValue" || attribute.Name.ToString() == "TValueAttribute";
165+
155166
public static bool IsStructIdTemplate(this AttributeData attribute)
156167
=> attribute.AttributeClass?.Name == "TStructId" ||
157168
attribute.AttributeClass?.Name == "TStructIdAttribute";

src/StructId.Analyzer/BaseGenerator.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Linq;
22
using System.Text;
33
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.CSharp;
45
using Microsoft.CodeAnalysis.Text;
56

67
namespace StructId;
@@ -72,8 +73,8 @@ public virtual void Initialize(IncrementalGeneratorInitializationContext context
7273
void GenerateCode(SourceProductionContext context, TemplateArgs args) => AddFromTemplate(
7374
context, args, $"{args.TSelf.ToFileName()}.cs",
7475
args.TId.Equals(args.KnownTypes.String, SymbolEqualityComparer.Default) ?
75-
(stringSyntax ??= CodeTemplate.Parse(stringTemplate)) :
76-
(typedSyntax ??= CodeTemplate.Parse(typeTemplate)));
76+
(stringSyntax ??= CodeTemplate.Parse(stringTemplate, args.KnownTypes.Compilation.GetParseOptions())) :
77+
(typedSyntax ??= CodeTemplate.Parse(typeTemplate, args.KnownTypes.Compilation.GetParseOptions())));
7778

7879
protected static void AddFromTemplate(SourceProductionContext context, TemplateArgs args, string hintName, SyntaxNode template)
7980
{

src/StructId.Analyzer/CodeTemplate.cs

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ namespace StructId;
1010

1111
public static class CodeTemplate
1212
{
13-
public static SyntaxNode Parse(string template)
13+
public static SyntaxNode Parse(string template, CSharpParseOptions? parseOptions = default)
1414
{
1515
var tree = CSharpSyntaxTree.ParseText(template,
16-
CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest));
16+
parseOptions ?? CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.Latest));
1717

1818
return tree.GetRoot();
1919
}
@@ -30,6 +30,17 @@ public static string Apply(string template, string structIdType, string valueTyp
3030
applied.ToFullString();
3131
}
3232

33+
public static string Apply(string template, string valueType, bool normalizeWhitespace = false)
34+
{
35+
var applied = ApplyImpl(Parse(template), valueType);
36+
37+
return normalizeWhitespace ?
38+
applied.NormalizeWhitespace().ToFullString().Trim() :
39+
applied.ToFullString().Trim();
40+
}
41+
42+
public static SyntaxNode ApplyValue(this SyntaxNode node, INamedTypeSymbol valueType) => ApplyImpl(node, valueType.ToFullName());
43+
3344
public static SyntaxNode Apply(this SyntaxNode node, INamedTypeSymbol structId)
3445
{
3546
var root = node.SyntaxTree.GetCompilationUnitRoot();
@@ -49,6 +60,17 @@ public static SyntaxNode Apply(this SyntaxNode node, INamedTypeSymbol structId)
4960
return ApplyImpl(root, structId.Name, tid, targetNamespace, corens);
5061
}
5162

63+
static SyntaxNode ApplyImpl(this SyntaxNode node, string valueType)
64+
{
65+
var root = node.SyntaxTree.GetCompilationUnitRoot();
66+
if (root == null)
67+
return node;
68+
69+
node = new ValueRewriter(valueType).Visit(root)!;
70+
71+
return node;
72+
}
73+
5274
static SyntaxNode ApplyImpl(this SyntaxNode node, string structIdType, string valueType, string? targetNamespace = default, string coreNamespace = "StructId")
5375
{
5476
var root = node.SyntaxTree.GetCompilationUnitRoot();
@@ -95,6 +117,84 @@ static SyntaxNode ApplyImpl(this SyntaxNode node, string structIdType, string va
95117
return node;
96118
}
97119

120+
class ValueRewriter(string tvalue) : CSharpSyntaxRewriter
121+
{
122+
public override SyntaxNode? VisitRecordDeclaration(RecordDeclarationSyntax node)
123+
{
124+
if (IsFileLocal(node))
125+
return null;
126+
127+
return node;
128+
}
129+
130+
public override SyntaxNode? VisitStructDeclaration(StructDeclarationSyntax node)
131+
{
132+
if (IsFileLocal(node))
133+
return null;
134+
135+
return base.VisitStructDeclaration(node);
136+
}
137+
138+
public override SyntaxNode? VisitClassDeclaration(ClassDeclarationSyntax node)
139+
{
140+
if (IsFileLocal(node))
141+
return null;
142+
143+
return base.VisitClassDeclaration(node);
144+
}
145+
146+
public override SyntaxNode? VisitAttribute(AttributeSyntax node)
147+
{
148+
if (node.IsValueTemplate())
149+
return null;
150+
151+
return base.VisitAttribute(node);
152+
}
153+
154+
public override SyntaxNode? VisitAttributeList(AttributeListSyntax node)
155+
{
156+
node = (AttributeListSyntax)base.VisitAttributeList(node)!;
157+
if (node.Attributes.Count == 0)
158+
return null;
159+
160+
return base.VisitAttributeList(node);
161+
}
162+
163+
public override SyntaxNode? VisitIdentifierName(IdentifierNameSyntax node)
164+
{
165+
if (node.Identifier.Text == "TValue")
166+
return IdentifierName(tvalue)
167+
.WithLeadingTrivia(node.Identifier.LeadingTrivia)
168+
.WithTrailingTrivia(node.Identifier.TrailingTrivia);
169+
170+
if (node.Identifier.Text.StartsWith("TValue_"))
171+
return IdentifierName(node.Identifier.Text.Replace("TValue_", tvalue.Replace('.', '_') + "_"))
172+
.WithLeadingTrivia(node.Identifier.LeadingTrivia)
173+
.WithTrailingTrivia(node.Identifier.TrailingTrivia);
174+
175+
return base.VisitIdentifierName(node);
176+
}
177+
178+
public override SyntaxToken VisitToken(SyntaxToken token)
179+
{
180+
if (token.IsKind(SyntaxKind.IdentifierToken) && token.Text == "TValue")
181+
return Identifier(tvalue)
182+
.WithLeadingTrivia(token.LeadingTrivia)
183+
.WithTrailingTrivia(token.TrailingTrivia);
184+
185+
if (token.IsKind(SyntaxKind.IdentifierToken) && token.Text.StartsWith("TValue_"))
186+
return Identifier(token.Text.Replace("TValue_", tvalue.Replace('.', '_') + "_"))
187+
.WithLeadingTrivia(token.LeadingTrivia)
188+
.WithTrailingTrivia(token.TrailingTrivia);
189+
190+
return base.VisitToken(token);
191+
}
192+
193+
bool IsFileLocal(TypeDeclarationSyntax node) =>
194+
node.Modifiers.Any(x => x.IsKind(SyntaxKind.FileKeyword)) &&
195+
!node.AttributeLists.Any(list => list.Attributes.Any(a => a.IsValueTemplate()));
196+
}
197+
98198
class TemplateRewriter(string tself, string tid) : CSharpSyntaxRewriter
99199
{
100200
public override SyntaxNode? VisitRecordDeclaration(RecordDeclarationSyntax node)
@@ -183,7 +283,7 @@ class TemplateRewriter(string tself, string tid) : CSharpSyntaxRewriter
183283
return IdentifierName(tself)
184284
.WithLeadingTrivia(node.Identifier.LeadingTrivia)
185285
.WithTrailingTrivia(node.Identifier.TrailingTrivia);
186-
else if (node.Identifier.Text == "TId")
286+
else if (node.Identifier.Text == "TId" || node.Identifier.Text == "TValue")
187287
return IdentifierName(tid)
188288
.WithLeadingTrivia(node.Identifier.LeadingTrivia)
189289
.WithTrailingTrivia(node.Identifier.TrailingTrivia);
@@ -198,7 +298,7 @@ public override SyntaxToken VisitToken(SyntaxToken token)
198298
return Identifier(tself)
199299
.WithLeadingTrivia(token.LeadingTrivia)
200300
.WithTrailingTrivia(token.TrailingTrivia);
201-
else if (token.IsKind(SyntaxKind.IdentifierToken) && token.Text == "TId")
301+
else if (token.IsKind(SyntaxKind.IdentifierToken) && (token.Text == "TId" || token.Text == "TValue"))
202302
return Identifier(tid)
203303
.WithLeadingTrivia(token.LeadingTrivia)
204304
.WithTrailingTrivia(token.TrailingTrivia);

src/StructId.Analyzer/DapperExtensions.sbn

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,20 +22,24 @@ public static partial class DapperExtensions
2222
{
2323
{{~ for id in Ids ~}}
2424
if (!SqlMapper.HasTypeHandler(typeof({{ id.TSelf }})))
25-
SqlMapper.AddTypeHandler(new DapperTypeHandler{{ id.TId }}<{{ id.TSelf }}>());
25+
SqlMapper.AddTypeHandler(new DapperTypeHandler{{ id.TValue }}<{{ id.TSelf }}>());
2626

2727
{{~ end ~}}
2828
{{~ for id in CustomIds ~}}
2929
if (!SqlMapper.HasTypeHandler(typeof({{ id.TSelf }})))
30-
SqlMapper.AddTypeHandler(new DapperTypeHandler<{{ id.TSelf }}, {{ id.TId }}, {{ id.THandler }}>());
30+
SqlMapper.AddTypeHandler(new DapperTypeHandler<{{ id.TSelf }}, {{ id.TValue }}, {{ id.THandler }}>());
3131

3232
{{~ end ~}}
33-
{{~ for handler in CustomHandlers ~}}
34-
if (!SqlMapper.HasTypeHandler(typeof({{ handler }})))
35-
SqlMapper.AddTypeHandler(new {{ handler }}());
33+
{{~ for handler in CustomValues ~}}
34+
if (!SqlMapper.HasTypeHandler(typeof({{ handler.TValue }})))
35+
SqlMapper.AddTypeHandler(new {{ handler.THandler }}());
3636

3737
{{~ end ~}}
38+
{{~ for handler in TemplatizedValueHandlers ~}}
39+
if (!SqlMapper.HasTypeHandler(typeof({{ handler.TValue }})))
40+
SqlMapper.AddTypeHandler(new {{ handler.THandler }}());
3841

42+
{{~ end ~}}
3943
return connection;
4044
}
4145

@@ -159,4 +163,8 @@ public static partial class DapperExtensions
159163
};
160164
}
161165
}
162-
}
166+
}
167+
168+
{{~ for handler in TemplatizedValueHandlers ~}}
169+
{{ handler.Code }}
170+
{{~ end ~}}

0 commit comments

Comments
 (0)