Skip to content

Commit

Permalink
Endpoint Validation Update to Factory (stphnwlsh#28)
Browse files Browse the repository at this point in the history
* Update Endpoint Validation to use a factory
* Inspiration from [Minimal API validation with ASP.NET 7.0 Endpoint Filters](https://benfoster.io/blog/minimal-api-validation-endpoint-filters/)
* Reduce application models to minimal records
* Remove the Entity model from the application models
* Removed DateCreated and DateModified from Application models
* Test Cleanup and Reduce Coverage Threshold
* Code Formatting
  • Loading branch information
stphnwlsh authored Feb 20, 2024
1 parent c677263 commit 4a35083
Show file tree
Hide file tree
Showing 50 changed files with 409 additions and 414 deletions.
6 changes: 4 additions & 2 deletions NUGET.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ dotnet new cleanminimalapi --name {YOUR_SOLUTION_NAMESPACE} --au "{YOU_AUTHORS_N

## 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), and/or follow me on [Medium](https://stphnwlsh.medium.com) or [GitHub](https://github.com/stphnwlsh).

[!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/guidelines/download-assets-sm-1.svg)](https://www.buymeacoffee.com/stphnwlsh)
If you want to see more updates or more projects then please support me at [GitHub Sponsors](https://github.com/stphnwlsh) or [Buy Me A Coffee](https://www.buymeacoffee.com/stphnwlsh)

![Please Sponsor Me](docs/sponsor.jpg)
11 changes: 8 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/stphnwlsh/cleanminimalapi/build-pipeline.yml?label=Build%20Pipeline%20&logo=github&style=for-the-badge)](https://github.com/stphnwlsh/CleanMinimalApi/actions/workflows/build-pipeline.yml)
[![Codecov](https://img.shields.io/codecov/c/github/stphnwlsh/CleanMinimalApi?label=Code%20Coverage&logo=codecov&logoColor=white&style=for-the-badge)](https://codecov.io/gh/stphnwlsh/CleanMinimalApi)
![Nuget](https://img.shields.io/nuget/v/CleanMinimalApi.Template?label=nuget%20template&logo=nuget&logoColor=white&style=for-the-badge)
[![Nuget](https://img.shields.io/nuget/v/CleanMinimalApi.Template?label=nuget%20template&logo=nuget&logoColor=white&style=for-the-badge)](https://www.nuget.org/packages/CleanMinimalApi.Template/)
[![GitHub Sponsors](https://img.shields.io/static/v1?label=GitHub%20Sponsors&message=$1&logo=githubsponsors&logoColor=white&color=ea4aaa&style=for-the-badge)](https://github.com/sponsors/stphnwlsh/sponsorships?sponsor=stphnwlsh&tier_id=333950)
[![Buy Me A Coffee](https://img.shields.io/static/v1?label=Buy%20Me%20A%20Coffee&message=$1&logo=buymeacoffee&logoColor=white&color=ffdd00&style=for-the-badge)](https://www.buymeacoffee.com/stphnwlsh)

This is a template API using a streamlined version of [Clean Architecture](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) alongside .NET's [Minimal APIs](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-7.0).

Expand Down Expand Up @@ -73,9 +75,12 @@ This sample would not have been possible without gaining inspiration from the fo
- [Damian Edwards - Minimal API Playground](https://github.com/DamianEdwards/MinimalApiPlayground)
- [Scott Hanselman - Minimal APIs in .NET 6 but where are the Unit Tests?](https://www.hanselman.com/blog/minimal-apis-in-net-6-but-where-are-the-unit-tests)
- [Andrew Lock - Reducing log verbosity with Serilog RequestLogging](https://andrewlock.net/using-serilog-aspnetcore-in-asp-net-core-3-reducing-log-verbosity/)
- [Ben Foster - Minimal API validation with ASP.NET 7.0 Endpoint Filters](https://benfoster.io/blog/minimal-api-validation-endpoint-filters/)

## 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), [Medium](https://stphnwlsh.medium.com) or [GitHub](https://github.com/stphnwlsh), and consider supporting me by sponsoring the project.
If you like this, or want to checkout my other work, please connect with me on [LinkedIn](https://www.linkedin.com/in/stphnwlsh), and/or follow me on [Medium](https://stphnwlsh.medium.com) or [GitHub](https://github.com/stphnwlsh).

[!["GitHub Sponsor Me"](https://github.blog/wp-content/uploads/2019/05/mona-heart-featured.png)](https://github.com/sponsors/stphnwlsh)
If you want to see more updates or more projects then please support me at [GitHub Sponsors](https://github.com/stphnwlsh) or [Buy Me A Coffee](https://www.buymeacoffee.com/stphnwlsh)

![Please Sponsor Me](docs/sponsor.jpg)
2 changes: 1 addition & 1 deletion codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ coverage:
project:
default:
target: 100%
threshold: 3%
threshold: 2%
Binary file added docs/sponsor.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 1 addition & 9 deletions src/Application/Authors/Entities/Author.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,5 @@
namespace CleanMinimalApi.Application.Authors.Entities;

using Application.Common.Entities;
using Application.Reviews.Entities;

public record Author : Entity
{
public string FirstName { get; set; }

public string LastName { get; set; }

public ICollection<Review> Reviews { get; set; }
}
public record Author(Guid Id, string FirstName, string LastName, ICollection<Review> Reviews = null);
9 changes: 1 addition & 8 deletions src/Application/Authors/Entities/ReviewAuthor.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
namespace CleanMinimalApi.Application.Authors.Entities;

using Application.Common.Entities;

public record ReviewAuthor : Entity
{
public string FirstName { get; set; }

public string LastName { get; set; }
}
public record ReviewAuthor(Guid Id, string FirstName, string LastName);
10 changes: 0 additions & 10 deletions src/Application/Common/Entities/Entity.cs

This file was deleted.

8 changes: 1 addition & 7 deletions src/Application/Movies/Entities/Movie.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,5 @@
namespace CleanMinimalApi.Application.Movies.Entities;

using Application.Common.Entities;
using Application.Reviews.Entities;

public record Movie : Entity
{
public string Title { get; init; }

public ICollection<Review> Reviews { get; init; }
}
public record Movie(Guid Id, string Title, ICollection<Review> Reviews = null);
8 changes: 0 additions & 8 deletions src/Application/Movies/Entities/ReviewMovie.cs

This file was deleted.

3 changes: 3 additions & 0 deletions src/Application/Movies/Entities/ReviewedMovie.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace CleanMinimalApi.Application.Movies.Entities;

public record ReviewedMovie(Guid Id, string Title);
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public async Task<Review> Handle(CreateReviewCommand request, CancellationToken
NotFoundException.Throw(EntityType.Movie);
}

return await reviewsRepository.CreateReview(request.AuthorId, request.MovieId, request.Stars, cancellationToken);
return await reviewsRepository
.CreateReview(request.AuthorId, request.MovieId, request.Stars, cancellationToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public async Task<bool> Handle(UpdateReviewCommand request, CancellationToken ca
NotFoundException.Throw(EntityType.Movie);
}

return await reviewsRepository.UpdateReview(request.Id, request.AuthorId, request.MovieId, request.Stars, cancellationToken);
return await reviewsRepository
.UpdateReview(request.Id, request.AuthorId, request.MovieId, request.Stars, cancellationToken);
}
}
14 changes: 5 additions & 9 deletions src/Application/Reviews/Entities/Review.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
namespace CleanMinimalApi.Application.Reviews.Entities;

using Application.Authors.Entities;
using Application.Common.Entities;
using Application.Movies.Entities;

public record Review : Entity
{
public int Stars { get; init; }

public ReviewMovie ReviewedMovie { get; init; }

public ReviewAuthor ReviewAuthor { get; init; }
}
public record Review(
Guid Id,
int Stars,
ReviewedMovie ReviewedMovie = null,
ReviewAuthor ReviewAuthor = null);
13 changes: 11 additions & 2 deletions src/Application/Reviews/IReviewsRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ namespace CleanMinimalApi.Application.Reviews;

public interface IReviewsRepository
{
Task<Review> CreateReview(Guid authorId, Guid movieId, int stars, CancellationToken cancellationToken);
Task<Review> CreateReview(
Guid authorId,
Guid movieId,
int stars,
CancellationToken cancellationToken);

Task<bool> DeleteReview(Guid id, CancellationToken cancellationToken);

Expand All @@ -15,5 +19,10 @@ public interface IReviewsRepository

Task<bool> ReviewExists(Guid id, CancellationToken cancellationToken);

Task<bool> UpdateReview(Guid id, Guid authorId, Guid movieId, int stars, CancellationToken cancellationToken);
Task<bool> UpdateReview(
Guid id,
Guid authorId,
Guid movieId,
int stars,
CancellationToken cancellationToken);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,14 @@ public class GetVersionHandler : IRequestHandler<GetVersionQuery, Version>
{
public Task<Version> Handle(GetVersionQuery request, CancellationToken cancellationToken)
{
var assembly = Assembly.GetEntryAssembly();

var version = new Version
{
FileVersion = $"{Assembly.GetEntryAssembly()?.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version}",
InformationalVersion = $"{Assembly.GetEntryAssembly()?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion}"
FileVersion =
$"{assembly?.GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version}",
InformationalVersion =
$"{assembly?.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion}"
};

return Task.FromResult(version);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ internal class EntityFrameworkMovieReviewsRepository : IAuthorsRepository, IMovi
private readonly TimeProvider timeProvider;
private readonly IMapper mapper;

public EntityFrameworkMovieReviewsRepository(MovieReviewsDbContext context, TimeProvider timeProvider, IMapper mapper)
public EntityFrameworkMovieReviewsRepository(
MovieReviewsDbContext context,
TimeProvider timeProvider,
IMapper mapper)
{
this.context = context;
this.timeProvider = timeProvider;
Expand All @@ -38,14 +41,22 @@ public EntityFrameworkMovieReviewsRepository(MovieReviewsDbContext context, Time

public virtual async Task<List<ApplicationAuthor>> GetAuthors(CancellationToken cancellationToken)
{
var authors = await this.context.Authors.Include(a => a.Reviews).ThenInclude(r => r.ReviewedMovie).AsNoTracking().ToListAsync(cancellationToken);
var authors = await this.context.Authors
.Include(a => a.Reviews)
.ThenInclude(r => r.ReviewedMovie)
.AsNoTracking()
.ToListAsync(cancellationToken);

return this.mapper.Map<List<ApplicationAuthor>>(authors);
}

public virtual async Task<ApplicationAuthor> GetAuthorById(Guid id, CancellationToken cancellationToken)
{
var author = await this.context.Authors.Where(r => r.Id == id).Include(a => a.Reviews).ThenInclude(r => r.ReviewedMovie).AsNoTracking().FirstOrDefaultAsync(cancellationToken);
var author = await this.context.Authors
.Where(r => r.Id == id).Include(a => a.Reviews)
.ThenInclude(r => r.ReviewedMovie)
.AsNoTracking()
.FirstOrDefaultAsync(cancellationToken);

return this.mapper.Map<ApplicationAuthor>(author);
;
Expand All @@ -62,14 +73,23 @@ public virtual async Task<bool> AuthorExists(Guid id, CancellationToken cancella

public virtual async Task<List<ApplicationMovie>> GetMovies(CancellationToken cancellationToken)
{
var result = await this.context.Movies.Include(m => m.Reviews).ThenInclude(r => r.ReviewAuthor).AsNoTracking().ToListAsync(cancellationToken);
var result = await this.context.Movies
.Include(m => m.Reviews)
.ThenInclude(r => r.ReviewAuthor)
.AsNoTracking()
.ToListAsync(cancellationToken);

return this.mapper.Map<List<ApplicationMovie>>(result);
}

public virtual async Task<ApplicationMovie> GetMovieById(Guid id, CancellationToken cancellationToken)
{
var result = await this.context.Movies.Where(r => r.Id == id).Include(m => m.Reviews).ThenInclude(r => r.ReviewAuthor).AsNoTracking().FirstOrDefaultAsync(cancellationToken);
var result = await this.context.Movies
.Where(r => r.Id == id)
.Include(m => m.Reviews)
.ThenInclude(r => r.ReviewAuthor)
.AsNoTracking()
.FirstOrDefaultAsync(cancellationToken);

return this.mapper.Map<ApplicationMovie>(result);
}
Expand All @@ -83,7 +103,11 @@ public virtual async Task<bool> MovieExists(Guid id, CancellationToken cancellat

#region Reviews

public async Task<ApplicationReview> CreateReview(Guid authorId, Guid movieId, int stars, CancellationToken cancellationToken)
public async Task<ApplicationReview> CreateReview(
Guid authorId,
Guid movieId,
int stars,
CancellationToken cancellationToken)
{
var review = new Review
{
Expand All @@ -98,7 +122,12 @@ public async Task<ApplicationReview> CreateReview(Guid authorId, Guid movieId, i

_ = await this.context.SaveChangesAsync(cancellationToken);

var result = await this.context.Reviews.Where(r => r.Id == id).Include(r => r.ReviewAuthor).Include(r => r.ReviewedMovie).AsNoTracking().FirstAsync(cancellationToken);
var result = await this.context.Reviews
.Where(r => r.Id == id)
.Include(r => r.ReviewAuthor)
.Include(r => r.ReviewedMovie)
.AsNoTracking()
.FirstAsync(cancellationToken);

return this.mapper.Map<ApplicationReview>(result);
}
Expand All @@ -120,14 +149,23 @@ public async Task<bool> DeleteReview(Guid id, CancellationToken cancellationToke

public async Task<List<ApplicationReview>> GetReviews(CancellationToken cancellationToken)
{
var result = await this.context.Reviews.Include(r => r.ReviewAuthor).Include(r => r.ReviewedMovie).AsNoTracking().ToListAsync(cancellationToken);
var result = await this.context.Reviews
.Include(r => r.ReviewAuthor)
.Include(r => r.ReviewedMovie)
.AsNoTracking()
.ToListAsync(cancellationToken);

return this.mapper.Map<List<ApplicationReview>>(result);
}

public async Task<ApplicationReview> GetReviewById(Guid id, CancellationToken cancellationToken)
{
var result = await this.context.Reviews.Where(r => r.Id == id).Include(r => r.ReviewAuthor).Include(r => r.ReviewedMovie).AsNoTracking().FirstOrDefaultAsync(cancellationToken);
var result = await this.context.Reviews
.Where(r => r.Id == id)
.Include(r => r.ReviewAuthor)
.Include(r => r.ReviewedMovie)
.AsNoTracking()
.FirstOrDefaultAsync(cancellationToken);

return this.mapper.Map<ApplicationReview>(result);
}
Expand All @@ -137,7 +175,12 @@ public async Task<bool> ReviewExists(Guid id, CancellationToken cancellationToke
return await this.context.Reviews.AnyAsync(r => r.Id == id, cancellationToken);
}

public async Task<bool> UpdateReview(Guid id, Guid authorId, Guid movieId, int stars, CancellationToken cancellationToken)
public async Task<bool> UpdateReview(
Guid id,
Guid authorId,
Guid movieId,
int stars,
CancellationToken cancellationToken)
{
try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ internal class AuthorMappingProfile : Profile
{
public AuthorMappingProfile()
{
_ = this.CreateMap<Infrastructure.Author, Application.Author>()
_ = this.CreateMap<Application.Author, Infrastructure.Author>()
.ForMember(d => d.DateCreated, o => o.Ignore())
.ForMember(d => d.DateModified, o => o.Ignore())
.ReverseMap();

_ = this.CreateMap<Infrastructure.Author, Application.ReviewAuthor>();
_ = this.CreateMap<Application.ReviewAuthor, Infrastructure.Author>()
.ForMember(d => d.Reviews, o => o.Ignore())
.ForMember(d => d.DateCreated, o => o.Ignore())
.ForMember(d => d.DateModified, o => o.Ignore())
.ReverseMap();
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,15 @@ internal class MovieMappingProfile : Profile
{
public MovieMappingProfile()
{
_ = this.CreateMap<Infrastructure.Movie, Application.Movie>()
_ = this.CreateMap<Application.Movie, Infrastructure.Movie>()
.ForMember(d => d.DateCreated, o => o.Ignore())
.ForMember(d => d.DateModified, o => o.Ignore())
.ReverseMap();

_ = this.CreateMap<Infrastructure.Movie, Application.ReviewMovie>();
_ = this.CreateMap<Application.ReviewedMovie, Infrastructure.Movie>()
.ForMember(d => d.Reviews, o => o.Ignore())
.ForMember(d => d.DateCreated, o => o.Ignore())
.ForMember(d => d.DateModified, o => o.Ignore())
.ReverseMap();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ public ReviewMappingProfile()
{
_ = this.CreateMap<Application.Review, Infrastructure.Review>()
.ForMember(d => d.ReviewAuthorId, o => o.Ignore())
.ForMember(d => d.ReviewAuthor, o => o.MapFrom(s => s.ReviewAuthor))
.ForMember(d => d.ReviewedMovieId, o => o.Ignore())
.ForMember(d => d.ReviewedMovie, o => o.MapFrom(s => s.ReviewedMovie))
.ForMember(d => d.DateCreated, o => o.Ignore())
.ForMember(d => d.DateModified, o => o.Ignore())
.ReverseMap();
}
}
4 changes: 2 additions & 2 deletions src/Infrastructure/Databases/MoviesReviews/Models/Author.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
namespace CleanMinimalApi.Infrastructure.Databases.MoviesReviews.Models;

using System.ComponentModel.DataAnnotations.Schema;
using System.Diagnostics.CodeAnalysis;

[ExcludeFromCodeCoverage]
internal record Author : Entity
{
public string FirstName { get; init; }

public string LastName { get; init; }

[InverseProperty("ReviewAuthor")]
public ICollection<Review> Reviews { get; init; }
}
Loading

0 comments on commit 4a35083

Please sign in to comment.