Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions csharp-architectural-patterns/OnionArchitecture/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# SQLite database the API creates on first run (EnsureCreated)
*.db
*.db-shm
*.db-wal
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using System.ComponentModel.DataAnnotations;
using Banking.Services;

namespace Banking.Api;

public sealed record WithdrawRequest([property: Range(typeof(decimal), "1", "1000")] decimal Amount);

public static class AccountEndpoints
{
public static void MapAccountEndpoints(this IEndpointRouteBuilder app)
{
var accounts = app.MapGroup("/api/accounts");

accounts.MapGet("/{accountId:int}", async (
int accountId,
IAccountService service,
CancellationToken cancellationToken) =>
{
var result = await service.GetByIdAsync(accountId, cancellationToken);

return result.IsSuccess ? Results.Ok(result.Value) : result.ToProblem();
});

accounts.MapPost("/{accountId:int}/withdrawals", async (
int accountId,
WithdrawRequest request,
IAccountService service,
CancellationToken cancellationToken) =>
{
var result = await service.WithdrawAsync(accountId, request.Amount, cancellationToken);

return result.IsSuccess ? Results.Ok(result.Value) : result.ToProblem();
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<ItemGroup>
<ProjectReference Include="..\Banking.Services\Banking.Services.csproj" />
<ProjectReference Include="..\Banking.Infrastructure\Banking.Infrastructure.csproj" />
</ItemGroup>

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
GET http://localhost:5000/api/accounts/1

###

POST http://localhost:5000/api/accounts/1/withdrawals
Content-Type: application/json

{ "amount": 30 }

###

POST http://localhost:5000/api/accounts/2/withdrawals
Content-Type: application/json

{ "amount": 50 }

###

POST http://localhost:5000/api/accounts/1/withdrawals
Content-Type: application/json

{ "amount": 0 }

###

GET http://localhost:5000/api/accounts/99
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Banking.Api;
using Banking.Infrastructure;
using Banking.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddServices();
builder.Services.AddInfrastructure(builder.Configuration.GetConnectionString("Banking")!);

builder.Services.AddProblemDetails();
builder.Services.AddValidation();

var app = builder.Build();

app.UseExceptionHandler();

using (var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<BankingDbContext>();
dbContext.Database.EnsureCreated();
}

app.MapAccountEndpoints();

app.Run();
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7254;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Banking.Domain;

namespace Banking.Api;

public static class ResultExtensions
{
public static IResult ToProblem(this Result result)
{
var error = result.Error
?? throw new InvalidOperationException("A successful result is not a problem.");

var statusCode = error.Type switch
{
ErrorType.NotFound => StatusCodes.Status404NotFound,
ErrorType.Conflict => StatusCodes.Status409Conflict,
_ => StatusCodes.Status500InternalServerError
};

return TypedResults.Problem(statusCode: statusCode, title: error.Code, detail: error.Description);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"ConnectionStrings": {
"Banking": "Data Source=banking.db"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.10.1" />
<PackageReference Include="NetArchTest.Rules" Version="1.3.2" />
<PackageReference Include="xunit.v3.mtp-off" Version="4.0.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="4.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Banking.Api\Banking.Api.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using Banking.Api;
using Banking.Domain;
using Banking.Services;
using NetArchTest.Rules;

namespace Banking.ArchitectureTests;

public class LayerTests
{
[Fact]
public void Domain_DependsOnNoOtherLayer()
{
var result = Types.InAssembly(typeof(Account).Assembly)
.ShouldNot()
.HaveDependencyOnAny("Banking.Services", "Banking.Infrastructure", "Banking.Api")
.GetResult();

Assert.True(result.IsSuccessful, "Offending types: " + string.Join(", ", result.FailingTypeNames ?? []));
}

[Fact]
public void Services_DoNotDependOnTheOuterRing()
{
var result = Types.InAssembly(typeof(IAccountService).Assembly)
.ShouldNot()
.HaveDependencyOnAny("Banking.Infrastructure", "Banking.Api",
"Microsoft.EntityFrameworkCore", "Microsoft.AspNetCore")
.GetResult();

Assert.True(result.IsSuccessful, "Offending types: " + string.Join(", ", result.FailingTypeNames ?? []));
}

[Fact]
public void Endpoints_GoThroughTheServices()
{
var result = Types.InAssembly(typeof(AccountEndpoints).Assembly)
.That()
.ResideInNamespace("Banking.Api")
.ShouldNot()
.HaveDependencyOnAny("Banking.Infrastructure", "Banking.Domain.IAccountRepository")
.GetResult();

Assert.True(result.IsSuccessful, "Offending types: " + string.Join(", ", result.FailingTypeNames ?? []));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
namespace Banking.Domain;

public sealed class Account
{
private Account(string owner, decimal balance)
{
Owner = owner;
Balance = balance;
}

public int Id { get; private set; }
public string Owner { get; private set; }
public decimal Balance { get; private set; }

public static Account Open(string owner, decimal initialDeposit)
{
ArgumentException.ThrowIfNullOrWhiteSpace(owner);
ArgumentOutOfRangeException.ThrowIfNegative(initialDeposit);

return new Account(owner, initialDeposit);
}

public Result Withdraw(decimal amount)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(amount);

if (amount > Balance)
return AccountErrors.InsufficientFunds(Id);

Balance -= amount;

return Result.Success();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace Banking.Domain;

public static class AccountErrors
{
public static Error NotFound(int accountId) =>
new("Account.NotFound", $"Account {accountId} was not found.", ErrorType.NotFound);

public static Error InsufficientFunds(int accountId) =>
new("Account.InsufficientFunds", $"Account {accountId} doesn't have enough money for this withdrawal.", ErrorType.Conflict);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Banking.Domain;

public interface IAccountRepository
{
Task<Account?> GetByIdAsync(int accountId, CancellationToken cancellationToken = default);

Task SaveChangesAsync(CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace Banking.Domain;

public enum ErrorType
{
NotFound,
Conflict
}

public sealed record Error(string Code, string Description, ErrorType Type);

public class Result
{
protected Result(Error? error) => Error = error;

public Error? Error { get; }
public bool IsSuccess => Error is null;

public static Result Success() => new(null);

public static implicit operator Result(Error error) => new(error);
}

public sealed class Result<TValue> : Result
{
private readonly TValue? _value;

private Result(TValue? value, Error? error)
: base(error) => _value = value;

public TValue Value => IsSuccess
? _value!
: throw new InvalidOperationException("A failed result has no value.");

public static implicit operator Result<TValue>(TValue value) =>
new(value, null);

public static implicit operator Result<TValue>(Error error) =>
new(default, error);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Banking.Domain;
using Microsoft.EntityFrameworkCore;

namespace Banking.Infrastructure;

internal sealed class AccountRepository(BankingDbContext dbContext) : IAccountRepository
{
public Task<Account?> GetByIdAsync(int accountId, CancellationToken cancellationToken = default) =>
dbContext.Accounts.FirstOrDefaultAsync(a => a.Id == accountId, cancellationToken);

public Task SaveChangesAsync(CancellationToken cancellationToken = default) =>
dbContext.SaveChangesAsync(cancellationToken);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<ProjectReference Include="..\Banking.Domain\Banking.Domain.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.12" />
</ItemGroup>

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
Loading
Loading