Skip to content

Commit

Permalink
books module (#170)
Browse files Browse the repository at this point in the history
* added books module

* added unit tests for books module, some other small improvements

* removed redundant files

* refreshed README

* removed iis entry

* removed books module from code coverage

* refreshed README

* clean-up around approval tests
  • Loading branch information
lkurzyniec authored Aug 28, 2023
1 parent e56786d commit 8e28f4d
Show file tree
Hide file tree
Showing 45 changed files with 730 additions and 22 deletions.
Binary file added .assets/books-tests.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .assets/books.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore

dist/
*.db

# Git files
*.orig
Expand Down Expand Up @@ -315,6 +316,5 @@ __pycache__/
# OpenCover UI analysis results
OpenCover/


# Local config overrides
appsettings.local.json
3 changes: 3 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<PackageVersion Include="AspNetCore.HealthChecks.SqlServer" Version="7.0.0" />
<PackageVersion Include="AspNetCore.HealthChecks.UI.Client" Version="7.1.0" />
<PackageVersion Include="Microsoft.FeatureManagement.AspNetCore" Version="2.6.1" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" />
<PackageVersion Include="Serilog.Enrichers.Environment" Version="2.2.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
Expand All @@ -16,6 +17,8 @@
<ItemGroup>
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.10" />
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.5" />
<PackageVersion Include="Dapper" Version="2.0.123" />
</ItemGroup>
<ItemGroup>
<PackageVersion Include="dbup-sqlserver" Version="5.0.8" />
Expand Down
5 changes: 4 additions & 1 deletion HappyCode.NetCoreBoilerplate.ruleset
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="HappyCode.NetCoreBoilerplate rules" Description=" " ToolsVersion="16.0">
<RuleSet Name="HappyCode.NetCoreBoilerplate rules" Description=" " ToolsVersion="17.0">
<Rules AnalyzerId="Microsoft.CodeAnalysis.CSharp.Features" RuleNamespace="Microsoft.CodeAnalysis.CSharp.Features">
<Rule Id="IDE0037" Action="None" />
</Rules>
<Rules AnalyzerId="Microsoft.CodeQuality.Analyzers" RuleNamespace="Microsoft.CodeQuality.Analyzers">
<Rule Id="CA1031" Action="Info" />
<Rule Id="CA1062" Action="Info" />
Expand Down
14 changes: 14 additions & 0 deletions HappyCode.NetCoreBoilerplate.sln
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ci", "ci", "{C20F30D0-1383-
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HappyCode.NetCoreBoilerplate.ArchitecturalTests", "test\HappyCode.NetCoreBoilerplate.ArchitecturalTests\HappyCode.NetCoreBoilerplate.ArchitecturalTests.csproj", "{C8166147-F446-4774-AFEC-9C1795B822B8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HappyCode.NetCoreBoilerplate.BooksModule", "src\HappyCode.NetCoreBoilerplate.BooksModule\HappyCode.NetCoreBoilerplate.BooksModule.csproj", "{75AF49EF-1411-408A-B9E7-CE8CADAB7FC8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HappyCode.NetCoreBoilerplate.BooksModule.IntegrationTests", "test\HappyCode.NetCoreBoilerplate.BooksModule.IntegrationTests\HappyCode.NetCoreBoilerplate.BooksModule.IntegrationTests.csproj", "{B3E52219-7B85-42B3-B607-F323401AC878}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -73,6 +77,14 @@ Global
{C8166147-F446-4774-AFEC-9C1795B822B8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C8166147-F446-4774-AFEC-9C1795B822B8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C8166147-F446-4774-AFEC-9C1795B822B8}.Release|Any CPU.Build.0 = Release|Any CPU
{75AF49EF-1411-408A-B9E7-CE8CADAB7FC8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{75AF49EF-1411-408A-B9E7-CE8CADAB7FC8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75AF49EF-1411-408A-B9E7-CE8CADAB7FC8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75AF49EF-1411-408A-B9E7-CE8CADAB7FC8}.Release|Any CPU.Build.0 = Release|Any CPU
{B3E52219-7B85-42B3-B607-F323401AC878}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B3E52219-7B85-42B3-B607-F323401AC878}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B3E52219-7B85-42B3-B607-F323401AC878}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B3E52219-7B85-42B3-B607-F323401AC878}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -85,6 +97,8 @@ Global
{E7FD0B44-8ACA-4EA6-8A38-E6D346462273} = {8AC3899F-4C0B-4EF6-AD2B-97D91FE107F5}
{AE04301F-4E74-48DA-BFF9-D879618DA124} = {6D3497A0-339F-4545-86B8-7E015B468025}
{C8166147-F446-4774-AFEC-9C1795B822B8} = {8AC3899F-4C0B-4EF6-AD2B-97D91FE107F5}
{75AF49EF-1411-408A-B9E7-CE8CADAB7FC8} = {6D3497A0-339F-4545-86B8-7E015B468025}
{B3E52219-7B85-42B3-B607-F323401AC878} = {8AC3899F-4C0B-4EF6-AD2B-97D91FE107F5}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FE6D8568-233C-4A49-84FF-2FE6C0DE238C}
Expand Down
38 changes: 36 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Boilerplate of API in ~~`.NET Core 3.1`~~ `.NET 7`

| GitHub | Codecov |
| GitHub | Codecov |
|:-------------:|:-------------:|
| [![GitHub Build Status](https://github.com/lkurzyniec/netcore-boilerplate/workflows/Build%20%26%20Test/badge.svg)](https://github.com/lkurzyniec/netcore-boilerplate/actions) | [![codecov](https://codecov.io/gh/lkurzyniec/netcore-boilerplate/branch/master/graph/badge.svg)](https://codecov.io/gh/lkurzyniec/netcore-boilerplate) |

Expand All @@ -19,11 +19,14 @@ of starting an empty project and adding the same snippets each time, you can use
1. [EF Core](https://docs.microsoft.com/ef/)
* [MySQL provider from Pomelo Foundation](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql)
* [MsSQL from Microsoft](https://github.com/aspnet/EntityFrameworkCore/)
1. [Dapper](https://github.com/DapperLib/Dapper)
* [Microsoft.Data.Sqlite](https://learn.microsoft.com/en-us/dotnet/standard/data/sqlite/)
1. Tests
* Integration tests with InMemory database
* [FluentAssertions]
* [xUnit]
* [Verify](https://github.com/VerifyTests/Verify/)
* [Verify.Http](https://github.com/VerifyTests/Verify.Http)
* TestServer
* Unit tests
* [AutoFixture](https://github.com/AutoFixture/AutoFixture)
Expand All @@ -38,7 +41,7 @@ of starting an empty project and adding the same snippets each time, you can use
* ~~[NBomber]~~(https://nbomber.com/)
1. Code quality
* [EditorConfig](https://editorconfig.org/) ([.editorconfig](.editorconfig))
* Analizers
* Analyzers
* [Microsoft.CodeAnalysis.Analyzers](https://github.com/dotnet/roslyn-analyzers)
* [Microsoft.AspNetCore.Mvc.Api.Analyzers](https://github.com/aspnet/AspNetCore/tree/master/src/Analyzers)
* [Microsoft.VisualStudio.Threading.Analyzers](https://github.com/microsoft/vs-threading)
Expand Down Expand Up @@ -154,6 +157,37 @@ of starting an empty project and adding the same snippets each time, you can use

![HappyCode.NetCoreBoilerplate.ArchitecturalTests](.assets/atests.png "HappyCode.NetCoreBoilerplate.ArchitecturalTests")

## Books module

Totally separate module, developed with a modular monolith approach.

### Module

The code organized around features (vertical slices).

[HappyCode.NetCoreBoilerplate.BooksModule](src/HappyCode.NetCoreBoilerplate.BooksModule)

* Features
* Delete book - [Endpoint.cs](src/HappyCode.NetCoreBoilerplate.BooksModule/Features/DeleteBook/Endpoint.cs), [Command.cs](src/HappyCode.NetCoreBoilerplate.BooksModule/Features/DeleteBook/Command.cs)
* Get book - [Endpoint.cs](src/HappyCode.NetCoreBoilerplate.BooksModule/Features/GetBook/Endpoint.cs), [Query.cs](src/HappyCode.NetCoreBoilerplate.BooksModule/Features/GetBook/Query.cs)
* Get books - [Endpoint.cs](src/HappyCode.NetCoreBoilerplate.BooksModule/Features/GetBooks/Endpoint.cs), [Query.cs](src/HappyCode.NetCoreBoilerplate.BooksModule/Features/GetBooks/Query.cs)
* Upsert book - [Endpoint.cs](src/HappyCode.NetCoreBoilerplate.BooksModule/Features/UpsertBook/Endpoint.cs), [Command.cs](src/HappyCode.NetCoreBoilerplate.BooksModule/Features/UpsertBook/Command.cs)
* Sqlite db initializer - [DbInitializer.cs](src/HappyCode.NetCoreBoilerplate.BooksModule/Infrastructure/DbInitializer.cs)
* Module configuration place - [BooksModuleConfigurations.cs](src/HappyCode.NetCoreBoilerplate.BooksModule/BooksModuleConfigurations.cs)

![HappyCode.NetCoreBoilerplate.BooksModule](.assets/books.png "HappyCode.NetCoreBoilerplate.BooksModule")

### Integration Tests

[HappyCode.NetCoreBoilerplate.BooksModule.IntegrationTests](test/HappyCode.NetCoreBoilerplate.BooksModule.IntegrationTests)

* Infrastructure
* Fixture with TestServer - [TestServerClientFixture.cs](test/HappyCode.NetCoreBoilerplate.BooksModule.IntegrationTests/Infrastructure/TestServerClientFixture.cs)
* Very simple data feeder - [BooksDataFeeder.cs](test/HappyCode.NetCoreBoilerplate.BooksModule.IntegrationTests/Infrastructure/DataFeeders/BooksDataFeeder.cs)
* Exemplary tests - [BooksTests.cs](test/HappyCode.NetCoreBoilerplate.BooksModule.IntegrationTests/BooksTests.cs)

![HappyCode.NetCoreBoilerplate.BooksModule.IntegrationTests](.assets/books-tests.png "HappyCode.NetCoreBoilerplate.BooksModule.IntegrationTests")

## How to adapt to your project

Generally it is totally up to you! But in case you do not have any plan, You can follow below simple steps:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\HappyCode.NetCoreBoilerplate.BooksModule\HappyCode.NetCoreBoilerplate.BooksModule.csproj" />
<ProjectReference Include="..\HappyCode.NetCoreBoilerplate.Core\HappyCode.NetCoreBoilerplate.Core.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,6 @@
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "swagger",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"HappyCode.NetCoreBoilerplate.Api": {
"commandName": "Project",
"launchBrowser": true,
Expand Down
11 changes: 9 additions & 2 deletions src/HappyCode.NetCoreBoilerplate.Api/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using HappyCode.NetCoreBoilerplate.Api.Infrastructure.Configurations;
using HappyCode.NetCoreBoilerplate.Api.Infrastructure.Filters;
using HappyCode.NetCoreBoilerplate.Api.Infrastructure.Registrations;
using HappyCode.NetCoreBoilerplate.BooksModule;
using HappyCode.NetCoreBoilerplate.Core;
using HappyCode.NetCoreBoilerplate.Core.Registrations;
using HappyCode.NetCoreBoilerplate.Core.Settings;
Expand Down Expand Up @@ -32,8 +33,9 @@ public virtual void ConfigureServices(IServiceCollection services)
{
services
.AddHttpContextAccessor()
.AddRouting(options => options.LowercaseUrls = true)
.AddMvcCore(options =>
.AddRouting(options => options.LowercaseUrls = true);

services.AddMvcCore(options =>
{
options.Filters.Add<HttpGlobalExceptionFilter>();
options.Filters.Add<ValidateModelStateFilter>();
Expand All @@ -54,6 +56,7 @@ public virtual void ConfigureServices(IServiceCollection services)
services.AddHttpClient(nameof(PingWebsiteBackgroundService));

services.AddCoreComponents();
services.AddBooksModule(_configuration);

services.AddFeatureManagement()
.AddFeatureFilter<TimeWindowFilter>();
Expand All @@ -74,6 +77,8 @@ public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env)
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapBooksModule();

endpoints.MapHealthChecks("/health", new HealthCheckOptions
{
ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse,
Expand All @@ -86,6 +91,8 @@ public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment env)
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Simple Api V1");
c.DocExpansion(DocExpansion.None);
});

app.InitBooksModule();
}
}
}
3 changes: 2 additions & 1 deletion src/HappyCode.NetCoreBoilerplate.Api/appsettings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"ConnectionStrings": {
"MySqlDb": "server=mysql;Database=employees;Uid=user;Pwd=simplepwd;",
"MsSqlDb": "Data Source=mssql;Initial Catalog=cars;User ID=user;Password=simplePWD123!;TrustServerCertificate=true;"
"MsSqlDb": "Data Source=mssql;Initial Catalog=cars;User ID=user;Password=simplePWD123!;TrustServerCertificate=true;",
"SqliteDb": "Data Source=books.db"
},

"PingWebsite": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Data;
using HappyCode.NetCoreBoilerplate.BooksModule.Features.DeleteBook;
using HappyCode.NetCoreBoilerplate.BooksModule.Features.GetBook;
using HappyCode.NetCoreBoilerplate.BooksModule.Features.GetBooks;
using HappyCode.NetCoreBoilerplate.BooksModule.Features.UpsertBook;
using HappyCode.NetCoreBoilerplate.BooksModule.Infrastructure;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace HappyCode.NetCoreBoilerplate.BooksModule;

public static class BooksModuleConfigurations
{
public static IServiceCollection AddBooksModule(this IServiceCollection services, IConfiguration configuration)
=> services
.AddEndpointsApiExplorer()
.AddSingleton<IDbConnection>(sp => new SqliteConnection(configuration.GetConnectionString("SqliteDb")))
.AddSingleton<DbInitializer>()
;

public static IEndpointRouteBuilder MapBooksModule(this IEndpointRouteBuilder endpoints)
=> endpoints
.MapGroup("/api/books")
.AddEndpointFilter<AuthFilter>()
.MapGetBooksEndpoint()
.MapGetBookEndpoint()
.MapUpsertBookEndpoint()
.MapDeleteBookEndpoint()
;

public static IApplicationBuilder InitBooksModule(this IApplicationBuilder app)
{
var initializer = app.ApplicationServices.GetRequiredService<DbInitializer>();
initializer.Init();

return app;
}
}
3 changes: 3 additions & 0 deletions src/HappyCode.NetCoreBoilerplate.BooksModule/Dtos/BookDto.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
namespace HappyCode.NetCoreBoilerplate.BooksModule.Dtos;

public record struct BookDto (int? Id, string Title);
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System.Data;
using Dapper;
using HappyCode.NetCoreBoilerplate.BooksModule.Dtos;

namespace HappyCode.NetCoreBoilerplate.BooksModule.Features.DeleteBook;

internal static class Command
{
private static readonly string _deleteBook = @$"
DELETE FROM Books
WHERE
{nameof(BookDto.Id)} = @id
";

public static async Task<int> DeleteBookAsync(this IDbConnection db, int id, CancellationToken cancellationToken)
=> await db.ExecuteAsync(new CommandDefinition(_deleteBook, new { id }, cancellationToken: cancellationToken));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using System.Data;

namespace HappyCode.NetCoreBoilerplate.BooksModule.Features.DeleteBook;

internal static class Endpoint
{
public static IEndpointRouteBuilder MapDeleteBookEndpoint(this IEndpointRouteBuilder endpoints)
{
endpoints
.MapDelete(
"/{id:int}",
async (
int id,
IDbConnection db,
CancellationToken ct
) =>
{
var affected = await db.DeleteBookAsync(id, ct);
return affected > 0
? Results.NoContent()
: Results.NotFound();
})
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status404NotFound)
.WithTags("Books");
return endpoints;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using HappyCode.NetCoreBoilerplate.BooksModule.Dtos;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using System.Data;

namespace HappyCode.NetCoreBoilerplate.BooksModule.Features.GetBook;

internal static class Endpoint
{
public static IEndpointRouteBuilder MapGetBookEndpoint(this IEndpointRouteBuilder endpoints)
{
endpoints
.MapGet(
"/{id:int}",
async (
int id,
IDbConnection db,
CancellationToken ct
) =>
{
var book = await db.GetBookAsync(id, ct);
return book.Id is not null
? Results.Ok(book)
: Results.NotFound();
})
.Produces<BookDto>()
.Produces(StatusCodes.Status404NotFound)
.WithTags("Books");
return endpoints;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Data;
using Dapper;
using HappyCode.NetCoreBoilerplate.BooksModule.Dtos;

namespace HappyCode.NetCoreBoilerplate.BooksModule.Features.GetBook;

internal static class Query
{
private static readonly string _getBook = @$"
SELECT
{nameof(BookDto.Id)},
{nameof(BookDto.Title)}
FROM
Books
WHERE
{nameof(BookDto.Id)} = @id
";

public static Task<BookDto> GetBookAsync(this IDbConnection db, int id, CancellationToken cancellationToken)
=> db.QuerySingleOrDefaultAsync<BookDto>(new CommandDefinition(_getBook, new { id }, cancellationToken: cancellationToken));
}
Loading

0 comments on commit 8e28f4d

Please sign in to comment.