Initial commit

This commit is contained in:
Thibaud Gasser 2020-04-12 01:36:20 +02:00
commit 50ffd34e07
38 changed files with 837 additions and 0 deletions

38
.gitignore vendored Normal file
View File

@ -0,0 +1,38 @@
*.swp
*.*~
project.lock.json
.DS_Store
*.pyc
nupkg/
# Visual Studio Code
.vscode
# Rider
.idea
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
msbuild.log
msbuild.err
msbuild.wrn
# Visual Studio 2015
.vs/

3
Dockerfile Normal file
View File

@ -0,0 +1,3 @@
FROM mcr.microsoft.com/dotnet/core/sdk:3.1
COPY app/ app/
ENTRYPOINT ["dotnet", "test", "app/"]

3
Dockerfile.debug Normal file
View File

@ -0,0 +1,3 @@
FROM mcr.microsoft.com/dotnet/core/sdk:3.1
COPY app/ app/
ENTRYPOINT ["dotnet", "vstest", "--logger:trx", "app/VegetableShop.*Tests*"]

41
Makefile Normal file
View File

@ -0,0 +1,41 @@
DOTNET="/usr/bin/dotnet"
SLN="./src/DDDDemo.sln"
TARGET="linux-x64"
all: clean restore publish publish-release
clean:
$(DOTNET) clean --verbosity=quiet $(SLN)
rm -rf release-$(TARGET) debug-$(TARGET)
mrproper: clean
find src/ \( -name "bin" -o -name "obj" \) -exec rm -rf {} +
restore:
$(DOTNET) restore --verbosity=quiet --runtime $(TARGET) $(SLN)
build:
$(DOTNET) build $(SLN)
publish:
$(DOTNET) restore --verbosity=quiet --runtime $(TARGET) $(SLN)
$(DOTNET) publish $(SLN) --no-restore \
--verbosity=quiet \
-c Debug \
--output debug-$(TARGET)
publish-release:
$(DOTNET) restore --verbosity=quiet --runtime $(TARGET) $(SLN)
$(DOTNET) publish $(SLN) --no-restore \
--verbosity=quiet \
-c Release \
--output release-$(TARGET)
run:
$(DOTNET) run $(SLN)
list-tests: restore
$(DOTNET) test --no-restore --verbosity=quiet --list-tests $(SLN)
tests: restore
$(DOTNET) test --logger=trx --no-restore --verbosity=quiet $(SLN)

19
README.md Normal file
View File

