Skip to content

Commit d530203

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

File tree

12 files changed

+281
-34
lines changed

12 files changed

+281
-34
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 & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
var builder = WebApplication.CreateBuilder(args);
1414

15-
1615
ValueOfTypeExtensions.ConfigureValueOfTypeConverters(typeof(UserId).Assembly);
1716
ValueOfDapperExtensions.ConfigureValueOfDapperTypeHandlers(typeof(UserId).Assembly);
1817

@@ -26,6 +25,10 @@
2625
#endif
2726
});
2827

28+
//for minimal apis
29+
builder.Services.Configure<Microsoft.AspNetCore.Http.Json.JsonOptions>(options =>
30+
options.SerializerOptions.Converters.Add(new ValueOfJsonAdapterFactory()));
31+
2932
builder.Services.AddEndpointsApiExplorer();
3033
builder.Services.AddSwaggerGen(opts =>
3134
{
@@ -58,7 +61,8 @@
5861
q = q.Where(u => u.Id == userId);
5962
}
6063

61-
return q.ToList();
64+
var res = await q.ToListAsync();
65+
return res;
6266
});
6367

6468

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: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
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+
private static readonly TypeConverter _converter = TypeDescriptor.GetConverter(typeof({t}));
97+
public static {t} Parse(string s, IFormatProvider? provider)
98+
{{
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+
if (_converter.CanConvertFrom(typeof(string)))
106+
{{
107+
result = ({t})_converter.ConvertFrom(s)!;
108+
return true;
109+
}}
110+
return false;
111+
}}
112+
}}
113+
";
114+
context.AddSource($"{className}.g.cs", SourceText.From(code, Encoding.UTF8));
115+
}
116+
}
117+
}
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
{
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using Microsoft.CodeAnalysis;
2+
using Microsoft.CodeAnalysis.CSharp;
3+
using ValueOf.Extensions.ParsableGenerator;
4+
5+
namespace ValueOf.Extensions.Tests;
6+
7+
public class SourceGeneratorParsableValueOfTests
8+
{
9+
private const string TestUserIdClassText = @"
10+
namespace TestNamespace;
11+
using ValueOf;
12+
13+
public sealed partial class TestUserId : ValueOf<int, TestUserId>
14+
{
15+
protected override void Validate()
16+
{
17+
var userId = Value;
18+
if (userId <= 0)
19+
throw new ArgumentOutOfRangeException(nameof(userId) + $"" must be positive integer but is: {userId}"");
20+
}
21+
}
22+
";
23+
24+
private const string ExpectedGeneratedClassText = @"
25+
// <auto-generated/>
26+
#nullable enable
27+
using System;
28+
using System.ComponentModel;
29+
using System.Diagnostics.CodeAnalysis;
30+
using System.Collections.Generic;
31+
32+
namespace TestNamespace;
33+
34+
public partial class TestUserId: IParsable<TestNamespace.TestUserId>
35+
{
36+
private static readonly TypeConverter _converter = TypeDescriptor.GetConverter(typeof(TestNamespace.TestUserId));
37+
public static TestNamespace.TestUserId Parse(string s, IFormatProvider? provider)
38+
{
39+
return (TestNamespace.TestUserId)_converter.ConvertFrom(s)!;
40+
}
41+
42+
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TestNamespace.TestUserId result)
43+
{
44+
result = default(TestNamespace.TestUserId);
45+
if (_converter.CanConvertFrom(typeof(string)))
46+
{
47+
result = (TestNamespace.TestUserId)_converter.ConvertFrom(s)!;
48+
return true;
49+
}
50+
return false;
51+
}
52+
}
53+
";
54+
55+
[Fact]
56+
public void GenerateIParsableMethods()
57+
{
58+
var generator = new SourceGeneratorParsableValueOf();
59+
60+
var driver = CSharpGeneratorDriver.Create(generator);
61+
62+
var compilation = CSharpCompilation.Create(nameof(SourceGeneratorParsableValueOf),
63+
new[] { CSharpSyntaxTree.ParseText(TestUserIdClassText) },
64+
new[]
65+
{
66+
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
67+
MetadataReference.CreateFromFile(typeof(ValueOf<,>).Assembly.Location)
68+
});
69+
70+
var runResult = driver.RunGenerators(compilation).GetRunResult();
71+
72+
var generatedFileSyntax = runResult.GeneratedTrees.Single(t => t.FilePath.EndsWith("TestUserId.g.cs"));
73+
74+
Assert.Equal(ExpectedGeneratedClassText.Trim(), generatedFileSyntax.GetText().ToString().Trim(),
75+
ignoreLineEndingDifferences: true);
76+
}
77+
}

0 commit comments

Comments
 (0)