Skip to content

Commit 6e578d9

Browse files
committed
SourceGenerator & DiagnosticAnalyzer
- improving source generator - add diagnostic warning for non-empty constructor on WebApiEndpoint - add Analyzer to warn for non-empty constructor on WebApiEndpoint - add Analyzer to warn if a BadRequest is returned that does not contain a ProblemDetails on WebApiEndpoint
1 parent a79d7a6 commit 6e578d9

10 files changed

+215
-9
lines changed

README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,19 @@ public class GreetingWebApiEndpoint : IWebApiEndpoint
2828
- [x] Vertical Slice Architecture, gives you the ability to add new features without changing existing code
2929
- [x] Structured way of building WebApiEndpoints using [minimal apis](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-7.0)
3030
- [x] [Easy setup](#easy-setup)
31-
- [x] Developer friendly
31+
- [x] Developer friendly, with a simple API and with a full suite of samples and tests
3232
- [x] Full support and built on top of [minimal apis](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-7.0)
3333
- [x] Full support for OpenApi
3434
- [x] Full support for [TypedResults](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults?view=aspnetcore-7.0)
3535
- [x] [Full compatibility](#full-compatibility-with-futurumcore) with [Futurum.Core](https://www.nuget.org/packages/Futurum.Core)
36-
- [x] [Supports uploading file(s) with additional JSON payload](#uploading-file--s--with-additional-json-payload)
36+
- [x] [Supports uploading file(s) with additional JSON payload](#uploading-files-with-additional-json-payload)
3737
- [x] Api Versioning baked-in
3838
- [x] Built in [sandbox runner](#sandbox-runner) with full [TypedResults support](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.typedresults?view=aspnetcore-7.0), catching unhandled exceptions and returning a [ProblemDetails](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails?view=aspnetcore-7.0) response
3939
- [x] [Built in Validation support](#validation)
4040
- [x] [Integrated FluentValidation](#fluentvalidationservice)
4141
- [x] [Integrated DataAnnotations](#dataannotationsvalidationservice)
4242
- [x] Autodiscovery of WebApiEndpoint(s), based on Source Generators
43+
- [x] [Roslyn Analysers](#roslyn-analysers) to help build your WebApiEndpoint(s), using best practices
4344
- [x] Built on dotnet 7
4445
- [x] Built in use of [ProblemDetails](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.problemdetails?view=aspnetcore-7.0) support
4546
- [x] [Tested solution](https://coveralls.io/github/futurum-dev/dotnet.futurum.webapiendpoint.micro)
@@ -596,7 +597,7 @@ There are examples showing the following:
596597
- [x] Weather Forecast
597598
- [x] Addition project containing WebApiEndpoints
598599

599-
![Futurum.WebApiEndpoint.Micro.Sample-openapi.png](docs/Futurum.WebApiEndpoint.Micro.Sample-openapi.png)
600+
![Futurum.WebApiEndpoint.Micro.Sample-openapi.png](https://github.com/futurum-dev/dotnet.futurum.webapiendpoint.micro/raw/main/docs/Futurum.WebApiEndpoint.Micro.Sample-openapi.png)
600601

601602
## Convention Customisation
602603
Although the default conventions are good enough for most cases, you can customise them.
@@ -653,4 +654,8 @@ This uses the following attributes:
653654

654655
```csharp
655656
[WebApiEndpointVersion(1)]
656-
```
657+
```
658+
659+
## Roslyn Analysers
660+
- FWAEM0001 - Non empty constructor found on WebApiEndpoint
661+
- FWAEM0002 - BadRequest without 'ProblemDetails' use found on WebApiEndpoint
32 KB
Loading
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Futurum.WebApiEndpoint.Micro.Sample.Analyzers;
2+
3+
[WebApiEndpoint(prefixRoute: "method-returning-BadRequest-without-ProblemDetails", group: "analyzer")]
4+
public class MethodReturningBadRequestWithoutProblemDetails : IWebApiEndpoint
5+
{
6+
public void Register(IEndpointRouteBuilder builder)
7+
{
8+
builder.MapGet("/", ResultErrorHandler);
9+
}
10+
11+
private static Results<Ok<string>, BadRequest<string>> ResultErrorHandler(HttpContext context) =>
12+
"This WebApiEndpoint has a non-empty constructor and will raise a warning.".ToOk();
13+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
namespace Futurum.WebApiEndpoint.Micro.Sample.Analyzers;
2+
3+
[WebApiEndpoint("non-empty-constructor", "analyzer")]
4+
public class NonEmptyConstructorOnWebApiEndpoint : IWebApiEndpoint
5+
{
6+
public NonEmptyConstructorOnWebApiEndpoint(IConfiguration configuration)
7+
{
8+
}
9+
10+
public void Register(IEndpointRouteBuilder builder)
11+
{
12+
builder.MapGet("/", GetHandler);
13+
}
14+
15+
private static Ok<string> GetHandler(HttpContext context) =>
16+
"This WebApiEndpoint has a non-empty constructor and will raise a warning.".ToOk();
17+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using Microsoft.CodeAnalysis;
2+
3+
namespace Futurum.WebApiEndpoint.Micro.Generator;
4+
5+
public static class DiagnosticDescriptors
6+
{
7+
public static readonly DiagnosticDescriptor WebApiEndpointNonEmptyConstructor = new(
8+
"FWAEM0001",
9+
"Non empty constructor found on WebApiEndpoint",
10+
$"WebApiEndpoint class '{{0}}' does not have an empty constructor.{Environment.NewLine}" +
11+
$"We recommend that WebApiEndpoint's have an empty constructor and to take any injectable dependencies as parameters via the minimal API method itself.{Environment.NewLine}" +
12+
$"Constructor dependencies will have a lifetime outside of the minimal API lifetime and could have unintended consequences.",
13+
"Futurum.WebApiEndpoint.Micro.Generator",
14+
DiagnosticSeverity.Warning,
15+
true);
16+
17+
public static readonly DiagnosticDescriptor WebApiEndpointMethodReturningBadRequestWithoutProblemDetails = new(
18+
"FWAEM0002",
19+
"BadRequest without 'ProblemDetails' use found on WebApiEndpoint",
20+
"Minimal API methods returning a 'BadRequest', should ensure that the 'BadRequest' is created with a 'ProblemDetails' instance.",
21+
"Futurum.WebApiEndpoint.Micro.Generator",
22+
DiagnosticSeverity.Warning,
23+
true);
24+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using Microsoft.CodeAnalysis;
2+
3+
namespace Futurum.WebApiEndpoint.Micro.Generator;
4+
5+
public static class Diagnostics
6+
{
7+
public static INamedTypeSymbol? GetWebApiEndpointInterfaceType(Compilation semanticModelCompilation) =>
8+
semanticModelCompilation.GetTypeByMetadataName("Futurum.WebApiEndpoint.Micro.IWebApiEndpoint");
9+
10+
public static class WebApiEndpointNonEmptyConstructor
11+
{
12+
public static IEnumerable<Diagnostic> Check(INamedTypeSymbol classSymbol)
13+
{
14+
foreach (var classSymbolConstructor in classSymbol.Constructors)
15+
{
16+
var emptyConstructor = !classSymbolConstructor.Parameters.Any();
17+
18+
if (!emptyConstructor)
19+
{
20+
yield return Diagnostic.Create(DiagnosticDescriptors.WebApiEndpointNonEmptyConstructor,
21+
classSymbolConstructor.Locations.First(),
22+
classSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat));
23+
}
24+
}
25+
}
26+
}
27+
28+
public static class WebApiEndpointMethodReturningBadRequestWithoutProblemDetails
29+
{
30+
public static IEnumerable<Diagnostic> Check(IMethodSymbol methodSymbol)
31+
{
32+
var returnType = methodSymbol.ReturnType;
33+
34+
if (!returnType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).StartsWith("global::Microsoft.AspNetCore.Http.HttpResults.Results"))
35+
yield break;
36+
37+
if (returnType is not INamedTypeSymbol namedTypeSymbol)
38+
yield break;
39+
40+
foreach (var typeSymbol in namedTypeSymbol.TypeArguments)
41+
{
42+
if (typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat).StartsWith("global::Microsoft.AspNetCore.Http.HttpResults.BadRequest"))
43+
{
44+
if (typeSymbol is INamedTypeSymbol typeSymbolOriginalDefinition)
45+
{
46+
var returnTypeArgument = typeSymbolOriginalDefinition.TypeArguments.First();
47+
48+
if (returnTypeArgument.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat) != "global::Microsoft.AspNetCore.Mvc.ProblemDetails")
49+
{
50+
yield return Diagnostic.Create(DiagnosticDescriptors.WebApiEndpointMethodReturningBadRequestWithoutProblemDetails,
51+
methodSymbol.Locations.First());
52+
}
53+
}
54+
}
55+
}
56+
}
57+
}
58+
}

src/Futurum.WebApiEndpoint.Micro.Generator/Futurum.WebApiEndpoint.Micro.Generator.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77
<LangVersion>10</LangVersion>
8+
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
9+
<BuildOutputTargetFolder>analyzers</BuildOutputTargetFolder>
810
<IncludeBuildOutput>false</IncludeBuildOutput>
911
<IsRoslynComponent>true</IsRoslynComponent>
1012
</PropertyGroup>
1113

1214
<ItemGroup>
13-
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.4.0"/>
15+
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.4.0" />
1416
<PackageReference Include="ThisAssembly" Version="1.2.12" PrivateAssets="all" />
1517
</ItemGroup>
1618

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using System.Collections.Immutable;
2+
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
6+
namespace Futurum.WebApiEndpoint.Micro.Generator;
7+
8+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
9+
public class WebApiEndpointMethodReturningBadRequestWithoutProblemDetailsAnalyzer : DiagnosticAnalyzer
10+
{
11+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
12+
= ImmutableArray.Create(DiagnosticDescriptors.WebApiEndpointMethodReturningBadRequestWithoutProblemDetails);
13+
14+
public override void Initialize(AnalysisContext context)
15+
{
16+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
17+
context.EnableConcurrentExecution();
18+
19+
context.RegisterSymbolAction(Execute, SymbolKind.Method);
20+
}
21+
22+
private static void Execute(SymbolAnalysisContext context)
23+
{
24+
if (context.Symbol is not IMethodSymbol methodSymbol)
25+
return;
26+
27+
var webApiEndpointInterfaceType = Diagnostics.GetWebApiEndpointInterfaceType(context.Compilation);
28+
if (methodSymbol.ContainingType.AllInterfaces.Contains(webApiEndpointInterfaceType))
29+
{
30+
var diagnostics = Diagnostics.WebApiEndpointMethodReturningBadRequestWithoutProblemDetails.Check(methodSymbol);
31+
32+
foreach (var diagnostic in diagnostics)
33+
{
34+
context.ReportDiagnostic(diagnostic);
35+
}
36+
}
37+
}
38+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using System.Collections.Immutable;
2+
3+
using Microsoft.CodeAnalysis;
4+
using Microsoft.CodeAnalysis.Diagnostics;
5+
6+
namespace Futurum.WebApiEndpoint.Micro.Generator;
7+
8+
[DiagnosticAnalyzer(LanguageNames.CSharp)]
9+
public class WebApiEndpointNonEmptyConstructorAnalyzer : DiagnosticAnalyzer
10+
{
11+
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; }
12+
= ImmutableArray.Create(DiagnosticDescriptors.WebApiEndpointNonEmptyConstructor);
13+
14+
public override void Initialize(AnalysisContext context)
15+
{
16+
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
17+
context.EnableConcurrentExecution();
18+
19+
context.RegisterSymbolAction(Execute, SymbolKind.NamedType);
20+
}
21+
22+
private static void Execute(SymbolAnalysisContext context)
23+
{
24+
if (context.Symbol is not INamedTypeSymbol classSymbol)
25+
return;
26+
27+
var webApiEndpointInterfaceType = Diagnostics.GetWebApiEndpointInterfaceType(context.Compilation);
28+
if (webApiEndpointInterfaceType is not null)
29+
{
30+
var implementsInterface = classSymbol.AllInterfaces.Contains(webApiEndpointInterfaceType);
31+
32+
if (implementsInterface)
33+
{
34+
var diagnostics = Diagnostics.WebApiEndpointNonEmptyConstructor.Check(classSymbol);
35+
36+
foreach (var diagnostic in diagnostics)
37+
{
38+
context.ReportDiagnostic(diagnostic);
39+
}
40+
}
41+
}
42+
}
43+
}

src/Futurum.WebApiEndpoint.Micro.Generator/WebApiEndpointSourceGenerator.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,21 @@ public static bool SemanticPredicate(SyntaxNode node, CancellationToken ct) =>
2121
var classDeclaration = (ClassDeclarationSyntax)context.Node;
2222
var semanticModel = context.SemanticModel;
2323
var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration, ct);
24-
var interfaceTypeSymbol = semanticModel.Compilation.GetTypeByMetadataName("Futurum.WebApiEndpoint.Micro.IWebApiEndpoint");
25-
if (classSymbol is not null && interfaceTypeSymbol is not null)
24+
25+
var webApiEndpointInterfaceType = Diagnostics.GetWebApiEndpointInterfaceType(semanticModel.Compilation);
26+
27+
if (classSymbol is not null && webApiEndpointInterfaceType is not null)
2628
{
27-
var implementsInterface = classSymbol.AllInterfaces.Contains(interfaceTypeSymbol);
29+
var implementsInterface = classSymbol.AllInterfaces.Contains(webApiEndpointInterfaceType);
2830

2931
if (implementsInterface)
3032
{
33+
var diagnostics = new List<Diagnostic>();
34+
diagnostics.AddRange(Diagnostics.WebApiEndpointNonEmptyConstructor.Check(classSymbol));
35+
3136
var webApiEndpointData = new WebApiEndpointDatum(classSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat));
32-
return new WebApiEndpointContext(webApiEndpointData: new[] { webApiEndpointData });
37+
38+
return new WebApiEndpointContext(diagnostics, new[] { webApiEndpointData });
3339
}
3440
}
3541

0 commit comments

Comments
 (0)