diff --git a/.github/workflows/build-and-package.yml b/.github/workflows/build-and-package.yml index 7a783be..6826a20 100644 --- a/.github/workflows/build-and-package.yml +++ b/.github/workflows/build-and-package.yml @@ -10,6 +10,8 @@ jobs: steps: - name: Install Dotnet uses: actions/setup-dotnet@v1 + with: + dotnet-version: 9.x - name: Check-out uses: actions/checkout@master - name: Building (Release) diff --git a/.github/workflows/test-and-report.yml b/.github/workflows/test-and-report.yml index bba5294..971c679 100644 --- a/.github/workflows/test-and-report.yml +++ b/.github/workflows/test-and-report.yml @@ -2,6 +2,8 @@ name: test-and-report.yml on: pull_request: push: + branches: + - 'main' paths: - 'samples/**' - 'src/**' @@ -15,15 +17,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Check-out - uses: actions/checkout@master + uses: actions/checkout@v4 - name: Install Dotnet - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 8.x + 9.x - name: Install ReportGenerator run: dotnet tool install dotnet-reportgenerator-globaltool --tool-path tools - name: Test run: dotnet test /p:CollectCoverage=true /p:CoverletOutputFormat=lcov - name: Generate Coverage Report - run: tools/reportgenerator "-reports:tests/**/coverage.info" "-targetdir:./coverage/" -reporttypes:lcov + run: tools/reportgenerator "-reports:tests/**/coverage.*.info" "-targetdir:./coverage/" -reporttypes:lcov - name: Upload to Coveralls uses: coverallsapp/github-action@master with: diff --git a/.gitignore b/.gitignore index ab5a00a..f840a53 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ [Bb]in/ [Tt]est[Rr]esults/ nupkgs/ +.idea/ +.editorconfig diff --git a/Panner.AspNetCore.sln b/Panner.AspNetCore.sln index fffad7a..ca9ab8e 100644 --- a/Panner.AspNetCore.sln +++ b/Panner.AspNetCore.sln @@ -9,9 +9,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Panner.AspNetCore", "src\Pa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Panner.AspNetCore.Tests", "tests\Panner.AspNetCore.Tests\Panner.AspNetCore.Tests.csproj", "{2FA57751-5D7A-430D-89F8-CB06B75937FA}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNetCore3_1", "samples\AspNetCore3_1\AspNetCore3_1.csproj", "{FC10AD84-66D7-4C98-9A3B-620D6ACC1357}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiNet9MinimalFluent", "samples\WebApiNet9MinimalFluent\WebApiNet9MinimalFluent.csproj", "{84F94171-2814-4993-9BC6-43D7F9535027}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AspNet5_0", "samples\AspNet5_0\AspNet5_0.csproj", "{00CBFE9C-C93E-4DC9-BDE5-87A10E170CF5}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiNet9ControllersFluent", "samples\WebApiNet9ControllersFluent\WebApiNet9ControllersFluent.csproj", "{D7BE4E4E-38E0-4DCC-A8CB-7D9CE22994DB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebAPINet8MinimalFluent", "samples\WebAPINet8MinimalFluent\WebAPINet8MinimalFluent.csproj", "{39BD3C99-BBB1-48BA-B015-92C30A8813CC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApi8ControllersFluent", "samples\WebApi8ControllersFluent\WebApi8ControllersFluent.csproj", "{8580601A-A694-4656-B72C-B56C3BEBF81B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FastEndpointsNet9", "sample\FastEndpoints\Source\FastEndpointsNet9.csproj", "{D155BED8-7ABE-4EA4-8C75-3E3CFFDCDF5C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -27,21 +33,36 @@ Global {2FA57751-5D7A-430D-89F8-CB06B75937FA}.Debug|Any CPU.Build.0 = Debug|Any CPU {2FA57751-5D7A-430D-89F8-CB06B75937FA}.Release|Any CPU.ActiveCfg = Release|Any CPU {2FA57751-5D7A-430D-89F8-CB06B75937FA}.Release|Any CPU.Build.0 = Release|Any CPU - {FC10AD84-66D7-4C98-9A3B-620D6ACC1357}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FC10AD84-66D7-4C98-9A3B-620D6ACC1357}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FC10AD84-66D7-4C98-9A3B-620D6ACC1357}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FC10AD84-66D7-4C98-9A3B-620D6ACC1357}.Release|Any CPU.Build.0 = Release|Any CPU - {00CBFE9C-C93E-4DC9-BDE5-87A10E170CF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {00CBFE9C-C93E-4DC9-BDE5-87A10E170CF5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {00CBFE9C-C93E-4DC9-BDE5-87A10E170CF5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {00CBFE9C-C93E-4DC9-BDE5-87A10E170CF5}.Release|Any CPU.Build.0 = Release|Any CPU + {84F94171-2814-4993-9BC6-43D7F9535027}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {84F94171-2814-4993-9BC6-43D7F9535027}.Debug|Any CPU.Build.0 = Debug|Any CPU + {84F94171-2814-4993-9BC6-43D7F9535027}.Release|Any CPU.ActiveCfg = Release|Any CPU + {84F94171-2814-4993-9BC6-43D7F9535027}.Release|Any CPU.Build.0 = Release|Any CPU + {D7BE4E4E-38E0-4DCC-A8CB-7D9CE22994DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D7BE4E4E-38E0-4DCC-A8CB-7D9CE22994DB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D7BE4E4E-38E0-4DCC-A8CB-7D9CE22994DB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D7BE4E4E-38E0-4DCC-A8CB-7D9CE22994DB}.Release|Any CPU.Build.0 = Release|Any CPU + {39BD3C99-BBB1-48BA-B015-92C30A8813CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39BD3C99-BBB1-48BA-B015-92C30A8813CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39BD3C99-BBB1-48BA-B015-92C30A8813CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39BD3C99-BBB1-48BA-B015-92C30A8813CC}.Release|Any CPU.Build.0 = Release|Any CPU + {8580601A-A694-4656-B72C-B56C3BEBF81B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8580601A-A694-4656-B72C-B56C3BEBF81B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8580601A-A694-4656-B72C-B56C3BEBF81B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8580601A-A694-4656-B72C-B56C3BEBF81B}.Release|Any CPU.Build.0 = Release|Any CPU + {D155BED8-7ABE-4EA4-8C75-3E3CFFDCDF5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D155BED8-7ABE-4EA4-8C75-3E3CFFDCDF5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D155BED8-7ABE-4EA4-8C75-3E3CFFDCDF5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D155BED8-7ABE-4EA4-8C75-3E3CFFDCDF5C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {FC10AD84-66D7-4C98-9A3B-620D6ACC1357} = {D40300B7-1803-48CB-8F7D-6A96AB75F666} - {00CBFE9C-C93E-4DC9-BDE5-87A10E170CF5} = {D40300B7-1803-48CB-8F7D-6A96AB75F666} + {84F94171-2814-4993-9BC6-43D7F9535027} = {D40300B7-1803-48CB-8F7D-6A96AB75F666} + {D7BE4E4E-38E0-4DCC-A8CB-7D9CE22994DB} = {D40300B7-1803-48CB-8F7D-6A96AB75F666} + {39BD3C99-BBB1-48BA-B015-92C30A8813CC} = {D40300B7-1803-48CB-8F7D-6A96AB75F666} + {8580601A-A694-4656-B72C-B56C3BEBF81B} = {D40300B7-1803-48CB-8F7D-6A96AB75F666} + {D155BED8-7ABE-4EA4-8C75-3E3CFFDCDF5C} = {D40300B7-1803-48CB-8F7D-6A96AB75F666} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D12812E7-06E1-4FFB-A601-D656FE2A0075} diff --git a/samples/AspNet5_0/EFModel/BlogContext.cs b/sample/FastEndpoints/Source/EFModel/BlogContext.cs similarity index 95% rename from samples/AspNet5_0/EFModel/BlogContext.cs rename to sample/FastEndpoints/Source/EFModel/BlogContext.cs index cecad33..1015a9a 100644 --- a/samples/AspNet5_0/EFModel/BlogContext.cs +++ b/sample/FastEndpoints/Source/EFModel/BlogContext.cs @@ -1,4 +1,4 @@ -namespace Panner.AspNetCore.Samples.AspNet5_0.EFModel +namespace Panner.AspNetCore.Samples.FastEndpointsNet9.EFModel { using Microsoft.EntityFrameworkCore; using System; diff --git a/samples/AspNetCore3_1/EFModel/Post.cs b/sample/FastEndpoints/Source/EFModel/Post.cs similarity index 84% rename from samples/AspNetCore3_1/EFModel/Post.cs rename to sample/FastEndpoints/Source/EFModel/Post.cs index 123523b..10f85ab 100644 --- a/samples/AspNetCore3_1/EFModel/Post.cs +++ b/sample/FastEndpoints/Source/EFModel/Post.cs @@ -1,4 +1,4 @@ -namespace Panner.AspNetCore.Samples.AspNetCore3_1.EFModel +namespace Panner.AspNetCore.Samples.FastEndpointsNet9.EFModel { using System; diff --git a/sample/FastEndpoints/Source/FastEndpointsNet9.csproj b/sample/FastEndpoints/Source/FastEndpointsNet9.csproj new file mode 100644 index 0000000..f940e73 --- /dev/null +++ b/sample/FastEndpoints/Source/FastEndpointsNet9.csproj @@ -0,0 +1,27 @@ + + + + net9.0 + enable + enable + Exe + CS1591;CA2016 + true + Panner.AspNetCore.Samples.FastEndpointsNet9 + Panner.AspNetCore.Samples.FastEndpointsNet9 + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sample/FastEndpoints/Source/Features/Posts/Endpoint.cs b/sample/FastEndpoints/Source/Features/Posts/Endpoint.cs new file mode 100644 index 0000000..934eaf5 --- /dev/null +++ b/sample/FastEndpoints/Source/Features/Posts/Endpoint.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore; +using Panner; +using Panner.AspNetCore.Samples.FastEndpointsNet9.EFModel; +using Views = Panner.AspNetCore.Samples.FastEndpointsNet9.Views; +using Panner.AspNetCore.Samples.FastEndpointsNet9.PannerExtensions; + +namespace Posts; + +sealed class Endpoint : Endpoint> +{ + private readonly BlogContext _context; + + public Endpoint(BlogContext context) + { + _context = context; + } + + public override void Configure() + { + Get("/posts"); + AllowAnonymous(); + } + + public override async Task HandleAsync(Request r, CancellationToken c) + { + _context.Database.EnsureCreated(); + var result = await _context.Posts + .Apply(r.Filters ?? Array.Empty>()) + .Apply(r.Sorts ?? Array.Empty>()) + .Select(x => new Views.Post + { + Id = x.Id, + Title = x.Title, + Content = x.Content, + Creation = x.CreatedOn + }) + .ToArrayAsync(c); + + await SendAsync(result, cancellation: c); + } +} diff --git a/sample/FastEndpoints/Source/Features/Posts/Request.cs b/sample/FastEndpoints/Source/Features/Posts/Request.cs new file mode 100644 index 0000000..5afaf57 --- /dev/null +++ b/sample/FastEndpoints/Source/Features/Posts/Request.cs @@ -0,0 +1,16 @@ +using Microsoft.AspNetCore.Mvc; + +using Panner; +using Panner.AspNetCore.Samples.FastEndpointsNet9.EFModel; +using FastEndpoints; + +namespace Posts; + +sealed class Request +{ + [FastEndpoints.FromQuery] + public IReadOnlyCollection>? Sorts { get; set; } + + [FastEndpoints.FromQuery] + public IReadOnlyCollection>? Filters { get; set; } +} diff --git a/sample/FastEndpoints/Source/Features/SayHello/Endpoint.cs b/sample/FastEndpoints/Source/Features/SayHello/Endpoint.cs new file mode 100644 index 0000000..ab4a687 --- /dev/null +++ b/sample/FastEndpoints/Source/Features/SayHello/Endpoint.cs @@ -0,0 +1,18 @@ +namespace SayHello; + +sealed class Endpoint : Endpoint +{ + public override void Configure() + { + Post("/api/hello"); + AllowAnonymous(); + } + + public override async Task HandleAsync(Request r, CancellationToken c) + { + await SendAsync(new() + { + Message = $"Hello {r.FirstName} {r.LastName}..." + }); + } +} \ No newline at end of file diff --git a/sample/FastEndpoints/Source/Features/SayHello/Request.cs b/sample/FastEndpoints/Source/Features/SayHello/Request.cs new file mode 100644 index 0000000..f28b0a1 --- /dev/null +++ b/sample/FastEndpoints/Source/Features/SayHello/Request.cs @@ -0,0 +1,18 @@ +using FluentValidation; + +namespace SayHello; + +sealed class Request +{ + public string FirstName { get; set; } + public string LastName { get; set; } + + internal sealed class Validator : Validator + { + public Validator() + { + RuleFor(x => x.FirstName).MinimumLength(3); + RuleFor(x => x.LastName).MinimumLength(5); + } + } +} \ No newline at end of file diff --git a/sample/FastEndpoints/Source/Features/SayHello/Response.cs b/sample/FastEndpoints/Source/Features/SayHello/Response.cs new file mode 100644 index 0000000..8ae48e0 --- /dev/null +++ b/sample/FastEndpoints/Source/Features/SayHello/Response.cs @@ -0,0 +1,6 @@ +namespace SayHello; + +sealed class Response +{ + public string Message { get; set; } +} \ No newline at end of file diff --git a/sample/FastEndpoints/Source/Meta.cs b/sample/FastEndpoints/Source/Meta.cs new file mode 100644 index 0000000..f26bf9e --- /dev/null +++ b/sample/FastEndpoints/Source/Meta.cs @@ -0,0 +1,9 @@ +global using FastEndpoints; +global using FastEndpoints.Security; +global using FastEndpoints.Swagger; +global using FastEndpoints; +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Tests")] + +public partial class Program; \ No newline at end of file diff --git a/samples/AspNet5_0/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs b/sample/FastEndpoints/Source/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs similarity index 75% rename from samples/AspNet5_0/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs rename to sample/FastEndpoints/Source/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs index f122f53..9db989b 100644 --- a/samples/AspNet5_0/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs +++ b/sample/FastEndpoints/Source/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs @@ -1,6 +1,6 @@ -namespace Panner.AspNetCore.Samples.AspNet5_0.PannerExtensions +namespace Panner.AspNetCore.Samples.FastEndpointsNet9.PannerExtensions { - using global::Panner.AspNetCore.Samples.AspNet5_0.EFModel; + using global::Panner.AspNetCore.Samples.FastEndpointsNet9.EFModel; using global::Panner.Builders; public static partial class PEntityBuilderExtensions diff --git a/samples/AspNetCore3_1/PannerExtensions/SortPostByPopularityParticle.cs b/sample/FastEndpoints/Source/PannerExtensions/SortPostByPopularityParticle.cs similarity index 82% rename from samples/AspNetCore3_1/PannerExtensions/SortPostByPopularityParticle.cs rename to sample/FastEndpoints/Source/PannerExtensions/SortPostByPopularityParticle.cs index f95b229..43b78fc 100644 --- a/samples/AspNetCore3_1/PannerExtensions/SortPostByPopularityParticle.cs +++ b/sample/FastEndpoints/Source/PannerExtensions/SortPostByPopularityParticle.cs @@ -1,6 +1,6 @@ -namespace Panner.AspNetCore.Samples.AspNetCore3_1.PannerExtensions +namespace Panner.AspNetCore.Samples.FastEndpointsNet9.PannerExtensions { - using global::Panner.AspNetCore.Samples.AspNetCore3_1.EFModel; + using global::Panner.AspNetCore.Samples.FastEndpointsNet9.EFModel; using System.Linq; public class SortPostByPopularityParticle : ISortParticle diff --git a/samples/AspNetCore3_1/PannerExtensions/SortPostsByPopularityParticleGenerator.cs b/sample/FastEndpoints/Source/PannerExtensions/SortPostsByPopularityParticleGenerator.cs similarity index 82% rename from samples/AspNetCore3_1/PannerExtensions/SortPostsByPopularityParticleGenerator.cs rename to sample/FastEndpoints/Source/PannerExtensions/SortPostsByPopularityParticleGenerator.cs index 3ae0837..7430d17 100644 --- a/samples/AspNetCore3_1/PannerExtensions/SortPostsByPopularityParticleGenerator.cs +++ b/sample/FastEndpoints/Source/PannerExtensions/SortPostsByPopularityParticleGenerator.cs @@ -1,6 +1,6 @@ -using Panner.AspNetCore.Samples.AspNetCore3_1.EFModel; +using Panner.AspNetCore.Samples.FastEndpointsNet9.EFModel; -namespace Panner.AspNetCore.Samples.AspNetCore3_1.PannerExtensions +namespace Panner.AspNetCore.Samples.FastEndpointsNet9.PannerExtensions { public class SortPostsByPopularityParticleGenerator : ISortParticleGenerator { diff --git a/sample/FastEndpoints/Source/Program.cs b/sample/FastEndpoints/Source/Program.cs new file mode 100644 index 0000000..9b39955 --- /dev/null +++ b/sample/FastEndpoints/Source/Program.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore; +using Panner.AspNetCore; +using Panner.AspNetCore.Samples.FastEndpointsNet9; +using Panner.AspNetCore.Samples.FastEndpointsNet9.EFModel; +using Panner.AspNetCore.Samples.FastEndpointsNet9.PannerExtensions; +using Views = Panner.AspNetCore.Samples.FastEndpointsNet9.Views; +using Panner.Builders; + +var bld = WebApplication.CreateBuilder(args); + +bld.Services + .AddAuthenticationJwtBearer(s => s.SigningKey = bld.Configuration["Auth:JwtKey"]) + .AddAuthorization() + .AddFastEndpoints(o => o.SourceGeneratorDiscoveredTypes = DiscoveredTypes.All) + .SwaggerDocument(); + +bld.Services.UsePanner(c => +{ + c.Entity() + .IsSortableByPopularity() + .Property(x => x.Id, o => o + .IsSortableAs(nameof(Views.Post.Id)) + .IsFilterableAs(nameof(Views.Post.Id)) + ) + .Property(x => x.Title, o => o + .IsSortableAs(nameof(Views.Post.Title)) + ) + .Property(x => x.CreatedOn, o => o + .IsSortableAs(nameof(Views.Post.Creation)) + .IsFilterableAs(nameof(Views.Post.Creation)) + ); +}); + +bld.Services.AddDbContext(o => o.UseInMemoryDatabase("BlogDb")); + +var app = bld.Build(); +app.UseAuthentication() + .UseAuthorization() + .UseFastEndpoints(c => + { + c.Errors.UseProblemDetails(); + }) + .UseSwaggerGen(); +app.Run(); diff --git a/sample/FastEndpoints/Source/Properties/launchSettings.json b/sample/FastEndpoints/Source/Properties/launchSettings.json new file mode 100644 index 0000000..f2fb237 --- /dev/null +++ b/sample/FastEndpoints/Source/Properties/launchSettings.json @@ -0,0 +1,14 @@ +{ + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/samples/AspNetCore3_1/Views/Post.cs b/sample/FastEndpoints/Source/Views/Post.cs similarity index 78% rename from samples/AspNetCore3_1/Views/Post.cs rename to sample/FastEndpoints/Source/Views/Post.cs index 254ad6e..b0ef8bb 100644 --- a/samples/AspNetCore3_1/Views/Post.cs +++ b/sample/FastEndpoints/Source/Views/Post.cs @@ -1,4 +1,4 @@ -namespace Panner.AspNetCore.Samples.AspNetCore3_1.Views +namespace Panner.AspNetCore.Samples.FastEndpointsNet9.Views { using System; diff --git a/sample/FastEndpoints/Source/appsettings.Development.json b/sample/FastEndpoints/Source/appsettings.Development.json new file mode 100644 index 0000000..833a367 --- /dev/null +++ b/sample/FastEndpoints/Source/appsettings.Development.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Auth": { + "JwtKey": "a long secret string used to sign jwts. usually set via matching environment variable." + } +} \ No newline at end of file diff --git a/sample/FastEndpoints/Source/appsettings.Testing.json b/sample/FastEndpoints/Source/appsettings.Testing.json new file mode 100644 index 0000000..0e0dcd2 --- /dev/null +++ b/sample/FastEndpoints/Source/appsettings.Testing.json @@ -0,0 +1,3 @@ +{ + +} \ No newline at end of file diff --git a/sample/FastEndpoints/Source/appsettings.json b/sample/FastEndpoints/Source/appsettings.json new file mode 100644 index 0000000..1b2d3ba --- /dev/null +++ b/sample/FastEndpoints/Source/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} \ No newline at end of file diff --git a/samples/AspNet5_0/AspNet5_0.csproj b/samples/AspNet5_0/AspNet5_0.csproj deleted file mode 100644 index 9058798..0000000 --- a/samples/AspNet5_0/AspNet5_0.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - net5.0 - Panner.AspNetCore.Samples.AspNetCore5_0 - Panner.AspNetCore.Samples.AspNetCore5_0 - false - - - - - - - - - - - - diff --git a/samples/AspNet5_0/Models/ErrorViewModel.cs b/samples/AspNet5_0/Models/ErrorViewModel.cs deleted file mode 100644 index aa14676..0000000 --- a/samples/AspNet5_0/Models/ErrorViewModel.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Panner.AspNetCore.Samples.AspNet5_0.Models -{ - public class ErrorViewModel - { - public string RequestId { get; set; } - - public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - } -} diff --git a/samples/AspNet5_0/Program.cs b/samples/AspNet5_0/Program.cs deleted file mode 100644 index 9419da5..0000000 --- a/samples/AspNet5_0/Program.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Panner.AspNetCore.Samples.AspNet5_0 -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} diff --git a/samples/AspNet5_0/Properties/launchSettings.json b/samples/AspNet5_0/Properties/launchSettings.json deleted file mode 100644 index e2f5235..0000000 --- a/samples/AspNet5_0/Properties/launchSettings.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:61036", - "sslPort": 44392 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "AspNet5_0": { - "commandName": "Project", - "dotnetRunMessages": "true", - "launchBrowser": true, - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/samples/AspNet5_0/Startup.cs b/samples/AspNet5_0/Startup.cs deleted file mode 100644 index c175cda..0000000 --- a/samples/AspNet5_0/Startup.cs +++ /dev/null @@ -1,83 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Panner.AspNetCore.Samples.AspNet5_0.EFModel; -using Panner.AspNetCore.Samples.AspNet5_0.PannerExtensions; -using Panner.Builders; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Panner.AspNetCore.Samples.AspNet5_0 -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers(); - - services.UsePanner(c => - { - c.Entity() - .IsSortableByPopularity() - .Property(x => x.Id, o => o - .IsSortableAs(nameof(Views.Post.Id)) - .IsFilterableAs(nameof(Views.Post.Id)) - ) - .Property(x => x.Title, o => o - .IsSortableAs(nameof(Views.Post.Title)) - ) - .Property(x => x.CreatedOn, o => o - .IsSortableAs(nameof(Views.Post.Creation)) - .IsFilterableAs(nameof(Views.Post.Creation)) - ); - }); - - services.AddDbContext(options => - { - options.UseInMemoryDatabase("BlogDb"); - }); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseExceptionHandler("/Home/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); - } - app.UseHttpsRedirection(); - app.UseStaticFiles(); - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllerRoute( - name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}"); - }); - } - } -} diff --git a/samples/AspNet5_0/appsettings.Development.json b/samples/AspNet5_0/appsettings.Development.json deleted file mode 100644 index 8983e0f..0000000 --- a/samples/AspNet5_0/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} diff --git a/samples/AspNetCore3_1/AspNetCore3_1.csproj b/samples/AspNetCore3_1/AspNetCore3_1.csproj deleted file mode 100644 index 75ad524..0000000 --- a/samples/AspNetCore3_1/AspNetCore3_1.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - netcoreapp3.1 - Panner.AspNetCore.Samples.AspNetCore3_1 - Panner.AspNetCore.Samples.AspNetCore3_1 - false - - - - - - - - - - - - diff --git a/samples/AspNetCore3_1/Models/ErrorViewModel.cs b/samples/AspNetCore3_1/Models/ErrorViewModel.cs deleted file mode 100644 index 735eee0..0000000 --- a/samples/AspNetCore3_1/Models/ErrorViewModel.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Panner.AspNetCore.Samples.AspNetCore3_1.Models -{ - public class ErrorViewModel - { - public string RequestId { get; set; } - - public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - } -} diff --git a/samples/AspNetCore3_1/Program.cs b/samples/AspNetCore3_1/Program.cs deleted file mode 100644 index 0d509ad..0000000 --- a/samples/AspNetCore3_1/Program.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace Panner.AspNetCore.Samples.AspNetCore3_1 -{ - public class Program - { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); - } -} diff --git a/samples/AspNetCore3_1/Properties/launchSettings.json b/samples/AspNetCore3_1/Properties/launchSettings.json deleted file mode 100644 index 8678457..0000000 --- a/samples/AspNetCore3_1/Properties/launchSettings.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:41110", - "sslPort": 44361 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "AspNetCore3_1": { - "commandName": "Project", - "launchBrowser": true, - "applicationUrl": "https://localhost:5001;http://localhost:5000", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - } - } -} diff --git a/samples/AspNetCore3_1/Startup.cs b/samples/AspNetCore3_1/Startup.cs deleted file mode 100644 index ffef553..0000000 --- a/samples/AspNetCore3_1/Startup.cs +++ /dev/null @@ -1,83 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.HttpsPolicy; -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Panner.AspNetCore.Samples.AspNetCore3_1.EFModel; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Panner.Builders; -using Panner.AspNetCore.Samples.AspNetCore3_1.PannerExtensions; - -namespace Panner.AspNetCore.Samples.AspNetCore3_1 -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers(); - - services.UsePanner(c => - { - c.Entity() - .IsSortableByPopularity() - .Property(x => x.Id, o => o - .IsSortableAs(nameof(Views.Post.Id)) - .IsFilterableAs(nameof(Views.Post.Id)) - ) - .Property(x => x.Title, o => o - .IsSortableAs(nameof(Views.Post.Title)) - ) - .Property(x => x.CreatedOn, o => o - .IsSortableAs(nameof(Views.Post.Creation)) - .IsFilterableAs(nameof(Views.Post.Creation)) - ); - }); - - services.AddDbContext(options => - { - options.UseInMemoryDatabase("BlogDb"); - }); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - else - { - app.UseExceptionHandler("/Home/Error"); - // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. - app.UseHsts(); - } - app.UseHttpsRedirection(); - app.UseStaticFiles(); - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllerRoute( - name: "default", - pattern: "{controller=Home}/{action=Index}/{id?}"); - }); - } - } -} diff --git a/samples/AspNetCore3_1/appsettings.Development.json b/samples/AspNetCore3_1/appsettings.Development.json deleted file mode 100644 index 8983e0f..0000000 --- a/samples/AspNetCore3_1/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} diff --git a/samples/AspNetCore3_1/EFModel/BlogContext.cs b/samples/WebAPINet8MinimalFluent/EFModel/BlogContext.cs similarity index 94% rename from samples/AspNetCore3_1/EFModel/BlogContext.cs rename to samples/WebAPINet8MinimalFluent/EFModel/BlogContext.cs index b92ad6f..c1d1b8e 100644 --- a/samples/AspNetCore3_1/EFModel/BlogContext.cs +++ b/samples/WebAPINet8MinimalFluent/EFModel/BlogContext.cs @@ -1,4 +1,4 @@ -namespace Panner.AspNetCore.Samples.AspNetCore3_1.EFModel +namespace Panner.AspNetCore.Samples.WebApiNet8MinimalFluent.EFModel { using Microsoft.EntityFrameworkCore; using System; diff --git a/samples/AspNet5_0/EFModel/Post.cs b/samples/WebAPINet8MinimalFluent/EFModel/Post.cs similarity index 83% rename from samples/AspNet5_0/EFModel/Post.cs rename to samples/WebAPINet8MinimalFluent/EFModel/Post.cs index 4674b54..bdc63c0 100644 --- a/samples/AspNet5_0/EFModel/Post.cs +++ b/samples/WebAPINet8MinimalFluent/EFModel/Post.cs @@ -1,4 +1,4 @@ -namespace Panner.AspNetCore.Samples.AspNet5_0.EFModel +namespace Panner.AspNetCore.Samples.WebApiNet8MinimalFluent.EFModel { using System; diff --git a/samples/AspNetCore3_1/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs b/samples/WebAPINet8MinimalFluent/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs similarity index 73% rename from samples/AspNetCore3_1/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs rename to samples/WebAPINet8MinimalFluent/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs index 7eaab68..c45b930 100644 --- a/samples/AspNetCore3_1/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs +++ b/samples/WebAPINet8MinimalFluent/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs @@ -1,6 +1,6 @@ -namespace Panner.AspNetCore.Samples.AspNetCore3_1.PannerExtensions +namespace Panner.AspNetCore.Samples.WebApiNet8MinimalFluent.PannerExtensions { - using global::Panner.AspNetCore.Samples.AspNetCore3_1.EFModel; + using global::Panner.AspNetCore.Samples.WebApiNet8MinimalFluent.EFModel; using global::Panner.Builders; public static partial class PEntityBuilderExtensions diff --git a/samples/AspNet5_0/PannerExtensions/SortPostByPopularityParticle.cs b/samples/WebAPINet8MinimalFluent/PannerExtensions/SortPostByPopularityParticle.cs similarity index 81% rename from samples/AspNet5_0/PannerExtensions/SortPostByPopularityParticle.cs rename to samples/WebAPINet8MinimalFluent/PannerExtensions/SortPostByPopularityParticle.cs index 9eda449..591e528 100644 --- a/samples/AspNet5_0/PannerExtensions/SortPostByPopularityParticle.cs +++ b/samples/WebAPINet8MinimalFluent/PannerExtensions/SortPostByPopularityParticle.cs @@ -1,6 +1,6 @@ -namespace Panner.AspNetCore.Samples.AspNet5_0.PannerExtensions +namespace Panner.AspNetCore.Samples.WebApiNet8MinimalFluent.PannerExtensions { - using global::Panner.AspNetCore.Samples.AspNet5_0.EFModel; + using global::Panner.AspNetCore.Samples.WebApiNet8MinimalFluent.EFModel; using System.Linq; public class SortPostByPopularityParticle : ISortParticle diff --git a/samples/AspNet5_0/PannerExtensions/SortPostsByPopularityParticleGenerator.cs b/samples/WebAPINet8MinimalFluent/PannerExtensions/SortPostsByPopularityParticleGenerator.cs similarity index 81% rename from samples/AspNet5_0/PannerExtensions/SortPostsByPopularityParticleGenerator.cs rename to samples/WebAPINet8MinimalFluent/PannerExtensions/SortPostsByPopularityParticleGenerator.cs index 5a163a6..945a8b7 100644 --- a/samples/AspNet5_0/PannerExtensions/SortPostsByPopularityParticleGenerator.cs +++ b/samples/WebAPINet8MinimalFluent/PannerExtensions/SortPostsByPopularityParticleGenerator.cs @@ -1,6 +1,6 @@ -using Panner.AspNetCore.Samples.AspNet5_0.EFModel; +using Panner.AspNetCore.Samples.WebApiNet8MinimalFluent.EFModel; -namespace Panner.AspNetCore.Samples.AspNet5_0.PannerExtensions +namespace Panner.AspNetCore.Samples.WebApiNet8MinimalFluent.PannerExtensions { public class SortPostsByPopularityParticleGenerator : ISortParticleGenerator { diff --git a/samples/WebAPINet8MinimalFluent/Program.cs b/samples/WebAPINet8MinimalFluent/Program.cs new file mode 100644 index 0000000..0efff1a --- /dev/null +++ b/samples/WebAPINet8MinimalFluent/Program.cs @@ -0,0 +1,67 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Panner; +using Panner.AspNetCore; +using Panner.AspNetCore.Samples.WebApiNet8MinimalFluent.EFModel; +using Panner.AspNetCore.Samples.WebApiNet8MinimalFluent.PannerExtensions; +using Views = Panner.AspNetCore.Samples.WebApiNet8MinimalFluent.Views; +using Panner.Builders; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.UsePanner(c => +{ + c.Entity() + .IsSortableByPopularity() + .Property(x => x.Id, o => o + .IsSortableAs(nameof(Views.Post.Id)) + .IsFilterableAs(nameof(Views.Post.Id)) + ) + .Property(x => x.Title, o => o + .IsSortableAs(nameof(Views.Post.Title)) + ) + .Property(x => x.CreatedOn, o => o + .IsSortableAs(nameof(Views.Post.Creation)) + .IsFilterableAs(nameof(Views.Post.Creation)) + ); +}); + +builder.Services.AddDbContext(options => +{ + options.UseInMemoryDatabase("BlogDb"); +}); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.MapGet("/posts", async ( + [FromServices] BlogContext blogContext, + SortParticlesMinimalBinder sorts, + FilterParticlesMinimalBinder filters) => +{ + blogContext.Database.EnsureCreated(); + var result = await blogContext.Posts + .Apply(filters) + .Apply(sorts) + .Select(x => new Views.Post + { + Id = x.Id, + Title = x.Title, + Content = x.Content, + Creation = x.CreatedOn + }) + .ToArrayAsync(); + return Results.Ok(result); +}).WithOpenApi(); + +app.Run(); diff --git a/samples/WebAPINet8MinimalFluent/Properties/launchSettings.json b/samples/WebAPINet8MinimalFluent/Properties/launchSettings.json new file mode 100644 index 0000000..f733bab --- /dev/null +++ b/samples/WebAPINet8MinimalFluent/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:8658", + "sslPort": 44331 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5277", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7160;http://localhost:5277", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/AspNet5_0/Views/Post.cs b/samples/WebAPINet8MinimalFluent/Views/Post.cs similarity index 76% rename from samples/AspNet5_0/Views/Post.cs rename to samples/WebAPINet8MinimalFluent/Views/Post.cs index 96cf581..b216412 100644 --- a/samples/AspNet5_0/Views/Post.cs +++ b/samples/WebAPINet8MinimalFluent/Views/Post.cs @@ -1,4 +1,4 @@ -namespace Panner.AspNetCore.Samples.AspNet5_0.Views +namespace Panner.AspNetCore.Samples.WebApiNet8MinimalFluent.Views { using System; diff --git a/samples/WebAPINet8MinimalFluent/WebAPINet8MinimalFluent.csproj b/samples/WebAPINet8MinimalFluent/WebAPINet8MinimalFluent.csproj new file mode 100644 index 0000000..812e7ee --- /dev/null +++ b/samples/WebAPINet8MinimalFluent/WebAPINet8MinimalFluent.csproj @@ -0,0 +1,23 @@ + + + + net8.0 + enable + enable + Panner.AspNetCore.Samples.WebApiNet8MinimalFluent + Panner.AspNetCore.Samples.WebApiNet8MinimalFluent + false + + + + + + + + + + + + + + diff --git a/samples/WebAPINet8MinimalFluent/WebAPINet8MinimalFluent.http b/samples/WebAPINet8MinimalFluent/WebAPINet8MinimalFluent.http new file mode 100644 index 0000000..9562c0d --- /dev/null +++ b/samples/WebAPINet8MinimalFluent/WebAPINet8MinimalFluent.http @@ -0,0 +1,6 @@ +@WebAPINet8MinimalFluent_HostAddress = http://localhost:5277 + +GET {{WebAPINet8MinimalFluent_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/samples/WebAPINet8MinimalFluent/appsettings.Development.json b/samples/WebAPINet8MinimalFluent/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/samples/WebAPINet8MinimalFluent/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/AspNetCore3_1/appsettings.json b/samples/WebAPINet8MinimalFluent/appsettings.json similarity index 56% rename from samples/AspNetCore3_1/appsettings.json rename to samples/WebAPINet8MinimalFluent/appsettings.json index d9d9a9b..10f68b8 100644 --- a/samples/AspNetCore3_1/appsettings.json +++ b/samples/WebAPINet8MinimalFluent/appsettings.json @@ -2,8 +2,7 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" diff --git a/samples/AspNet5_0/Controllers/PostsController.cs b/samples/WebApi8ControllersFluent/Controllers/PostsController.cs similarity index 91% rename from samples/AspNet5_0/Controllers/PostsController.cs rename to samples/WebApi8ControllersFluent/Controllers/PostsController.cs index d0aa944..cf7c3dd 100644 --- a/samples/AspNet5_0/Controllers/PostsController.cs +++ b/samples/WebApi8ControllersFluent/Controllers/PostsController.cs @@ -1,9 +1,9 @@ -namespace Panner.AspNetCore.Samples.AspNet5_0.Controllers +namespace Panner.AspNetCore.Samples.WebApi8ControllersFluent.Controllers { using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; - using Panner.AspNetCore.Samples.AspNet5_0.EFModel; + using Panner.AspNetCore.Samples.WebApi8ControllersFluent.EFModel; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/samples/WebApi8ControllersFluent/EFModel/BlogContext.cs b/samples/WebApi8ControllersFluent/EFModel/BlogContext.cs new file mode 100644 index 0000000..a27f4e5 --- /dev/null +++ b/samples/WebApi8ControllersFluent/EFModel/BlogContext.cs @@ -0,0 +1,47 @@ +namespace Panner.AspNetCore.Samples.WebApi8ControllersFluent.EFModel +{ + using Microsoft.EntityFrameworkCore; + using System; + + public class BlogContext : DbContext + { + public DbSet Posts { get; set; } + + public BlogContext() : base() + { + } + + public BlogContext(DbContextOptions options) : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasData(new Post() + { + Id = 1, + Title = Guid.NewGuid().ToString(), + Content = Guid.NewGuid().ToString(), + CreatedOn = DateTime.UtcNow, + IsVisible = true + }, new Post() + { + Id = 2, + Title = Guid.NewGuid().ToString(), + Content = Guid.NewGuid().ToString(), + CreatedOn = DateTime.UtcNow, + IsVisible = false + }, new Post() + { + Id = 3, + Title = Guid.NewGuid().ToString(), + Content = Guid.NewGuid().ToString(), + CreatedOn = DateTime.UtcNow, + IsVisible = true + }); + } + } +} diff --git a/samples/WebApi8ControllersFluent/EFModel/Post.cs b/samples/WebApi8ControllersFluent/EFModel/Post.cs new file mode 100644 index 0000000..f237d0d --- /dev/null +++ b/samples/WebApi8ControllersFluent/EFModel/Post.cs @@ -0,0 +1,15 @@ +namespace Panner.AspNetCore.Samples.WebApi8ControllersFluent.EFModel +{ + using System; + + public class Post + { + public int Id { get; set; } + public string Title { get; set; } + public string Content { get; set; } + public bool IsVisible { get; set; } + public int AmtLikes { get; set; } + public int AmtComments { get; set; } + public DateTime CreatedOn { get; set; } + } +} diff --git a/samples/WebApi8ControllersFluent/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs b/samples/WebApi8ControllersFluent/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs new file mode 100644 index 0000000..c73cb21 --- /dev/null +++ b/samples/WebApi8ControllersFluent/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs @@ -0,0 +1,15 @@ +namespace Panner.AspNetCore.Samples.WebApi8ControllersFluent.PannerExtensions +{ + using global::Panner.AspNetCore.Samples.WebApi8ControllersFluent.EFModel; + using global::Panner.Builders; + + public static partial class PEntityBuilderExtensions + { + /// Marks the entity as sortable by popularity. + public static PEntityBuilder IsSortableByPopularity(this PEntityBuilder builder) + { + builder.GetOrCreateGenerator, SortPostsByPopularityParticleGenerator>(); + return builder; + } + } +} diff --git a/samples/WebApi8ControllersFluent/PannerExtensions/SortPostByPopularityParticle.cs b/samples/WebApi8ControllersFluent/PannerExtensions/SortPostByPopularityParticle.cs new file mode 100644 index 0000000..4a683aa --- /dev/null +++ b/samples/WebApi8ControllersFluent/PannerExtensions/SortPostByPopularityParticle.cs @@ -0,0 +1,27 @@ +namespace Panner.AspNetCore.Samples.WebApi8ControllersFluent.PannerExtensions +{ + using global::Panner.AspNetCore.Samples.WebApi8ControllersFluent.EFModel; + using System.Linq; + + public class SortPostByPopularityParticle : ISortParticle + { + readonly bool Descending; + + public SortPostByPopularityParticle(bool descending) + { + this.Descending = descending; + } + + public IOrderedQueryable ApplyTo(IOrderedQueryable source) + { + if (this.Descending) + return source + .ThenByDescending(x => x.AmtLikes) + .ThenByDescending(x => x.AmtComments); + else + return source + .ThenBy(x => x.AmtLikes) + .ThenBy(x => x.AmtComments); + } + } +} diff --git a/samples/WebApi8ControllersFluent/PannerExtensions/SortPostsByPopularityParticleGenerator.cs b/samples/WebApi8ControllersFluent/PannerExtensions/SortPostsByPopularityParticleGenerator.cs new file mode 100644 index 0000000..f454d2c --- /dev/null +++ b/samples/WebApi8ControllersFluent/PannerExtensions/SortPostsByPopularityParticleGenerator.cs @@ -0,0 +1,22 @@ +using Panner.AspNetCore.Samples.WebApi8ControllersFluent.EFModel; + +namespace Panner.AspNetCore.Samples.WebApi8ControllersFluent.PannerExtensions +{ + public class SortPostsByPopularityParticleGenerator : ISortParticleGenerator + { + public bool TryGenerate(IPContext context, string input, out ISortParticle particle) + { + var descending = input.StartsWith('-'); + var remaining = descending ? input.Substring(1) : input; + + if (!remaining.Trim().Equals("Popularity", System.StringComparison.OrdinalIgnoreCase)) + { + particle = null; + return false; + } + + particle = new SortPostByPopularityParticle(descending); + return true; + } + } +} diff --git a/samples/WebApi8ControllersFluent/Program.cs b/samples/WebApi8ControllersFluent/Program.cs new file mode 100644 index 0000000..5b06885 --- /dev/null +++ b/samples/WebApi8ControllersFluent/Program.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore; +using Panner.AspNetCore; +using Panner.AspNetCore.Samples.WebApi8ControllersFluent.EFModel; +using Panner.AspNetCore.Samples.WebApi8ControllersFluent.PannerExtensions; +using Views = Panner.AspNetCore.Samples.WebApi8ControllersFluent.Views; +using Panner.Builders; + + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.UsePanner(c => +{ + c.Entity() + .IsSortableByPopularity() + .Property(x => x.Id, o => o + .IsSortableAs(nameof(Views.Post.Id)) + .IsFilterableAs(nameof(Views.Post.Id)) + ) + .Property(x => x.Title, o => o + .IsSortableAs(nameof(Views.Post.Title)) + ) + .Property(x => x.CreatedOn, o => o + .IsSortableAs(nameof(Views.Post.Creation)) + .IsFilterableAs(nameof(Views.Post.Creation)) + ); +}); + +builder.Services.AddDbContext(options => +{ + options.UseInMemoryDatabase("BlogDb"); +}); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.UseAuthorization(); +app.MapControllers(); + +app.Run(); diff --git a/samples/WebApi8ControllersFluent/Properties/launchSettings.json b/samples/WebApi8ControllersFluent/Properties/launchSettings.json new file mode 100644 index 0000000..1f65c51 --- /dev/null +++ b/samples/WebApi8ControllersFluent/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:34778", + "sslPort": 44332 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5274", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7209;http://localhost:5274", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/WebApi8ControllersFluent/Views/Post.cs b/samples/WebApi8ControllersFluent/Views/Post.cs new file mode 100644 index 0000000..7725396 --- /dev/null +++ b/samples/WebApi8ControllersFluent/Views/Post.cs @@ -0,0 +1,12 @@ +namespace Panner.AspNetCore.Samples.WebApi8ControllersFluent.Views +{ + using System; + + public class Post + { + public int Id { get; set; } + public string Title { get; set; } + public string Content { get; set; } + public DateTime Creation { get; set; } + } +} diff --git a/samples/WebApi8ControllersFluent/WebApi8ControllersFluent.csproj b/samples/WebApi8ControllersFluent/WebApi8ControllersFluent.csproj new file mode 100644 index 0000000..d4aeee2 --- /dev/null +++ b/samples/WebApi8ControllersFluent/WebApi8ControllersFluent.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/samples/WebApi8ControllersFluent/WebApi8ControllersFluent.http b/samples/WebApi8ControllersFluent/WebApi8ControllersFluent.http new file mode 100644 index 0000000..eb08849 --- /dev/null +++ b/samples/WebApi8ControllersFluent/WebApi8ControllersFluent.http @@ -0,0 +1,6 @@ +@WebApi8ControllersFluent_HostAddress = http://localhost:5274 + +GET {{WebApi8ControllersFluent_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/samples/WebApi8ControllersFluent/appsettings.Development.json b/samples/WebApi8ControllersFluent/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/samples/WebApi8ControllersFluent/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/AspNet5_0/appsettings.json b/samples/WebApi8ControllersFluent/appsettings.json similarity index 56% rename from samples/AspNet5_0/appsettings.json rename to samples/WebApi8ControllersFluent/appsettings.json index d9d9a9b..10f68b8 100644 --- a/samples/AspNet5_0/appsettings.json +++ b/samples/WebApi8ControllersFluent/appsettings.json @@ -2,8 +2,7 @@ "Logging": { "LogLevel": { "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.AspNetCore": "Warning" } }, "AllowedHosts": "*" diff --git a/samples/AspNetCore3_1/Controllers/PostsController.cs b/samples/WebApiNet9ControllersFluent/Controllers/PostsController.cs similarity index 90% rename from samples/AspNetCore3_1/Controllers/PostsController.cs rename to samples/WebApiNet9ControllersFluent/Controllers/PostsController.cs index 46e9670..c5be6b3 100644 --- a/samples/AspNetCore3_1/Controllers/PostsController.cs +++ b/samples/WebApiNet9ControllersFluent/Controllers/PostsController.cs @@ -1,9 +1,9 @@ -namespace Panner.AspNetCore.Samples.AspNetCore3_1.Controllers +namespace Panner.AspNetCore.Samples.WebApiNet9ControllersFluent.Controllers { using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; - using Panner.AspNetCore.Samples.AspNetCore3_1.EFModel; + using Panner.AspNetCore.Samples.WebApiNet9ControllersFluent.EFModel; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; diff --git a/samples/WebApiNet9ControllersFluent/EFModel/BlogContext.cs b/samples/WebApiNet9ControllersFluent/EFModel/BlogContext.cs new file mode 100644 index 0000000..ae32a42 --- /dev/null +++ b/samples/WebApiNet9ControllersFluent/EFModel/BlogContext.cs @@ -0,0 +1,47 @@ +namespace Panner.AspNetCore.Samples.WebApiNet9ControllersFluent.EFModel +{ + using Microsoft.EntityFrameworkCore; + using System; + + public class BlogContext : DbContext + { + public DbSet Posts { get; set; } + + public BlogContext() : base() + { + } + + public BlogContext(DbContextOptions options) : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasData(new Post() + { + Id = 1, + Title = Guid.NewGuid().ToString(), + Content = Guid.NewGuid().ToString(), + CreatedOn = DateTime.UtcNow, + IsVisible = true + }, new Post() + { + Id = 2, + Title = Guid.NewGuid().ToString(), + Content = Guid.NewGuid().ToString(), + CreatedOn = DateTime.UtcNow, + IsVisible = false + }, new Post() + { + Id = 3, + Title = Guid.NewGuid().ToString(), + Content = Guid.NewGuid().ToString(), + CreatedOn = DateTime.UtcNow, + IsVisible = true + }); + } + } +} diff --git a/samples/WebApiNet9ControllersFluent/EFModel/Post.cs b/samples/WebApiNet9ControllersFluent/EFModel/Post.cs new file mode 100644 index 0000000..e22de4a --- /dev/null +++ b/samples/WebApiNet9ControllersFluent/EFModel/Post.cs @@ -0,0 +1,15 @@ +namespace Panner.AspNetCore.Samples.WebApiNet9ControllersFluent.EFModel +{ + using System; + + public class Post + { + public int Id { get; set; } + public string Title { get; set; } + public string Content { get; set; } + public bool IsVisible { get; set; } + public int AmtLikes { get; set; } + public int AmtComments { get; set; } + public DateTime CreatedOn { get; set; } + } +} diff --git a/samples/WebApiNet9ControllersFluent/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs b/samples/WebApiNet9ControllersFluent/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs new file mode 100644 index 0000000..3e3a3ff --- /dev/null +++ b/samples/WebApiNet9ControllersFluent/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs @@ -0,0 +1,15 @@ +namespace Panner.AspNetCore.Samples.WebApiNet9ControllersFluent.PannerExtensions +{ + using global::Panner.AspNetCore.Samples.WebApiNet9ControllersFluent.EFModel; + using global::Panner.Builders; + + public static partial class PEntityBuilderExtensions + { + /// Marks the entity as sortable by popularity. + public static PEntityBuilder IsSortableByPopularity(this PEntityBuilder builder) + { + builder.GetOrCreateGenerator, SortPostsByPopularityParticleGenerator>(); + return builder; + } + } +} diff --git a/samples/WebApiNet9ControllersFluent/PannerExtensions/SortPostByPopularityParticle.cs b/samples/WebApiNet9ControllersFluent/PannerExtensions/SortPostByPopularityParticle.cs new file mode 100644 index 0000000..6774749 --- /dev/null +++ b/samples/WebApiNet9ControllersFluent/PannerExtensions/SortPostByPopularityParticle.cs @@ -0,0 +1,27 @@ +namespace Panner.AspNetCore.Samples.WebApiNet9ControllersFluent.PannerExtensions +{ + using global::Panner.AspNetCore.Samples.WebApiNet9ControllersFluent.EFModel; + using System.Linq; + + public class SortPostByPopularityParticle : ISortParticle + { + readonly bool Descending; + + public SortPostByPopularityParticle(bool descending) + { + this.Descending = descending; + } + + public IOrderedQueryable ApplyTo(IOrderedQueryable source) + { + if (this.Descending) + return source + .ThenByDescending(x => x.AmtLikes) + .ThenByDescending(x => x.AmtComments); + else + return source + .ThenBy(x => x.AmtLikes) + .ThenBy(x => x.AmtComments); + } + } +} diff --git a/samples/WebApiNet9ControllersFluent/PannerExtensions/SortPostsByPopularityParticleGenerator.cs b/samples/WebApiNet9ControllersFluent/PannerExtensions/SortPostsByPopularityParticleGenerator.cs new file mode 100644 index 0000000..ef7aa91 --- /dev/null +++ b/samples/WebApiNet9ControllersFluent/PannerExtensions/SortPostsByPopularityParticleGenerator.cs @@ -0,0 +1,22 @@ +using Panner.AspNetCore.Samples.WebApiNet9ControllersFluent.EFModel; + +namespace Panner.AspNetCore.Samples.WebApiNet9ControllersFluent.PannerExtensions +{ + public class SortPostsByPopularityParticleGenerator : ISortParticleGenerator + { + public bool TryGenerate(IPContext context, string input, out ISortParticle particle) + { + var descending = input.StartsWith('-'); + var remaining = descending ? input.Substring(1) : input; + + if (!remaining.Trim().Equals("Popularity", System.StringComparison.OrdinalIgnoreCase)) + { + particle = null; + return false; + } + + particle = new SortPostByPopularityParticle(descending); + return true; + } + } +} diff --git a/samples/WebApiNet9ControllersFluent/Program.cs b/samples/WebApiNet9ControllersFluent/Program.cs new file mode 100644 index 0000000..6eb0e70 --- /dev/null +++ b/samples/WebApiNet9ControllersFluent/Program.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore; +using Panner.AspNetCore; +using Panner.AspNetCore.Samples.WebApiNet9ControllersFluent.EFModel; +using Panner.AspNetCore.Samples.WebApiNet9ControllersFluent.PannerExtensions; +using Views = Panner.AspNetCore.Samples.WebApiNet9ControllersFluent.Views; +using Panner.Builders; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddOpenApi(); + +builder.Services.UsePanner(c => +{ + c.Entity() + .IsSortableByPopularity() + .Property(x => x.Id, o => o + .IsSortableAs(nameof(Views.Post.Id)) + .IsFilterableAs(nameof(Views.Post.Id)) + ) + .Property(x => x.Title, o => o + .IsSortableAs(nameof(Views.Post.Title)) + ) + .Property(x => x.CreatedOn, o => o + .IsSortableAs(nameof(Views.Post.Creation)) + .IsFilterableAs(nameof(Views.Post.Creation)) + ); +}); + +builder.Services.AddDbContext(options => +{ + options.UseInMemoryDatabase("BlogDb"); +}); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/samples/WebApiNet9ControllersFluent/Properties/launchSettings.json b/samples/WebApiNet9ControllersFluent/Properties/launchSettings.json new file mode 100644 index 0000000..b2b4448 --- /dev/null +++ b/samples/WebApiNet9ControllersFluent/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5170", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7140;http://localhost:5170", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/WebApiNet9ControllersFluent/Views/Post.cs b/samples/WebApiNet9ControllersFluent/Views/Post.cs new file mode 100644 index 0000000..ffd878b --- /dev/null +++ b/samples/WebApiNet9ControllersFluent/Views/Post.cs @@ -0,0 +1,12 @@ +namespace Panner.AspNetCore.Samples.WebApiNet9ControllersFluent.Views +{ + using System; + + public class Post + { + public int Id { get; set; } + public string Title { get; set; } + public string Content { get; set; } + public DateTime Creation { get; set; } + } +} diff --git a/samples/WebApiNet9ControllersFluent/WeatherForecast.cs b/samples/WebApiNet9ControllersFluent/WeatherForecast.cs new file mode 100644 index 0000000..5cf7dd6 --- /dev/null +++ b/samples/WebApiNet9ControllersFluent/WeatherForecast.cs @@ -0,0 +1,12 @@ +namespace WebApiNet9ControllersFluent; + +public class WeatherForecast +{ + public DateOnly Date { get; set; } + + public int TemperatureC { get; set; } + + public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); + + public string? Summary { get; set; } +} \ No newline at end of file diff --git a/samples/WebApiNet9ControllersFluent/WebApiNet9ControllersFluent.csproj b/samples/WebApiNet9ControllersFluent/WebApiNet9ControllersFluent.csproj new file mode 100644 index 0000000..990755f --- /dev/null +++ b/samples/WebApiNet9ControllersFluent/WebApiNet9ControllersFluent.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + enable + enable + Panner.AspNetCore.Samples.WebApiNet9ControllersFluent + Panner.AspNetCore.Samples.WebApiNet9ControllersFluent + false + + + + + + + + + + + + + diff --git a/samples/WebApiNet9ControllersFluent/WebApiNet9ControllersFluent.http b/samples/WebApiNet9ControllersFluent/WebApiNet9ControllersFluent.http new file mode 100644 index 0000000..5825eff --- /dev/null +++ b/samples/WebApiNet9ControllersFluent/WebApiNet9ControllersFluent.http @@ -0,0 +1,6 @@ +@WebApiNet9ControllersFluent_HostAddress = http://localhost:5170 + +GET {{WebApiNet9ControllersFluent_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/samples/WebApiNet9ControllersFluent/appsettings.Development.json b/samples/WebApiNet9ControllersFluent/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/samples/WebApiNet9ControllersFluent/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/WebApiNet9ControllersFluent/appsettings.json b/samples/WebApiNet9ControllersFluent/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/samples/WebApiNet9ControllersFluent/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/WebApiNet9MinimalFluent/EFModel/BlogContext.cs b/samples/WebApiNet9MinimalFluent/EFModel/BlogContext.cs new file mode 100644 index 0000000..bb0fd7c --- /dev/null +++ b/samples/WebApiNet9MinimalFluent/EFModel/BlogContext.cs @@ -0,0 +1,47 @@ +namespace Panner.AspNetCore.Samples.WebApiNet9MinimalFluent.EFModel +{ + using Microsoft.EntityFrameworkCore; + using System; + + public class BlogContext : DbContext + { + public DbSet Posts { get; set; } + + public BlogContext() : base() + { + } + + public BlogContext(DbContextOptions options) : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity() + .HasData(new Post() + { + Id = 1, + Title = Guid.NewGuid().ToString(), + Content = Guid.NewGuid().ToString(), + CreatedOn = DateTime.UtcNow, + IsVisible = true + }, new Post() + { + Id = 2, + Title = Guid.NewGuid().ToString(), + Content = Guid.NewGuid().ToString(), + CreatedOn = DateTime.UtcNow, + IsVisible = false + }, new Post() + { + Id = 3, + Title = Guid.NewGuid().ToString(), + Content = Guid.NewGuid().ToString(), + CreatedOn = DateTime.UtcNow, + IsVisible = true + }); + } + } +} diff --git a/samples/WebApiNet9MinimalFluent/EFModel/Post.cs b/samples/WebApiNet9MinimalFluent/EFModel/Post.cs new file mode 100644 index 0000000..9509e1e --- /dev/null +++ b/samples/WebApiNet9MinimalFluent/EFModel/Post.cs @@ -0,0 +1,15 @@ +namespace Panner.AspNetCore.Samples.WebApiNet9MinimalFluent.EFModel +{ + using System; + + public class Post + { + public int Id { get; set; } + public string Title { get; set; } + public string Content { get; set; } + public bool IsVisible { get; set; } + public int AmtLikes { get; set; } + public int AmtComments { get; set; } + public DateTime CreatedOn { get; set; } + } +} diff --git a/samples/WebApiNet9MinimalFluent/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs b/samples/WebApiNet9MinimalFluent/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs new file mode 100644 index 0000000..d789560 --- /dev/null +++ b/samples/WebApiNet9MinimalFluent/PannerExtensions/PEntityBuilder.Post.IsSortableByPopularity.cs @@ -0,0 +1,15 @@ +namespace Panner.AspNetCore.Samples.WebApiNet9MinimalFluent.PannerExtensions +{ + using global::Panner.AspNetCore.Samples.WebApiNet9MinimalFluent.EFModel; + using global::Panner.Builders; + + public static partial class PEntityBuilderExtensions + { + /// Marks the entity as sortable by popularity. + public static PEntityBuilder IsSortableByPopularity(this PEntityBuilder builder) + { + builder.GetOrCreateGenerator, SortPostsByPopularityParticleGenerator>(); + return builder; + } + } +} diff --git a/samples/WebApiNet9MinimalFluent/PannerExtensions/SortPostByPopularityParticle.cs b/samples/WebApiNet9MinimalFluent/PannerExtensions/SortPostByPopularityParticle.cs new file mode 100644 index 0000000..1c1194e --- /dev/null +++ b/samples/WebApiNet9MinimalFluent/PannerExtensions/SortPostByPopularityParticle.cs @@ -0,0 +1,27 @@ +namespace Panner.AspNetCore.Samples.WebApiNet9MinimalFluent.PannerExtensions +{ + using global::Panner.AspNetCore.Samples.WebApiNet9MinimalFluent.EFModel; + using System.Linq; + + public class SortPostByPopularityParticle : ISortParticle + { + readonly bool Descending; + + public SortPostByPopularityParticle(bool descending) + { + this.Descending = descending; + } + + public IOrderedQueryable ApplyTo(IOrderedQueryable source) + { + if (this.Descending) + return source + .ThenByDescending(x => x.AmtLikes) + .ThenByDescending(x => x.AmtComments); + else + return source + .ThenBy(x => x.AmtLikes) + .ThenBy(x => x.AmtComments); + } + } +} diff --git a/samples/WebApiNet9MinimalFluent/PannerExtensions/SortPostsByPopularityParticleGenerator.cs b/samples/WebApiNet9MinimalFluent/PannerExtensions/SortPostsByPopularityParticleGenerator.cs new file mode 100644 index 0000000..a1e3f94 --- /dev/null +++ b/samples/WebApiNet9MinimalFluent/PannerExtensions/SortPostsByPopularityParticleGenerator.cs @@ -0,0 +1,22 @@ +using Panner.AspNetCore.Samples.WebApiNet9MinimalFluent.EFModel; + +namespace Panner.AspNetCore.Samples.WebApiNet9MinimalFluent.PannerExtensions +{ + public class SortPostsByPopularityParticleGenerator : ISortParticleGenerator + { + public bool TryGenerate(IPContext context, string input, out ISortParticle particle) + { + var descending = input.StartsWith('-'); + var remaining = descending ? input.Substring(1) : input; + + if (!remaining.Trim().Equals("Popularity", System.StringComparison.OrdinalIgnoreCase)) + { + particle = null; + return false; + } + + particle = new SortPostByPopularityParticle(descending); + return true; + } + } +} diff --git a/samples/WebApiNet9MinimalFluent/Program.cs b/samples/WebApiNet9MinimalFluent/Program.cs new file mode 100644 index 0000000..9183ec1 --- /dev/null +++ b/samples/WebApiNet9MinimalFluent/Program.cs @@ -0,0 +1,66 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Panner; +using Panner.AspNetCore; +using Panner.AspNetCore.Samples.WebApiNet9MinimalFluent.EFModel; +using Panner.AspNetCore.Samples.WebApiNet9MinimalFluent.PannerExtensions; +using Views = Panner.AspNetCore.Samples.WebApiNet9MinimalFluent.Views; +using Panner.Builders; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddOpenApi(); + +builder.Services.UsePanner(c => +{ + c.Entity() + .IsSortableByPopularity() + .Property(x => x.Id, o => o + .IsSortableAs(nameof(Views.Post.Id)) + .IsFilterableAs(nameof(Views.Post.Id)) + ) + .Property(x => x.Title, o => o + .IsSortableAs(nameof(Views.Post.Title)) + ) + .Property(x => x.CreatedOn, o => o + .IsSortableAs(nameof(Views.Post.Creation)) + .IsFilterableAs(nameof(Views.Post.Creation)) + ); +}); + +builder.Services.AddDbContext(options => +{ + options.UseInMemoryDatabase("BlogDb"); +}); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.MapOpenApi(); +} + +app.UseHttpsRedirection(); + +app.MapGet("/posts", async ( + [FromServices] BlogContext blogContext, + SortParticlesMinimalBinder sorts, + FilterParticlesMinimalBinder filters +) => +{ + blogContext.Database.EnsureCreated(); + var result = await blogContext.Posts + .Apply(filters) + .Apply(sorts) + .Select(x => new Views.Post + { + Id = x.Id, + Title = x.Title, + Content = x.Content, + Creation = x.CreatedOn + }) + .ToArrayAsync(); + return Results.Ok(result); +}); + +app.Run(); diff --git a/samples/WebApiNet9MinimalFluent/Properties/launchSettings.json b/samples/WebApiNet9MinimalFluent/Properties/launchSettings.json new file mode 100644 index 0000000..024e3d4 --- /dev/null +++ b/samples/WebApiNet9MinimalFluent/Properties/launchSettings.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5254", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "https://localhost:7104;http://localhost:5254", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/samples/WebApiNet9MinimalFluent/Views/Post.cs b/samples/WebApiNet9MinimalFluent/Views/Post.cs new file mode 100644 index 0000000..b6a283c --- /dev/null +++ b/samples/WebApiNet9MinimalFluent/Views/Post.cs @@ -0,0 +1,12 @@ +namespace Panner.AspNetCore.Samples.WebApiNet9MinimalFluent.Views +{ + using System; + + public class Post + { + public int Id { get; set; } + public string Title { get; set; } + public string Content { get; set; } + public DateTime Creation { get; set; } + } +} diff --git a/samples/WebApiNet9MinimalFluent/WebApiNet9.http b/samples/WebApiNet9MinimalFluent/WebApiNet9.http new file mode 100644 index 0000000..6f2eeb2 --- /dev/null +++ b/samples/WebApiNet9MinimalFluent/WebApiNet9.http @@ -0,0 +1,6 @@ +@WebApiNet9_HostAddress = http://localhost:5254 + +GET {{WebApiNet9_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/samples/WebApiNet9MinimalFluent/WebApiNet9MinimalFluent.csproj b/samples/WebApiNet9MinimalFluent/WebApiNet9MinimalFluent.csproj new file mode 100644 index 0000000..f8462fa --- /dev/null +++ b/samples/WebApiNet9MinimalFluent/WebApiNet9MinimalFluent.csproj @@ -0,0 +1,22 @@ + + + + net9.0 + enable + enable + Panner.AspNetCore.Samples.WebApiNet9MinimalFluent + Panner.AspNetCore.Samples.WebApiNet9MinimalFluent + false + + + + + + + + + + + + + diff --git a/samples/WebApiNet9MinimalFluent/appsettings.Development.json b/samples/WebApiNet9MinimalFluent/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/samples/WebApiNet9MinimalFluent/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/samples/WebApiNet9MinimalFluent/appsettings.json b/samples/WebApiNet9MinimalFluent/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/samples/WebApiNet9MinimalFluent/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..2f4a93d --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,2 @@ +# Exclude samples from code duplication. We want samples to be self-contained so they might duplicate eachother. +sonar.cpd.exclusions=samples/** diff --git a/src/Binders/FilterParticlesMinimalBinder.cs b/src/Binders/FilterParticlesMinimalBinder.cs new file mode 100644 index 0000000..4233b76 --- /dev/null +++ b/src/Binders/FilterParticlesMinimalBinder.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Panner.AspNetCore +{ + public sealed class FilterParticlesMinimalBinder : IReadOnlyCollection> + where TEntity : class + { + private readonly IReadOnlyCollection> _particles; + + private FilterParticlesMinimalBinder(IReadOnlyCollection> particles) + { + _particles = particles; + } + + public static ValueTask> BindAsync(HttpContext context, ParameterInfo parameter) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + if (parameter == null) + { + throw new ArgumentNullException(nameof(parameter)); + } + + var queryName = parameter.Name ?? "filters"; + StringValues value = context.Request.Query[queryName]; + + var pContext = context.RequestServices.GetRequiredService(); + + if (StringValues.IsNullOrEmpty(value)) + { + return ValueTask.FromResult(new FilterParticlesMinimalBinder(Array.Empty>())); + } + + if (!pContext.TryParseCsv(value.ToString(), out IEnumerable> particles)) + { + throw new InvalidOperationException("Could not parse provided filters."); + } + + return ValueTask.FromResult(new FilterParticlesMinimalBinder(particles.ToArray())); + } + + public int Count => _particles.Count; + public IEnumerator> GetEnumerator() => _particles.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public IReadOnlyCollection> Value => _particles; + } +} diff --git a/src/Binders/SortParticlesMinimalBinder.cs b/src/Binders/SortParticlesMinimalBinder.cs new file mode 100644 index 0000000..4b25e98 --- /dev/null +++ b/src/Binders/SortParticlesMinimalBinder.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Primitives; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Panner.AspNetCore +{ + public sealed class SortParticlesMinimalBinder : IReadOnlyCollection> + where TEntity : class + { + private readonly IReadOnlyCollection> _particles; + + private SortParticlesMinimalBinder(IReadOnlyCollection> particles) + { + _particles = particles; + } + + public static ValueTask> BindAsync(HttpContext context, ParameterInfo parameter) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + if (parameter == null) + { + throw new ArgumentNullException(nameof(parameter)); + } + + var queryName = parameter.Name ?? "sorts"; + StringValues value = context.Request.Query[queryName]; + + var pContext = context.RequestServices.GetRequiredService(); + + if (StringValues.IsNullOrEmpty(value)) + { + return ValueTask.FromResult(new SortParticlesMinimalBinder(Array.Empty>())); + } + + if (!pContext.TryParseCsv(value.ToString(), out IEnumerable> particles)) + { + throw new InvalidOperationException("Could not parse provided sorts."); + } + + return ValueTask.FromResult(new SortParticlesMinimalBinder(particles.ToArray())); + } + + public int Count => _particles.Count; + public IEnumerator> GetEnumerator() => _particles.GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public IReadOnlyCollection> Value => _particles; + } +} diff --git a/src/Panner.AspNetCore.csproj b/src/Panner.AspNetCore.csproj index ddcc438..96eaa75 100644 --- a/src/Panner.AspNetCore.csproj +++ b/src/Panner.AspNetCore.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1;net5.0 + net8.0;net9.0 Panner.AspNetCore Panner.AspNetCore OSDKDev @@ -15,7 +15,7 @@ Copyright (c) 2019 OSDKDev Sorting and filtering made easy for your ASP.NET Core project! Go from a CSV input to a filtered/sorted IQueryable with no effort. true - 8.0 + 12.0 @@ -25,8 +25,8 @@ - - + + diff --git a/tests/Panner.AspNetCore.Tests/Panner.AspNetCore.Tests.csproj b/tests/Panner.AspNetCore.Tests/Panner.AspNetCore.Tests.csproj index 1371a55..a4404c5 100644 --- a/tests/Panner.AspNetCore.Tests/Panner.AspNetCore.Tests.csproj +++ b/tests/Panner.AspNetCore.Tests/Panner.AspNetCore.Tests.csproj @@ -1,18 +1,18 @@  - netcoreapp3.1;net5.0 - 8.0 + net8.0;net9.0 + 12.0 false - - - - - - + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Panner.AspNetCore.Tests/ParticlesModelBinderWorks.cs b/tests/Panner.AspNetCore.Tests/ParticlesModelBinderWorks.cs index bf420c0..561eb14 100644 --- a/tests/Panner.AspNetCore.Tests/ParticlesModelBinderWorks.cs +++ b/tests/Panner.AspNetCore.Tests/ParticlesModelBinderWorks.cs @@ -41,13 +41,13 @@ public void NullIPContextThrows() } [Fact] - public void NullBindingContextThrows() + public async Task NullBindingContextThrows() { var x = Create(GetPannerContext()); - Assert.Throws(() => + await Assert.ThrowsAsync(async () => { - x.BindModelAsync(null); + await x.BindModelAsync(null); }); }