Skip to content

Commit 7752c24

Browse files
committed
added source code generator to generate IParsable interface for ValueOf types for minimal apis
1 parent c004ba1 commit 7752c24

File tree

13 files changed

+314
-33
lines changed

13 files changed

+314
-33
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ obj/
77
riderModule.iml
88
/_ReSharper.Caches/
99
!.github
10+
.DS_Store
1011

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.ComponentModel;
2+
using System.Diagnostics.CodeAnalysis;
3+
4+
namespace ValueOf.Extensions.Examples;
5+
6+
public interface IParsableValueOf<TU, T> : IParsable<T> where T : ValueOf<TU, T>, IParsable<T>, new()
7+
{
8+
static T IParsable<T>.Parse(string s, IFormatProvider? provider)
9+
{
10+
var converter = TypeDescriptor.GetConverter(typeof(T));
11+
return (T)converter.ConvertFrom(s)!;
12+
}
13+
14+
static bool IParsable<T>.TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider,
15+
[MaybeNullWhen(false)] out T result)
16+
{
17+
result = default(T);
18+
var converter = TypeDescriptor.GetConverter(typeof(T));
19+
if (converter.CanConvertFrom(typeof(string)))
20+
{
21+
result = (T)converter.ConvertFrom(s!)!;
22+
return true;
23+
}
24+
25+
return false;
26+
}
27+
}

ValueOf.Extensions.Examples/Models/UserId.cs

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,8 @@
1-
using System.ComponentModel;
21
using System.Diagnostics.CodeAnalysis;
32

43
namespace ValueOf.Extensions.Examples;
54

6-
public interface IParsableValueOf<TU, T> : IParsable<T> where T : ValueOf<TU, T>, IParsable<T>, new()
7-
{
8-
static T IParsable<T>.Parse(string s, IFormatProvider? provider)
9-
{
10-
var converter = TypeDescriptor.GetConverter(typeof(T));
11-
return (T)converter.ConvertFrom(s)!;
12-
}
13-
14-
static bool IParsable<T>.TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider,
15-
[MaybeNullWhen(false)] out T result)
16-
{
17-
result = default(T);
18-
var converter = TypeDescriptor.GetConverter(typeof(T));
19-
if (converter.CanConvertFrom(typeof(string)))
20-
{
21-
result = (T)converter.ConvertFrom(s)!;
22-
return true;
23-
}
24-
25-
return false;
26-
}
27-
}
28-
29-
public sealed class UserId : ValueOf<int, UserId>, IParsableValueOf<int, UserId>
5+
public sealed partial class UserId : ValueOf<int, UserId>
306
{
317
protected override void Validate()
328
{

ValueOf.Extensions.Examples/Program.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
#endif
2727
});
2828

29+
//for minimal apis
30+
builder.Services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options =>
31+
options.SerializerOptions.Converters.Add(new ValueOfJsonAdapterFactory()));
32+
2933
builder.Services.AddEndpointsApiExplorer();
3034
builder.Services.AddSwaggerGen(opts =>
3135
{
@@ -58,7 +62,8 @@
5862
q = q.Where(u => u.Id == userId);
5963
}
6064

61-
return q.ToList();
65+
var res = await q.ToListAsync();
66+
return res;
6267
});
6368

6469