@ -0,0 +1,19 @@
# Domain Driven Design : des armes pour affronter la complexité
[](https://blog.octo.com/domain-driven-design-des-armes-pour-affronter-la-complexite/)
« La complexité, cest comme le cholestérol. Il faut surtout se débarasser du mauvais. » (Proverbe gascon-malgache)
DDD est lacronyme de Domain Driven Design. Ce nest ni un framework, ni une
méthodologie, mais plutôt une approche décrite dans louvrage du même nom
dEric Evans. Un de ses objectifs est de définir une vision et un langage
partagés par toutes les personnes impliquées dans la construction dune
application, afin de mieux en appréhender la complexité. Nous ne souhaitons pas
faire ici une présentation de DDD (voir plutôt ici pour une introduction). Nous
voulons montrer comment DDD peut adresser certaines problématiques évoquées
dans larticle “Jai mal à mon application ! Ca se soigne ?” au travers dun
exemple dapplication (“je veux vendre et acheter des légumes sur internet”),
tout en sinscrivant dans une démarche de développement Agile.

45
src/DDDDemo.sln Normal file
View File

@ -0,0 +1,45 @@

Microsoft Visual Studio Solution File, Format Version 12.00
#
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VegetableShop.Domain", "VegetableShop.Domain\VegetableShop.Domain.csproj", "{9EE77ACA-9351-46CF-B55B-CE76EFBCDB04}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VegetableShop.API.IntegrationTests", "VegetableShop.API.IntegrationTests\VegetableShop.API.IntegrationTests.csproj", "{7D802ED7-C73C-437B-A91E-BA013939AF7C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VegetableShop.Infrastructure", "VegetableShop.Infrastructure\VegetableShop.Infrastructure.csproj", "{CCD37EDB-52CE-4C5B-B413-9A90A30E4F29}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VegetableShop.API", "VegetableShop.API\VegetableShop.API.csproj", "{AA3D52BE-056A-40CD-8F3D-82B9053E04D6}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VegetableShop.Domain.AcceptanceTests", "VegetableShop.Domain.AcceptanceTests\VegetableShop.Domain.AcceptanceTests.csproj", "{BADB4EB0-3817-4114-B256-8162B76C86A3}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{9EE77ACA-9351-46CF-B55B-CE76EFBCDB04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9EE77ACA-9351-46CF-B55B-CE76EFBCDB04}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9EE77ACA-9351-46CF-B55B-CE76EFBCDB04}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9EE77ACA-9351-46CF-B55B-CE76EFBCDB04}.Release|Any CPU.Build.0 = Release|Any CPU
{7D802ED7-C73C-437B-A91E-BA013939AF7C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D802ED7-C73C-437B-A91E-BA013939AF7C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7D802ED7-C73C-437B-A91E-BA013939AF7C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D802ED7-C73C-437B-A91E-BA013939AF7C}.Release|Any CPU.Build.0 = Release|Any CPU
{1838A5EB-E5F8-4CAF-BA53-EBF5ACD6954A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1838A5EB-E5F8-4CAF-BA53-EBF5ACD6954A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1838A5EB-E5F8-4CAF-BA53-EBF5ACD6954A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1838A5EB-E5F8-4CAF-BA53-EBF5ACD6954A}.Release|Any CPU.Build.0 = Release|Any CPU
{CCD37EDB-52CE-4C5B-B413-9A90A30E4F29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CCD37EDB-52CE-4C5B-B413-9A90A30E4F29}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CCD37EDB-52CE-4C5B-B413-9A90A30E4F29}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CCD37EDB-52CE-4C5B-B413-9A90A30E4F29}.Release|Any CPU.Build.0 = Release|Any CPU
{AA3D52BE-056A-40CD-8F3D-82B9053E04D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AA3D52BE-056A-40CD-8F3D-82B9053E04D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AA3D52BE-056A-40CD-8F3D-82B9053E04D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AA3D52BE-056A-40CD-8F3D-82B9053E04D6}.Release|Any CPU.Build.0 = Release|Any CPU
{BADB4EB0-3817-4114-B256-8162B76C86A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BADB4EB0-3817-4114-B256-8162B76C86A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BADB4EB0-3817-4114-B256-8162B76C86A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BADB4EB0-3817-4114-B256-8162B76C86A3}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,31 @@
using Microsoft.Extensions.Logging.Abstractions;
using VegetableShop.API.Services;
using VegetableShop.Infrastructure.Persistence;
using Xunit;
namespace VegetableShop.API.IntegrationTests
{
public class ConsumersServicesTests
{
private readonly ConsumerServices _consumerServices;
public ConsumersServicesTests()
{
var loggerFactory = new NullLoggerFactory();
var consumerRepository = new ConsumerRepositoryImpl(loggerFactory);
var farmerRepository = new FarmerRepositoryImpl(loggerFactory);
var vegetableRepositoryImpl = new VegetableRepositoryImpl();
_consumerServices = new ConsumerServices(loggerFactory,
consumerRepository,
farmerRepository,
vegetableRepositoryImpl);
}
[Fact]
public void ShouldBuyVegetables()
{
_consumerServices.BuyVegetables(1L, 1L, 1L, 10);
}
}
}

View File

@ -0,0 +1,32 @@
using Microsoft.Extensions.Logging.Abstractions;
using VegetableShop.API.Services;
using VegetableShop.Domain.Model;
using VegetableShop.Infrastructure.Persistence;
using Xunit;
namespace VegetableShop.API.IntegrationTests
{
public class FarmerServicesTests
{
private readonly FarmerServices _farmerServices;
private readonly FarmerRepositoryImpl _farmerRepository;
public FarmerServicesTests()
{
var loggerFactory = new NullLoggerFactory();
var vegetableRepository = new VegetableRepositoryImpl();
_farmerRepository = new FarmerRepositoryImpl(loggerFactory);
_farmerServices = new FarmerServices(loggerFactory, _farmerRepository, vegetableRepository);
}
[Fact]
public void ShouldPutVegetableOnSale()
{
var price = new Price(1, "EUR");
_farmerServices.PutOnSale(1L, 1L, price);
var vegetable = _farmerRepository.GetById(1L);
Assert.NotNull(vegetable);
}
}
}

