├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── .spectral.yml
├── .vscode
├── launch.json
└── tasks.json
├── Directory.Packages.props
├── README.md
├── TrainingApi.sln
├── global.json
├── nuget.config
├── shared
├── Client.cs
├── Level.cs
├── Trainer.cs
├── TrainingApi.Shared.csproj
└── TrainingDb.cs
├── src
├── Apis
│ ├── ClientApis.cs
│ ├── TrainerApis.cs
│ └── XmlResult.cs
├── Data
│ └── DataGenerator.cs
├── OpenApi
│ └── OpenApiTransformersExtensions.cs
├── Program.cs
├── Properties
│ └── launchSettings.json
├── Services
│ ├── ClientsService.cs
│ └── TrainersService.cs
├── TrainingApi.csproj
├── TrainingApi.http
├── TrainingApi.json
├── appsettings.Development.json
└── appsettings.json
└── tests
├── ApiApplication.cs
├── IntegrationTests.cs
├── TestDbAsyncQueryProvider.cs
├── TrainingApi.Tests.csproj
├── UnitTests.cs
└── Usings.cs
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Build and lint APIs
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - 9.0
7 | push:
8 | branches:
9 | - 9.0
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-latest
14 | steps:
15 | # Check out the repository
16 | - uses: actions/checkout@v3
17 |
18 | - name: Setup .NET (global.json)
19 | uses: actions/setup-dotnet@v3
20 |
21 | - name: Install dependencies
22 | run: dotnet restore
23 |
24 | - name: Build
25 | run: dotnet build --no-restore
26 |
27 | - name: Run Spectral
28 | # Run Spectral
29 | uses: stoplightio/spectral-action@latest
30 | with:
31 | file_glob: 'src/TrainingApi.json'
32 | spectral_ruleset: .spectral.yml
33 |
34 | - name: Test
35 | run: dotnet test --no-build --verbosity normal
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore Flax project files
2 | Binaries/
3 | Cache/
4 | Logs/
5 | Output/
6 | Screenshots/
7 | *.HotReload.*
8 |
9 | # Ignore thumbnails created by Windows
10 | Thumbs.db
11 |
12 | # Ignore files built by Visual Studio
13 | *.obj
14 | *.exe
15 | *.pdb
16 | *.user
17 | *.aps
18 | *.pch
19 | *.vspscc
20 | *_i.c
21 | *_p.c
22 | *.ncb
23 | *.suo
24 | *.tlb
25 | *.tlh
26 | *.bak
27 | *.cache
28 | *.ilk
29 | *.log
30 | [Bb]in
31 | [Dd]ebug*/
32 | *.lib
33 | *.sbr
34 | obj/
35 | [Rr]elease*/
36 | _ReSharper*/
37 | [Tt]est[Rr]esult*
38 | .vs/
39 |
40 | # Ignore Nuget packages folder
41 | packages/
42 | .dotnet
43 |
44 | # General
45 | .DS_Store
46 | .AppleDouble
47 | .LSOverride
48 |
49 | # Icon must end with two \r
50 | Icon
51 |
52 |
53 | # Thumbnails
54 | ._*
55 |
56 | # Files that might appear in the root of a volume
57 | .DocumentRevisions-V100
58 | .fseventsd
59 | .Spotlight-V100
60 | .TemporaryItems
61 | .Trashes
62 | .VolumeIcon.icns
63 | .com.apple.timemachine.donotpresent
64 |
65 | # Directories potentially created on remote AFP share
66 | .AppleDB
67 | .AppleDesktop
68 | Network Trash Folder
69 | Temporary Items
70 | .apdisk
--------------------------------------------------------------------------------
/.spectral.yml:
--------------------------------------------------------------------------------
1 | extends: spectral:oas
2 | rules:
3 | info-contact: off
4 | oas3-api-servers: off
5 |
6 | success-response:
7 | description: All operations should have a success response.
8 | message: Operation is missing a success response.
9 | severity: warn
10 | given: $.paths.*.*.responses
11 | then:
12 | function: schema
13 | functionOptions:
14 | schema:
15 | anyOf:
16 | - required: ["200"]
17 | - required: ["201"]
18 | - required: ["204"]
19 |
20 | error-response:
21 | description: All operations should have a error response.
22 | message: Operation is missing a error response.
23 | severity: warn
24 | given: $.paths.*.*.responses
25 | then:
26 | function: schema
27 | functionOptions:
28 | schema:
29 | anyOf:
30 | - required: ["400"]
31 | - required: ["404"]
32 |
33 | parameter-description:
34 | description: All parameters should have a description
35 | message: Parameter is missing a description.
36 | severity: warn
37 | given: $.paths.*.*.parameters[*]
38 | then:
39 | field: description
40 | function: truthy
41 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "name": ".NET Core Launch (web)",
9 | "type": "coreclr",
10 | "request": "launch",
11 | "preLaunchTask": "build",
12 | "program": "${workspaceFolder}/bin/Debug/net10.0/TrainingApi.dll",
13 | "args": [],
14 | "cwd": "${workspaceFolder}",
15 | "stopAtEntry": false,
16 | "serverReadyAction": {
17 | "action": "openExternally",
18 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)"
19 | },
20 | "env": {
21 | "ASPNETCORE_ENVIRONMENT": "Development"
22 | },
23 | "sourceFileMap": {
24 | "/Views": "${workspaceFolder}/Views"
25 | }
26 | },
27 | {
28 | "name": ".NET Core Attach",
29 | "type": "coreclr",
30 | "request": "attach"
31 | }
32 | ]
33 | }
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.0.0",
3 | "tasks": [
4 | {
5 | "label": "build",
6 | "command": "dotnet",
7 | "type": "process",
8 | "args": [
9 | "build",
10 | "${workspaceFolder}/TrainingApi.csproj",
11 | "/property:GenerateFullPaths=true",
12 | "/consoleloggerparameters:NoSummary"
13 | ],
14 | "problemMatcher": "$msCompile"
15 | },
16 | {
17 | "label": "publish",
18 | "command": "dotnet",
19 | "type": "process",
20 | "args": [
21 | "publish",
22 | "${workspaceFolder}/TrainingApi.csproj",
23 | "/property:GenerateFullPaths=true",
24 | "/consoleloggerparameters:NoSummary"
25 | ],
26 | "problemMatcher": "$msCompile"
27 | },
28 | {
29 | "label": "watch",
30 | "command": "dotnet",
31 | "type": "process",
32 | "args": [
33 | "watch",
34 | "run",
35 | "--project",
36 | "${workspaceFolder}/TrainingApi.csproj"
37 | ],
38 | "problemMatcher": "$msCompile"
39 | }
40 | ]
41 | }
--------------------------------------------------------------------------------
/Directory.Packages.props:
--------------------------------------------------------------------------------
1 |
2 |
3 | true
4 |
5 |
6 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TrainingApi
2 |
3 | TrainingApi is a demo application showcasing the new features in minimal APIs for .NET 9.
4 |
5 | ## Run Application
6 |
7 | To run the API, navigate to the `TrainingApi` directory and execute `dotnet run`.
8 |
9 | ```
10 | $ cd TrainingApi
11 | $ dotnet run
12 | Building...
13 | info: Microsoft.EntityFrameworkCore.Update[30100]
14 | Saved 7 entities to in-memory store.
15 | info: Microsoft.Hosting.Lifetime[14]
16 | Now listening on: http://localhost:5198
17 | info: Microsoft.Hosting.Lifetime[0]
18 | Application started. Press Ctrl+C to shut down.
19 | info: Microsoft.Hosting.Lifetime[0]
20 | Hosting environment: Development
21 | info: Microsoft.Hosting.Lifetime[0]
22 | Content root path: /Users/captainsafia/github.com/TrainingApi/TrainingApi
23 | ```
24 |
25 | Navigate to http://localhost:5198/ to view the Scalar UI for interacting with the application.
26 |
27 | ### Run Tests
28 |
29 | Unit and integration tests are located under the `Tests` subdirectory.
30 |
31 | ```
32 | $ cd Tests
33 | $ dotnet test
34 | ```
35 |
36 | ## Contributing
37 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
38 |
39 | ## License
40 | [MIT](https://choosealicense.com/licenses/mit/)
--------------------------------------------------------------------------------
/TrainingApi.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.31903.59
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrainingApi", "src\TrainingApi.csproj", "{8C1F2529-55DE-4AC8-A816-B5E80BCB4C85}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrainingApi.Tests", "tests\TrainingApi.Tests.csproj", "{26C8B7F3-24D1-48B2-9883-6BC80E7D909C}"
9 | EndProject
10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TrainingApi.Shared", "shared\TrainingApi.Shared.csproj", "{540D803C-BDFB-4812-94DF-BF8C22E46B5F}"
11 | EndProject
12 | Global
13 | GlobalSection(SolutionProperties) = preSolution
14 | HideSolutionNode = FALSE
15 | EndGlobalSection
16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
17 | Debug|Any CPU = Debug|Any CPU
18 | Debug|x64 = Debug|x64
19 | Debug|x86 = Debug|x86
20 | Release|Any CPU = Release|Any CPU
21 | Release|x64 = Release|x64
22 | Release|x86 = Release|x86
23 | EndGlobalSection
24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
25 | {8C1F2529-55DE-4AC8-A816-B5E80BCB4C85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26 | {8C1F2529-55DE-4AC8-A816-B5E80BCB4C85}.Debug|Any CPU.Build.0 = Debug|Any CPU
27 | {8C1F2529-55DE-4AC8-A816-B5E80BCB4C85}.Debug|x64.ActiveCfg = Debug|Any CPU
28 | {8C1F2529-55DE-4AC8-A816-B5E80BCB4C85}.Debug|x64.Build.0 = Debug|Any CPU
29 | {8C1F2529-55DE-4AC8-A816-B5E80BCB4C85}.Debug|x86.ActiveCfg = Debug|Any CPU
30 | {8C1F2529-55DE-4AC8-A816-B5E80BCB4C85}.Debug|x86.Build.0 = Debug|Any CPU
31 | {8C1F2529-55DE-4AC8-A816-B5E80BCB4C85}.Release|Any CPU.ActiveCfg = Release|Any CPU
32 | {8C1F2529-55DE-4AC8-A816-B5E80BCB4C85}.Release|Any CPU.Build.0 = Release|Any CPU
33 | {8C1F2529-55DE-4AC8-A816-B5E80BCB4C85}.Release|x64.ActiveCfg = Release|Any CPU
34 | {8C1F2529-55DE-4AC8-A816-B5E80BCB4C85}.Release|x64.Build.0 = Release|Any CPU
35 | {8C1F2529-55DE-4AC8-A816-B5E80BCB4C85}.Release|x86.ActiveCfg = Release|Any CPU
36 | {8C1F2529-55DE-4AC8-A816-B5E80BCB4C85}.Release|x86.Build.0 = Release|Any CPU
37 | {26C8B7F3-24D1-48B2-9883-6BC80E7D909C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38 | {26C8B7F3-24D1-48B2-9883-6BC80E7D909C}.Debug|Any CPU.Build.0 = Debug|Any CPU
39 | {26C8B7F3-24D1-48B2-9883-6BC80E7D909C}.Debug|x64.ActiveCfg = Debug|Any CPU
40 | {26C8B7F3-24D1-48B2-9883-6BC80E7D909C}.Debug|x64.Build.0 = Debug|Any CPU
41 | {26C8B7F3-24D1-48B2-9883-6BC80E7D909C}.Debug|x86.ActiveCfg = Debug|Any CPU
42 | {26C8B7F3-24D1-48B2-9883-6BC80E7D909C}.Debug|x86.Build.0 = Debug|Any CPU
43 | {26C8B7F3-24D1-48B2-9883-6BC80E7D909C}.Release|Any CPU.ActiveCfg = Release|Any CPU
44 | {26C8B7F3-24D1-48B2-9883-6BC80E7D909C}.Release|Any CPU.Build.0 = Release|Any CPU
45 | {26C8B7F3-24D1-48B2-9883-6BC80E7D909C}.Release|x64.ActiveCfg = Release|Any CPU
46 | {26C8B7F3-24D1-48B2-9883-6BC80E7D909C}.Release|x64.Build.0 = Release|Any CPU
47 | {26C8B7F3-24D1-48B2-9883-6BC80E7D909C}.Release|x86.ActiveCfg = Release|Any CPU
48 | {26C8B7F3-24D1-48B2-9883-6BC80E7D909C}.Release|x86.Build.0 = Release|Any CPU
49 | {540D803C-BDFB-4812-94DF-BF8C22E46B5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
50 | {540D803C-BDFB-4812-94DF-BF8C22E46B5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
51 | {540D803C-BDFB-4812-94DF-BF8C22E46B5F}.Debug|x64.ActiveCfg = Debug|Any CPU
52 | {540D803C-BDFB-4812-94DF-BF8C22E46B5F}.Debug|x64.Build.0 = Debug|Any CPU
53 | {540D803C-BDFB-4812-94DF-BF8C22E46B5F}.Debug|x86.ActiveCfg = Debug|Any CPU
54 | {540D803C-BDFB-4812-94DF-BF8C22E46B5F}.Debug|x86.Build.0 = Debug|Any CPU
55 | {540D803C-BDFB-4812-94DF-BF8C22E46B5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
56 | {540D803C-BDFB-4812-94DF-BF8C22E46B5F}.Release|Any CPU.Build.0 = Release|Any CPU
57 | {540D803C-BDFB-4812-94DF-BF8C22E46B5F}.Release|x64.ActiveCfg = Release|Any CPU
58 | {540D803C-BDFB-4812-94DF-BF8C22E46B5F}.Release|x64.Build.0 = Release|Any CPU
59 | {540D803C-BDFB-4812-94DF-BF8C22E46B5F}.Release|x86.ActiveCfg = Release|Any CPU
60 | {540D803C-BDFB-4812-94DF-BF8C22E46B5F}.Release|x86.Build.0 = Release|Any CPU
61 | EndGlobalSection
62 | EndGlobal
63 |
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "10.0.100-preview.1.25110.2"
4 | }
5 | }
--------------------------------------------------------------------------------
/nuget.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/shared/Client.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.ComponentModel;
3 |
4 | namespace TrainingApi.Shared;
5 |
6 | ///
7 | /// Represents a client with personal information.
8 | ///
9 | /// The unique identifier of the client, assigned by the system when the client is created.
10 | /// The first name of the client.
11 | /// The last name of the client.
12 | /// The email of the client.
13 | /// The weight of the client in pounds, rounded to the nearest pound.
14 | /// The height of the client in inches, rounded to the nearest inch.
15 | /// The date of birth of the client.
16 | public record Client(
17 | int Id,
18 | string FirstName,
19 | string LastName,
20 | string Email,
21 | int Weight,
22 | int Height,
23 | DateTime BirthDate
24 | );
25 |
--------------------------------------------------------------------------------
/shared/Level.cs:
--------------------------------------------------------------------------------
1 | using System.Text.Json.Serialization;
2 |
3 | namespace TrainingApi.Shared;
4 |
5 | [JsonConverter(typeof(JsonStringEnumConverter))]
6 | public enum Level
7 | {
8 | Junior,
9 | Senior,
10 | Elite
11 | }
--------------------------------------------------------------------------------
/shared/Trainer.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 |
3 | namespace TrainingApi.Shared;
4 |
5 | public record Trainer(int Id, string FirstName, string LastName, string Email, Level Level, bool IsCertificationActive)
6 | {
7 | public Trainer()
8 | : this(0, "", "", "", Level.Junior, false)
9 | {
10 | }
11 |
12 | ///
13 | /// The unique identifier of the trainer, assigned by the system when the trainer is created.
14 | ///
15 | public int Id { get; set; } = Id;
16 |
17 | ///
18 | /// The first name of the trainer.
19 | ///
20 | public string FirstName { get; set; } = FirstName;
21 |
22 | ///
23 | /// The last name of the trainer.
24 | ///
25 | public string LastName { get; set; } = LastName;
26 |
27 | ///
28 | /// The email address of the trainer.
29 | ///
30 | public string Email { get; set; } = Email;
31 |
32 | ///
33 | /// The level of the trainer.
34 | ///
35 | public Level Level { get; set; } = Level;
36 |
37 | ///
38 | /// Indicates whether the trainer's certification is active.
39 | ///
40 | public bool IsCertificationActive { get; set; } = IsCertificationActive;
41 | }
42 |
--------------------------------------------------------------------------------
/shared/TrainingApi.Shared.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net10.0
5 | enable
6 | enable
7 | true
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/shared/TrainingDb.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 |
3 | namespace TrainingApi.Shared;
4 |
5 | public class TrainingDb : DbContext
6 | {
7 | // Parameterless constructor to support mocking in unit tests
8 | public TrainingDb() { }
9 | public TrainingDb(DbContextOptions options) : base(options) { }
10 |
11 | // Virtual DbSets to support mocking in unit tests
12 | public virtual DbSet Clients { get; set; }
13 | public virtual DbSet Trainers { get; set; }
14 | }
--------------------------------------------------------------------------------
/src/Apis/ClientApis.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 | using TrainingApi.Services;
3 | using TrainingApi.Shared;
4 |
5 | namespace TrainingApi.Apis;
6 |
7 | public static class ClientApis
8 | {
9 | public static IEndpointRouteBuilder MapClientApis(this IEndpointRouteBuilder app)
10 | {
11 | var clients = app.MapGroup("/clients")
12 | .WithTags("Clients");
13 |
14 | clients.MapGet("/{id}", (
15 | [Description("The unique identifier of the client, assigned by the system when the client is created")] int id,
16 | ClientsService service) => service.GetClientById(id))
17 | .WithName("GetClient")
18 | .WithDescription("Get a client");
19 |
20 | clients.MapPut("/{id}", (
21 | [Description("The unique identifier of the client, assigned by the system when the client is created")] int id,
22 | Client updatedClient,
23 | ClientsService service) => service.UpdateClientById(id, updatedClient))
24 | .WithName("UpdateClient")
25 | .WithDescription("Update a client");
26 |
27 | clients.MapPost("", (ClientsService service, Client client) => service.CreateClient(client))
28 | .WithName("CreateClient")
29 | .WithDescription("Create a client");
30 |
31 | clients.MapPost("/{id}", (
32 | [Description("The unique identifier of the client, assigned by the system when the client is created")] int id,
33 | ClientsService service) => service.DeleteClientById(id))
34 | .WithName("DeleteClient")
35 | .WithDescription("Delete a client");
36 |
37 | return app;
38 | }
39 | }
--------------------------------------------------------------------------------
/src/Apis/TrainerApis.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel;
2 | using TrainingApi.Services;
3 | using TrainingApi.Shared;
4 |
5 | namespace TrainingApi.Apis;
6 |
7 | public static class TrainerApis
8 | {
9 | public static IEndpointRouteBuilder MapTrainerApis(this IEndpointRouteBuilder app)
10 | {
11 | var trainers = app.MapGroup("/trainers")
12 | .RequireAuthorization("trainer_access")
13 | .WithTags("Trainers");
14 |
15 | trainers.MapGet("/", (TrainersService service) => service.GetTrainers())
16 | .WithName("ListTrainers")
17 | .WithDescription("List all trainers");
18 |
19 | trainers.MapPut("/{id}", (
20 | [Description("The unique identifier of the trainer, assigned by the system when the client is created")] int id,
21 | Trainer updatedTrainer,
22 | TrainersService service) => service.UpdateTrainerById(id, updatedTrainer))
23 | .WithName("UpdateTrainer")
24 | .WithDescription("Update a trainer");
25 |
26 | trainers.MapDelete("/{id}", (
27 | [Description("The unique identifier of the trainer, assigned by the system when the client is created")] int id,
28 | TrainersService service) => service.DeleteTrainerById(id))
29 | .WithName("DeleteTrainer")
30 | .WithDescription("Delete a trainer");
31 |
32 | trainers.MapPost("/", (TrainersService service, Trainer trainer) => service.CreateTrainer(trainer))
33 | .WithName("CreateTrainer")
34 | .WithDescription("Create a trainer");
35 |
36 | return app;
37 | }
38 | }
--------------------------------------------------------------------------------
/src/Apis/XmlResult.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Xml.Serialization;
3 | using Microsoft.AspNetCore.Http.Metadata;
4 |
5 | public class XmlResult : IResult, IEndpointMetadataProvider
6 | {
7 | private static readonly XmlSerializer _xmlSerializer = new(typeof(T));
8 |
9 | private readonly T _result;
10 |
11 | public XmlResult(T result)
12 | {
13 | _result = result;
14 | }
15 |
16 | public Task ExecuteAsync(HttpContext httpContext)
17 | {
18 | // Pool this for better efficiency
19 | using var ms = new MemoryStream();
20 |
21 | _xmlSerializer.Serialize(ms, _result);
22 |
23 | httpContext.Response.ContentType = "application/xml";
24 |
25 | ms.Position = 0;
26 | return ms.CopyToAsync(httpContext.Response.Body);
27 | }
28 |
29 | public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder)
30 | {
31 | builder.Metadata.Add(new XmlResponseTypeMetadata(typeof(T)));
32 | }
33 | }
34 |
35 | public class XmlResponseTypeMetadata : IProducesResponseTypeMetadata
36 | {
37 | public XmlResponseTypeMetadata(Type? type)
38 | {
39 | Type = type;
40 | }
41 |
42 | public Type? Type { get; set; }
43 |
44 | public int StatusCode { get; set; } = StatusCodes.Status200OK;
45 |
46 | public IEnumerable ContentTypes { get; set; } = ["application/xml"];
47 |
48 | public string? Description { get; set; }
49 | }
50 |
51 | public static class XmlResultExtensions
52 | {
53 | public static XmlResult Xml(this IResultExtensions _, T result) => new(result);
54 | }
--------------------------------------------------------------------------------
/src/Data/DataGenerator.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 | using Microsoft.EntityFrameworkCore;
3 | using TrainingApi.Shared;
4 |
5 | public static class DataGenerator
6 | {
7 | public static IApplicationBuilder InitializeDatabase(this IApplicationBuilder app)
8 | {
9 | var serviceProvider = app.ApplicationServices.CreateScope().ServiceProvider;
10 | var options = serviceProvider.GetRequiredService>();
11 | using var context = new TrainingDb(options);
12 | if (context.Clients.Any())
13 | {
14 | return app;
15 | }
16 |
17 | var clientA = new Client(1, "Vonnie", "Mawer", "vmawer0@go.com", 149, 66, DateTime.Parse("4/24/2000", CultureInfo.InvariantCulture));
18 | var clientB = new Client(2, "Langston", "Feldberg", "lfeldberg1@hc360.com", 329, 73, DateTime.Parse("10/20/1982", CultureInfo.InvariantCulture));
19 | var clientC = new Client(3, "Olwen", "Maeer", "omaeer3@purevolume.com", 261, 70, DateTime.Parse("8/22/1993", CultureInfo.InvariantCulture));
20 |
21 | context.Clients.AddRange(
22 | clientA,
23 | clientB,
24 | clientC
25 | );
26 |
27 | var trainerA = new Trainer(1, "Inna", "Spedroni", "ispedroni0@studiopress.com", Level.Junior, true);
28 | var trainerB = new Trainer(2, "Nikoletta", "Orrell", "norrell1@nydailynews.com", Level.Senior, true);
29 | var trainerC = new Trainer(3, "Briana", "Diprose", "bdiprose0@t.co", Level.Senior, true);
30 | var trainerD = new Trainer(4, "Zerk", "Riepl", "svanshin5@google.com", Level.Elite, true);
31 |
32 | context.Trainers.AddRange(
33 | trainerA,
34 | trainerB,
35 | trainerC,
36 | trainerD
37 | );
38 |
39 | context.SaveChanges();
40 |
41 | return app;
42 | }
43 | }
--------------------------------------------------------------------------------
/src/OpenApi/OpenApiTransformersExtensions.cs:
--------------------------------------------------------------------------------
1 |
2 | using Microsoft.OpenApi.Models;
3 | using Microsoft.AspNetCore.Authentication.JwtBearer;
4 | using Microsoft.AspNetCore.OpenApi;
5 | using Microsoft.AspNetCore.Authorization;
6 | using TrainingApi.Shared;
7 | using System.Text.Json.Nodes;
8 | using Microsoft.OpenApi.Models.References;
9 | using Microsoft.OpenApi.Models.Interfaces;
10 |
11 | public static class OpenApiTransformersExtensions
12 | {
13 | public static OpenApiOptions UseJwtBearerAuthentication(this OpenApiOptions options)
14 | {
15 | var scheme = new OpenApiSecurityScheme()
16 | {
17 | Type = SecuritySchemeType.Http,
18 | Name = JwtBearerDefaults.AuthenticationScheme,
19 | Scheme = JwtBearerDefaults.AuthenticationScheme,
20 | };
21 | options.AddDocumentTransformer((document, context, cancellationToken) =>
22 | {
23 | document.Components ??= new();
24 | document.Components.SecuritySchemes ??= new Dictionary();
25 | document.Components.SecuritySchemes.Add(JwtBearerDefaults.AuthenticationScheme, scheme);
26 | return Task.CompletedTask;
27 | });
28 | options.AddOperationTransformer((operation, context, cancellationToken) =>
29 | {
30 | if (context.Description.ActionDescriptor.EndpointMetadata.OfType().Any())
31 | {
32 | operation.Security = [new() {{ new OpenApiSecuritySchemeReference(JwtBearerDefaults.AuthenticationScheme), [] }}];
33 | }
34 | return Task.CompletedTask;
35 | });
36 | return options;
37 | }
38 |
39 | public static OpenApiOptions UseExamples(this OpenApiOptions options)
40 | {
41 | options.AddSchemaTransformer((schema, context, cancellationToken) =>
42 | {
43 | if (context.JsonTypeInfo.Type == typeof(Trainer))
44 | {
45 | schema.Example = new JsonObject
46 | {
47 | ["id"] = 1,
48 | ["firstName"] = "John",
49 | ["lastName"] = "Doe",
50 | ["email"] = "john.doe@email.com",
51 | ["level"] = "Junior",
52 | ["isCertificationActive"] = false
53 | };
54 | }
55 | if (context.JsonTypeInfo.Type == typeof(Client))
56 | {
57 | schema.Example = new JsonObject
58 | {
59 | ["id"] = 1,
60 | ["firstName"] = "Jane",
61 | ["lastName"] = "Smith",
62 | ["email"] ="jane.smith@email.com",
63 | ["weight"] = 60,
64 | ["height"] = 170,
65 | ["birthDate"] = "1990-01-01"
66 | };
67 | }
68 | return Task.CompletedTask;
69 | });
70 | return options;
71 | }
72 |
73 | public static OpenApiOptions MapType(this OpenApiOptions options, JsonSchemaType type)
74 | {
75 | options.AddSchemaTransformer((schema, context, cancellationToken) =>
76 | {
77 | if (context.JsonTypeInfo.Type == typeof(T))
78 | {
79 | schema.Type = type;
80 | }
81 | return Task.CompletedTask;
82 | });
83 |
84 | return options;
85 | }
86 | }
--------------------------------------------------------------------------------
/src/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Scalar.AspNetCore;
3 | using TrainingApi.Apis;
4 | using TrainingApi.Services;
5 | using TrainingApi.Shared;
6 |
7 | var builder = WebApplication.CreateBuilder(args);
8 |
9 | // Data and API dependencies
10 | builder.Services
11 | .AddDbContext(options => options.UseInMemoryDatabase("training"));
12 | builder.Services.AddScoped();
13 | builder.Services.AddScoped();
14 | // Authentication and authorization dependencies
15 | builder.Services.AddAuthentication().AddJwtBearer();
16 | builder.Services.AddAuthorizationBuilder().AddPolicy("trainer_access", policy =>
17 | policy.RequireRole("trainer").RequireClaim("permission", "admin"));
18 | // OpenAPI dependencies
19 | builder.Services.AddOpenApi(options =>
20 | {
21 | options.UseJwtBearerAuthentication();
22 | options.UseExamples();
23 | });
24 |
25 | var app = builder.Build();
26 |
27 | if (app.Environment.IsDevelopment())
28 | {
29 | // Set up OpenAPI-related endpoints
30 | app.MapOpenApi();
31 | app.MapScalarApiReference(options =>
32 | {
33 | options.DefaultFonts = false;
34 | });
35 | // Seed the database with mock data
36 | app.InitializeDatabase();
37 | }
38 |
39 | // Redirect for OpenAPI view
40 | app.MapGet("/", () => Results.Redirect("/scalar/v1"))
41 | .ExcludeFromDescription();
42 | // Register /client and /trainer APIs
43 | app.MapClientApis();
44 | app.MapTrainerApis();
45 |
46 | app.Run();
47 |
--------------------------------------------------------------------------------
/src/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:1739",
7 | "sslPort": 44360
8 | }
9 | },
10 | "profiles": {
11 | "http": {
12 | "commandName": "Project",
13 | "dotnetRunMessages": true,
14 | "launchBrowser": true,
15 | "applicationUrl": "http://localhost:5198",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | }
19 | },
20 | "https": {
21 | "commandName": "Project",
22 | "dotnetRunMessages": true,
23 | "launchBrowser": true,
24 | "applicationUrl": "https://localhost:7088;http://localhost:5198",
25 | "environmentVariables": {
26 | "ASPNETCORE_ENVIRONMENT": "Development"
27 | }
28 | },
29 | "IIS Express": {
30 | "commandName": "IISExpress",
31 | "launchBrowser": true,
32 | "environmentVariables": {
33 | "ASPNETCORE_ENVIRONMENT": "Development"
34 | }
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Services/ClientsService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http.HttpResults;
2 | using Microsoft.EntityFrameworkCore;
3 | using TrainingApi.Shared;
4 |
5 | namespace TrainingApi.Services;
6 |
7 | public class ClientsService(TrainingDb trainingDb)
8 | {
9 | public async Task, NotFound>> GetClientById(int id)
10 | {
11 | var client = await trainingDb.Clients.FindAsync(id);
12 | return client is null ? TypedResults.NotFound() : TypedResults.Ok(client);
13 | }
14 | public async Task, NotFound>> UpdateClientById(int id, Client updatedClient)
15 | {
16 | var client = await trainingDb.Clients.FindAsync(id);
17 | if (client is null) return TypedResults.NotFound();
18 | client = updatedClient;
19 | await trainingDb.SaveChangesAsync();
20 | return TypedResults.Created($"/clients/{client.Id}", client);
21 | }
22 |
23 | public async Task, NotFound>> DeleteClientById(int id)
24 | {
25 | var client = await trainingDb.Clients.FindAsync(id);
26 | if (client is null) return TypedResults.NotFound();
27 | trainingDb.Clients.Remove(client);
28 | return TypedResults.Ok(client);
29 | }
30 |
31 | public Results, NotFound> CreateClient(Client client)
32 | {
33 | trainingDb.Clients.Add(client);
34 | return TypedResults.Created($"/clients/{client.Id}", client);
35 | }
36 |
37 | public async Task>, NotFound>> GetClients()
38 | {
39 | var clients = await trainingDb.Clients.ToListAsync();
40 | return TypedResults.Ok(clients);
41 | }
42 | }
--------------------------------------------------------------------------------
/src/Services/TrainersService.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http.HttpResults;
2 | using Microsoft.EntityFrameworkCore;
3 | using TrainingApi.Shared;
4 |
5 | namespace TrainingApi.Services;
6 |
7 | public class TrainersService(TrainingDb trainingDb)
8 | {
9 | public async Task, NotFound>> UpdateTrainerById(int id, Trainer updatedTrainer)
10 | {
11 | var trainer = await trainingDb.Trainers.FindAsync(id);
12 | if (trainer is null) return TypedResults.NotFound();
13 | trainer = updatedTrainer;
14 | await trainingDb.SaveChangesAsync();
15 | return TypedResults.Created($"/trainers/{trainer.Id}", trainer);
16 | }
17 |
18 | public Results, NotFound> CreateTrainer(Trainer trainer)
19 | {
20 | trainingDb.Trainers.Add(trainer);
21 | return TypedResults.Created($"/trainers/{trainer.Id}", trainer);
22 | }
23 |
24 | public async Task>, NotFound>> GetTrainers()
25 | {
26 | var trainers = await trainingDb.Trainers.ToListAsync();
27 | return Results.Extensions.Xml(trainers);
28 | }
29 |
30 | public async Task, NotFound>> DeleteTrainerById(int id)
31 | {
32 | var trainer = await trainingDb.Trainers.FindAsync(id);
33 | if (trainer is null) return TypedResults.NotFound();
34 | trainingDb.Trainers.Remove(trainer);
35 | return TypedResults.Ok(trainer);
36 | }
37 |
38 | public async Task, NotFound>> GetTrainerById(int id)
39 | {
40 | var trainer = await trainingDb.Trainers.FindAsync(id);
41 | return trainer is null ? TypedResults.NotFound() : TypedResults.Ok(trainer);
42 | }
43 | }
--------------------------------------------------------------------------------
/src/TrainingApi.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net10.0
5 | enable
6 | enable
7 | f5d748ed-7c5f-4b69-a11a-f2e2b88c6c7d
8 | true
9 |
10 |
11 |
12 | $(MSBuildProjectDirectory)
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | all
24 | runtime; build; native; contentfiles; analyzers
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/TrainingApi.http:
--------------------------------------------------------------------------------
1 | @HostAddress = http://localhost:5198
2 |
3 | GET {{HostAddress}}/clients/1
4 |
5 | ###
6 |
7 | GET {{HostAddress}}/clients/666
8 |
9 |
--------------------------------------------------------------------------------
/src/TrainingApi.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.1.1",
3 | "info": {
4 | "title": "TrainingApi | v1",
5 | "version": "1.0.0"
6 | },
7 | "paths": {
8 | "/clients/{id}": {
9 | "get": {
10 | "tags": [
11 | "Clients"
12 | ],
13 | "description": "Get a client",
14 | "operationId": "GetClient",
15 | "parameters": [
16 | {
17 | "name": "id",
18 | "in": "path",
19 | "description": "The unique identifier of the client, assigned by the system when the client is created",
20 | "required": true,
21 | "schema": {
22 | "pattern": "^-?(?:0|[1-9]\\d*)$",
23 | "type": [
24 | "integer",
25 | "string"
26 | ],
27 | "format": "int32"
28 | }
29 | }
30 | ],
31 | "responses": {
32 | "200": {
33 | "description": "OK",
34 | "content": {
35 | "application/json": {
36 | "schema": {
37 | "$ref": "#/components/schemas/Client"
38 | }
39 | }
40 | }
41 | },
42 | "404": {
43 | "description": "Not Found"
44 | }
45 | }
46 | },
47 | "put": {
48 | "tags": [
49 | "Clients"
50 | ],
51 | "description": "Update a client",
52 | "operationId": "UpdateClient",
53 | "parameters": [
54 | {
55 | "name": "id",
56 | "in": "path",
57 | "description": "The unique identifier of the client, assigned by the system when the client is created",
58 | "required": true,
59 | "schema": {
60 | "pattern": "^-?(?:0|[1-9]\\d*)$",
61 | "type": [
62 | "integer",
63 | "string"
64 | ],
65 | "format": "int32"
66 | }
67 | }
68 | ],
69 | "requestBody": {
70 | "content": {
71 | "application/json": {
72 | "schema": {
73 | "$ref": "#/components/schemas/Client"
74 | }
75 | }
76 | },
77 | "required": true
78 | },
79 | "responses": {
80 | "201": {
81 | "description": "Created",
82 | "content": {
83 | "application/json": {
84 | "schema": {
85 | "$ref": "#/components/schemas/Client"
86 | }
87 | }
88 | }
89 | },
90 | "404": {
91 | "description": "Not Found"
92 | }
93 | }
94 | },
95 | "post": {
96 | "tags": [
97 | "Clients"
98 | ],
99 | "description": "Delete a client",
100 | "operationId": "DeleteClient",
101 | "parameters": [
102 | {
103 | "name": "id",
104 | "in": "path",
105 | "description": "The unique identifier of the client, assigned by the system when the client is created",
106 | "required": true,
107 | "schema": {
108 | "pattern": "^-?(?:0|[1-9]\\d*)$",
109 | "type": [
110 | "integer",
111 | "string"
112 | ],
113 | "format": "int32"
114 | }
115 | }
116 | ],
117 | "responses": {
118 | "200": {
119 | "description": "OK",
120 | "content": {
121 | "application/json": {
122 | "schema": {
123 | "$ref": "#/components/schemas/Client"
124 | }
125 | }
126 | }
127 | },
128 | "404": {
129 | "description": "Not Found"
130 | }
131 | }
132 | }
133 | },
134 | "/clients": {
135 | "post": {
136 | "tags": [
137 | "Clients"
138 | ],
139 | "description": "Create a client",
140 | "operationId": "CreateClient",
141 | "requestBody": {
142 | "content": {
143 | "application/json": {
144 | "schema": {
145 | "$ref": "#/components/schemas/Client"
146 | }
147 | }
148 | },
149 | "required": true
150 | },
151 | "responses": {
152 | "201": {
153 | "description": "Created",
154 | "content": {
155 | "application/json": {
156 | "schema": {
157 | "$ref": "#/components/schemas/Client"
158 | }
159 | }
160 | }
161 | },
162 | "404": {
163 | "description": "Not Found"
164 | }
165 | }
166 | }
167 | },
168 | "/trainers": {
169 | "get": {
170 | "tags": [
171 | "Trainers"
172 | ],
173 | "description": "List all trainers",
174 | "operationId": "ListTrainers",
175 | "responses": {
176 | "200": {
177 | "description": "OK",
178 | "content": {
179 | "application/xml": {
180 | "schema": {
181 | "type": "array",
182 | "items": {
183 | "$ref": "#/components/schemas/Trainer"
184 | }
185 | }
186 | }
187 | }
188 | },
189 | "404": {
190 | "description": "Not Found"
191 | }
192 | },
193 | "security": [
194 | { }
195 | ]
196 | },
197 | "post": {
198 | "tags": [
199 | "Trainers"
200 | ],
201 | "description": "Create a trainer",
202 | "operationId": "CreateTrainer",
203 | "requestBody": {
204 | "content": {
205 | "application/json": {
206 | "schema": {
207 | "$ref": "#/components/schemas/Trainer"
208 | }
209 | }
210 | },
211 | "required": true
212 | },
213 | "responses": {
214 | "201": {
215 | "description": "Created",
216 | "content": {
217 | "application/json": {
218 | "schema": {
219 | "$ref": "#/components/schemas/Trainer"
220 | }
221 | }
222 | }
223 | },
224 | "404": {
225 | "description": "Not Found"
226 | }
227 | },
228 | "security": [
229 | { }
230 | ]
231 | }
232 | },
233 | "/trainers/{id}": {
234 | "put": {
235 | "tags": [
236 | "Trainers"
237 | ],
238 | "description": "Update a trainer",
239 | "operationId": "UpdateTrainer",
240 | "parameters": [
241 | {
242 | "name": "id",
243 | "in": "path",
244 | "description": "The unique identifier of the trainer, assigned by the system when the client is created",
245 | "required": true,
246 | "schema": {
247 | "pattern": "^-?(?:0|[1-9]\\d*)$",
248 | "type": [
249 | "integer",
250 | "string"
251 | ],
252 | "format": "int32"
253 | }
254 | }
255 | ],
256 | "requestBody": {
257 | "content": {
258 | "application/json": {
259 | "schema": {
260 | "$ref": "#/components/schemas/Trainer"
261 | }
262 | }
263 | },
264 | "required": true
265 | },
266 | "responses": {
267 | "201": {
268 | "description": "Created",
269 | "content": {
270 | "application/json": {
271 | "schema": {
272 | "$ref": "#/components/schemas/Trainer"
273 | }
274 | }
275 | }
276 | },
277 | "404": {
278 | "description": "Not Found"
279 | }
280 | },
281 | "security": [
282 | { }
283 | ]
284 | },
285 | "delete": {
286 | "tags": [
287 | "Trainers"
288 | ],
289 | "description": "Delete a trainer",
290 | "operationId": "DeleteTrainer",
291 | "parameters": [
292 | {
293 | "name": "id",
294 | "in": "path",
295 | "description": "The unique identifier of the trainer, assigned by the system when the client is created",
296 | "required": true,
297 | "schema": {
298 | "pattern": "^-?(?:0|[1-9]\\d*)$",
299 | "type": [
300 | "integer",
301 | "string"
302 | ],
303 | "format": "int32"
304 | }
305 | }
306 | ],
307 | "responses": {
308 | "200": {
309 | "description": "OK",
310 | "content": {
311 | "application/json": {
312 | "schema": {
313 | "$ref": "#/components/schemas/Trainer"
314 | }
315 | }
316 | }
317 | },
318 | "404": {
319 | "description": "Not Found"
320 | }
321 | },
322 | "security": [
323 | { }
324 | ]
325 | }
326 | }
327 | },
328 | "components": {
329 | "schemas": {
330 | "Client": {
331 | "required": [
332 | "id",
333 | "firstName",
334 | "lastName",
335 | "email",
336 | "weight",
337 | "height",
338 | "birthDate"
339 | ],
340 | "type": "object",
341 | "properties": {
342 | "id": {
343 | "pattern": "^-?(?:0|[1-9]\\d*)$",
344 | "type": [
345 | "integer",
346 | "string"
347 | ],
348 | "description": "The unique identifier of the client, assigned by the system when the client is created.",
349 | "format": "int32"
350 | },
351 | "firstName": {
352 | "type": "string",
353 | "description": "The first name of the client."
354 | },
355 | "lastName": {
356 | "type": "string",
357 | "description": "The last name of the client."
358 | },
359 | "email": {
360 | "type": "string",
361 | "description": "The email of the client."
362 | },
363 | "weight": {
364 | "pattern": "^-?(?:0|[1-9]\\d*)$",
365 | "type": [
366 | "integer",
367 | "string"
368 | ],
369 | "description": "The weight of the client in pounds, rounded to the nearest pound.",
370 | "format": "int32"
371 | },
372 | "height": {
373 | "pattern": "^-?(?:0|[1-9]\\d*)$",
374 | "type": [
375 | "integer",
376 | "string"
377 | ],
378 | "description": "The height of the client in inches, rounded to the nearest inch.",
379 | "format": "int32"
380 | },
381 | "birthDate": {
382 | "type": "string",
383 | "description": "The date of birth of the client.",
384 | "format": "date-time"
385 | }
386 | },
387 | "description": "Represents a client with personal information.",
388 | "example": {
389 | "id": 1,
390 | "firstName": "Jane",
391 | "lastName": "Smith",
392 | "email": "jane.smith@email.com",
393 | "weight": 60,
394 | "height": 170,
395 | "birthDate": "1990-01-01T00:00:00.0000000-08:00"
396 | }
397 | },
398 | "Level": {
399 | "enum": [
400 | "Junior",
401 | "Senior",
402 | "Elite"
403 | ],
404 | "description": "The level of the trainer."
405 | },
406 | "Trainer": {
407 | "type": "object",
408 | "properties": {
409 | "id": {
410 | "pattern": "^-?(?:0|[1-9]\\d*)$",
411 | "type": [
412 | "integer",
413 | "string"
414 | ],
415 | "description": "The unique identifier of the trainer, assigned by the system when the trainer is created.",
416 | "format": "int32"
417 | },
418 | "firstName": {
419 | "type": "string",
420 | "description": "The first name of the trainer."
421 | },
422 | "lastName": {
423 | "type": "string",
424 | "description": "The last name of the trainer."
425 | },
426 | "email": {
427 | "type": "string",
428 | "description": "The email address of the trainer."
429 | },
430 | "level": {
431 | "$ref": "#/components/schemas/Level"
432 | },
433 | "isCertificationActive": {
434 | "type": "boolean",
435 | "description": "Indicates whether the trainer's certification is active."
436 | }
437 | },
438 | "example": {
439 | "id": 1,
440 | "firstName": "John",
441 | "lastName": "Doe",
442 | "email": "john.doe@email.com",
443 | "level": "Junior",
444 | "isCertificationActive": false
445 | }
446 | }
447 | },
448 | "securitySchemes": {
449 | "Bearer": {
450 | "type": "http",
451 | "scheme": "Bearer"
452 | }
453 | }
454 | },
455 | "tags": [
456 | {
457 | "name": "Clients"
458 | },
459 | {
460 | "name": "Trainers"
461 | }
462 | ]
463 | }
--------------------------------------------------------------------------------
/src/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "Authentication": {
9 | "DefaultScheme": "Bearer",
10 | "Authentication": {
11 | "Schemes": {
12 | "Bearer": {
13 | "ValidAudiences": [
14 | "http://localhost:1739",
15 | "https://localhost:44360",
16 | "http://localhost:5198",
17 | "https://localhost:7088"
18 | ],
19 | "ValidIssuer": "dotnet-user-jwts"
20 | }
21 | }
22 | },
23 | "Schemes": {
24 | "Bearer": {
25 | "ValidAudiences": [
26 | "http://localhost:1739",
27 | "https://localhost:44360",
28 | "http://localhost:5198",
29 | "https://localhost:7088"
30 | ],
31 | "ValidIssuer": "dotnet-user-jwts"
32 | }
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/src/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | },
8 | "AllowedHosts": "*"
9 | }
10 |
--------------------------------------------------------------------------------
/tests/ApiApplication.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc.Testing;
2 | using Microsoft.EntityFrameworkCore;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Microsoft.Extensions.Hosting;
5 | using Microsoft.AspNetCore.Authentication;
6 | using TrainingApi.Shared;
7 |
8 | internal class ApiApplication : WebApplicationFactory
9 | {
10 | private readonly string _environment;
11 |
12 | public ApiApplication(string environment = "Development")
13 | {
14 | _environment = environment;
15 | }
16 |
17 | protected override IHost CreateHost(IHostBuilder builder)
18 | {
19 | builder.UseEnvironment(_environment);
20 |
21 | builder.ConfigureServices(services =>
22 | {
23 | services.AddScoped(sp =>
24 | {
25 | // Replace SQL with in-memory database for tests
26 | return new DbContextOptionsBuilder()
27 | .UseInMemoryDatabase("Tests")
28 | .UseApplicationServiceProvider(sp)
29 | .Options;
30 | });
31 | });
32 |
33 | return base.CreateHost(builder);
34 | }
35 | }
--------------------------------------------------------------------------------
/tests/IntegrationTests.cs:
--------------------------------------------------------------------------------
1 | using System.Net;
2 | using System.Net.Http.Json;
3 | using TrainingApi.Shared;
4 | using TrainingApi;
5 |
6 | namespace TrainingApi.Tests;
7 |
8 | public class IntegrationTests
9 | {
10 | [Fact]
11 | public async Task GET_Client_ReturnsClient()
12 | {
13 | // Arrange
14 | var app = new ApiApplication();
15 |
16 | // Act
17 | var client = app.CreateClient();
18 | var response = await client.GetAsync("/clients/1");
19 | var responseBody = await response.Content.ReadAsStringAsync();
20 |
21 | // Assert
22 | Assert.Equal(HttpStatusCode.OK, response.StatusCode);
23 | }
24 | }
--------------------------------------------------------------------------------
/tests/TestDbAsyncQueryProvider.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Query;
2 | using System.Linq.Expressions;
3 |
4 | internal class TestDbAsyncQueryProvider : IAsyncQueryProvider
5 | {
6 | private readonly IQueryProvider _inner;
7 |
8 | internal TestDbAsyncQueryProvider(IQueryProvider inner)
9 | {
10 | _inner = inner;
11 | }
12 |
13 | public IQueryable CreateQuery(Expression expression)
14 | {
15 | return new TestDbAsyncEnumerable(expression);
16 | }
17 |
18 | public IQueryable CreateQuery(Expression expression)
19 | {
20 | return new TestDbAsyncEnumerable(expression);
21 | }
22 |
23 | public object Execute(Expression expression)
24 | {
25 | return CompileExpressionItem