From 3a0729dd5fffeb1666ee7245af2ca2acce71f0fe Mon Sep 17 00:00:00 2001 From: Jonathan Channon Date: Sat, 18 May 2024 18:05:04 +0100 Subject: [PATCH 1/9] Add ValidateAsync method to use FV async validation (#344) --- .../Features/CastMembers/CastMemberModule.cs | 4 ++-- .../CastMemberValidator.cs | 7 +++++- .../ModelBinding/ValidationExtensions.cs | 18 +++++++++++++++ test/Carter.Samples.Tests/FunctionalTests.cs | 22 +++++++++++++------ 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/samples/CarterSample/Features/CastMembers/CastMemberModule.cs b/samples/CarterSample/Features/CastMembers/CastMemberModule.cs index 65bc0230..20a989d2 100644 --- a/samples/CarterSample/Features/CastMembers/CastMemberModule.cs +++ b/samples/CarterSample/Features/CastMembers/CastMemberModule.cs @@ -4,9 +4,9 @@ public class CastMemberModule : ICarterModule { public void AddRoutes(IEndpointRouteBuilder app) { - app.MapPost("/castmembers", (HttpRequest req, CastMember castMember) => + app.MapPost("/castmembers", async (HttpRequest req, CastMember castMember) => { - var result = req.Validate(castMember); + var result = await req.ValidateAsync(castMember); if (!result.IsValid) { diff --git a/samples/ValidatorOnlyProject/CastMemberValidator.cs b/samples/ValidatorOnlyProject/CastMemberValidator.cs index 4d9b53da..2658cebb 100644 --- a/samples/ValidatorOnlyProject/CastMemberValidator.cs +++ b/samples/ValidatorOnlyProject/CastMemberValidator.cs @@ -1,12 +1,17 @@ namespace ValidatorOnlyProject { + using System.Threading.Tasks; using FluentValidation; public class CastMemberValidator : AbstractValidator { public CastMemberValidator() { - this.RuleFor(x => x.Name).NotEmpty(); + this.RuleFor(x => x.Name).NotEmpty().MustAsync(async (name, cancellation) => + { + await Task.Delay(50); + return true; + }); } } } diff --git a/src/Carter/ModelBinding/ValidationExtensions.cs b/src/Carter/ModelBinding/ValidationExtensions.cs index 8a61f77b..f3443b97 100644 --- a/src/Carter/ModelBinding/ValidationExtensions.cs +++ b/src/Carter/ModelBinding/ValidationExtensions.cs @@ -7,6 +7,7 @@ namespace Carter.ModelBinding; using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; public static class ValidationExtensions { @@ -26,6 +27,23 @@ public static ValidationResult Validate(this HttpRequest request, T model) ? throw new InvalidOperationException($"Cannot find validator for model of type '{typeof(T).Name}'") : validator.Validate(new ValidationContext(model)); } + + /// + /// Performs validation on the specified instance + /// + /// The type of the that is being validated + /// Current + /// The model instance that is being validated + /// + public static async Task ValidateAsync(this HttpRequest request, T model) + { + var validatorLocator = request.HttpContext.RequestServices.GetRequiredService(); + var validator = validatorLocator.GetValidator(); + + return validator == null + ? throw new InvalidOperationException($"Cannot find validator for model of type '{typeof(T).Name}'") + : await validator.ValidateAsync(new ValidationContext(model)); + } /// /// Retrieve formatted validation errors diff --git a/test/Carter.Samples.Tests/FunctionalTests.cs b/test/Carter.Samples.Tests/FunctionalTests.cs index 0712c6e1..b0bce237 100644 --- a/test/Carter.Samples.Tests/FunctionalTests.cs +++ b/test/Carter.Samples.Tests/FunctionalTests.cs @@ -14,6 +14,7 @@ namespace Carter.Samples.Tests using CarterSample.Features.FunctionalProgramming.UpdateDirector; using Microsoft.AspNetCore.Mvc.Testing; using Newtonsoft.Json; + using ValidatorOnlyProject; using Xunit; public class FunctionalTests @@ -23,13 +24,7 @@ public class FunctionalTests public FunctionalTests() { var server = new WebApplicationFactory() - .WithWebHostBuilder(builder => - { - builder.ConfigureServices(services => - { - //services.AddSingleton(); - }); - }); + .WithWebHostBuilder(builder => { }); this.client = server.CreateClient(); } @@ -192,5 +187,18 @@ public async Task Should_return_403_if_user_not_allowed_on_delete() //Then Assert.Equal(403, (int)res.StatusCode); } + + [Fact] + public async Task Should_return_422_for_blank_castmember_with_async_validation() + { + + //When + var res = await this.client.PostAsync("/castmembers", + new StringContent(JsonConvert.SerializeObject(new CastMember{ Name = "" }), Encoding.UTF8, + "application/json")); + + //Then + Assert.Equal(422, (int)res.StatusCode); + } } } From 57736768a784eca71a74404422f4132757ad221b Mon Sep 17 00:00:00 2001 From: Jonathan Channon Date: Sat, 18 May 2024 18:25:36 +0100 Subject: [PATCH 2/9] Add Nuget README.md (#345) --- README.md | 2 +- src/Carter/Carter.csproj | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 282e4a00..b99825eb 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ For a better understanding, take a good look at the [samples](https://github.com Other extensions include: -* `Validate` - [FluentValidation](https://github.com/JeremySkinner/FluentValidation) extensions to validate incoming HTTP requests which is not available with ASP.NET Core Minimal APIs. +* `Validate / ValidateAsync` - [FluentValidation](https://github.com/JeremySkinner/FluentValidation) extensions to validate incoming HTTP requests which is not available with ASP.NET Core Minimal APIs. * `BindFile/BindFiles/BindFileAndSave/BindFilesAndSave` - Allows you to easily get access to a file/files that has been uploaded. Alternatively you can call `BindFilesAndSave` and this will save it to a path you specify. * Routes to use in common ASP.NET Core middleware e.g., `app.UseExceptionHandler("/errorhandler");`. * `IResponseNegotiator`s allow you to define how the response should look on a certain Accept header(content negotiation). Handling JSON is built in the default response but implementing an interface allows the user to choose how they want to represent resources. diff --git a/src/Carter/Carter.csproj b/src/Carter/Carter.csproj index f75ab146..369fd952 100644 --- a/src/Carter/Carter.csproj +++ b/src/Carter/Carter.csproj @@ -12,9 +12,11 @@ true $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb true + README.md + From c928087c186ee303c1746468e8e72bec31456740 Mon Sep 17 00:00:00 2001 From: Jonathan Channon Date: Sat, 18 May 2024 18:51:18 +0100 Subject: [PATCH 3/9] add slack.png --- slack.png | Bin 0 -> 2604 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 slack.png diff --git a/slack.png b/slack.png new file mode 100644 index 0000000000000000000000000000000000000000..66e4298e6c5e55d0c1bd16fb239f6a0e2088f958 GIT binary patch literal 2604 zcmV+{3e)w8P)s*(VYDXRX4v6S ztWQ{u97ROM6u_z!%i z1@>Ncg*pZ0tGR!^eoQWt(ZQGZ)!_O{wZ6Sxx6@ zsJTKy!&FsO<=Ph%6rw*{ujAug-jqo02{3;;Y+e{Wz14IVCz?>}IOE?vH&BS(*`XRm(x`R8B%`+#lk z?;Wt$v&wZjHNpmLQgM#vhMlmCHN3G>-ssh<*W%*GjGgHDVjevaK(Txq1qKD{mtT(v z*jP5-ym_Pi`~sDfl!#-=mg(skI&t!}&R@8wyu3Vl(zWY1bo=&QA&%qy1qB6sF7^Zf zOg6OX{)30WXStdD{R4IX{zLs3tSm=+y@^HZ$0oeBq znu;ZIt!4jaX1yWZccSASLmd$w7=O6=zbLPbSIu5(IDOBVt}$%EXhUfWZLUrfnw$2ZxXuDL_p2@fz4JU3+3t$h4X= zbp~T(Vz(KVeLHsS7P@}@CfA~GzHLO-Vu)p#V^}F{z_Mx#A2FISWoBloyu93E2{vus z#&s(+G@Na~^i&5}z$QkG9!COj3oP?Hea39vxN(cjuNE!avY+3Ax080{R*Zw!KH*eWie_evIkmoA+@w)O4T~*1w2pN^!3RThh$Eq$al}nYy zDoehs(pQJ=@%o4=a>7+tVGXE0J9p`6?pBH{8?Xrpi55V}2xOJH^A-vjU?)tRVuNbK zhRt3ByKvD`8J#A>`3w;Q%efth#>|J26-^`)07&c(z;}bY1uHBNASFT!X=8IrHJp#*hQ84qG z-=f}%uEc+humM|6P^O(rz=N30G8I3W%vLbXYK+(5H6|i9pnoD O0000 literal 0 HcmV?d00001 From 094f1110b137afbe18f015615ec941c76e667691 Mon Sep 17 00:00:00 2001 From: Jonathan Channon Date: Sat, 18 May 2024 18:55:10 +0100 Subject: [PATCH 4/9] make readme show images on nuget.org (#346) --- README.md | 2 +- slack.svg | 107 ------------------------------------------------------ 2 files changed, 1 insertion(+), 108 deletions(-) delete mode 100644 slack.svg diff --git a/README.md b/README.md index b99825eb..3548e646 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Other extensions include: ### Join our Slack Channel - +[![Join our slack channel](https://raw.githubusercontent.com/CarterCommunity/Carter/main/slack.png)](https://join.slack.com/t/cartercommunity/shared_invite/enQtMzY2Nzc0NjU2MTgyLWY3M2Y2Yjk3NzViN2Y3YTQ4ZDA5NWFlMTYxMTIwNDFkMTc5YWEwMDFiOWUyM2Q4ZmY5YmRkODYyYTllZDViMmE) #### Routing diff --git a/slack.svg b/slack.svg deleted file mode 100644 index 06cbe709..00000000 --- a/slack.svg +++ /dev/null @@ -1,107 +0,0 @@ - - - -btn-sign-in-with-slack -Created with Sketch. - - - - - - - - - - - - - - - - - From 6b4f683df89252ed3b964612957b0fac5eb13678 Mon Sep 17 00:00:00 2001 From: Jonathan Channon Date: Sat, 18 May 2024 19:03:55 +0100 Subject: [PATCH 5/9] Make BindFile(s) public (#347) --- src/Carter/ModelBinding/BindExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Carter/ModelBinding/BindExtensions.cs b/src/Carter/ModelBinding/BindExtensions.cs index d7ed410f..54b65905 100644 --- a/src/Carter/ModelBinding/BindExtensions.cs +++ b/src/Carter/ModelBinding/BindExtensions.cs @@ -41,7 +41,7 @@ private static async Task> BindFiles(this HttpRequest req /// /// Current /// - private static Task> BindFiles(this HttpRequest request) + public static Task> BindFiles(this HttpRequest request) { return request.BindFiles(returnOnFirst: false); } @@ -51,7 +51,7 @@ private static Task> BindFiles(this HttpRequest request) /// /// Current /// - private static async Task BindFile(this HttpRequest request) + public static async Task BindFile(this HttpRequest request) { var files = await request.BindFiles(returnOnFirst: true); From a598d57a3e6135e4d8774b551cad6686481c47ea Mon Sep 17 00:00:00 2001 From: Jonathan Channon Date: Mon, 20 May 2024 10:38:42 +0100 Subject: [PATCH 6/9] add xml remarks regarding dependencies (#348) --- src/Carter/ICarterModule.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Carter/ICarterModule.cs b/src/Carter/ICarterModule.cs index c558a67a..d6b131ba 100644 --- a/src/Carter/ICarterModule.cs +++ b/src/Carter/ICarterModule.cs @@ -234,11 +234,13 @@ public CarterModule RequireRateLimiting(string policyName) /// /// An interface to define HTTP routes /// +/// Implementations of should not inject constructor dependencies. All dependencies should be supplied in the route public interface ICarterModule { /// /// Invoked at startup to add routes to the HTTP pipeline /// + /// Implementations of should not inject constructor dependencies. All dependencies should be supplied in the route /// An instance of void AddRoutes(IEndpointRouteBuilder app); } From 89007d29d173b021cfddd699c1c07bb6ee779b89 Mon Sep 17 00:00:00 2001 From: Jonathan Channon Date: Fri, 24 May 2024 13:15:46 +0100 Subject: [PATCH 7/9] add github workflow permissions --- .github/workflows/dotnetcore.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index 66d622f5..cae3b925 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -18,7 +18,9 @@ jobs: name: Github Actions Build runs-on: ubuntu-latest - + permissions: + checks: write + steps: - uses: actions/checkout@v1 - name: Setup .NET Core From f419ab6b42f0391ba2b80e76002556760dc19d71 Mon Sep 17 00:00:00 2001 From: Jonathan Channon Date: Fri, 24 May 2024 13:20:58 +0100 Subject: [PATCH 8/9] all the permissions --- .github/workflows/dotnetcore.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/dotnetcore.yml b/.github/workflows/dotnetcore.yml index cae3b925..cd1a0ee4 100644 --- a/.github/workflows/dotnetcore.yml +++ b/.github/workflows/dotnetcore.yml @@ -18,8 +18,7 @@ jobs: name: Github Actions Build runs-on: ubuntu-latest - permissions: - checks: write + permissions: write-all steps: - uses: actions/checkout@v1 From 8456dd0077c91a3d42e0863fb6cb876d2e39aaa0 Mon Sep 17 00:00:00 2001 From: Collin Alpert Date: Fri, 24 May 2024 15:09:21 +0200 Subject: [PATCH 9/9] Add Roslyn Analyzer project (#349) --- ...ModuleShouldNotHaveDependenciesAnalyzer.cs | 71 ++++ src/Carter/Analyzers/DiagnosticDescriptors.cs | 16 + src/Carter/Carter.csproj | 8 +- .../DependencyContextAssemblyCatalog.cs | 2 + src/Carter/ModelBinding/BindExtensions.cs | 2 + ...terModuleShouldNotHaveDependenciesTests.cs | 327 ++++++++++++++++++ test/Carter.Tests/Carter.Tests.csproj | 1 + 7 files changed, 426 insertions(+), 1 deletion(-) create mode 100644 src/Carter/Analyzers/CarterModuleShouldNotHaveDependenciesAnalyzer.cs create mode 100644 src/Carter/Analyzers/DiagnosticDescriptors.cs create mode 100644 test/Carter.Tests/Analyzers/CarterModuleShouldNotHaveDependenciesTests.cs diff --git a/src/Carter/Analyzers/CarterModuleShouldNotHaveDependenciesAnalyzer.cs b/src/Carter/Analyzers/CarterModuleShouldNotHaveDependenciesAnalyzer.cs new file mode 100644 index 00000000..70ac8adf --- /dev/null +++ b/src/Carter/Analyzers/CarterModuleShouldNotHaveDependenciesAnalyzer.cs @@ -0,0 +1,71 @@ +namespace Carter.Analyzers; + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +internal sealed class CarterModuleShouldNotHaveDependenciesAnalyzer : DiagnosticAnalyzer +{ + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.RegisterCompilationStartAction(OnCompilationStart); + } + + private static void OnCompilationStart(CompilationStartAnalysisContext context) + { + var carterModuleType = context.Compilation.GetTypeByMetadataName("Carter.ICarterModule"); + if (carterModuleType is null) + { + return; + } + + context.RegisterSymbolAction(ctx => OnTypeAnalysis(ctx, carterModuleType), SymbolKind.NamedType); + } + + private static void OnTypeAnalysis(SymbolAnalysisContext context, INamedTypeSymbol carterModuleType) + { + var typeSymbol = (INamedTypeSymbol)context.Symbol; + if (!typeSymbol.Interfaces.Contains(carterModuleType, SymbolEqualityComparer.Default)) + { + return; + } + + foreach (var constructor in typeSymbol.Constructors) + { + if (constructor.DeclaredAccessibility == Accessibility.Private || constructor.Parameters.Length == 0) + { + continue; + } + + foreach (var syntaxReference in constructor.DeclaringSyntaxReferences) + { + var node = syntaxReference.GetSyntax(); + SyntaxToken identifier; + if (node is ConstructorDeclarationSyntax constructorDeclaration) + { + identifier = constructorDeclaration.Identifier; + } else if (node is RecordDeclarationSyntax recordDeclaration) + { + identifier = recordDeclaration.Identifier; + } + else + { + continue; + } + + var diagnostic = Diagnostic.Create( + DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies, + identifier.GetLocation(), + identifier.Text + ); + context.ReportDiagnostic(diagnostic); + } + } + } + + public override ImmutableArray SupportedDiagnostics { get; } = [DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies]; +} diff --git a/src/Carter/Analyzers/DiagnosticDescriptors.cs b/src/Carter/Analyzers/DiagnosticDescriptors.cs new file mode 100644 index 00000000..975ddb59 --- /dev/null +++ b/src/Carter/Analyzers/DiagnosticDescriptors.cs @@ -0,0 +1,16 @@ +using Microsoft.CodeAnalysis; + +namespace Carter.Analyzers; + +internal static class DiagnosticDescriptors +{ + public static readonly DiagnosticDescriptor CarterModuleShouldNotHaveDependencies = new( + "CARTER1", + "Carter module should not have dependencies", + "'{0}' should not have dependencies", + "Usage", + DiagnosticSeverity.Warning, + true, + "When a class implements ICarterModule, it should not have any dependencies. This is because Carter uses minimal APIs and dependencies should be declared in the request delegate." + ); +} \ No newline at end of file diff --git a/src/Carter/Carter.csproj b/src/Carter/Carter.csproj index 369fd952..2aa310fe 100644 --- a/src/Carter/Carter.csproj +++ b/src/Carter/Carter.csproj @@ -13,21 +13,27 @@ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb true README.md + true + RS2008 + + all - + + + diff --git a/src/Carter/DependencyContextAssemblyCatalog.cs b/src/Carter/DependencyContextAssemblyCatalog.cs index 300ae560..610f2d85 100644 --- a/src/Carter/DependencyContextAssemblyCatalog.cs +++ b/src/Carter/DependencyContextAssemblyCatalog.cs @@ -89,7 +89,9 @@ private static Assembly SafeLoadAssembly(AssemblyName assemblyName) { try { +#pragma warning disable RS1035 return Assembly.Load(assemblyName); +#pragma warning restore RS1035 } catch (Exception) { diff --git a/src/Carter/ModelBinding/BindExtensions.cs b/src/Carter/ModelBinding/BindExtensions.cs index 54b65905..7e4fa52b 100644 --- a/src/Carter/ModelBinding/BindExtensions.cs +++ b/src/Carter/ModelBinding/BindExtensions.cs @@ -88,6 +88,7 @@ public static async Task BindAndSaveFile(this HttpRequest request, string saveLo private static async Task SaveFileInternal(IFormFile file, string saveLocation, string fileName = "") { +#pragma warning disable RS1035 if (!Directory.Exists(saveLocation)) Directory.CreateDirectory(saveLocation); @@ -95,5 +96,6 @@ private static async Task SaveFileInternal(IFormFile file, string saveLocation, using (var fileToSave = File.Create(Path.Combine(saveLocation, fileName))) await file.CopyToAsync(fileToSave); +#pragma warning restore RS1035 } } \ No newline at end of file diff --git a/test/Carter.Tests/Analyzers/CarterModuleShouldNotHaveDependenciesTests.cs b/test/Carter.Tests/Analyzers/CarterModuleShouldNotHaveDependenciesTests.cs new file mode 100644 index 00000000..2d58df56 --- /dev/null +++ b/test/Carter.Tests/Analyzers/CarterModuleShouldNotHaveDependenciesTests.cs @@ -0,0 +1,327 @@ +namespace Carter.Tests.Analyzers; + +using System.IO; +using System.Threading.Tasks; +using Carter.Analyzers; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Microsoft.CodeAnalysis.Testing.Verifiers; +using Xunit; + +public sealed class CarterModuleShouldNotHaveDependenciesTests +{ + [Theory] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record struct")] + public Task TypeWithDependency_Diagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + internal {|#0:MyCarterModule|}(string s) {} + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies) + .WithLocation(0) + .WithArguments("MyCarterModule"); + + return this.VerifyAsync(code, diagnosticResult); + } + + [Theory] + [InlineData("record")] + [InlineData("record struct")] + public Task RecordWithDependency_Diagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} {|#0:MyCarterModule|}(string S) : ICarterModule + { + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies) + .WithLocation(0) + .WithArguments("MyCarterModule"); + + return this.VerifyAsync(code, diagnosticResult); + } + + [Theory] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record struct")] + public Task TypeWithMultipleDependencies_Diagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + internal {|#0:MyCarterModule|}(string s, int i) {} + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies) + .WithLocation(0) + .WithArguments("MyCarterModule"); + + return this.VerifyAsync(code, diagnosticResult); + } + + [Theory] + [InlineData("record")] + [InlineData("record struct")] + public Task RecordWithMultipleDependencies_Diagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} {|#0:MyCarterModule|}(string S, int I) : ICarterModule + { + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies) + .WithLocation(0) + .WithArguments("MyCarterModule"); + + return this.VerifyAsync(code, diagnosticResult); + } + + [Theory] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record struct")] + public Task TypeWithDefaultDependencies_Diagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + internal {|#0:MyCarterModule|}(string s = "", char c = 'c') {} + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies) + .WithLocation(0) + .WithArguments("MyCarterModule"); + + return this.VerifyAsync(code, diagnosticResult); + } + + [Theory] + [InlineData("record")] + [InlineData("record struct")] + public Task RecordWithDefaultDependencies_Diagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} {|#0:MyCarterModule|}(string S = "", char C = 'c') : ICarterModule + { + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + var diagnosticResult = new DiagnosticResult(DiagnosticDescriptors.CarterModuleShouldNotHaveDependencies) + .WithLocation(0) + .WithArguments("MyCarterModule"); + + return this.VerifyAsync(code, diagnosticResult); + } + + [Theory] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record struct")] + public Task TypeWithDependencies_WhenConstructorIsPrivate_NoDiagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + private MyCarterModule(string s) {} + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + return this.VerifyAsync(code); + } + + [Theory] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record struct")] + public Task TypeWithDependencies_WhenConstructorIsImplicitlyPrivate_NoDiagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + MyCarterModule(string s) {} + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + return this.VerifyAsync(code); + } + + [Theory] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record struct")] + public Task TypeWithoutConstructor_NoDiagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + void M() {} + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + return this.VerifyAsync(code); + } + + [Theory] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record struct")] + public Task TypeWithZeroParameterConstructor_NoDiagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + using System; + + {{type}} MyCarterModule : ICarterModule + { + public MyCarterModule() + { + Console.WriteLine("Hello World."); + } + + public void AddRoutes(IEndpointRouteBuilder app) {} + } + """; + + return this.VerifyAsync(code); + } + + [Theory] + [InlineData("class")] + [InlineData("struct")] + [InlineData("record")] + [InlineData("record struct")] + public Task NonCarterModuleWithConstructorDependencies_NoDiagnostic(string type) + { + var code = $$""" + using System; + + {{type}} MyCarterModule + { + internal MyCarterModule(string s, int i) {} + } + """; + + return this.VerifyAsync(code); + } + + [Theory] + [InlineData("record")] + [InlineData("record struct")] + public Task RecordNonCarterModuleWithConstructorDependencies_NoDiagnostic(string type) + { + var code = $$""" + using System; + + {{type}} MyCarterModule(string S, int I) + { + } + """; + + return this.VerifyAsync(code); + } + + [Theory] + [InlineData("class")] + [InlineData("record")] + public Task CarterSubModuleWithConstructorDependencies_NoDiagnostic(string type) + { + var code = $$""" + using Carter; + using Microsoft.AspNetCore.Routing; + + {{type}} MyCarterModule : ICarterModule + { + public void AddRoutes(IEndpointRouteBuilder app) {} + } + + {{type}} MySubCarterModule : MyCarterModule + { + public MySubCarterModule(string s) {} + } + """; + + return this.VerifyAsync(code); + } + + private Task VerifyAsync(string code, DiagnosticResult? diagnosticResult = null) + { + var carterPackage = new PackageIdentity("Carter", "8.1.0"); + var aspNetPackage = new PackageIdentity("Microsoft.AspNetCore.App.Ref", "8.0.0"); + var bclPackage = new PackageIdentity("Microsoft.NETCore.App.Ref", "8.0.0"); + var referenceAssemblies = new ReferenceAssemblies("net8.0", bclPackage, Path.Combine("ref", "net8.0")) + .AddPackages([carterPackage, aspNetPackage]); + AnalyzerTest test = new CSharpAnalyzerTest + { + TestCode = code, + ReferenceAssemblies = referenceAssemblies + }; + + if (diagnosticResult.HasValue) + { + test.ExpectedDiagnostics.Add(diagnosticResult.Value); + } + + return test.RunAsync(); + } +} diff --git a/test/Carter.Tests/Carter.Tests.csproj b/test/Carter.Tests/Carter.Tests.csproj index a926504f..98b491d0 100644 --- a/test/Carter.Tests/Carter.Tests.csproj +++ b/test/Carter.Tests/Carter.Tests.csproj @@ -7,5 +7,6 @@ +