From 1b5afc9b19e03b79f08e69f486e2e85d4e407b87 Mon Sep 17 00:00:00 2001 From: Aymen TROUDI Date: Sun, 17 Dec 2023 00:11:52 +0100 Subject: [PATCH] First commit ! --- .github/workflows/ci.yml | 28 +++ .gitignore | 1 + ModelBindingDemo.sln | 56 ++++++ README.md | 12 ++ global.json | 7 + src/Example01/Binders/BooleanModelBinder.cs | 39 +++++ src/Example01/Controllers/ApiController.cs | 100 +++++++++++ src/Example01/Example01.csproj | 17 ++ src/Example01/Payloads/ApiQuery.cs | 3 + src/Example01/Payloads/ApiRequest.cs | 3 + src/Example01/Payloads/ApiResponse.cs | 3 + src/Example01/Program.cs | 24 +++ src/Example01/Properties/launchSettings.json | 41 +++++ src/Example01/appsettings.Development.json | 8 + src/Example01/appsettings.json | 9 + src/Example02/Binders/BooleanModelBinder.cs | 34 ++++ src/Example02/Example02.csproj | 17 ++ src/Example02/Payloads/ApiQuery.cs | 3 + src/Example02/Payloads/ApiRequest.cs | 3 + src/Example02/Payloads/ApiResponse.cs | 3 + src/Example02/Program.cs | 105 ++++++++++++ src/Example02/Properties/launchSettings.json | 41 +++++ src/Example02/appsettings.Development.json | 8 + src/Example02/appsettings.json | 9 + test/Example01.Tests/Example01.Tests.csproj | 29 ++++ test/Example01.Tests/Example01.Tests.http | 55 ++++++ test/Example01.Tests/Extensions.cs | 10 ++ test/Example01.Tests/GlobalUsings.cs | 1 + test/Example01.Tests/IntegrationTests.cs | 171 +++++++++++++++++++ test/Example01.Tests/WebApiTestFixture.cs | 19 +++ test/Example02.Tests/Example02.Tests.csproj | 31 ++++ test/Example02.Tests/Example02.Tests.http | 55 ++++++ test/Example02.Tests/Extensions.cs | 10 ++ test/Example02.Tests/GlobalUsings.cs | 1 + test/Example02.Tests/IntegrationTests.cs | 171 +++++++++++++++++++ test/Example02.Tests/WebApiTestFixture.cs | 19 +++ 36 files changed, 1146 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 ModelBindingDemo.sln create mode 100644 global.json create mode 100644 src/Example01/Binders/BooleanModelBinder.cs create mode 100644 src/Example01/Controllers/ApiController.cs create mode 100644 src/Example01/Example01.csproj create mode 100644 src/Example01/Payloads/ApiQuery.cs create mode 100644 src/Example01/Payloads/ApiRequest.cs create mode 100644 src/Example01/Payloads/ApiResponse.cs create mode 100644 src/Example01/Program.cs create mode 100644 src/Example01/Properties/launchSettings.json create mode 100644 src/Example01/appsettings.Development.json create mode 100644 src/Example01/appsettings.json create mode 100644 src/Example02/Binders/BooleanModelBinder.cs create mode 100644 src/Example02/Example02.csproj create mode 100644 src/Example02/Payloads/ApiQuery.cs create mode 100644 src/Example02/Payloads/ApiRequest.cs create mode 100644 src/Example02/Payloads/ApiResponse.cs create mode 100644 src/Example02/Program.cs create mode 100644 src/Example02/Properties/launchSettings.json create mode 100644 src/Example02/appsettings.Development.json create mode 100644 src/Example02/appsettings.json create mode 100644 test/Example01.Tests/Example01.Tests.csproj create mode 100644 test/Example01.Tests/Example01.Tests.http create mode 100644 test/Example01.Tests/Extensions.cs create mode 100644 test/Example01.Tests/GlobalUsings.cs create mode 100644 test/Example01.Tests/IntegrationTests.cs create mode 100644 test/Example01.Tests/WebApiTestFixture.cs create mode 100644 test/Example02.Tests/Example02.Tests.csproj create mode 100644 test/Example02.Tests/Example02.Tests.http create mode 100644 test/Example02.Tests/Extensions.cs create mode 100644 test/Example02.Tests/GlobalUsings.cs create mode 100644 test/Example02.Tests/IntegrationTests.cs create mode 100644 test/Example02.Tests/WebApiTestFixture.cs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..da74b91 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,28 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: .NET + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 7.0.x + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal diff --git a/.gitignore b/.gitignore index 8a30d25..4251007 100644 --- a/.gitignore +++ b/.gitignore @@ -396,3 +396,4 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml +/.idea/* diff --git a/ModelBindingDemo.sln b/ModelBindingDemo.sln new file mode 100644 index 0000000..b3b6efb --- /dev/null +++ b/ModelBindingDemo.sln @@ -0,0 +1,56 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{D733C4D0-DA18-41A2-945E-E1907BD0F471}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example01", "src\Example01\Example01.csproj", "{362D3A36-4B85-469E-9964-7C31E9F22757}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{3D88ADFF-B52F-4B4D-8FA9-EFEEB9F69BAE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example01.Tests", "test\Example01.Tests\Example01.Tests.csproj", "{64A216A5-9BB6-48C5-A2B2-9EF4EFBEDC21}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example02", "src\Example02\Example02.csproj", "{E6A941D9-BC3E-454C-B4A5-AB8DEE5F69BB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example02.Tests", "test\Example02.Tests\Example02.Tests.csproj", "{97814823-3068-4C3E-8CB2-B0FC34E05FA2}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "SolutionFiles", "SolutionFiles", "{11AA728D-845B-46B3-8F8A-1D12664ED8E3}" + ProjectSection(SolutionItems) = preProject + .github\workflows\ci.yml = .github\workflows\ci.yml + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {362D3A36-4B85-469E-9964-7C31E9F22757}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {362D3A36-4B85-469E-9964-7C31E9F22757}.Debug|Any CPU.Build.0 = Debug|Any CPU + {362D3A36-4B85-469E-9964-7C31E9F22757}.Release|Any CPU.ActiveCfg = Release|Any CPU + {362D3A36-4B85-469E-9964-7C31E9F22757}.Release|Any CPU.Build.0 = Release|Any CPU + {64A216A5-9BB6-48C5-A2B2-9EF4EFBEDC21}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64A216A5-9BB6-48C5-A2B2-9EF4EFBEDC21}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64A216A5-9BB6-48C5-A2B2-9EF4EFBEDC21}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64A216A5-9BB6-48C5-A2B2-9EF4EFBEDC21}.Release|Any CPU.Build.0 = Release|Any CPU + {E6A941D9-BC3E-454C-B4A5-AB8DEE5F69BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6A941D9-BC3E-454C-B4A5-AB8DEE5F69BB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6A941D9-BC3E-454C-B4A5-AB8DEE5F69BB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6A941D9-BC3E-454C-B4A5-AB8DEE5F69BB}.Release|Any CPU.Build.0 = Release|Any CPU + {97814823-3068-4C3E-8CB2-B0FC34E05FA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {97814823-3068-4C3E-8CB2-B0FC34E05FA2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {97814823-3068-4C3E-8CB2-B0FC34E05FA2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {97814823-3068-4C3E-8CB2-B0FC34E05FA2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {362D3A36-4B85-469E-9964-7C31E9F22757} = {D733C4D0-DA18-41A2-945E-E1907BD0F471} + {64A216A5-9BB6-48C5-A2B2-9EF4EFBEDC21} = {3D88ADFF-B52F-4B4D-8FA9-EFEEB9F69BAE} + {E6A941D9-BC3E-454C-B4A5-AB8DEE5F69BB} = {D733C4D0-DA18-41A2-945E-E1907BD0F471} + {97814823-3068-4C3E-8CB2-B0FC34E05FA2} = {3D88ADFF-B52F-4B4D-8FA9-EFEEB9F69BAE} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index bf82787..5f5cd6c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,14 @@ +[![.NET](https://github.com/aimenux/ModelBindingDemo/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/aimenux/ModelBindingDemo/actions/workflows/ci.yml) + # ModelBindingDemo +``` Using various model binding ways +``` + +In this repo, i m using various model binding ways in web api projects +> +> :one: `Example01` use [model binding with controller api](https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding) +> +> :two: `Example02` use [model binding with minimal api](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/parameter-binding) +> +**`Tools`** : net 7.0, web api, integration-testing, fluent-assertions diff --git a/global.json b/global.json new file mode 100644 index 0000000..3ca2e3c --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "7.0.400", + "allowPrerelease": false, + "rollForward": "latestFeature" + } +} \ No newline at end of file diff --git a/src/Example01/Binders/BooleanModelBinder.cs b/src/Example01/Binders/BooleanModelBinder.cs new file mode 100644 index 0000000..4717b93 --- /dev/null +++ b/src/Example01/Binders/BooleanModelBinder.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; + +namespace Example01.Binders; + +public class BooleanModelBinder : IModelBinder +{ + public Task BindModelAsync(ModelBindingContext bindingContext) + { + ArgumentNullException.ThrowIfNull(bindingContext); + + var modelName = bindingContext.ModelName; + var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName); + if (valueProviderResult == ValueProviderResult.None) + { + return Task.CompletedTask; + } + + var value = valueProviderResult.FirstValue; + var (isTrue, isFalse) = (IsTrueValue(value), IsFalseValue(value)); + if (isTrue || isFalse) + { + bindingContext.Result = ModelBindingResult.Success(isTrue); + } + + return Task.CompletedTask; + } + + private static bool IsTrueValue(string value) => IgnoreCaseEquals(value, "True") + || IgnoreCaseEquals(value, "Yes") + || IgnoreCaseEquals(value, "Oui") + || IgnoreCaseEquals(value, "1"); + + private static bool IsFalseValue(string value) => IgnoreCaseEquals(value, "False") + || IgnoreCaseEquals(value, "No") + || IgnoreCaseEquals(value, "Non") + || IgnoreCaseEquals(value, "0"); + + private static bool IgnoreCaseEquals(string left, string right) => string.Equals(left, right, StringComparison.OrdinalIgnoreCase); +} \ No newline at end of file diff --git a/src/Example01/Controllers/ApiController.cs b/src/Example01/Controllers/ApiController.cs new file mode 100644 index 0000000..6dd5e0b --- /dev/null +++ b/src/Example01/Controllers/ApiController.cs @@ -0,0 +1,100 @@ +using Example01.Binders; +using Example01.Payloads; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Internal; + +namespace Example01.Controllers; + +[ApiController] +[Route("[controller]")] +public class ApiController : ControllerBase +{ + [HttpGet("{id:int}")] + public IActionResult Get([FromRoute] int id) + { + var response = new + { + Id = id, + Source = "FromRoute" + }; + return Ok(response); + } + + [HttpGet("trace")] + public IActionResult Get([FromHeader(Name = "X-Trace-Id")] string traceId) + { + var response = new + { + TraceId = traceId, + Source = "FromHeader" + }; + return Ok(response); + } + + [HttpGet("search")] + public IActionResult Search([FromQuery] string keyword) + { + var response = new + { + KeyWord = keyword, + Source = "FromQuery" + }; + return Ok(response); + } + + [HttpGet("list")] + public IActionResult List([FromQuery] ApiQuery query) + { + var response = new + { + ApiQuery = query, + Source = "FromQuery" + }; + return Ok(response); + } + + [HttpGet("date")] + public IActionResult Date([FromServices] ISystemClock systemClock) + { + var response = new + { + UtcNow = systemClock.UtcNow, + Source = "FromServices" + }; + return Ok(response); + } + + [HttpGet("custom")] + public IActionResult Custom([FromQuery] [ModelBinder(BinderType = typeof(BooleanModelBinder))] bool answer) + { + var response = new + { + Answer = answer, + Source = "CustomBinding" + }; + return Ok(response); + } + + [HttpPost] + public IActionResult Post([FromBody] ApiRequest request) + { + var response = new + { + ApiResponse = new ApiResponse($"{request.FirstName} {request.LastName}"), + Source = "FromBody" + }; + return Ok(response); + } + + [HttpPut("{id:int}")] + public IActionResult Put([FromRoute] int id, [FromBody] ApiRequest request) + { + return request.Id == id ? NoContent() : BadRequest(); + } + + [HttpDelete("{id:int}")] + public IActionResult Delete(int id) + { + return id > 0 ? NoContent() : BadRequest(); + } +} diff --git a/src/Example01/Example01.csproj b/src/Example01/Example01.csproj new file mode 100644 index 0000000..8b22d8f --- /dev/null +++ b/src/Example01/Example01.csproj @@ -0,0 +1,17 @@ + + + + net7.0 + enable + + + + + + + + + + + + diff --git a/src/Example01/Payloads/ApiQuery.cs b/src/Example01/Payloads/ApiQuery.cs new file mode 100644 index 0000000..f43defc --- /dev/null +++ b/src/Example01/Payloads/ApiQuery.cs @@ -0,0 +1,3 @@ +namespace Example01.Payloads; + +public record ApiQuery(int Take, int Skip); \ No newline at end of file diff --git a/src/Example01/Payloads/ApiRequest.cs b/src/Example01/Payloads/ApiRequest.cs new file mode 100644 index 0000000..059b608 --- /dev/null +++ b/src/Example01/Payloads/ApiRequest.cs @@ -0,0 +1,3 @@ +namespace Example01.Payloads; + +public record ApiRequest(int Id, string FirstName, string LastName); \ No newline at end of file diff --git a/src/Example01/Payloads/ApiResponse.cs b/src/Example01/Payloads/ApiResponse.cs new file mode 100644 index 0000000..89160c8 --- /dev/null +++ b/src/Example01/Payloads/ApiResponse.cs @@ -0,0 +1,3 @@ +namespace Example01.Payloads; + +public record ApiResponse(string FullName); \ No newline at end of file diff --git a/src/Example01/Program.cs b/src/Example01/Program.cs new file mode 100644 index 0000000..febebb7 --- /dev/null +++ b/src/Example01/Program.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Internal; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers(); +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/src/Example01/Properties/launchSettings.json b/src/Example01/Properties/launchSettings.json new file mode 100644 index 0000000..002d8fb --- /dev/null +++ b/src/Example01/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:14242", + "sslPort": 44303 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5010", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5020;http://localhost:5010", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Example01/appsettings.Development.json b/src/Example01/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/Example01/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Example01/appsettings.json b/src/Example01/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/src/Example01/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Example02/Binders/BooleanModelBinder.cs b/src/Example02/Binders/BooleanModelBinder.cs new file mode 100644 index 0000000..44dd96c --- /dev/null +++ b/src/Example02/Binders/BooleanModelBinder.cs @@ -0,0 +1,34 @@ +namespace Example02.Binders; + +public class BooleanModelBinder +{ + public bool Value { get; private init; } + + public static bool TryParse(string value, out BooleanModelBinder model) + { + var (isTrue, isFalse) = (IsTrueValue(value), IsFalseValue(value)); + if (!isTrue && !isFalse) + { + model = null; + } + + model = new BooleanModelBinder + { + Value = isTrue + }; + + return true; + } + + private static bool IsTrueValue(string value) => IgnoreCaseEquals(value, "True") + || IgnoreCaseEquals(value, "Yes") + || IgnoreCaseEquals(value, "Oui") + || IgnoreCaseEquals(value, "1"); + + private static bool IsFalseValue(string value) => IgnoreCaseEquals(value, "False") + || IgnoreCaseEquals(value, "No") + || IgnoreCaseEquals(value, "Non") + || IgnoreCaseEquals(value, "0"); + + private static bool IgnoreCaseEquals(string left, string right) => string.Equals(left, right, StringComparison.OrdinalIgnoreCase); +} \ No newline at end of file diff --git a/src/Example02/Example02.csproj b/src/Example02/Example02.csproj new file mode 100644 index 0000000..1d03ba9 --- /dev/null +++ b/src/Example02/Example02.csproj @@ -0,0 +1,17 @@ + + + + net7.0 + enable + + + + + + + + + + + + diff --git a/src/Example02/Payloads/ApiQuery.cs b/src/Example02/Payloads/ApiQuery.cs new file mode 100644 index 0000000..5c78947 --- /dev/null +++ b/src/Example02/Payloads/ApiQuery.cs @@ -0,0 +1,3 @@ +namespace Example02.Payloads; + +public record ApiQuery(int Take, int Skip); \ No newline at end of file diff --git a/src/Example02/Payloads/ApiRequest.cs b/src/Example02/Payloads/ApiRequest.cs new file mode 100644 index 0000000..656f352 --- /dev/null +++ b/src/Example02/Payloads/ApiRequest.cs @@ -0,0 +1,3 @@ +namespace Example02.Payloads; + +public record ApiRequest(int Id, string FirstName, string LastName); \ No newline at end of file diff --git a/src/Example02/Payloads/ApiResponse.cs b/src/Example02/Payloads/ApiResponse.cs new file mode 100644 index 0000000..d79bf08 --- /dev/null +++ b/src/Example02/Payloads/ApiResponse.cs @@ -0,0 +1,3 @@ +namespace Example02.Payloads; + +public record ApiResponse(string FullName); \ No newline at end of file diff --git a/src/Example02/Program.cs b/src/Example02/Program.cs new file mode 100644 index 0000000..42d0dee --- /dev/null +++ b/src/Example02/Program.cs @@ -0,0 +1,105 @@ +using Example02.Binders; +using Example02.Payloads; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Internal; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); + +app + .MapGet("api/{id:int}", (int id) => + { + var response = new + { + Id = id, + Source = "FromRoute" + }; + return Results.Ok(response); + }); + +app + .MapGet("api/trace", ([FromHeader(Name = "X-Trace-Id")] string traceId) => + { + var response = new + { + TraceId = traceId, + Source = "FromHeader" + }; + return Results.Ok(response); + }); + +app + .MapGet("api/search", ([FromQuery] string keyword) => + { + var response = new + { + KeyWord = keyword, + Source = "FromQuery" + }; + return Results.Ok(response); + }); + +app + .MapGet("api/list", ([AsParameters] ApiQuery query) => + { + var response = new + { + ApiQuery = query, + Source = "FromQuery" + }; + return Results.Ok(response); + }); + +app + .MapGet("api/custom", ([FromQuery] BooleanModelBinder answer) => + { + var response = new + { + Value = answer.Value, + Source = "CustomBinding" + }; + return Results.Ok(response); + }); + +app + .MapGet("api/date", ([FromServices] ISystemClock systemClock) => + { + var response = new + { + UtcNow = systemClock.UtcNow, + Source = "FromQuery" + }; + return Results.Ok(response); + }); + +app + .MapPost("api", ([FromBody] ApiRequest request) => + { + var response = new + { + ApiResponse = new ApiResponse($"{request.FirstName} {request.LastName}"), + Source = "FromBody" + }; + return Results.Ok(response); + }); + +app + .MapPut("api/{id:int}", ([FromRoute] int id, [FromBody] ApiRequest request) => request.Id == id ? Results.NoContent() : Results.BadRequest()); + +app + .MapDelete("api/{id:int}", ([FromRoute] int id) => id > 0 ? Results.NoContent() : Results.BadRequest()); + +app.Run(); \ No newline at end of file diff --git a/src/Example02/Properties/launchSettings.json b/src/Example02/Properties/launchSettings.json new file mode 100644 index 0000000..9685493 --- /dev/null +++ b/src/Example02/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:33937", + "sslPort": 44341 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5010", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5020;http://localhost:5010", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Example02/appsettings.Development.json b/src/Example02/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/src/Example02/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/src/Example02/appsettings.json b/src/Example02/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/src/Example02/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/test/Example01.Tests/Example01.Tests.csproj b/test/Example01.Tests/Example01.Tests.csproj new file mode 100644 index 0000000..01a3813 --- /dev/null +++ b/test/Example01.Tests/Example01.Tests.csproj @@ -0,0 +1,29 @@ + + + + net7.0 + enable + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/test/Example01.Tests/Example01.Tests.http b/test/Example01.Tests/Example01.Tests.http new file mode 100644 index 0000000..60a2257 --- /dev/null +++ b/test/Example01.Tests/Example01.Tests.http @@ -0,0 +1,55 @@ +### + +GET http://localhost:5010/Api/123 + +### + +GET http://localhost:5010/Api/search?keyword=walter + +### + +GET http://localhost:5010/Api/trace +X-Trace-Id: xyz + +### + +GET http://localhost:5010/Api/list?Take=20&Skip=10 + +### + +GET http://localhost:5010/Api/date + +### + +GET http://localhost:5010/Api/custom?answer=oui + +### + +POST http://localhost:5010/Api +Content-Type: application/json + +{ + "id": 123, + "firstName": "Walter", + "lastName": "White" +} + +### + +PUT http://localhost:5010/Api/123 +Content-Type: application/json + +{ + "id": 123, + "firstName": "Walter", + "lastName": "White" +} + +### + +DELETE http://localhost:5010/Api/123 + +### + + + diff --git a/test/Example01.Tests/Extensions.cs b/test/Example01.Tests/Extensions.cs new file mode 100644 index 0000000..3ecdcb4 --- /dev/null +++ b/test/Example01.Tests/Extensions.cs @@ -0,0 +1,10 @@ +namespace Example01.Tests; + +internal static class Extensions +{ + public static HttpClient WithRequestHeader(this HttpClient client, string headerName, string headerValue) + { + client.DefaultRequestHeaders.Add(headerName, headerValue); + return client; + } +} \ No newline at end of file diff --git a/test/Example01.Tests/GlobalUsings.cs b/test/Example01.Tests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/test/Example01.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/test/Example01.Tests/IntegrationTests.cs b/test/Example01.Tests/IntegrationTests.cs new file mode 100644 index 0000000..c8daa1e --- /dev/null +++ b/test/Example01.Tests/IntegrationTests.cs @@ -0,0 +1,171 @@ +using System.Net; +using System.Net.Http.Json; +using Example01.Payloads; +using FluentAssertions; + +namespace Example01.Tests; + +public class IntegrationTests +{ + [Theory] + [InlineData(1)] + [InlineData(2)] + public async Task Get_Route_Should_Return_Success_Response(int id) + { + // arrange + var fixture = new WebApiTestFixture(); + var client = fixture.CreateClient(); + + // act + var response = await client.GetAsync($"/api/{id}"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + responseBody.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task Trace_Route_Should_Return_Success_Response() + { + // arrange + var fixture = new WebApiTestFixture(); + var client = fixture.CreateClient(); + + // act + var response = await client + .WithRequestHeader("X-Trace-Id", "xyz") + .GetAsync("api/trace"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + responseBody.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task Search_Route_Should_Return_Success_Response() + { + // arrange + var fixture = new WebApiTestFixture(); + var client = fixture.CreateClient(); + + // act + var response = await client.GetAsync("api/search?keyword=xyz"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + responseBody.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task List_Route_Should_Return_Success_Response() + { + // arrange + var fixture = new WebApiTestFixture(); + var client = fixture.CreateClient(); + + // act + var response = await client.GetAsync("api/list?take=50&skip=10"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + responseBody.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task Date_Route_Should_Return_Success_Response() + { + // arrange + var fixture = new WebApiTestFixture(); + var client = fixture.CreateClient(); + + // act + var response = await client.GetAsync(@"api/date"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + responseBody.Should().NotBeNullOrWhiteSpace(); + } + + [Theory] + [InlineData("true")] + [InlineData("false")] + [InlineData("yes")] + [InlineData("no")] + [InlineData("oui")] + [InlineData("non")] + [InlineData("1")] + [InlineData("0")] + public async Task Custom_Route_Should_Return_Success_Response(string answer) + { + // arrange + var fixture = new WebApiTestFixture(); + var client = fixture.CreateClient(); + + // act + var response = await client.GetAsync($"api/custom?answer={answer}"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + responseBody.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task Post_Route_Should_Return_Success_Response() + { + // arrange + var fixture = new WebApiTestFixture(); + var client = fixture.CreateClient(); + var request = new ApiRequest(1, "Walter", "White"); + + // act + var response = await client.PostAsJsonAsync("api", request); + var responseBody = await response.Content.ReadAsStringAsync(); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + responseBody.Should().NotBeNullOrWhiteSpace(); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + public async Task Put_Route_Should_Return_Success_Response(int id) + { + // arrange + var fixture = new WebApiTestFixture(); + var client = fixture.CreateClient(); + var request = new ApiRequest(id, "Walter", "White"); + + // act + var response = await client.PutAsJsonAsync($"api/{id}", request); + var responseBody = await response.Content.ReadAsStringAsync(); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + responseBody.Should().BeEmpty(); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + public async Task Delete_Route_Should_Return_Success_Response(int id) + { + // arrange + var fixture = new WebApiTestFixture(); + var client = fixture.CreateClient(); + + // act + var response = await client.DeleteAsync($"api/{id}"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + responseBody.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/test/Example01.Tests/WebApiTestFixture.cs b/test/Example01.Tests/WebApiTestFixture.cs new file mode 100644 index 0000000..6bd8ffa --- /dev/null +++ b/test/Example01.Tests/WebApiTestFixture.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; + +namespace Example01.Tests; + +internal class WebApiTestFixture : WebApplicationFactory +{ + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureAppConfiguration((_, configBuilder) => + { + }); + + builder.ConfigureTestServices(services => + { + }); + } +} \ No newline at end of file diff --git a/test/Example02.Tests/Example02.Tests.csproj b/test/Example02.Tests/Example02.Tests.csproj new file mode 100644 index 0000000..391e57d --- /dev/null +++ b/test/Example02.Tests/Example02.Tests.csproj @@ -0,0 +1,31 @@ + + + + net7.0 + enable + enable + + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/test/Example02.Tests/Example02.Tests.http b/test/Example02.Tests/Example02.Tests.http new file mode 100644 index 0000000..67b73a5 --- /dev/null +++ b/test/Example02.Tests/Example02.Tests.http @@ -0,0 +1,55 @@ +### + +GET http://localhost:5010/Api/123 + +### + +GET http://localhost:5010/Api/search?keyword=walter + +### + +GET http://localhost:5010/Api/trace +X-Trace-Id: xyz + +### + +GET http://localhost:5010/Api/list?Take=20&Skip=10 + +### + +GET http://localhost:5010/Api/custom?answer=oui + +### + +GET http://localhost:5010/Api/date + +### + +POST http://localhost:5010/Api +Content-Type: application/json + +{ + "id": 123, + "firstName": "Walter", + "lastName": "White" +} + +### + +PUT http://localhost:5010/Api/123 +Content-Type: application/json + +{ + "id": 123, + "firstName": "Walter", + "lastName": "White" +} + +### + +DELETE http://localhost:5010/Api/123 + +### + + + diff --git a/test/Example02.Tests/Extensions.cs b/test/Example02.Tests/Extensions.cs new file mode 100644 index 0000000..2fbe790 --- /dev/null +++ b/test/Example02.Tests/Extensions.cs @@ -0,0 +1,10 @@ +namespace Example02.Tests; + +internal static class Extensions +{ + public static HttpClient WithRequestHeader(this HttpClient client, string headerName, string headerValue) + { + client.DefaultRequestHeaders.Add(headerName, headerValue); + return client; + } +} \ No newline at end of file diff --git a/test/Example02.Tests/GlobalUsings.cs b/test/Example02.Tests/GlobalUsings.cs new file mode 100644 index 0000000..8c927eb --- /dev/null +++ b/test/Example02.Tests/GlobalUsings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/test/Example02.Tests/IntegrationTests.cs b/test/Example02.Tests/IntegrationTests.cs new file mode 100644 index 0000000..3ab679a --- /dev/null +++ b/test/Example02.Tests/IntegrationTests.cs @@ -0,0 +1,171 @@ +using System.Net; +using System.Net.Http.Json; +using Example02.Payloads; +using FluentAssertions; + +namespace Example02.Tests; + +public class IntegrationTests +{ + [Theory] + [InlineData(1)] + [InlineData(2)] + public async Task Get_Route_Should_Return_Success_Response(int id) + { + // arrange + var fixture = new WebApiTestFixture(); + var client = fixture.CreateClient(); + + // act + var response = await client.GetAsync($"/api/{id}"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + responseBody.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task Trace_Route_Should_Return_Success_Response() + { + // arrange + var fixture = new WebApiTestFixture(); + var client = fixture.CreateClient(); + + // act + var response = await client + .WithRequestHeader("X-Trace-Id", "xyz") + .GetAsync("api/trace"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + responseBody.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task Search_Route_Should_Return_Success_Response() + { + // arrange + var fixture = new WebApiTestFixture(); + var client = fixture.CreateClient(); + + // act + var response = await client.GetAsync("api/search?keyword=xyz"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + responseBody.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task List_Route_Should_Return_Success_Response() + { + // arrange + var fixture = new WebApiTestFixture(); + var client = fixture.CreateClient(); + + // act + var response = await client.GetAsync("api/list?take=50&skip=10"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + responseBody.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task Date_Route_Should_Return_Success_Response() + { + // arrange + var fixture = new WebApiTestFixture(); + var client = fixture.CreateClient(); + + // act + var response = await client.GetAsync(@"api/date"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + responseBody.Should().NotBeNullOrWhiteSpace(); + } + + [Theory] + [InlineData("true")] + [InlineData("false")] + [InlineData("yes")] + [InlineData("no")] + [InlineData("oui")] + [InlineData("non")] + [InlineData("1")] + [InlineData("0")] + public async Task Custom_Route_Should_Return_Success_Response(string answer) + { + // arrange + var fixture = new WebApiTestFixture(); + var client = fixture.CreateClient(); + + // act + var response = await client.GetAsync($"api/custom?answer={answer}"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + responseBody.Should().NotBeNullOrWhiteSpace(); + } + + [Fact] + public async Task Post_Route_Should_Return_Success_Response() + { + // arrange + var fixture = new WebApiTestFixture(); + var client = fixture.CreateClient(); + var request = new ApiRequest(1, "Walter", "White"); + + // act + var response = await client.PostAsJsonAsync("api", request); + var responseBody = await response.Content.ReadAsStringAsync(); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.OK); + responseBody.Should().NotBeNullOrWhiteSpace(); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + public async Task Put_Route_Should_Return_Success_Response(int id) + { + // arrange + var fixture = new WebApiTestFixture(); + var client = fixture.CreateClient(); + var request = new ApiRequest(id, "Walter", "White"); + + // act + var response = await client.PutAsJsonAsync($"api/{id}", request); + var responseBody = await response.Content.ReadAsStringAsync(); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + responseBody.Should().BeEmpty(); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + public async Task Delete_Route_Should_Return_Success_Response(int id) + { + // arrange + var fixture = new WebApiTestFixture(); + var client = fixture.CreateClient(); + + // act + var response = await client.DeleteAsync($"api/{id}"); + var responseBody = await response.Content.ReadAsStringAsync(); + + // assert + response.StatusCode.Should().Be(HttpStatusCode.NoContent); + responseBody.Should().BeEmpty(); + } +} \ No newline at end of file diff --git a/test/Example02.Tests/WebApiTestFixture.cs b/test/Example02.Tests/WebApiTestFixture.cs new file mode 100644 index 0000000..add3ae1 --- /dev/null +++ b/test/Example02.Tests/WebApiTestFixture.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; + +namespace Example02.Tests; + +internal class WebApiTestFixture : WebApplicationFactory +{ + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureAppConfiguration((_, configBuilder) => + { + }); + + builder.ConfigureTestServices(services => + { + }); + } +} \ No newline at end of file