ValueOf.Extensions.Examples/ValueOf.Extensions.Examples.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<ItemGroup>
2222
<ProjectReference Include="..\ValueOf.Extensions.Dapper\ValueOf.Extensions.Dapper.csproj"/>
2323
<ProjectReference Include="..\ValueOf.Extensions.EFCore\ValueOf.Extensions.EFCore.csproj"/>
24+
<ProjectReference Include="..\ValueOf.Extensions.ParsableGenerator\ValueOf.Extensions.ParsableGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
2425
<ProjectReference Include="..\ValueOf.Extensions.SwashbuckleSwagger\ValueOf.Extensions.SwashbuckleSwagger.csproj"/>
2526
<ProjectReference Include="..\ValueOf.Extensions\ValueOf.Extensions.csproj"/>
2627
</ItemGroup>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"$schema": "https://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"DebugRoslynSourceGenerator": {
5+
"commandName": "DebugRoslynComponent",
6+
"targetProject": "../../ValueOf.Extensions.Examples/ValueOf.Extensions.Examples.csproj"
7+
}
8+
}
9+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Roslyn Source Generators Sample
2+
3+
A set of three projects that illustrates Roslyn source generators. Enjoy this template to learn from and modify source generators for your own needs.
4+
5+
## Content
6+
### ValueOf.Extensions.ParsableGenerator
7+
A .NET Standard project with implementations of sample source generators.
8+
**You must build this project to see the result (generated code) in the IDE.**
9+
10+
- [SampleSourceGenerator.cs](SampleSourceGenerator.cs): A source generator that creates C# classes based on a text file (in this case, Domain Driven Design ubiquitous language registry).
11+
- [SampleIncrementalSourceGenerator.cs](SampleIncrementalSourceGenerator.cs): A source generator that creates a custom report based on class properties. The target class should be annotated with the `Generators.ReportAttribute` attribute.
12+
13+
### ValueOf.Extensions.ParsableGenerator.Sample
14+
A project that references source generators. Note the parameters of `ProjectReference` in [ValueOf.Extensions.ParsableGenerator.Sample.csproj](../ValueOf.Extensions.ParsableGenerator.Sample/ValueOf.Extensions.ParsableGenerator.Sample.csproj), they make sure that the project is referenced as a set of source generators.
15+
16+
### ValueOf.Extensions.ParsableGenerator.Tests
17+
Unit tests for source generators. The easiest way to develop language-related features is to start with unit tests.
18+
19+
## How To?
20+
### How to debug?
21+
- Use the [launchSettings.json](Properties/launchSettings.json) profile.
22+
- Debug tests.
23+
24+
### How can I determine which syntax nodes I should expect?
25+
Consider using the Roslyn Visualizer toolwindow, witch allow you to observe syntax tree.
26+
27+
### How to learn more about wiring source generators?
28+
Watch the walkthrough video: [Let’s Build an Incremental Source Generator With Roslyn, by Stefan Pölz](https://youtu.be/azJm_Y2nbAI)
29+
The complete set of information is available in [Source Generators Cookbook](https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md).
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
using System;
2+
using System.Collections.Immutable;
3+
using System.Linq;
4+
using System.Net;
5+
using System.Text;
6+
using Microsoft.CodeAnalysis;
7+
using Microsoft.CodeAnalysis.CSharp.Syntax;
8+
using Microsoft.CodeAnalysis.Text;
9+
10+
11+
namespace ValueOf.Extensions.ParsableGenerator;
12+
13+
/// <summary>
14+
/// A sample source generator that creates a custom report based on class properties. The target class should be annotated with the 'Generators.ReportAttribute' attribute.
15+
/// When using the source code as a baseline, an incremental source generator is preferable because it reduces the performance overhead.
16+
/// </summary>
17+
[Generator]
18+
public class SourceGeneratorParsableValueOf : IIncrementalGenerator
19+
{
20+
public void Initialize(IncrementalGeneratorInitializationContext context)
21+
{
22+
var provider = context.SyntaxProvider
23+
.CreateSyntaxProvider(
24+
(s, _) => s is ClassDeclarationSyntax,
25+
(ctx, _) => GetClassDeclarationForSourceGen(ctx))
26+
.Where(t => t.isValueOfType)
27+
.Select((t, _) => t.Item1);
28+
29+
context.RegisterSourceOutput(context.CompilationProvider.Combine(provider.Collect()),
30+
((ctx, t) => GenerateCode(ctx, t.Left, t.Right)));
31+
}
32+
33+
private static (ClassDeclarationSyntax, bool isValueOfType) GetClassDeclarationForSourceGen(
34+
GeneratorSyntaxContext context)
35+
{
36+
var classDeclarationSyntax = (ClassDeclarationSyntax)context.Node;
37+
38+
if (classDeclarationSyntax.BaseList is null)
39+
{
40+
return (classDeclarationSyntax, false);
41+
}
42+
43+
if (classDeclarationSyntax.Modifiers.All(m => m.Text != "partial"))
44+
{
45+
return (classDeclarationSyntax, false);
46+
}
47+
48+
foreach (var baseType in classDeclarationSyntax.BaseList.Types)
49+
{
50+
if (context.SemanticModel.GetSymbolInfo(baseType.Type).Symbol is not INamedTypeSymbol baseTypeSymbol)
51+
continue;
52+
if (baseTypeSymbol.IsGenericType && baseTypeSymbol.ContainingNamespace.Name == "ValueOf" &&
53+
baseTypeSymbol.Name == "ValueOf")
54+
{
55+
return (classDeclarationSyntax, true);
56+
}
57+
}
58+
59+
return (classDeclarationSyntax, false);
60+
}
61+
62+
private void GenerateCode(SourceProductionContext context, Compilation compilation,
63+
ImmutableArray<ClassDeclarationSyntax> classDeclarations)
64+
{
65+
foreach (var classDeclarationSyntax in classDeclarations)
66+
{
67+
var className = classDeclarationSyntax.Identifier.Text;
68+
69+
var semanticModel = compilation.GetSemanticModel(classDeclarationSyntax.SyntaxTree);
70+
if (semanticModel.GetDeclaredSymbol(classDeclarationSyntax) is not INamedTypeSymbol classSymbol)
71+
continue;
72+
73+
var modifier =
74+
classDeclarationSyntax.Modifiers.FirstOrDefault(m => m.Text is "public" or "internal" or "private");
75+
var modiferText = modifier.Text is null ? "" : modifier.Text + " ";
76+
77+
var namespaceName = classSymbol.ContainingNamespace.ToDisplayString();
78+
79+
var baseType = classSymbol.BaseType!;
80+
var typeArguments = baseType.TypeArguments;
81+
82+
var t = typeArguments[1].ToDisplayString();
83+
84+
// Build up the source code
85+
var code = $@"// <auto-generated/>
86+
#nullable enable
87+
using System;
88+
using System.ComponentModel;
89+
using System.Diagnostics.CodeAnalysis;
90+
using System.Collections.Generic;
91+
92+
namespace {namespaceName};
93+
94+
{modiferText}partial class {className}: IParsable<{t}>
95+
{{
96+
public static {t} Parse(string s, IFormatProvider? provider)
97+
{{
98+
var converter = TypeDescriptor.GetConverter(typeof({t}));
99+
return ({t})converter.ConvertFrom(s)!;
100+
}}
101+
102+
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out {t} result)
103+
{{
104+
result = default({t});
105+
var converter = TypeDescriptor.GetConverter(typeof({t}));
106+
if (converter.CanConvertFrom(typeof(string)))
107+
{{
108+
result = ({t})converter.ConvertFrom(s)!;
109+
return true;
110+
}}
111+
112+
return false;
113+
}}
114+
}}
115+
";
116+
context.AddSource($"{className}.g.cs", SourceText.From(code, Encoding.UTF8));
117+
}
118+
}
119+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<IsPackable>false</IsPackable>
6+
<Nullable>enable</Nullable>
7+
<LangVersion>latest</LangVersion>
8+
9+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
10+
<IsRoslynComponent>true</IsRoslynComponent>
11+
12+
<RootNamespace>ValueOf.Extensions.ParsableGenerator</RootNamespace>
13+
<PackageId>ValueOf.Extensions.ParsableGenerator</PackageId>
14+
</PropertyGroup>
15+
16+
<ItemGroup>
17+
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
18+
<PrivateAssets>all</PrivateAssets>
19+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
20+
</PackageReference>
21+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0"/>
22+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.0"/>
23+
</ItemGroup>
24+
</Project>

ValueOf.Extensions.Tests/Models/TestUserId.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
namespace ValueOf.Extensions.Tests.Models;
22

3-
public sealed class TestUserId : ValueOf<int, TestUserId>
3+
public sealed partial class TestUserId : ValueOf<int, TestUserId>
44
{
55
protected override void Validate()
66
{

0 commit comments

Comments
 (0)