View File

@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<PackageReference Include="coverlet.collector" Version="1.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\VegetableShop.API\VegetableShop.API.csproj" />
<ProjectReference Include="..\VegetableShop.Infrastructure\VegetableShop.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,64 @@
using Microsoft.Extensions.Logging;
using VegetableShop.API.Services;
using VegetableShop.Domain.Events;
using VegetableShop.Domain.Model;
using VegetableShop.Domain.Repositories;
using VegetableShop.Infrastructure.Persistence;
namespace VegetableShop.API
{
public class Program
{
private readonly ConsumerServices _consumerServices;
private readonly FarmerServices _farmerServices;
private ILogger<Program> _logger;
private ILoggerFactory _loggerFactory;
private ILoggerFactory SetupLogging()
{
_loggerFactory = LoggerFactory.Create(builder =>
{
builder
.AddFilter("Microsoft", LogLevel.Warning)
.AddFilter("System", LogLevel.Warning)
.AddFilter("LoggingConsoleApp.Program", LogLevel.Debug)
.AddConsole();
});
_logger = _loggerFactory.CreateLogger<Program>();
return _loggerFactory;
}
private Program()
{
var loggerFactory = SetupLogging();
ConsumerRepository consumerRepository = new ConsumerRepositoryImpl(loggerFactory);
FarmerRepository farmerRepository = new FarmerRepositoryImpl(loggerFactory);
VegetableRepository vegetableRepository = new VegetableRepositoryImpl();
_consumerServices = new ConsumerServices(loggerFactory,
consumerRepository,
farmerRepository,
vegetableRepository);
_farmerServices = new FarmerServices(loggerFactory,
farmerRepository,
vegetableRepository);
DomainEvents.AddObserver(new LogItemOnFarmerPutOnSaleEvent(_loggerFactory));
}
private void Run()
{
_logger.LogInformation("Calling _consumerServices.BuyVegetables");
_farmerServices.PutOnSale(1, 1, new Price(1, "EUR"));
_consumerServices.BuyVegetables(1, 1, 1, 10);
}
public static void Main(string[] args)
{
new Program().Run();
}
}
}

View File

@ -0,0 +1,33 @@
using Microsoft.Extensions.Logging;
using VegetableShop.Domain.Repositories;
namespace VegetableShop.API.Services
{
public class ConsumerServices
{
private readonly ILogger<ConsumerServices> _logger;
private readonly ConsumerRepository _consumerRepository;
private readonly FarmerRepository _farmerRepository;
private readonly VegetableRepository _vegetableRepository;
public ConsumerServices(ILoggerFactory loggerFactory,
ConsumerRepository consumerRepository,
FarmerRepository farmerRepository,
VegetableRepository vegetableRepository)
{
_logger = loggerFactory.CreateLogger<ConsumerServices>();
_consumerRepository = consumerRepository;
_farmerRepository = farmerRepository;
_vegetableRepository = vegetableRepository;
}
public void BuyVegetables(long consumerId, long farmerId, long vegetableId, uint quantity)
{
var consumer = _consumerRepository.GetById(consumerId);
var farmer = _farmerRepository.GetById(farmerId);
var vegetable = _vegetableRepository.GetById(vegetableId);
_logger.LogDebug($"{consumer}, {farmer}, {vegetable}");
consumer.Buy(quantity, vegetable, farmer);
}
}
}

View File

