diff --git a/csharp-architectural-patterns/OnionArchitecture/.gitignore b/csharp-architectural-patterns/OnionArchitecture/.gitignore new file mode 100644 index 000000000..3102f3a35 --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/.gitignore @@ -0,0 +1,4 @@ +# SQLite database the API creates on first run (EnsureCreated) +*.db +*.db-shm +*.db-wal diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Api/AccountEndpoints.cs b/csharp-architectural-patterns/OnionArchitecture/Banking.Api/AccountEndpoints.cs new file mode 100644 index 000000000..9fa29ab07 --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Api/AccountEndpoints.cs @@ -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(); + }); + } +} diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Api/Banking.Api.csproj b/csharp-architectural-patterns/OnionArchitecture/Banking.Api/Banking.Api.csproj new file mode 100644 index 000000000..1f5c1652a --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Api/Banking.Api.csproj @@ -0,0 +1,14 @@ + + + + + + + + + net10.0 + enable + enable + + + diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Api/Banking.Api.http b/csharp-architectural-patterns/OnionArchitecture/Banking.Api/Banking.Api.http new file mode 100644 index 000000000..9670e9a9d --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Api/Banking.Api.http @@ -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 diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Api/Program.cs b/csharp-architectural-patterns/OnionArchitecture/Banking.Api/Program.cs new file mode 100644 index 000000000..7c1de899d --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Api/Program.cs @@ -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(); + dbContext.Database.EnsureCreated(); +} + +app.MapAccountEndpoints(); + +app.Run(); diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Api/Properties/launchSettings.json b/csharp-architectural-patterns/OnionArchitecture/Banking.Api/Properties/launchSettings.json new file mode 100644 index 000000000..304f4337a --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Api/Properties/launchSettings.json @@ -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" + } + } + } +} diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Api/ResultExtensions.cs b/csharp-architectural-patterns/OnionArchitecture/Banking.Api/ResultExtensions.cs new file mode 100644 index 000000000..1059a2e4e --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Api/ResultExtensions.cs @@ -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); + } +} diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Api/appsettings.Development.json b/csharp-architectural-patterns/OnionArchitecture/Banking.Api/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Api/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Api/appsettings.json b/csharp-architectural-patterns/OnionArchitecture/Banking.Api/appsettings.json new file mode 100644 index 000000000..c74c0dff2 --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Api/appsettings.json @@ -0,0 +1,12 @@ +{ + "ConnectionStrings": { + "Banking": "Data Source=banking.db" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.ArchitectureTests/Banking.ArchitectureTests.csproj b/csharp-architectural-patterns/OnionArchitecture/Banking.ArchitectureTests/Banking.ArchitectureTests.csproj new file mode 100644 index 000000000..96cb7866a --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.ArchitectureTests/Banking.ArchitectureTests.csproj @@ -0,0 +1,26 @@ + + + + net10.0 + enable + enable + false + true + + + + + + + + + + + + + + + + + + diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.ArchitectureTests/LayerTests.cs b/csharp-architectural-patterns/OnionArchitecture/Banking.ArchitectureTests/LayerTests.cs new file mode 100644 index 000000000..02e92bdf9 --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.ArchitectureTests/LayerTests.cs @@ -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 ?? [])); + } +} diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Domain/Account.cs b/csharp-architectural-patterns/OnionArchitecture/Banking.Domain/Account.cs new file mode 100644 index 000000000..ab82a1d79 --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Domain/Account.cs @@ -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(); + } +} diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Domain/AccountErrors.cs b/csharp-architectural-patterns/OnionArchitecture/Banking.Domain/AccountErrors.cs new file mode 100644 index 000000000..fb6dedf99 --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Domain/AccountErrors.cs @@ -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); +} diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Domain/Banking.Domain.csproj b/csharp-architectural-patterns/OnionArchitecture/Banking.Domain/Banking.Domain.csproj new file mode 100644 index 000000000..b76014470 --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Domain/Banking.Domain.csproj @@ -0,0 +1,9 @@ + + + + net10.0 + enable + enable + + + diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Domain/IAccountRepository.cs b/csharp-architectural-patterns/OnionArchitecture/Banking.Domain/IAccountRepository.cs new file mode 100644 index 000000000..5587568c8 --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Domain/IAccountRepository.cs @@ -0,0 +1,8 @@ +namespace Banking.Domain; + +public interface IAccountRepository +{ + Task GetByIdAsync(int accountId, CancellationToken cancellationToken = default); + + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Domain/Result.cs b/csharp-architectural-patterns/OnionArchitecture/Banking.Domain/Result.cs new file mode 100644 index 000000000..597112ba3 --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Domain/Result.cs @@ -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 : 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 value) => + new(value, null); + + public static implicit operator Result(Error error) => + new(default, error); +} diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Infrastructure/AccountRepository.cs b/csharp-architectural-patterns/OnionArchitecture/Banking.Infrastructure/AccountRepository.cs new file mode 100644 index 000000000..dace4285f --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Infrastructure/AccountRepository.cs @@ -0,0 +1,13 @@ +using Banking.Domain; +using Microsoft.EntityFrameworkCore; + +namespace Banking.Infrastructure; + +internal sealed class AccountRepository(BankingDbContext dbContext) : IAccountRepository +{ + public Task GetByIdAsync(int accountId, CancellationToken cancellationToken = default) => + dbContext.Accounts.FirstOrDefaultAsync(a => a.Id == accountId, cancellationToken); + + public Task SaveChangesAsync(CancellationToken cancellationToken = default) => + dbContext.SaveChangesAsync(cancellationToken); +} diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Infrastructure/Banking.Infrastructure.csproj b/csharp-architectural-patterns/OnionArchitecture/Banking.Infrastructure/Banking.Infrastructure.csproj new file mode 100644 index 000000000..fb5f147fd --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Infrastructure/Banking.Infrastructure.csproj @@ -0,0 +1,17 @@ + + + + + + + + + + + + net10.0 + enable + enable + + + diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Infrastructure/BankingDbContext.cs b/csharp-architectural-patterns/OnionArchitecture/Banking.Infrastructure/BankingDbContext.cs new file mode 100644 index 000000000..2d89b5b60 --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Infrastructure/BankingDbContext.cs @@ -0,0 +1,22 @@ +using Banking.Domain; +using Microsoft.EntityFrameworkCore; + +namespace Banking.Infrastructure; + +public sealed class BankingDbContext(DbContextOptions options) + : DbContext(options) +{ + public DbSet Accounts => Set(); + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(builder => + { + builder.Property(a => a.Owner).HasMaxLength(100); + + builder.HasData( + new { Id = 1, Owner = "Anna Smith", Balance = 100m }, + new { Id = 2, Owner = "Mark Jones", Balance = 20m }); + }); + } +} diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Infrastructure/DependencyInjection.cs b/csharp-architectural-patterns/OnionArchitecture/Banking.Infrastructure/DependencyInjection.cs new file mode 100644 index 000000000..329085a15 --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Infrastructure/DependencyInjection.cs @@ -0,0 +1,17 @@ +using Banking.Domain; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Banking.Infrastructure; + +public static class DependencyInjection +{ + public static IServiceCollection AddInfrastructure( + this IServiceCollection services, string connectionString) + { + services.AddDbContext(options => options.UseSqlite(connectionString)); + services.AddScoped(); + + return services; + } +} diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Services/AccountService.cs b/csharp-architectural-patterns/OnionArchitecture/Banking.Services/AccountService.cs new file mode 100644 index 000000000..036ecbb7a --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Services/AccountService.cs @@ -0,0 +1,35 @@ +using Banking.Domain; + +namespace Banking.Services; + +internal sealed class AccountService(IAccountRepository accounts) : IAccountService +{ + public async Task> GetByIdAsync( + int accountId, CancellationToken cancellationToken = default) + { + var account = await accounts.GetByIdAsync(accountId, cancellationToken); + if (account is null) + return AccountErrors.NotFound(accountId); + + return ToDto(account); + } + + public async Task> WithdrawAsync( + int accountId, decimal amount, CancellationToken cancellationToken = default) + { + var account = await accounts.GetByIdAsync(accountId, cancellationToken); + if (account is null) + return AccountErrors.NotFound(accountId); + + var withdrawal = account.Withdraw(amount); + if (!withdrawal.IsSuccess) + return withdrawal.Error!; + + await accounts.SaveChangesAsync(cancellationToken); + + return ToDto(account); + } + + private static AccountDto ToDto(Account account) => + new(account.Id, account.Owner, account.Balance); +} diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Services/Banking.Services.csproj b/csharp-architectural-patterns/OnionArchitecture/Banking.Services/Banking.Services.csproj new file mode 100644 index 000000000..4396d1e70 --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Services/Banking.Services.csproj @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + net10.0 + enable + enable + + + diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Services/DependencyInjection.cs b/csharp-architectural-patterns/OnionArchitecture/Banking.Services/DependencyInjection.cs new file mode 100644 index 000000000..972e1d99f --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Services/DependencyInjection.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace Banking.Services; + +public static class DependencyInjection +{ + public static IServiceCollection AddServices(this IServiceCollection services) + { + services.AddScoped(); + + return services; + } +} diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.Services/IAccountService.cs b/csharp-architectural-patterns/OnionArchitecture/Banking.Services/IAccountService.cs new file mode 100644 index 000000000..da8c10b64 --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.Services/IAccountService.cs @@ -0,0 +1,12 @@ +using Banking.Domain; + +namespace Banking.Services; + +public sealed record AccountDto(int Id, string Owner, decimal Balance); + +public interface IAccountService +{ + Task> GetByIdAsync(int accountId, CancellationToken cancellationToken = default); + + Task> WithdrawAsync(int accountId, decimal amount, CancellationToken cancellationToken = default); +} diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.UnitTests/AccountServiceTests.cs b/csharp-architectural-patterns/OnionArchitecture/Banking.UnitTests/AccountServiceTests.cs new file mode 100644 index 000000000..50b55ab88 --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.UnitTests/AccountServiceTests.cs @@ -0,0 +1,49 @@ +using Banking.Domain; +using Banking.Services; + +namespace Banking.UnitTests; + +public class AccountServiceTests +{ + [Fact] + public async Task WithdrawAsync_WhenTheBalanceCoversIt_SavesOnce() + { + var accounts = new FakeAccountRepository(Account.Open("Anna Smith", initialDeposit: 100m)); + var service = new AccountService(accounts); + + var result = await service.WithdrawAsync( + accountId: 1, amount: 30m, TestContext.Current.CancellationToken); + + Assert.True(result.IsSuccess); + Assert.Equal(70m, result.Value.Balance); + Assert.Equal(1, accounts.SaveCount); + } + + [Fact] + public async Task WithdrawAsync_WhenTheBalanceIsTooLow_SavesNothing() + { + var accounts = new FakeAccountRepository(Account.Open("Mark Jones", initialDeposit: 20m)); + var service = new AccountService(accounts); + + var result = await service.WithdrawAsync( + accountId: 2, amount: 50m, TestContext.Current.CancellationToken); + + Assert.False(result.IsSuccess); + Assert.Equal("Account.InsufficientFunds", result.Error!.Code); + Assert.Equal(0, accounts.SaveCount); + } +} + +internal sealed class FakeAccountRepository(Account? account) : IAccountRepository +{ + public int SaveCount { get; private set; } + + public Task GetByIdAsync(int accountId, CancellationToken cancellationToken = default) => + Task.FromResult(account); + + public Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + SaveCount++; + return Task.CompletedTask; + } +} diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.UnitTests/AccountTests.cs b/csharp-architectural-patterns/OnionArchitecture/Banking.UnitTests/AccountTests.cs new file mode 100644 index 000000000..57a5fff58 --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.UnitTests/AccountTests.cs @@ -0,0 +1,29 @@ +using Banking.Domain; + +namespace Banking.UnitTests; + +public class AccountTests +{ + [Fact] + public void Withdraw_WhenTheBalanceCoversIt_TakesTheMoney() + { + var account = Account.Open("Anna Smith", initialDeposit: 100m); + + var result = account.Withdraw(30m); + + Assert.True(result.IsSuccess); + Assert.Equal(70m, account.Balance); + } + + [Fact] + public void Withdraw_WhenTheBalanceIsTooLow_ReturnsAnErrorAndKeepsTheMoney() + { + var account = Account.Open("Mark Jones", initialDeposit: 20m); + + var result = account.Withdraw(50m); + + Assert.False(result.IsSuccess); + Assert.Equal("Account.InsufficientFunds", result.Error!.Code); + Assert.Equal(20m, account.Balance); + } +} diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.UnitTests/Banking.UnitTests.csproj b/csharp-architectural-patterns/OnionArchitecture/Banking.UnitTests/Banking.UnitTests.csproj new file mode 100644 index 000000000..7fb25bc03 --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.UnitTests/Banking.UnitTests.csproj @@ -0,0 +1,25 @@ + + + + net10.0 + enable + enable + false + true + + + + + + + + + + + + + + + + + diff --git a/csharp-architectural-patterns/OnionArchitecture/Banking.sln b/csharp-architectural-patterns/OnionArchitecture/Banking.sln new file mode 100644 index 000000000..5c1f8c4ef --- /dev/null +++ b/csharp-architectural-patterns/OnionArchitecture/Banking.sln @@ -0,0 +1,104 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banking.Domain", "Banking.Domain\Banking.Domain.csproj", "{06F01CD1-449D-486C-ABF8-03D3DB1EED4E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banking.Services", "Banking.Services\Banking.Services.csproj", "{F416B60F-6BC7-47FC-8316-110B48BD64EE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banking.Infrastructure", "Banking.Infrastructure\Banking.Infrastructure.csproj", "{45B57D8B-8530-4679-94E8-7DF1D3793EB2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banking.Api", "Banking.Api\Banking.Api.csproj", "{C78040F3-EE19-4DC7-93F1-E086D8896D02}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banking.UnitTests", "Banking.UnitTests\Banking.UnitTests.csproj", "{A100026C-6EDF-443A-9BE5-5DB3FC4F7EC6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Banking.ArchitectureTests", "Banking.ArchitectureTests\Banking.ArchitectureTests.csproj", "{A7E49AE1-CCB6-44C9-9CD4-0CA4AF9A18C9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {06F01CD1-449D-486C-ABF8-03D3DB1EED4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {06F01CD1-449D-486C-ABF8-03D3DB1EED4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {06F01CD1-449D-486C-ABF8-03D3DB1EED4E}.Debug|x64.ActiveCfg = Debug|Any CPU + {06F01CD1-449D-486C-ABF8-03D3DB1EED4E}.Debug|x64.Build.0 = Debug|Any CPU + {06F01CD1-449D-486C-ABF8-03D3DB1EED4E}.Debug|x86.ActiveCfg = Debug|Any CPU + {06F01CD1-449D-486C-ABF8-03D3DB1EED4E}.Debug|x86.Build.0 = Debug|Any CPU + {06F01CD1-449D-486C-ABF8-03D3DB1EED4E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {06F01CD1-449D-486C-ABF8-03D3DB1EED4E}.Release|Any CPU.Build.0 = Release|Any CPU + {06F01CD1-449D-486C-ABF8-03D3DB1EED4E}.Release|x64.ActiveCfg = Release|Any CPU + {06F01CD1-449D-486C-ABF8-03D3DB1EED4E}.Release|x64.Build.0 = Release|Any CPU + {06F01CD1-449D-486C-ABF8-03D3DB1EED4E}.Release|x86.ActiveCfg = Release|Any CPU + {06F01CD1-449D-486C-ABF8-03D3DB1EED4E}.Release|x86.Build.0 = Release|Any CPU + {F416B60F-6BC7-47FC-8316-110B48BD64EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F416B60F-6BC7-47FC-8316-110B48BD64EE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F416B60F-6BC7-47FC-8316-110B48BD64EE}.Debug|x64.ActiveCfg = Debug|Any CPU + {F416B60F-6BC7-47FC-8316-110B48BD64EE}.Debug|x64.Build.0 = Debug|Any CPU + {F416B60F-6BC7-47FC-8316-110B48BD64EE}.Debug|x86.ActiveCfg = Debug|Any CPU + {F416B60F-6BC7-47FC-8316-110B48BD64EE}.Debug|x86.Build.0 = Debug|Any CPU + {F416B60F-6BC7-47FC-8316-110B48BD64EE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F416B60F-6BC7-47FC-8316-110B48BD64EE}.Release|Any CPU.Build.0 = Release|Any CPU + {F416B60F-6BC7-47FC-8316-110B48BD64EE}.Release|x64.ActiveCfg = Release|Any CPU + {F416B60F-6BC7-47FC-8316-110B48BD64EE}.Release|x64.Build.0 = Release|Any CPU + {F416B60F-6BC7-47FC-8316-110B48BD64EE}.Release|x86.ActiveCfg = Release|Any CPU + {F416B60F-6BC7-47FC-8316-110B48BD64EE}.Release|x86.Build.0 = Release|Any CPU + {45B57D8B-8530-4679-94E8-7DF1D3793EB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {45B57D8B-8530-4679-94E8-7DF1D3793EB2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {45B57D8B-8530-4679-94E8-7DF1D3793EB2}.Debug|x64.ActiveCfg = Debug|Any CPU + {45B57D8B-8530-4679-94E8-7DF1D3793EB2}.Debug|x64.Build.0 = Debug|Any CPU + {45B57D8B-8530-4679-94E8-7DF1D3793EB2}.Debug|x86.ActiveCfg = Debug|Any CPU + {45B57D8B-8530-4679-94E8-7DF1D3793EB2}.Debug|x86.Build.0 = Debug|Any CPU + {45B57D8B-8530-4679-94E8-7DF1D3793EB2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {45B57D8B-8530-4679-94E8-7DF1D3793EB2}.Release|Any CPU.Build.0 = Release|Any CPU + {45B57D8B-8530-4679-94E8-7DF1D3793EB2}.Release|x64.ActiveCfg = Release|Any CPU + {45B57D8B-8530-4679-94E8-7DF1D3793EB2}.Release|x64.Build.0 = Release|Any CPU + {45B57D8B-8530-4679-94E8-7DF1D3793EB2}.Release|x86.ActiveCfg = Release|Any CPU + {45B57D8B-8530-4679-94E8-7DF1D3793EB2}.Release|x86.Build.0 = Release|Any CPU + {C78040F3-EE19-4DC7-93F1-E086D8896D02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C78040F3-EE19-4DC7-93F1-E086D8896D02}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C78040F3-EE19-4DC7-93F1-E086D8896D02}.Debug|x64.ActiveCfg = Debug|Any CPU + {C78040F3-EE19-4DC7-93F1-E086D8896D02}.Debug|x64.Build.0 = Debug|Any CPU + {C78040F3-EE19-4DC7-93F1-E086D8896D02}.Debug|x86.ActiveCfg = Debug|Any CPU + {C78040F3-EE19-4DC7-93F1-E086D8896D02}.Debug|x86.Build.0 = Debug|Any CPU + {C78040F3-EE19-4DC7-93F1-E086D8896D02}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C78040F3-EE19-4DC7-93F1-E086D8896D02}.Release|Any CPU.Build.0 = Release|Any CPU + {C78040F3-EE19-4DC7-93F1-E086D8896D02}.Release|x64.ActiveCfg = Release|Any CPU + {C78040F3-EE19-4DC7-93F1-E086D8896D02}.Release|x64.Build.0 = Release|Any CPU + {C78040F3-EE19-4DC7-93F1-E086D8896D02}.Release|x86.ActiveCfg = Release|Any CPU + {C78040F3-EE19-4DC7-93F1-E086D8896D02}.Release|x86.Build.0 = Release|Any CPU + {A100026C-6EDF-443A-9BE5-5DB3FC4F7EC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A100026C-6EDF-443A-9BE5-5DB3FC4F7EC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A100026C-6EDF-443A-9BE5-5DB3FC4F7EC6}.Debug|x64.ActiveCfg = Debug|Any CPU + {A100026C-6EDF-443A-9BE5-5DB3FC4F7EC6}.Debug|x64.Build.0 = Debug|Any CPU + {A100026C-6EDF-443A-9BE5-5DB3FC4F7EC6}.Debug|x86.ActiveCfg = Debug|Any CPU + {A100026C-6EDF-443A-9BE5-5DB3FC4F7EC6}.Debug|x86.Build.0 = Debug|Any CPU + {A100026C-6EDF-443A-9BE5-5DB3FC4F7EC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A100026C-6EDF-443A-9BE5-5DB3FC4F7EC6}.Release|Any CPU.Build.0 = Release|Any CPU + {A100026C-6EDF-443A-9BE5-5DB3FC4F7EC6}.Release|x64.ActiveCfg = Release|Any CPU + {A100026C-6EDF-443A-9BE5-5DB3FC4F7EC6}.Release|x64.Build.0 = Release|Any CPU + {A100026C-6EDF-443A-9BE5-5DB3FC4F7EC6}.Release|x86.ActiveCfg = Release|Any CPU + {A100026C-6EDF-443A-9BE5-5DB3FC4F7EC6}.Release|x86.Build.0 = Release|Any CPU + {A7E49AE1-CCB6-44C9-9CD4-0CA4AF9A18C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A7E49AE1-CCB6-44C9-9CD4-0CA4AF9A18C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A7E49AE1-CCB6-44C9-9CD4-0CA4AF9A18C9}.Debug|x64.ActiveCfg = Debug|Any CPU + {A7E49AE1-CCB6-44C9-9CD4-0CA4AF9A18C9}.Debug|x64.Build.0 = Debug|Any CPU + {A7E49AE1-CCB6-44C9-9CD4-0CA4AF9A18C9}.Debug|x86.ActiveCfg = Debug|Any CPU + {A7E49AE1-CCB6-44C9-9CD4-0CA4AF9A18C9}.Debug|x86.Build.0 = Debug|Any CPU + {A7E49AE1-CCB6-44C9-9CD4-0CA4AF9A18C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A7E49AE1-CCB6-44C9-9CD4-0CA4AF9A18C9}.Release|Any CPU.Build.0 = Release|Any CPU + {A7E49AE1-CCB6-44C9-9CD4-0CA4AF9A18C9}.Release|x64.ActiveCfg = Release|Any CPU + {A7E49AE1-CCB6-44C9-9CD4-0CA4AF9A18C9}.Release|x64.Build.0 = Release|Any CPU + {A7E49AE1-CCB6-44C9-9CD4-0CA4AF9A18C9}.Release|x86.ActiveCfg = Release|Any CPU + {A7E49AE1-CCB6-44C9-9CD4-0CA4AF9A18C9}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal