Skip to content

Commit 219d5c2

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

File tree

13 files changed

+299
-37
lines changed

13 files changed

+299
-37
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 & 3 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,10 +61,10 @@
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

64-
6568
app.Run();
6669

6770
public partial class Program

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/ApiIntegrationTests.cs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public ApiIntegrationTests(CustomWebApplicationFactory<Program> factory, ITestOu
2525
public async Task ValueOf_FromRoute_Works()
2626
{
2727
var client = _factory.CreateClient();
28-
var response = await client.GetAsync($"Users/12");
28+
var response = await client.GetAsync("Users/12");
2929

3030
response.EnsureSuccessStatusCode(); // Status Code 200-299
3131
Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType!.ToString());
@@ -40,7 +40,7 @@ public async Task ValueOf_FromRoute_Works()
4040
public async Task ValueOf_FromQuery_Works()
4141
{
4242
var client = _factory.CreateClient();
43-
var response = await client.GetAsync($"Users/findByEmail?email=xiaobao@gmail.com");
43+
var response = await client.GetAsync("Users/findByEmail?email=xiaobao@gmail.com");
4444

4545
response.EnsureSuccessStatusCode(); // Status Code 200-299
4646
Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType!.ToString());
@@ -51,6 +51,22 @@ public async Task ValueOf_FromQuery_Works()
5151
Assert.Equal("xiaobao@gmail.com", email);
5252
}
5353

54+
[Fact]
55+
public async Task ValueOf_MinimalAPI_Works()
56+
{
57+
var client = _factory.CreateClient();
58+
var response = await client.GetAsync("/searchUsers?userId=22");
59+
60+
response.EnsureSuccessStatusCode(); // Status Code 200-299
61+
Assert.Equal("application/json; charset=utf-8", response.Content.Headers.ContentType!.ToString());
62+
var content = await response.Content.ReadAsStringAsync();
63+
var first = JsonDocument.Parse(content).RootElement.EnumerateArray().FirstOrDefault();
64+
var id = first.GetProperty("id").GetInt32();
65+
var email = first.GetProperty("email").GetString();
66+
Assert.Equal(22, id);
67+
Assert.Equal("xiaobao@gmail.com", email);
68+
}
69+
5470
[Fact]
5571
public async Task SwaggerWorks()
5672
{

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)