@ -0,0 +1,30 @@
using Microsoft.Extensions.Logging;
using VegetableShop.Domain.Model;
using VegetableShop.Domain.Repositories;
namespace VegetableShop.API.Services
{
public class FarmerServices
{
private readonly ILogger<FarmerServices> _logger;
private readonly FarmerRepository _farmerRepository;
private readonly VegetableRepository _vegetableRepository;
public FarmerServices(ILoggerFactory loggerFactory,
FarmerRepository farmerRepository,
VegetableRepository vegetableRepository)
{
_logger = loggerFactory.CreateLogger<FarmerServices>();
_farmerRepository = farmerRepository;
_vegetableRepository = vegetableRepository;
}
public void PutOnSale(long farmerId, long vegetableId, Price price)
{
var farmer = _farmerRepository.GetById(farmerId);
var vegetable = _vegetableRepository.GetById(vegetableId);
_logger.LogDebug($"{farmer}, {vegetable}");
farmer.PutOnSale(vegetable, price);
}
}
}

View File

@ -0,0 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>VegetableShop.API</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="3.0.1" />
<ProjectReference Include="..\VegetableShop.Domain\VegetableShop.Domain.csproj" />
<ProjectReference Include="..\VegetableShop.Infrastructure\VegetableShop.Infrastructure.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,37 @@
using Xunit;
using Xunit.Gherkin.Quick;
namespace VegetableShop.Domain.AcceptanceTests
{
[FeatureFile("./Features/AddTwoNumbers.feature")]
public sealed class AddTwoNumbers : Feature
{
private readonly Calculator _calculator = new Calculator();
[Given(@"I chose (\d+) as first number")]
public void I_chose_first_number(int firstNumber)
{
_calculator.SetFirstNumber(firstNumber);
}
[And(@"I chose (\d+) as second number")]
public void I_chose_second_number(int secondNumber)
{
_calculator.SetSecondNumber(secondNumber);
}
[When(@"I press add")]
public void I_press_add()
{
_calculator.AddNumbers();
}
[Then(@"the result should be (\d+) on the screen")]
public void The_result_should_be_z_on_the_screen(int expectedResult)
{
var actualResult = _calculator.Result;
Assert.Equal(expectedResult, actualResult);
}
}
}

View File

@ -0,0 +1,15 @@
namespace VegetableShop.Domain.AcceptanceTests
{
public class Calculator
{
private int _firstNumber;
private int _secondNumber;
public void SetFirstNumber(in int firstNumber) => _firstNumber = firstNumber;
public void SetSecondNumber(in int secondNumber) => _secondNumber = secondNumber;
public void AddNumbers() => Result = _firstNumber + _secondNumber;
public int Result { get; private set; }
}
}

View File

@ -0,0 +1,10 @@
Feature: AddTwoNumbers
In order to learn Math
As a regular human
I want to add two numbers using Calculator
Scenario: Add two numbers
Given I chose 12 as first number
And I chose 15 as second number
When I press add
Then the result should be 27 on the screen

View File

@ -0,0 +1,7 @@
Feature: PutOnSale
Scenario: PutOnSale
Given A farmer
And A vegetable of name carrot
When I put the vegetable on sale for 42 EUR
Then The vegetable carrot should be on sale at price 42 EUR

View File

@ -0,0 +1,48 @@
using Microsoft.Extensions.Logging.Abstractions;
using VegetableShop.Domain.Model;
using Xunit;
using Xunit.Gherkin.Quick;
namespace VegetableShop.Domain.AcceptanceTests
{
[FeatureFile("./Features/PutOnSale.feature")]
public sealed class PutOnSale : Feature
{
private static readonly NullLoggerFactory LoggerFactory = new NullLoggerFactory();
private Farmer _farmer;
private Vegetable _vegetable;
[Given(@"A farmer")]
public void A_Farmer()
{
_farmer = new Farmer(LoggerFactory);
Assert.NotNull(_farmer);
}
[And(@"A vegetable of name (\w+)")]
public void A_Vegetable(string name)
{
_vegetable = new Vegetable(name);
}
[When(@"I put the vegetable on sale for (\d+) (\w+)")]
public void I_Put_On_Sale(decimal value, string currency)
{
var price = new Price(value, currency);
_farmer.PutOnSale(_vegetable, price);
Assert.Contains(_farmer.VegetablesForSale, pair => pair.Key == _vegetable);
}
[Then(@"The vegetable (\w+) should be on sale at price (\d+) (\w+)")]
public void The_vegetable_should_be_on_sale_at_price_x(string name,
decimal value,
string currency)
{
Assert.Equal(_vegetable.Name, name);
var price = _farmer.VegetablesForSale[_vegetable];
Assert.Equal(price.Value, value);
Assert.Equal(price.Currency, currency);
}
}
}

