Skip to content

Commit

Permalink
Refactoring to Update - Endpoints, Validation and Swagger (stphnwlsh#21)
Browse files Browse the repository at this point in the history
- Moving request validation to the Presentation project
- Use IEndpointFilter to capture validation
- Refactoring endpoints into mapping and methods
- Update Swagger implementation
- Adding more test coverage
- General cleanup and refactoring
  • Loading branch information
stphnwlsh authored Aug 20, 2023
1 parent ca83b34 commit 79c04db
Show file tree
Hide file tree
Showing 80 changed files with 2,072 additions and 1,041 deletions.
9 changes: 8 additions & 1 deletion CleanMinimalApi.sln
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{F76F0E9E-98D
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{D79C9477-FC71-4927-BDB1-46982F6EB423}"
ProjectSection(SolutionItems) = preProject
build\Dockerfile = Dockerfile
build\Dockerfile = build\Dockerfile
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{9DBFD6CA-EFB8-43C7-9BAB-E318EE6F134B}"
Expand All @@ -34,6 +34,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application.Tests.Unit", "t
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Presentation.Tests.Integration", "tests\Presentation.Tests.Integration\Presentation.Tests.Integration.csproj", "{DB7F98AB-3D02-4652-80BC-681A00168ECC}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Presentation.Tests.Unit", "tests\Presentation.Tests.Unit\Presentation.Tests.Unit.csproj", "{77B504BC-C261-4998-9B09-E2BD1A477AB7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -64,6 +66,10 @@ Global
{DB7F98AB-3D02-4652-80BC-681A00168ECC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DB7F98AB-3D02-4652-80BC-681A00168ECC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DB7F98AB-3D02-4652-80BC-681A00168ECC}.Release|Any CPU.Build.0 = Release|Any CPU
{77B504BC-C261-4998-9B09-E2BD1A477AB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{77B504BC-C261-4998-9B09-E2BD1A477AB7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{77B504BC-C261-4998-9B09-E2BD1A477AB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{77B504BC-C261-4998-9B09-E2BD1A477AB7}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -75,6 +81,7 @@ Global
{7704E123-22D3-4001-A020-BDFD8F7F3E75} = {9DBFD6CA-EFB8-43C7-9BAB-E318EE6F134B}
{27106CF1-B15D-4274-98BE-3316495ACEEC} = {9DBFD6CA-EFB8-43C7-9BAB-E318EE6F134B}
{DB7F98AB-3D02-4652-80BC-681A00168ECC} = {9DBFD6CA-EFB8-43C7-9BAB-E318EE6F134B}
{77B504BC-C261-4998-9B09-E2BD1A477AB7} = {9DBFD6CA-EFB8-43C7-9BAB-E318EE6F134B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {507BE0AF-5740-4AE0-8EFB-2F84E40BFADB}
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ RUN dotnet build --no-restore -c Release -v minimal -p:VersionPrefix=${VERSION_P

# Dotnet Test
FROM build AS test
RUN dotnet test --no-restore --no-build -c Release -v minimal -p:CollectCoverage=true -p:CoverletOutput=../results/ -p:MergeWith="../results/coverage.json" -p:CoverletOutputFormat=opencover%2cjson -m:1
RUN dotnet test --no-restore --no-build -c Release -v minimal -p:CollectCoverage=true -p:CoverletOutput=../results/ -p:MergeWith="../results/coverage.json" -p:CoverletOutputFormat=opencover%2cjson -m:1 -p:ExcludeByFile="**/program.cs"

FROM scratch AS coverage
COPY --from=test /sln/tests/results/*.xml .
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,6 @@ This sample would not have been possible without gaining inspiration from the fo

## Connect and Support

If you like this, or want to checkout my other work, please connect with me on [LinkedIn](https://www.linkedin.com/in/stphnwlsh), [Twitter](https://twitter.com/stphnwlsh) or [GitHub](https://github.com/stphnwlsh), and consider supporting me at [Buy Me a Coffee].
If you like this, or want to checkout my other work, please connect with me on [LinkedIn](https://www.linkedin.com/in/stphnwlsh), [Twitter](https://twitter.com/stphnwlsh) or [GitHub](https://github.com/stphnwlsh), and consider supporting me by sponsoring the project.

[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/guidelines/download-assets-sm-1.svg)](https://www.buymeacoffee.com/stphnwlsh)
[!["GitHub Sponsor Me"](https://github.blog/wp-content/uploads/2019/05/mona-heart-featured.png)](https://github.com/sponsors/stphnwlsh)
1 change: 0 additions & 1 deletion src/Application/Application.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.5.1" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="11.1.0" />
</ItemGroup>

Expand Down

This file was deleted.

36 changes: 0 additions & 36 deletions src/Application/Common/Behaviours/ValidationBehaviour.cs

This file was deleted.

6 changes: 3 additions & 3 deletions src/Application/Common/Exceptions/NotFoundException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ namespace CleanMinimalApi.Application.Common.Exceptions;
using Enums;

[Serializable]
[ExcludeFromCodeCoverage]
public class NotFoundException : Exception
{
public NotFoundException(string message)
: base(message)
{
}

[ExcludeFromCodeCoverage]
protected NotFoundException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}

/// <summary>Throws an <see cref="NotFoundException"/> if <paramref name="argument"/> is null.</summary>
/// <summary>Throws a <see cref="NotFoundException"/> if <paramref name="argument"/> is null.</summary>
/// <param name="argument">The reference type argument to validate as non-null.</param>
/// <param name="entityType">The entity type of the <paramref name="argument"/> parameter.</param>
public static void ThrowIfNull(object argument, EntityType entityType)
Expand All @@ -30,7 +30,7 @@ public static void ThrowIfNull(object argument, EntityType entityType)
}
}

/// <summary>Throws an <see cref="NotFoundException"/></summary>
/// <summary>Throws a <see cref="NotFoundException"/></summary>
/// <param name="entityType">The entity type of the <paramref name="argument"/> parameter.</param>
public static void Throw(EntityType entityType)
{
Expand Down
5 changes: 0 additions & 5 deletions src/Application/DependencyInjection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ namespace CleanMinimalApi.Application;

using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Common.Behaviours;
using FluentValidation;
using MediatR;
using Microsoft.Extensions.DependencyInjection;

Expand All @@ -13,9 +11,6 @@ public static class DependencyInjection
public static IServiceCollection AddApplication(this IServiceCollection services)
{
_ = services.AddMediatR(Assembly.GetExecutingAssembly());
_ = services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly(), ServiceLifetime.Transient);

_ = services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));

return services;
}
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,42 @@ namespace CleanMinimalApi.Application.Reviews.Commands.UpdateReview;

using System.Threading;
using System.Threading.Tasks;
using CleanMinimalApi.Application.Authors;
using CleanMinimalApi.Application.Movies;
using Common.Enums;
using Common.Exceptions;
using MediatR;

public class UpdateReviewHandler : IRequestHandler<UpdateReviewCommand, bool>
{
private readonly IReviewsRepository repository;
private readonly IAuthorsRepository authorsRepository;
private readonly IMoviesRepository moviesRepository;
private readonly IReviewsRepository reviewsRepository;

public UpdateReviewHandler(IReviewsRepository repository)
public UpdateReviewHandler(IAuthorsRepository authorsRepository, IMoviesRepository moviesRepository, IReviewsRepository reviewsRepository)
{
this.repository = repository;
this.authorsRepository = authorsRepository;
this.moviesRepository = moviesRepository;
this.reviewsRepository = reviewsRepository;
}

public async Task<bool> Handle(UpdateReviewCommand request, CancellationToken cancellationToken)
{
if (!await this.repository.ReviewExists(request.Id, cancellationToken))
if (!await this.reviewsRepository.ReviewExists(request.Id, cancellationToken))
{
NotFoundException.Throw(EntityType.Review);
}

return await this.repository.UpdateReview(request.Id, request.AuthorId, request.MovieId, request.Stars, cancellationToken);
if (!await this.authorsRepository.AuthorExists(request.AuthorId, cancellationToken))
{
NotFoundException.Throw(EntityType.Author);
}

if (!await this.moviesRepository.MovieExists(request.MovieId, cancellationToken))
{
NotFoundException.Throw(EntityType.Movie);
}

return await this.reviewsRepository.UpdateReview(request.Id, request.AuthorId, request.MovieId, request.Stars, cancellationToken);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ namespace CleanMinimalApi.Application.Versions.Queries.GetVersion;

public class GetVersionHandler : IRequestHandler<GetVersionQuery, Version>
{
public async Task<Version> Handle(GetVersionQuery request, CancellationToken cancellationToken)
public Task<Version> Handle(GetVersionQuery request, CancellationToken cancellationToken)
{
return await Task.FromResult(new Version
var version = new Version
{
FileVersion = $"{Assembly.GetEntryAssembly()?.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version}",
InformationalVersion = $"{Assembly.GetEntryAssembly()?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion}"
});
};

return Task.FromResult(version);
}
}
Binary file added src/Infrastructure/.DS_Store
Binary file not shown.
Binary file added src/Infrastructure/Databases/.DS_Store
Binary file not shown.
Binary file not shown.
2 changes: 0 additions & 2 deletions src/Infrastructure/DependencyInjection.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
namespace CleanMinimalApi.Infrastructure;

using System.Diagnostics.CodeAnalysis;
using Application.Authors;
using Application.Movies;
using Application.Reviews;
Expand All @@ -9,7 +8,6 @@ namespace CleanMinimalApi.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
using SimpleDateTimeProvider;

[ExcludeFromCodeCoverage]
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(this IServiceCollection services)
Expand Down
4 changes: 2 additions & 2 deletions src/Infrastructure/Infrastructure.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.0" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="Bogus" Version="34.0.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.10" />
<PackageReference Include="SimpleDateTimeProvider" Version="2022.11.13.46" />
</ItemGroup>

Expand Down
39 changes: 0 additions & 39 deletions src/Presentation/Endpoints/Authors/AuthorsEndpoints.cs

This file was deleted.

67 changes: 67 additions & 0 deletions src/Presentation/Endpoints/AuthorsEndpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
namespace CleanMinimalApi.Presentation.Endpoints;

using CleanMinimalApi.Application.Common.Exceptions;
using CleanMinimalApi.Presentation.Filters;
using MediatR;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Entities = Application.Authors.Entities;
using Queries = Application.Authors.Queries;

public static class AuthorsEndpoints
{
public static WebApplication MapAuthorEndpoints(this WebApplication app)
{
var root = app.MapGroup("/api/authors")
.WithTags("authors")
.WithOpenApi();

_ = root.MapGet("/", GetAuthors)
.Produces<List<Entities.Author>>()
.ProducesProblem(StatusCodes.Status500InternalServerError)
.WithSummary("Lookup all Authors")
.WithDescription("\n GET /Authors");

_ = root.MapGet("/{id}", GetAuthorById)
.AddEndpointFilter<ValidationFilter<Guid>>()
.Produces<Entities.Author>()
.ProducesValidationProblem()
.ProducesProblem(StatusCodes.Status404NotFound)
.ProducesProblem(StatusCodes.Status500InternalServerError)
.WithSummary("Lookup an Author by their Id")
.WithDescription("\n GET /Authors/00000000-0000-0000-0000-000000000000");

return app;
}

public static async Task<IResult> GetAuthors(IMediator mediator)
{
try
{
return Results.Ok(await mediator.Send(new Queries.GetAuthors.GetAuthorsQuery()));
}
catch (Exception ex)
{
return Results.Problem(ex.StackTrace, ex.Message, StatusCodes.Status500InternalServerError);
}
}

public static async Task<IResult> GetAuthorById(Guid id, IMediator mediator)
{
try
{
return Results.Ok(await mediator.Send(new Queries.GetAuthorById.GetAuthorByIdQuery
{
Id = id
}));
}
catch (NotFoundException ex)
{
return Results.NotFound(ex.Message);
}
catch (Exception ex)
{
return Results.Problem(ex.StackTrace, ex.Message, StatusCodes.Status500InternalServerError);
}
}
}
Loading

0 comments on commit 79c04db

Please sign in to comment.