Skip to content

Commit

Permalink
dotnet 6 update (#36)
Browse files Browse the repository at this point in the history
* Update backend to dotnet 6

* Update readme with .NET 6 update info

* Update backend CI to dotnet 6

* Fix failing unit tests
  • Loading branch information
baratgabor committed Mar 25, 2023
1 parent 65ab084 commit b72d78c
Show file tree
Hide file tree
Showing 184 changed files with 4,229 additions and 3,818 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/backend-CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v1
with:
dotnet-version: 5.0.x
dotnet-version: 6.0.x
- name: Restore dependencies
run: dotnet restore
- name: Build
Expand Down
123 changes: 119 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@


## MyWarehouse – A Clean Architecture, Vertical Slicing, DDD sample in ASP.NET Core
## MyWarehouse – A Clean Architecture, Vertical Slicing, DDD sample in ASP.NET Core

[![Backend CI](https://github.com/baratgabor/MyWarehouse/actions/workflows/backend-CI.yml/badge.svg)](https://github.com/baratgabor/MyWarehouse/actions/workflows/backend-CI.yml) [![Backend CD](https://github.com/baratgabor/MyWarehouse/actions/workflows/backend-CD.yml/badge.svg)](https://github.com/baratgabor/MyWarehouse/actions/workflows/backend-CD.yml) [![Frontend CD](https://github.com/baratgabor/MyWarehouse/actions/workflows/frontend-CD.yml/badge.svg)](https://github.com/baratgabor/MyWarehouse/actions/workflows/frontend-CD.yml) [![App Core Coverage Status](https://coveralls.io/repos/github/baratgabor/MyWarehouse/badge.svg?branch=master)](https://coveralls.io/github/baratgabor/MyWarehouse?branch=master)

Expand Down Expand Up @@ -63,6 +61,7 @@ The following, rather informatively written documentation page has a two-fold pu
* [More feature-rich Angular frontend](#more-feature-rich-angular-frontend)
- [Potential improvements](#potential-improvements)
- [How to set up to run locally](#how-to-set-up-to-run-locally)
- [.NET 6.0 update improvements](#.net-6.0-update-improvements)

## Motivation

Expand All @@ -78,7 +77,7 @@ This project had an older version I originally developed for a job application,

## Technologies

[C# 9](https://devblogs.microsoft.com/dotnet/welcome-to-c-9-0/)[.NET 5](https://dotnet.microsoft.com/download/dotnet/5.0)[ASP.NET Core 5](https://docs.microsoft.com/en-us/aspnet/core/?view=aspnetcore-5.0)[Entity Framework Core 5](https://docs.microsoft.com/en-us/ef/core/)[Angular 11](https://angular.io/)[Bootstrap](https://getbootstrap.com/)[MediatR](https://github.com/jbogard/MediatR)[AutoMapper](https://automapper.org/)[FluentValidation](https://fluentvalidation.net/)[NUnit](https://nunit.org/)[Moq](https://github.com/Moq/moq4/wiki/Quickstart)[FluentAssertions](https://fluentassertions.com/)[Respawn](https://github.com/jbogard/Respawn)[Swagger](https://swagger.io/)
[C# 10](https://devblogs.microsoft.com/dotnet/welcome-to-c-10-0/)[.NET 6](https://dotnet.microsoft.com/download/dotnet/6.0)[ASP.NET Core 6](https://docs.microsoft.com/en-us/aspnet/core/?view=aspnetcore-6.0)[Entity Framework Core 5](https://docs.microsoft.com/en-us/ef/core/)[Angular 11](https://angular.io/)[Bootstrap](https://getbootstrap.com/)[MediatR](https://github.com/jbogard/MediatR)[AutoMapper](https://automapper.org/)[FluentValidation](https://fluentvalidation.net/)[NUnit](https://nunit.org/)[Moq](https://github.com/Moq/moq4/wiki/Quickstart)[FluentAssertions](https://fluentassertions.com/)[Respawn](https://github.com/jbogard/Respawn)[Swagger](https://swagger.io/)

## Backend Design Paradigms

Expand Down Expand Up @@ -314,3 +313,119 @@ It you wish to clone this repository and use it (for whatever purpose you see fi
Thank you for your interest in this project and/or in this lengthy documentation page. I recognize that my style of writing readmes is different from what people usually do on GitHub, but since I don't publish on any platform I usually have plenty of things bottled up, so it just feels *nice* to write about them in a context where they are at least relevant.

I'm always open to improving my designs, and, as I mentioned, this was pretty much my first foray into DDD, so feel free to suggest improvements if you'd like to.



## .NET 6.0 update improvements

With plenty of delay, the project was finally updated to .NET 6 (which we were using at my workplace pretty much from release day). The sections below will introduce you to the notable improvements that have occurred in the scope of this update.

### ● IOptions pattern improvements

If I'm not mistaken, 6.0 is the version when we got the new `ValidateDataAnnotations()` and `ValidateOnStart()` methods on `OptionsBuilder`, both of which come very handy in strongly typed options validation. So I took the opportunity to update my options registration extension helper method in the following way:

```csharp
public static void RegisterMyOptions<T>(this IServiceCollection services, bool requiredToExistInConfiguration = true) where T : class
{
var optionsBuilder = services.AddOptions<T>()
.BindConfiguration(typeof(T).Name)
.ValidateDataAnnotations() // <- Note
.ValidateOnStart(); // <- Note
// Note this hack too :)
// You can pass a validation lambda to Validate() that will check if the section actually exists in configuration.
// This check runs on startup if ValidateOnStart() is called.
if (requiredToExistInConfiguration)
optionsBuilder.Validate<IConfiguration>((_, configuration)
=> configuration.GetSection(typeof(T).Name).Exists(), string.Format(ConfigMissingErrorMessage, typeof(T).Name));

services.AddSingleton(resolver => resolver.GetRequiredService<IOptions<T>>().Value);
}
```

### ● EF 6 "ConfigureConventions" – Centralized decimal precision config

There is a new feature in EF 6 that makes it **much** easier to configure entity properties in a cross-cutting manner. Previously you had to call `modelBuilder.Entity<entity>().Property(e => e.PropertyName)` to set configuration on a per-property basis, even if you wanted those constraints to apply to *all properties of a given type*, for example *all* strings to have the same maximum length, or *all* decimals to have the same precision. This lead to awkward solutions like the one below to have at least some sort of cross-cutting control (directly copied from this codebase before the .NET 6 update):

```csharp
// Before .NET 6
/// <summary>
/// Set all decimal properties to a custom uniform precision.
/// </summary>
private static void ConfigureDecimalPrecision(ModelBuilder builder)
{
foreach (var entityType in builder.Model.GetEntityTypes())
{
foreach (var decimalProperty in entityType.GetProperties()
.Where(x => x.ClrType == typeof(decimal)))
{
decimalProperty.SetPrecision(18);
decimalProperty.SetScale(4);
}
}
}
```

Now, after EF 6, we can do:

```csharp
// After .NET 6
protected override void ConfigureConventions(ModelConfigurationBuilder configBuilder)
{
configBuilder.Properties<decimal>()
.HavePrecision(precision: 18, scale: 4);
} }
```

Bam! 👌

### ● EF 6 "ConfigureConventions" – Centralized conversion config

Similarly to the section above, configuring conversions for *all* properties of a given type is much easier now as well. Consider that the following code was present in the codebase before the .NET 6 update:

```csharp
// Before .NET 6
// Notice that conversion had to be set individually for each property of a given type.
// I had to resort to creating some local functions to reduce code repetition.
// Store and restore mass unit as symbol.
builder.Entity<Product>().OwnsOne(p => p.Mass, StoreMassUnitAsSymbol);

// Store and restore currency as currency code.
builder.Entity<Product>().OwnsOne(p => p.Price, StoreCurrencyAsCode);
builder.Entity<Transaction>().OwnsOne(p => p.Total, StoreCurrencyAsCode);
builder.Entity<TransactionLine>().OwnsOne(p => p.UnitPrice, StoreCurrencyAsCode);

static void StoreCurrencyAsCode<T>(OwnedNavigationBuilder<T, Money> onb) where T : class
=> onb.Property(m => m.Currency)
.HasConversion(
c => c.Code,
c => Currency.FromCode(c))
.HasMaxLength(3);

static void StoreMassUnitAsSymbol<T>(OwnedNavigationBuilder<T, Mass> onb) where T : class
=> onb.Property(m => m.Unit)
.HasConversion(
u => u.Symbol,
s => MassUnit.FromSymbol(s))
.HasMaxLength(3);
```

Now after .NET 6 this has become:

```csharp
protected override void ConfigureConventions(ModelConfigurationBuilder configBuilder)
{
configBuilder.Properties<Currency>()
.HaveConversion<CurrencyConverter>() // Notice how we're setting a converter for all props of this type.
.HaveMaxLength(3);

configBuilder.Properties<MassUnit>()
.HaveConversion<MassUnitConverter>()
.HaveMaxLength(3);
}
```

The only downside is that we're forced to write converter classes (e.g. `CurrencyConverter` above), which is arguably a bit too much ceremony for very simple conversions where a short lambda would suffice.
2 changes: 1 addition & 1 deletion scripts/addMigration.cmd
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
::Usage: AddMigration <migrationName>
@dotnet ef migrations add --project ../src/MyWarehouse.Infrastructure --startup-project ../src/MyWarehouse.Api %*
@dotnet ef migrations add --project ../src/Infrastructure --startup-project ../src/WebApi %*
2 changes: 1 addition & 1 deletion scripts/dbContextInfo.cmd
Original file line number Diff line number Diff line change
@@ -1 +1 @@
@dotnet ef dbcontext info --project ../src/MyWarehouse.Infrastructure --startup-project ../src/MyWarehouse.Api
@dotnet ef dbcontext info --project ../src/Infrastructure --startup-project ../src/WebApi
2 changes: 1 addition & 1 deletion scripts/removeLastMigration.cmd
Original file line number Diff line number Diff line change
@@ -1 +1 @@
@dotnet ef migrations remove --project ../src/MyWarehouse.Infrastructure --startup-project ../src/MyWarehouse.Api
@dotnet ef migrations remove --project ../src/Infrastructure --startup-project ../src/WebApi
2 changes: 1 addition & 1 deletion scripts/updateDatabase.cmd
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
::Usage: UpdateDatebase <migrationName> (where <migrationName> is required only when reverting to an older migration)
@dotnet ef database update --project ../src/MyWarehouse.Infrastructure --startup-project ../src/MyWarehouse.Api %*
@dotnet ef database update --project ../src/Infrastructure --startup-project ../src/WebApi %*
14 changes: 8 additions & 6 deletions src/Application/Application.csproj
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<AssemblyName>MyWarehouse.Application</AssemblyName>
<RootNamespace>MyWarehouse.Application</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
Expand All @@ -13,10 +15,10 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="8.1.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="9.3.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="5.0.0" />
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="11.0.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.1.0" />
<PackageReference Include="MediatR.Extensions.Microsoft.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.1" />
</ItemGroup>

<ItemGroup>
Expand Down
24 changes: 10 additions & 14 deletions src/Application/ApplicationStartup.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
using AutoMapper;
using FluentValidation;
using MediatR;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
using MyWarehouse.Application.Common.Behaviors;
using System.Reflection;

namespace MyWarehouse.Application
namespace MyWarehouse.Application;

public static class ApplicationStartup
{
public static class ApplicationStartup
public static void AddMyApplicationDependencies(this IServiceCollection services)
{
public static void AddMyApplicationDependencies(this IServiceCollection services)
{
services.AddAutoMapper(Assembly.GetExecutingAssembly());
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
services.AddMediatR(Assembly.GetExecutingAssembly());
services.AddAutoMapper(Assembly.GetExecutingAssembly());
services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
services.AddMediatR(Assembly.GetExecutingAssembly());

services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ExceptionLoggingBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestValidationBehavior<,>));
}
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ExceptionLoggingBehavior<,>));
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(RequestValidationBehavior<,>));
}
}
52 changes: 24 additions & 28 deletions src/Application/Common/Behaviors/ExceptionLoggingBehavior.cs
Original file line number Diff line number Diff line change
@@ -1,39 +1,35 @@
using MediatR;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using MyWarehouse.Application.Common.Exceptions;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace MyWarehouse.Application.Common.Behaviors
namespace MyWarehouse.Application.Common.Behaviors;

public class ExceptionLoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest: IRequest<TResponse>
{
public class ExceptionLoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
private readonly ILogger<TRequest> _logger;
private readonly ILogger<TRequest> _logger;

public ExceptionLoggingBehavior(ILogger<TRequest> logger)
=> _logger = logger;
public ExceptionLoggingBehavior(ILogger<TRequest> logger)
=> _logger = logger;

public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
try
{
try
{
return await next();
}
catch (InputValidationException ive)
{
var requestName = typeof(TRequest).Name;
_logger.LogError(ive, "Validation error occurred in request '{RequestName}'\r\n\tRequestPayload: {@RequestPayload}\r\n\tErrors: {@Errors}.", requestName, request, ive.Errors);
return await next();
}
catch (InputValidationException ive)
{
var requestName = typeof(TRequest).Name;
_logger.LogError(ive, "Validation error occurred in request '{RequestName}'\r\n\tRequestPayload: {@RequestPayload}\r\n\tErrors: {@Errors}.", requestName, request, ive.Errors);

throw;
}
catch (Exception e)
{
var requestName = typeof(TRequest).Name;
_logger.LogError(e, "Exception occurred in request '{RequestName}'\r\n\tRequestPayload: {@RequestPayload}", requestName, request);
throw;
}
catch (Exception e)
{
var requestName = typeof(TRequest).Name;
_logger.LogError(e, "Exception occurred in request '{RequestName}'\r\n\tRequestPayload: {@RequestPayload}", requestName, request);

throw;
}
throw;
}
}
}
50 changes: 23 additions & 27 deletions src/Application/Common/Behaviors/RequestValidationBehavior.cs
Original file line number Diff line number Diff line change
@@ -1,42 +1,38 @@
using FluentValidation;
using MediatR;
using MyWarehouse.Application.Common.Exceptions;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace MyWarehouse.Application.Common.Behaviors;

namespace MyWarehouse.Application.Common.Behaviors
public class RequestValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
public class RequestValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
private readonly IEnumerable<IValidator<TRequest>> _validators;

public RequestValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
{
private readonly IEnumerable<IValidator<TRequest>> _validators;
_validators = validators;
}

public RequestValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
public async Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
if (_validators.Any())
{
_validators = validators;
}
var context = new ValidationContext<TRequest>(request);

public Task<TResponse> Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate<TResponse> next)
{
if (_validators.Any())
{
var context = new ValidationContext<TRequest>(request);
var results = await Task.WhenAll(_validators
.Select(v => v.ValidateAsync(context)));

var failures = _validators
.Select(v => v.Validate(context))
.SelectMany(result => result.Errors)
.Where(f => f != null)
.ToList();
var failures = results
.Where(r => !r.IsValid)
.SelectMany(r => r.Errors)
.ToList();

if (failures.Count != 0)
{
throw new InputValidationException(failures);
}
if (failures.Any())
{
throw new InputValidationException(failures);
}

return next();
}

return await next();
}
}
25 changes: 11 additions & 14 deletions src/Application/Common/Dependencies/DataAccess/IUnitOfWork.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,16 @@
using MyWarehouse.Application.Common.Dependencies.DataAccess.Repositories;
using System;
using System.Threading.Tasks;

namespace MyWarehouse.Application.Common.Dependencies.DataAccess
namespace MyWarehouse.Application.Common.Dependencies.DataAccess;

public interface IUnitOfWork : IDisposable
{
public interface IUnitOfWork : IDisposable
{
public IPartnerRepository Partners { get; }
public IProductRepository Products { get; }
public ITransactionRepository Transactions { get; }
bool HasActiveTransaction { get; }
public IPartnerRepository Partners { get; }
public IProductRepository Products { get; }
public ITransactionRepository Transactions { get; }
bool HasActiveTransaction { get; }

Task BeginTransactionAsync();
Task CommitTransactionAsync();
Task RollbackTransactionAsync();
public Task SaveChanges();
}
Task BeginTransactionAsync();
Task CommitTransactionAsync();
Task RollbackTransactionAsync();
public Task SaveChanges();
}
Loading

0 comments on commit b72d78c

Please sign in to comment.