View File

@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="Xunit.Gherkin.Quick" Version="4.0.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
<PackageReference Include="coverlet.collector" Version="1.2.0" />
</ItemGroup>
<ItemGroup>
<None Update="Features\AddTwoNumbers.feature">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Features\PutOnSale.feature">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\VegetableShop.Domain\VegetableShop.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="PutOnSale.cs">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Compile>
<Compile Update="AddTwoNumbers.cs">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Compile>
</ItemGroup>
</Project>

View File

@ -0,0 +1,13 @@
using System;
using VegetableShop.Domain.Model;
namespace VegetableShop.Domain.Errors
{
public class UnauthorizedSaleException : Exception
{
public UnauthorizedSaleException(Farmer farmer, Vegetable vegetable)
{
throw new NotImplementedException();
}
}
}

View File

@ -0,0 +1,21 @@
using System.Collections;
using System.Collections.Generic;
namespace VegetableShop.Domain.Events
{
public static class DomainEvents
{
private static readonly ICollection<EventHandler> Observers = new List<EventHandler>();
public static void Notify(Event @event)
{
foreach (var obs in Observers)
{
obs.Handle(@event);
}
}
public static void AddObserver(EventHandler obs) => Observers.Add(obs);
public static bool RemoveObserver(EventHandler obs) => Observers.Remove(obs);
}
}

View File

@ -0,0 +1,6 @@
namespace VegetableShop.Domain.Events
{
public interface Event
{
}
}

View File

@ -0,0 +1,7 @@
namespace VegetableShop.Domain.Events
{
public interface EventHandler
{
void Handle(Event @event);
}
}

View File

@ -0,0 +1,23 @@
using VegetableShop.Domain.Model;
namespace VegetableShop.Domain.Events
{
public class FarmerPutOnSaleEvent : Event
{
public long Id { get; }
public Vegetable Vegetable { get; }
public Price Price { get; }
public FarmerPutOnSaleEvent(in long id, Vegetable vegetable, Price price)
{
Id = id;
Vegetable = vegetable;
Price = price;
}
public override string ToString()
{
return $"{nameof(Id)}: {Id}, {nameof(Vegetable)}: {Vegetable}, {nameof(Price)}: {Price}";
}
}
}

View File

@ -0,0 +1,19 @@
using Microsoft.Extensions.Logging;
namespace VegetableShop.Domain.Events
{
public class LogItemOnFarmerPutOnSaleEvent: EventHandler
{
private readonly ILogger<LogItemOnFarmerPutOnSaleEvent> _logger;
public LogItemOnFarmerPutOnSaleEvent(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<LogItemOnFarmerPutOnSaleEvent>();
}
private void Handle(FarmerPutOnSaleEvent @event)
=> _logger.LogInformation($"Received {nameof(@event)}, {@event}");
public void Handle(Event @event) => Handle(@event as FarmerPutOnSaleEvent);
}
}

View File

@ -0,0 +1,24 @@
using Microsoft.Extensions.Logging;
namespace VegetableShop.Domain.Model
{
public class Consumer
{
private readonly ILogger<Consumer> _logger;
public Consumer(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<Consumer>();
}
public void Buy(uint quantity, Vegetable vegetable, Farmer farmer)
{
_logger.LogInformation($"Buying {quantity} {vegetable} from {farmer}");
}
public void SubscribeToPriceDrop(Farmer farmer)
{
_logger.LogInformation($"Subscribing to price drop from {farmer}");
}
}
}

View File

@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using VegetableShop.Domain.Errors;
using VegetableShop.Domain.Events;
using VegetableShop.Domain.Specifications;
namespace VegetableShop.Domain.Model
{
public class Farmer
{
private readonly ILogger<Farmer> _logger;
public Dictionary<Vegetable, Price> VegetablesForSale { get; } = new Dictionary<Vegetable, Price>();
private static readonly long Id = new Random().Next(int.MaxValue);
public Farmer(ILoggerFactory loggerFactory) => _logger = loggerFactory.CreateLogger<Farmer>();
public void PutOnSale(Vegetable vegetable, Price price)
{
if (AuthorizedSaleSpecification.IsSatisfiedBy(this, vegetable, price))
{
VegetablesForSale.Add(vegetable, price);
_logger.LogInformation($"Add {vegetable} for price {price}");
DomainEvents.Notify(new FarmerPutOnSaleEvent(Id, vegetable, price));
}
else
{
throw new UnauthorizedSaleException(this, vegetable);
}
}
public void WithdrawFromSale() => throw new NotImplementedException();
public void ChangePrice() => throw new NotImplementedException();
public override string ToString() => $"Farmer[id={Id}]";
}
}

