diff --git a/.build/.build.csproj b/.build/.build.csproj index 2806a6fde..22e38ec00 100644 --- a/.build/.build.csproj +++ b/.build/.build.csproj @@ -4,10 +4,11 @@ Exe net6.0 false - + False CS0649;CS0169 1 + true diff --git a/.build/Build.CI.cs b/.build/Build.CI.cs index e2e21f5b5..e7e3857b6 100644 --- a/.build/Build.CI.cs +++ b/.build/Build.CI.cs @@ -70,7 +70,7 @@ internal class LocalConstants [PrintCIEnvironment] [UploadLogs] [TitleEvents] -public partial class Solution +public partial class BuildSolution { public static RocketSurgeonGitHubActionsConfiguration CiIgnoreMiddleware( RocketSurgeonGitHubActionsConfiguration configuration diff --git a/.build/Build.cs b/.build/Build.cs index bfa0405f6..cdcd5451c 100644 --- a/.build/Build.cs +++ b/.build/Build.cs @@ -1,11 +1,19 @@ +using System.Diagnostics; +using System.Net; +using System.Net.Sockets; +using System.Reflection; using Nuke.Common; using Nuke.Common.CI; using Nuke.Common.Execution; using Nuke.Common.Git; +using Nuke.Common.ProjectModel; +using Nuke.Common.Tooling; using Nuke.Common.Tools.DotNet; using Nuke.Common.Tools.GitVersion; using Nuke.Common.Tools.MSBuild; using Rocket.Surgery.Nuke.DotNetCore; +using Serilog; +using NukeSolution = Nuke.Common.ProjectModel.Solution; [PublicAPI] [CheckBuildProjectConfigurations] @@ -17,18 +25,19 @@ [MSBuildVerbosityMapping] [NuGetVerbosityMapping] [ShutdownDotNetAfterServerBuild] -public partial class Solution : NukeBuild, - ICanRestoreWithDotNetCore, - ICanBuildWithDotNetCore, - ICanTestWithDotNetCore, - IHaveNuGetPackages, - IHaveDataCollector, - ICanClean, - ICanUpdateReadme, - IGenerateCodeCoverageReport, - IGenerateCodeCoverageSummary, - IGenerateCodeCoverageBadges, - IHaveConfiguration +public partial class BuildSolution : NukeBuild, + ICanRestoreWithDotNetCore, + ICanBuildWithDotNetCore, + ICanTestWithDotNetCore, + IComprehendSamples, + IHaveNuGetPackages, + IHaveDataCollector, + ICanClean, + ICanUpdateReadme, + IGenerateCodeCoverageReport, + IGenerateCodeCoverageSummary, + IGenerateCodeCoverageBadges, + IHaveConfiguration { /// /// Support plugins are available for: @@ -39,7 +48,26 @@ public partial class Solution : NukeBuild, /// public static int Main() { - return Execute(x => x.Default); + return Execute(x => x.Default); + } + + public static int FindFreePort() + { + var port = 0; + var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); + try + { + var localEP = new IPEndPoint(IPAddress.Any, 0); + socket.Bind(localEP); + localEP = (IPEndPoint)socket.LocalEndPoint; + port = localEP.Port; + } + finally + { + socket.Close(); + } + + return port; } [OptionalGitRepository] public GitRepository? GitRepository { get; } @@ -74,6 +102,78 @@ public static int Main() .Before(Default) .Before(Clean); + public Target UpdateGraphQl => _ => _.DependentFor(Test).After(Build).Executes( + async () => + { + var port = FindFreePort(); + var tcs = new TaskCompletionSource(); + var cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromMinutes(1)); + cts.Token.Register(() => tcs.TrySetCanceled()); + var url = $"http://localhost:{port}"; + var process1 = ProcessTasks.StartProcess( + "dotnet", + "run --no-launch-profile", + logOutput: true, + logInvocation: true, + timeout: Convert.ToInt32(TimeSpan.FromMinutes(1).TotalSeconds), + customLogger: (type, s) => + { + if (s.Contains("Application started.")) + { + tcs.TrySetResult(); + } + + if (type == OutputType.Std) + { + Log.Logger.Debug(s); + } + else + { + Log.Logger.Error(s); + } + }, + environmentVariables: new Dictionary(EnvironmentInfo.Variables) + { + ["ASPNETCORE_URLS"] = url, + ["ASPNETCORE_ENVIRONMENT"] = "Development", + }, + workingDirectory: this.As().SampleDirectory / "Sample.Graphql" + ); + + var process = (Process)typeof(Process2).GetField("_process", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(process1)!; + + try + { + await tcs.Task; + DotNetTasks.DotNet( + $"graphql update -u {url}/graphql/", + this.As().TestsDirectory / "Sample.Graphql.Tests" + ); + } + finally + { + if (OperatingSystem.IsWindows()) + { + process1.Kill(); + } + else + { + ProcessTasks.StartProcess("kill", $"-s TERM {process!.Id}"); + } + + process1.WaitForExit(); + } + + if (!IsLocalBuild) + { + await Task.Delay(TimeSpan.FromSeconds(5)); + } + } + ); + + [Solution(GenerateProjects = true)] private NukeSolution Solution { get; } = null!; + private Target Default => _ => _ .DependsOn(Restore) .DependsOn(Build) @@ -81,6 +181,7 @@ public static int Main() .DependsOn(Pack); public Target Build => _ => _.Inherit(x => x.CoreBuild); + NukeSolution IHaveSolution.Solution => Solution; [ComputedGitVersion] public GitVersion GitVersion { get; } = null!; @@ -90,3 +191,11 @@ public static int Main() [Parameter("Configuration to build")] public Configuration Configuration { get; } = IsLocalBuild ? Configuration.Debug : Configuration.Release; } + +public static class Extensions +{ + public static T As(this T value) + { + return value; + } +} diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 65c321684..431cc839f 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -33,6 +33,10 @@ "nukeeper": { "version": "0.35.0", "commands": ["nukeeper"] + }, + "strawberryshake.tools": { + "version": "12.6.0", + "commands": ["dotnet-graphql"] } } } diff --git a/.editorconfig b/.editorconfig index 36d7ac44f..39317fa90 100644 --- a/.editorconfig +++ b/.editorconfig @@ -259,6 +259,7 @@ dotnet_naming_style.begins_with_t.capitalization = pascal_case dotnet_diagnostic.ide0058.severity = none dotnet_diagnostic.cs0436.severity = none +dotnet_diagnostic.RCS1008.severity = none # CodeQuality # CA1000: Do not declare static members on generic types diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72494f875..7b1b3e397 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -110,6 +110,9 @@ jobs: - name: ⚙ Build run: | dotnet nuke Build --skip + - name: Update Graph Ql + run: | + dotnet nuke UpdateGraphQl --skip - name: 🚦 Test run: | dotnet nuke Test TriggerCodeCoverageReports GenerateCodeCoverageReportCobertura GenerateCodeCoverageBadges GenerateCodeCoverageSummary GenerateCodeCoverageReport --skip diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index c79abd131..6d511bc81 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -100,7 +100,8 @@ "Restore", "Test", "Trigger_Code_Coverage_Reports", - "TriggerCodeCoverageReports" + "TriggerCodeCoverageReports", + "UpdateGraphQl" ] } }, @@ -135,7 +136,8 @@ "Restore", "Test", "Trigger_Code_Coverage_Reports", - "TriggerCodeCoverageReports" + "TriggerCodeCoverageReports", + "UpdateGraphQl" ] } }, diff --git a/.nuke/parameters.json b/.nuke/parameters.json index aff6906b6..02baa2f67 100644 --- a/.nuke/parameters.json +++ b/.nuke/parameters.json @@ -1,4 +1,4 @@ { - "$schema": "./build.schema.json", - "Solution": "LaunchPad.sln" -} \ No newline at end of file + "$schema": "./build.schema.json", + "Solution": "LaunchPad.sln" +} diff --git a/Directory.Packages.props b/Directory.Packages.props index 34ccb8309..d0ee8845d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -44,7 +44,11 @@ + + + + @@ -76,6 +80,7 @@ + @@ -133,6 +138,9 @@ + + + WARNING ShowAndRun True - <?xml version="1.0" encoding="utf-16"?><Profile name="Full Cleanup"><CSReorderTypeMembers>True</CSReorderTypeMembers><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeArgumentsStyle="True" ArrangeCodeBodyStyle="True" ArrangeVarStyle="True" /><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><IDEA_SETTINGS>&lt;profile version="1.0"&gt; + <?xml version="1.0" encoding="utf-16"?><Profile name="Full Cleanup"><CSReorderTypeMembers>True</CSReorderTypeMembers><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeArgumentsStyle="True" ArrangeCodeBodyStyle="True" ArrangeVarStyle="True" ArrangeNamespaces="True" /><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CSOptimizeUsings></CSOptimizeUsings><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags><IDEA_SETTINGS>&lt;profile version="1.0"&gt; &lt;option name="myName" value="Full Cleanup" /&gt; &lt;inspection_tool class="ES6ShorthandObjectProperty" enabled="false" level="INFORMATION" enabled_by_default="false" /&gt; &lt;inspection_tool class="JSArrowFunctionBracesCanBeRemoved" enabled="false" level="INFORMATION" enabled_by_default="false" /&gt; @@ -17,9 +17,77 @@ &lt;inspection_tool class="UnnecessaryLabelOnBreakStatementJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; &lt;inspection_tool class="UnnecessaryLabelOnContinueStatementJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; &lt;inspection_tool class="UnnecessaryReturnJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; - &lt;inspection_tool class="UnterminatedStatementJS" enabled="false" level="WARNING" enabled_by_default="false" /&gt; &lt;inspection_tool class="WrongPropertyKeyValueDelimiter" enabled="false" level="WEAK WARNING" enabled_by_default="false" /&gt; -&lt;/profile&gt;</IDEA_SETTINGS></Profile> +&lt;/profile&gt;</IDEA_SETTINGS><RIDER_SETTINGS>&lt;profile&gt; + &lt;Language id="CSS"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;/Language&gt; + &lt;Language id="EditorConfig"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="GraphQL"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="GraphQL Endpoint"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="HTML"&gt; + &lt;OptimizeImports&gt;true&lt;/OptimizeImports&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;/Language&gt; + &lt;Language id="HTTP Request"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Handlebars"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Ini"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="JAVA"&gt; + &lt;OptimizeImports&gt;true&lt;/OptimizeImports&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;/Language&gt; + &lt;Language id="JSON"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Jade"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="JavaScript"&gt; + &lt;OptimizeImports&gt;true&lt;/OptimizeImports&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;/Language&gt; + &lt;Language id="Markdown"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="PowerShell"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="Properties"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="RELAX-NG"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="SQL"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="TOML"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; + &lt;Language id="XML"&gt; + &lt;OptimizeImports&gt;true&lt;/OptimizeImports&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;Rearrange&gt;true&lt;/Rearrange&gt; + &lt;/Language&gt; + &lt;Language id="yaml"&gt; + &lt;Reformat&gt;true&lt;/Reformat&gt; + &lt;/Language&gt; +&lt;/profile&gt;</RIDER_SETTINGS><CSShortenReferences>True</CSShortenReferences><RemoveCodeRedundancies>True</RemoveCodeRedundancies></Profile> Full Cleanup <?xml version="1.0" encoding="utf-16"?> <Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns"> diff --git a/sample/Directory.Packages.props b/sample/Directory.Packages.props index 3bd752a4b..bd082a0e8 100644 --- a/sample/Directory.Packages.props +++ b/sample/Directory.Packages.props @@ -1,7 +1,11 @@ - + - \ No newline at end of file + + + false + + diff --git a/sample/Sample.BlazorServer/Pages/Rockets/Delete.razor.cs b/sample/Sample.BlazorServer/Pages/Rockets/Delete.razor.cs index 43fe15445..a80964b31 100644 --- a/sample/Sample.BlazorServer/Pages/Rockets/Delete.razor.cs +++ b/sample/Sample.BlazorServer/Pages/Rockets/Delete.razor.cs @@ -7,7 +7,7 @@ namespace Sample.BlazorServer.Pages.Rockets; public partial class Delete : ComponentBase { - [Parameter] public Guid Id { get; set; } + [Parameter] public RocketId Id { get; set; } public RocketModel Model { get; set; } = null!; diff --git a/sample/Sample.BlazorServer/Pages/Rockets/Edit.razor.cs b/sample/Sample.BlazorServer/Pages/Rockets/Edit.razor.cs index 1458ddf06..f1d5d988c 100644 --- a/sample/Sample.BlazorServer/Pages/Rockets/Edit.razor.cs +++ b/sample/Sample.BlazorServer/Pages/Rockets/Edit.razor.cs @@ -1,13 +1,14 @@ using AutoMapper; using MediatR; using Microsoft.AspNetCore.Components; +using Sample.Core.Models; using Sample.Core.Operations.Rockets; namespace Sample.BlazorServer.Pages.Rockets; public partial class Edit : ComponentBase { - [Parameter] public Guid Id { get; set; } + [Parameter] public RocketId Id { get; set; } public EditRocket.Request Model { get; set; } = new(); diff --git a/sample/Sample.BlazorServer/Pages/Rockets/Index.razor.cs b/sample/Sample.BlazorServer/Pages/Rockets/Index.razor.cs index d250ab45d..aa96810e3 100644 --- a/sample/Sample.BlazorServer/Pages/Rockets/Index.razor.cs +++ b/sample/Sample.BlazorServer/Pages/Rockets/Index.razor.cs @@ -13,6 +13,6 @@ public partial class Index : ComponentBase protected override async Task OnInitializedAsync() { - Rockets = await Mediator.Send(new ListRockets.Request()); + Rockets = await Mediator.CreateStream(new ListRockets.Request(null)).ToListAsync(); } } diff --git a/sample/Sample.BlazorServer/Pages/Rockets/View.razor.cs b/sample/Sample.BlazorServer/Pages/Rockets/View.razor.cs index 45a9abba7..8414577df 100644 --- a/sample/Sample.BlazorServer/Pages/Rockets/View.razor.cs +++ b/sample/Sample.BlazorServer/Pages/Rockets/View.razor.cs @@ -7,7 +7,7 @@ namespace Sample.BlazorServer.Pages.Rockets; public partial class View : ComponentBase { - [Parameter] public Guid Id { get; set; } + [Parameter] public RocketId Id { get; set; } public RocketModel Model { get; set; } = null!; [Inject] private IMediator Mediator { get; set; } = null!; diff --git a/sample/Sample.Core/DataConvention.cs b/sample/Sample.Core/DataConvention.cs index c6ce60552..4807b6d5d 100644 --- a/sample/Sample.Core/DataConvention.cs +++ b/sample/Sample.Core/DataConvention.cs @@ -1,5 +1,6 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Rocket.Surgery.Conventions; @@ -29,6 +30,7 @@ public void Register(IConventionContext context, IConfiguration configuration, I .AddPooledDbContextFactory( #endif x => x + .ReplaceService() .EnableDetailedErrors() .EnableSensitiveDataLogging() .EnableServiceProviderCaching().UseSqlite( diff --git a/sample/Sample.Core/Domain/LaunchRecord.cs b/sample/Sample.Core/Domain/LaunchRecord.cs index 8b9633b95..5733223d3 100644 --- a/sample/Sample.Core/Domain/LaunchRecord.cs +++ b/sample/Sample.Core/Domain/LaunchRecord.cs @@ -1,18 +1,20 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Rocket.Surgery.LaunchPad.EntityFramework; +using Sample.Core.Models; namespace Sample.Core.Domain; -public class LaunchRecord +public class LaunchRecord // : ILaunchRecord { - public Guid Id { get; set; } + public LaunchRecordId Id { get; set; } public string? Partner { get; set; } = null!; public string? Payload { get; set; } = null!; public long PayloadWeightKg { get; set; } public DateTimeOffset? ActualLaunchDate { get; set; } public DateTimeOffset ScheduledLaunchDate { get; set; } - public Guid RocketId { get; set; } + public RocketId RocketId { get; set; } public ReadyRocket Rocket { get; set; } = null!; private class EntityConfiguration : IEntityTypeConfiguration @@ -20,6 +22,19 @@ private class EntityConfiguration : IEntityTypeConfiguration public void Configure(EntityTypeBuilder builder) { builder.HasKey(x => x.Id); + builder.Property(z => z.Id) + .ValueGeneratedOnAdd() + .HasValueGenerator(StronglyTypedIdValueGenerator.Create(LaunchRecordId.New)); } } } + +public interface ILaunchRecord +{ + LaunchRecordId Id { get; set; } + string? Partner { get; set; } + string? Payload { get; set; } + long PayloadWeightKg { get; set; } + DateTimeOffset? ActualLaunchDate { get; set; } + DateTimeOffset ScheduledLaunchDate { get; set; } +} diff --git a/sample/Sample.Core/Domain/ReadyRocket.cs b/sample/Sample.Core/Domain/ReadyRocket.cs index 1f487884b..19baeadc3 100644 --- a/sample/Sample.Core/Domain/ReadyRocket.cs +++ b/sample/Sample.Core/Domain/ReadyRocket.cs @@ -1,14 +1,16 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Rocket.Surgery.LaunchPad.EntityFramework; +using Sample.Core.Models; namespace Sample.Core.Domain; /// /// A rocket in inventory /// -public class ReadyRocket +public class ReadyRocket // : IReadyRocket { - public Guid Id { get; set; } + public RocketId Id { get; set; } public string SerialNumber { get; set; } = null!; public RocketType Type { get; set; } @@ -19,7 +21,17 @@ private class EntityConfiguration : IEntityTypeConfiguration public void Configure(EntityTypeBuilder builder) { builder.HasKey(x => x.Id); + builder.Property(z => z.Id) + .ValueGeneratedOnAdd() + .HasValueGenerator(StronglyTypedIdValueGenerator.Create(RocketId.New)); builder.ToTable("Rockets"); } } } + +public interface IReadyRocket +{ + RocketId Id { get; set; } + string SerialNumber { get; set; } + RocketType Type { get; set; } +} diff --git a/sample/Sample.Core/Domain/RocketDbContext.cs b/sample/Sample.Core/Domain/RocketDbContext.cs index ec9ddcb26..c58eb5faf 100644 --- a/sample/Sample.Core/Domain/RocketDbContext.cs +++ b/sample/Sample.Core/Domain/RocketDbContext.cs @@ -1,4 +1,6 @@ -using Microsoft.EntityFrameworkCore; +using System.Collections.Concurrent; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Rocket.Surgery.LaunchPad.EntityFramework; namespace Sample.Core.Domain; @@ -12,3 +14,60 @@ public class RocketDbContext : LpContext public DbSet Rockets { get; set; } = null!; public DbSet LaunchRecords { get; set; } = null!; } + +public class StronglyTypedIdValueConverterSelector : ValueConverterSelector +{ + private static Type UnwrapNullableType(Type type) + { + if (type is null) + { + return null; + } + + return Nullable.GetUnderlyingType(type) ?? type; + } + + // The dictionary in the base type is private, so we need our own one here. + private readonly ConcurrentDictionary<(Type ModelClrType, Type ProviderClrType), ValueConverterInfo> _converters + = new ConcurrentDictionary<(Type ModelClrType, Type ProviderClrType), ValueConverterInfo>(); + + public StronglyTypedIdValueConverterSelector(ValueConverterSelectorDependencies dependencies) : base(dependencies) + { + } + + public override IEnumerable Select(Type modelClrType, Type providerClrType = null) + { + var baseConverters = base.Select(modelClrType, providerClrType); + foreach (var converter in baseConverters) + { + yield return converter; + } + + // Extract the "real" type T from Nullable if required + var underlyingModelType = UnwrapNullableType(modelClrType); + var underlyingProviderType = UnwrapNullableType(providerClrType); + + // 'null' means 'get any value converters for the modelClrType' + if (underlyingProviderType is null || underlyingProviderType == typeof(Guid)) + { + // Try and get a nested class with the expected name. + var converterType = underlyingModelType.GetNestedType("EfCoreValueConverter"); + + if (converterType != null) + { + yield return _converters.GetOrAdd( + ( underlyingModelType, typeof(Guid) ), + k => + { + // Create an instance of the converter whenever it's requested. + Func factory = + info => (ValueConverter)Activator.CreateInstance(converterType, info.MappingHints); + + // Build the info for our strongly-typed ID => Guid converter + return new ValueConverterInfo(modelClrType, typeof(Guid), factory); + } + ); + } + } + } +} diff --git a/sample/Sample.Core/Domain/RocketType.cs b/sample/Sample.Core/Domain/RocketType.cs index a04637296..629d8a937 100644 --- a/sample/Sample.Core/Domain/RocketType.cs +++ b/sample/Sample.Core/Domain/RocketType.cs @@ -1,8 +1,22 @@ namespace Sample.Core.Domain; +/// +/// The available rocket types +/// public enum RocketType { + /// + /// Your best bet + /// Falcon9, + + /// + /// For those huge payloads + /// FalconHeavy, + + /// + /// We stole our competitors rocket platform! + /// AtlasV } diff --git a/sample/Sample.Core/LaunchRecordFaker.cs b/sample/Sample.Core/LaunchRecordFaker.cs index 67888c7a2..82263c0a5 100644 --- a/sample/Sample.Core/LaunchRecordFaker.cs +++ b/sample/Sample.Core/LaunchRecordFaker.cs @@ -1,5 +1,6 @@ using Bogus; using Sample.Core.Domain; +using Sample.Core.Models; namespace Sample.Core; @@ -7,7 +8,7 @@ public class LaunchRecordFaker : Faker { public LaunchRecordFaker(IReadOnlyCollection rockets) { - RuleFor(x => x.Id, x => x.Random.Guid()); + RuleFor(x => x.Id, x => new LaunchRecordId(x.Random.Guid())); RuleFor(x => x.Partner, x => x.Company.CompanyName()); RuleFor(x => x.Rocket, x => x.PickRandom(rockets.AsEnumerable())); RuleFor(x => x.RocketId, (_, v) => v.Rocket.Id); diff --git a/sample/Sample.Core/Models/LaunchRecordModel.cs b/sample/Sample.Core/Models/LaunchRecordModel.cs index 8f6f67d09..9e45f8146 100644 --- a/sample/Sample.Core/Models/LaunchRecordModel.cs +++ b/sample/Sample.Core/Models/LaunchRecordModel.cs @@ -1,18 +1,67 @@ using AutoMapper; using NodaTime; using Sample.Core.Domain; +using StronglyTypedIds; namespace Sample.Core.Models; +/// +/// Unique Id of a launch record +/// +[StronglyTypedId( + StronglyTypedIdBackingType.Guid, + StronglyTypedIdConverter.SystemTextJson | StronglyTypedIdConverter.EfCoreValueConverter | StronglyTypedIdConverter.TypeConverter +)] +public partial struct LaunchRecordId +{ +} + +/// +/// The launch record details +/// public record LaunchRecordModel { - public Guid Id { get; init; } + /// + /// The launch record id + /// + public LaunchRecordId Id { get; init; } + + /// + /// The launch partner + /// public string Partner { get; init; } = null!; + + /// + /// The payload details + /// public string Payload { get; init; } = null!; + + /// + /// The payload weight in Kg + /// public long PayloadWeightKg { get; init; } + + /// + /// The actual launch date + /// + /// + /// Will be updated when the launch happens + /// public Instant? ActualLaunchDate { get; init; } + + /// + /// The intended date for the launch + /// public Instant ScheduledLaunchDate { get; init; } + + /// + /// The serial number of the reusable rocket + /// public string RocketSerialNumber { get; init; } = null!; + + /// + /// The kind of rocket that will be launching + /// public RocketType RocketType { get; init; } private class Mapper : Profile diff --git a/sample/Sample.Core/Models/RocketModel.cs b/sample/Sample.Core/Models/RocketModel.cs index 177594327..52fdef8cb 100644 --- a/sample/Sample.Core/Models/RocketModel.cs +++ b/sample/Sample.Core/Models/RocketModel.cs @@ -1,12 +1,38 @@ using AutoMapper; using Sample.Core.Domain; +using StronglyTypedIds; namespace Sample.Core.Models; +/// +/// Unique Id of a rocket +/// +[StronglyTypedId( + StronglyTypedIdBackingType.Guid, + StronglyTypedIdConverter.SystemTextJson | StronglyTypedIdConverter.EfCoreValueConverter | StronglyTypedIdConverter.TypeConverter +)] +public partial struct RocketId +{ +} + +/// +/// The details of a given rocket +/// public record RocketModel { - public Guid Id { get; init; } + /// + /// The unique rocket identifier + /// + public RocketId Id { get; init; } + + /// + /// The serial number of the rocket + /// public string Sn { get; init; } = null!; + + /// + /// The type of the rocket + /// public RocketType Type { get; init; } private class Mapper : Profile diff --git a/sample/Sample.Core/Operations/LaunchRecords/CreateLaunchRecord.cs b/sample/Sample.Core/Operations/LaunchRecords/CreateLaunchRecord.cs index 61d48872e..560e04569 100644 --- a/sample/Sample.Core/Operations/LaunchRecords/CreateLaunchRecord.cs +++ b/sample/Sample.Core/Operations/LaunchRecords/CreateLaunchRecord.cs @@ -4,25 +4,58 @@ using NodaTime; using Rocket.Surgery.LaunchPad.Foundation; using Sample.Core.Domain; +using Sample.Core.Models; namespace Sample.Core.Operations.LaunchRecords; [PublicAPI] public static class CreateLaunchRecord { + /// + /// Create a launch record + /// public record Request : IRequest { + /// + /// The rocket to use + /// + public RocketId RocketId { get; set; } // TODO: Make generator that can be used to create a writable view model + + /// + /// The launch partner + /// public string? Partner { get; set; } = null!; // TODO: Make generator that can be used to create a writable view model + + /// + /// The launch partners payload + /// public string? Payload { get; set; } = null!; // TODO: Make generator that can be used to create a writable view model + + /// + /// The payload weight + /// public double PayloadWeightKg { get; set; } // TODO: Make generator that can be used to create a writable view model + + /// + /// The actual launch date + /// public Instant? ActualLaunchDate { get; set; } // TODO: Make generator that can be used to create a writable view model + + /// + /// The intended launch date + /// public Instant ScheduledLaunchDate { get; set; } // TODO: Make generator that can be used to create a writable view model - public Guid RocketId { get; set; } // TODO: Make generator that can be used to create a writable view model } + /// + /// The launch record creation response + /// public record Response { - public Guid Id { get; init; } + /// + /// The id of the new launch record + /// + public LaunchRecordId Id { get; init; } } private class Mapper : Profile diff --git a/sample/Sample.Core/Operations/LaunchRecords/DeleteLaunchRecord.cs b/sample/Sample.Core/Operations/LaunchRecords/DeleteLaunchRecord.cs index 34a568cbe..b835c1828 100644 --- a/sample/Sample.Core/Operations/LaunchRecords/DeleteLaunchRecord.cs +++ b/sample/Sample.Core/Operations/LaunchRecords/DeleteLaunchRecord.cs @@ -2,15 +2,22 @@ using MediatR; using Rocket.Surgery.LaunchPad.Foundation; using Sample.Core.Domain; +using Sample.Core.Models; namespace Sample.Core.Operations.LaunchRecords; [PublicAPI] public static class DeleteLaunchRecord { + /// + /// The request to delete a launch record + /// public record Request : IRequest { - public Guid Id { get; init; } + /// + /// The launch record to delete + /// + public LaunchRecordId Id { get; init; } } [UsedImplicitly] diff --git a/sample/Sample.Core/Operations/LaunchRecords/EditLaunchRecord.cs b/sample/Sample.Core/Operations/LaunchRecords/EditLaunchRecord.cs index ef0b82e55..4bf29c68b 100644 --- a/sample/Sample.Core/Operations/LaunchRecords/EditLaunchRecord.cs +++ b/sample/Sample.Core/Operations/LaunchRecords/EditLaunchRecord.cs @@ -12,26 +12,45 @@ namespace Sample.Core.Operations.LaunchRecords; [PublicAPI] public static partial class EditLaunchRecord { - // TODO: Make generator that can be used to map this directly - public static Request CreateRequest(Guid id, Model model, IMapper mapper) + /// + /// The launch record update request + /// + public partial record Request : IRequest { - return mapper.Map(model, new Request { Id = id }); - } + /// + /// The launch record to update + /// + public LaunchRecordId Id { get; init; } - public record Model - { + /// + /// The updated launch partner + /// public string Partner { get; set; } = null!; // TODO: Make generator that can be used to create a writable view model + + /// + /// The updated launch payload + /// public string Payload { get; set; } = null!; // TODO: Make generator that can be used to create a writable view model + + /// + /// The updated payload weight + /// public double PayloadWeightKg { get; set; } // TODO: Make generator that can be used to create a writable view model + + /// + /// The updated actual launch date + /// public Instant? ActualLaunchDate { get; set; } // TODO: Make generator that can be used to create a writable view model + + /// + /// The scheduled launch date + /// public Instant ScheduledLaunchDate { get; set; } // TODO: Make generator that can be used to create a writable view model - public Guid RocketId { get; set; } // TODO: Make generator that can be used to create a writable view model - } - [InheritFrom(typeof(Model))] - public partial record Request : Model, IRequest - { - public Guid Id { get; init; } + /// + /// The update rocket id + /// + public RocketId RocketId { get; set; } // TODO: Make generator that can be used to create a writable view model } private class Mapper : Profile @@ -42,43 +61,39 @@ public Mapper() .ForMember(x => x.Rocket, x => x.Ignore()) .ForMember(x => x.Id, x => x.Ignore()) ; - CreateMap() - .ForMember(z => z.Id, z => z.Ignore()); } } - private class ModelValidator : AbstractValidator + private class Validator : AbstractValidator { - public ModelValidator() + public Validator() { + RuleFor(z => z.Id) + .NotEmpty() + .NotNull(); + RuleFor(x => x.Partner) .NotEmpty() .NotNull(); + RuleFor(x => x.RocketId) .NotEmpty() .NotNull(); + RuleFor(x => x.Payload) .NotEmpty() .NotNull(); + RuleFor(x => x.ActualLaunchDate); + RuleFor(x => x.ScheduledLaunchDate) .NotNull(); + RuleFor(x => x.PayloadWeightKg) .GreaterThanOrEqualTo(0d); } } - private class Validator : AbstractValidator - { - public Validator() - { - Include(new ModelValidator()); - RuleFor(z => z.Id) - .NotEmpty() - .NotNull(); - } - } - private class Handler : IRequestHandler { private readonly RocketDbContext _dbContext; diff --git a/sample/Sample.Core/Operations/LaunchRecords/GetLaunchRecord.cs b/sample/Sample.Core/Operations/LaunchRecords/GetLaunchRecord.cs index 728cdcb1b..62c9b2514 100644 --- a/sample/Sample.Core/Operations/LaunchRecords/GetLaunchRecord.cs +++ b/sample/Sample.Core/Operations/LaunchRecords/GetLaunchRecord.cs @@ -11,9 +11,15 @@ namespace Sample.Core.Operations.LaunchRecords; [PublicAPI] public static class GetLaunchRecord { + /// + /// The request to get a launch record + /// public record Request : IRequest { - public Guid Id { get; init; } + /// + /// The launch record to find + /// + public LaunchRecordId Id { get; init; } } private class Validator : AbstractValidator diff --git a/sample/Sample.Core/Operations/LaunchRecords/ListLaunchRecords.cs b/sample/Sample.Core/Operations/LaunchRecords/ListLaunchRecords.cs index 0ee1e83d7..3d434cf18 100644 --- a/sample/Sample.Core/Operations/LaunchRecords/ListLaunchRecords.cs +++ b/sample/Sample.Core/Operations/LaunchRecords/ListLaunchRecords.cs @@ -11,14 +11,18 @@ namespace Sample.Core.Operations.LaunchRecords; [PublicAPI] public static class ListLaunchRecords { + /// + /// The launch record search + /// + /// The rocket type // TODO: Paging model! - public record Request : IRequest>; + public record Request(RocketType? RocketType) : IStreamRequest; private class Validator : AbstractValidator { } - private class Handler : IRequestHandler> + private class Handler : IStreamRequestHandler { private readonly RocketDbContext _dbContext; private readonly IMapper _mapper; @@ -29,15 +33,19 @@ public Handler(RocketDbContext dbContext, IMapper mapper) _mapper = mapper; } - public async Task> Handle(Request request, CancellationToken cancellationToken) + public IAsyncEnumerable Handle(Request request, CancellationToken cancellationToken) { - return ( - await _dbContext.LaunchRecords - .Include(x => x.Rocket) - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(cancellationToken) - ) - .ToArray(); + var query = _dbContext.LaunchRecords + .Include(x => x.Rocket) + .AsQueryable(); + if (request.RocketType.HasValue) + { + query = query.Where(z => z.Rocket.Type == request.RocketType); + } + + return query + .ProjectTo(_mapper.ConfigurationProvider) + .ToAsyncEnumerable(); } } } diff --git a/sample/Sample.Core/Operations/Rockets/CreateRocket.cs b/sample/Sample.Core/Operations/Rockets/CreateRocket.cs index 5744f3ef4..63833a024 100644 --- a/sample/Sample.Core/Operations/Rockets/CreateRocket.cs +++ b/sample/Sample.Core/Operations/Rockets/CreateRocket.cs @@ -4,21 +4,38 @@ using Microsoft.EntityFrameworkCore; using Rocket.Surgery.LaunchPad.Foundation; using Sample.Core.Domain; +using Sample.Core.Models; namespace Sample.Core.Operations.Rockets; [PublicAPI] public static class CreateRocket { + /// + /// The operation to create a new rocket record + /// public record Request : IRequest { + /// + /// The serial number of the rocket + /// public string SerialNumber { get; set; } = null!; // TODO: Make generator that can be used to create a writable view model + + /// + /// The type of rocket + /// public RocketType Type { get; set; } // TODO: Make generator that can be used to create a writable view model } + /// + /// The identifier of the rocket that was created + /// public record Response { - public Guid Id { get; init; } + /// + /// The rocket id + /// + public RocketId Id { get; init; } } private class Mapper : Profile diff --git a/sample/Sample.Core/Operations/Rockets/DeleteRocket.cs b/sample/Sample.Core/Operations/Rockets/DeleteRocket.cs index 106d8921b..3e5581736 100644 --- a/sample/Sample.Core/Operations/Rockets/DeleteRocket.cs +++ b/sample/Sample.Core/Operations/Rockets/DeleteRocket.cs @@ -2,15 +2,22 @@ using MediatR; using Rocket.Surgery.LaunchPad.Foundation; using Sample.Core.Domain; +using Sample.Core.Models; namespace Sample.Core.Operations.Rockets; [PublicAPI] public static class DeleteRocket { + /// + /// The request to remove a rocket from the system + /// public record Request : IRequest { - public Guid Id { get; init; } + /// + /// The rocket id + /// + public RocketId Id { get; init; } } private class Validator : AbstractValidator diff --git a/sample/Sample.Core/Operations/Rockets/EditRocket.cs b/sample/Sample.Core/Operations/Rockets/EditRocket.cs index a823e673a..c759e343f 100644 --- a/sample/Sample.Core/Operations/Rockets/EditRocket.cs +++ b/sample/Sample.Core/Operations/Rockets/EditRocket.cs @@ -10,19 +10,25 @@ namespace Sample.Core.Operations.Rockets; [PublicAPI] public static partial class EditRocket { - public record Model + /// + /// The edit operation to update a rocket + /// + public record Request : IRequest { - // TODO: Make generator that can be used to create a writable view model - public string SerialNumber { get; set; } = null!; + /// + /// The rocket id + /// + public RocketId Id { get; init; } - // TODO: Make generator that can be used to create a writable view model - public RocketType Type { get; set; } - } + /// + /// The serial number of the rocket + /// + public string SerialNumber { get; set; } = null!; // TODO: Make generator that can be used to create a writable view model - [InheritFrom(typeof(Model))] - public partial record Request : Model, IRequest - { - public Guid Id { get; init; } + /// + /// The type of the rocket + /// + public RocketType Type { get; set; } // TODO: Make generator that can be used to create a writable view model } private class Mapper : Profile @@ -39,10 +45,14 @@ public Mapper() } } - private class ModelValidator : AbstractValidator + private class RequestValidator : AbstractValidator { - public ModelValidator() + public RequestValidator() { + RuleFor(x => x.Id) + .NotEmpty() + .NotNull(); + RuleFor(x => x.Type) .NotNull() .IsInEnum(); @@ -54,17 +64,6 @@ public ModelValidator() } } - private class RequestValidator : AbstractValidator - { - public RequestValidator() - { - Include(new ModelValidator()); - RuleFor(x => x.Id) - .NotEmpty() - .NotNull(); - } - } - private class Handler : IRequestHandler { private readonly RocketDbContext _dbContext; diff --git a/sample/Sample.Core/Operations/Rockets/GetRocket.cs b/sample/Sample.Core/Operations/Rockets/GetRocket.cs index b77534e30..bf5ac0813 100644 --- a/sample/Sample.Core/Operations/Rockets/GetRocket.cs +++ b/sample/Sample.Core/Operations/Rockets/GetRocket.cs @@ -10,9 +10,15 @@ namespace Sample.Core.Operations.Rockets; [PublicAPI] public static class GetRocket { + /// + /// Request to fetch information about a rocket + /// public record Request : IRequest { - public Guid Id { get; set; } + /// + /// The rocket id + /// + public RocketId Id { get; set; } } private class Validator : AbstractValidator diff --git a/sample/Sample.Core/Operations/Rockets/GetRocketLaunchRecord.cs b/sample/Sample.Core/Operations/Rockets/GetRocketLaunchRecord.cs new file mode 100644 index 000000000..f6b3f13f0 --- /dev/null +++ b/sample/Sample.Core/Operations/Rockets/GetRocketLaunchRecord.cs @@ -0,0 +1,67 @@ +using AutoMapper; +using FluentValidation; +using MediatR; +using Rocket.Surgery.LaunchPad.Foundation; +using Sample.Core.Domain; +using Sample.Core.Models; + +namespace Sample.Core.Operations.Rockets; + +[PublicAPI] +public static class GetRocketLaunchRecord +{ + public record Request : IRequest + { + /// + /// The rocket id + /// + public RocketId Id { get; init; } + + /// + /// The launch record id + /// + public LaunchRecordId LaunchRecordId { get; init; } + } + + private class Validator : AbstractValidator + { + public Validator() + { + RuleFor(x => x.Id) + .NotEmpty() + .NotNull(); + RuleFor(x => x.LaunchRecordId) + .NotEmpty() + .NotNull(); + } + } + + private class Handler : IRequestHandler + { + private readonly RocketDbContext _dbContext; + private readonly IMapper _mapper; + + public Handler(RocketDbContext dbContext, IMapper mapper) + { + _dbContext = dbContext; + _mapper = mapper; + } + + public async Task Handle(Request request, CancellationToken cancellationToken) + { + var rocket = await _dbContext.Rockets.FindAsync(new object[] { request.Id }, cancellationToken); + if (rocket == null) + { + throw new NotFoundException(); + } + + var launchRecord = _dbContext.LaunchRecords.FindAsync(new object[] { request.LaunchRecordId }, cancellationToken); + if (launchRecord == null) + { + throw new NotFoundException(); + } + + return _mapper.Map(launchRecord); + } + } +} diff --git a/sample/Sample.Core/Operations/Rockets/GetRocketLaunchRecords.cs b/sample/Sample.Core/Operations/Rockets/GetRocketLaunchRecords.cs new file mode 100644 index 000000000..1042123fe --- /dev/null +++ b/sample/Sample.Core/Operations/Rockets/GetRocketLaunchRecords.cs @@ -0,0 +1,62 @@ +using System.Runtime.CompilerServices; +using AutoMapper; +using AutoMapper.QueryableExtensions; +using FluentValidation; +using MediatR; +using Microsoft.EntityFrameworkCore; +using Rocket.Surgery.LaunchPad.Foundation; +using Sample.Core.Domain; +using Sample.Core.Models; + +namespace Sample.Core.Operations.Rockets; + +[PublicAPI] +public static class GetRocketLaunchRecords +{ + public record Request : IStreamRequest + { + /// + /// The rocket id + /// + public RocketId Id { get; init; } + } + + private class Validator : AbstractValidator + { + public Validator() + { + RuleFor(x => x.Id) + .NotEmpty() + .NotNull(); + } + } + + private class Handler : IStreamRequestHandler + { + private readonly RocketDbContext _dbContext; + private readonly IMapper _mapper; + + public Handler(RocketDbContext dbContext, IMapper mapper) + { + _dbContext = dbContext; + _mapper = mapper; + } + + public async IAsyncEnumerable Handle(Request request, [EnumeratorCancellation] CancellationToken cancellationToken) + { + var rocket = await _dbContext.Rockets.FindAsync(new object[] { request.Id }, cancellationToken); + if (rocket == null) + { + throw new NotFoundException(); + } + + var query = _dbContext.LaunchRecords.AsQueryable() + .Where(z => z.RocketId == rocket.Id) + .ProjectTo(_mapper.ConfigurationProvider); + await foreach (var item in query.AsAsyncEnumerable().WithCancellation(cancellationToken)) + { + yield return item; + } + } + } +} diff --git a/sample/Sample.Core/Operations/Rockets/ListRockets.cs b/sample/Sample.Core/Operations/Rockets/ListRockets.cs index 29aca47c8..05d638b60 100644 --- a/sample/Sample.Core/Operations/Rockets/ListRockets.cs +++ b/sample/Sample.Core/Operations/Rockets/ListRockets.cs @@ -12,13 +12,17 @@ namespace Sample.Core.Operations.Rockets; public static class ListRockets { // TODO: Paging model! - public record Request : IRequest>; + /// + /// The request to search for different rockets + /// + /// The type of the rocket + public record Request(RocketType? RocketType) : IStreamRequest; private class Validator : AbstractValidator { } - private class Handler : IRequestHandler> + private class Handler : IStreamRequestHandler { private readonly RocketDbContext _dbContext; private readonly IMapper _mapper; @@ -29,12 +33,15 @@ public Handler(RocketDbContext dbContext, IMapper mapper) _mapper = mapper; } - public async Task> Handle(Request request, CancellationToken cancellationToken) + public IAsyncEnumerable Handle(Request request, CancellationToken cancellationToken) { - return await _dbContext.Rockets - .ProjectTo(_mapper.ConfigurationProvider) - .ToListAsync(cancellationToken) - .ConfigureAwait(false); + var query = _dbContext.Rockets.AsQueryable(); + if (request.RocketType.HasValue) + { + query = query.Where(z => z.Type == request.RocketType); + } + + return query.ProjectTo(_mapper.ConfigurationProvider).AsAsyncEnumerable(); } } } diff --git a/sample/Sample.Core/RocketFaker.cs b/sample/Sample.Core/RocketFaker.cs index 5ae2b3c44..68934f933 100644 --- a/sample/Sample.Core/RocketFaker.cs +++ b/sample/Sample.Core/RocketFaker.cs @@ -1,5 +1,6 @@ using Bogus; using Sample.Core.Domain; +using Sample.Core.Models; namespace Sample.Core; @@ -7,7 +8,7 @@ public class RocketFaker : Faker { public RocketFaker() { - RuleFor(x => x.Id, x => x.Random.Guid()); + RuleFor(x => x.Id, x => new RocketId(x.Random.Guid())); RuleFor(x => x.Type, x => x.PickRandom()); RuleFor(x => x.SerialNumber, x => x.Vehicle.Vin()); } diff --git a/sample/Sample.Core/Sample.Core.csproj b/sample/Sample.Core/Sample.Core.csproj index e9fa5f925..6969c4d05 100644 --- a/sample/Sample.Core/Sample.Core.csproj +++ b/sample/Sample.Core/Sample.Core.csproj @@ -2,18 +2,26 @@ netstandard2.1;net6.0 + true + + - + diff --git a/sample/Sample.Graphql/ConfigureLaunchRecordType.cs b/sample/Sample.Graphql/ConfigureLaunchRecordType.cs index 71dfb15a4..09b08f08e 100644 --- a/sample/Sample.Graphql/ConfigureLaunchRecordType.cs +++ b/sample/Sample.Graphql/ConfigureLaunchRecordType.cs @@ -3,6 +3,7 @@ using HotChocolate.Types.Pagination; using Rocket.Surgery.LaunchPad.EntityFramework.HotChocolate; using Sample.Core.Domain; +using Sample.Core.Models; namespace Sample.Graphql; @@ -12,7 +13,7 @@ public override void Configure(IObjectFieldDescriptor fieldDescriptor) { fieldDescriptor .UsePaging( - typeof(ObjectType), + typeof(ObjectType), options: new PagingOptions { DefaultPageSize = 10, @@ -21,7 +22,7 @@ public override void Configure(IObjectFieldDescriptor fieldDescriptor) } ) .UseOffsetPaging( - typeof(ObjectType), + typeof(ObjectType), options: new PagingOptions { DefaultPageSize = 10, @@ -29,14 +30,14 @@ public override void Configure(IObjectFieldDescriptor fieldDescriptor) MaxPageSize = 20 } ) - .UseProjection() - .UseFiltering() +// .UseProjection() + .UseFiltering(x => x.Ignore(x => x.ActualLaunchDate)) .UseSorting(); } - private class LaunchRecordSort : SortInputType + private class LaunchRecordSort : SortInputType { - protected override void Configure(ISortInputTypeDescriptor descriptor) + protected override void Configure(ISortInputTypeDescriptor descriptor) { descriptor.BindFieldsExplicitly(); descriptor.Field(z => z.Partner); diff --git a/sample/Sample.Graphql/ConfigureReadyRocketType.cs b/sample/Sample.Graphql/ConfigureReadyRocketType.cs index 49841ac7b..cc82a985d 100644 --- a/sample/Sample.Graphql/ConfigureReadyRocketType.cs +++ b/sample/Sample.Graphql/ConfigureReadyRocketType.cs @@ -3,6 +3,7 @@ using HotChocolate.Types.Pagination; using Rocket.Surgery.LaunchPad.EntityFramework.HotChocolate; using Sample.Core.Domain; +using Sample.Core.Models; namespace Sample.Graphql; @@ -12,7 +13,7 @@ public override void Configure(IObjectFieldDescriptor fieldDescriptor) { fieldDescriptor .UsePaging( - typeof(ObjectType), + typeof(ObjectType), options: new PagingOptions { DefaultPageSize = 10, @@ -25,13 +26,13 @@ public override void Configure(IObjectFieldDescriptor fieldDescriptor) .UseSorting(); } - private class RocketSort : SortInputType + private class RocketSort : SortInputType { - protected override void Configure(ISortInputTypeDescriptor descriptor) + protected override void Configure(ISortInputTypeDescriptor descriptor) { descriptor.BindFieldsExplicitly(); descriptor.Field(z => z.Type); - descriptor.Field(z => z.SerialNumber); + descriptor.Field(z => z.Sn); } } } diff --git a/sample/Sample.Graphql/Program.cs b/sample/Sample.Graphql/Program.cs index 3d407d490..8b4c3cb8a 100644 --- a/sample/Sample.Graphql/Program.cs +++ b/sample/Sample.Graphql/Program.cs @@ -1,6 +1,7 @@ using Microsoft.Extensions.DependencyModel; using Rocket.Surgery.Conventions; using Rocket.Surgery.Hosting; +using Rocket.Surgery.LaunchPad.HotChocolate; namespace Sample.Graphql; @@ -17,7 +18,23 @@ public static void Main(string[] args) public static IHostBuilder CreateHostBuilder(string[] args) { return Host.CreateDefaultBuilder(args) - .LaunchWith(RocketBooster.ForDependencyContext(DependencyContext.Default), z => z.WithConventionsFrom(GetConventions)) + .LaunchWith( + RocketBooster.ForDependencyContext(DependencyContext.Default), + z => z + .WithConventionsFrom(GetConventions) + .Set( + new RocketChocolateOptions + { + RequestPredicate = type => + type is { IsNested: true, DeclaringType: { } } + && !( type.Name.StartsWith("Get", StringComparison.Ordinal) + || type.Name.StartsWith( + "List", StringComparison.Ordinal + ) ), + IncludeAssemblyInfoQuery = true + } + ) + ) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); } } diff --git a/sample/Sample.Graphql/Properties/launchSettings.json b/sample/Sample.Graphql/Properties/launchSettings.json new file mode 100644 index 000000000..3e7303144 --- /dev/null +++ b/sample/Sample.Graphql/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:9140", + "sslPort": 44304 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "graphql", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Sample.Restful": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "graphql", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/sample/Sample.Graphql/Sample.Graphql.csproj b/sample/Sample.Graphql/Sample.Graphql.csproj index 35ff6d19f..ac16dc6ae 100644 --- a/sample/Sample.Graphql/Sample.Graphql.csproj +++ b/sample/Sample.Graphql/Sample.Graphql.csproj @@ -2,6 +2,7 @@ net6.0 + Sample.Graphql.Executable @@ -12,6 +13,12 @@ + + + + + + diff --git a/sample/Sample.Graphql/Startup.cs b/sample/Sample.Graphql/Startup.cs index 827264283..c6e3336e3 100644 --- a/sample/Sample.Graphql/Startup.cs +++ b/sample/Sample.Graphql/Startup.cs @@ -1,6 +1,11 @@ -using Rocket.Surgery.LaunchPad.AspNetCore; -using Rocket.Surgery.LaunchPad.EntityFramework.HotChocolate; +using HotChocolate; +using HotChocolate.Data.Filters; +using HotChocolate.Data.Sorting; +using HotChocolate.Types; +using HotChocolate.Types.Pagination; +using Rocket.Surgery.LaunchPad.AspNetCore; using Sample.Core.Domain; +using Sample.Core.Models; using Serilog; namespace Sample.Graphql; @@ -13,23 +18,33 @@ public void ConfigureServices(IServiceCollection services) { services .AddGraphQLServer() - .AddDefaultTransactionScopeHandler() +// .AddDefaultTransactionScopeHandler() .AddQueryType() .AddMutationType() + .ModifyRequestOptions( + options => { options.IncludeExceptionDetails = true; } + ) + .AddTypeConverter(source => source.Value) + .AddTypeConverter(source => new RocketId(source)) + .AddTypeConverter(source => source.Value) + .AddTypeConverter(source => new LaunchRecordId(source)) .ConfigureSchema( s => { - s.AddType( - new ConfigureConfigureEntityFrameworkContextQueryType( - new ConfigureReadyRocketType(), - new ConfigureLaunchRecordType() - ) - ); + s.AddType(); + s.AddType(); + s.AddType(); + + s.BindClrType(); + s.BindClrType(); + s.BindRuntimeType(ScalarNames.UUID); + s.BindRuntimeType(ScalarNames.UUID); } ) .AddSorting() .AddFiltering() - .AddProjections(); + .AddProjections() + .AddConvention(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -52,3 +67,189 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) ); } } + +[ExtendObjectType(OperationTypeNames.Query)] +public class QueryType : ObjectTypeExtension +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor + .Field(t => t.GetLaunchRecords(default)) + .UsePaging>>( + options: new PagingOptions + { + DefaultPageSize = 10, + IncludeTotalCount = true, + MaxPageSize = 20 + } + ) + .UseOffsetPaging>>( + options: new PagingOptions + { + DefaultPageSize = 10, + IncludeTotalCount = true, + MaxPageSize = 20 + } + ) + .UseProjection() + .UseFiltering() + .UseSorting(); + descriptor + .Field(t => t.GetRockets(default)) + .UsePaging>>( + options: new PagingOptions + { + DefaultPageSize = 10, + IncludeTotalCount = true, + MaxPageSize = 20 + } + ) + .UseOffsetPaging>>( + options: new PagingOptions + { + DefaultPageSize = 10, + IncludeTotalCount = true, + MaxPageSize = 20 + } + ) + .UseProjection() + .UseFiltering() + .UseSorting(); + } +} + +internal class LaunchRecordSort : SortInputType +{ + protected override void Configure(ISortInputTypeDescriptor descriptor) + { + descriptor.BindFieldsExplicitly(); + descriptor.Field(z => z.Partner); + descriptor.Field(z => z.Payload); + descriptor.Field(z => z.PayloadWeightKg); + descriptor.Field(z => z.ActualLaunchDate); + descriptor.Field(z => z.ScheduledLaunchDate); + } +} + +internal class RocketSort : SortInputType +{ + protected override void Configure(ISortInputTypeDescriptor descriptor) + { + descriptor.BindFieldsExplicitly(); + descriptor.Field(z => z.Type); + descriptor.Field(z => z.SerialNumber); + } +} + +[ExtendObjectType(OperationTypeNames.Query)] +public class Query +{ + public IQueryable GetLaunchRecords([Service] RocketDbContext dbContext) + { + return dbContext.LaunchRecords; + } + + public IQueryable GetRockets([Service] RocketDbContext dbContext) + { + return dbContext.Rockets; + } +} + +public class CustomFilterConventionExtension : FilterConventionExtension +{ + protected override void Configure(IFilterConventionDescriptor descriptor) + { + base.Configure(descriptor); + + descriptor + .BindRuntimeType() + .BindRuntimeType(); + } +} + +public class LaunchRecordBase : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Name(nameof(LaunchRecordBase)); + descriptor.Field(z => z.Rocket).Ignore(); + descriptor.Field(z => z.RocketId).Ignore(); + } +} + +public class ReadyRocketBase : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { + descriptor.Name(nameof(ReadyRocketBase)); + descriptor.Field(z => z.LaunchRecords).Ignore(); + } +} + +public class ReadyRocketType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { +// descriptor.Implements>(); + descriptor.Field(z => z.LaunchRecords) + .Type>>>() + .UseFiltering() + .UseSorting(); +// descriptor.Ignore(z => z.LaunchRecords); + } +} + +internal class LaunchRecordSortBase : SortInputType +{ + protected override void Configure(ISortInputTypeDescriptor descriptor) + { + descriptor.Field(nameof(LaunchRecord.Partner)); + descriptor.Field(nameof(LaunchRecord.Payload)); + descriptor.Field(nameof(LaunchRecord.PayloadWeightKg)); + descriptor.Field(nameof(LaunchRecord.ActualLaunchDate)); + descriptor.Field(nameof(LaunchRecord.ScheduledLaunchDate)); + } +} + +public class LaunchRecordType : ObjectType +{ + protected override void Configure(IObjectTypeDescriptor descriptor) + { +// descriptor.Implements>(); + descriptor.Field(z => z.Rocket) + .Type>(); + descriptor.Field(z => z.RocketId).Ignore(); +// descriptor.Ignore(z => z.LaunchRecords); + } +} + +public class StronglyTypedIdOperationFilterInputType + : FilterInputType + , IComparableOperationFilterInputType +{ + public StronglyTypedIdOperationFilterInputType() + { + } + + public StronglyTypedIdOperationFilterInputType(Action configure) + : base(configure) + { + } + + protected override void Configure(IFilterInputTypeDescriptor descriptor) + { + descriptor.Operation(DefaultFilterOperations.Equals) + .Type(); + + descriptor.Operation(DefaultFilterOperations.NotEquals) + .Type(); + + descriptor.Operation(DefaultFilterOperations.In) + .Type>(); + + descriptor.Operation(DefaultFilterOperations.NotIn) + .Type>(); + + descriptor.AllowAnd(false).AllowOr(false); + } +} diff --git a/sample/Sample.Grpc/Protos/rockets.proto b/sample/Sample.Grpc/Protos/rockets.proto index 39ca28699..fd0050245 100644 --- a/sample/Sample.Grpc/Protos/rockets.proto +++ b/sample/Sample.Grpc/Protos/rockets.proto @@ -34,7 +34,7 @@ message GetLaunchRecordRequest } message ListLaunchRecordsRequest { - + NullableRocketType RocketType = 1; } message ListLaunchRecordsResponse { @@ -72,6 +72,12 @@ enum RocketType { FalconHeavy = 1; AtlasV = 2; } +message NullableRocketType { + oneof kind { + google.protobuf.NullValue null = 1; + RocketType data = 2; + } +} message CreateRocketRequest { string SerialNumber = 1; @@ -107,6 +113,7 @@ message RocketModel { message ListRocketsRequest { + NullableRocketType RocketType = 1; } message ListRocketsResponse @@ -120,7 +127,7 @@ package greet; service LaunchRecords { // Sends a greeting rpc GetLaunchRecords (GetLaunchRecordRequest) returns (LaunchRecordModel); - rpc ListLaunchRecords (ListLaunchRecordsRequest) returns (ListLaunchRecordsResponse); + rpc ListLaunchRecords (ListLaunchRecordsRequest) returns (stream LaunchRecordModel); rpc CreateLaunchRecord (CreateLaunchRecordRequest) returns (CreateLaunchRecordResponse); rpc EditLaunchRecord (UpdateLaunchRecordRequest) returns (LaunchRecordModel); rpc DeleteLaunchRecord (DeleteLaunchRecordRequest) returns (google.protobuf.Empty); @@ -129,7 +136,7 @@ service LaunchRecords { // The greeting service definition. service Rockets { rpc GetRockets (GetRocketRequest) returns (RocketModel); - rpc ListRockets (ListRocketsRequest) returns (ListRocketsResponse); + rpc ListRockets (ListRocketsRequest) returns (stream RocketModel); rpc CreateRocket (CreateRocketRequest) returns (CreateRocketResponse); rpc EditRocket (UpdateRocketRequest) returns (RocketModel); rpc DeleteRocket(DeleteRocketRequest) returns (google.protobuf.Empty); diff --git a/sample/Sample.Grpc/Services/LaunchRecordsService.cs b/sample/Sample.Grpc/Services/LaunchRecordsService.cs index 0e65d80f9..adc57c1d8 100644 --- a/sample/Sample.Grpc/Services/LaunchRecordsService.cs +++ b/sample/Sample.Grpc/Services/LaunchRecordsService.cs @@ -46,14 +46,15 @@ public override async Task GetLaunchRecords(GetLaunchRecordRe return _mapper.Map(response); } - public override async Task ListLaunchRecords(ListLaunchRecordsRequest request, ServerCallContext context) + public override async Task ListLaunchRecords( + ListLaunchRecordsRequest request, IServerStreamWriter responseStream, ServerCallContext context + ) { var mRequest = _mapper.Map(request); - var response = await _mediator.Send(mRequest, context.CancellationToken); - return new ListLaunchRecordsResponse + await foreach (var item in _mediator.CreateStream(mRequest, context.CancellationToken)) { - Results = { response.Select(_mapper.Map) } - }; + await responseStream.WriteAsync(_mapper.Map(item)); + } } [UsedImplicitly] diff --git a/sample/Sample.Grpc/Services/RocketsService.cs b/sample/Sample.Grpc/Services/RocketsService.cs index 04be3e0f1..c6b91d575 100644 --- a/sample/Sample.Grpc/Services/RocketsService.cs +++ b/sample/Sample.Grpc/Services/RocketsService.cs @@ -3,6 +3,7 @@ using Google.Protobuf.WellKnownTypes; using Grpc.Core; using MediatR; +using Sample.Core.Models; using Sample.Core.Operations.Rockets; namespace Sample.Grpc.Services; @@ -42,13 +43,13 @@ public override async Task GetRockets(GetRocketRequest request, Ser return _mapper.Map(response); } - public override async Task ListRockets(ListRocketsRequest request, ServerCallContext context) + public override async Task ListRockets(ListRocketsRequest request, IServerStreamWriter responseStream, ServerCallContext context) { - var response = await _mediator.Send(_mapper.Map(request), context.CancellationToken); - return new ListRocketsResponse + var mRequest = _mapper.Map(request); + await foreach (var item in _mediator.CreateStream(mRequest, context.CancellationToken)) { - Results = { response.Select(_mapper.Map) } - }; + await responseStream.WriteAsync(_mapper.Map(item)); + } } [UsedImplicitly] @@ -63,6 +64,21 @@ public Mapper() CreateMap(); CreateMap(); CreateMap(); + CreateMap().ConvertUsing(x => x.Value.ToString()); + CreateMap().ConvertUsing(x => new RocketId(Guid.Parse(x))); + CreateMap().ConvertUsing(x => x.Value.ToString()); + CreateMap().ConvertUsing(x => new LaunchRecordId(Guid.Parse(x))); + + CreateMap().ConvertUsing( + static ts => + ts != null && ts.KindCase == NullableRocketType.KindOneofCase.Data + ? (Core.Domain.RocketType)ts.Data + : default + ); + CreateMap().ConvertUsing( + static ts => + new NullableRocketType { Data = (RocketType)ts } + ); } } diff --git a/sample/Sample.Pages/Pages/Rockets/Index.cshtml.cs b/sample/Sample.Pages/Pages/Rockets/Index.cshtml.cs index d579e7134..ddfbe9305 100644 --- a/sample/Sample.Pages/Pages/Rockets/Index.cshtml.cs +++ b/sample/Sample.Pages/Pages/Rockets/Index.cshtml.cs @@ -20,6 +20,6 @@ public RocketIndexModel(IMediator mediator) public async Task OnGet() { - Rockets = await _mediator.Send(new ListRockets.Request()); + Rockets = await _mediator.CreateStream(new ListRockets.Request(null)).ToListAsync(HttpContext.RequestAborted); } } diff --git a/sample/Sample.Pages/Pages/Rockets/View.cshtml.cs b/sample/Sample.Pages/Pages/Rockets/View.cshtml.cs index 4dbc236cb..8162d4f0c 100644 --- a/sample/Sample.Pages/Pages/Rockets/View.cshtml.cs +++ b/sample/Sample.Pages/Pages/Rockets/View.cshtml.cs @@ -9,7 +9,7 @@ public class RocketViewModel : MediatorPageModel { [UsedImplicitly] [BindProperty(SupportsGet = true)] - public Guid Id { get; set; } + public RocketId Id { get; set; } public RocketModel Rocket { get; set; } = null!; diff --git a/sample/Sample.Restful.Client/Sample.Restful.Client.csproj b/sample/Sample.Restful.Client/Sample.Restful.Client.csproj index 5ccbe753e..7861938a3 100644 --- a/sample/Sample.Restful.Client/Sample.Restful.Client.csproj +++ b/sample/Sample.Restful.Client/Sample.Restful.Client.csproj @@ -7,7 +7,7 @@ @@ -18,6 +18,8 @@ ClassName="{controller}Client" CodeGenerator="NSwagCSharp" Namespace="$(RootNamespace)" + PrivateAssets="All" + ReferenceOutputAssembly="false" > /generateClientClasses:true /generateClientInterfaces:true /injectHttpClient:true /disposeHttpClient:false /generateExceptionClasses:true /wrapDtoExceptions:true /useBaseUrl:false /generateBaseUrlProperty:false /operationGenerationMode:"MultipleClientsFromFirstTagAndOperationId" /generateOptionalParameters:true /generateJsonMethods:false /enforceFlagEnums:true /parameterArrayType:"System.Collections.Generic.IEnumerable" /parameterDictionaryType:"System.Collections.Generic.IDictionary" /responseArrayType:"System.Collections.Generic.ICollection" /responseDictionaryType:"System.Collections.Generic.IDictionary" /wrapResponses:true /generateResponseClasses:true /responseClass:"Response" /requiredPropertiesMustBeDefined:true /dateType:"System.DateTimeOffset" /dateTimeType:"System.DateTimeOffset" /timeType:"System.TimeSpan" /timeSpanType:"System.TimeSpan" /arrayType:"System.Collections.ObjectModel.Collection" /arrayInstanceType:"System.Collections.ObjectModel.Collection" /dictionaryType:"System.Collections.Generic.IDictionary" /dictionaryInstanceType:"System.Collections.Generic.Dictionary" /arrayBaseType:"System.Collections.ObjectModel.Collection" /dictionaryBaseType:"System.Collections.Generic.Dictionary" /classStyle:"Poco" /generateDefaultValues:true /generateDataAnnotations:true /generateImmutableArrayProperties:true /generateImmutableDictionaryProperties:true /generateDtoTypes:true /generateOptionalPropertiesAsNullable:true diff --git a/sample/Sample.Restful.Client/Test1.cs b/sample/Sample.Restful.Client/Test1.cs new file mode 100644 index 000000000..64b3eb596 --- /dev/null +++ b/sample/Sample.Restful.Client/Test1.cs @@ -0,0 +1,5 @@ +namespace JetBrains.Annotations; + +public class Test1 +{ +} diff --git a/sample/Sample.Restful/Controllers/LaunchRecordController.cs b/sample/Sample.Restful/Controllers/LaunchRecordController.cs index 6319af8eb..c0f13b685 100644 --- a/sample/Sample.Restful/Controllers/LaunchRecordController.cs +++ b/sample/Sample.Restful/Controllers/LaunchRecordController.cs @@ -7,40 +7,48 @@ namespace Sample.Restful.Controllers; [Route("[controller]")] -public class LaunchRecordController : RestfulApiController +public partial class LaunchRecordController : RestfulApiController { + /// + /// Search for launch records + /// + /// The search context + /// [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - public Task>> ListLaunchRecords() - { - return Send(new ListLaunchRecords.Request(), x => Ok(x)); - } + public partial IAsyncEnumerable ListLaunchRecords(ListLaunchRecords.Request request); + /// + /// Load details of a specific launch record + /// + /// The request context + /// [HttpGet("{id:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - public Task> GetLaunchRecord([BindRequired] [FromRoute] GetLaunchRecord.Request request) - { - return Send(request, x => Ok(x)); - } + public partial Task> GetLaunchRecord(GetLaunchRecord.Request request); + /// + /// Create a new launch record + /// + /// The launch record details + /// [HttpPost] - public Task> CreateLaunchRecord([BindRequired] [FromBody] CreateLaunchRecord.Request request) - { - return Send( - request, - x => CreatedAtAction(nameof(GetLaunchRecord), new { id = x.Id }, x) - ); - } + [Created(nameof(GetLaunchRecord))] + public partial Task> CreateLaunchRecord(CreateLaunchRecord.Request request); + /// + /// Update a given launch record + /// + /// The id of the launch record + /// The request details + /// [HttpPut("{id:guid}")] - public Task UpdateLaunchRecord([BindRequired] [FromRoute] Guid id, [BindRequired] [FromBody] EditLaunchRecord.Model model) - { - return Send(new EditLaunchRecord.Request { Id = id }.With(model), NoContent); - } + // ReSharper disable once RouteTemplates.ParameterTypeAndConstraintsMismatch + public partial Task EditLaunchRecord([BindRequired] [FromRoute] LaunchRecordId id, EditLaunchRecord.Request model); + /// + /// Remove a launch record + /// + /// + /// [HttpDelete("{id:guid}")] - public Task RemoveLaunchRecord([BindRequired] [FromRoute] DeleteLaunchRecord.Request request) - { - return Send(request); - } + public partial Task DeleteLaunchRecord(DeleteLaunchRecord.Request request); } diff --git a/sample/Sample.Restful/Controllers/RocketController.cs b/sample/Sample.Restful/Controllers/RocketController.cs index 709511713..e25189fcd 100644 --- a/sample/Sample.Restful/Controllers/RocketController.cs +++ b/sample/Sample.Restful/Controllers/RocketController.cs @@ -7,40 +7,64 @@ namespace Sample.Restful.Controllers; [Route("[controller]")] -public class RocketController : RestfulApiController +public partial class RocketController : RestfulApiController { + /// + /// Search for rockets + /// + /// The search context + /// [HttpGet] - [ProducesResponseType(StatusCodes.Status200OK)] - public Task>> ListRockets() - { - return Send(new ListRockets.Request(), x => Ok(x)); - } + public partial IAsyncEnumerable ListRockets(ListRockets.Request request); + /// + /// Load details of a specific rocket + /// + /// The request context + /// [HttpGet("{id:guid}")] - [ProducesResponseType(StatusCodes.Status200OK)] - public Task> GetRocket([BindRequired] [FromRoute] GetRocket.Request request) - { - return Send(request, x => Ok(x)); - } + public partial Task> GetRocket(GetRocket.Request request); + /// + /// Create a new rocket + /// + /// The rocket details + /// [HttpPost] - public Task> CreateRocket([BindRequired] [FromBody] CreateRocket.Request request) - { - return Send( - request, - x => CreatedAtAction(nameof(GetRocket), new { id = x.Id }, x) - ); - } + [Created(nameof(GetRocket))] + public partial Task> CreateRocket(CreateRocket.Request request); + /// + /// Update a given rocket + /// + /// The id of the rocket + /// The request details + /// [HttpPut("{id:guid}")] - public Task UpdateRocket([BindRequired] Guid id, [BindRequired] [FromBody] EditRocket.Model model) - { - return Send(new EditRocket.Request { Id = id }.With(model), NoContent); - } + // ReSharper disable once RouteTemplates.ParameterTypeAndConstraintsMismatch + public partial Task> EditRocket([BindRequired] [FromRoute] RocketId id, [BindRequired] [FromBody] EditRocket.Request model); + /// + /// Remove a rocket + /// + /// + /// [HttpDelete("{id:guid}")] - public Task RemoveRocket([BindRequired] [FromRoute] DeleteRocket.Request request) - { - return Send(request); - } + public partial Task RemoveRocket([BindRequired] [FromRoute] DeleteRocket.Request request); + + /// + /// Get the launch records for a given rocket + /// + /// + [HttpGet("{id:guid}/launch-records")] + // ReSharper disable once RouteTemplates.ParameterTypeAndConstraintsMismatch + public partial IAsyncEnumerable GetRocketLaunchRecords(GetRocketLaunchRecords.Request request); + + /// + /// Get a specific launch record for a given rocket + /// + /// + [HttpGet("{id:guid}/launch-records/{launchRecordId:guid}")] + // ReSharper disable once RouteTemplates.ParameterTypeAndConstraintsMismatch + public partial Task> GetRocketLaunchRecord(GetRocketLaunchRecord.Request request); } diff --git a/sample/Sample.Restful/Sample.Restful.csproj b/sample/Sample.Restful/Sample.Restful.csproj index 482660c47..768f4ce19 100644 --- a/sample/Sample.Restful/Sample.Restful.csproj +++ b/sample/Sample.Restful/Sample.Restful.csproj @@ -3,6 +3,7 @@ netcoreapp3.1;net6.0 true true + true diff --git a/sample/Sample.Worker/Worker.cs b/sample/Sample.Worker/Worker.cs index 152f4754f..e6ffab8fd 100644 --- a/sample/Sample.Worker/Worker.cs +++ b/sample/Sample.Worker/Worker.cs @@ -19,7 +19,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { - var items = await _mediator.Invoke(m => m.Send(new ListRockets.Request(), stoppingToken)); + var items = await _mediator.Invoke(m => m.CreateStream(new ListRockets.Request(null), stoppingToken)).ToListAsync(); _logger.LogInformation("Items: {@Items}", items); await Task.Delay(1000, stoppingToken).ConfigureAwait(false); } diff --git a/src/Analyzers/Composition/ApiConventionNameMatchBehavior.cs b/src/Analyzers/Composition/ApiConventionNameMatchBehavior.cs new file mode 100644 index 000000000..a08da13a7 --- /dev/null +++ b/src/Analyzers/Composition/ApiConventionNameMatchBehavior.cs @@ -0,0 +1,35 @@ +namespace Rocket.Surgery.LaunchPad.Analyzers.Composition; + +/// +/// The behavior for matching the name of a convention parameter or method. +/// +internal enum ApiConventionNameMatchBehavior +{ + /// + /// Matches any name. Use this if the parameter does not need to be matched. + /// + Any, + + /// + /// The parameter or method name must exactly match the convention. + /// + Exact, + + /// + /// The parameter or method name in the convention is a proper prefix. + /// + /// Casing is used to delineate words in a given name. For instance, with this behavior + /// the convention name "Get" will match "Get", "GetPerson" or "GetById", but not "getById", "Getaway". + /// + /// + Prefix, + + /// + /// The parameter or method name in the convention is a proper suffix. + /// + /// Casing is used to delineate words in a given name. For instance, with this behavior + /// the convention name "id" will match "id", or "personId" but not "grid" or "personid". + /// + /// + Suffix, +} diff --git a/src/Analyzers/Composition/ApiConventionTypeMatchBehavior.cs b/src/Analyzers/Composition/ApiConventionTypeMatchBehavior.cs new file mode 100644 index 000000000..f2bf20f11 --- /dev/null +++ b/src/Analyzers/Composition/ApiConventionTypeMatchBehavior.cs @@ -0,0 +1,18 @@ +namespace Rocket.Surgery.LaunchPad.Analyzers.Composition; + +/// +/// The behavior for matching the type of a convention parameter. +/// +internal enum ApiConventionTypeMatchBehavior +{ + /// + /// Matches any type. Use this if the parameter does not need to be matched. + /// + Any, + + /// + /// The parameter in the convention is the exact type or a subclass of the type + /// specified in the convention. + /// + AssignableFrom, +} diff --git a/src/Analyzers/Composition/IRestfulApiMethodMatcher.cs b/src/Analyzers/Composition/IRestfulApiMethodMatcher.cs new file mode 100644 index 000000000..d7906506f --- /dev/null +++ b/src/Analyzers/Composition/IRestfulApiMethodMatcher.cs @@ -0,0 +1,10 @@ +namespace Rocket.Surgery.LaunchPad.Analyzers.Composition; + +internal interface IRestfulApiMethodMatcher +{ + RestfulApiMethod Method { get; } + ApiConventionNameMatchBehavior NameMatch { get; } + string[] Names { get; } + IDictionary Parameters { get; } + bool IsMatch(ActionModel actionModel); +} diff --git a/src/Analyzers/Composition/IRestfulApiParameterMatcher.cs b/src/Analyzers/Composition/IRestfulApiParameterMatcher.cs new file mode 100644 index 000000000..a28741546 --- /dev/null +++ b/src/Analyzers/Composition/IRestfulApiParameterMatcher.cs @@ -0,0 +1,13 @@ +using Microsoft.CodeAnalysis; + +namespace Rocket.Surgery.LaunchPad.Analyzers.Composition; + +internal interface IRestfulApiParameterMatcher +{ + Index ParameterIndex { get; } + ApiConventionNameMatchBehavior NameMatch { get; } + string[] Names { get; } + ApiConventionTypeMatchBehavior TypeMatch { get; } + INamedTypeSymbol? Type { get; } + bool IsMatch(ActionModel actionModel); +} diff --git a/src/Analyzers/Composition/Index.cs b/src/Analyzers/Composition/Index.cs new file mode 100644 index 000000000..1497e74c7 --- /dev/null +++ b/src/Analyzers/Composition/Index.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Globalization; +using System.Runtime.CompilerServices; + +// ReSharper disable once CheckNamespace +namespace System; + +/// Represent a type can be used to index a collection either from the start or the end. +/// +/// Index is used by the C# compiler to support the new index syntax +/// +/// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; +/// int lastElement = someArray[^1]; // lastElement = 5 +/// +/// +internal readonly struct Index : IEquatable +{ + private readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "Non-negative number required."); + } + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + private Index(int value) + { + _value = value; + } + + /// Create an Index pointing at first element. + public static Index Start => new Index(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new Index(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "Non-negative number required."); + } + + return new Index(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "Non-negative number required."); + } + + return new Index(~value); + } + + /// Returns the index value. + public int Value + { + get + { + if (_value < 0) + return ~_value; + return _value; + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + var offset = _value; + if (IsFromEnd) + { + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + + offset += length + 1; + } + + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? value) + { + return value is Index && _value == ( (Index)value )._value; + } + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) + { + return _value == other._value; + } + + /// Returns the hash code for this instance. + public override int GetHashCode() + { + return _value; + } + + /// Converts integer number to an Index. + public static implicit operator Index(int value) + { + return FromStart(value); + } + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + return $"^{( (uint)Value ).ToString(CultureInfo.InvariantCulture)}"; + + return ( (uint)Value ).ToString(CultureInfo.InvariantCulture); + } +} diff --git a/src/Analyzers/Composition/MatcherDefaults.cs b/src/Analyzers/Composition/MatcherDefaults.cs new file mode 100644 index 000000000..2dc1c111f --- /dev/null +++ b/src/Analyzers/Composition/MatcherDefaults.cs @@ -0,0 +1,69 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Rocket.Surgery.LaunchPad.Analyzers.Composition; + +internal static class MatcherDefaults +{ + /// + /// The cache of default status codes for a given method type. + /// + public static Dictionary MethodStatusCodeMap { get; } = new Dictionary + { + [RestfulApiMethod.List] = StatusCodes.Status200OK, + [RestfulApiMethod.Read] = StatusCodes.Status200OK, + [RestfulApiMethod.Create] = StatusCodes.Status201Created, + [RestfulApiMethod.Update] = StatusCodes.Status200OK, + [RestfulApiMethod.Delete] = StatusCodes.Status204NoContent, + }; + + public static ImmutableArray GetMatchers(Compilation compilation) + { + return DefaultMatchers(compilation).Where(z => z.IsValid()).OfType().ToImmutableArray(); + } + + private static IEnumerable DefaultMatchers(Compilation compilation) + { + var IBaseRequest = compilation.GetTypeByMetadataName("MediatR.IBaseRequest")!; + var IStreamRequest = compilation.GetTypeByMetadataName("MediatR.IStreamRequest`1")!; + yield return new RestfulApiMethodBuilder(RestfulApiMethod.List) + .MatchPrefix("List", "Search") + .MatchParameterType(^1, IBaseRequest); + yield return new RestfulApiMethodBuilder(RestfulApiMethod.List) + .MatchPrefix("List", "Search") + .MatchParameterType(^1, IStreamRequest); + yield return new RestfulApiMethodBuilder(RestfulApiMethod.Read) + .MatchPrefix("Get", "Find", "Fetch", "Read") + .MatchParameterType(^1, IBaseRequest); + yield return new RestfulApiMethodBuilder(RestfulApiMethod.Read) + .MatchPrefix("Get", "Find", "Fetch", "Read") + .MatchParameterType(^1, IStreamRequest); + yield return new RestfulApiMethodBuilder(RestfulApiMethod.Create) + .MatchPrefix("Post", "Create", "Add") + .MatchParameterType(^1, IBaseRequest); + yield return new RestfulApiMethodBuilder(RestfulApiMethod.Create) + .MatchPrefix("Post", "Create", "Add") + .MatchParameterType(^1, IStreamRequest); + yield return new RestfulApiMethodBuilder(RestfulApiMethod.Update) + .MatchPrefix("Put", "Edit", "Update") + .MatchParameterSuffix(^2, "id") + .MatchParameterType(^1, IBaseRequest); + yield return new RestfulApiMethodBuilder(RestfulApiMethod.Update) + .MatchPrefix("Put", "Edit", "Update") + .MatchParameterSuffix(^2, "id") + .MatchParameterType(^1, IStreamRequest); + yield return new RestfulApiMethodBuilder(RestfulApiMethod.Update) + .MatchPrefix("Put", "Edit", "Update") + .MatchParameterSuffix(^2, "id") + .MatchParameterSuffix(^1, "model", "request"); + yield return new RestfulApiMethodBuilder(RestfulApiMethod.Delete) + .MatchPrefix("Delete", "Remove") + .MatchParameterType(^1, IBaseRequest); + yield return new RestfulApiMethodBuilder(RestfulApiMethod.Delete) + .MatchPrefix("Delete", "Remove") + .MatchParameterType(^1, IStreamRequest); + yield return new RestfulApiMethodBuilder(RestfulApiMethod.Delete) + .MatchPrefix("Delete", "Remove") + .MatchParameterSuffix(^1, "id"); + } +} diff --git a/src/Analyzers/Composition/RestfulApiMethod.cs b/src/Analyzers/Composition/RestfulApiMethod.cs new file mode 100644 index 000000000..214901296 --- /dev/null +++ b/src/Analyzers/Composition/RestfulApiMethod.cs @@ -0,0 +1,32 @@ +namespace Rocket.Surgery.LaunchPad.Analyzers.Composition; + +/// +/// Restful api method types +/// +internal enum RestfulApiMethod +{ + /// + /// This method returns a list or paged list + /// + List, + + /// + /// This method creates a new item + /// + Create, + + /// + /// This method returns a single item + /// + Read, + + /// + /// This method updates a single item + /// + Update, + + /// + /// This method removes an item + /// + Delete +} diff --git a/src/Analyzers/Composition/RestfulApiMethodBuilder.cs b/src/Analyzers/Composition/RestfulApiMethodBuilder.cs new file mode 100644 index 000000000..5ea2fc637 --- /dev/null +++ b/src/Analyzers/Composition/RestfulApiMethodBuilder.cs @@ -0,0 +1,274 @@ +using Microsoft.CodeAnalysis; + +namespace Rocket.Surgery.LaunchPad.Analyzers.Composition; + +/// +/// This class allows you to define methods default conventions when using the RestfulApiController +/// This allows the response codes to be automatically inferred based on the rules defined. +/// +internal class RestfulApiMethodBuilder : IRestfulApiMethodMatcher +{ + private static RestfulApiParameterMatcher DefaultMatcher(Index index) + { + return new RestfulApiParameterMatcher( + index, + ApiConventionNameMatchBehavior.Any, + Array.Empty(), + ApiConventionTypeMatchBehavior.Any, + null + ); + } + + private readonly RestfulApiMethod _method; + + private readonly IDictionary _parameters = new Dictionary(); + private int? _parameterCount; + private ApiConventionNameMatchBehavior _nameMatchBehavior = ApiConventionNameMatchBehavior.Any; + private string[] _names = Array.Empty(); + + /// + /// Create a method build for the given . + /// + /// + public RestfulApiMethodBuilder(RestfulApiMethod method) + { + _method = method; + } + + /// + /// Match against one of the given suffixes + /// + /// The first suffix + /// Any additional suffixes + /// + public RestfulApiMethodBuilder MatchSuffix(string value, params string[] values) + { + _names = new[] { value }.Concat(values).ToArray(); + _nameMatchBehavior = ApiConventionNameMatchBehavior.Suffix; + + return this; + } + + /// + /// Match against one of the given prefixes + /// + /// The first prefix + /// Any additional prefixes + /// + public RestfulApiMethodBuilder MatchPrefix(string value, params string[] values) + { + _names = new[] { value }.Concat(values).ToArray(); + _nameMatchBehavior = ApiConventionNameMatchBehavior.Prefix; + + return this; + } + + /// + /// Match based one of the given method names + /// + /// The first name + /// Any additional names + /// + public RestfulApiMethodBuilder MatchName(string value, params string[] values) + { + _names = new[] { value }.Concat(values).ToArray(); + _nameMatchBehavior = ApiConventionNameMatchBehavior.Exact; + + return this; + } + + /// + /// Matched on the names of the parameter at the given index + /// + /// + /// The index can be either positive or negative to allow for comparing against first or last parameters + /// + /// The parameter, maybe a positive or negative + /// The first name + /// Any additional names + /// + public RestfulApiMethodBuilder MatchParameterName(Index parameter, string value, params string[] values) + { + if (!_parameters.TryGetValue(parameter, out var item)) + { + item = _parameters[parameter] = DefaultMatcher(parameter); + } + + _parameters[parameter] = new RestfulApiParameterMatcher( + parameter, + ApiConventionNameMatchBehavior.Exact, + new[] { value }.Concat(values).ToArray(), + item.TypeMatch, + item.Type + ); + + return this; + } + + /// + /// Match only if a parameter exists at the given index + /// + /// + /// The index can be either positive or negative to allow for comparing against first or last parameters + /// + /// The parameter, maybe a positive or negative + /// + public RestfulApiMethodBuilder HasParameter(Index parameter) + { + if (!_parameters.TryGetValue(parameter, out var item)) + { + item = _parameters[parameter] = DefaultMatcher(parameter); + } + + _parameters[parameter] = + new RestfulApiParameterMatcher( + parameter, + item.NameMatch, + item.Names.Length > 0 ? item.Names : Array.Empty(), + item.TypeMatch, + item.Type + ); + + return this; + } + + /// + /// Match only if there are a given number of parameters + /// + /// + /// + public RestfulApiMethodBuilder MatchParameterCount(int count) + { + _parameterCount = count; + return this; + } + + /// + /// Match against the prefixes for the given parameter at the given index + /// + /// + /// The index can be either positive or negative to allow for comparing against first or last parameters + /// + /// The parameter, maybe a positive or negative + /// The first prefix + /// Any additional prefixes + /// + public RestfulApiMethodBuilder MatchParameterPrefix(Index parameter, string value, params string[] values) + { + if (!_parameters.TryGetValue(parameter, out var item)) + { + item = _parameters[parameter] = DefaultMatcher(parameter); + } + + _parameters[parameter] = new RestfulApiParameterMatcher( + parameter, + ApiConventionNameMatchBehavior.Prefix, + new[] { value }.Concat(values).ToArray(), + item.TypeMatch, + item.Type + ); + + return this; + } + + /// + /// Match against the suffixes for the given parameter at the given index + /// + /// + /// The index can be either positive or negative to allow for comparing against first or last parameters + /// + /// The parameter, maybe a positive or negative + /// The first suffix + /// Any additional suffixes + /// + public RestfulApiMethodBuilder MatchParameterSuffix(Index parameter, string value, params string[] values) + { + if (!_parameters.TryGetValue(parameter, out var item)) + { + item = _parameters[parameter] = DefaultMatcher(parameter); + } + + _parameters[parameter] = new RestfulApiParameterMatcher( + parameter, + ApiConventionNameMatchBehavior.Suffix, + new[] { value }.Concat(values).ToArray(), + item.TypeMatch, + item.Type + ); + + return this; + } + + /// + /// Match the type parameter at the given index. + /// + /// + /// The index can be either positive or negative to allow for comparing against first or last parameters + /// + /// The parameter, maybe a positive or negative + /// The type of the parameter + /// + public RestfulApiMethodBuilder MatchParameterType(Index parameter, INamedTypeSymbol type) + { + if (!_parameters.TryGetValue(parameter, out var item)) + { + item = _parameters[parameter] = DefaultMatcher(parameter); + } + + _parameters[parameter] = new RestfulApiParameterMatcher( + parameter, + item.NameMatch, + item.Names, + ApiConventionTypeMatchBehavior.AssignableFrom, + type + ); + + return this; + } + + internal bool IsValid() + { + return _names.Length > 0; + } + + internal bool IsMatch(ActionModel actionModel) + { + var nameMatch = _nameMatchBehavior switch + { + ApiConventionNameMatchBehavior.Exact => _names.Any(name => actionModel.ActionName.Equals(name, StringComparison.OrdinalIgnoreCase)), + ApiConventionNameMatchBehavior.Prefix => _names.Any(name => actionModel.ActionName.StartsWith(name, StringComparison.OrdinalIgnoreCase)), + ApiConventionNameMatchBehavior.Suffix => _names.Any(name => actionModel.ActionName.EndsWith(name, StringComparison.OrdinalIgnoreCase)), + _ => true + }; + + if (!nameMatch) + return false; + + var parameters = actionModel.ActionMethod.Parameters; + if (_parameterCount.HasValue && parameters.Length != _parameterCount.Value) + { + return false; + } + + if (parameters.Length >= _parameters.Count) + { + return _parameters.Values.All(z => z.IsMatch(actionModel)); + } + + return false; + } + + RestfulApiMethod IRestfulApiMethodMatcher.Method => _method; + ApiConventionNameMatchBehavior IRestfulApiMethodMatcher.NameMatch => _nameMatchBehavior; + string[] IRestfulApiMethodMatcher.Names => _names; + IDictionary IRestfulApiMethodMatcher.Parameters => _parameters; + + bool IRestfulApiMethodMatcher.IsMatch(ActionModel actionModel) + { + return IsMatch(actionModel); + } +} + +internal record ActionModel(Compilation Compilation, string ActionName, IMethodSymbol ActionMethod) +{ +} diff --git a/src/Analyzers/Composition/RestfulApiParameterMatcher.cs b/src/Analyzers/Composition/RestfulApiParameterMatcher.cs new file mode 100644 index 000000000..93ed8eda1 --- /dev/null +++ b/src/Analyzers/Composition/RestfulApiParameterMatcher.cs @@ -0,0 +1,82 @@ +using Microsoft.CodeAnalysis; + +#pragma warning disable 1591 +#pragma warning disable CA1801 + +namespace Rocket.Surgery.LaunchPad.Analyzers.Composition; + +internal class RestfulApiParameterMatcher : IRestfulApiParameterMatcher +{ + public RestfulApiParameterMatcher( + Index parameterIndex, + ApiConventionNameMatchBehavior nameMatch, + string[] names, + ApiConventionTypeMatchBehavior typeMatch, + INamedTypeSymbol? type + ) + { + ParameterIndex = parameterIndex; + NameMatch = nameMatch; + Names = names; + TypeMatch = typeMatch; + Type = type; + } + + public Index ParameterIndex { get; } + public ApiConventionNameMatchBehavior NameMatch { get; } + public string[] Names { get; } + public ApiConventionTypeMatchBehavior TypeMatch { get; } + public INamedTypeSymbol? Type { get; } + + public bool IsMatch(ActionModel actionModel) + { + var parameters = actionModel.ActionMethod.Parameters; + var offset = ParameterIndex.GetOffset(parameters.Length); + if (offset >= 0 && offset < parameters.Length) + { + var parameter = parameters[ParameterIndex]; + if (TypeMatch == ApiConventionTypeMatchBehavior.AssignableFrom && Type != null) + { + if (Type.IsGenericType) + { + static bool CheckType(ITypeSymbol symbol, INamedTypeSymbol type) + { + if (symbol is not INamedTypeSymbol namedTypeSymbol) + { + return false; + } + + return namedTypeSymbol.IsGenericType && SymbolEqualityComparer.Default.Equals(namedTypeSymbol.OriginalDefinition, type); + } + + var parameterIs = CheckType(parameter.Type, Type); + if (!parameterIs) + { + parameterIs = parameter.Type.AllInterfaces.Any(inter => CheckType(inter, Type)); + } + + if (!parameterIs) + { + return false; + } + } + else if (!actionModel.Compilation.HasImplicitConversion(parameter.Type, Type)) + { + return false; + } + } + + return NameMatch switch + { + ApiConventionNameMatchBehavior.Exact => Names.Any(name => parameter.Name!.Equals(name, StringComparison.OrdinalIgnoreCase)), + ApiConventionNameMatchBehavior.Prefix => Names.Any(name => parameter.Name!.StartsWith(name, StringComparison.OrdinalIgnoreCase)), + ApiConventionNameMatchBehavior.Suffix => Names.Any(name => parameter.Name!.EndsWith(name, StringComparison.OrdinalIgnoreCase)), + _ => true + }; + } + + return false; + } +} + +// IApiDescriptionProvider diff --git a/src/Analyzers/Composition/StatusCodes.cs b/src/Analyzers/Composition/StatusCodes.cs new file mode 100644 index 000000000..d615a6a21 --- /dev/null +++ b/src/Analyzers/Composition/StatusCodes.cs @@ -0,0 +1,80 @@ +namespace Rocket.Surgery.LaunchPad.Analyzers.Composition; + +// ReSharper disable InconsistentNaming +// ReSharper disable IdentifierTypo +#pragma warning disable CS1591 +[PublicAPI] +internal static class StatusCodes +{ + public const int Status100Continue = 100; + public const int Status101SwitchingProtocols = 101; + public const int Status102Processing = 102; + + public const int Status200OK = 200; + public const int Status201Created = 201; + public const int Status202Accepted = 202; + public const int Status203NonAuthoritative = 203; + public const int Status204NoContent = 204; + public const int Status205ResetContent = 205; + public const int Status206PartialContent = 206; + public const int Status207MultiStatus = 207; + public const int Status208AlreadyReported = 208; + public const int Status226IMUsed = 226; + + public const int Status300MultipleChoices = 300; + public const int Status301MovedPermanently = 301; + public const int Status302Found = 302; + public const int Status303SeeOther = 303; + public const int Status304NotModified = 304; + public const int Status305UseProxy = 305; + public const int Status306SwitchProxy = 306; // RFC 2616, removed + public const int Status307TemporaryRedirect = 307; + public const int Status308PermanentRedirect = 308; + + public const int Status400BadRequest = 400; + public const int Status401Unauthorized = 401; + public const int Status402PaymentRequired = 402; + public const int Status403Forbidden = 403; + public const int Status404NotFound = 404; + public const int Status405MethodNotAllowed = 405; + public const int Status406NotAcceptable = 406; + public const int Status407ProxyAuthenticationRequired = 407; + public const int Status408RequestTimeout = 408; + public const int Status409Conflict = 409; + public const int Status410Gone = 410; + public const int Status411LengthRequired = 411; + public const int Status412PreconditionFailed = 412; + public const int Status413RequestEntityTooLarge = 413; // RFC 2616, renamed + public const int Status413PayloadTooLarge = 413; // RFC 7231 + public const int Status414RequestUriTooLong = 414; // RFC 2616, renamed + public const int Status414UriTooLong = 414; // RFC 7231 + public const int Status415UnsupportedMediaType = 415; + public const int Status416RequestedRangeNotSatisfiable = 416; // RFC 2616, renamed + public const int Status416RangeNotSatisfiable = 416; // RFC 7233 + public const int Status417ExpectationFailed = 417; + public const int Status418ImATeapot = 418; + public const int Status419AuthenticationTimeout = 419; // Not defined in any RFC + public const int Status421MisdirectedRequest = 421; + public const int Status422UnprocessableEntity = 422; + public const int Status423Locked = 423; + public const int Status424FailedDependency = 424; + public const int Status426UpgradeRequired = 426; + public const int Status428PreconditionRequired = 428; + public const int Status429TooManyRequests = 429; + public const int Status431RequestHeaderFieldsTooLarge = 431; + public const int Status451UnavailableForLegalReasons = 451; + + public const int Status500InternalServerError = 500; + public const int Status501NotImplemented = 501; + public const int Status502BadGateway = 502; + public const int Status503ServiceUnavailable = 503; + public const int Status504GatewayTimeout = 504; + public const int Status505HttpVersionNotsupported = 505; + public const int Status506VariantAlsoNegotiates = 506; + public const int Status507InsufficientStorage = 507; + public const int Status508LoopDetected = 508; + public const int Status510NotExtended = 510; + public const int Status511NetworkAuthenticationRequired = 511; +} + +#pragma warning restore CS1591 diff --git a/src/Analyzers/ControllerActionBodyGenerator.cs b/src/Analyzers/ControllerActionBodyGenerator.cs new file mode 100644 index 000000000..e0e03799e --- /dev/null +++ b/src/Analyzers/ControllerActionBodyGenerator.cs @@ -0,0 +1,741 @@ +using System.Collections.Immutable; +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Rocket.Surgery.LaunchPad.Analyzers.Composition; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace Rocket.Surgery.LaunchPad.Analyzers; + +/// +/// Generator to automatically implement partial methods for controllers to call mediatr methods +/// +[Generator] +public class ControllerActionBodyGenerator : IIncrementalGenerator +{ + /// + /// Same as Pascalize except that the first character is lower case + /// + /// + /// + private static string Camelize(string input) + { + var word = Pascalize(input); +#pragma warning disable CA1308 + return word.Length > 0 ? string.Concat(word.Substring(0, 1).ToLower(CultureInfo.InvariantCulture), word.Substring(1)) : word; +#pragma warning restore CA1308 + } + + /// + /// By default, pascalize converts strings to UpperCamelCase also removing underscores + /// + /// + /// + private static string Pascalize(string input) + { + return Regex.Replace(input, "(?:^|_| +)(.)", match => match.Groups[1].Value.ToUpper(CultureInfo.InvariantCulture)); + } + + private static MethodDeclarationSyntax? GenerateMethod( + SourceProductionContext context, + IRestfulApiMethodMatcher? matcher, + IReadOnlyDictionary statusCodeMap, + MethodDeclarationSyntax syntax, + IMethodSymbol symbol, + IParameterSymbol parameter, + ImmutableArray<(MethodDeclarationSyntax method, IMethodSymbol symbol, IRestfulApiMethodMatcher? matcher, IParameterSymbol? request)> members + ) + { + var otherParams = symbol.Parameters.Remove(parameter); + var parameterType = (INamedTypeSymbol)parameter.Type; + var isUnit = parameterType.AllInterfaces.Any(z => z.MetadataName == "IRequest"); + var isUnitResult = symbol.ReturnType is INamedTypeSymbol { Arity: 1 } nts && nts.TypeArguments[0].MetadataName == "ActionResult"; + var isRecord = parameterType.IsRecord; + var isStream = symbol.ReturnType.MetadataName == "IAsyncEnumerable`1"; + + var newSyntax = syntax + .WithParameterList( + syntax.ParameterList.RemoveNodes( + syntax.ParameterList.Parameters.SelectMany(z => z.AttributeLists), SyntaxRemoveOptions.KeepNoTrivia + )! + ) + .WithAttributeLists(List()) + ; + + if (!isStream) + { + newSyntax = newSyntax + .AddModifiers(Token(SyntaxKind.AsyncKeyword)); + } + + var block = Block(); + var resultName = parameter.Name == "result" ? "r" : "result"; + var sendRequestExpression = isStream ? streamMediatorRequest(IdentifierName(parameter.Name)) : sendMediatorRequest(IdentifierName(parameter.Name)); + + if (otherParams.Length > 0) + { + var declaredParam = newSyntax.ParameterList.Parameters.Single(z => z.Identifier.Text == parameter.Name); + var parameterProperties = parameterType.MemberNames + .Join( + otherParams, z => z, z => z.Name, (memberName, symbol) => ( memberName, symbol ), + StringComparer.OrdinalIgnoreCase + ) + .ToArray(); + + var mapping = parameterType.MemberNames.ToLookup(z => z, StringComparer.OrdinalIgnoreCase); + var failed = false; + foreach (var param in otherParams) + { + if (!mapping[param.Name].Any()) + { + context.ReportDiagnostic( + Diagnostic.Create( + GeneratorDiagnostics.ParameterMustBeAPropertyOfTheRequest, + param.Locations.First(), + param.Locations.Skip(1), + param.Name, + parameterType.Name + ) + ); + failed = true; + } + else if (parameterType.GetMembers(mapping[param.Name].First()).First() is IPropertySymbol property) + { + if (!SymbolEqualityComparer.IncludeNullability.Equals(property.Type, param.Type)) + { + context.ReportDiagnostic( + Diagnostic.Create( + GeneratorDiagnostics.ParameterMustBeSameTypeAsTheRelatedProperty, + param.Locations.First(), + param.Locations.Skip(1), + param.Name, + param.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), + parameterType.Name, + property.Type.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat) + ) + ); + failed = true; + } + } + } + + if (failed) + { + return null; + } + + var bindingMembers = parameterType.GetMembers() + .Where(z => z is IPropertySymbol { IsImplicitlyDeclared: false } ps && ps.Name != "EqualityContract") + .Select(z => z.Name) + .Except(parameterProperties.Select(z => z.memberName)) + .ToArray(); + + var newParam = declaredParam.AddAttributeLists( + AttributeList( + SeparatedList( + new[] + { + Attribute( + IdentifierName("Bind"), AttributeArgumentList( + SeparatedList( + bindingMembers + .Select(z => AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(z)))) + ) + ) + ), + Attribute( + IdentifierName("CustomizeValidator") + ) + .WithArgumentList( + AttributeArgumentList( + SingletonSeparatedList( + AttributeArgument( + LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal(string.Join(",", bindingMembers)) + ) + ) + .WithNameEquals( + NameEquals( + IdentifierName("Properties") + ) + ) + ) + ) + ) + } + ) + ) + ); + + newSyntax = newSyntax.WithParameterList(newSyntax.ParameterList.ReplaceNode(declaredParam, newParam)); + + + if (parameterType.IsRecord) + { + var withExpression = WithExpression( + IdentifierName(parameter.Name), + InitializerExpression( + SyntaxKind.WithInitializerExpression, + SeparatedList( + parameterProperties.Select( + tuple => AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName(tuple.memberName), + IdentifierName(tuple.symbol.Name) + ) + ) + ) + ) + ); + sendRequestExpression = isStream ? streamMediatorRequest(withExpression) : sendMediatorRequest(withExpression); + } + else + { + foreach (var (memberName, s) in parameterProperties) + { + block = block.AddStatements( + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(parameter.Name), + IdentifierName(memberName) + ), + IdentifierName(s.Name) + ) + ) + ); + } + } + } + + if (isUnit) + { + block = block.AddStatements(ExpressionStatement(sendRequestExpression)); + } + else + { + block = block + .AddStatements( + LocalDeclarationStatement( + VariableDeclaration(IdentifierName(Identifier(TriviaList(), SyntaxKind.VarKeyword, "var", "var", TriviaList()))) + .WithVariables( + SingletonSeparatedList(VariableDeclarator(Identifier(resultName)).WithInitializer(EqualsValueClause(sendRequestExpression))) + ) + ) + ); + } + + var knownStatusCodes = symbol.GetAttributes() + .Where(z => z.AttributeClass?.Name == "ProducesResponseTypeAttribute") + .SelectMany( + z => + { + var namedArguments = z.NamedArguments + .Where( + x => x is + { + Key: "StatusCode", Value: { Kind: TypedConstantKind.Primitive, Value: int } + } + ) + .Select(z => (int)z.Value.Value!); + var constructorArguments = z + .ConstructorArguments.Where(z => z is { Kind: TypedConstantKind.Primitive, Value: int }) + .Select(z => (int)z.Value!); + + return namedArguments.Concat(constructorArguments); + } + ) + .ToImmutableHashSet(); + + var hasSuccess = knownStatusCodes.Any(z => z is >= 200 and < 300); + var hasDefault = symbol.GetAttribute("ProducesDefaultResponseTypeAttribute") is { }; + + { + var attributes = parameter.GetAttributes().ToLookup(z => z.AttributeClass?.Name ?? ""); + var declaredParam = newSyntax.ParameterList.Parameters.Single(z => z.Identifier.Text == parameter.Name); + var newParam = declaredParam; + + if (matcher is { }) + { + switch (matcher) + { + case { Method: RestfulApiMethod.List }: + { + if (!attributes["FromQueryAttribute"].Any()) newParam = newParam.AddAttributeLists(createSimpleAttribute("FromQuery")); + break; + } + + case { Method: RestfulApiMethod.Update }: + case { Method: RestfulApiMethod.Create }: + { + if (!attributes["BindRequiredAttribute"].Any()) newParam = newParam.AddAttributeLists(createSimpleAttribute("BindRequired")); + if (!attributes["FromBodyAttribute"].Any()) newParam = newParam.AddAttributeLists(createSimpleAttribute("FromBody")); + break; + } + + case { Method: RestfulApiMethod.Read }: + case { Method: RestfulApiMethod.Delete }: + { + if (!attributes["BindRequiredAttribute"].Any()) newParam = newParam.AddAttributeLists(createSimpleAttribute("BindRequired")); + if (!attributes["FromRouteAttribute"].Any()) newParam = newParam.AddAttributeLists(createSimpleAttribute("FromRoute")); + break; + } + } + } + + newSyntax = newSyntax.WithParameterList(newSyntax.ParameterList.ReplaceNode(declaredParam, newParam)); + } + + if (!hasDefault) + { + newSyntax = newSyntax.AddAttributeLists(createSimpleAttribute("ProducesDefaultResponseType")); + } + + var statusCode = hasSuccess ? knownStatusCodes.First(z => z is >= 200 and < 300) : StatusCodes.Status200OK; + + if (!hasSuccess) + { + if (isUnitResult) + { + statusCode = StatusCodes.Status204NoContent; + } + else if (matcher is { }) + { + statusCode = statusCodeMap[matcher.Method]; + } + + newSyntax = newSyntax.AddAttributeLists(produces(statusCode)); + } + + if (!knownStatusCodes.Contains(StatusCodes.Status404NotFound) && matcher?.Method != RestfulApiMethod.List) + { + newSyntax = newSyntax.AddAttributeLists(produces(StatusCodes.Status404NotFound, "ProblemDetails")); + } + + if (!knownStatusCodes.Contains(StatusCodes.Status400BadRequest)) + { + newSyntax = newSyntax.AddAttributeLists(produces(StatusCodes.Status400BadRequest, "ProblemDetails")); + } + + if (!knownStatusCodes.Contains(StatusCodes.Status422UnprocessableEntity)) + { + newSyntax = newSyntax.AddAttributeLists( + produces(StatusCodes.Status422UnprocessableEntity, "FluentValidationProblemDetails") + ); + } + + + if (isStream) + { + block = block.AddStatements(ReturnStatement(IdentifierName(resultName))); + } + else + { + if (symbol.GetAttribute("CreatedAttribute") is { ConstructorArguments: { Length: 1 } } created) + { + var actionName = (string)created.ConstructorArguments[0].Value!; + var relatedMember = members.Single(z => z.symbol.Name == actionName); + var routeValues = getRouteValues(parameterType, resultName, relatedMember); + + block = block.AddStatements( + ReturnStatement(routeResult("CreatedAtActionResult", resultName, actionName, routeValues)) + ); + } + else if (symbol.GetAttribute("AcceptedAttribute") is { ConstructorArguments: { Length: 1 } } accepted) + { + var actionName = (string)accepted.ConstructorArguments[0].Value!; + var relatedMember = members.Single(z => z.symbol.Name == actionName); + var routeValues = getRouteValues(parameterType, resultName, relatedMember); + + block = block.AddStatements( + ReturnStatement( + routeResult("AcceptedAtActionResult", resultName, actionName, routeValues) + ) + ); + } + else + { + block = block.AddStatements(ReturnStatement(isUnitResult ? statusCodeResult(statusCode) : objectResult(resultName, statusCode))); + } + } + + return newSyntax + .WithBody(block.NormalizeWhitespace()) + .WithSemicolonToken(Token(SyntaxKind.None)); + + static ImmutableArray availableRouteParameters( + (MethodDeclarationSyntax method, IMethodSymbol symbol, IRestfulApiMethodMatcher? matcher, IParameterSymbol? request) relatedMember + ) + { + var parameterNames = relatedMember.symbol.Parameters.Remove(relatedMember.request).Select(z => z.Name); + parameterNames = parameterNames.Concat( + relatedMember.request?.Type.GetMembers() + .Where(z => z is IPropertySymbol { IsImplicitlyDeclared: false } ps && ps.Name != "EqualityContract") + .Select(z => z.Name) ?? Enumerable.Empty() + ); + return parameterNames.Select(Camelize).Distinct().ToImmutableArray(); + } + + static ExpressionSyntax getRouteValues( + INamedTypeSymbol parameterType, + string resultName, + (MethodDeclarationSyntax method, IMethodSymbol symbol, IRestfulApiMethodMatcher? matcher, IParameterSymbol? request) relatedMember + ) + { + var parameterNames = availableRouteParameters(relatedMember); + var response = parameterType.AllInterfaces.First(z => z is { Name: "IRequest", Arity: 1 }).TypeArguments[0]; + + var properties = response.GetMembers().Select(z => z.Name); + var routeValues = parameterNames.Join( + properties, + z => z, + z => z, + (right, left) => AnonymousObjectMemberDeclarator( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(resultName), + IdentifierName(left) + ) + ) + .WithNameEquals( + NameEquals( + IdentifierName(right) + ) + ), + StringComparer.OrdinalIgnoreCase + ); + return AnonymousObjectCreationExpression(SeparatedList(routeValues)); + } + + static ExpressionSyntax objectResult(string contentName, int statusCode) + { + return ObjectCreationExpression(IdentifierName("ObjectResult")) + .WithArgumentList(ArgumentList(SingletonSeparatedList(Argument(IdentifierName(contentName))))) + .WithInitializer( + InitializerExpression( + SyntaxKind.ObjectInitializerExpression, + SingletonSeparatedList( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName("StatusCode"), + LiteralExpression( + SyntaxKind.NumericLiteralExpression, + Literal(statusCode) + ) + ) + ) + ) + ); + } + + static ExpressionSyntax routeResult( + string resultType, string resultName, string actionName, ExpressionSyntax routeValues, string? controllerName = null + ) + { + return ObjectCreationExpression(IdentifierName(resultType)) + .WithArgumentList( + ArgumentList( + SeparatedList( + new[] + { + Argument( + LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(actionName)) + ), + Argument( + controllerName is null + ? LiteralExpression(SyntaxKind.NullLiteralExpression) + : LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(controllerName)) + ), + Argument(routeValues), + Argument(IdentifierName(resultName)) + } + ) + ) + ); + } + + static ExpressionSyntax statusCodeResult(int statusCode) + { + return ObjectCreationExpression(IdentifierName("StatusCodeResult")) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + LiteralExpression( + SyntaxKind.NumericLiteralExpression, + Literal(statusCode) + ) + ) + ) + ) + ); + } + + static AttributeListSyntax produces(int statusCode, string? responseType = null) + { + if (responseType is { }) + { + return AttributeList( + SingletonSeparatedList( + Attribute( + IdentifierName("ProducesResponseType"), + AttributeArgumentList( + SeparatedList( + new[] + { + AttributeArgument(TypeOfExpression(ParseName(responseType))), + AttributeArgument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(statusCode))) + } + ) + ) + ) + ) + ); + } + + return AttributeList( + SingletonSeparatedList( + Attribute( + IdentifierName("ProducesResponseType"), + AttributeArgumentList( + SingletonSeparatedList( + AttributeArgument(LiteralExpression(SyntaxKind.NumericLiteralExpression, Literal(statusCode))) + ) + ) + ) + ) + ); + } + + static AttributeListSyntax createSimpleAttribute(string name) + { + return AttributeList( + SingletonSeparatedList( + Attribute( + IdentifierName(name) + ) + ) + ); + } + + + static ExpressionSyntax sendMediatorRequest(ExpressionSyntax nameSyntax) + { + return AwaitExpression( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("Mediator"), + IdentifierName("Send") + ) + ) + .WithArgumentList( + ArgumentList( + SeparatedList( + new[] + { + Argument(nameSyntax), + Argument( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("HttpContext"), + IdentifierName("RequestAborted") + ) + ) + } + ) + ) + ), + IdentifierName("ConfigureAwait") + ) + ) + .WithArgumentList( + ArgumentList( + SingletonSeparatedList( + Argument( + LiteralExpression( + SyntaxKind.FalseLiteralExpression + ) + ) + ) + ) + ) + ); + } + + + static ExpressionSyntax streamMediatorRequest(ExpressionSyntax nameSyntax) + { + return InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("Mediator"), + IdentifierName("CreateStream") + ) + ) + .WithArgumentList( + ArgumentList( + SeparatedList( + new[] + { + Argument(nameSyntax), + Argument( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("HttpContext"), + IdentifierName("RequestAborted") + ) + ) + } + ) + ) + ); + } + } + + private void GenerateMethods( + SourceProductionContext context, + ((ClassDeclarationSyntax syntax, INamedTypeSymbol symbol, SemanticModel semanticModel) Left, Compilation Right) valueTuple + ) + { + var (syntax, symbol, semanticModel) = valueTuple.Left; + var compilation = valueTuple.Right; + var matchers = MatcherDefaults.GetMatchers(compilation); + var members = syntax.Members + .OfType() + .Where(z => z.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) + .Select( + method => + { + var methodSymbol = semanticModel.GetDeclaredSymbol(method); + // ReSharper disable once UseNullPropagationWhenPossible + if (methodSymbol is null) return default; + + var actionModel = new ActionModel(compilation, methodSymbol.Name, methodSymbol); + var matcher = matchers.FirstOrDefault(z => z.IsMatch(actionModel)); + var request = methodSymbol.Parameters.FirstOrDefault(p => p.Type.AllInterfaces.Any(i => i.MetadataName == "IRequest`1")); + var streamRequest = methodSymbol.Parameters.FirstOrDefault( + p => p.Type.AllInterfaces.Any(i => i.MetadataName == "IStreamRequest`1") + ); + return ( method, symbol: methodSymbol!, matcher, request: request ?? streamRequest ); + } + ) + .Where(z => z is { symbol: { }, method: { } }) + .ToImmutableArray(); + + var newClass = syntax.WithMembers(List()) + .WithConstraintClauses(List()) + .WithAttributeLists(List()) + .WithBaseList(null) + ; + + + foreach (var (method, methodSymbol, matcher, request) in members) + { + if (request != null) + { + var methodBody = GenerateMethod(context, matcher, MatcherDefaults.MethodStatusCodeMap, method, methodSymbol, request, members); + if (methodBody is null) continue; + newClass = newClass.AddMembers(methodBody); + } + } + + var additionalUsings = new[] + { + "Microsoft.AspNetCore.Mvc", + "FluentValidation.AspNetCore", + "Rocket.Surgery.LaunchPad.AspNetCore.Validation" + }; + + var usings = syntax.SyntaxTree.GetCompilationUnitRoot().Usings; + foreach (var additionalUsing in additionalUsings) + { + if (usings.Any(z => z.Name.ToString() == additionalUsing)) continue; + usings = usings.Add(UsingDirective(ParseName(additionalUsing))); + } + + var cu = CompilationUnit( + List(), + List(usings), + List(), + SingletonList( + symbol.ContainingNamespace.IsGlobalNamespace + ? newClass.ReparentDeclaration(context, syntax) + : NamespaceDeclaration(ParseName(symbol.ContainingNamespace.ToDisplayString())) + .WithMembers(SingletonList(newClass.ReparentDeclaration(context, syntax))) + ) + ) + .WithLeadingTrivia() + .WithTrailingTrivia() + .WithLeadingTrivia(Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true))) + .WithTrailingTrivia(Trivia(NullableDirectiveTrivia(Token(SyntaxKind.RestoreKeyword), true)), CarriageReturnLineFeed); + + context.AddSource($"{newClass.Identifier.Text}_Methods.cs", cu.NormalizeWhitespace().GetText(Encoding.UTF8)); + } + + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var syntaxProvider = context + .SyntaxProvider + .CreateSyntaxProvider( + (node, _) => node is ClassDeclarationSyntax cds + && cds.BaseList?.Types.FirstOrDefault()?.Type.GetSyntaxName() == "RestfulApiController" + && cds.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)) + && cds.Members.Any( + z => z is MethodDeclarationSyntax && z.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword)) + ), + (syntaxContext, _) => + ( + syntax: (ClassDeclarationSyntax)syntaxContext.Node, + symbol: syntaxContext.SemanticModel.GetDeclaredSymbol((ClassDeclarationSyntax)syntaxContext.Node, _)!, + semanticModel: syntaxContext.SemanticModel + ) + ) + .Where(z => z.symbol is { }) + ; + + context.RegisterPostInitializationOutput( + initializationContext => + { + initializationContext.AddSource( + "Attributes.cs", @" +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +}" + ); + } + ); + + context.RegisterSourceOutput(syntaxProvider.Combine(context.CompilationProvider), GenerateMethods); + } +} + +internal static class SymbolExtensions +{ + public static AttributeData? GetAttribute(this ISymbol symbol, string attributeClassName) + { + return symbol.GetAttributes().FirstOrDefault(z => z.AttributeClass?.Name == attributeClassName); + } +} diff --git a/src/Analyzers/GeneratorDiagnostics.cs b/src/Analyzers/GeneratorDiagnostics.cs index 671f7cf2d..6a4f2094a 100644 --- a/src/Analyzers/GeneratorDiagnostics.cs +++ b/src/Analyzers/GeneratorDiagnostics.cs @@ -21,4 +21,22 @@ internal static class GeneratorDiagnostics DiagnosticSeverity.Error, true ); + + public static DiagnosticDescriptor ParameterMustBeAPropertyOfTheRequest { get; } = new( + "LPAD0003", + "The parameter must map to a property of the request object", + "The parameter {0} map to a property of the request {1} object", + "LaunchPad", + DiagnosticSeverity.Error, + true + ); + + public static DiagnosticDescriptor ParameterMustBeSameTypeAsTheRelatedProperty { get; } = new( + "LPAD0004", + "The parameter type and property type must match", + "The parameter {0} type {1} must match the property {2} type {3}", + "LaunchPad", + DiagnosticSeverity.Error, + true + ); } diff --git a/src/Analyzers/InheritFromGenerator.cs b/src/Analyzers/InheritFromGenerator.cs index 89929bfd7..46fc68b0f 100644 --- a/src/Analyzers/InheritFromGenerator.cs +++ b/src/Analyzers/InheritFromGenerator.cs @@ -109,8 +109,10 @@ AttributeData[] attributes List(declaration.SyntaxTree.GetCompilationUnitRoot().Usings), List(), SingletonList( - NamespaceDeclaration(ParseName(symbol.ContainingNamespace.ToDisplayString())) - .WithMembers(SingletonList(classToInherit.ReparentDeclaration(context, declaration))) + symbol.ContainingNamespace.IsGlobalNamespace + ? classToInherit.ReparentDeclaration(context, declaration) + : NamespaceDeclaration(ParseName(symbol.ContainingNamespace.ToDisplayString())) + .WithMembers(SingletonList(classToInherit.ReparentDeclaration(context, declaration))) ) ) .WithLeadingTrivia() diff --git a/src/Analyzers/MutableGenerator.cs b/src/Analyzers/MutableGenerator.cs index 965c2530c..7482c2929 100644 --- a/src/Analyzers/MutableGenerator.cs +++ b/src/Analyzers/MutableGenerator.cs @@ -6,29 +6,10 @@ namespace Rocket.Surgery.LaunchPad.Analyzers; /// Generator to convert an immutable type into a mutable one /// [Generator] -public class MutableGenerator : ISourceGenerator +public class MutableGenerator : IIncrementalGenerator { /// - public void Initialize(GeneratorInitializationContext context) + public void Initialize(IncrementalGeneratorInitializationContext context) { - context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); - } - - /// - public void Execute(GeneratorExecutionContext context) - { - if (context.SyntaxReceiver is not SyntaxReceiver) - { -// return; - } - -// var compliation = ( context.Compilation as CSharpCompilation )!; - } - - private class SyntaxReceiver : ISyntaxReceiver - { - public void OnVisitSyntaxNode(SyntaxNode syntaxNode) - { - } } } diff --git a/src/Analyzers/RecordInterop.cs b/src/Analyzers/RecordInterop.cs new file mode 100644 index 000000000..50871349a --- /dev/null +++ b/src/Analyzers/RecordInterop.cs @@ -0,0 +1,23 @@ +#pragma warning disable MA0048 // File name must match type name +#define INTERNAL_RECORD_ATTRIBUTES +#if NETSTANDARD || NETCOREAPP +using System.ComponentModel; + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] +#if INTERNAL_RECORD_ATTRIBUTES + internal +#else + public +#endif + static class IsExternalInit + { + } +} +#endif diff --git a/src/AspNetCore/Composition/IValidationActionResultFactory.cs b/src/AspNetCore/Composition/IValidationActionResultFactory.cs deleted file mode 100644 index c699129f3..000000000 --- a/src/AspNetCore/Composition/IValidationActionResultFactory.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Microsoft.AspNetCore.Mvc; - -namespace Rocket.Surgery.LaunchPad.AspNetCore.Composition; - -/// -/// A factory used to created an Action Result that will be returned for validation results -/// -public interface IValidationActionResultFactory -{ - /// - /// The status code - /// - int StatusCode { get; } - - /// - /// The factory method - /// - /// - /// - ActionResult CreateActionResult(ValidationProblemDetails problemDetails); -} diff --git a/src/AspNetCore/Composition/RestfulApiActionModelConvention.cs b/src/AspNetCore/Composition/RestfulApiActionModelConvention.cs index 8942fd639..5b5cfe37f 100644 --- a/src/AspNetCore/Composition/RestfulApiActionModelConvention.cs +++ b/src/AspNetCore/Composition/RestfulApiActionModelConvention.cs @@ -1,14 +1,19 @@ -using Microsoft.AspNetCore.Http; +using System.Collections.Concurrent; +using FluentValidation.AspNetCore; +using MediatR; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ApplicationModels; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; using Rocket.Surgery.LaunchPad.AspNetCore.Validation; +using Swashbuckle.AspNetCore.SwaggerGen; namespace Rocket.Surgery.LaunchPad.AspNetCore.Composition; -internal class RestfulApiActionModelConvention : IActionModelConvention +internal class RestfulApiActionModelConvention : IActionModelConvention, ISchemaFilter { private static string? GetHttpMethod(ActionModel action) { @@ -26,6 +31,70 @@ internal class RestfulApiActionModelConvention : IActionModelConvention return httpMethods[0]; } + private static void ExtractParameterDetails(ActionModel action) + { + var requestParameter = action.Parameters.FirstOrDefault( + z => z.ParameterInfo?.ParameterType.GetInterfaces().Any( + i => i.IsGenericType && + ( typeof(IRequest<>) == i.GetGenericTypeDefinition() + || typeof(IStreamRequest<>) == i.GetGenericTypeDefinition() ) + ) == true + ) + ; + if (requestParameter is null) return; + + // Likely hit the generator, no point running from here + if (requestParameter.Attributes.Count(z => z is BindAttribute or CustomizeValidatorAttribute) == 2) + { + _propertiesToHideFromOpenApi.TryAdd( + requestParameter.ParameterType, + requestParameter.ParameterType.GetProperties().Select(z => z.Name).Except( + requestParameter.Attributes.OfType().SelectMany(z => z.Include) + ).ToArray() + ); + return; + } + + var index = action.Parameters.IndexOf(requestParameter); + var newAttributes = requestParameter.Attributes.ToList(); + var otherParams = action.Parameters + .Except(new[] { requestParameter }) + .Select(z => z.ParameterName) + .ToArray(); + + var propertyAndFieldNames = requestParameter.ParameterType + .GetProperties() + .Select(z => z.Name) + .ToArray(); + var bindNames = propertyAndFieldNames + .Except(otherParams, StringComparer.OrdinalIgnoreCase) + .ToArray(); + + if (propertyAndFieldNames.Length != bindNames.Length) + { + var ignoreBindings = propertyAndFieldNames.Except(bindNames, StringComparer.OrdinalIgnoreCase).ToArray(); + newAttributes.Add(new BindAttribute(bindNames)); + action.Properties.Add( + typeof(CustomizeValidatorAttribute), + bindNames + ); + _propertiesToHideFromOpenApi.TryAdd(requestParameter.ParameterType, ignoreBindings); + + var model = action.Parameters[index] = new ParameterModel(requestParameter.ParameterInfo, newAttributes) + { + Action = requestParameter.Action, + BindingInfo = requestParameter.BindingInfo, + ParameterName = requestParameter.ParameterName + }; + foreach (var item in requestParameter.Properties) + { + model.Properties.Add(item); + } + } + } + + private static readonly ConcurrentDictionary _propertiesToHideFromOpenApi = new(); + private readonly ILookup _matchers; private readonly RestfulApiOptions _options; @@ -43,7 +112,7 @@ ActionModel actionModel var providerLookup = actionModel.Filters.OfType() .ToLookup(x => x.StatusCode); - var hasSuccess = providerLookup.Any(z => z.Key >= 200 && z.Key < 300); + var hasSuccess = providerLookup.Any(z => z.Key is >= 200 and < 300); var match = _matchers .SelectMany(z => z) .FirstOrDefault(x => x.IsMatch(actionModel)); @@ -82,10 +151,10 @@ ActionModel actionModel actionModel.Filters.Add(new ProducesResponseTypeAttribute(typeof(ProblemDetails), StatusCodes.Status400BadRequest)); } - if (!providerLookup[_options.ValidationActionResultFactory.StatusCode].Any()) + if (!providerLookup[_options.ValidationStatusCode].Any()) { actionModel.Filters.Add( - new ProducesResponseTypeAttribute(typeof(FluentValidationProblemDetails), _options.ValidationActionResultFactory.StatusCode) + new ProducesResponseTypeAttribute(typeof(FluentValidationProblemDetails), _options.ValidationStatusCode) ); } } @@ -102,5 +171,19 @@ public void Apply(ActionModel action) return; UpdateProviders(action); + ExtractParameterDetails(action); + } + + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + if (_propertiesToHideFromOpenApi.TryGetValue(context.Type, out var propertiesToRemove)) + { + foreach (var property in propertiesToRemove + .Join(schema.Properties, z => z, z => z.Key, (a, b) => b.Key, StringComparer.OrdinalIgnoreCase) + .ToArray()) + { + schema.Properties.Remove(property); + } + } } } diff --git a/src/AspNetCore/Composition/RestfulApiParameterMatcher.cs b/src/AspNetCore/Composition/RestfulApiParameterMatcher.cs index d393b6a95..13b17ca6b 100644 --- a/src/AspNetCore/Composition/RestfulApiParameterMatcher.cs +++ b/src/AspNetCore/Composition/RestfulApiParameterMatcher.cs @@ -38,7 +38,17 @@ public bool IsMatch(ActionModel actionModel) var parameter = parameters[ParameterIndex]; if (TypeMatch == ApiConventionTypeMatchBehavior.AssignableFrom && Type != null) { - if (!Type.IsAssignableFrom(parameter.ParameterType)) + if (Type.IsGenericTypeDefinition) + { + var parameterIs = parameter.ParameterType.IsGenericType && parameter.ParameterType.GetGenericTypeDefinition() == Type; + if (!parameterIs) + parameterIs = parameter.ParameterType.GetInterfaces().Any(inter => inter.IsGenericType && inter.GetGenericTypeDefinition() == Type); + if (!parameterIs) + { + return false; + } + } + else if (!Type.IsAssignableFrom(parameter.ParameterType)) { return false; } diff --git a/src/AspNetCore/Composition/UnprocessableEntityActionResultFactory.cs b/src/AspNetCore/Composition/UnprocessableEntityActionResultFactory.cs deleted file mode 100644 index 3f2351925..000000000 --- a/src/AspNetCore/Composition/UnprocessableEntityActionResultFactory.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; - -namespace Rocket.Surgery.LaunchPad.AspNetCore.Composition; - -internal class UnprocessableEntityActionResultFactory : IValidationActionResultFactory -{ - public ActionResult CreateActionResult(ValidationProblemDetails problemDetails) - { - problemDetails.Status = StatusCode; - return new UnprocessableEntityObjectResult(problemDetails); - } - - public int StatusCode { get; } = StatusCodes.Status422UnprocessableEntity; -} diff --git a/src/AspNetCore/Conventions/AspNetCoreConvention.cs b/src/AspNetCore/Conventions/AspNetCoreConvention.cs index 242c45ec1..e54b15f75 100644 --- a/src/AspNetCore/Conventions/AspNetCoreConvention.cs +++ b/src/AspNetCore/Conventions/AspNetCoreConvention.cs @@ -112,6 +112,9 @@ public void Register(IConventionContext context, IConfiguration configuration, I } services +#if NET6_0_OR_GREATER + .AddEndpointsApiExplorer() +#endif .AddMvcCore() .AddApiExplorer(); PopulateDefaultParts( diff --git a/src/AspNetCore/Conventions/SwashbuckleConvention.cs b/src/AspNetCore/Conventions/SwashbuckleConvention.cs index 8ff3c8919..f87a9f6f8 100644 --- a/src/AspNetCore/Conventions/SwashbuckleConvention.cs +++ b/src/AspNetCore/Conventions/SwashbuckleConvention.cs @@ -9,6 +9,7 @@ using Microsoft.OpenApi.Models; using Rocket.Surgery.Conventions; using Rocket.Surgery.Conventions.DependencyInjection; +using Rocket.Surgery.LaunchPad.AspNetCore.Composition; using Rocket.Surgery.LaunchPad.AspNetCore.OpenApi; using Swashbuckle.AspNetCore.SwaggerGen; @@ -52,6 +53,8 @@ public void Register(IConventionContext context, IConfiguration configuration, I options => options.ServiceLifetime = ServiceLifetime.Singleton ); +// services.TryAddEnumerable(ServiceDescriptor.Transient()); + services.AddOptions() .Configure>( (options, mvcOptions) => options.ConfigureForNodaTime(mvcOptions.Value.JsonSerializerOptions) @@ -59,7 +62,9 @@ public void Register(IConventionContext context, IConfiguration configuration, I services.AddSwaggerGen( options => { + options.SchemaFilter(); options.SchemaFilter(); + options.SchemaFilter(); options.OperationFilter(); options.OperationFilter(); options.OperationFilter(); @@ -104,7 +109,7 @@ public void Register(IConventionContext context, IConfiguration configuration, I { try { - options.IncludeXmlComments(item); + options.IncludeXmlComments(item, true); } #pragma warning disable CA1031 catch (Exception e) diff --git a/src/AspNetCore/OpenApi/StronglyTypedIdSchemaFilter.cs b/src/AspNetCore/OpenApi/StronglyTypedIdSchemaFilter.cs new file mode 100644 index 000000000..f1ac9b274 --- /dev/null +++ b/src/AspNetCore/OpenApi/StronglyTypedIdSchemaFilter.cs @@ -0,0 +1,46 @@ +using System.Reflection; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Rocket.Surgery.LaunchPad.AspNetCore.OpenApi; + +internal class StronglyTypedIdHelpers +{ + public static bool IsStronglyTypedId(Type? type) + { + return GetStronglyTypedIdType(type) is { }; + } + + public static Type? GetStronglyTypedIdType(Type? type) + { + if (type?.GetMember("New", BindingFlags.Static | BindingFlags.Public).FirstOrDefault() is MethodInfo + && type.GetMember("Empty", BindingFlags.Static | BindingFlags.Public).FirstOrDefault() is FieldInfo { } + && type.GetMember("Value", BindingFlags.Instance | BindingFlags.Public).FirstOrDefault() is PropertyInfo propertyInfo) + { + return propertyInfo.PropertyType; + } + + return null; + } +} + +internal class StronglyTypedIdSchemaFilter : ISchemaFilter +{ + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + if (StronglyTypedIdHelpers.GetStronglyTypedIdType(context.Type) is not { } type) return; + schema.Properties.Clear(); + var s2 = context.SchemaGenerator.GenerateSchema(type, context.SchemaRepository, context.MemberInfo, context.ParameterInfo); + schema.Format = s2.Format; + schema.Type = s2.Type; + schema.Enum = s2.Enum; + schema.Default = s2.Default; + schema.Maximum = s2.Maximum; + schema.Minimum = s2.Minimum; + schema.Reference = s2.Reference; + schema.Pattern = s2.Pattern; + schema.Nullable = s2.Nullable; + schema.Required = s2.Required; + schema.Not = s2.Not; + } +} diff --git a/src/AspNetCore/RestfulApiController.cs b/src/AspNetCore/RestfulApiController.cs index 5d7fe9a36..95af8120d 100644 --- a/src/AspNetCore/RestfulApiController.cs +++ b/src/AspNetCore/RestfulApiController.cs @@ -67,30 +67,6 @@ Func> success return success(await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false)); } - /// - /// Send an request and allow for async - /// - /// The request model - /// The method to call when the request succeeds - protected async Task Send( - IRequest request, - Func> success - ) - { - if (request is null) - { - throw new ArgumentNullException(nameof(request)); - } - - if (success is null) - { - throw new ArgumentNullException(nameof(success)); - } - - await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); - return await success().ConfigureAwait(false); - } - /// /// Send an request and allow for sync /// diff --git a/src/AspNetCore/RestfulApiOptions.cs b/src/AspNetCore/RestfulApiOptions.cs index 24bedc070..d2bdf9f1b 100644 --- a/src/AspNetCore/RestfulApiOptions.cs +++ b/src/AspNetCore/RestfulApiOptions.cs @@ -30,16 +30,29 @@ public class RestfulApiOptions new RestfulApiMethodBuilder(RestfulApiMethod.List) .MatchPrefix("List", "Search") .MatchParameterType(^1, typeof(IBaseRequest)), + new RestfulApiMethodBuilder(RestfulApiMethod.List) + .MatchPrefix("List", "Search") + .MatchParameterType(^1, typeof(IStreamRequest<>)), new RestfulApiMethodBuilder(RestfulApiMethod.Read) .MatchPrefix("Get", "Find", "Fetch", "Read") .MatchParameterType(^1, typeof(IBaseRequest)), + new RestfulApiMethodBuilder(RestfulApiMethod.Read) + .MatchPrefix("Get", "Find", "Fetch", "Read") + .MatchParameterType(^1, typeof(IStreamRequest<>)), new RestfulApiMethodBuilder(RestfulApiMethod.Create) .MatchPrefix("Post", "Create", "Add") .MatchParameterType(^1, typeof(IBaseRequest)), + new RestfulApiMethodBuilder(RestfulApiMethod.Create) + .MatchPrefix("Post", "Create", "Add") + .MatchParameterType(^1, typeof(IStreamRequest<>)), new RestfulApiMethodBuilder(RestfulApiMethod.Update) .MatchPrefix("Put", "Edit", "Update") .MatchParameterSuffix(^2, "id") .MatchParameterType(^1, typeof(IBaseRequest)), + new RestfulApiMethodBuilder(RestfulApiMethod.Update) + .MatchPrefix("Put", "Edit", "Update") + .MatchParameterSuffix(^2, "id") + .MatchParameterType(^1, typeof(IStreamRequest<>)), new RestfulApiMethodBuilder(RestfulApiMethod.Update) .MatchPrefix("Put", "Edit", "Update") .MatchParameterSuffix(^2, "id") @@ -47,6 +60,9 @@ public class RestfulApiOptions new RestfulApiMethodBuilder(RestfulApiMethod.Delete) .MatchPrefix("Delete", "Remove") .MatchParameterType(^1, typeof(IBaseRequest)), + new RestfulApiMethodBuilder(RestfulApiMethod.Delete) + .MatchPrefix("Delete", "Remove") + .MatchParameterType(^1, typeof(IStreamRequest<>)), new RestfulApiMethodBuilder(RestfulApiMethod.Delete) .MatchPrefix("Delete", "Remove") .MatchParameterSuffix(^1, "id"), @@ -55,7 +71,7 @@ public class RestfulApiOptions /// /// The factory to use for Validation results /// - public IValidationActionResultFactory ValidationActionResultFactory { get; set; } = new UnprocessableEntityActionResultFactory(); + public int ValidationStatusCode { get; set; } = StatusCodes.Status422UnprocessableEntity; internal ILookup GetMatchers() { diff --git a/src/AspNetCore/Validation/ValidatorInterceptor.cs b/src/AspNetCore/Validation/ValidatorInterceptor.cs index d85b19c74..4362aabb2 100644 --- a/src/AspNetCore/Validation/ValidatorInterceptor.cs +++ b/src/AspNetCore/Validation/ValidatorInterceptor.cs @@ -15,6 +15,15 @@ public IValidationContext BeforeAspNetValidation(ActionContext actionContext, IV public ValidationResult AfterAspNetValidation(ActionContext actionContext, IValidationContext validationContext, ValidationResult result) { actionContext.HttpContext.Items[typeof(ValidationResult)] = result; + if (actionContext.ActionDescriptor.Properties.TryGetValue(typeof(CustomizeValidatorAttribute), out var value) + && value is string[] includeProperties) + { + return new ValidationResult( + result.Errors + .Join(includeProperties, z => z.PropertyName, z => z, (a, b) => a, StringComparer.OrdinalIgnoreCase) + ); + } + return result; } } diff --git a/src/EntityFramework.HotChocolate/AutoConfigureDbContextConfigureQueryType.cs b/src/EntityFramework.HotChocolate/AutoConfigureDbContextConfigureQueryType.cs index bb8d0c33c..559356eb7 100644 --- a/src/EntityFramework.HotChocolate/AutoConfigureDbContextConfigureQueryType.cs +++ b/src/EntityFramework.HotChocolate/AutoConfigureDbContextConfigureQueryType.cs @@ -14,7 +14,7 @@ namespace Rocket.Surgery.LaunchPad.EntityFramework.HotChocolate; public class ConfigureConfigureEntityFrameworkContextQueryType : ObjectTypeExtension where TContext : DbContext { - private static IObjectFieldDescriptor ConfigureResolve(IObjectFieldDescriptor typeDescriptor, PropertyInfo propertyInfo) + private static IObjectFieldDescriptor ConfigureResolve(IObjectFieldDescriptor typeDescriptor, PropertyInfo propertyInfo) { var resolverContextProperty = Expression.Parameter(typeof(IPureResolverContext), "ctx"); var cancellationTokenProperty = Expression.Parameter(typeof(CancellationToken), "ct"); @@ -22,13 +22,40 @@ private static IObjectFieldDescriptor ConfigureResolve(IObjectFieldDesc var serviceCall = Expression.Call(resolverContextProperty, nameof(IPureResolverContext.Service), new[] { typeof(TContext) }); var contextProperty = Expression.Property(serviceCall, propertyInfo); - var method = Expression.Lambda>(contextProperty, resolverContextProperty, cancellationTokenProperty) + var method = Expression.Lambda>(contextProperty, resolverContextProperty, cancellationTokenProperty) .Compile(); return typeDescriptor .UseDbContext() .Resolve(method); } + private static IObjectFieldDescriptor ConfigureResolveModel(IObjectFieldDescriptor typeDescriptor, PropertyInfo propertyInfo) + { + var resolverContextProperty = Expression.Parameter(typeof(IResolverContext), "ctx"); + var cancellationTokenProperty = Expression.Parameter(typeof(CancellationToken), "ct"); + + var serviceCall = Expression.Call( + resolverContextProperty, + typeof(IPureResolverContext).GetMethod(nameof(IPureResolverContext.Service), BindingFlags.Public | BindingFlags.Instance)!.MakeGenericMethod( + typeof(TContext) + ) + ); + var contextProperty = Expression.Property(serviceCall, propertyInfo); + + var projectToMethod = typeof(AutoMapperQueryableExtensions).GetMethod( + nameof(AutoMapperQueryableExtensions.ProjectTo), BindingFlags.Static | BindingFlags.Public + ); + var projectTo = Expression.Call(null, projectToMethod!.MakeGenericMethod(typeof(TEntity), typeof(TModel)), contextProperty, resolverContextProperty); + + var method = Expression + .Lambda>>(projectTo, resolverContextProperty, cancellationTokenProperty) + .Compile(); + return typeDescriptor + .UseDbContext() + .Resolve(method); + } + + private readonly IEnumerable _configureQueryEntities; /// @@ -49,6 +76,8 @@ public ConfigureConfigureEntityFrameworkContextQueryType(params IConfigureEntity _configureQueryEntities = configureQueryEntities; } + public Func MapModel { get; set; } + /// protected override void Configure(IObjectTypeDescriptor descriptor) { @@ -57,6 +86,10 @@ protected override void Configure(IObjectTypeDescriptor descriptor) nameof(ConfigureResolve), BindingFlags.NonPublic | BindingFlags.Static )!; + var configureModel = typeof(ConfigureConfigureEntityFrameworkContextQueryType).GetMethod( + nameof(ConfigureResolveModel), + BindingFlags.NonPublic | BindingFlags.Static + )!; var sets = typeof(TContext) .GetProperties() .Where(z => z.PropertyType.IsGenericType && z.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)) @@ -65,7 +98,17 @@ protected override void Configure(IObjectTypeDescriptor descriptor) foreach (var set in sets) { var field = descriptor.Field(set.Name.Humanize().Pluralize().Dehumanize().Camelize()); - configure.MakeGenericMethod(set.PropertyType).Invoke(null, new object[] { field, set }); + var entityType = set.PropertyType.GetGenericArguments()[0]; + var modelType = MapModel?.Invoke(entityType); + if (modelType == entityType) + { + configure.MakeGenericMethod(set.PropertyType).Invoke(null, new object[] { field, set }); + } + else if (modelType is { }) + { + configureModel.MakeGenericMethod(set.PropertyType.GetGenericArguments()[0], modelType).Invoke(null, new object[] { field, set }); + } + Configure(field, set); foreach (var item in _configureQueryEntities.Where(z => z.Match(set))) { diff --git a/src/EntityFramework.HotChocolate/Rocket.Surgery.LaunchPad.EntityFramework.HotChocolate.csproj b/src/EntityFramework.HotChocolate/Rocket.Surgery.LaunchPad.EntityFramework.HotChocolate.csproj index 526d52572..d1bc13238 100644 --- a/src/EntityFramework.HotChocolate/Rocket.Surgery.LaunchPad.EntityFramework.HotChocolate.csproj +++ b/src/EntityFramework.HotChocolate/Rocket.Surgery.LaunchPad.EntityFramework.HotChocolate.csproj @@ -1,11 +1,12 @@  net6.0 - + $(PackageTags) + diff --git a/src/EntityFramework/NodaTimeDateTimeValueGenerator.cs b/src/EntityFramework/NodaTimeDateTimeValueGenerator.cs index f15095aa3..9c328b987 100644 --- a/src/EntityFramework/NodaTimeDateTimeValueGenerator.cs +++ b/src/EntityFramework/NodaTimeDateTimeValueGenerator.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.ValueGeneration; using NodaTime; @@ -30,3 +31,43 @@ public override DateTime Next(EntityEntry entry) return _clock.GetCurrentInstant().ToDateTimeUtc(); } } + +/// +/// Value generator for StronglyTypedIds +/// +/// +public class StronglyTypedIdValueGenerator : ValueGenerator +{ + private readonly Func _factory; + + internal StronglyTypedIdValueGenerator(Func factory) + { + _factory = factory; + } + + /// + public override bool GeneratesTemporaryValues => false; + + /// + public override T Next(EntityEntry entry) + { + return _factory(); + } +} + +/// +/// Factory a StronglyTypedId value generator +/// +public static class StronglyTypedIdValueGenerator +{ + /// + /// Create a value generator for the given type + /// + /// + /// + /// + public static Func Create(Func factory) where T : struct + { + return (_, _) => new StronglyTypedIdValueGenerator(factory); + } +} diff --git a/src/Foundation.NewtonsoftJson/NewtonsoftJsonDateTimeOffsetPattern.cs b/src/Foundation.NewtonsoftJson/NewtonsoftJsonDateTimeOffsetPattern.cs deleted file mode 100644 index ccce4b32b..000000000 --- a/src/Foundation.NewtonsoftJson/NewtonsoftJsonDateTimeOffsetPattern.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Text; -using NodaTime; -using NodaTime.Extensions; -using NodaTime.Text; - -namespace Rocket.Surgery.LaunchPad.Foundation; - -internal class NewtonsoftJsonDateTimeOffsetPattern : IPattern -{ - public ParseResult Parse(string text) - { - return DateTimeOffset.TryParse(text, out var value) - ? ParseResult.ForValue(value.ToInstant()) - : ParseResult.ForException(() => new FormatException("Could not parse DateTimeOffset")); - } - - public string Format(Instant value) - { - return InstantPattern.ExtendedIso.Format(value); - } - - public StringBuilder AppendFormat(Instant value, StringBuilder builder) - { - return InstantPattern.ExtendedIso.AppendFormat(value, builder); - } -} diff --git a/src/Foundation.NewtonsoftJson/NodaTimeNewtonsoftSerializationExtensions.cs b/src/Foundation.NewtonsoftJson/NodaTimeNewtonsoftSerializationExtensions.cs index 4b1f828bf..0469327f3 100644 --- a/src/Foundation.NewtonsoftJson/NodaTimeNewtonsoftSerializationExtensions.cs +++ b/src/Foundation.NewtonsoftJson/NodaTimeNewtonsoftSerializationExtensions.cs @@ -49,7 +49,7 @@ private static void ReplaceConverters(IList converters) new NewtonsoftJsonCompositeNodaPatternConverter( InstantPattern.ExtendedIso, InstantPattern.General, - new NewtonsoftJsonDateTimeOffsetPattern() + new InstantDateTimeOffsetPattern() ) ); ReplaceConverter( @@ -64,17 +64,14 @@ private static void ReplaceConverters(IList converters) new NewtonsoftJsonCompositeNodaPatternConverter( LocalDateTimePattern.ExtendedIso, LocalDateTimePattern.GeneralIso, - LocalDateTimePattern.BclRoundtrip, - LocalDateTimePattern.FullRoundtrip, - LocalDateTimePattern.FullRoundtripWithoutCalendar + LocalDateTimePattern.BclRoundtrip ) ); ReplaceConverter( converters, new NewtonsoftJsonCompositeNodaPatternConverter( LocalTimePattern.ExtendedIso, - LocalTimePattern.GeneralIso, - LocalTimePattern.LongExtendedIso + LocalTimePattern.GeneralIso ) ); ReplaceConverter( diff --git a/src/Foundation/Conventions/FluentValidationConvention.cs b/src/Foundation/Conventions/FluentValidationConvention.cs index b1aeeffde..f15667972 100644 --- a/src/Foundation/Conventions/FluentValidationConvention.cs +++ b/src/Foundation/Conventions/FluentValidationConvention.cs @@ -61,5 +61,8 @@ public void Register(IConventionContext context, IConfiguration configuration, I services.TryAdd(ServiceDescriptor.Describe(typeof(IValidatorFactory), typeof(ValidatorFactory), ServiceLifetime.Singleton)); services.TryAddEnumerable(ServiceDescriptor.Describe(typeof(IPipelineBehavior<,>), typeof(ValidationPipelineBehavior<,>), _options.MediatorLifetime)); + services.TryAddEnumerable( + ServiceDescriptor.Describe(typeof(IStreamPipelineBehavior<,>), typeof(ValidationStreamPipelineBehavior<,>), _options.MediatorLifetime) + ); } } diff --git a/src/Foundation/SystemTextJsonDateTimeOffsetPattern.cs b/src/Foundation/InstantDateTimeOffsetPattern.cs similarity index 74% rename from src/Foundation/SystemTextJsonDateTimeOffsetPattern.cs rename to src/Foundation/InstantDateTimeOffsetPattern.cs index 9ba72d338..ea79e824d 100644 --- a/src/Foundation/SystemTextJsonDateTimeOffsetPattern.cs +++ b/src/Foundation/InstantDateTimeOffsetPattern.cs @@ -5,8 +5,12 @@ namespace Rocket.Surgery.LaunchPad.Foundation; -internal class SystemTextJsonDateTimeOffsetPattern : IPattern +/// +/// A pattern used to create an instant from a serialized DateTimeOffset value +/// +public class InstantDateTimeOffsetPattern : IPattern { + /// public ParseResult Parse(string text) { return DateTimeOffset.TryParse(text, out var value) @@ -14,11 +18,13 @@ public ParseResult Parse(string text) : ParseResult.ForException(() => new FormatException("Could not parse DateTimeOffset")); } + /// public string Format(Instant value) { return InstantPattern.General.Format(value); } + /// public StringBuilder AppendFormat(Instant value, StringBuilder builder) { return InstantPattern.General.AppendFormat(value, builder); diff --git a/src/Foundation/NodaTimeSystemTextJsonSerializationExtensions.cs b/src/Foundation/NodaTimeSystemTextJsonSerializationExtensions.cs index 942ec5ca6..972b026e6 100644 --- a/src/Foundation/NodaTimeSystemTextJsonSerializationExtensions.cs +++ b/src/Foundation/NodaTimeSystemTextJsonSerializationExtensions.cs @@ -26,7 +26,7 @@ public static JsonSerializerOptions ConfigureNodaTimeForLaunchPad(this JsonSeria new SystemTextJsonCompositeNodaPatternConverter( InstantPattern.General, InstantPattern.ExtendedIso, - new SystemTextJsonDateTimeOffsetPattern() + new InstantDateTimeOffsetPattern() ) ); ReplaceConverter( @@ -41,17 +41,14 @@ public static JsonSerializerOptions ConfigureNodaTimeForLaunchPad(this JsonSeria new SystemTextJsonCompositeNodaPatternConverter( LocalDateTimePattern.GeneralIso, LocalDateTimePattern.ExtendedIso, - LocalDateTimePattern.BclRoundtrip, - LocalDateTimePattern.FullRoundtrip, - LocalDateTimePattern.FullRoundtripWithoutCalendar + LocalDateTimePattern.BclRoundtrip ) ); ReplaceConverter( options.Converters, new SystemTextJsonCompositeNodaPatternConverter( LocalTimePattern.ExtendedIso, - LocalTimePattern.GeneralIso, - LocalTimePattern.LongExtendedIso + LocalTimePattern.GeneralIso ) ); ReplaceConverter( diff --git a/src/Foundation/Validation/ValidationPipelineBehavior.cs b/src/Foundation/Validation/ValidationPipelineBehavior.cs index fa00232a9..93d842717 100644 --- a/src/Foundation/Validation/ValidationPipelineBehavior.cs +++ b/src/Foundation/Validation/ValidationPipelineBehavior.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using FluentValidation; using MediatR; @@ -32,3 +33,33 @@ public async Task Handle(T request, CancellationToken cancellationToken, Requ return await next().ConfigureAwait(false); } } + +internal class ValidationStreamPipelineBehavior : IStreamPipelineBehavior where T : IStreamRequest +{ + private readonly IValidatorFactory _validatorFactory; + private readonly IServiceProvider _serviceProvider; + + public ValidationStreamPipelineBehavior(IValidatorFactory validatorFactory, IServiceProvider serviceProvider) + { + _validatorFactory = validatorFactory; + _serviceProvider = serviceProvider; + } + + public async IAsyncEnumerable Handle(T request, [EnumeratorCancellation] CancellationToken cancellationToken, StreamHandlerDelegate next) + { + var validator = _validatorFactory.GetValidator(); + if (validator != null) + { + var context = new ValidationContext(request); + context.SetServiceProvider(_serviceProvider); + + var response = await validator.ValidateAsync(context, cancellationToken).ConfigureAwait(false); + if (!response.IsValid) + { + throw new ValidationException(response.Errors); + } + } + + await foreach (var item in next().WithCancellation(cancellationToken)) yield return item; + } +} diff --git a/src/HotChocolate/Conventions/GraphqlConvention.cs b/src/HotChocolate/Conventions/GraphqlConvention.cs index a05ebcf5f..d519e872d 100644 --- a/src/HotChocolate/Conventions/GraphqlConvention.cs +++ b/src/HotChocolate/Conventions/GraphqlConvention.cs @@ -1,5 +1,4 @@ -using FairyBread; -using MediatR; +using MediatR; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -21,7 +20,6 @@ public class GraphqlConvention : IServiceConvention { private readonly FoundationOptions _foundationOptions; private readonly RocketChocolateOptions _rocketChocolateOptions; - private readonly IFairyBreadOptions _options; /// /// The graphql convention @@ -30,14 +28,12 @@ public class GraphqlConvention : IServiceConvention /// /// public GraphqlConvention( - IFairyBreadOptions? options = null, RocketChocolateOptions? rocketChocolateOptions = null, FoundationOptions? foundationOptions = null ) { _foundationOptions = foundationOptions ?? new FoundationOptions(); _rocketChocolateOptions = rocketChocolateOptions ?? new RocketChocolateOptions(); - _options = options ?? new DefaultFairyBreadOptions(); } /// @@ -46,23 +42,13 @@ public void Register(IConventionContext context, IConfiguration configuration, I var types = context.AssemblyCandidateFinder.GetCandidateAssemblies("MediatR") .SelectMany(z => z.GetTypes()) .Where(typeof(IBaseRequest).IsAssignableFrom) - .Where(z => z is { IsNested: true, DeclaringType: { } }) // TODO: Configurable? + .Where(z => z is { IsAbstract: false }) + .Where(_rocketChocolateOptions.RequestPredicate) .ToArray(); - services.TryAdd( - ServiceDescriptor.Describe( - typeof(IValidatorProvider), - typeof(FairyBreadValidatorProvider), - ServiceLifetime.Singleton - ) - ); - services.TryAddSingleton(); - services.TryAddSingleton(_options); - var sb = services .AddGraphQL() - .AddErrorFilter() - ; + .AddErrorFilter(); sb.ConfigureSchema(c => c.AddType(new AutoConfigureMediatRMutation(types))); if (!_rocketChocolateOptions.IncludeAssemblyInfoQuery) diff --git a/src/HotChocolate/Extensions/IPatternExtensions.cs b/src/HotChocolate/Extensions/IPatternExtensions.cs deleted file mode 100644 index c23d2657a..000000000 --- a/src/HotChocolate/Extensions/IPatternExtensions.cs +++ /dev/null @@ -1,34 +0,0 @@ -using NodaTime.Text; - -namespace Rocket.Surgery.LaunchPad.HotChocolate.Extensions; - -internal static class IPatternExtensions -{ - public static bool TryParse(this IPattern pattern, string text, out NodaTimeType? output) - where NodaTimeType : struct - { - var result = pattern.Parse(text); - if (result.Success) - { - output = result.Value; - return true; - } - - output = null; - return false; - } - - public static bool TryParse(this IPattern pattern, string text, out NodaTimeType? output) - where NodaTimeType : class - { - var result = pattern.Parse(text); - if (result.Success) - { - output = result.Value; - return true; - } - - output = null; - return false; - } -} diff --git a/src/HotChocolate/Extensions/ISchemaBuilderExtensions.cs b/src/HotChocolate/Extensions/ISchemaBuilderExtensions.cs index d3d4b6c6e..f51be88dd 100644 --- a/src/HotChocolate/Extensions/ISchemaBuilderExtensions.cs +++ b/src/HotChocolate/Extensions/ISchemaBuilderExtensions.cs @@ -1,5 +1,9 @@ using HotChocolate; -using Rocket.Surgery.LaunchPad.HotChocolate.Types; +using HotChocolate.Data.Filters; +using HotChocolate.Types.NodaTime; +using NodaTime; +using NodaTime.Text; +using Rocket.Surgery.LaunchPad.Foundation; namespace Rocket.Surgery.LaunchPad.HotChocolate.Extensions; @@ -15,19 +19,556 @@ public static class ISchemaBuilderExtensions /// public static ISchemaBuilder AddNodaTime(this ISchemaBuilder schemaBuilder) { + schemaBuilder + .AddFiltering() + .AddConvention( + new FilterConventionExtension( + descriptor => descriptor + .BindRuntimeType>() + .BindRuntimeType>() + .BindRuntimeType>() + .BindRuntimeType>() + .BindRuntimeType>() + .BindRuntimeType>() + .BindRuntimeType>() + .BindRuntimeType>() + .BindRuntimeType>() + .BindRuntimeType>() + .BindRuntimeType>() + .BindRuntimeType>() + .BindRuntimeType>() + .BindRuntimeType>() + ) + ); + return schemaBuilder .AddType() - .AddType() - .AddType() + .AddType(new DurationType(DurationPattern.JsonRoundtrip, DurationPattern.Roundtrip)) + .AddType(new InstantType(InstantPattern.General, InstantPattern.ExtendedIso, new InstantDateTimeOffsetPattern())) .AddType() - .AddType() - .AddType() - .AddType() - .AddType() - .AddType() - .AddType() - .AddType() - .AddType() + .AddType(new LocalDateTimeType(LocalDateTimePattern.GeneralIso, LocalDateTimePattern.ExtendedIso, LocalDateTimePattern.BclRoundtrip)) + .AddType(new LocalDateType(LocalDatePattern.Iso, LocalDatePattern.FullRoundtrip)) + .AddType(new LocalTimeType(LocalTimePattern.ExtendedIso, LocalTimePattern.GeneralIso)) + .AddType(new OffsetDateTimeType(OffsetDateTimePattern.GeneralIso, OffsetDateTimePattern.FullRoundtrip)) + .AddType(new OffsetDateType(OffsetDatePattern.GeneralIso, OffsetDatePattern.FullRoundtrip)) + .AddType(new OffsetTimeType(OffsetTimePattern.Rfc3339, OffsetTimePattern.GeneralIso, OffsetTimePattern.ExtendedIso)) + .AddType(new OffsetType(OffsetPattern.GeneralInvariant, OffsetPattern.GeneralInvariantWithZ)) + .AddType(new PeriodType(PeriodPattern.Roundtrip, PeriodPattern.NormalizingIso)) .AddType(); } + + /// + /// Represents a fixed (and calendar-independent) length of time. + /// + public class DurationType : StringToStructBaseType + { + private readonly IPattern[] _allowedPatterns; + private readonly IPattern _serializationPattern; + + /// + /// Initializes a new instance of . + /// + public DurationType() : this(DurationPattern.Roundtrip) + { + } + + /// + /// Initializes a new instance of . + /// + public DurationType(params IPattern[] allowedPatterns) : base("Duration") + { + _allowedPatterns = allowedPatterns; + _serializationPattern = allowedPatterns[0]; + Description = "Represents a fixed (and calendar-independent) length of time."; + } + + /// + protected override string Serialize(Duration runtimeValue) + { + return _serializationPattern + .Format(runtimeValue); + } + + /// + protected override bool TryDeserialize( + string resultValue, + [NotNullWhen(true)] out Duration? runtimeValue + ) + { + return _allowedPatterns.TryParse(resultValue, out runtimeValue); + } + } + + /// + /// Represents an instant on the global timeline, with nanosecond resolution. + /// + public class InstantType : StringToStructBaseType + { + private readonly IPattern[] _allowedPatterns; + private readonly IPattern _serializationPattern; + + /// + /// Initializes a new instance of . + /// + public InstantType() : this(InstantPattern.ExtendedIso) + { + } + + /// + /// Initializes a new instance of . + /// + public InstantType(params IPattern[] allowedPatterns) : base("Instant") + { + _allowedPatterns = allowedPatterns; + _serializationPattern = allowedPatterns[0]; + Description = "Represents an instant on the global timeline, with nanosecond resolution."; + } + + /// + protected override string Serialize(Instant runtimeValue) + { + return _serializationPattern + .Format(runtimeValue); + } + + /// + protected override bool TryDeserialize( + string resultValue, + [NotNullWhen(true)] out Instant? runtimeValue + ) + { + return _allowedPatterns.TryParse(resultValue, out runtimeValue); + } + } + + + /// + /// A date and time in a particular calendar system. + /// + public class LocalDateTimeType : StringToStructBaseType + { + private readonly IPattern[] _allowedPatterns; + private readonly IPattern _serializationPattern; + + /// + /// Initializes a new instance of . + /// + public LocalDateTimeType() : this(LocalDateTimePattern.ExtendedIso) + { + } + + /// + /// Initializes a new instance of . + /// + public LocalDateTimeType(params IPattern[] allowedPatterns) : base("LocalDateTime") + { + _allowedPatterns = allowedPatterns; + _serializationPattern = allowedPatterns[0]; + Description = "A date and time in a particular calendar system."; + } + + /// + protected override string Serialize(LocalDateTime runtimeValue) + { + return _serializationPattern + .Format(runtimeValue); + } + + /// + protected override bool TryDeserialize( + string resultValue, + [NotNullWhen(true)] out LocalDateTime? runtimeValue + ) + { + return _allowedPatterns.TryParse(resultValue, out runtimeValue); + } + } + + /// + /// LocalDate is an immutable struct representing a date within the calendar, + /// with no reference to a particular time zone or time of day. + /// + public class LocalDateType : StringToStructBaseType + { + private readonly IPattern[] _allowedPatterns; + private readonly IPattern _serializationPattern; + + /// + /// Initializes a new instance of . + /// + public LocalDateType() : this(LocalDatePattern.Iso) + { + } + + /// + /// Initializes a new instance of . + /// + public LocalDateType(params IPattern[] allowedPatterns) : base("LocalDate") + { + _allowedPatterns = allowedPatterns; + _serializationPattern = allowedPatterns[0]; + Description = + "LocalDate is an immutable struct representing a date within the calendar, with no reference to a particular time zone or time of day."; + } + + /// + protected override string Serialize(LocalDate runtimeValue) + { + return _serializationPattern + .Format(runtimeValue); + } + + /// + protected override bool TryDeserialize( + string resultValue, + [NotNullWhen(true)] out LocalDate? runtimeValue + ) + { + return _allowedPatterns.TryParse(resultValue, out runtimeValue); + } + } + + /// + /// LocalTime is an immutable struct representing a time of day, + /// with no reference to a particular calendar, time zone or date. + /// + public class LocalTimeType : StringToStructBaseType + { + private readonly IPattern[] _allowedPatterns; + private readonly IPattern _serializationPattern; + + /// + /// Initializes a new instance of . + /// + public LocalTimeType() : this(LocalTimePattern.ExtendedIso) + { + } + + /// + /// Initializes a new instance of . + /// + public LocalTimeType(params IPattern[] allowedPatterns) : base("LocalTime") + { + _allowedPatterns = allowedPatterns; + _serializationPattern = allowedPatterns[0]; + Description = "LocalTime is an immutable struct representing a time of day, with no reference to a particular calendar, time zone or date."; + } + + /// + protected override string Serialize(LocalTime runtimeValue) + { + return _serializationPattern + .Format(runtimeValue); + } + + /// + protected override bool TryDeserialize( + string resultValue, + [NotNullWhen(true)] out LocalTime? runtimeValue + ) + { + return _allowedPatterns.TryParse(resultValue, out runtimeValue); + } + } + + /// + /// A local date and time in a particular calendar system, combined with an offset from UTC. + /// + public class OffsetDateTimeType : StringToStructBaseType + { + private readonly IPattern[] _allowedPatterns; + private readonly IPattern _serializationPattern; + + /// + /// Initializes a new instance of . + /// + public OffsetDateTimeType() : this(OffsetDateTimePattern.ExtendedIso) + { + // Backwards compatibility with the original code's behavior + _serializationPattern = OffsetDateTimePattern.GeneralIso; + _allowedPatterns = new IPattern[] { OffsetDateTimePattern.ExtendedIso }; + } + + /// + /// Initializes a new instance of . + /// + public OffsetDateTimeType(params IPattern[] allowedPatterns) : base("OffsetDateTime") + { + _allowedPatterns = allowedPatterns; + _serializationPattern = _allowedPatterns[0]; + Description = "A local date and time in a particular calendar system, combined with an offset from UTC."; + } + + /// + protected override string Serialize(OffsetDateTime runtimeValue) + { + return _serializationPattern + .Format(runtimeValue); + } + + /// + protected override bool TryDeserialize( + string resultValue, + [NotNullWhen(true)] out OffsetDateTime? runtimeValue + ) + { + return _allowedPatterns.TryParse(resultValue, out runtimeValue); + } + } + + /// + /// A combination of a LocalDate and an Offset, + /// to represent a date at a specific offset from UTC but + /// without any time-of-day information. + /// + public class OffsetDateType : StringToStructBaseType + { + private readonly IPattern[] _allowedPatterns; + private readonly IPattern _serializationPattern; + + /// + /// Initializes a new instance of . + /// + public OffsetDateType() : this(OffsetDatePattern.GeneralIso) + { + } + + /// + /// Initializes a new instance of . + /// + public OffsetDateType(params IPattern[] allowedPatterns) : base("OffsetDate") + { + _allowedPatterns = allowedPatterns; + _serializationPattern = allowedPatterns[0]; + Description = + "A combination of a LocalDate and an Offset, to represent a date at a specific offset from UTC but without any time-of-day information."; + } + + /// + protected override string Serialize(OffsetDate runtimeValue) + { + return _serializationPattern + .Format(runtimeValue); + } + + /// + protected override bool TryDeserialize( + string resultValue, + [NotNullWhen(true)] out OffsetDate? runtimeValue + ) + { + return _allowedPatterns.TryParse(resultValue, out runtimeValue); + } + } + + /// + /// A combination of a LocalTime and an Offset, to represent a time-of-day at a specific offset + /// from UTC but without any date information. + /// + public class OffsetTimeType : StringToStructBaseType + { + private readonly IPattern[] _allowedPatterns; + private readonly IPattern _serializationPattern; + + /// + /// Initializes a new instance of . + /// + public OffsetTimeType() : this(OffsetTimePattern.GeneralIso) + { + } + + /// + /// Initializes a new instance of . + /// + public OffsetTimeType(params IPattern[] allowedPatterns) : base("OffsetTime") + { + _allowedPatterns = allowedPatterns; + _serializationPattern = _allowedPatterns[0]; + + Description = + "A combination of a LocalTime and an Offset, to represent a time-of-day at a specific offset from UTC but without any date information."; + } + + /// + protected override string Serialize(OffsetTime runtimeValue) + { + return _serializationPattern + .Format(runtimeValue); + } + + /// + protected override bool TryDeserialize( + string resultValue, + [NotNullWhen(true)] out OffsetTime? runtimeValue + ) + { + return _allowedPatterns.TryParse(resultValue, out runtimeValue); + } + } + + /// + /// An offset from UTC in seconds. + /// A positive value means that the local time is ahead of UTC (e.g. for Europe); + /// a negative value means that the local time is behind UTC (e.g. for America). + /// + public class OffsetType : StringToStructBaseType + { + private readonly IPattern[] _allowedPatterns; + private readonly IPattern _serializationPattern; + + /// + /// Initializes a new instance of . + /// + public OffsetType() : this(OffsetPattern.GeneralInvariantWithZ) + { + } + + /// + /// Initializes a new instance of . + /// + public OffsetType(params IPattern[] allowedPatterns) : base("Offset") + { + _allowedPatterns = allowedPatterns; + _serializationPattern = allowedPatterns[0]; + Description = + "An offset from UTC in seconds. A positive value means that the local time is ahead of UTC (e.g. for Europe); a negative value means that the local time is behind UTC (e.g. for America)."; + } + + /// + protected override string Serialize(Offset runtimeValue) + { + return _serializationPattern + .Format(runtimeValue); + } + + /// + protected override bool TryDeserialize( + string resultValue, + [NotNullWhen(true)] out Offset? runtimeValue + ) + { + return _allowedPatterns.TryParse(resultValue, out runtimeValue); + } + } + + /// + /// Represents a period of time expressed in human chronological terms: + /// hours, days, weeks, months and so on. + /// + public class PeriodType : StringToClassBaseType + { + private readonly IPattern[] _allowedPatterns; + private readonly IPattern _serializationPattern; + + /// + /// Initializes a new instance of . + /// + public PeriodType() : this(PeriodPattern.Roundtrip) + { + } + + /// + /// Initializes a new instance of . + /// + public PeriodType(params IPattern[] allowedPatterns) : base("Period") + { + _allowedPatterns = allowedPatterns; + _serializationPattern = allowedPatterns[0]; + Description = "Represents a period of time expressed in human chronological terms: hours, days, weeks, months and so on."; + } + + /// + protected override string Serialize(Period runtimeValue) + { + return _serializationPattern.Format(runtimeValue); + } + + /// + protected override bool TryDeserialize( + string resultValue, + [NotNullWhen(true)] out Period? runtimeValue + ) + { + return _allowedPatterns.TryParse(resultValue, out runtimeValue); + } + } +} + +internal static class PatternExtensions +{ + public static bool TryParse( + this IPattern pattern, + string text, + [NotNullWhen(true)] out NodaTimeType? output + ) + where NodaTimeType : struct + { + var result = pattern.Parse(text); + + if (result.Success) + { + output = result.Value; + return true; + } + + output = null; + return false; + } + + public static bool TryParse( + this IPattern pattern, + string text, + [NotNullWhen(true)] out NodaTimeType? output + ) + where NodaTimeType : class + { + var result = pattern.Parse(text); + + if (result.Success) + { + output = result.Value; + return true; + } + + output = null; + return false; + } + + public static bool TryParse( + this IPattern[] patterns, + string text, + [NotNullWhen(true)] out NodaTimeType? output + ) + where NodaTimeType : struct + { + foreach (var pattern in patterns) + { + if (pattern.TryParse(text, out output)) + { + return true; + } + } + + output = default; + return false; + } + + public static bool TryParse( + this IPattern[] patterns, + string text, + [NotNullWhen(true)] out NodaTimeType? output + ) + where NodaTimeType : class + { + foreach (var pattern in patterns) + { + if (pattern.TryParse(text, out output)) + { + return true; + } + } + + output = default; + return false; + } } diff --git a/src/HotChocolate/GraphqlErrorFilter.cs b/src/HotChocolate/GraphqlErrorFilter.cs index aa90354af..34e5a298d 100644 --- a/src/HotChocolate/GraphqlErrorFilter.cs +++ b/src/HotChocolate/GraphqlErrorFilter.cs @@ -15,29 +15,41 @@ internal class GraphqlErrorFilter : IErrorFilter public IError OnError(IError error) { - if (error.Exception is IProblemDetailsData ex) + if (error.Exception is ValidationException vx) { - return ErrorBuilder.FromError(error) - .SetMessage(error.Exception.Message) - .RemoveException() - .WithProblemDetails(ex) - .Build(); + var childErrors = + vx.Errors.Select(x => new FluentValidationProblemDetail(x)) + .Select( + x => new Error(x.ErrorMessage) + .WithCode(x.ErrorCode) + .SetExtension("severity", x.Severity.ToString()) + .SetExtension("attemptedValue", x.AttemptedValue) + .SetExtension("field", x.PropertyName) + .SetExtension("propertyName", x.PropertyName) + ); + var result = new AggregateError(childErrors); + return result; } - if (error.Exception is ValidationException vx) + if (error.Exception is IProblemDetailsData ex) { - return ErrorBuilder.FromError(error) - .SetMessage(vx.Message) - .RemoveException() - .SetExtension("type", "ValidationProblemDetails") - .SetExtension("title", "Unprocessable Entity") - .SetExtension("link", "https://tools.ietf.org/html/rfc4918#section-11.2") - .SetExtension("errors", vx.Errors.Select(FormatFailure).ToArray()) - .Build(); - // return ErrorBuilder.FromError(error) - // .SetMessage(vx.Message) - // .SetException(null) - // .Build(); + var builder = ErrorBuilder.FromError(error); + builder + .SetException(error.Exception) + .SetMessage(error.Exception.Message) + .WithProblemDetails(ex); + + if (error.Exception is NotFoundException) + { + builder.SetCode("NOTFOUND"); + } + + if (error.Exception is RequestFailedException) + { + builder.SetCode("FAILED"); + } + + return builder.Build(); } return error; diff --git a/src/HotChocolate/Rocket.Surgery.LaunchPad.HotChocolate.csproj b/src/HotChocolate/Rocket.Surgery.LaunchPad.HotChocolate.csproj index 529cb1642..c7ed1c1be 100644 --- a/src/HotChocolate/Rocket.Surgery.LaunchPad.HotChocolate.csproj +++ b/src/HotChocolate/Rocket.Surgery.LaunchPad.HotChocolate.csproj @@ -1,11 +1,13 @@  netstandard2.1;net6.0 - + $(PackageTags) + + diff --git a/src/HotChocolate/RocketChocolateOptions.cs b/src/HotChocolate/RocketChocolateOptions.cs index 0d241d559..0d8b64a25 100644 --- a/src/HotChocolate/RocketChocolateOptions.cs +++ b/src/HotChocolate/RocketChocolateOptions.cs @@ -9,4 +9,9 @@ public class RocketChocolateOptions /// Include the assembly info query data automagically /// public bool IncludeAssemblyInfoQuery { get; set; } + + /// + /// A check that can be used to select specific MediatR requests that will be converted to mutations. + /// + public Func RequestPredicate { get; set; } = z => z is { IsNested: true, DeclaringType: { } }; } diff --git a/src/HotChocolate/Types/DateTimeZoneType.cs b/src/HotChocolate/Types/DateTimeZoneType.cs deleted file mode 100644 index 6d3d7c107..000000000 --- a/src/HotChocolate/Types/DateTimeZoneType.cs +++ /dev/null @@ -1,42 +0,0 @@ -using NodaTime; -using Rocket.Surgery.LaunchPad.HotChocolate.Helpers; - -namespace Rocket.Surgery.LaunchPad.HotChocolate.Types; - -/// -/// Represents a in Hot Chocolate -/// -[UsedImplicitly] -public class DateTimeZoneType : StringToClassBaseType -{ - /// - /// The constuctor - /// - public DateTimeZoneType() : base("DateTimeZone") - { - Description = - "Represents a time zone - a mapping between UTC and local time.\n" + - "A time zone maps UTC instants to local times - or, equivalently, " + - "to the offset from UTC at any particular instant."; - } - - /// - protected override string Serialize(DateTimeZone baseValue) - { - return baseValue.Id; - } - - /// - protected override bool TryDeserialize(string str, [NotNullWhen(true)] out DateTimeZone? output) - { - var result = DateTimeZoneProviders.Tzdb.GetZoneOrNull(str); - if (result == null) - { - output = null; - return false; - } - - output = result; - return true; - } -} diff --git a/src/HotChocolate/Types/DurationType.cs b/src/HotChocolate/Types/DurationType.cs deleted file mode 100644 index 6f62032c2..000000000 --- a/src/HotChocolate/Types/DurationType.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Globalization; -using NodaTime; -using NodaTime.Text; -using Rocket.Surgery.LaunchPad.HotChocolate.Extensions; -using Rocket.Surgery.LaunchPad.HotChocolate.Helpers; - -namespace Rocket.Surgery.LaunchPad.HotChocolate.Types; - -/// -/// Represents a in Hot Chocolate -/// -public class DurationType : StringToStructBaseType -{ - /// - /// The constructor - /// - public DurationType() : base("Duration") - { - Description = "Represents a fixed (and calendar-independent) length of time."; - } - - /// - protected override string Serialize(Duration baseValue) - { - return DurationPattern.Roundtrip - .WithCulture(CultureInfo.InvariantCulture) - .Format(baseValue); - } - - /// - protected override bool TryDeserialize(string str, [NotNullWhen(true)] out Duration? output) - { - return DurationPattern.Roundtrip - .WithCulture(CultureInfo.InvariantCulture) - .TryParse(str, out output); - } -} diff --git a/src/HotChocolate/Types/InstantType.cs b/src/HotChocolate/Types/InstantType.cs deleted file mode 100644 index 0fdc29495..000000000 --- a/src/HotChocolate/Types/InstantType.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Globalization; -using NodaTime; -using NodaTime.Text; -using Rocket.Surgery.LaunchPad.HotChocolate.Extensions; -using Rocket.Surgery.LaunchPad.HotChocolate.Helpers; - -namespace Rocket.Surgery.LaunchPad.HotChocolate.Types; - -/// -/// Represents an in Hot Chocolate -/// -[UsedImplicitly] -public class InstantType : StringToStructBaseType -{ - /// - /// The constructor - /// - public InstantType() : base("Instant") - { - Description = "Represents an instant on the global timeline, with nanosecond resolution."; - } - - /// - protected override string Serialize(Instant baseValue) - { - return InstantPattern.ExtendedIso - .WithCulture(CultureInfo.InvariantCulture) - .Format(baseValue); - } - - /// - protected override bool TryDeserialize(string str, [NotNullWhen(true)] out Instant? output) - { - return InstantPattern.ExtendedIso - .WithCulture(CultureInfo.InvariantCulture) - .TryParse(str, out output); - } -} diff --git a/src/HotChocolate/Types/IsoDayOfWeekType.cs b/src/HotChocolate/Types/IsoDayOfWeekType.cs deleted file mode 100644 index 57b305bb5..000000000 --- a/src/HotChocolate/Types/IsoDayOfWeekType.cs +++ /dev/null @@ -1,46 +0,0 @@ -using NodaTime; -using Rocket.Surgery.LaunchPad.HotChocolate.Helpers; - -namespace Rocket.Surgery.LaunchPad.HotChocolate.Types; - -/// -/// Represents an in Hot Chocolate -/// -public class IsoDayOfWeekType : IntToStructBaseType -{ - /// - /// The constructor - /// - public IsoDayOfWeekType() : base("IsoDayOfWeek") - { - Description = - "Equates the days of the week with their numerical value according to ISO-8601.\n" + - "Monday = 1, Tuesday = 2, Wednesday = 3, Thursday = 4, Friday = 5, Saturday = 6, Sunday = 7."; - } - - /// - protected override bool TrySerialize(IsoDayOfWeek baseValue, [NotNullWhen(true)] out int? output) - { - if (baseValue == IsoDayOfWeek.None) - { - output = null; - return false; - } - - output = (int)baseValue; - return true; - } - - /// - protected override bool TryDeserialize(int val, [NotNullWhen(true)] out IsoDayOfWeek? output) - { - if (val < 1 || val > 7) - { - output = null; - return false; - } - - output = (IsoDayOfWeek)val; - return true; - } -} diff --git a/src/HotChocolate/Types/LocalDateTimeType.cs b/src/HotChocolate/Types/LocalDateTimeType.cs deleted file mode 100644 index 5a1d7c11d..000000000 --- a/src/HotChocolate/Types/LocalDateTimeType.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Globalization; -using NodaTime; -using NodaTime.Text; -using Rocket.Surgery.LaunchPad.HotChocolate.Extensions; -using Rocket.Surgery.LaunchPad.HotChocolate.Helpers; - -namespace Rocket.Surgery.LaunchPad.HotChocolate.Types; - -/// -/// Represents a in HotChocolate -/// -public class LocalDateTimeType : StringToStructBaseType -{ - /// - /// The constructor - /// - public LocalDateTimeType() : base("LocalDateTime") - { - Description = "A date and time in a particular calendar system."; - } - - /// - protected override string Serialize(LocalDateTime baseValue) - { - return LocalDateTimePattern.ExtendedIso - .WithCulture(CultureInfo.InvariantCulture) - .Format(baseValue); - } - - /// - protected override bool TryDeserialize(string str, [NotNullWhen(true)] out LocalDateTime? output) - { - return LocalDateTimePattern.ExtendedIso - .WithCulture(CultureInfo.InvariantCulture) - .TryParse(str, out output); - } -} diff --git a/src/HotChocolate/Types/LocalDateType.cs b/src/HotChocolate/Types/LocalDateType.cs deleted file mode 100644 index e1cd557c9..000000000 --- a/src/HotChocolate/Types/LocalDateType.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Globalization; -using NodaTime; -using NodaTime.Text; -using Rocket.Surgery.LaunchPad.HotChocolate.Extensions; -using Rocket.Surgery.LaunchPad.HotChocolate.Helpers; - -namespace Rocket.Surgery.LaunchPad.HotChocolate.Types; - -/// -/// Represents a in Hot Chocolate -/// -public class LocalDateType : StringToStructBaseType -{ - /// - /// The constructor - /// - public LocalDateType() : base("LocalDate") - { - Description = - "LocalDate is an immutable struct representing a date " + - "within the calendar, with no reference to a particular " + - "time zone or time of day."; - } - - /// - protected override string Serialize(LocalDate baseValue) - { - return LocalDatePattern.Iso - .WithCulture(CultureInfo.InvariantCulture) - .Format(baseValue); - } - - /// - protected override bool TryDeserialize(string str, [NotNullWhen(true)] out LocalDate? output) - { - return LocalDatePattern.Iso - .WithCulture(CultureInfo.InvariantCulture) - .TryParse(str, out output); - } -} diff --git a/src/HotChocolate/Types/LocalTimeType.cs b/src/HotChocolate/Types/LocalTimeType.cs deleted file mode 100644 index 94a30883d..000000000 --- a/src/HotChocolate/Types/LocalTimeType.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Globalization; -using NodaTime; -using NodaTime.Text; -using Rocket.Surgery.LaunchPad.HotChocolate.Extensions; -using Rocket.Surgery.LaunchPad.HotChocolate.Helpers; - -namespace Rocket.Surgery.LaunchPad.HotChocolate.Types; - -/// -/// Represents a in Hot Chocolate -/// -public class LocalTimeType : StringToStructBaseType -{ - /// - /// The constructor - /// - public LocalTimeType() : base("LocalTime") - { - Description = "LocalTime is an immutable struct representing a time of day, with no reference to a particular calendar, time zone or date."; - } - - /// - protected override string Serialize(LocalTime baseValue) - { - return LocalTimePattern.ExtendedIso - .WithCulture(CultureInfo.InvariantCulture) - .Format(baseValue); - } - - /// - protected override bool TryDeserialize(string str, [NotNullWhen(true)] out LocalTime? output) - { - return LocalTimePattern.ExtendedIso - .WithCulture(CultureInfo.InvariantCulture) - .TryParse(str, out output); - } -} diff --git a/src/HotChocolate/Types/OffsetDateTimeType.cs b/src/HotChocolate/Types/OffsetDateTimeType.cs deleted file mode 100644 index 7e21caa31..000000000 --- a/src/HotChocolate/Types/OffsetDateTimeType.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System.Globalization; -using NodaTime; -using NodaTime.Text; -using Rocket.Surgery.LaunchPad.HotChocolate.Extensions; -using Rocket.Surgery.LaunchPad.HotChocolate.Helpers; - -namespace Rocket.Surgery.LaunchPad.HotChocolate.Types; - -/// -/// Represents a in Hot Chocolate -/// -public class OffsetDateTimeType : StringToStructBaseType -{ - /// - /// The constructor - /// - public OffsetDateTimeType() : base("OffsetDateTime") - { - Description = "A local date and time in a particular calendar system, combined with an offset from UTC."; - } - - /// - protected override string Serialize(OffsetDateTime baseValue) - { - return OffsetDateTimePattern.GeneralIso - .WithCulture(CultureInfo.InvariantCulture) - .Format(baseValue); - } - - /// - protected override bool TryDeserialize(string str, [NotNullWhen(true)] out OffsetDateTime? output) - { - return OffsetDateTimePattern.ExtendedIso - .WithCulture(CultureInfo.InvariantCulture) - .TryParse(str, out output); - } -} diff --git a/src/HotChocolate/Types/OffsetDateType.cs b/src/HotChocolate/Types/OffsetDateType.cs deleted file mode 100644 index 160a9da77..000000000 --- a/src/HotChocolate/Types/OffsetDateType.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Globalization; -using NodaTime; -using NodaTime.Text; -using Rocket.Surgery.LaunchPad.HotChocolate.Extensions; -using Rocket.Surgery.LaunchPad.HotChocolate.Helpers; - -namespace Rocket.Surgery.LaunchPad.HotChocolate.Types; - -/// -/// Represents a in Hot Chocolate -/// -public class OffsetDateType : StringToStructBaseType -{ - /// - /// The constructor - /// - public OffsetDateType() : base("OffsetDate") - { - Description = - "A combination of a LocalDate and an Offset, to represent a date " + - "at a specific offset from UTC but without any time-of-day information."; - } - - /// - protected override string Serialize(OffsetDate baseValue) - { - return OffsetDatePattern.GeneralIso - .WithCulture(CultureInfo.InvariantCulture) - .Format(baseValue); - } - - /// - protected override bool TryDeserialize(string str, [NotNullWhen(true)] out OffsetDate? output) - { - return OffsetDatePattern.GeneralIso - .WithCulture(CultureInfo.InvariantCulture) - .TryParse(str, out output); - } -} diff --git a/src/HotChocolate/Types/OffsetTimeType.cs b/src/HotChocolate/Types/OffsetTimeType.cs deleted file mode 100644 index c153d585a..000000000 --- a/src/HotChocolate/Types/OffsetTimeType.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Globalization; -using NodaTime; -using NodaTime.Text; -using Rocket.Surgery.LaunchPad.HotChocolate.Extensions; -using Rocket.Surgery.LaunchPad.HotChocolate.Helpers; - -namespace Rocket.Surgery.LaunchPad.HotChocolate.Types; - -/// -/// Represents a in Hot Chocolate -/// -public class OffsetTimeType : StringToStructBaseType -{ - /// - /// The constructor - /// - public OffsetTimeType() : base("OffsetTime") - { - Description = - "A combination of a LocalTime and an Offset, " + - "to represent a time-of-day at a specific offset from UTC " + - "but without any date information."; - } - - /// - protected override string Serialize(OffsetTime baseValue) - { - return OffsetTimePattern.GeneralIso - .WithCulture(CultureInfo.InvariantCulture) - .Format(baseValue); - } - - /// - protected override bool TryDeserialize(string str, [NotNullWhen(true)] out OffsetTime? output) - { - return OffsetTimePattern.GeneralIso - .WithCulture(CultureInfo.InvariantCulture) - .TryParse(str, out output); - } -} diff --git a/src/HotChocolate/Types/OffsetType.cs b/src/HotChocolate/Types/OffsetType.cs deleted file mode 100644 index 2d5e0e5dc..000000000 --- a/src/HotChocolate/Types/OffsetType.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System.Globalization; -using NodaTime; -using NodaTime.Text; -using Rocket.Surgery.LaunchPad.HotChocolate.Extensions; -using Rocket.Surgery.LaunchPad.HotChocolate.Helpers; - -namespace Rocket.Surgery.LaunchPad.HotChocolate.Types; - -/// -/// Represents an in Hot Chocolate -/// -public class OffsetType : StringToStructBaseType -{ - /// - /// The constructor - /// - public OffsetType() : base("Offset") - { - Description = - "An offset from UTC in seconds.\n" + - "A positive value means that the local time is ahead of UTC (e.g. for Europe); " + - "a negative value means that the local time is behind UTC (e.g. for America)."; - } - - /// - protected override string Serialize(Offset baseValue) - { - return OffsetPattern.GeneralInvariantWithZ - .WithCulture(CultureInfo.InvariantCulture) - .Format(baseValue); - } - - /// - protected override bool TryDeserialize(string str, [NotNullWhen(true)] out Offset? output) - { - return OffsetPattern.GeneralInvariantWithZ - .WithCulture(CultureInfo.InvariantCulture) - .TryParse(str, out output); - } -} diff --git a/src/HotChocolate/Types/PeriodType.cs b/src/HotChocolate/Types/PeriodType.cs deleted file mode 100644 index 0c88265fe..000000000 --- a/src/HotChocolate/Types/PeriodType.cs +++ /dev/null @@ -1,36 +0,0 @@ -using NodaTime; -using NodaTime.Text; -using Rocket.Surgery.LaunchPad.HotChocolate.Extensions; -using Rocket.Surgery.LaunchPad.HotChocolate.Helpers; - -namespace Rocket.Surgery.LaunchPad.HotChocolate.Types; - -/// -/// Represents a in Hot Chocolate -/// -public class PeriodType : StringToClassBaseType -{ - /// - /// The constructor - /// - public PeriodType() : base("Period") - { - Description = - "Represents a period of time expressed in human chronological " + - "terms: hours, days, weeks, months and so on."; - } - - /// - protected override string Serialize(Period baseValue) - { - return PeriodPattern.Roundtrip - .Format(baseValue); - } - - /// - protected override bool TryDeserialize(string str, [NotNullWhen(true)] out Period? output) - { - return PeriodPattern.Roundtrip - .TryParse(str, out output); - } -} diff --git a/src/HotChocolate/Types/ZonedDateTimeType.cs b/src/HotChocolate/Types/ZonedDateTimeType.cs deleted file mode 100644 index 4804e647e..000000000 --- a/src/HotChocolate/Types/ZonedDateTimeType.cs +++ /dev/null @@ -1,42 +0,0 @@ -using NodaTime; -using NodaTime.Text; -using Rocket.Surgery.LaunchPad.HotChocolate.Extensions; -using Rocket.Surgery.LaunchPad.HotChocolate.Helpers; - -namespace Rocket.Surgery.LaunchPad.HotChocolate.Types; - -/// -/// Represents a in Hot Chocolate -/// -public class ZonedDateTimeType : StringToStructBaseType -{ - private const string FormatString = "uuuu'-'MM'-'dd'T'HH':'mm':'ss' 'z' 'o"; - - /// - /// The constructor - /// - public ZonedDateTimeType() - : base("ZonedDateTime") - { - Description = - "A LocalDateTime in a specific time zone and with a particular offset to " + - "distinguish between otherwise-ambiguous instants.\n" + - "A ZonedDateTime is global, in that it maps to a single Instant."; - } - - /// - protected override string Serialize(ZonedDateTime baseValue) - { - return ZonedDateTimePattern - .CreateWithInvariantCulture(FormatString, DateTimeZoneProviders.Tzdb) - .Format(baseValue); - } - - /// - protected override bool TryDeserialize(string str, [NotNullWhen(true)] out ZonedDateTime? output) - { - return ZonedDateTimePattern - .CreateWithInvariantCulture(FormatString, DateTimeZoneProviders.Tzdb) - .TryParse(str, out output); - } -} diff --git a/test/Analyzers.Tests/Analyzers.Tests.csproj b/test/Analyzers.Tests/Analyzers.Tests.csproj index 10dd4107d..0d456bfb8 100644 --- a/test/Analyzers.Tests/Analyzers.Tests.csproj +++ b/test/Analyzers.Tests/Analyzers.Tests.csproj @@ -6,7 +6,11 @@ + - + + + + diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Error_If_Controller_Is_Not_Partial.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Error_If_Controller_Is_Not_Partial.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Error_If_Controller_Is_Not_Partial.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Error_If_Controller_Is_Not_Partial.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Error_If_Controller_Is_Not_Partial.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Error_If_Controller_Is_Not_Partial.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithAcceptReturnType_sources=.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithAcceptReturnType_sources=.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithAcceptReturnType_sources=.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithAcceptReturnType_sources=.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithAcceptReturnType_sources=.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithAcceptReturnType_sources=.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithAcceptReturnType_sources=.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithAcceptReturnType_sources=.02.verified.cs.txt new file mode 100644 index 000000000..5242555d7 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithAcceptReturnType_sources=.02.verified.cs.txt @@ -0,0 +1,42 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> GetRocket(GetRocket.Request request) + { + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new ObjectResult(result) + {StatusCode = 200}; + } + + [ProducesDefaultResponseType] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> CreateRocket([BindRequired][FromBody] CreateRocket.Request request) + { + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new AcceptedAtActionResult("GetRocket", null, new + { + id = result.Id + } + + , result); + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithCreatedReturn_sources=.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithCreatedReturn_sources=.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithCreatedReturn_sources=.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithCreatedReturn_sources=.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithCreatedReturn_sources=.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithCreatedReturn_sources=.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithCreatedReturn_sources=.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithCreatedReturn_sources=.02.verified.cs.txt new file mode 100644 index 000000000..59c680cb7 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithCreatedReturn_sources=.02.verified.cs.txt @@ -0,0 +1,43 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> GetRocket(GetRocket.Request request) + { + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new ObjectResult(result) + {StatusCode = 200}; + } + + [ProducesDefaultResponseType] + [ProducesResponseType(201)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> CreateRocket([BindRequired][FromBody] CreateRocket.Request request) + { + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new CreatedAtActionResult("GetRocket", null, new + { + id = result.Id + } + + , result); + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters2_sources=.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters2_sources=.00.verified.txt new file mode 100644 index 000000000..e31a40ce5 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters2_sources=.00.verified.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0003, + Title: The parameter must map to a property of the request object, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (16,128)-(16,142), + Description: , + HelpLink: , + MessageFormat: The parameter {0} map to a property of the request {1} object, + Message: The parameter launchRecordId map to a property of the request Request object, + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [ + { + Id: LPAD0003, + Title: The parameter must map to a property of the request object, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (16,128)-(16,142), + Description: , + HelpLink: , + MessageFormat: The parameter {0} map to a property of the request {1} object, + Message: The parameter launchRecordId map to a property of the request Request object, + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters2_sources=.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters2_sources=.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters2_sources=.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters2_sources=.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters2_sources=.02.verified.cs.txt new file mode 100644 index 000000000..df4c7afc7 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters2_sources=.02.verified.cs.txt @@ -0,0 +1,16 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters3_sources=.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters3_sources=.00.verified.txt new file mode 100644 index 000000000..54fe4edbc --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters3_sources=.00.verified.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0003, + Title: The parameter must map to a property of the request object, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (16,128)-(16,136), + Description: , + HelpLink: , + MessageFormat: The parameter {0} map to a property of the request {1} object, + Message: The parameter launchId map to a property of the request Request object, + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [ + { + Id: LPAD0003, + Title: The parameter must map to a property of the request object, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (16,128)-(16,136), + Description: , + HelpLink: , + MessageFormat: The parameter {0} map to a property of the request {1} object, + Message: The parameter launchId map to a property of the request Request object, + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters3_sources=.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters3_sources=.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters3_sources=.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters3_sources=.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters3_sources=.02.verified.cs.txt new file mode 100644 index 000000000..df4c7afc7 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters3_sources=.02.verified.cs.txt @@ -0,0 +1,16 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters4_sources=.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters4_sources=.00.verified.txt new file mode 100644 index 000000000..80b11b3db --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters4_sources=.00.verified.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0004, + Title: The parameter type and property type must match, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (16,130)-(16,144), + Description: , + HelpLink: , + MessageFormat: The parameter {0} type {1} must match the property {2} type {3}, + Message: The parameter launchRecordId type string must match the property Request type Guid, + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [ + { + Id: LPAD0004, + Title: The parameter type and property type must match, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (16,130)-(16,144), + Description: , + HelpLink: , + MessageFormat: The parameter {0} type {1} must match the property {2} type {3}, + Message: The parameter launchRecordId type string must match the property Request type Guid, + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters4_sources=.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters4_sources=.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters4_sources=.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters4_sources=.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters4_sources=.02.verified.cs.txt new file mode 100644 index 000000000..df4c7afc7 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters4_sources=.02.verified.cs.txt @@ -0,0 +1,16 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters_sources=.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters_sources=.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters_sources=.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters_sources=.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters_sources=.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters_sources=.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters_sources=.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters_sources=.02.verified.cs.txt new file mode 100644 index 000000000..23d199164 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodiesWithMultipleParameters_sources=.02.verified.cs.txt @@ -0,0 +1,37 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(204)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task GetRocketLaunchRecords(Guid id, [Bind(), CustomizeValidator(Properties = "")][BindRequired][FromRoute] GetRocketLaunchRecords.Request request) + { + var result = await Mediator.Send(request with {Id = id}, HttpContext.RequestAborted).ConfigureAwait(false); + return new StatusCodeResult(204); + } + + [ProducesDefaultResponseType] + [ProducesResponseType(204)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task GetRocketLaunchRecord(Guid id, Guid launchRecordId, [Bind(), CustomizeValidator(Properties = "")][BindRequired][FromRoute] GetRocketLaunchRecord.Request request) + { + var result = await Mediator.Send(request with {Id = id, LaunchRecordId = launchRecordId}, HttpContext.RequestAborted).ConfigureAwait(false); + return new StatusCodeResult(204); + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyForListAction_sources=.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyForListAction_sources=.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyForListAction_sources=.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyForListAction_sources=.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyForListAction_sources=.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyForListAction_sources=.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyForListAction_sources=.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyForListAction_sources=.02.verified.cs.txt new file mode 100644 index 000000000..df6417af3 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyForListAction_sources=.02.verified.cs.txt @@ -0,0 +1,25 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial IAsyncEnumerable ListRockets([FromQuery] ListRockets.Request model) + { + var result = Mediator.CreateStream(model, HttpContext.RequestAborted); + return result; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyForRequest_sources=.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyForRequest_sources=.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyForRequest_sources=.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyForRequest_sources=.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyForRequest_sources=.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyForRequest_sources=.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyForRequest_sources=.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyForRequest_sources=.02.verified.cs.txt new file mode 100644 index 000000000..1f48c0e7b --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyForRequest_sources=.02.verified.cs.txt @@ -0,0 +1,27 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> GetRocket(GetRocket.Request request) + { + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new ObjectResult(result) + {StatusCode = 200}; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyWithIdParameterAndAddBindRequired_sources=.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyWithIdParameterAndAddBindRequired_sources=.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyWithIdParameterAndAddBindRequired_sources=.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyWithIdParameterAndAddBindRequired_sources=.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyWithIdParameterAndAddBindRequired_sources=.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyWithIdParameterAndAddBindRequired_sources=.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyWithIdParameterAndAddBindRequired_sources=.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyWithIdParameterAndAddBindRequired_sources=.02.verified.cs.txt new file mode 100644 index 000000000..99c4946e6 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyWithIdParameterAndAddBindRequired_sources=.02.verified.cs.txt @@ -0,0 +1,26 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(204)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task SaveRocket(Guid id, [Bind(), CustomizeValidator(Properties = "")] SaveRocket.Request request) + { + await Mediator.Send(request with {Id = id}, HttpContext.RequestAborted).ConfigureAwait(false); + return new StatusCodeResult(204); + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyWithIdParameter_sources=.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyWithIdParameter_sources=.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyWithIdParameter_sources=.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyWithIdParameter_sources=.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyWithIdParameter_sources=.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyWithIdParameter_sources=.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyWithIdParameter_sources=.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyWithIdParameter_sources=.02.verified.cs.txt new file mode 100644 index 000000000..bff1937a5 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_key=GenerateBodyWithIdParameter_sources=.02.verified.cs.txt @@ -0,0 +1,28 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> Save2Rocket(Guid id, [Bind("Sn"), CustomizeValidator(Properties = "Sn")] Save2Rocket.Request request) + { + request.Id = id; + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new ObjectResult(result) + {StatusCode = 200}; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=05E8E26E151CF08BC5FBE4824554D.00.received.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=05E8E26E151CF08BC5FBE4824554D.00.received.txt new file mode 100644 index 000000000..e00796c7c --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=05E8E26E151CF08BC5FBE4824554D.00.received.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0003, + Title: The parameter type and property type must match, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (23,128)-(23,142), + Description: , + HelpLink: , + MessageFormat: The parameter {0} type {1} must match the property {2} type {3}, + Message: The parameter launchRecordId type Guid must match the property Request type LaunchRecordId, + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [ + { + Id: LPAD0003, + Title: The parameter type and property type must match, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (23,128)-(23,142), + Description: , + HelpLink: , + MessageFormat: The parameter {0} type {1} must match the property {2} type {3}, + Message: The parameter launchRecordId type Guid must match the property Request type LaunchRecordId, + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=05E8E26E151CF08BC5FBE4824554D.01.received.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=05E8E26E151CF08BC5FBE4824554D.01.received.cs new file mode 100644 index 000000000..0e05543a3 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=05E8E26E151CF08BC5FBE4824554D.01.received.cs @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=05E8E26E151CF08BC5FBE4824554D.02.received.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=05E8E26E151CF08BC5FBE4824554D.02.received.cs new file mode 100644 index 000000000..c1cc08819 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=05E8E26E151CF08BC5FBE4824554D.02.received.cs @@ -0,0 +1,26 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(204)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task GetRocketLaunchRecords(Guid id, [Bind(), CustomizeValidator(Properties = "")][BindRequired][FromRoute] GetRocketLaunchRecords.Request request) + { + var result = await Mediator.Send(request with {Id = id}, HttpContext.RequestAborted).ConfigureAwait(false); + return new StatusCodeResult(204); + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=10EA2414EFDB884BCF89D446A2264.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=10EA2414EFDB884BCF89D446A2264.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=10EA2414EFDB884BCF89D446A2264.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=10EA2414EFDB884BCF89D446A2264.01.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=10EA2414EFDB884BCF89D446A2264.01.verified.cs new file mode 100644 index 000000000..0e05543a3 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=10EA2414EFDB884BCF89D446A2264.01.verified.cs @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=10EA2414EFDB884BCF89D446A2264.02.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=10EA2414EFDB884BCF89D446A2264.02.verified.cs new file mode 100644 index 000000000..9badb7806 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=10EA2414EFDB884BCF89D446A2264.02.verified.cs @@ -0,0 +1,28 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> Save2Rocket(Guid id, [Bind("Sn"), CustomizeValidator(Properties = "Sn")] Save2Rocket.Request request) + { + request.Id = id; + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new ObjectResult(result) + {StatusCode = 200}; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=232A1331EB7AA594E9F1AFADCD7C8D97.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=232A1331EB7AA594E9F1AFADCD7C8D97.00.verified.txt new file mode 100644 index 000000000..e31a40ce5 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=232A1331EB7AA594E9F1AFADCD7C8D97.00.verified.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0003, + Title: The parameter must map to a property of the request object, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (16,128)-(16,142), + Description: , + HelpLink: , + MessageFormat: The parameter {0} map to a property of the request {1} object, + Message: The parameter launchRecordId map to a property of the request Request object, + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [ + { + Id: LPAD0003, + Title: The parameter must map to a property of the request object, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (16,128)-(16,142), + Description: , + HelpLink: , + MessageFormat: The parameter {0} map to a property of the request {1} object, + Message: The parameter launchRecordId map to a property of the request Request object, + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=232A1331EB7AA594E9F1AFADCD7C8D97.01.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=232A1331EB7AA594E9F1AFADCD7C8D97.01.verified.cs new file mode 100644 index 000000000..0e05543a3 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=232A1331EB7AA594E9F1AFADCD7C8D97.01.verified.cs @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=232A1331EB7AA594E9F1AFADCD7C8D97.02.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=232A1331EB7AA594E9F1AFADCD7C8D97.02.verified.cs new file mode 100644 index 000000000..d25bb07ba --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=232A1331EB7AA594E9F1AFADCD7C8D97.02.verified.cs @@ -0,0 +1,16 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=2461ccee1a287f67d915298adabd0a4.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=2461ccee1a287f67d915298adabd0a4.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=2461ccee1a287f67d915298adabd0a4.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=2461ccee1a287f67d915298adabd0a4.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=2461ccee1a287f67d915298adabd0a4.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=2461ccee1a287f67d915298adabd0a4.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=2461ccee1a287f67d915298adabd0a4.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=2461ccee1a287f67d915298adabd0a4.02.verified.cs.txt new file mode 100644 index 000000000..5242555d7 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=2461ccee1a287f67d915298adabd0a4.02.verified.cs.txt @@ -0,0 +1,42 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> GetRocket(GetRocket.Request request) + { + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new ObjectResult(result) + {StatusCode = 200}; + } + + [ProducesDefaultResponseType] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> CreateRocket([BindRequired][FromBody] CreateRocket.Request request) + { + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new AcceptedAtActionResult("GetRocket", null, new + { + id = result.Id + } + + , result); + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=2955978835EDA7FAE221C312B78DF6E8.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=2955978835EDA7FAE221C312B78DF6E8.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=2955978835EDA7FAE221C312B78DF6E8.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=2955978835EDA7FAE221C312B78DF6E8.01.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=2955978835EDA7FAE221C312B78DF6E8.01.verified.cs new file mode 100644 index 000000000..0e05543a3 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=2955978835EDA7FAE221C312B78DF6E8.01.verified.cs @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=2955978835EDA7FAE221C312B78DF6E8.02.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=2955978835EDA7FAE221C312B78DF6E8.02.verified.cs new file mode 100644 index 000000000..dad506594 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=2955978835EDA7FAE221C312B78DF6E8.02.verified.cs @@ -0,0 +1,26 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(204)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task SaveRocket(Guid id, [Bind(), CustomizeValidator(Properties = "")] SaveRocket.Request request) + { + await Mediator.Send(request with {Id = id}, HttpContext.RequestAborted).ConfigureAwait(false); + return new StatusCodeResult(204); + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=5fc328cefd6edf316ac85f765a1b331.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=5fc328cefd6edf316ac85f765a1b331.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=5fc328cefd6edf316ac85f765a1b331.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=5fc328cefd6edf316ac85f765a1b331.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=5fc328cefd6edf316ac85f765a1b331.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=5fc328cefd6edf316ac85f765a1b331.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=5fc328cefd6edf316ac85f765a1b331.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=5fc328cefd6edf316ac85f765a1b331.02.verified.cs.txt new file mode 100644 index 000000000..59c680cb7 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=5fc328cefd6edf316ac85f765a1b331.02.verified.cs.txt @@ -0,0 +1,43 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> GetRocket(GetRocket.Request request) + { + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new ObjectResult(result) + {StatusCode = 200}; + } + + [ProducesDefaultResponseType] + [ProducesResponseType(201)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> CreateRocket([BindRequired][FromBody] CreateRocket.Request request) + { + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new CreatedAtActionResult("GetRocket", null, new + { + id = result.Id + } + + , result); + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=6624240b93e22953e40a68a29f41d47.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=6624240b93e22953e40a68a29f41d47.00.verified.txt new file mode 100644 index 000000000..54fe4edbc --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=6624240b93e22953e40a68a29f41d47.00.verified.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0003, + Title: The parameter must map to a property of the request object, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (16,128)-(16,136), + Description: , + HelpLink: , + MessageFormat: The parameter {0} map to a property of the request {1} object, + Message: The parameter launchId map to a property of the request Request object, + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [ + { + Id: LPAD0003, + Title: The parameter must map to a property of the request object, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (16,128)-(16,136), + Description: , + HelpLink: , + MessageFormat: The parameter {0} map to a property of the request {1} object, + Message: The parameter launchId map to a property of the request Request object, + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=6624240b93e22953e40a68a29f41d47.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=6624240b93e22953e40a68a29f41d47.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=6624240b93e22953e40a68a29f41d47.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=6624240b93e22953e40a68a29f41d47.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=6624240b93e22953e40a68a29f41d47.02.verified.cs.txt new file mode 100644 index 000000000..df4c7afc7 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=6624240b93e22953e40a68a29f41d47.02.verified.cs.txt @@ -0,0 +1,16 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=7A81114CEA6429DB6C8C52BBACE8DF.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=7A81114CEA6429DB6C8C52BBACE8DF.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=7A81114CEA6429DB6C8C52BBACE8DF.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=7A81114CEA6429DB6C8C52BBACE8DF.01.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=7A81114CEA6429DB6C8C52BBACE8DF.01.verified.cs new file mode 100644 index 000000000..0e05543a3 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=7A81114CEA6429DB6C8C52BBACE8DF.01.verified.cs @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=7A81114CEA6429DB6C8C52BBACE8DF.02.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=7A81114CEA6429DB6C8C52BBACE8DF.02.verified.cs new file mode 100644 index 000000000..b3456c01f --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=7A81114CEA6429DB6C8C52BBACE8DF.02.verified.cs @@ -0,0 +1,42 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> GetRocket(GetRocket.Request request) + { + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new ObjectResult(result) + {StatusCode = 200}; + } + + [ProducesDefaultResponseType] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> CreateRocket([BindRequired][FromBody] CreateRocket.Request request) + { + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new AcceptedAtActionResult("GetRocket", null, new + { + id = result.Id + } + + , result); + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=8159AC6D85C24FFF319627DB792DA1C0.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=8159AC6D85C24FFF319627DB792DA1C0.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=8159AC6D85C24FFF319627DB792DA1C0.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=8159AC6D85C24FFF319627DB792DA1C0.01.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=8159AC6D85C24FFF319627DB792DA1C0.01.verified.cs new file mode 100644 index 000000000..0e05543a3 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=8159AC6D85C24FFF319627DB792DA1C0.01.verified.cs @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=8159AC6D85C24FFF319627DB792DA1C0.02.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=8159AC6D85C24FFF319627DB792DA1C0.02.verified.cs new file mode 100644 index 000000000..03edbf677 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=8159AC6D85C24FFF319627DB792DA1C0.02.verified.cs @@ -0,0 +1,27 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> GetRocket(GetRocket.Request request) + { + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new ObjectResult(result) + {StatusCode = 200}; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=84a5373b85d94e1da5a674671612822.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=84a5373b85d94e1da5a674671612822.00.verified.txt new file mode 100644 index 000000000..e31a40ce5 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=84a5373b85d94e1da5a674671612822.00.verified.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0003, + Title: The parameter must map to a property of the request object, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (16,128)-(16,142), + Description: , + HelpLink: , + MessageFormat: The parameter {0} map to a property of the request {1} object, + Message: The parameter launchRecordId map to a property of the request Request object, + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [ + { + Id: LPAD0003, + Title: The parameter must map to a property of the request object, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (16,128)-(16,142), + Description: , + HelpLink: , + MessageFormat: The parameter {0} map to a property of the request {1} object, + Message: The parameter launchRecordId map to a property of the request Request object, + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=84a5373b85d94e1da5a674671612822.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=84a5373b85d94e1da5a674671612822.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=84a5373b85d94e1da5a674671612822.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=84a5373b85d94e1da5a674671612822.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=84a5373b85d94e1da5a674671612822.02.verified.cs.txt new file mode 100644 index 000000000..df4c7afc7 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=84a5373b85d94e1da5a674671612822.02.verified.cs.txt @@ -0,0 +1,16 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=8C7B75D5949DBD6C73C2921123F93B4D.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=8C7B75D5949DBD6C73C2921123F93B4D.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=8C7B75D5949DBD6C73C2921123F93B4D.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=8C7B75D5949DBD6C73C2921123F93B4D.01.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=8C7B75D5949DBD6C73C2921123F93B4D.01.verified.cs new file mode 100644 index 000000000..0e05543a3 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=8C7B75D5949DBD6C73C2921123F93B4D.01.verified.cs @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=8C7B75D5949DBD6C73C2921123F93B4D.02.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=8C7B75D5949DBD6C73C2921123F93B4D.02.verified.cs new file mode 100644 index 000000000..003313bc2 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=8C7B75D5949DBD6C73C2921123F93B4D.02.verified.cs @@ -0,0 +1,37 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(204)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task GetRocketLaunchRecords(Guid id, [Bind(), CustomizeValidator(Properties = "")][BindRequired][FromRoute] GetRocketLaunchRecords.Request request) + { + var result = await Mediator.Send(request with {Id = id}, HttpContext.RequestAborted).ConfigureAwait(false); + return new StatusCodeResult(204); + } + + [ProducesDefaultResponseType] + [ProducesResponseType(204)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task GetRocketLaunchRecord(Guid id, Guid launchRecordId, [Bind(), CustomizeValidator(Properties = "")][BindRequired][FromRoute] GetRocketLaunchRecord.Request request) + { + var result = await Mediator.Send(request with {Id = id, LaunchRecordId = launchRecordId}, HttpContext.RequestAborted).ConfigureAwait(false); + return new StatusCodeResult(204); + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=96f03fa6bb8a139c98313316fa7047.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=96f03fa6bb8a139c98313316fa7047.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=96f03fa6bb8a139c98313316fa7047.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=96f03fa6bb8a139c98313316fa7047.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=96f03fa6bb8a139c98313316fa7047.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=96f03fa6bb8a139c98313316fa7047.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=96f03fa6bb8a139c98313316fa7047.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=96f03fa6bb8a139c98313316fa7047.02.verified.cs.txt new file mode 100644 index 000000000..23d199164 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=96f03fa6bb8a139c98313316fa7047.02.verified.cs.txt @@ -0,0 +1,37 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(204)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task GetRocketLaunchRecords(Guid id, [Bind(), CustomizeValidator(Properties = "")][BindRequired][FromRoute] GetRocketLaunchRecords.Request request) + { + var result = await Mediator.Send(request with {Id = id}, HttpContext.RequestAborted).ConfigureAwait(false); + return new StatusCodeResult(204); + } + + [ProducesDefaultResponseType] + [ProducesResponseType(204)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task GetRocketLaunchRecord(Guid id, Guid launchRecordId, [Bind(), CustomizeValidator(Properties = "")][BindRequired][FromRoute] GetRocketLaunchRecord.Request request) + { + var result = await Mediator.Send(request with {Id = id, LaunchRecordId = launchRecordId}, HttpContext.RequestAborted).ConfigureAwait(false); + return new StatusCodeResult(204); + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=AE59E43B973BE845D66A697A2A194EBE.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=AE59E43B973BE845D66A697A2A194EBE.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=AE59E43B973BE845D66A697A2A194EBE.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=AE59E43B973BE845D66A697A2A194EBE.01.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=AE59E43B973BE845D66A697A2A194EBE.01.verified.cs new file mode 100644 index 000000000..0e05543a3 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=AE59E43B973BE845D66A697A2A194EBE.01.verified.cs @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=AE59E43B973BE845D66A697A2A194EBE.02.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=AE59E43B973BE845D66A697A2A194EBE.02.verified.cs new file mode 100644 index 000000000..03edbf677 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=AE59E43B973BE845D66A697A2A194EBE.02.verified.cs @@ -0,0 +1,27 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> GetRocket(GetRocket.Request request) + { + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new ObjectResult(result) + {StatusCode = 200}; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=BC52CEC122CE3A9ACDB0C946B1C2E41.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=BC52CEC122CE3A9ACDB0C946B1C2E41.00.verified.txt new file mode 100644 index 000000000..78420f679 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=BC52CEC122CE3A9ACDB0C946B1C2E41.00.verified.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0003, + Title: The parameter type and property type must match, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (16,130)-(16,144), + Description: , + HelpLink: , + MessageFormat: The parameter {0} type {1} must match the property {2} type {3}, + Message: The parameter launchRecordId type string must match the property Request type Guid, + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [ + { + Id: LPAD0003, + Title: The parameter type and property type must match, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (16,130)-(16,144), + Description: , + HelpLink: , + MessageFormat: The parameter {0} type {1} must match the property {2} type {3}, + Message: The parameter launchRecordId type string must match the property Request type Guid, + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=BC52CEC122CE3A9ACDB0C946B1C2E41.01.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=BC52CEC122CE3A9ACDB0C946B1C2E41.01.verified.cs new file mode 100644 index 000000000..0e05543a3 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=BC52CEC122CE3A9ACDB0C946B1C2E41.01.verified.cs @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=BC52CEC122CE3A9ACDB0C946B1C2E41.02.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=BC52CEC122CE3A9ACDB0C946B1C2E41.02.verified.cs new file mode 100644 index 000000000..d25bb07ba --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=BC52CEC122CE3A9ACDB0C946B1C2E41.02.verified.cs @@ -0,0 +1,16 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=C6293BCBFC678DDFC6951B82AC11E6D6.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=C6293BCBFC678DDFC6951B82AC11E6D6.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=C6293BCBFC678DDFC6951B82AC11E6D6.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=C6293BCBFC678DDFC6951B82AC11E6D6.01.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=C6293BCBFC678DDFC6951B82AC11E6D6.01.verified.cs new file mode 100644 index 000000000..0e05543a3 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=C6293BCBFC678DDFC6951B82AC11E6D6.01.verified.cs @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=C6293BCBFC678DDFC6951B82AC11E6D6.02.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=C6293BCBFC678DDFC6951B82AC11E6D6.02.verified.cs new file mode 100644 index 000000000..80e2112c2 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=C6293BCBFC678DDFC6951B82AC11E6D6.02.verified.cs @@ -0,0 +1,43 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> GetRocket(GetRocket.Request request) + { + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new ObjectResult(result) + {StatusCode = 200}; + } + + [ProducesDefaultResponseType] + [ProducesResponseType(201)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> CreateRocket([BindRequired][FromBody] CreateRocket.Request request) + { + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new CreatedAtActionResult("GetRocket", null, new + { + id = result.Id + } + + , result); + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=C8781FE45921DAD16658C5C624B59B73.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=C8781FE45921DAD16658C5C624B59B73.00.verified.txt new file mode 100644 index 000000000..54fe4edbc --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=C8781FE45921DAD16658C5C624B59B73.00.verified.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0003, + Title: The parameter must map to a property of the request object, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (16,128)-(16,136), + Description: , + HelpLink: , + MessageFormat: The parameter {0} map to a property of the request {1} object, + Message: The parameter launchId map to a property of the request Request object, + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [ + { + Id: LPAD0003, + Title: The parameter must map to a property of the request object, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (16,128)-(16,136), + Description: , + HelpLink: , + MessageFormat: The parameter {0} map to a property of the request {1} object, + Message: The parameter launchId map to a property of the request Request object, + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=C8781FE45921DAD16658C5C624B59B73.01.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=C8781FE45921DAD16658C5C624B59B73.01.verified.cs new file mode 100644 index 000000000..0e05543a3 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=C8781FE45921DAD16658C5C624B59B73.01.verified.cs @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=C8781FE45921DAD16658C5C624B59B73.02.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=C8781FE45921DAD16658C5C624B59B73.02.verified.cs new file mode 100644 index 000000000..d25bb07ba --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=C8781FE45921DAD16658C5C624B59B73.02.verified.cs @@ -0,0 +1,16 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=E7F68867E1D71B73C1A1523FBA44C0A6.00.received.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=E7F68867E1D71B73C1A1523FBA44C0A6.00.received.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=E7F68867E1D71B73C1A1523FBA44C0A6.00.received.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=E7F68867E1D71B73C1A1523FBA44C0A6.01.received.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=E7F68867E1D71B73C1A1523FBA44C0A6.01.received.cs new file mode 100644 index 000000000..0e05543a3 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=E7F68867E1D71B73C1A1523FBA44C0A6.01.received.cs @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=E7F68867E1D71B73C1A1523FBA44C0A6.02.received.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=E7F68867E1D71B73C1A1523FBA44C0A6.02.received.cs new file mode 100644 index 000000000..812e11568 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=E7F68867E1D71B73C1A1523FBA44C0A6.02.received.cs @@ -0,0 +1,39 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> GetRocket(GetRocket.Request request) + { + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new ObjectResult(result) + {StatusCode = 200}; + } + + [ProducesDefaultResponseType] + [ProducesResponseType(201)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> CreateRocket([BindRequired][FromBody] CreateRocket.Request request) + { + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new ObjectResult(result) + {StatusCode = 201}; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=FCDB11746679F9DD3943DBA6E468252.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=FCDB11746679F9DD3943DBA6E468252.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=FCDB11746679F9DD3943DBA6E468252.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=FCDB11746679F9DD3943DBA6E468252.01.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=FCDB11746679F9DD3943DBA6E468252.01.verified.cs new file mode 100644 index 000000000..0e05543a3 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=FCDB11746679F9DD3943DBA6E468252.01.verified.cs @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=FCDB11746679F9DD3943DBA6E468252.02.verified.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=FCDB11746679F9DD3943DBA6E468252.02.verified.cs new file mode 100644 index 000000000..d591f87e3 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=FCDB11746679F9DD3943DBA6E468252.02.verified.cs @@ -0,0 +1,25 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial IAsyncEnumerable ListRockets([FromQuery] ListRockets.Request model) + { + var result = Mediator.CreateStream(model, HttpContext.RequestAborted); + return result; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=b9dcb99a10a7899eba82901c52da299c.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=b9dcb99a10a7899eba82901c52da299c.00.verified.txt new file mode 100644 index 000000000..80b11b3db --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=b9dcb99a10a7899eba82901c52da299c.00.verified.txt @@ -0,0 +1,34 @@ +{ + ResultDiagnostics: [ + { + Id: LPAD0004, + Title: The parameter type and property type must match, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (16,130)-(16,144), + Description: , + HelpLink: , + MessageFormat: The parameter {0} type {1} must match the property {2} type {3}, + Message: The parameter launchRecordId type string must match the property Request type Guid, + Category: LaunchPad, + CustomTags: [] + } + ], + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [ + { + Id: LPAD0004, + Title: The parameter type and property type must match, + Severity: Error, + WarningLevel: 0, + Location: Test3.cs: (16,130)-(16,144), + Description: , + HelpLink: , + MessageFormat: The parameter {0} type {1} must match the property {2} type {3}, + Message: The parameter launchRecordId type string must match the property Request type Guid, + Category: LaunchPad, + CustomTags: [] + } + ] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=b9dcb99a10a7899eba82901c52da299c.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=b9dcb99a10a7899eba82901c52da299c.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=b9dcb99a10a7899eba82901c52da299c.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=b9dcb99a10a7899eba82901c52da299c.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=b9dcb99a10a7899eba82901c52da299c.02.verified.cs.txt new file mode 100644 index 000000000..df4c7afc7 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=b9dcb99a10a7899eba82901c52da299c.02.verified.cs.txt @@ -0,0 +1,16 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=c4867cab2a3cb9a5ce2e63e29f3eeee.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=c4867cab2a3cb9a5ce2e63e29f3eeee.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=c4867cab2a3cb9a5ce2e63e29f3eeee.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=c4867cab2a3cb9a5ce2e63e29f3eeee.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=c4867cab2a3cb9a5ce2e63e29f3eeee.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=c4867cab2a3cb9a5ce2e63e29f3eeee.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=c4867cab2a3cb9a5ce2e63e29f3eeee.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=c4867cab2a3cb9a5ce2e63e29f3eeee.02.verified.cs.txt new file mode 100644 index 000000000..99c4946e6 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=c4867cab2a3cb9a5ce2e63e29f3eeee.02.verified.cs.txt @@ -0,0 +1,26 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(204)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task SaveRocket(Guid id, [Bind(), CustomizeValidator(Properties = "")] SaveRocket.Request request) + { + await Mediator.Send(request with {Id = id}, HttpContext.RequestAborted).ConfigureAwait(false); + return new StatusCodeResult(204); + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=d0c2a5fcbbe6271310e88997ca8671ae.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=d0c2a5fcbbe6271310e88997ca8671ae.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=d0c2a5fcbbe6271310e88997ca8671ae.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=d0c2a5fcbbe6271310e88997ca8671ae.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=d0c2a5fcbbe6271310e88997ca8671ae.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=d0c2a5fcbbe6271310e88997ca8671ae.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=d0c2a5fcbbe6271310e88997ca8671ae.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=d0c2a5fcbbe6271310e88997ca8671ae.02.verified.cs.txt new file mode 100644 index 000000000..1f48c0e7b --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=d0c2a5fcbbe6271310e88997ca8671ae.02.verified.cs.txt @@ -0,0 +1,27 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> GetRocket(GetRocket.Request request) + { + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new ObjectResult(result) + {StatusCode = 200}; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=e173dbcfe6771f02ad65131da4a7bb.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=e173dbcfe6771f02ad65131da4a7bb.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=e173dbcfe6771f02ad65131da4a7bb.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=e173dbcfe6771f02ad65131da4a7bb.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=e173dbcfe6771f02ad65131da4a7bb.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=e173dbcfe6771f02ad65131da4a7bb.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=e173dbcfe6771f02ad65131da4a7bb.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=e173dbcfe6771f02ad65131da4a7bb.02.verified.cs.txt new file mode 100644 index 000000000..df6417af3 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=e173dbcfe6771f02ad65131da4a7bb.02.verified.cs.txt @@ -0,0 +1,25 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial IAsyncEnumerable ListRockets([FromQuery] ListRockets.Request model) + { + var result = Mediator.CreateStream(model, HttpContext.RequestAborted); + return result; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=e7f8f61a43f3dd2108b1ffba63752e.00.verified.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=e7f8f61a43f3dd2108b1ffba63752e.00.verified.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=e7f8f61a43f3dd2108b1ffba63752e.00.verified.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=e7f8f61a43f3dd2108b1ffba63752e.01.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=e7f8f61a43f3dd2108b1ffba63752e.01.verified.cs.txt new file mode 100644 index 000000000..6552cc842 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=e7f8f61a43f3dd2108b1ffba63752e.01.verified.cs.txt @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatedAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=e7f8f61a43f3dd2108b1ffba63752e.02.verified.cs.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=e7f8f61a43f3dd2108b1ffba63752e.02.verified.cs.txt new file mode 100644 index 000000000..bff1937a5 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Bodies_sources=e7f8f61a43f3dd2108b1ffba63752e.02.verified.cs.txt @@ -0,0 +1,28 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers/Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator/RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> Save2Rocket(Guid id, [Bind("Sn"), CustomizeValidator(Properties = "Sn")] Save2Rocket.Request request) + { + request.Id = id; + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new ObjectResult(result) + {StatusCode = 200}; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Body.00.received.txt b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Body.00.received.txt new file mode 100644 index 000000000..337c130bf --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Body.00.received.txt @@ -0,0 +1,5 @@ +{ + Results: { + Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator: [] + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Body.01.received.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Body.01.received.cs new file mode 100644 index 000000000..aec148e41 --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Body.01.received.cs @@ -0,0 +1,17 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\Attributes.cs + +using System; + +namespace Rocket.Surgery.LaunchPad.AspNetCore +{ + [AttributeUsage(AttributeTargets.Method)] + class CreatedAttribute : Attribute + { + public CreatesAttribute(string methodName){} + } + [AttributeUsage(AttributeTargets.Method)] + class AcceptedAttribute : Attribute + { + public AcceptedAttribute(string methodName){} + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Body.02.received.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Body.02.received.cs new file mode 100644 index 000000000..c9062968d --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.Should_Generate_Method_Body.02.received.cs @@ -0,0 +1,61 @@ +//HintName: Rocket.Surgery.LaunchPad.Analyzers\Rocket.Surgery.LaunchPad.Analyzers.ControllerActionBodyGenerator\RocketController_Methods.cs +#nullable enable +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; +using FluentValidation.AspNetCore; +using Rocket.Surgery.LaunchPad.AspNetCore.Validation; + +namespace MyNamespace.Controllers +{ + public partial class RocketController + { + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial IAsyncEnumerable ListRockets([FromQuery] ListRockets.Request model) + { + var result = Mediator.CreateStream(model, HttpContext.RequestAborted); + return result; + } + + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> GetRocket(GetRocket.Request request) + { + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new ObjectResult(result) + {StatusCode = 200}; + } + + [ProducesDefaultResponseType] + [ProducesResponseType(204)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task SaveRocket(Guid id, [Bind(), CustomizeValidator(Properties = "")] SaveRocket.Request request) + { + await Mediator.Send(request with {Id = id}, HttpContext.RequestAborted).ConfigureAwait(false); + return new StatusCodeResult(204); + } + + [ProducesDefaultResponseType] + [ProducesResponseType(200)] + [ProducesResponseType(typeof(ProblemDetails), 404)] + [ProducesResponseType(typeof(ProblemDetails), 400)] + [ProducesResponseType(typeof(FluentValidationProblemDetails), 422)] + public partial async Task> Save2Rocket(Guid id, [Bind("Sn"), CustomizeValidator(Properties = "Sn")] Save2Rocket.Request request) + { + request.Id = id; + var result = await Mediator.Send(request, HttpContext.RequestAborted).ConfigureAwait(false); + return new ObjectResult(result) + {StatusCode = 200}; + } + } +} +#nullable restore diff --git a/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.cs b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.cs new file mode 100644 index 000000000..ec91e94ae --- /dev/null +++ b/test/Analyzers.Tests/ControllerActionBodyGeneratorTests.cs @@ -0,0 +1,513 @@ +using System.Security.Cryptography; +using System.Text; +using Analyzers.Tests.Helpers; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Rocket.Surgery.LaunchPad.Analyzers; +using Rocket.Surgery.LaunchPad.AspNetCore; +using Xunit.Abstractions; + +namespace Analyzers.Tests; + +[UsesVerify] +public class ControllerActionBodyGeneratorTests : GeneratorTest +{ + [Fact] + public async Task Should_Error_If_Controller_Is_Not_Partial() + { + var source2 = @" +namespace TestNamespace; + +public record RocketModel +{ + public Guid Id { get; init; } + public string Sn { get; init; } = null!; +} + +public static class GetRocket +{ + public record Request : IRequest + { + public Guid Id { get; set; } + } +} + +public static class ListRockets +{ + // TODO: Paging model! + public record Request : IRequest>; +} + +"; + var source1 = @"using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; + +namespace MyNamespace.Controllers; + +[Route(""[controller]"")] +public class RocketController : RestfulApiController +{ + [HttpGet] + public partial Task>> ListRockets([FromQuery] ListRockets.Request request); + + [HttpGet(""{id:guid}"")] + public partial Task> GetRocket([BindRequired] [FromRoute] GetRocket.Request request); +} +"; + await Verify(await GenerateAsync(source1, source2)); + } + + public ControllerActionBodyGeneratorTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper, LogLevel.Trace) + { + WithGenerator(); + AddReferences( + typeof(Guid), + typeof(IRequest), + typeof(IMediator), + typeof(Task<>), + typeof(IEnumerable<>), + typeof(ControllerBase), + typeof(Controller), + typeof(RouteAttribute), + typeof(RestfulApiController) + ); + AddSources( + @" +global using MediatR; +global using System; +global using System.Collections.Generic; +global using System.Threading.Tasks; +" + ); + } + + [Theory] + [ClassData(typeof(MethodBodyData))] + public async Task Should_Generate_Method_Bodies(string key, string[] sources) + { + await Verify(await GenerateAsync(sources)).UseParameters(key, ""); + } + + private class MethodBodyData : TheoryData + { + private const string defaultString = @" +namespace TestNamespace; +public record RocketModel +{ + public Guid Id { get; init; } + public string Sn { get; init; } = null!; +} +"; + + public MethodBodyData() + { + Add( + "GenerateBodyForRequest", + new[] + { + defaultString, + @" +namespace TestNamespace; +public static class GetRocket +{ + public record Request : IRequest + { + public Guid Id { get; set; } + } +}", + @"using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; + +namespace MyNamespace.Controllers; + +[Route(""[controller]"")] +public partial class RocketController : RestfulApiController +{ + [HttpGet(""{id:guid}"")] + public partial Task> GetRocket([BindRequired] [FromRoute] GetRocket.Request request); +}" + } + ); + Add( + "GenerateBodyWithIdParameterAndAddBindRequired", + new[] + { + defaultString, + @" +namespace TestNamespace; +public static class SaveRocket +{ + public record Request : IRequest + { + public Guid Id { get; set; } + } +}", + @"using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; + +namespace MyNamespace.Controllers; + +[Route(""[controller]"")] +public partial class RocketController : RestfulApiController +{ + [HttpPost(""{id:guid}"")] + public partial Task SaveRocket([BindRequired][FromRoute] Guid id, [FromRoute] SaveRocket.Request request); +}" + } + ); + Add( + "GenerateBodyWithIdParameter", + new[] + { + defaultString, + @" +namespace TestNamespace; +public static class Save2Rocket +{ + public class Request : IRequest + { + public Guid Id { get; set; } + public string Sn { get; init; } = null!; + } +}", + @"using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; + +namespace MyNamespace.Controllers; + +[Route(""[controller]"")] +public partial class RocketController : RestfulApiController +{ + [HttpPost(""{id:guid}"")] + public partial Task> Save2Rocket([BindRequired][FromRoute] Guid id, [BindRequired] [FromRoute] Save2Rocket.Request request); +}" + } + ); + Add( + "GenerateBodyForListAction", + new[] + { + defaultString, + @" +namespace TestNamespace; +public static class ListRockets +{ + // TODO: Paging model! + public record Request : IStreamRequest>; +}", + @"using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; + +namespace MyNamespace.Controllers; + +[Route(""[controller]"")] +public partial class RocketController : RestfulApiController +{ + [HttpGet] + public partial IAsyncEnumerable ListRockets(ListRockets.Request model); +}" + } + ); + + Add( + "GenerateBodiesWithCreatedReturn", + new[] + { + defaultString, + @" +namespace TestNamespace; +public static class GetRocket +{ + public record Request : IRequest + { + public Guid Id { get; set; } + } +} + +public static class CreateRocket +{ + public record Request : IRequest + { + public string SerialNumber { get; set; } = null!; + public RocketType Type { get; set; } + } + + public record Response + { + public Guid Id { get; init; } + } +}", + @" +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; + +namespace MyNamespace.Controllers; + +[Route(""[controller]"")] +public partial class RocketController : RestfulApiController +{ + [HttpGet(""{id:guid}"")] + public partial Task> GetRocket([BindRequired] [FromRoute] GetRocket.Request request); + + [HttpPost] + [Created(nameof(GetRocket))] + public partial Task> CreateRocket(CreateRocket.Request request); +}" + } + ); + Add( + "GenerateBodiesWithAcceptReturnType", + new[] + { + defaultString, + @" +namespace TestNamespace; +public static class GetRocket +{ + public record Request : IRequest + { + public Guid Id { get; set; } + } +} + +public static class CreateRocket +{ + public record Request : IRequest + { + public string SerialNumber { get; set; } = null!; + public RocketType Type { get; set; } + } + + public record Response + { + public Guid Id { get; init; } + } +}", + @" +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; + +namespace MyNamespace.Controllers; + +[Route(""[controller]"")] +public partial class RocketController : RestfulApiController +{ + [HttpGet(""{id:guid}"")] + public partial Task> GetRocket([BindRequired] [FromRoute] GetRocket.Request request); + + [HttpPost] + [Accepted(nameof(GetRocket))] + [ProducesResponseType(202)] + public partial Task> CreateRocket(CreateRocket.Request request); +}" + } + ); + Add( + "GenerateBodiesWithMultipleParameters", + new[] + { + defaultString, + @" +namespace TestNamespace; + +public record LaunchRecordModel +{ + public Guid Id { get; init; } + public string Partner { get; init; } = null!; + public string Payload { get; init; } = null!; +} + +public static class GetRocketLaunchRecords +{ + public record Request : IStreamRequest + { + public Guid Id { get; init; } + } +} + +public static class GetRocketLaunchRecord +{ + public record Request : IRequest + { + public Guid Id { get; init; } + + public Guid LaunchRecordId { get; init; } + } +}", + @" +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; + +namespace MyNamespace.Controllers; + +[Route(""[controller]"")] +public partial class RocketController : RestfulApiController +{ + /// + /// Get the launch records for a given rocket + /// + /// + [HttpGet(""{id:guid}/launch-records"")] + public partial Task GetRocketLaunchRecords([BindRequired] [FromRoute] Guid id, GetRocketLaunchRecords.Request request); + + /// + /// Get a specific launch record for a given rocket + /// + /// + [HttpGet(""{id:guid}/launch-records/{launchRecordId:guid}"")] + public partial Task GetRocketLaunchRecord([BindRequired] [FromRoute] Guid id, [BindRequired] [FromRoute] Guid launchRecordId, GetRocketLaunchRecord.Request request); +}" + } + ); + Add( + "GenerateBodiesWithMultipleParameters2", + new[] + { + defaultString, + @" +namespace TestNamespace; + +public record LaunchRecordModel +{ + public Guid Id { get; init; } + public string Partner { get; init; } = null!; + public string Payload { get; init; } = null!; +} + +public static class GetRocketLaunchRecord +{ + public record Request : IRequest + { + public Guid Id { get; init; } + + public Guid LaunchId { get; init; } + } +}", + @" +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; + +namespace MyNamespace.Controllers; + +[Route(""[controller]"")] +public partial class RocketController : RestfulApiController +{ + /// + /// Get a specific launch record for a given rocket + /// + /// + [HttpGet(""{id:guid}/launch-records/{launchRecordId:guid}"")] + public partial Task GetRocketLaunchRecord([BindRequired] [FromRoute] Guid id, [BindRequired] [FromRoute] Guid launchRecordId, GetRocketLaunchRecord.Request request); +}" + } + ); + Add( + "GenerateBodiesWithMultipleParameters3", + new[] + { + defaultString, + @" +namespace TestNamespace; + +public record LaunchRecordModel +{ + public Guid Id { get; init; } + public string Partner { get; init; } = null!; + public string Payload { get; init; } = null!; +} + +public static class GetRocketLaunchRecord +{ + public record Request : IRequest + { + public Guid Id { get; init; } + + public Guid LaunchRecordId { get; init; } + } +}", + @" +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; + +namespace MyNamespace.Controllers; + +[Route(""[controller]"")] +public partial class RocketController : RestfulApiController +{ + /// + /// Get a specific launch record for a given rocket + /// + /// + [HttpGet(""{id:guid}/launch-records/{launchRecordId:guid}"")] + public partial Task GetRocketLaunchRecord([BindRequired] [FromRoute] Guid id, [BindRequired] [FromRoute] Guid launchId, GetRocketLaunchRecord.Request request); +}" + } + ); + Add( + "GenerateBodiesWithMultipleParameters4", + new[] + { + defaultString, + @" +namespace TestNamespace; + +public record LaunchRecordModel +{ + public Guid Id { get; init; } + public string Partner { get; init; } = null!; + public string Payload { get; init; } = null!; +} + +public static class GetRocketLaunchRecord +{ + public record Request : IRequest + { + public Guid Id { get; init; } + + public Guid LaunchRecordId { get; init; } + } +}", + @" +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Rocket.Surgery.LaunchPad.AspNetCore; +using TestNamespace; + +namespace MyNamespace.Controllers; + +[Route(""[controller]"")] +public partial class RocketController : RestfulApiController +{ + /// + /// Get a specific launch record for a given rocket + /// + /// + [HttpGet(""{id:guid}/launch-records/{launchRecordId:guid}"")] + public partial Task GetRocketLaunchRecord([BindRequired] [FromRoute] Guid id, [BindRequired] [FromRoute] string launchRecordId, GetRocketLaunchRecord.Request request); +}" + } + ); + } + } +} diff --git a/test/Analyzers.Tests/Helpers/GenerationTestResults.cs b/test/Analyzers.Tests/Helpers/GenerationTestResults.cs index 8b049aec1..870f47819 100644 --- a/test/Analyzers.Tests/Helpers/GenerationTestResults.cs +++ b/test/Analyzers.Tests/Helpers/GenerationTestResults.cs @@ -1,7 +1,6 @@ using System.Collections.Immutable; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Xunit; namespace Analyzers.Tests.Helpers; @@ -9,7 +8,8 @@ public record GenerationTestResults( CSharpCompilation InputCompilation, ImmutableArray InputDiagnostics, ImmutableArray InputSyntaxTrees, - ImmutableDictionary Results + ImmutableDictionary Results, + ImmutableArray ResultDiagnostics ) { public bool TryGetResult(Type type, [NotNullWhen(true)] out GenerationTestResult? result) diff --git a/test/Analyzers.Tests/Helpers/GeneratorTest.cs b/test/Analyzers.Tests/Helpers/GeneratorTest.cs index 5ea8d3a79..6fe67e499 100644 --- a/test/Analyzers.Tests/Helpers/GeneratorTest.cs +++ b/test/Analyzers.Tests/Helpers/GeneratorTest.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.Logging; using Rocket.Surgery.Conventions; using Rocket.Surgery.Extensions.Testing; -using Xunit; using Xunit.Abstractions; namespace Analyzers.Tests.Helpers; @@ -15,6 +14,7 @@ public abstract class GeneratorTest : LoggerTest { private readonly HashSet _metadataReferences = new(ReferenceEqualityComparer.Instance); private readonly HashSet _generators = new(); + private readonly List _sources = new(); protected GeneratorTest(ITestOutputHelper testOutputHelper, LogLevel minLevel) : base(testOutputHelper, minLevel) { @@ -93,18 +93,23 @@ protected GeneratorTest AddReferences(params Assembly[] references) return this; } + protected GeneratorTest AddSources(params string[] sources) + { + _sources.AddRange(sources); + return this; + } public async Task GenerateAsync(params string[] sources) { Logger.LogInformation("Starting Generation for {SourceCount}", sources.Length); if (Logger.IsEnabled(LogLevel.Trace)) { - Logger.LogTrace("--- References {Count} ---", sources.Length); + Logger.LogTrace("--- References {Count} ---", _metadataReferences.Count); foreach (var reference in _metadataReferences) Logger.LogTrace(" Reference: {Name}", reference.Display); } - var project = GenerationHelpers.CreateProject(_metadataReferences, sources.ToArray()); + var project = GenerationHelpers.CreateProject(_metadataReferences, _sources.Concat(sources).ToArray()); var compilation = (CSharpCompilation?)await project.GetCompilationAsync().ConfigureAwait(false); if (compilation is null) @@ -115,7 +120,7 @@ public async Task GenerateAsync(params string[] sources) var diagnostics = compilation.GetDiagnostics(); if (Logger.IsEnabled(LogLevel.Trace) && diagnostics is { Length: > 0 }) { - Logger.LogTrace("--- Input Diagnostics {Count} ---", sources.Length); + Logger.LogTrace("--- Input Diagnostics {Count} ---", diagnostics.Length); foreach (var d in diagnostics) Logger.LogTrace(" Reference: {Name}", d.ToString()); } @@ -124,7 +129,8 @@ public async Task GenerateAsync(params string[] sources) compilation, diagnostics, compilation.SyntaxTrees, - ImmutableDictionary.Empty + ImmutableDictionary.Empty, + ImmutableArray.Empty ); var builder = ImmutableDictionary.Empty.ToBuilder(); @@ -138,6 +144,7 @@ public async Task GenerateAsync(params string[] sources) if (Logger.IsEnabled(LogLevel.Trace) && diagnostics is { Length: > 0 }) { + results = results with { ResultDiagnostics = results.ResultDiagnostics.AddRange(diagnostics) }; Logger.LogTrace("--- Diagnostics {Count} ---", sources.Length); foreach (var d in diagnostics) Logger.LogTrace(" Reference: {Name}", d.ToString()); diff --git a/test/Analyzers.Tests/ViewModelGeneratorTests.cs b/test/Analyzers.Tests/InheritFromGeneratorTests.cs similarity index 96% rename from test/Analyzers.Tests/ViewModelGeneratorTests.cs rename to test/Analyzers.Tests/InheritFromGeneratorTests.cs index 3c14345da..994bce9f9 100644 --- a/test/Analyzers.Tests/ViewModelGeneratorTests.cs +++ b/test/Analyzers.Tests/InheritFromGeneratorTests.cs @@ -1,23 +1,13 @@ -using Analyzers.Tests.Helpers; +using Analyzers.Tests.Helpers; using FluentAssertions; using MediatR; using Microsoft.Extensions.Logging; using Rocket.Surgery.LaunchPad.Analyzers; using Rocket.Surgery.LaunchPad.Foundation; -using Xunit; using Xunit.Abstractions; namespace Analyzers.Tests; -public class MutableGeneratorTests : GeneratorTest -{ - public MutableGeneratorTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper, LogLevel.Trace) - { - WithGenerator(); - AddReferences(typeof(MutableAttribute)); - } -} - public class InheritFromGeneratorTests : GeneratorTest { [Fact] diff --git a/test/Analyzers.Tests/ModuleInitializer.cs b/test/Analyzers.Tests/ModuleInitializer.cs new file mode 100644 index 000000000..4e891f880 --- /dev/null +++ b/test/Analyzers.Tests/ModuleInitializer.cs @@ -0,0 +1,42 @@ +using System.Runtime.CompilerServices; +using Analyzers.Tests.Helpers; +using DiffEngine; +using Microsoft.CodeAnalysis; + +public static class ModuleInitializer +{ + [ModuleInitializer] + public static void Init() + { + VerifySourceGenerators.Enable(); + VerifyNodaTime.Enable(); + + DiffRunner.Disabled = true; + VerifierSettings.RegisterFileConverter(Convert); + VerifierSettings.RegisterFileConverter(Convert); + } + + private static ConversionResult Convert(GenerationTestResults target, IReadOnlyDictionary context) + { + var targets = new List(); +// targets.AddRange(target.InputSyntaxTrees.Select(Selector)); + foreach (var item in target.Results) + { + targets.AddRange(item.Value.SyntaxTrees.Select(Selector)); + } + + return new(new { target.ResultDiagnostics, Results = target.Results.ToDictionary(z => z.Key.FullName!, z => z.Value.Diagnostics) }, targets); + } + + private static Target Selector(SyntaxTree source) + { + var data = $@"//HintName: {source.FilePath.Replace("\\", "/", StringComparison.OrdinalIgnoreCase)} +{source.GetText()}"; + return new("cs.txt", data.Replace("\r", string.Empty, StringComparison.OrdinalIgnoreCase)); + } + + private static ConversionResult Convert(GenerationTestResult target, IReadOnlyDictionary context) + { + return new(new { target.Diagnostics }, target.SyntaxTrees.Select(Selector)); + } +} diff --git a/test/Analyzers.Tests/MutableGeneratorTests.cs b/test/Analyzers.Tests/MutableGeneratorTests.cs new file mode 100644 index 000000000..4708f1ae7 --- /dev/null +++ b/test/Analyzers.Tests/MutableGeneratorTests.cs @@ -0,0 +1,16 @@ +using Analyzers.Tests.Helpers; +using Microsoft.Extensions.Logging; +using Rocket.Surgery.LaunchPad.Analyzers; +using Rocket.Surgery.LaunchPad.Foundation; +using Xunit.Abstractions; + +namespace Analyzers.Tests; + +public class MutableGeneratorTests : GeneratorTest +{ + public MutableGeneratorTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper, LogLevel.Trace) + { + WithGenerator(); + AddReferences(typeof(MutableAttribute)); + } +} diff --git a/test/Directory.Build.targets b/test/Directory.Build.targets index e64130918..c6d40add9 100644 --- a/test/Directory.Build.targets +++ b/test/Directory.Build.targets @@ -16,6 +16,10 @@ + + + + diff --git a/test/Extensions.Tests/NewtonsoftJsonNodaTimeTests.cs b/test/Extensions.Tests/NewtonsoftJsonNodaTimeTests.cs index 2e5e8a29f..df386b412 100644 --- a/test/Extensions.Tests/NewtonsoftJsonNodaTimeTests.cs +++ b/test/Extensions.Tests/NewtonsoftJsonNodaTimeTests.cs @@ -43,7 +43,6 @@ public void LocalDate_Tests(string value) [InlineData("2020-01-01T12:12:12")] [InlineData("2020-01-01T12:12:12.000000000")] [InlineData("2020-01-01T12:12:12.0000000")] - [InlineData("2020-01-01T12:12:12.000000000 (ISO)")] public void LocalDateTime_Tests(string value) { JsonConvert.DeserializeObject("\"" + value + "\"", _settings) diff --git a/test/Extensions.Tests/SystemTextJsonNodaTimeTests.cs b/test/Extensions.Tests/SystemTextJsonNodaTimeTests.cs index ab6f25c9d..24fb72901 100644 --- a/test/Extensions.Tests/SystemTextJsonNodaTimeTests.cs +++ b/test/Extensions.Tests/SystemTextJsonNodaTimeTests.cs @@ -42,7 +42,6 @@ public void LocalDate_Tests(string value) [InlineData("2020-01-01T12:12:12")] [InlineData("2020-01-01T12:12:12.000000000")] [InlineData("2020-01-01T12:12:12.0000000")] - [InlineData("2020-01-01T12:12:12.000000000 (ISO)")] public void LocalDateTime_Tests(string value) { JsonSerializer.Deserialize("\"" + value + "\"", _settings) diff --git a/test/Sample.Core.Tests/LaunchRecords/CreateLaunchRecordTests.cs b/test/Sample.Core.Tests/LaunchRecords/CreateLaunchRecordTests.cs index 9ea5d7e86..0c36ac441 100644 --- a/test/Sample.Core.Tests/LaunchRecords/CreateLaunchRecordTests.cs +++ b/test/Sample.Core.Tests/LaunchRecords/CreateLaunchRecordTests.cs @@ -1,5 +1,4 @@ using Bogus; -using FluentAssertions; using MediatR; using Microsoft.Extensions.Logging; using NodaTime; @@ -46,7 +45,7 @@ public async Task Should_Create_A_LaunchRecord() ) ); - response.Id.Should().NotBeEmpty(); + response.Id.Value.Should().NotBeEmpty(); } public CreateLaunchRecordTests(ITestOutputHelper outputHelper) : base(outputHelper, LogLevel.Trace) diff --git a/test/Sample.Core.Tests/LaunchRecords/GetLaunchRecordTests.cs b/test/Sample.Core.Tests/LaunchRecords/GetLaunchRecordTests.cs index 9d84ef60e..fff3deec8 100644 --- a/test/Sample.Core.Tests/LaunchRecords/GetLaunchRecordTests.cs +++ b/test/Sample.Core.Tests/LaunchRecords/GetLaunchRecordTests.cs @@ -5,6 +5,7 @@ using NodaTime; using Rocket.Surgery.DependencyInjection; using Sample.Core.Domain; +using Sample.Core.Models; using Sample.Core.Operations.LaunchRecords; using Xunit; using Xunit.Abstractions; @@ -22,7 +23,7 @@ public async Task Should_Get_A_LaunchRecord() { var rocket = new ReadyRocket { - Id = Guid.NewGuid(), + Id = RocketId.New(), Type = RocketType.Falcon9, SerialNumber = "12345678901234" }; diff --git a/test/Sample.Core.Tests/LaunchRecords/ListLaunchRecordsTests.cs b/test/Sample.Core.Tests/LaunchRecords/ListLaunchRecordsTests.cs index 525d28157..edcf348f4 100644 --- a/test/Sample.Core.Tests/LaunchRecords/ListLaunchRecordsTests.cs +++ b/test/Sample.Core.Tests/LaunchRecords/ListLaunchRecordsTests.cs @@ -29,7 +29,7 @@ await ServiceProvider.WithScoped() ); var response = await ServiceProvider.WithScoped().Invoke( - mediator => mediator.Send(new ListLaunchRecords.Request()) + mediator => mediator.CreateStream(new ListLaunchRecords.Request(null)).ToListAsync() ); response.Should().HaveCount(10); diff --git a/test/Sample.Core.Tests/LaunchRecords/UpdateLaunchRecordTests.cs b/test/Sample.Core.Tests/LaunchRecords/UpdateLaunchRecordTests.cs index 6abfbd7e6..b75248a2c 100644 --- a/test/Sample.Core.Tests/LaunchRecords/UpdateLaunchRecordTests.cs +++ b/test/Sample.Core.Tests/LaunchRecords/UpdateLaunchRecordTests.cs @@ -6,6 +6,7 @@ using NodaTime.Extensions; using Rocket.Surgery.DependencyInjection; using Sample.Core.Domain; +using Sample.Core.Models; using Sample.Core.Operations.LaunchRecords; using Xunit; using Xunit.Abstractions; @@ -23,7 +24,7 @@ public async Task Should_Update_A_LaunchRecord() { var rocket = new ReadyRocket { - Id = Guid.NewGuid(), + Id = RocketId.New(), Type = RocketType.Falcon9, SerialNumber = "12345678901234" }; diff --git a/test/Sample.Core.Tests/Rockets/CreateRocketTests.cs b/test/Sample.Core.Tests/Rockets/CreateRocketTests.cs index a83358297..7f3639106 100644 --- a/test/Sample.Core.Tests/Rockets/CreateRocketTests.cs +++ b/test/Sample.Core.Tests/Rockets/CreateRocketTests.cs @@ -28,7 +28,7 @@ public async Task Should_Create_A_Rocket() ) ); - response.Id.Should().NotBeEmpty(); + response.Id.Value.Should().NotBeEmpty(); } [Fact] diff --git a/test/Sample.Core.Tests/Rockets/ListRocketsTests.cs b/test/Sample.Core.Tests/Rockets/ListRocketsTests.cs index 2629ec57d..5c21a4c7e 100644 --- a/test/Sample.Core.Tests/Rockets/ListRocketsTests.cs +++ b/test/Sample.Core.Tests/Rockets/ListRocketsTests.cs @@ -27,9 +27,7 @@ await ServiceProvider.WithScoped() ); var response = await ServiceProvider.WithScoped().Invoke( - mediator => mediator.Send( - new ListRockets.Request() - ) + mediator => mediator.CreateStream(new ListRockets.Request(null)).ToListAsync() ); response.Should().HaveCount(10); diff --git a/test/Sample.Core.Tests/Rockets/UpdateRocketTests.cs b/test/Sample.Core.Tests/Rockets/UpdateRocketTests.cs index 11b134f93..5e3367fc6 100644 --- a/test/Sample.Core.Tests/Rockets/UpdateRocketTests.cs +++ b/test/Sample.Core.Tests/Rockets/UpdateRocketTests.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using Rocket.Surgery.DependencyInjection; using Sample.Core.Domain; +using Sample.Core.Models; using Sample.Core.Operations.Rockets; using Xunit; using Xunit.Abstractions; @@ -73,13 +74,13 @@ public ShouldValidateUsersRequiredFieldData() Add( new EditRocket.Request { - Id = Guid.NewGuid() + Id = RocketId.New() }, nameof(EditRocket.Request.SerialNumber) ); Add( new EditRocket.Request { - Id = Guid.NewGuid(), + Id = RocketId.New(), SerialNumber = Faker.Random.String2(0, 9) }, nameof(EditRocket.Request.SerialNumber) @@ -87,7 +88,7 @@ public ShouldValidateUsersRequiredFieldData() Add( new EditRocket.Request { - Id = Guid.NewGuid(), + Id = RocketId.New(), SerialNumber = Faker.Random.String2(600, 800) }, nameof(EditRocket.Request.SerialNumber) diff --git a/test/Sample.Graphql.Tests/.graphqlconfig b/test/Sample.Graphql.Tests/.graphqlconfig new file mode 100644 index 000000000..f3813d35e --- /dev/null +++ b/test/Sample.Graphql.Tests/.graphqlconfig @@ -0,0 +1,4 @@ +{ + "name": "Untitled GraphQL Schema", + "schemaPath": "schema.graphql" +} diff --git a/test/Sample.Graphql.Tests/.graphqlrc.json b/test/Sample.Graphql.Tests/.graphqlrc.json new file mode 100644 index 000000000..666545c9e --- /dev/null +++ b/test/Sample.Graphql.Tests/.graphqlrc.json @@ -0,0 +1,29 @@ +{ + "schema": "schema.graphql", + "documents": "**/*.graphql", + "extensions": { + "strawberryShake": { + "name": "RocketClient", + "url": "https://localhost:5001/graphql/", + "dependencyInjection": true, + "strictSchemaValidation": true, + "hashAlgorithm": "md5", + "useSingleFile": true, + "requestStrategy": "Default", + "outputDirectoryName": "Generated", + "noStore": false, + "emitGeneratedCode": false, + "razorComponents": false, + "records": { + "inputs": true, + "entities": true + }, + "transportProfiles": [ + { + "default": "Http", + "subscription": "WebSocket" + } + ] + } + } +} diff --git a/test/Sample.Graphql.Tests/HandleWebHostBase.cs b/test/Sample.Graphql.Tests/HandleWebHostBase.cs index d9297e212..e4b61b962 100644 --- a/test/Sample.Graphql.Tests/HandleWebHostBase.cs +++ b/test/Sample.Graphql.Tests/HandleWebHostBase.cs @@ -1,6 +1,8 @@ -using Microsoft.Data.Sqlite; +using Microsoft.AspNetCore.Mvc.Testing.Handlers; +using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http; using Microsoft.Extensions.Logging; using Rocket.Surgery.Conventions; using Rocket.Surgery.DependencyInjection; @@ -13,11 +15,11 @@ namespace Sample.Graphql.Tests; [ImportConventions] -public abstract partial class HandleGrpcHostBase : LoggerTest, IAsyncLifetime +public abstract partial class HandleWebHostBase : LoggerTest, IAsyncLifetime { private SqliteConnection _connection = null!; - protected HandleGrpcHostBase( + protected HandleWebHostBase( ITestOutputHelper outputHelper, LogLevel logLevel = LogLevel.Trace ) : base( @@ -29,7 +31,43 @@ protected HandleGrpcHostBase( Factory = new TestWebHost() .ConfigureHostBuilder( b => b - .ConfigureHosting((context, z) => z.ConfigureServices((_, s) => s.AddSingleton(context))) + .ConfigureHosting( + (context, z) => z.ConfigureServices( + (_, s) => + { + s.AddSingleton(context); + s.AddHttpClient(); + var clientBuilder = s.AddRocketClient(); + + + s.Configure( + clientBuilder.ClientName, options => + { + options.HttpMessageHandlerBuilderActions.Add( + builder => + { + if (Factory.ClientOptions.AllowAutoRedirect) + { + builder.AdditionalHandlers.Add(new RedirectHandler(Factory.ClientOptions.MaxAutomaticRedirections)); + } + + if (Factory.ClientOptions.HandleCookies) + { + builder.AdditionalHandlers.Add(new CookieContainerHandler()); + } + + builder.PrimaryHandler = Factory.Server.CreateHandler(); + } + ); + + options.HttpClientActions.Add( + client => client.BaseAddress = new Uri(Factory.ClientOptions.BaseAddress + "graphql/") + ); + } + ); + } + ) + ) .EnableConventionAttributes() ) .ConfigureLoggerFactory(LoggerFactory); diff --git a/test/Sample.Graphql.Tests/LaunchRecords/CreateLaunchRecordTests.cs b/test/Sample.Graphql.Tests/LaunchRecords/CreateLaunchRecordTests.cs new file mode 100644 index 000000000..7055d28e5 --- /dev/null +++ b/test/Sample.Graphql.Tests/LaunchRecords/CreateLaunchRecordTests.cs @@ -0,0 +1,53 @@ +using Microsoft.Extensions.DependencyInjection; +using NodaTime; +using Rocket.Surgery.DependencyInjection; +using Sample.Core.Domain; +using CoreRocketType = Sample.Core.Domain.RocketType; + +namespace Sample.Graphql.Tests.LaunchRecords; + +public class CreateLaunchRecordTests : HandleWebHostBase +{ + [Fact] + public async Task Should_Create_A_LaunchRecord() + { + var client = Factory.Services.GetRequiredService(); + var clock = ServiceProvider.GetRequiredService(); + var rocket = await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var rocket = new ReadyRocket + { + Type = CoreRocketType.Falcon9, + SerialNumber = "12345678901234" + }; + z.Add(rocket); + + await z.SaveChangesAsync(); + return rocket; + } + ); + + + var response = await client.CreateLaunchRecord.ExecuteAsync( + new CreateLaunchRecordRequest + { + Partner = "partner", + Payload = "geo-fence-ftl", + RocketId = rocket.Id.Value, + ScheduledLaunchDate = clock.GetCurrentInstant().ToDateTimeOffset().ToString("O"), + PayloadWeightKg = 100, + } + ); + response.EnsureNoErrors(); + + response.Data.CreateLaunchRecord.Id.Should().NotBeEmpty(); + } + + public CreateLaunchRecordTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + } + + private static readonly Faker Faker = new(); +} diff --git a/test/Sample.Graphql.Tests/LaunchRecords/GetLaunchRecordTests.cs b/test/Sample.Graphql.Tests/LaunchRecords/GetLaunchRecordTests.cs new file mode 100644 index 000000000..072bedabc --- /dev/null +++ b/test/Sample.Graphql.Tests/LaunchRecords/GetLaunchRecordTests.cs @@ -0,0 +1,58 @@ +using Microsoft.Extensions.DependencyInjection; +using NodaTime; +using Rocket.Surgery.DependencyInjection; +using Sample.Core.Domain; +using Sample.Core.Models; +using CoreRocketType = Sample.Core.Domain.RocketType; + +namespace Sample.Graphql.Tests.LaunchRecords; + +public class GetLaunchRecordTests : HandleWebHostBase +{ + [Fact] + public async Task Should_Get_A_LaunchRecord() + { + var client = Factory.Services.GetRequiredService(); + var record = await ServiceProvider.WithScoped() + .Invoke( + async (context, clock) => + { + var rocket = new ReadyRocket + { + Id = RocketId.New(), + Type = CoreRocketType.Falcon9, + SerialNumber = "12345678901234" + }; + + var record = new LaunchRecord + { + Partner = "partner", + Payload = "geo-fence-ftl", + RocketId = rocket.Id, + ScheduledLaunchDate = clock.GetCurrentInstant().ToDateTimeOffset(), + PayloadWeightKg = 100, + }; + context.Add(rocket); + context.Add(record); + + await context.SaveChangesAsync(); + return record; + } + ); + + var response = await client.GetLaunchRecord.ExecuteAsync(record.Id.Value); + response.EnsureNoErrors(); + + response.Data.LaunchRecords.Items[0].Partner.Should().Be("partner"); + response.Data.LaunchRecords.Items[0].Payload.Should().Be("geo-fence-ftl"); + response.Data.LaunchRecords.Items[0].Rocket.Type.Should().Be(RocketType.Falcon9); + response.Data.LaunchRecords.Items[0].Rocket.SerialNumber.Should().Be("12345678901234"); + response.Data.LaunchRecords.Items[0].ScheduledLaunchDate.Should().Be(record.ScheduledLaunchDate); + } + + public GetLaunchRecordTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + } + + private static readonly Faker Faker = new(); +} diff --git a/test/Sample.Graphql.Tests/LaunchRecords/ListLaunchRecordsTests.cs b/test/Sample.Graphql.Tests/LaunchRecords/ListLaunchRecordsTests.cs new file mode 100644 index 000000000..4c8bde2bc --- /dev/null +++ b/test/Sample.Graphql.Tests/LaunchRecords/ListLaunchRecordsTests.cs @@ -0,0 +1,65 @@ +using Bogus; +using Microsoft.Extensions.DependencyInjection; +using Rocket.Surgery.DependencyInjection; +using Sample.Core; +using Sample.Core.Domain; +using Xunit; +using Xunit.Abstractions; + +namespace Sample.Graphql.Tests.LaunchRecords; + +public class ListLaunchRecordsTests : HandleWebHostBase +{ + [Fact] + public async Task Should_List_LaunchRecords() + { + var client = Factory.Services.GetRequiredService(); + await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var faker = new RocketFaker(); + var rockets = faker.Generate(3); + var records = new LaunchRecordFaker(rockets).Generate(10); + z.AddRange(rockets); + z.AddRange(records); + await z.SaveChangesAsync(); + } + ); + + var response = await client.GetLaunchRecords.ExecuteAsync(); + response.EnsureNoErrors(); + + response.Data.LaunchRecords.Items.Should().HaveCount(10); + } + + + [Fact] + public async Task Should_List_Specific_Kinds_Of_LaunchRecords() + { + var client = Factory.Services.GetRequiredService(); + await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var faker = new RocketFaker(); + var rockets = faker.UseSeed(100).Generate(3); + var records = new LaunchRecordFaker(rockets).UseSeed(100).Generate(10); + z.AddRange(rockets); + z.AddRange(records); + await z.SaveChangesAsync(); + } + ); + + var response = await client.GetFilteredLaunchRecords.ExecuteAsync(RocketType.FalconHeavy); + response.EnsureNoErrors(); + + response.Data.LaunchRecords.Items.Should().HaveCount(3); + } + + public ListLaunchRecordsTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + } + + private static readonly Faker Faker = new(); +} diff --git a/test/Sample.Graphql.Tests/LaunchRecords/RemoveLaunchRecordsTests.cs b/test/Sample.Graphql.Tests/LaunchRecords/RemoveLaunchRecordsTests.cs new file mode 100644 index 000000000..8d7809eae --- /dev/null +++ b/test/Sample.Graphql.Tests/LaunchRecords/RemoveLaunchRecordsTests.cs @@ -0,0 +1,44 @@ +using Bogus; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Rocket.Surgery.DependencyInjection; +using Sample.Core; +using Sample.Core.Domain; +using Xunit; +using Xunit.Abstractions; + +namespace Sample.Graphql.Tests.LaunchRecords; + +public class RemoveLaunchRecordsTests : HandleWebHostBase +{ + [Fact] + public async Task Should_Remove_LaunchRecord() + { + var client = Factory.Services.GetRequiredService(); + var id = await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var faker = new RocketFaker(); + var rocket = faker.Generate(); + var record = new LaunchRecordFaker(new[] { rocket }.ToList()).Generate(); + z.Add(rocket); + z.Add(record); + + await z.SaveChangesAsync(); + return record.Id; + } + ); + + var response = await client.DeleteLaunchRecord.ExecuteAsync(new DeleteLaunchRecordRequest { Id = id.Value }); + response.EnsureNoErrors(); + + ServiceProvider.WithScoped().Invoke(c => c.LaunchRecords.Should().BeEmpty()); + } + + public RemoveLaunchRecordsTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + } + + private static readonly Faker Faker = new(); +} diff --git a/test/Sample.Graphql.Tests/LaunchRecords/UpdateLaunchRecordTests.cs b/test/Sample.Graphql.Tests/LaunchRecords/UpdateLaunchRecordTests.cs new file mode 100644 index 000000000..71faa3df5 --- /dev/null +++ b/test/Sample.Graphql.Tests/LaunchRecords/UpdateLaunchRecordTests.cs @@ -0,0 +1,70 @@ +using Microsoft.Extensions.DependencyInjection; +using NodaTime; +using NodaTime.Extensions; +using Rocket.Surgery.DependencyInjection; +using Sample.Core.Domain; +using Sample.Core.Models; +using CoreRocketType = Sample.Core.Domain.RocketType; + +namespace Sample.Graphql.Tests.LaunchRecords; + +public class UpdateLaunchRecordTests : HandleWebHostBase +{ + [Fact] + public async Task Should_Update_A_LaunchRecord() + { + var client = Factory.Services.GetRequiredService(); + var clock = ServiceProvider.GetRequiredService(); + var record = await ServiceProvider.WithScoped() + .Invoke( + async (context, clk) => + { + var rocket = new ReadyRocket + { + Id = RocketId.New(), + Type = CoreRocketType.Falcon9, + SerialNumber = "12345678901234" + }; + + var record = new LaunchRecord + { + Partner = "partner", + Payload = "geo-fence-ftl", + RocketId = rocket.Id, + ScheduledLaunchDate = clk.GetCurrentInstant().ToDateTimeOffset(), + PayloadWeightKg = 100, + }; + context.Add(rocket); + context.Add(record); + + await context.SaveChangesAsync(); + return record; + } + ); + + await client.UpdateLaunchRecord.ExecuteAsync( + new EditLaunchRecordRequest + { + Id = record.Id.Value, + Partner = "partner", + Payload = "geo-fence-ftl", + RocketId = record.RocketId.Value, + ScheduledLaunchDate = record.ScheduledLaunchDate.AddSeconds(1).ToString("O"), + PayloadWeightKg = 200, + } + ); + + var response = await client.GetLaunchRecord.ExecuteAsync(record.Id.Value); + response.EnsureNoErrors(); + + response.Data.LaunchRecords.Items[0].ScheduledLaunchDate.Should() + .Be(( record.ScheduledLaunchDate.ToInstant() + Duration.FromSeconds(1) ).ToDateTimeOffset()); + response.Data.LaunchRecords.Items[0].PayloadWeightKg.Should().Be(200); + } + + public UpdateLaunchRecordTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + } + + private static readonly Faker Faker = new(); +} diff --git a/test/Sample.Graphql.Tests/Queries/mutations.graphql b/test/Sample.Graphql.Tests/Queries/mutations.graphql new file mode 100644 index 000000000..7632a59ef --- /dev/null +++ b/test/Sample.Graphql.Tests/Queries/mutations.graphql @@ -0,0 +1,33 @@ +mutation CreateRocket($req: CreateRocketRequest) { + CreateRocket(request: $req) { + id + } +} +mutation UpdateRocket($req: EditRocketRequest) { + EditRocket(request: $req) { + id + type + serialNumber: sn + } +} +mutation DeleteRocket($req: DeleteRocketRequest) { + DeleteRocket(request: $req) +} +mutation CreateLaunchRecord($req: CreateLaunchRecordRequest) { + CreateLaunchRecord(request: $req) { + id + } +} +mutation UpdateLaunchRecord($req: EditLaunchRecordRequest) { + EditLaunchRecord(request: $req) { + id + partner + scheduledLaunchDate + actualLaunchDate + rocketSerialNumber + rocketType + } +} +mutation DeleteLaunchRecord($req: DeleteLaunchRecordRequest) { + DeleteLaunchRecord(request: $req) +} diff --git a/test/Sample.Graphql.Tests/Queries/query.graphql b/test/Sample.Graphql.Tests/Queries/query.graphql new file mode 100644 index 000000000..81a215b63 --- /dev/null +++ b/test/Sample.Graphql.Tests/Queries/query.graphql @@ -0,0 +1,89 @@ +query GetLaunchRecords { + launchRecords { + items { + id + rocket { + id + type + serialNumber + } + payload + payloadWeightKg + actualLaunchDate + scheduledLaunchDate + } + } +} + +query GetFilteredLaunchRecords($rocketType: RocketType!) { + launchRecords(where: { rocket: { type: { eq: $rocketType } } }) { + items { + id + rocket { + id + type + serialNumber + } + payload + payloadWeightKg + actualLaunchDate + scheduledLaunchDate + } + } +} +query GetLaunchRecord($id: UUID) { + launchRecords(where: { id: { eq: $id } }) { + items { + id + rocket { + id + type + serialNumber + } + partner + payload + payloadWeightKg + actualLaunchDate + scheduledLaunchDate + } + } +} + +query GetRockets { + rockets { + items { + id + serialNumber + type + launchRecords { + partner + } + } + } +} + +query GetFilteredRockets($rocketType: RocketType) { + rockets(where: {type: { eq: $rocketType } }) { + items { + id + serialNumber + type + launchRecords { + partner + } + } + } +} + +query GetRocket($id: UUID) { + rockets(where: { id: { eq: $id } }) { + items { + id + serialNumber + type + launchRecords { + partner + } + } + } +} diff --git a/test/Sample.Graphql.Tests/Rockets/CreateRocketTests.cs b/test/Sample.Graphql.Tests/Rockets/CreateRocketTests.cs new file mode 100644 index 000000000..77733effc --- /dev/null +++ b/test/Sample.Graphql.Tests/Rockets/CreateRocketTests.cs @@ -0,0 +1,56 @@ +using Microsoft.Extensions.DependencyInjection; +using CoreRocketType = Sample.Core.Domain.RocketType; + +namespace Sample.Graphql.Tests.Rockets; + +public class CreateRocketTests : HandleWebHostBase +{ + [Fact] + public async Task Should_Create_A_Rocket() + { + var client = Factory.Services.GetRequiredService(); + var response = await client.CreateRocket.ExecuteAsync( + new CreateRocketRequest + { + Type = RocketType.Falcon9, + SerialNumber = "12345678901234" + } + ); + + response.Data.CreateRocket.Id.Should().NotBeEmpty(); + } + + [Fact] + public async Task Should_Throw_If_Rocket_Exists() + { + var client = Factory.Services.GetRequiredService(); + var response = await client.CreateRocket.ExecuteAsync( + new CreateRocketRequest + { + Type = RocketType.Falcon9, + SerialNumber = "12345678901234" + } + ); + + Func action = () => client.CreateRocket.ExecuteAsync( + new CreateRocketRequest + { + Type = RocketType.Falcon9, + SerialNumber = "12345678901234" + } + ); + var response2 = await client.CreateRocket.ExecuteAsync( + new CreateRocketRequest + { + Type = RocketType.Falcon9, + SerialNumber = "12345678901234" + } + ); + response2.IsErrorResult().Should().BeTrue(); + response2.Errors[0].Message.Should().Be("A Rocket already exists with that serial number!"); + } + + public CreateRocketTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + } +} diff --git a/test/Sample.Graphql.Tests/Rockets/GetRocketTests.cs b/test/Sample.Graphql.Tests/Rockets/GetRocketTests.cs new file mode 100644 index 000000000..bafaabf81 --- /dev/null +++ b/test/Sample.Graphql.Tests/Rockets/GetRocketTests.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.DependencyInjection; +using Rocket.Surgery.DependencyInjection; +using Sample.Core.Domain; +using CoreRocketType = Sample.Core.Domain.RocketType; + +namespace Sample.Graphql.Tests.Rockets; + +public class GetRocketTests : HandleWebHostBase +{ + [Fact] + public async Task Should_Get_A_Rocket() + { + var client = Factory.Services.GetRequiredService(); + var rocket = await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var rocket = new ReadyRocket + { + Type = CoreRocketType.Falcon9, + SerialNumber = "12345678901234" + }; + z.Add(rocket); + + await z.SaveChangesAsync(); + return rocket.Id; + } + ); + + var response = await client.GetRocket.ExecuteAsync(rocket.Value); + response.EnsureNoErrors(); + + response.Data.Rockets.Items[0].Type.Should().Be(RocketType.Falcon9); + response.Data.Rockets.Items[0].SerialNumber.Should().Be("12345678901234"); + } + + public GetRocketTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + } + + private static readonly Faker Faker = new(); +} diff --git a/test/Sample.Graphql.Tests/Rockets/ListRocketsTests.cs b/test/Sample.Graphql.Tests/Rockets/ListRocketsTests.cs new file mode 100644 index 000000000..9b237d1b5 --- /dev/null +++ b/test/Sample.Graphql.Tests/Rockets/ListRocketsTests.cs @@ -0,0 +1,57 @@ +using Microsoft.Extensions.DependencyInjection; +using Rocket.Surgery.DependencyInjection; +using Sample.Core; +using Sample.Core.Domain; + +namespace Sample.Graphql.Tests.Rockets; + +public class ListRocketsTests : HandleWebHostBase +{ + [Fact] + public async Task Should_List_Rockets() + { + var client = Factory.Services.GetRequiredService(); + await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var faker = new RocketFaker(); + z.AddRange(faker.Generate(10)); + + await z.SaveChangesAsync(); + } + ); + + var response = await client.GetRockets.ExecuteAsync(); + response.EnsureNoErrors(); + + response.Data.Rockets.Items.Should().HaveCount(10); + } + + [Fact] + public async Task Should_List_Specific_Kinds_Of_Rockets() + { + var client = Factory.Services.GetRequiredService(); + await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var faker = new RocketFaker(); + z.AddRange(faker.UseSeed(100).Generate(10)); + + await z.SaveChangesAsync(); + } + ); + + var response = await client.GetFilteredRockets.ExecuteAsync(RocketType.AtlasV); + response.EnsureNoErrors(); + + response.Data.Rockets.Items.Should().HaveCount(5); + } + + public ListRocketsTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + } + + private static readonly Faker Faker = new(); +} diff --git a/test/Sample.Graphql.Tests/Rockets/RemoveRocketsTests.cs b/test/Sample.Graphql.Tests/Rockets/RemoveRocketsTests.cs new file mode 100644 index 000000000..fe3a9a8a6 --- /dev/null +++ b/test/Sample.Graphql.Tests/Rockets/RemoveRocketsTests.cs @@ -0,0 +1,41 @@ +using Bogus; +using FluentAssertions; +using Microsoft.Extensions.DependencyInjection; +using Rocket.Surgery.DependencyInjection; +using Sample.Core; +using Sample.Core.Domain; +using Xunit; +using Xunit.Abstractions; + +namespace Sample.Graphql.Tests.Rockets; + +public class RemoveRocketsTests : HandleWebHostBase +{ + [Fact] + public async Task Should_Remove_Rocket() + { + var client = Factory.Services.GetRequiredService(); + var id = await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var faker = new RocketFaker(); + var rocket = faker.Generate(); + z.Add(rocket); + + await z.SaveChangesAsync().ConfigureAwait(false); + return rocket.Id; + } + ); + + await client.DeleteRocket.ExecuteAsync(new DeleteRocketRequest { Id = id.Value }); + + ServiceProvider.WithScoped().Invoke(c => c.Rockets.Should().BeEmpty()); + } + + public RemoveRocketsTests(ITestOutputHelper outputHelper) : base(outputHelper) + { + } + + private static readonly Faker Faker = new(); +} diff --git a/test/Sample.Graphql.Tests/Rockets/UpdateRocketTests.cs b/test/Sample.Graphql.Tests/Rockets/UpdateRocketTests.cs new file mode 100644 index 000000000..94bb76579 --- /dev/null +++ b/test/Sample.Graphql.Tests/Rockets/UpdateRocketTests.cs @@ -0,0 +1,83 @@ +using Humanizer; +using Microsoft.Extensions.DependencyInjection; +using Rocket.Surgery.DependencyInjection; +using Sample.Core.Domain; +using CoreRocketType = Sample.Core.Domain.RocketType; + +namespace Sample.Graphql.Tests.Rockets; + +public class UpdateRocketTests : HandleWebHostBase +{ + [Fact] + public async Task Should_Update_A_Rocket() + { + var client = Factory.Services.GetRequiredService(); + + var rocket = await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var rocket = new ReadyRocket + { + Type = CoreRocketType.Falcon9, + SerialNumber = "12345678901234" + }; + z.Add(rocket); + + await z.SaveChangesAsync(); + return rocket; + } + ); + + var u = await client.UpdateRocket.ExecuteAsync( + new EditRocketRequest + { + Id = rocket.Id.Value, + Type = RocketType.FalconHeavy, + SerialNumber = string.Join("", rocket.SerialNumber.Reverse()) + } + ); + u.IsSuccessResult().Should().Be(true); + } + + public UpdateRocketTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) + { + } + + private static readonly Faker Faker = new(); + + [Theory] + [ClassData(typeof(ShouldValidateUsersRequiredFieldData))] + public async Task Should_Validate_Required_Fields(EditRocketRequest request, string propertyName) + { + var client = Factory.Services.GetRequiredService(); + request = request with { Id = Guid.NewGuid() }; + var response = await client.UpdateRocket.ExecuteAsync(request); + response.IsErrorResult().Should().BeTrue(); + response.Errors[0].Extensions!["field"].As().Split('.').Last() + .Pascalize().Should().Be(propertyName); + } + + private class ShouldValidateUsersRequiredFieldData : TheoryData + { + public ShouldValidateUsersRequiredFieldData() + { + Add( + new EditRocketRequest { Type = RocketType.Falcon9 }, + nameof(EditRocketRequest.SerialNumber) + ); + Add( + new EditRocketRequest { SerialNumber = Faker.Random.String2(0, 9), Type = RocketType.FalconHeavy }, + nameof(EditRocketRequest.SerialNumber) + ); + Add( + new EditRocketRequest { SerialNumber = Faker.Random.String2(600, 800), Type = RocketType.AtlasV }, + nameof(EditRocketRequest.SerialNumber) + ); + Add( + new EditRocketRequest { SerialNumber = Faker.Random.String2(11) }, + nameof(EditRocketRequest.Type) + ); + } + } +} diff --git a/test/Sample.Graphql.Tests/Sample.Graphql.Tests.csproj b/test/Sample.Graphql.Tests/Sample.Graphql.Tests.csproj index 5a81c23c9..c7690c347 100644 --- a/test/Sample.Graphql.Tests/Sample.Graphql.Tests.csproj +++ b/test/Sample.Graphql.Tests/Sample.Graphql.Tests.csproj @@ -1,13 +1,19 @@  net6.0 + + true + + + + diff --git a/test/Sample.Graphql.Tests/schema.extensions.graphql b/test/Sample.Graphql.Tests/schema.extensions.graphql new file mode 100644 index 000000000..0b5fbd98b --- /dev/null +++ b/test/Sample.Graphql.Tests/schema.extensions.graphql @@ -0,0 +1,13 @@ +scalar _KeyFieldSet + +directive @key(fields: _KeyFieldSet!) on SCHEMA | OBJECT + +directive @serializationType(name: String!) on SCALAR + +directive @runtimeType(name: String!) on SCALAR + +directive @enumValue(value: String!) on ENUM_VALUE + +directive @rename(name: String!) on INPUT_FIELD_DEFINITION | INPUT_OBJECT | ENUM | ENUM_VALUE + +extend schema @key(fields: "id") \ No newline at end of file diff --git a/test/Sample.Graphql.Tests/schema.graphql b/test/Sample.Graphql.Tests/schema.graphql new file mode 100644 index 000000000..a9b480973 --- /dev/null +++ b/test/Sample.Graphql.Tests/schema.graphql @@ -0,0 +1,423 @@ +schema { + query: Query + mutation: Mutation +} + +scalar UUID + +"Represents a time zone - a mapping between UTC and local time.\nA time zone maps UTC instants to local times - or, equivalently, to the offset from UTC at any particular instant." +scalar DateTimeZone + +"Represents a fixed (and calendar-independent) length of time." +scalar Duration + +"Represents an instant on the global timeline, with nanosecond resolution." +scalar Instant + +"Equates the days of the week with their numerical value according to ISO-8601.\n Monday = 1, Tuesday = 2, Wednesday = 3, Thursday = 4, Friday = 5, Saturday = 6, Sunday = 7." +scalar IsoDayOfWeek + +"A date and time in a particular calendar system." +scalar LocalDateTime + +"LocalDate is an immutable struct representing a date within the calendar, with no reference to a particular time zone or time of day." +scalar LocalDate + +"LocalTime is an immutable struct representing a time of day, with no reference to a particular calendar, time zone or date." +scalar LocalTime + +"A local date and time in a particular calendar system, combined with an offset from UTC." +scalar OffsetDateTime + +"A combination of a LocalDate and an Offset, to represent a date at a specific offset from UTC but without any time-of-day information." +scalar OffsetDate + +"A combination of a LocalTime and an Offset, to represent a time-of-day at a specific offset from UTC but without any date information." +scalar OffsetTime + +"An offset from UTC in seconds. A positive value means that the local time is ahead of UTC (e.g. for Europe); a negative value means that the local time is behind UTC (e.g. for America)." +scalar Offset + +"Represents a period of time expressed in human chronological terms: hours, days, weeks, months and so on." +scalar Period + +"A LocalDateTime in a specific time zone and with a particular offset to distinguish between otherwise-ambiguous instants.\nA ZonedDateTime is global, in that it maps to a single Instant." +scalar ZonedDateTime + +"Returns assembly information for the given application" +type Query { + launchRecords(skip: Int take: Int "Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: LaunchRecordFilterInput order: [LaunchRecordSortInput!]): LaunchRecordCollectionSegment + rockets(skip: Int take: Int "Returns the first _n_ elements from the list." first: Int "Returns the elements in the list that come after the specified cursor." after: String "Returns the last _n_ elements from the list." last: Int "Returns the elements in the list that come before the specified cursor." before: String where: ReadyRocketFilterInput order: [ReadyRocketSortInput!]): ReadyRocketCollectionSegment + "Get the assembly version information" + version: AssemblyInfo! +} + +"GraphQL operations are hierarchical and composed, describing a tree of information.\nWhile Scalar types describe the leaf values of these hierarchical operations,\nObjects describe the intermediate levels.\n \nGraphQL Objects represent a list of named fields, each of which yield a value of a\nspecific type. Object values should be serialized as ordered maps, where the selected\nfield names (or aliases) are the keys and the result of evaluating the field is the value,\nordered by the order in which they appear in the selection set.\n \nAll fields defined within an Object type must not have a name which begins\nwith \"__\" (two underscores), as this is used exclusively by\nGraphQL’s introspection system." +type Mutation { + CreateRocket(request: CreateRocketRequest): CreateRocketResponse + DeleteRocket(request: DeleteRocketRequest): Void + EditRocket(request: EditRocketRequest): RocketModel + GetRocket(request: GetRocketRequest): RocketModel + GetRocketLaunchRecord(request: GetRocketLaunchRecordRequest): LaunchRecordModel + CreateLaunchRecord(request: CreateLaunchRecordRequest): CreateLaunchRecordResponse + DeleteLaunchRecord(request: DeleteLaunchRecordRequest): Void + EditLaunchRecord(request: EditLaunchRecordRequest): LaunchRecordModel + GetLaunchRecord(request: GetLaunchRecordRequest): LaunchRecordModel +} + +"A rocket in inventory" +type ReadyRocket { + launchRecords(where: LaunchRecordFilterInput order: [LaunchRecordSortInput!]): [LaunchRecordBase!]! + id: UUID! + serialNumber: String! + type: RocketType! +} + +type LaunchRecord { + rocket: ReadyRocketBase! + id: UUID! + partner: String + payload: String + payloadWeightKg: Long! + actualLaunchDate: DateTime + scheduledLaunchDate: DateTime! +} + +scalar Void + +input LaunchRecordFilterInput { + and: [LaunchRecordFilterInput!] + or: [LaunchRecordFilterInput!] + id: StronglyTypedIdOperationFilterInput + partner: StringOperationFilterInput + payload: StringOperationFilterInput + payloadWeightKg: ComparableInt64OperationFilterInput + actualLaunchDate: ComparableNullableOfDateTimeOffsetOperationFilterInput + scheduledLaunchDate: ComparableDateTimeOffsetOperationFilterInput + rocketId: StronglyTypedIdOperationFilterInput + rocket: ReadyRocketFilterInput +} + +input LaunchRecordSortInput { + partner: SortEnumType + payload: SortEnumType + payloadWeightKg: SortEnumType + actualLaunchDate: SortEnumType + scheduledLaunchDate: SortEnumType +} + +"A rocket in inventory" +input ReadyRocketFilterInput { + and: [ReadyRocketFilterInput!] + or: [ReadyRocketFilterInput!] + id: StronglyTypedIdOperationFilterInput + serialNumber: StringOperationFilterInput + type: RocketTypeOperationFilterInput + launchRecords: ListFilterInputTypeOfLaunchRecordFilterInput +} + +"A rocket in inventory" +input ReadyRocketSortInput { + type: SortEnumType + serialNumber: SortEnumType +} + +"The collection segment represents one page of a pageable dataset \/ collection." +type LaunchRecordCollectionSegment { + "The items that belong to this page." + items: [LaunchRecord!] + "Information to aid in pagination." + pageInfo: CollectionSegmentInfo! + totalCount: Int! +} + +"The collection segment represents one page of a pageable dataset \/ collection." +type ReadyRocketCollectionSegment { + "The items that belong to this page." + items: [ReadyRocket!] + "Information to aid in pagination." + pageInfo: CollectionSegmentInfo! + totalCount: Int! +} + +type LaunchRecordBase { + id: UUID! + partner: String + payload: String + payloadWeightKg: Long! + actualLaunchDate: DateTime + scheduledLaunchDate: DateTime! +} + +"A rocket in inventory" +type ReadyRocketBase { + id: UUID! + serialNumber: String! + type: RocketType! +} + +input StronglyTypedIdOperationFilterInput { + eq: UUID + neq: UUID + in: [UUID] + nin: [UUID] +} + +input StringOperationFilterInput { + and: [StringOperationFilterInput!] + or: [StringOperationFilterInput!] + eq: String + neq: String + contains: String + ncontains: String + in: [String] + nin: [String] + startsWith: String + nstartsWith: String + endsWith: String + nendsWith: String +} + +input ComparableInt64OperationFilterInput { + eq: Long + neq: Long + in: [Long!] + nin: [Long!] + gt: Long + ngt: Long + gte: Long + ngte: Long + lt: Long + nlt: Long + lte: Long + nlte: Long +} + +input ComparableNullableOfDateTimeOffsetOperationFilterInput { + eq: DateTime + neq: DateTime + in: [DateTime] + nin: [DateTime] + gt: DateTime + ngt: DateTime + gte: DateTime + ngte: DateTime + lt: DateTime + nlt: DateTime + lte: DateTime + nlte: DateTime +} + +input ComparableDateTimeOffsetOperationFilterInput { + eq: DateTime + neq: DateTime + in: [DateTime!] + nin: [DateTime!] + gt: DateTime + ngt: DateTime + gte: DateTime + ngte: DateTime + lt: DateTime + nlt: DateTime + lte: DateTime + nlte: DateTime +} + +enum SortEnumType { + ASC + DESC +} + +input RocketTypeOperationFilterInput { + eq: RocketType + neq: RocketType + in: [RocketType!] + nin: [RocketType!] +} + +input ListFilterInputTypeOfLaunchRecordFilterInput { + all: LaunchRecordFilterInput + none: LaunchRecordFilterInput + some: LaunchRecordFilterInput + any: Boolean +} + +"Information about the offset pagination." +type CollectionSegmentInfo { + "Indicates whether more items exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more items exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! +} + +"The identifier of the rocket that was created" +type CreateRocketResponse { + "The rocket id" + id: UUID! +} + +"The operation to create a new rocket record" +input CreateRocketRequest { + "The serial number of the rocket" + serialNumber: String! + "The type of rocket" + type: RocketType! +} + +"The request to remove a rocket from the system" +input DeleteRocketRequest { + "The rocket id" + id: UUID! +} + +"The details of a given rocket" +type RocketModel { + "The unique rocket identifier" + id: UUID! + "The serial number of the rocket" + sn: String! + "The type of the rocket" + type: RocketType! +} + +"The edit operation to update a rocket" +input EditRocketRequest { + "The rocket id" + id: UUID! + "The serial number of the rocket" + serialNumber: String! + "The type of the rocket" + type: RocketType! +} + +"Request to fetch information about a rocket" +input GetRocketRequest { + "The rocket id" + id: UUID! +} + +"The launch record details" +type LaunchRecordModel { + "The launch record id" + id: UUID! + "The launch partner" + partner: String! + "The payload details" + payload: String! + "The payload weight in Kg" + payloadWeightKg: Long! + "The actual launch date" + actualLaunchDate: Instant + "The intended date for the launch" + scheduledLaunchDate: Instant! + "The serial number of the reusable rocket" + rocketSerialNumber: String! + "The kind of rocket that will be launching" + rocketType: RocketType! +} + +input GetRocketLaunchRecordRequest { + "The rocket id" + id: UUID! + "The launch record id" + launchRecordId: UUID! +} + +"The launch record creation response" +type CreateLaunchRecordResponse { + "The id of the new launch record" + id: UUID! +} + +"Create a launch record" +input CreateLaunchRecordRequest { + "The rocket to use" + rocketId: UUID! + "The launch partner" + partner: String + "The launch partners payload" + payload: String + "The payload weight" + payloadWeightKg: Float! + "The actual launch date" + actualLaunchDate: Instant + "The intended launch date" + scheduledLaunchDate: Instant! +} + +"The request to delete a launch record" +input DeleteLaunchRecordRequest { + "The launch record to delete" + id: UUID! +} + +"The launch record update request" +input EditLaunchRecordRequest { + "The launch record to update" + id: UUID! + "The updated launch partner" + partner: String! + "The updated launch payload" + payload: String! + "The updated payload weight" + payloadWeightKg: Float! + "The updated actual launch date" + actualLaunchDate: Instant + "The scheduled launch date" + scheduledLaunchDate: Instant! + "The update rocket id" + rocketId: UUID! +} + +"The request to get a launch record" +input GetLaunchRecordRequest { + "The launch record to find" + id: UUID! +} + +"The available rocket types" +enum RocketType { + "Your best bet" + FALCON9 + "For those huge payloads" + FALCON_HEAVY + "We stole our competitors rocket platform!" + ATLAS_V +} + +"The `Long` scalar type represents non-fractional signed whole 64-bit numeric values. Long can represent values between -(2^63) and 2^63 - 1." +scalar Long + +"The `DateTime` scalar represents an ISO-8601 compliant date time type." +scalar DateTime + +"The assembly info object" +type AssemblyInfo { + "The assembly version" + version: String + "The assembly created date" + created: Instant + "The assembly updated date" + updated: Instant + "The assembly company" + company: String + "The configuration the assembly was built with" + configuration: String + "The assembly copyright" + copyright: String + "The assembly description" + description: String + "The assembly product" + product: String + "The assembly title" + title: String + "The assembly trademark" + trademark: String + "The assembly metadata" + metadata: [KeyValuePairOfStringAndString!]! +} + +type KeyValuePairOfStringAndString { + key: String! + value: String! +} \ No newline at end of file diff --git a/test/Sample.Grpc.Tests/LaunchRecords/GetLaunchRecordTests.cs b/test/Sample.Grpc.Tests/LaunchRecords/GetLaunchRecordTests.cs index bf3fb7a5c..d0684caef 100644 --- a/test/Sample.Grpc.Tests/LaunchRecords/GetLaunchRecordTests.cs +++ b/test/Sample.Grpc.Tests/LaunchRecords/GetLaunchRecordTests.cs @@ -4,6 +4,7 @@ using NodaTime; using Rocket.Surgery.DependencyInjection; using Sample.Core.Domain; +using Sample.Core.Models; using Sample.Grpc.Tests.Validation; using Xunit; using Xunit.Abstractions; @@ -23,7 +24,7 @@ public async Task Should_Get_A_LaunchRecord() { var rocket = new ReadyRocket { - Id = Guid.NewGuid(), + Id = RocketId.New(), Type = Core.Domain.RocketType.Falcon9, SerialNumber = "12345678901234" }; diff --git a/test/Sample.Grpc.Tests/LaunchRecords/ListLaunchRecordsTests.cs b/test/Sample.Grpc.Tests/LaunchRecords/ListLaunchRecordsTests.cs index 80a635cfa..a557aa6d0 100644 --- a/test/Sample.Grpc.Tests/LaunchRecords/ListLaunchRecordsTests.cs +++ b/test/Sample.Grpc.Tests/LaunchRecords/ListLaunchRecordsTests.cs @@ -1,5 +1,6 @@ using Bogus; using FluentAssertions; +using Grpc.Core; using Rocket.Surgery.DependencyInjection; using Sample.Core; using Sample.Core.Domain; @@ -29,9 +30,39 @@ await ServiceProvider.WithScoped() } ); - var response = await client.ListLaunchRecordsAsync(new ListLaunchRecordsRequest()); + var response = await client.ListLaunchRecords(new ListLaunchRecordsRequest()).ResponseStream.ReadAllAsync().ToListAsync(); - response.Results.Should().HaveCount(10); + response.Should().HaveCount(10); + } + + [Fact] + public async Task Should_List_Specific_Kinds_Of_LaunchRecords() + { + var client = new LR.LaunchRecordsClient(Factory.CreateGrpcChannel()); + await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var faker = new RocketFaker(); + var rockets = faker.UseSeed(100).Generate(3); + var records = new LaunchRecordFaker(rockets).UseSeed(100).Generate(10); + z.AddRange(rockets); + z.AddRange(records); + await z.SaveChangesAsync(); + } + ); + + var response = await client.ListLaunchRecords( + new() + { + RocketType = new NullableRocketType + { + Data = RocketType.FalconHeavy + } + } + ).ResponseStream.ReadAllAsync().ToListAsync(); + + response.Should().HaveCount(3); } public ListLaunchRecordsTests(ITestOutputHelper outputHelper) : base(outputHelper) diff --git a/test/Sample.Grpc.Tests/LaunchRecords/UpdateLaunchRecordTests.cs b/test/Sample.Grpc.Tests/LaunchRecords/UpdateLaunchRecordTests.cs index f1d40e54c..1a432200c 100644 --- a/test/Sample.Grpc.Tests/LaunchRecords/UpdateLaunchRecordTests.cs +++ b/test/Sample.Grpc.Tests/LaunchRecords/UpdateLaunchRecordTests.cs @@ -6,6 +6,7 @@ using NodaTime.Extensions; using Rocket.Surgery.DependencyInjection; using Sample.Core.Domain; +using Sample.Core.Models; using Sample.Grpc.Tests.Validation; using Xunit; using Xunit.Abstractions; @@ -27,7 +28,7 @@ public async Task Should_Update_A_LaunchRecord() { var rocket = new ReadyRocket { - Id = Guid.NewGuid(), + Id = RocketId.New(), Type = Core.Domain.RocketType.Falcon9, SerialNumber = "12345678901234" }; diff --git a/test/Sample.Grpc.Tests/Rockets/ListRocketsTests.cs b/test/Sample.Grpc.Tests/Rockets/ListRocketsTests.cs index 763a22e20..1c130d90d 100644 --- a/test/Sample.Grpc.Tests/Rockets/ListRocketsTests.cs +++ b/test/Sample.Grpc.Tests/Rockets/ListRocketsTests.cs @@ -1,5 +1,6 @@ using Bogus; using FluentAssertions; +using Grpc.Core; using Rocket.Surgery.DependencyInjection; using Sample.Core; using Sample.Core.Domain; @@ -27,9 +28,37 @@ await ServiceProvider.WithScoped() } ); - var response = await client.ListRocketsAsync(new ListRocketsRequest()); + var response = await client.ListRockets(new ListRocketsRequest()).ResponseStream.ReadAllAsync().ToListAsync(); - response.Results.Should().HaveCount(10); + response.Should().HaveCount(10); + } + + [Fact] + public async Task Should_List_Specific_Kinds_Of_Rockets() + { + var client = new R.RocketsClient(Factory.CreateGrpcChannel()); + await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var faker = new RocketFaker(); + z.AddRange(faker.UseSeed(100).Generate(10)); + + await z.SaveChangesAsync(); + } + ); + + var response = await client.ListRockets( + new ListRocketsRequest + { + RocketType = new NullableRocketType + { + Data = RocketType.AtlasV + } + } + ).ResponseStream.ReadAllAsync().ToListAsync(); + + response.Should().HaveCount(5); } public ListRocketsTests(ITestOutputHelper outputHelper) : base(outputHelper) diff --git a/test/Sample.Grpc.Tests/Validation/Integration/CustomValidatorIntegrationTest.cs b/test/Sample.Grpc.Tests/Validation/Integration/CustomValidatorIntegrationTest.cs index 89bac6959..930f3a145 100644 --- a/test/Sample.Grpc.Tests/Validation/Integration/CustomValidatorIntegrationTest.cs +++ b/test/Sample.Grpc.Tests/Validation/Integration/CustomValidatorIntegrationTest.cs @@ -32,10 +32,10 @@ await _factory.Services.WithScoped() var client = new Grpc.Rockets.RocketsClient(_factory.CreateGrpcChannel()); // When - var response = await client.ListRocketsAsync(new ListRocketsRequest()); + var response = await client.ListRockets(new ListRocketsRequest()).ResponseStream.ReadAllAsync().ToListAsync(); // Then nothing happen. - response.Results.Should().HaveCountGreaterThan(0); + response.Should().HaveCountGreaterThan(0); } [Fact] diff --git a/test/Sample.Restful.Tests/ApiDescriptionData.cs b/test/Sample.Restful.Tests/ApiDescriptionData.cs index f8b77c6df..7dd554ffd 100644 --- a/test/Sample.Restful.Tests/ApiDescriptionData.cs +++ b/test/Sample.Restful.Tests/ApiDescriptionData.cs @@ -5,7 +5,7 @@ namespace Sample.Restful.Tests; -public class ApiDescriptionData : TheoryData +public class ApiDescriptionData : TheoryData where T : WebApplicationFactory, new() { public ApiDescriptionData() @@ -14,7 +14,22 @@ public ApiDescriptionData() var provider = host.Services.GetRequiredService(); foreach (var item in provider.ApiDescriptionGroups.Items.SelectMany(z => z.Items)) { - Add(item); + Add(new ApiDescriptionData(item)); } } } + +public class ApiDescriptionData +{ + public ApiDescriptionData(ApiDescription description) + { + Description = description; + } + + public ApiDescription Description { get; } + + public override string ToString() + { + return $"[{Description.HttpMethod}] {Description.RelativePath}"; + } +} diff --git a/test/Sample.Restful.Tests/LaunchRecords/CreateLaunchRecordTests.cs b/test/Sample.Restful.Tests/LaunchRecords/CreateLaunchRecordTests.cs index ecb3c66e9..8e9a7e784 100644 --- a/test/Sample.Restful.Tests/LaunchRecords/CreateLaunchRecordTests.cs +++ b/test/Sample.Restful.Tests/LaunchRecords/CreateLaunchRecordTests.cs @@ -40,7 +40,7 @@ public async Task Should_Create_A_LaunchRecord() { Partner = "partner", Payload = "geo-fence-ftl", - RocketId = rocket.Id, + RocketId = rocket.Id.Value, ScheduledLaunchDate = clock.GetCurrentInstant().ToDateTimeOffset(), PayloadWeightKg = 100, } diff --git a/test/Sample.Restful.Tests/LaunchRecords/GetLaunchRecordTests.cs b/test/Sample.Restful.Tests/LaunchRecords/GetLaunchRecordTests.cs index 78eade483..ce08749c4 100644 --- a/test/Sample.Restful.Tests/LaunchRecords/GetLaunchRecordTests.cs +++ b/test/Sample.Restful.Tests/LaunchRecords/GetLaunchRecordTests.cs @@ -3,6 +3,7 @@ using NodaTime; using Rocket.Surgery.DependencyInjection; using Sample.Core.Domain; +using Sample.Core.Models; using Sample.Restful.Client; using Xunit; using Xunit.Abstractions; @@ -23,7 +24,7 @@ public async Task Should_Get_A_LaunchRecord() { var rocket = new ReadyRocket { - Id = Guid.NewGuid(), + Id = RocketId.New(), Type = RocketType.Falcon9, SerialNumber = "12345678901234" }; @@ -44,7 +45,7 @@ public async Task Should_Get_A_LaunchRecord() } ); - var response = ( await client.GetLaunchRecordAsync(record.Id) ).Result; + var response = ( await client.GetLaunchRecordAsync(record.Id.Value) ).Result; response.Partner.Should().Be("partner"); response.Payload.Should().Be("geo-fence-ftl"); diff --git a/test/Sample.Restful.Tests/LaunchRecords/ListLaunchRecordsTests.cs b/test/Sample.Restful.Tests/LaunchRecords/ListLaunchRecordsTests.cs index 2dcdba523..cf8f6d764 100644 --- a/test/Sample.Restful.Tests/LaunchRecords/ListLaunchRecordsTests.cs +++ b/test/Sample.Restful.Tests/LaunchRecords/ListLaunchRecordsTests.cs @@ -1,4 +1,5 @@ -using Bogus; +#if NET6_0_OR_GREATER +using Bogus; using FluentAssertions; using Rocket.Surgery.DependencyInjection; using Sample.Core; @@ -6,6 +7,7 @@ using Sample.Restful.Client; using Xunit; using Xunit.Abstractions; +using RocketType = Sample.Restful.Client.RocketType; namespace Sample.Restful.Tests.LaunchRecords; @@ -33,9 +35,31 @@ await ServiceProvider.WithScoped() response.Result.Should().HaveCount(10); } + [Fact] + public async Task Should_List_Specific_Kinds_Of_LaunchRecords() + { + var client = new LaunchRecordClient(Factory.CreateClient()); + await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var faker = new RocketFaker(); + var rockets = faker.UseSeed(100).Generate(3); + var records = new LaunchRecordFaker(rockets).UseSeed(100).Generate(10); + z.AddRange(rockets); + z.AddRange(records); + await z.SaveChangesAsync(); + } + ); + + var response = await client.ListLaunchRecordsAsync(RocketType.FalconHeavy); + response.Result.Should().HaveCount(3); + } + public ListLaunchRecordsTests(ITestOutputHelper outputHelper) : base(outputHelper) { } private static readonly Faker Faker = new(); } +#endif diff --git a/test/Sample.Restful.Tests/LaunchRecords/RemoveLaunchRecordsTests.cs b/test/Sample.Restful.Tests/LaunchRecords/RemoveLaunchRecordsTests.cs index 1a881777e..bbc99057b 100644 --- a/test/Sample.Restful.Tests/LaunchRecords/RemoveLaunchRecordsTests.cs +++ b/test/Sample.Restful.Tests/LaunchRecords/RemoveLaunchRecordsTests.cs @@ -30,7 +30,7 @@ public async Task Should_Remove_LaunchRecord() } ); - await client.RemoveLaunchRecordAsync(id); + await client.DeleteLaunchRecordAsync(id.Value); ServiceProvider.WithScoped().Invoke(c => c.LaunchRecords.Should().BeEmpty()); } diff --git a/test/Sample.Restful.Tests/LaunchRecords/UpdateLaunchRecordTests.cs b/test/Sample.Restful.Tests/LaunchRecords/UpdateLaunchRecordTests.cs index 4d7edabec..827a66ef9 100644 --- a/test/Sample.Restful.Tests/LaunchRecords/UpdateLaunchRecordTests.cs +++ b/test/Sample.Restful.Tests/LaunchRecords/UpdateLaunchRecordTests.cs @@ -5,6 +5,7 @@ using NodaTime.Extensions; using Rocket.Surgery.DependencyInjection; using Sample.Core.Domain; +using Sample.Core.Models; using Sample.Restful.Client; using Xunit; using Xunit.Abstractions; @@ -25,7 +26,7 @@ public async Task Should_Update_A_LaunchRecord() { var rocket = new ReadyRocket { - Id = Guid.NewGuid(), + Id = RocketId.New(), Type = RocketType.Falcon9, SerialNumber = "12345678901234" }; @@ -46,19 +47,19 @@ public async Task Should_Update_A_LaunchRecord() } ); - await client.UpdateLaunchRecordAsync( - record.Id, - new EditLaunchRecordModel + await client.EditLaunchRecordAsync( + record.Id.Value, + new EditLaunchRecordRequest { Partner = "partner", Payload = "geo-fence-ftl", - RocketId = record.RocketId, + RocketId = record.RocketId.Value, ScheduledLaunchDate = clock.GetCurrentInstant().ToDateTimeOffset(), PayloadWeightKg = 200, } ); - var response = await client.GetLaunchRecordAsync(record.Id); + var response = await client.GetLaunchRecordAsync(record.Id.Value); response.Result.ScheduledLaunchDate.Should().Be(( record.ScheduledLaunchDate.ToInstant() + Duration.FromSeconds(1) ).ToDateTimeOffset()); response.Result.PayloadWeightKg.Should().Be(200); diff --git a/test/Sample.Restful.Tests/RestfulConventionTests.cs b/test/Sample.Restful.Tests/RestfulConventionTests.cs index cb0eb2499..9d3d37b7b 100644 --- a/test/Sample.Restful.Tests/RestfulConventionTests.cs +++ b/test/Sample.Restful.Tests/RestfulConventionTests.cs @@ -1,6 +1,6 @@ using FluentAssertions; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Controllers; using Xunit; namespace Sample.Restful.Tests; @@ -9,29 +9,31 @@ public class RestfulConventionTests { [Theory] [ClassData(typeof(ApiDescriptionData))] - public void Should_Have_Success_Response_Types(ApiDescription description) + public void Should_Have_Success_Response_Types(ApiDescriptionData description) { - description.SupportedResponseTypes.Should().Contain(z => z.StatusCode >= 200 && z.StatusCode < 300); + description.Description.SupportedResponseTypes.Should().Contain(z => z.StatusCode >= 200 && z.StatusCode < 300); } [Theory] [ClassData(typeof(ApiDescriptionData))] - public void Should_Have_Not_Found_Responses(ApiDescription description) + public void Should_Have_Not_Found_Responses(ApiDescriptionData description) { - description.SupportedResponseTypes.Should().Contain(z => z.StatusCode == StatusCodes.Status404NotFound); + var method = ( description.Description.ActionDescriptor as ControllerActionDescriptor )!.MethodInfo!; + if (method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(IAsyncEnumerable<>)) return; + description.Description.SupportedResponseTypes.Should().Contain(z => z.StatusCode == StatusCodes.Status404NotFound); } [Theory] [ClassData(typeof(ApiDescriptionData))] - public void Should_Have_Validation_Responses(ApiDescription description) + public void Should_Have_Validation_Responses(ApiDescriptionData description) { - description.SupportedResponseTypes.Should().Contain(z => z.StatusCode == StatusCodes.Status422UnprocessableEntity); + description.Description.SupportedResponseTypes.Should().Contain(z => z.StatusCode == StatusCodes.Status422UnprocessableEntity); } [Theory] [ClassData(typeof(ApiDescriptionData))] - public void Should_Have_Bad_Request_Responses(ApiDescription description) + public void Should_Have_Bad_Request_Responses(ApiDescriptionData description) { - description.SupportedResponseTypes.Should().Contain(z => z.IsDefaultResponse); + description.Description.SupportedResponseTypes.Should().Contain(z => z.IsDefaultResponse); } } diff --git a/test/Sample.Restful.Tests/Rockets/GetRocketTests.cs b/test/Sample.Restful.Tests/Rockets/GetRocketTests.cs index 8a5d9a5d9..187e579cb 100644 --- a/test/Sample.Restful.Tests/Rockets/GetRocketTests.cs +++ b/test/Sample.Restful.Tests/Rockets/GetRocketTests.cs @@ -32,7 +32,7 @@ public async Task Should_Get_A_Rocket() } ); - var response = await client.GetRocketAsync(rocket); + var response = await client.GetRocketAsync(rocket.Value); response.Result.Type.Should().Be(HttpRocketType.Falcon9); response.Result.Sn.Should().Be("12345678901234"); diff --git a/test/Sample.Restful.Tests/Rockets/ListRocketsTests.cs b/test/Sample.Restful.Tests/Rockets/ListRocketsTests.cs index 391fa0520..18d718ae0 100644 --- a/test/Sample.Restful.Tests/Rockets/ListRocketsTests.cs +++ b/test/Sample.Restful.Tests/Rockets/ListRocketsTests.cs @@ -1,4 +1,5 @@ -using Bogus; +#if NET6_0_OR_GREATER +using Bogus; using FluentAssertions; using Rocket.Surgery.DependencyInjection; using Sample.Core; @@ -6,6 +7,7 @@ using Sample.Restful.Client; using Xunit; using Xunit.Abstractions; +using RocketType = Sample.Restful.Client.RocketType; namespace Sample.Restful.Tests.Rockets; @@ -31,9 +33,30 @@ await ServiceProvider.WithScoped() response.Result.Should().HaveCount(10); } + [Fact] + public async Task Should_List_Specific_Kinds_Of_Rockets() + { + var client = new RocketClient(Factory.CreateClient()); + await ServiceProvider.WithScoped() + .Invoke( + async z => + { + var faker = new RocketFaker(); + z.AddRange(faker.UseSeed(100).Generate(10)); + + await z.SaveChangesAsync(); + } + ); + + var response = await client.ListRocketsAsync(RocketType.AtlasV); + + response.Result.Should().HaveCount(5); + } + public ListRocketsTests(ITestOutputHelper outputHelper) : base(outputHelper) { } private static readonly Faker Faker = new(); } +#endif diff --git a/test/Sample.Restful.Tests/Rockets/RemoveRocketsTests.cs b/test/Sample.Restful.Tests/Rockets/RemoveRocketsTests.cs index 1331a8aec..22a99210b 100644 --- a/test/Sample.Restful.Tests/Rockets/RemoveRocketsTests.cs +++ b/test/Sample.Restful.Tests/Rockets/RemoveRocketsTests.cs @@ -28,7 +28,7 @@ public async Task Should_Remove_Rocket() } ); - await client.RemoveRocketAsync(id); + await client.RemoveRocketAsync(id.Value); ServiceProvider.WithScoped().Invoke(c => c.Rockets.Should().BeEmpty()); } diff --git a/test/Sample.Restful.Tests/Rockets/UpdateRocketTests.cs b/test/Sample.Restful.Tests/Rockets/UpdateRocketTests.cs index 3f3d817e4..052374851 100644 --- a/test/Sample.Restful.Tests/Rockets/UpdateRocketTests.cs +++ b/test/Sample.Restful.Tests/Rockets/UpdateRocketTests.cs @@ -32,9 +32,9 @@ public async Task Should_Update_A_Rocket() } ); - var u = await client.UpdateRocketAsync( - rocket.Id, - new EditRocketModel + var u = await client.EditRocketAsync( + rocket.Id.Value, + new EditRocketRequest { Type = Client.RocketType.FalconHeavy, SerialNumber = string.Join("", rocket.SerialNumber.Reverse()) @@ -56,10 +56,10 @@ public UpdateRocketTests(ITestOutputHelper testOutputHelper) : base(testOutputHe [Theory] [ClassData(typeof(ShouldValidateUsersRequiredFieldData))] - public async Task Should_Validate_Required_Fields(EditRocketModel request, string propertyName) + public async Task Should_Validate_Required_Fields(EditRocketRequest request, string propertyName) { var client = new RocketClient(Factory.CreateClient()); - Func a = () => client.UpdateRocketAsync(Guid.NewGuid(), request); + Func a = () => client.EditRocketAsync(Guid.NewGuid(), request); ( await a.Should().ThrowAsync>() ) .And .Result.Errors.Values @@ -69,21 +69,21 @@ public async Task Should_Validate_Required_Fields(EditRocketModel request, strin .Contain(propertyName); } - private class ShouldValidateUsersRequiredFieldData : TheoryData + private class ShouldValidateUsersRequiredFieldData : TheoryData { public ShouldValidateUsersRequiredFieldData() { Add( - new EditRocketModel(), - nameof(EditRocketModel.SerialNumber) + new EditRocketRequest(), + nameof(EditRocketRequest.SerialNumber) ); Add( - new EditRocketModel { SerialNumber = Faker.Random.String2(0, 9) }, - nameof(EditRocketModel.SerialNumber) + new EditRocketRequest { SerialNumber = Faker.Random.String2(0, 9) }, + nameof(EditRocketRequest.SerialNumber) ); Add( - new EditRocketModel { SerialNumber = Faker.Random.String2(600, 800) }, - nameof(EditRocketModel.SerialNumber) + new EditRocketRequest { SerialNumber = Faker.Random.String2(600, 800) }, + nameof(EditRocketRequest.SerialNumber) ); } }