View File

@ -0,0 +1,17 @@
namespace VegetableShop.Domain.Model
{
public readonly struct Price
{
public decimal Value { get; }
public string Currency { get; }
public Price(decimal value, string currency = "EUR")
{
Value = value;
Currency = currency;
}
public override string ToString() => $"{Value} {Currency}";
}
}

View File

@ -0,0 +1,19 @@
using System;
using System.Collections.Generic;
namespace VegetableShop.Domain.Model
{
public class Vegetable
{
private readonly long _id;
public string Name { get; }
public Vegetable(string name)
{
_id = new Random().Next(int.MaxValue);
Name = name;
}
public override string ToString() => $"Vegetable[id={_id}, name={Name}";
}
}

View File

@ -0,0 +1,9 @@
using VegetableShop.Domain.Model;
namespace VegetableShop.Domain.Repositories
{
public interface ConsumerRepository
{
Consumer GetById(in long consumerId);
}
}

View File

@ -0,0 +1,9 @@
using VegetableShop.Domain.Model;
namespace VegetableShop.Domain.Repositories
{
public interface FarmerRepository
{
Farmer GetById(in long farmerId);
}
}

View File

@ -0,0 +1,9 @@
using VegetableShop.Domain.Model;
namespace VegetableShop.Domain.Repositories
{
public interface VegetableRepository
{
Vegetable GetById(in long vegetableId);
}
}

View File

@ -0,0 +1,9 @@
using VegetableShop.Domain.Model;
namespace VegetableShop.Domain.Specifications
{
public static class AuthorizedSaleSpecification
{
public static bool IsSatisfiedBy(Farmer farmer, Vegetable vegetable, Price price) => true;
}
}

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.0.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,22 @@
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using VegetableShop.Domain.Model;
using VegetableShop.Domain.Repositories;
namespace VegetableShop.Infrastructure.Persistence
{
public class ConsumerRepositoryImpl: ConsumerRepository
{
private readonly Dictionary<long, Consumer> _consumers;
public ConsumerRepositoryImpl(ILoggerFactory loggerFactory)
{
_consumers = new Dictionary<long, Consumer>
{
{ 1L, new Consumer(loggerFactory) }
};
}
public Consumer GetById(in long consumerId) => _consumers[consumerId];
}
}

View File

@ -0,0 +1,25 @@
using System.Collections.Generic;
using Microsoft.Extensions.Logging;
using VegetableShop.Domain.Model;
using VegetableShop.Domain.Repositories;
namespace VegetableShop.Infrastructure.Persistence
{
public class FarmerRepositoryImpl: FarmerRepository
{
private readonly ILoggerFactory _loggerFactory;
public FarmerRepositoryImpl(ILoggerFactory loggerFactory)
{
_loggerFactory = loggerFactory;
_farmers = new Dictionary<long, Farmer>
{
{ 1L, new Farmer(_loggerFactory) }
};
}
private readonly Dictionary<long, Farmer> _farmers;
public Farmer GetById(in long farmerId) => _farmers[farmerId];
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
using VegetableShop.Domain.Model;
using VegetableShop.Domain.Repositories;
namespace VegetableShop.Infrastructure.Persistence
{
public class VegetableRepositoryImpl: VegetableRepository
{
private readonly Dictionary<long, Vegetable> _vegetables = new Dictionary<long, Vegetable>
{
{ 1L, new Vegetable("carrot") }
};
public Vegetable GetById(in long vegetableId) => _vegetables[vegetableId];
}
}

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\VegetableShop.Domain\VegetableShop.Domain.csproj" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="3.0.1" />
</ItemGroup>
</Project>