├── .gitignore
├── LICENSE
├── README.md
└── src
├── .nuget
├── NuGet.Config
├── NuGet.exe
└── NuGet.targets
├── CQRSShop.Contracts
├── CQRSShop.Contracts.Helpers.fs
├── CQRSShop.Contracts.fsproj
├── Commands.fs
├── Events.fs
└── Types.fs
├── CQRSShop.Domain.Tests
├── BasketTests
│ ├── AddItemToBasketTest.cs
│ ├── CheckoutBasketTests.cs
│ ├── CreateBasketTests.cs
│ ├── MakePaymentTests.cs
│ └── ProceedCheckoutBasketTests.cs
├── CQRSShop.Domain.Tests.csproj
├── CustomerTests
│ ├── CreateCustomerTest.cs
│ └── MakeCustomerPreferredTest.cs
├── OrderTests
│ └── AllTheOrderTests.cs
├── ProductTests
│ └── CreateProductTests.cs
├── Properties
│ └── AssemblyInfo.cs
├── TestBase.cs
└── packages.config
├── CQRSShop.Domain
├── Aggregates
│ ├── Basket.cs
│ ├── Customer.cs
│ ├── Order.cs
│ └── Product.cs
├── CQRSShop.Domain.csproj
├── CommandHandlers
│ ├── BasketCommandHandler.cs
│ ├── CustomerCommandHandler.cs
│ ├── OrderHandler.cs
│ └── ProductCommandHandler.cs
├── DomainEntry.cs
├── Exceptions
│ ├── CustomerAlreadyExistsException.cs
│ └── DuplicateAggregateException.cs
└── Properties
│ └── AssemblyInfo.cs
├── CQRSShop.Infrastructure
├── AggregateBase.cs
├── CQRSShop.Infrastructure.csproj
├── CommandDispatcher.cs
├── DomainRepositoryBase.cs
├── EventStoreDomainRepository.cs
├── Exceptions
│ └── AggregateNotFoundException.cs
├── IAggregate.cs
├── ICommand.cs
├── IDomainRepository.cs
├── IEvent.cs
├── IHandle.cs
├── IdGenerator.cs
├── InMemoryDomainRespository.cs
├── Properties
│ └── AssemblyInfo.cs
└── packages.config
├── CQRSShop.Search
├── CQRSShop.Search.csproj
├── Class1.cs
└── Properties
│ └── AssemblyInfo.cs
├── CQRSShop.Service
├── App.config
├── CQRSShop.Service.csproj
├── Documents
│ ├── Basket.cs
│ ├── Customer.cs
│ └── Product.cs
├── EventSerialization.cs
├── EventStoreConnectionWrapper.cs
├── Indexer.cs
├── IndexingServie.cs
├── Program.cs
├── Properties
│ └── AssemblyInfo.cs
└── packages.config
├── CQRSShop.Web
├── Api
│ ├── BasePostEndpoint.cs
│ ├── Basket
│ │ ├── Checkout
│ │ │ └── PostEndpoint.cs
│ │ ├── Items
│ │ │ └── PostEndpoint.cs
│ │ ├── Pay
│ │ │ └── PostEndpoint.cs
│ │ ├── PostEndpoint.cs
│ │ └── Proceed
│ │ │ └── PostEndpoint.cs
│ ├── Customer
│ │ ├── PostEndpoint.cs
│ │ └── Preferred
│ │ │ └── PostEndpoint.cs
│ ├── GetEndpoint.cs
│ ├── Order
│ │ ├── Approve
│ │ │ └── PostEndpoint.cs
│ │ ├── Cancel
│ │ │ └── PostEndpoint.cs
│ │ ├── Ship
│ │ │ └── PostEndpoint.cs
│ │ └── StartShipping
│ │ │ └── PostEndpoint.cs
│ └── Product
│ │ └── PostEndpoint.cs
├── CQRSShop.Web.csproj
├── Configuration.cs
├── OwinAppSetup.cs
├── Properties
│ └── AssemblyInfo.cs
├── Web.Debug.config
├── Web.Release.config
├── Web.config
└── packages.config
└── CQRSShop.sln
/.gitignore:
--------------------------------------------------------------------------------
1 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs)
2 | [Bb]in/
3 | [Oo]bj/
4 |
5 | # mstest test results
6 | TestResults
7 |
8 | ## Ignore Visual Studio temporary files, build results, and
9 | ## files generated by popular Visual Studio add-ons.
10 |
11 | # User-specific files
12 | *.suo
13 | *.user
14 | *.sln.docstates
15 |
16 | # Build results
17 | [Dd]ebug/
18 | [Rr]elease/
19 | x64/
20 | *_i.c
21 | *_p.c
22 | *.ilk
23 | *.meta
24 | *.obj
25 | *.pch
26 | *.pdb
27 | *.pgc
28 | *.pgd
29 | *.rsp
30 | *.sbr
31 | *.tlb
32 | *.tli
33 | *.tlh
34 | *.tmp
35 | *.log
36 | *.vspscc
37 | *.vssscc
38 | .builds
39 |
40 | # Visual C++ cache files
41 | ipch/
42 | *.aps
43 | *.ncb
44 | *.opensdf
45 | *.sdf
46 |
47 | # Visual Studio profiler
48 | *.psess
49 | *.vsp
50 | *.vspx
51 |
52 | # Guidance Automation Toolkit
53 | *.gpState
54 |
55 | # ReSharper is a .NET coding add-in
56 | _ReSharper*
57 |
58 | # NCrunch
59 | *.ncrunch*
60 | .*crunch*.local.xml
61 |
62 | # Installshield output folder
63 | [Ee]xpress
64 |
65 | # DocProject is a documentation generator add-in
66 | DocProject/buildhelp/
67 | DocProject/Help/*.HxT
68 | DocProject/Help/*.HxC
69 | DocProject/Help/*.hhc
70 | DocProject/Help/*.hhk
71 | DocProject/Help/*.hhp
72 | DocProject/Help/Html2
73 | DocProject/Help/html
74 |
75 | # Click-Once directory
76 | publish
77 |
78 | # Publish Web Output
79 | *.Publish.xml
80 |
81 | # NuGet Packages Directory
82 | packages
83 |
84 | # Windows Azure Build Output
85 | csx
86 | *.build.csdef
87 |
88 | # Windows Store app package directory
89 | AppPackages/
90 |
91 | # Others
92 | [Bb]in
93 | [Oo]bj
94 | sql
95 | TestResults
96 | [Tt]est[Rr]esult*
97 | *.Cache
98 | ClientBin
99 | [Ss]tyle[Cc]op.*
100 | ~$*
101 | *.dbmdl
102 | Generated_Code #added for RIA/Silverlight projects
103 |
104 | # Backup & report files from converting an old project file to a newer
105 | # Visual Studio version. Backup files are not needed, because we have git ;-)
106 | _UpgradeReport_Files/
107 | Backup*/
108 | UpgradeLog*.XML
109 |
110 | src/Rapporteringsregisteret.Web/assets/less/*.css
111 |
112 | MetricResults/
113 | *.sln.ide/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2014 Tomas Jansson
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | CQRSShop
2 | ========
3 |
4 | Simple CQRS and eventsourcing with eventstore, elasticsearch and neo4j. I've written about it here: http://blog.tomasjansson.com/tag/cqrsshop/
5 |
6 | For a pure functional version see: https://github.com/mastoj/FsCQRSShop
7 |
--------------------------------------------------------------------------------
/src/.nuget/NuGet.Config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/.nuget/NuGet.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mastoj/CQRSShop/8af7e28e7b4ea386a41dc0b0e2f1448334d0e0a6/src/.nuget/NuGet.exe
--------------------------------------------------------------------------------
/src/.nuget/NuGet.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(MSBuildProjectDirectory)\..\
5 |
6 |
7 | false
8 |
9 |
10 | false
11 |
12 |
13 | true
14 |
15 |
16 | false
17 |
18 |
19 |
20 |
21 |
22 |
26 |
27 |
28 |
29 |
30 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget"))
31 |
32 |
33 |
34 |
35 | $(SolutionDir).nuget
36 |
37 |
38 |
39 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName.Replace(' ', '_')).config
40 | $(MSBuildProjectDirectory)\packages.$(MSBuildProjectName).config
41 |
42 |
43 |
44 | $(MSBuildProjectDirectory)\packages.config
45 | $(PackagesProjectConfig)
46 |
47 |
48 |
49 |
50 | $(NuGetToolsPath)\NuGet.exe
51 | @(PackageSource)
52 |
53 | "$(NuGetExePath)"
54 | mono --runtime=v4.0.30319 "$(NuGetExePath)"
55 |
56 | $(TargetDir.Trim('\\'))
57 |
58 | -RequireConsent
59 | -NonInteractive
60 |
61 | "$(SolutionDir) "
62 | "$(SolutionDir)"
63 |
64 |
65 | $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(NonInteractiveSwitch) $(RequireConsentSwitch) -solutionDir $(PaddedSolutionDir)
66 | $(NuGetCommand) pack "$(ProjectPath)" -Properties "Configuration=$(Configuration);Platform=$(Platform)" $(NonInteractiveSwitch) -OutputDirectory "$(PackageOutputDir)" -symbols
67 |
68 |
69 |
70 | RestorePackages;
71 | $(BuildDependsOn);
72 |
73 |
74 |
75 |
76 | $(BuildDependsOn);
77 | BuildPackage;
78 |
79 |
80 |
81 |
82 |
83 |
84 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
99 |
100 |
103 |
104 |
105 |
106 |
108 |
109 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
141 |
142 |
143 |
144 |
145 |
--------------------------------------------------------------------------------
/src/CQRSShop.Contracts/CQRSShop.Contracts.Helpers.fs:
--------------------------------------------------------------------------------
1 | module CQRSShop.Contracts.Helpers
2 |
3 | let ToFSharpList x = List.ofSeq x
4 |
5 |
--------------------------------------------------------------------------------
/src/CQRSShop.Contracts/CQRSShop.Contracts.fsproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | 2.0
8 | 66f74977-c96f-4af0-9f98-d2ca94749649
9 | Library
10 | CQRSShop.Contracts
11 | CQRSShop.Contracts
12 | v4.5.1
13 | 4.3.1.0
14 | CQRSShop.Contracts
15 |
16 |
17 | true
18 | full
19 | false
20 | false
21 | bin\Debug\
22 | DEBUG;TRACE
23 | 3
24 | bin\Debug\CQRSShop.Contracts.XML
25 |
26 |
27 | pdbonly
28 | true
29 | true
30 | bin\Release\
31 | TRACE
32 | 3
33 | bin\Release\CQRSShop.Contracts.XML
34 |
35 |
36 |
37 |
38 | True
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | CQRSShop.Infrastructure
53 | {57ae018a-ba49-471d-97d7-c4ad2040d4b0}
54 | True
55 |
56 |
57 |
58 | 11
59 |
60 |
61 |
62 |
63 | $(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets
64 |
65 |
66 |
67 |
68 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets
69 |
70 |
71 |
72 |
73 |
80 |
--------------------------------------------------------------------------------
/src/CQRSShop.Contracts/Commands.fs:
--------------------------------------------------------------------------------
1 | namespace CQRSShop.Contracts.Commands
2 | open CQRSShop.Contracts.Types
3 | open CQRSShop.Infrastructure
4 | open System
5 |
6 | // Customer commands
7 | type CreateCustomer = {Id: Guid; Name: string } with interface ICommand
8 | type MarkCustomerAsPreferred = {Id: Guid; Discount: int } with interface ICommand
9 |
10 | // Product commands
11 | type CreateProduct = {Id: Guid; Name: string; Price: int } with interface ICommand
12 |
13 | // Basket commands
14 | type CreateBasket = { Id: Guid; CustomerId: Guid} with interface ICommand
15 | type AddItemToBasket = { Id: Guid; ProductId: Guid; Quantity: int } with interface ICommand
16 | type ProceedToCheckout = { Id: Guid } with interface ICommand
17 | type CheckoutBasket = { Id: Guid; ShippingAddress: Address } with interface ICommand
18 | type MakePayment = {Id: Guid; Payment: int } with interface ICommand
19 |
20 | // Order commands
21 | type StartShippingProcess = { Id: Guid } with interface ICommand
22 | type CancelOrder = { Id: Guid } with interface ICommand
23 | type ShipOrder = { Id: Guid } with interface ICommand
24 | type ApproveOrder = { Id: Guid } with interface ICommand
25 |
--------------------------------------------------------------------------------
/src/CQRSShop.Contracts/Events.fs:
--------------------------------------------------------------------------------
1 | namespace CQRSShop.Contracts.Events
2 | open CQRSShop.Contracts.Types
3 | open CQRSShop.Infrastructure
4 | open System
5 |
6 | // Customer events
7 | type CustomerCreated = {Id: Guid; Name: string }
8 | with interface IEvent with member this.Id with get() = this.Id
9 |
10 | type CustomerMarkedAsPreferred = {Id: Guid; Discount: int }
11 | with interface IEvent with member this.Id with get() = this.Id
12 |
13 | // Product events
14 | type ProductCreated = {Id: Guid; Name: string; Price: int }
15 | with interface IEvent with member this.Id with get() = this.Id
16 |
17 | // Basket events
18 | type BasketCreated = { Id: Guid; CustomerId: Guid; Discount: int}
19 | with interface IEvent with member this.Id with get() = this.Id
20 |
21 | type ItemAdded = { Id: Guid; OrderLine: OrderLine}
22 | with
23 | override this.ToString() = sprintf "Item added. Id: %O, %O" this.Id this.OrderLine
24 | interface IEvent with member this.Id with get() = this.Id
25 |
26 | type CustomerIsCheckingOutBasket = { Id: Guid }
27 | with interface IEvent with member this.Id with get() = this.Id
28 |
29 | type BasketCheckedOut = { Id: Guid; ShippingAddress: Address }
30 | with interface IEvent with member this.Id with get() = this.Id
31 |
32 | // Order events
33 | type OrderCreated ={ Id: Guid; BasketId: Guid; OrderLines: OrderLine list }
34 | with interface IEvent with member this.Id with get() = this.Id
35 |
36 | type ShippingProcessStarted = {Id: Guid}
37 | with interface IEvent with member this.Id with get() = this.Id
38 |
39 | type OrderCancelled = {Id: Guid}
40 | with interface IEvent with member this.Id with get() = this.Id
41 |
42 | type OrderShipped = {Id: Guid}
43 | with interface IEvent with member this.Id with get() = this.Id
44 |
45 | type NeedsApproval = {Id: Guid}
46 | with interface IEvent with member this.Id with get() = this.Id
47 |
48 | type OrderApproved = {Id: Guid}
49 | with interface IEvent with member this.Id with get() = this.Id
50 |
51 |
--------------------------------------------------------------------------------
/src/CQRSShop.Contracts/Types.fs:
--------------------------------------------------------------------------------
1 | namespace CQRSShop.Contracts.Types
2 | open System
3 |
4 | type Address = { Street: string }
5 | type OrderLine = {ProductId: Guid; ProductName: string; OriginalPrice: int; DiscountedPrice: int; Quantity: int}
6 | with override this.ToString() = sprintf "ProdcutName: %s, Price: %d, Discounted: %d, Quantity: %d" this.ProductName this.OriginalPrice this.DiscountedPrice this.Quantity
7 |
--------------------------------------------------------------------------------
/src/CQRSShop.Domain.Tests/BasketTests/AddItemToBasketTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CQRSShop.Contracts.Commands;
3 | using CQRSShop.Contracts.Events;
4 | using CQRSShop.Contracts.Types;
5 | using CQRSShop.Tests;
6 | using NUnit.Framework;
7 |
8 | namespace CQRSShop.Domain.Tests.BasketTests
9 | {
10 | [TestFixture]
11 | public class AddItemToBasketTest : TestBase
12 | {
13 | [TestCase("NameA", 100, 10)]
14 | [TestCase("NameB", 200, 20)]
15 | public void GivenWeHaveABasketForARegularCustomer_WhenAddingItems_ThePriceOfTheBasketShouldNotBeDiscounted(string productName, int itemPrice, int quantity)
16 | {
17 | var customerId = Guid.NewGuid();
18 | var productId = Guid.NewGuid();
19 | var id = Guid.NewGuid();
20 | var expectedOrderLine = new OrderLine(productId, productName, itemPrice, itemPrice, quantity);
21 | Given(new ProductCreated(productId, productName, itemPrice),
22 | new BasketCreated(id, customerId, 0));
23 | When(new AddItemToBasket(id, productId, quantity));
24 | Then(new ItemAdded(id, expectedOrderLine));
25 | }
26 |
27 | [TestCase("NameA", 100, 10, 10, 90)]
28 | [TestCase("NameB", 200, 20, 80, 40)]
29 | public void GivenWeHaveABasketForAPreferredCustomer_WhenAddingItems_ThePriceOfTheBasketShouldBeDiscounted(string productName, int itemPrice, int quantity, int discountPercentage, int discountedPrice)
30 | {
31 | var customerId = Guid.NewGuid();
32 | var productId = Guid.NewGuid();
33 | var id = Guid.NewGuid();
34 | var expectedOrderLine = new OrderLine(productId, productName, itemPrice, discountedPrice, quantity);
35 | Given(new CustomerCreated(customerId, "John Doe"),
36 | new CustomerMarkedAsPreferred(customerId, discountPercentage),
37 | new ProductCreated(productId, productName, itemPrice),
38 | new BasketCreated(id, customerId, discountPercentage));
39 | When(new AddItemToBasket(id, productId, quantity));
40 | Then(new ItemAdded(id, expectedOrderLine));
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Domain.Tests/BasketTests/CheckoutBasketTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CQRSShop.Contracts.Commands;
3 | using CQRSShop.Contracts.Events;
4 | using CQRSShop.Contracts.Types;
5 | using CQRSShop.Domain.Exceptions;
6 | using CQRSShop.Tests;
7 | using NUnit.Framework;
8 |
9 | namespace CQRSShop.Domain.Tests.BasketTests
10 | {
11 | [TestFixture]
12 | public class CheckoutBasketTests : TestBase
13 | {
14 | [TestCase(null)]
15 | [TestCase("")]
16 | [TestCase(" ")]
17 | public void WhenTheUserCheckoutWithInvalidAddress_IShouldGetNotified(string street)
18 | {
19 | var address = street == null ? null : new Address(street);
20 | var id = Guid.NewGuid();
21 | Given(new BasketCreated(id, Guid.NewGuid(), 0));
22 | WhenThrows(new CheckoutBasket(id, address));
23 | }
24 |
25 | [Test]
26 | public void WhenTheUserCheckoutWithAValidAddress_IShouldProceedToTheNextStep()
27 | {
28 | var address = new Address("Valid street");
29 | var id = Guid.NewGuid();
30 | Given(new BasketCreated(id, Guid.NewGuid(), 0));
31 | When(new CheckoutBasket(id, address));
32 | Then(new BasketCheckedOut(id, address));
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Domain.Tests/BasketTests/CreateBasketTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CQRSShop.Contracts.Commands;
3 | using CQRSShop.Contracts.Events;
4 | using CQRSShop.Domain.Exceptions;
5 | using CQRSShop.Infrastructure.Exceptions;
6 | using CQRSShop.Tests;
7 | using NUnit.Framework;
8 |
9 | namespace CQRSShop.Domain.Tests.BasketTests
10 | {
11 | [TestFixture]
12 | public class CreateBasketTests : TestBase
13 | {
14 | [Test]
15 | public void GivenCustomerWithIdXExists_WhenCreatingABasketForCustomerX_ThenTheBasketShouldBeCreated()
16 | {
17 | var id = Guid.NewGuid();
18 | var customerId = Guid.NewGuid();
19 | int discount = 0;
20 | string name = "John doe";
21 | Given(new CustomerCreated(customerId, name));
22 | When(new CreateBasket(id, customerId));
23 | Then(new BasketCreated(id, customerId, discount));
24 | }
25 |
26 | [Test]
27 | public void GivenNoCustomerWithIdXExists_WhenCreatingABasketForCustomerX_IShouldGetNotified()
28 | {
29 | var id = Guid.NewGuid();
30 | var customerId = Guid.NewGuid();
31 | WhenThrows(new CreateBasket(id, customerId));
32 | }
33 |
34 | [Test]
35 | public void GivenCustomerWithIdXExistsAndBasketAlreadyExistsForIdY_WhenCreatingABasketForCustomerXAndIdY_IShouldGetNotified()
36 | {
37 | var id = Guid.NewGuid();
38 | var customerId = Guid.NewGuid();
39 | string name = "John doe";
40 | int discount = 0;
41 | Given(new BasketCreated(id, Guid.NewGuid(), discount),
42 | new CustomerCreated(customerId, name));
43 | WhenThrows(new CreateBasket(id, customerId));
44 | }
45 |
46 | [Test]
47 | public void GivenACustomerWithADiscount_CreatingABasketForTheCustomer_TheDiscountShouldBeIncluded()
48 | {
49 | var id = Guid.NewGuid();
50 | var customerId = Guid.NewGuid();
51 | int discount = 89;
52 | string name = "John doe";
53 | Given(new CustomerCreated(customerId, name),
54 | new CustomerMarkedAsPreferred(customerId, discount));
55 | When(new CreateBasket(id, customerId));
56 | Then(new BasketCreated(id, customerId, discount));
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Domain.Tests/BasketTests/MakePaymentTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CQRSShop.Contracts;
3 | using CQRSShop.Contracts.Commands;
4 | using CQRSShop.Contracts.Events;
5 | using CQRSShop.Contracts.Types;
6 | using CQRSShop.Domain.Exceptions;
7 | using CQRSShop.Infrastructure;
8 | using CQRSShop.Tests;
9 | using Microsoft.FSharp.Collections;
10 | using NUnit.Framework;
11 |
12 | namespace CQRSShop.Domain.Tests.BasketTests
13 | {
14 | [TestFixture]
15 | public class MakePaymentTests : TestBase
16 | {
17 | [TestCase(100, 101)]
18 | [TestCase(100, 99)]
19 | [TestCase(100, 91)]
20 | [TestCase(100, 89)]
21 | public void WhenNotPayingTheExpectedAmount_IShouldGetNotified(int productPrice, int payment)
22 | {
23 | var id = Guid.NewGuid();
24 | var existingOrderLine = new OrderLine(Guid.NewGuid(), "", productPrice, productPrice, 1);
25 | Given(new BasketCreated(id, Guid.NewGuid(), 0),
26 | new ItemAdded(id, existingOrderLine));
27 | WhenThrows(new MakePayment(id, payment));
28 | }
29 |
30 | [TestCase(100, 101, 101)]
31 | [TestCase(100, 80, 80)]
32 | public void WhenPayingTheExpectedAmount_ThenANewOrderShouldBeCreatedFromTheResult(int productPrice, int discountPrice, int payment)
33 | {
34 | var id = Guid.NewGuid();
35 | int dontCare = 0;
36 | var orderId = Guid.NewGuid();
37 | IdGenerator.GenerateGuid = () => orderId;
38 | var existingOrderLine = new OrderLine(Guid.NewGuid(), "Ball", productPrice, discountPrice, 1);
39 | Given(new BasketCreated(id, Guid.NewGuid(), dontCare),
40 | new ItemAdded(id, existingOrderLine));
41 | When(new MakePayment(id, payment));
42 |
43 | var items = Helpers.ToFSharpList(new[] {existingOrderLine});
44 | Then(new OrderCreated(orderId, id, items),
45 | new OrderApproved(orderId));
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Domain.Tests/BasketTests/ProceedCheckoutBasketTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CQRSShop.Contracts.Commands;
3 | using CQRSShop.Contracts.Events;
4 | using CQRSShop.Tests;
5 | using NUnit.Framework;
6 |
7 | namespace CQRSShop.Domain.Tests.BasketTests
8 | {
9 | [TestFixture]
10 | public class ProceedCheckoutBasketTests : TestBase
11 | {
12 | [Test]
13 | public void GivenABasket_WhenCreatingABasketForCustomerX_ThenTheBasketShouldBeCreated()
14 | {
15 | var id = Guid.NewGuid();
16 | var customerId = Guid.NewGuid();
17 | int discount = 0;
18 | Given(new BasketCreated(id, customerId, discount));
19 | When(new ProceedToCheckout(id));
20 | Then(new CustomerIsCheckingOutBasket(id));
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Domain.Tests/CQRSShop.Domain.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {737BFB25-3617-437E-90D6-C15ECAD8FB25}
8 | Library
9 | Properties
10 | CQRSShop.Domain.Tests
11 | CQRSShop.Domain.Tests
12 | v4.5.1
13 | 512
14 | ..\
15 | true
16 |
17 |
18 | true
19 | full
20 | false
21 | bin\Debug\
22 | DEBUG;TRACE
23 | prompt
24 | 4
25 |
26 |
27 | pdbonly
28 | true
29 | bin\Release\
30 | TRACE
31 | prompt
32 | 4
33 |
34 |
35 |
36 |
37 | ..\packages\NUnit.2.6.3\lib\nunit.framework.dll
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | {66f74977-c96f-4af0-9f98-d2ca94749649}
66 | CQRSShop.Contracts
67 |
68 |
69 | {00afe5da-93ef-4c5a-be98-006ecf0e42e6}
70 | CQRSShop.Domain
71 |
72 |
73 | {57ae018a-ba49-471d-97d7-c4ad2040d4b0}
74 | CQRSShop.Infrastructure
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
83 |
84 |
85 |
86 |
93 |
--------------------------------------------------------------------------------
/src/CQRSShop.Domain.Tests/CustomerTests/CreateCustomerTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CQRSShop.Contracts.Commands;
3 | using CQRSShop.Contracts.Events;
4 | using CQRSShop.Domain.Exceptions;
5 | using CQRSShop.Tests;
6 | using NUnit.Framework;
7 |
8 | namespace CQRSShop.Domain.Tests.CustomerTests
9 | {
10 | [TestFixture]
11 | public class CreateCustomerTest : TestBase
12 | {
13 | [Test]
14 | public void WhenCreatingTheCustomer_TheCustomerShouldBeCreatedWithTheRightName()
15 | {
16 | Guid id = Guid.NewGuid();
17 | When(new CreateCustomer(id, "Tomas"));
18 | Then(new CustomerCreated(id, "Tomas"));
19 | }
20 |
21 | [Test]
22 | public void GivenAUserWithIdXExists_WhenCreatingACustomerWithIdX_IShouldGetNotifiedThatTheUserAlreadyExists()
23 | {
24 | Guid id = Guid.NewGuid();
25 | Given(new CustomerCreated(id, "Something I don't care about"));
26 | WhenThrows(new CreateCustomer(id, "Tomas"));
27 | }
28 | }
29 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Domain.Tests/CustomerTests/MakeCustomerPreferredTest.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CQRSShop.Contracts.Commands;
3 | using CQRSShop.Contracts.Events;
4 | using CQRSShop.Tests;
5 | using NUnit.Framework;
6 |
7 | namespace CQRSShop.Domain.Tests.CustomerTests
8 | {
9 | [TestFixture]
10 | public class MarkCustomerAsPreferredTest : TestBase
11 | {
12 | [TestCase(25)]
13 | [TestCase(50)]
14 | [TestCase(70)]
15 | public void GivenTheUserExists_WhenMarkingCustomerAsPreferred_ThenTheCustomerShouldBePreferred(int discount)
16 | {
17 | Guid id = Guid.NewGuid();
18 | Given(new CustomerCreated(id, "Superman"));
19 | When(new MarkCustomerAsPreferred(id, discount));
20 | Then(new CustomerMarkedAsPreferred(id, discount));
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Domain.Tests/OrderTests/AllTheOrderTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CQRSShop.Contracts.Commands;
3 | using CQRSShop.Contracts.Events;
4 | using CQRSShop.Contracts.Types;
5 | using CQRSShop.Domain.Exceptions;
6 | using CQRSShop.Infrastructure;
7 | using CQRSShop.Tests;
8 | using Microsoft.FSharp.Collections;
9 | using CQRSShop.Contracts;
10 | using NUnit.Framework;
11 |
12 | namespace CQRSShop.Domain.Tests.OrderTests
13 | {
14 | [TestFixture]
15 | public class AllTheOrderTests : TestBase
16 | {
17 | [Test]
18 | public void WhenStartingShippingProcess_TheShippingShouldBeStarted()
19 | {
20 | var id = Guid.NewGuid();
21 | var orderCreated = BuildOrderCreated(id, basketId: Guid.NewGuid(), numberOfOrderLines: 1);
22 | Given(orderCreated);
23 | When(new StartShippingProcess(id));
24 | Then(new ShippingProcessStarted(id));
25 | }
26 |
27 | [Test]
28 | public void WhenCancellingAnOrderThatHasntBeenStartedShipping_TheOrderShouldBeCancelled()
29 | {
30 | var id = Guid.NewGuid();
31 | var orderCreated = BuildOrderCreated(id, basketId: Guid.NewGuid(), numberOfOrderLines: 1);
32 | Given(orderCreated);
33 | When(new CancelOrder(id));
34 | Then(new OrderCancelled(id));
35 | }
36 |
37 | [Test]
38 | public void WhenTryingToStartShippingACancelledOrder_IShouldBeNotified()
39 | {
40 | var id = Guid.NewGuid();
41 | var orderCreated = BuildOrderCreated(id, basketId: Guid.NewGuid(), numberOfOrderLines: 1);
42 | Given(orderCreated,
43 | new OrderCancelled(id));
44 | WhenThrows(new StartShippingProcess(id));
45 | }
46 |
47 | [Test]
48 | public void WhenTryingToCancelAnOrderThatIsAboutToShip_IShouldBeNotified()
49 | {
50 | var id = Guid.NewGuid();
51 | var orderCreated = BuildOrderCreated(id, basketId: Guid.NewGuid(), numberOfOrderLines: 1);
52 | Given(orderCreated,
53 | new ShippingProcessStarted(id));
54 | WhenThrows(new CancelOrder(id));
55 | }
56 |
57 | [Test]
58 | public void WhenShippingAnOrderThatTheShippingProcessIsStarted_ItShouldBeMarkedAsShipped()
59 | {
60 | var id = Guid.NewGuid();
61 | var orderCreated = BuildOrderCreated(id, basketId: Guid.NewGuid(), numberOfOrderLines: 1);
62 | Given(orderCreated,
63 | new ShippingProcessStarted(id));
64 | When(new ShipOrder(id));
65 | Then(new OrderShipped(id));
66 | }
67 |
68 | [Test]
69 | public void WhenShippingAnOrderWhereShippingIsNotStarted_IShouldGetNotified()
70 | {
71 | var id = Guid.NewGuid();
72 | var orderCreated = BuildOrderCreated(id, basketId: Guid.NewGuid(), numberOfOrderLines: 1);
73 | Given(orderCreated);
74 | WhenThrows(new ShipOrder(id));
75 | }
76 |
77 | [Test]
78 | public void WhenTheUserCheckoutWithAnAmountLargerThan100000_TheOrderNeedsApproval()
79 | {
80 | var address = new Address("Valid street");
81 | var basketId = Guid.NewGuid();
82 | var orderId = Guid.NewGuid();
83 | IdGenerator.GenerateGuid = () => orderId;
84 | var orderLine = new OrderLine(Guid.NewGuid(), "Ball", 100000, 100001, 1);
85 | Given(new BasketCreated(basketId, Guid.NewGuid(), 0),
86 | new ItemAdded(basketId, orderLine),
87 | new BasketCheckedOut(basketId, address));
88 | When(new MakePayment(basketId, 100001));
89 | Then(new OrderCreated(orderId, basketId, Helpers.ToFSharpList(new [] {orderLine})),
90 | new NeedsApproval(orderId));
91 | }
92 |
93 | [Test]
94 | public void WhenTheUserCheckoutWithAnAmountLessThan100000_TheOrderIsAutomaticallyApproved()
95 | {
96 | var address = new Address("Valid street");
97 | var basketId = Guid.NewGuid();
98 | var orderId = Guid.NewGuid();
99 | IdGenerator.GenerateGuid = () => orderId;
100 | var orderLine = new OrderLine(Guid.NewGuid(), "Ball", 100000, 100000, 1);
101 | Given(new BasketCreated(basketId, Guid.NewGuid(), 0),
102 | new ItemAdded(basketId, orderLine),
103 | new BasketCheckedOut(basketId, address));
104 | When(new MakePayment(basketId, 100000));
105 | Then(new OrderCreated(orderId, basketId, Helpers.ToFSharpList(new[] { orderLine })),
106 | new OrderApproved(orderId));
107 | }
108 |
109 | [Test]
110 | public void WhenApprovingAnOrder_ItShouldBeApproved()
111 | {
112 | var orderId = Guid.NewGuid();
113 | Given(new OrderCreated(orderId, Guid.NewGuid(), FSharpList.Empty));
114 | When(new ApproveOrder(orderId));
115 | Then(new OrderApproved(orderId));
116 | }
117 |
118 | private OrderCreated BuildOrderCreated(Guid orderId, Guid basketId, int numberOfOrderLines, int pricePerProduct = 100)
119 | {
120 | var orderLines = FSharpList.Empty;
121 | for (var i = 0; i < numberOfOrderLines; i++)
122 | {
123 | orderLines = FSharpList.Cons(new OrderLine(Guid.NewGuid(), "Line " + i, pricePerProduct, pricePerProduct, 1), orderLines);
124 | }
125 | return new OrderCreated(orderId, basketId, orderLines);
126 | }
127 | }
128 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Domain.Tests/ProductTests/CreateProductTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CQRSShop.Contracts.Commands;
3 | using CQRSShop.Contracts.Events;
4 | using CQRSShop.Domain.Exceptions;
5 | using CQRSShop.Tests;
6 | using NUnit.Framework;
7 |
8 | namespace CQRSShop.Domain.Tests.ProductTests
9 | {
10 | [TestFixture]
11 | public class CreateProductTests : TestBase
12 | {
13 | [TestCase("ball", 1000)]
14 | [TestCase("train", 10000)]
15 | [TestCase("universe", 999999)]
16 | public void WhenCreatingAProduct_TheProductShouldBeCreatedWithTheCorrectPrice(string productName, int price)
17 | {
18 | Guid id = Guid.NewGuid();
19 | When(new CreateProduct(id, productName, price));
20 | Then(new ProductCreated(id, productName, price));
21 | }
22 |
23 | [Test]
24 | public void GivenProductXExists_WhenCreatingAProductWithIdX_IShouldGetNotifiedThatTheProductAlreadyExists()
25 | {
26 | Guid id = Guid.NewGuid();
27 | Given(new ProductCreated(id, "Something I don't care about", 9999));
28 | WhenThrows(new CreateProduct(id, "Sugar", 999));
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Domain.Tests/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("CQRSShop.Tests")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("CQRSShop.Tests")]
13 | [assembly: AssemblyCopyright("Copyright © 2014")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("10e8c961-d47d-4ed2-87eb-d6b49ff24faa")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/src/CQRSShop.Domain.Tests/TestBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using CQRSShop.Domain;
5 | using CQRSShop.Infrastructure;
6 | using NUnit.Framework;
7 |
8 | namespace CQRSShop.Tests
9 | {
10 | public class TestBase
11 | {
12 | private InMemoryDomainRespository _domainRepository;
13 | private DomainEntry _domainEntry;
14 | private Dictionary> _preConditions = new Dictionary>();
15 |
16 | private DomainEntry BuildApplication()
17 | {
18 | _domainRepository = new InMemoryDomainRespository();
19 | _domainRepository.AddEvents(_preConditions);
20 | return new DomainEntry(_domainRepository);
21 | }
22 |
23 | [TestFixtureTearDown]
24 | public void TearDown()
25 | {
26 | IdGenerator.GenerateGuid = null;
27 | _preConditions = new Dictionary>();
28 | }
29 |
30 | protected void When(ICommand command)
31 | {
32 | var application = BuildApplication();
33 | application.ExecuteCommand(command);
34 | }
35 |
36 | protected void Then(params IEvent[] expectedEvents)
37 | {
38 | var latestEvents = _domainRepository.GetLatestEvents().ToList();
39 | var expectedEventsList = expectedEvents.ToList();
40 | Assert.AreEqual(expectedEventsList.Count, latestEvents.Count);
41 |
42 | for (int i = 0; i < latestEvents.Count; i++)
43 | {
44 | Assert.AreEqual(expectedEvents[i], latestEvents[i]);
45 | }
46 | }
47 |
48 | protected void WhenThrows(ICommand command) where TException : Exception
49 | {
50 | try
51 | {
52 | When(command);
53 | Assert.Fail("Expected exception " + typeof(TException));
54 | }
55 | catch (TException)
56 | {
57 | }
58 | }
59 |
60 | protected void Given(params IEvent[] existingEvents)
61 | {
62 | _preConditions = existingEvents
63 | .GroupBy(y => y.Id)
64 | .ToDictionary(y => y.Key, y => y.AsEnumerable());
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/CQRSShop.Domain.Tests/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/CQRSShop.Domain/Aggregates/Basket.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using CQRSShop.Contracts.Events;
5 | using CQRSShop.Contracts.Types;
6 | using CQRSShop.Domain.Exceptions;
7 | using CQRSShop.Infrastructure;
8 | using Microsoft.FSharp.Collections;
9 |
10 | namespace CQRSShop.Domain.Aggregates
11 | {
12 | internal class Basket : AggregateBase
13 | {
14 | private int _discount;
15 | private FSharpList _orderLines;
16 |
17 | private Basket(Guid id, Guid customerId, int discount) : this()
18 | {
19 | RaiseEvent(new BasketCreated(id, customerId, discount));
20 | }
21 |
22 | public Basket()
23 | {
24 | RegisterTransition(Apply);
25 | RegisterTransition(Apply);
26 | }
27 |
28 | private void Apply(ItemAdded obj)
29 | {
30 | _orderLines = FSharpList.Cons(obj.OrderLine, _orderLines);
31 | }
32 |
33 | private void Apply(BasketCreated obj)
34 | {
35 | Id = obj.Id;
36 | _discount = obj.Discount;
37 | _orderLines = FSharpList.Empty;
38 | }
39 |
40 | internal static IAggregate Create(Guid id, Customer customer)
41 | {
42 | return new Basket(id, customer.Id, customer.Discount);
43 | }
44 |
45 | internal void AddItem(Product product, int quantity)
46 | {
47 | var discount = (int)(product.Price * ((double)_discount/100));
48 | var discountedPrice = product.Price - discount;
49 | var orderLine = new OrderLine(product.Id, product.Name, product.Price, discountedPrice, quantity);
50 | RaiseEvent(new ItemAdded(Id, orderLine));
51 | }
52 |
53 | internal void ProceedToCheckout()
54 | {
55 | RaiseEvent(new CustomerIsCheckingOutBasket(Id));
56 | }
57 |
58 | internal void Checkout(Address shippingAddress)
59 | {
60 | if(shippingAddress == null || string.IsNullOrWhiteSpace(shippingAddress.Street))
61 | throw new MissingAddressException();
62 | RaiseEvent(new BasketCheckedOut(Id, shippingAddress));
63 | }
64 |
65 | internal IAggregate MakePayment(int payment)
66 | {
67 | var expectedPayment = _orderLines.Sum(y => y.DiscountedPrice * y.Quantity);
68 | if(expectedPayment != payment)
69 | throw new UnexpectedPaymentException();
70 | return new Order(Id, _orderLines);
71 | }
72 | }
73 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Domain/Aggregates/Customer.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CQRSShop.Contracts.Events;
3 | using CQRSShop.Infrastructure;
4 |
5 | namespace CQRSShop.Domain.Aggregates
6 | {
7 | internal class Customer : AggregateBase
8 | {
9 | public Customer()
10 | {
11 | RegisterTransition(Apply);
12 | RegisterTransition(Apply);
13 | }
14 |
15 | private Customer(Guid id, string name) : this()
16 | {
17 | RaiseEvent(new CustomerCreated(id, name));
18 | }
19 |
20 | internal int Discount { get; set; }
21 |
22 | private void Apply(CustomerCreated obj)
23 | {
24 | Id = obj.Id;
25 | }
26 |
27 | private void Apply(CustomerMarkedAsPreferred obj)
28 | {
29 | Discount = obj.Discount;
30 | }
31 |
32 | internal static IAggregate Create(Guid id, string name)
33 | {
34 | return new Customer(id, name);
35 | }
36 |
37 | internal void MakePreferred(int discount)
38 | {
39 | RaiseEvent(new CustomerMarkedAsPreferred(Id, discount));
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Domain/Aggregates/Order.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using CQRSShop.Contracts.Events;
4 | using CQRSShop.Contracts.Types;
5 | using CQRSShop.Domain.Exceptions;
6 | using CQRSShop.Infrastructure;
7 | using Microsoft.FSharp.Collections;
8 |
9 | namespace CQRSShop.Domain.Aggregates
10 | {
11 | internal class Order : AggregateBase
12 | {
13 | private OrderState _orderState;
14 |
15 | private enum OrderState
16 | {
17 | ShippingProcessStarted,
18 | Created,
19 | Cancelled
20 | }
21 |
22 | public Order()
23 | {
24 | RegisterTransition(Apply);
25 | RegisterTransition(Apply);
26 | RegisterTransition(Apply);
27 | }
28 |
29 | private void Apply(OrderCancelled obj)
30 | {
31 | _orderState = OrderState.Cancelled;
32 | }
33 |
34 | private void Apply(ShippingProcessStarted obj)
35 | {
36 | _orderState = OrderState.ShippingProcessStarted;
37 | }
38 |
39 | private void Apply(OrderCreated obj)
40 | {
41 | _orderState = OrderState.Created;
42 | Id = obj.Id;
43 | }
44 |
45 | internal Order(Guid basketId, FSharpList orderLines) : this()
46 | {
47 | var id = IdGenerator.GenerateGuid();
48 | RaiseEvent(new OrderCreated(id, basketId, orderLines));
49 | var totalPrice = orderLines.Sum(y => y.DiscountedPrice);
50 | if (totalPrice > 100000)
51 | {
52 | RaiseEvent(new NeedsApproval(id));
53 | }
54 | else
55 | {
56 | RaiseEvent(new OrderApproved(id));
57 | }
58 | }
59 |
60 | internal void Approve()
61 | {
62 | RaiseEvent(new OrderApproved(Id));
63 | }
64 |
65 | internal void StartShippingProcess()
66 | {
67 | if (_orderState == OrderState.Cancelled)
68 | throw new OrderCancelledException();
69 |
70 | RaiseEvent(new ShippingProcessStarted(Id));
71 | }
72 |
73 | internal void Cancel()
74 | {
75 | if (_orderState == OrderState.Created)
76 | {
77 | RaiseEvent(new OrderCancelled(Id));
78 | }
79 | else
80 | {
81 | throw new ShippingStartedException();
82 | }
83 | }
84 |
85 | internal void ShipOrder()
86 | {
87 | if (_orderState != OrderState.ShippingProcessStarted)
88 | throw new InvalidOrderState();
89 | RaiseEvent(new OrderShipped(Id));
90 | }
91 | }
92 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Domain/Aggregates/Product.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CQRSShop.Contracts.Events;
3 | using CQRSShop.Infrastructure;
4 |
5 | namespace CQRSShop.Domain.Aggregates
6 | {
7 | internal class Product : AggregateBase
8 | {
9 | public Product()
10 | {
11 | RegisterTransition(Apply);
12 | }
13 |
14 | internal string Name { get; private set; }
15 | internal int Price { get; private set; }
16 |
17 | private void Apply(ProductCreated obj)
18 | {
19 | Id = obj.Id;
20 | Name = obj.Name;
21 | Price = obj.Price;
22 | }
23 |
24 | private Product(Guid id, string name, int price) : this()
25 | {
26 | RaiseEvent(new ProductCreated(id, name, price));
27 | }
28 |
29 | internal static IAggregate Create(Guid id, string name, int price)
30 | {
31 | return new Product(id, name, price);
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Domain/CQRSShop.Domain.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {00AFE5DA-93EF-4C5A-BE98-006ECF0E42E6}
8 | Library
9 | Properties
10 | CQRSShop.Domain
11 | CQRSShop.Domain
12 | v4.5.1
13 | 512
14 |
15 |
16 | true
17 | full
18 | false
19 | bin\Debug\
20 | DEBUG;TRACE
21 | prompt
22 | 4
23 |
24 |
25 | pdbonly
26 | true
27 | bin\Release\
28 | TRACE
29 | prompt
30 | 4
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | {66f74977-c96f-4af0-9f98-d2ca94749649}
59 | CQRSShop.Contracts
60 |
61 |
62 | {57ae018a-ba49-471d-97d7-c4ad2040d4b0}
63 | CQRSShop.Infrastructure
64 |
65 |
66 |
67 |
74 |
--------------------------------------------------------------------------------
/src/CQRSShop.Domain/CommandHandlers/BasketCommandHandler.cs:
--------------------------------------------------------------------------------
1 | using CQRSShop.Contracts.Commands;
2 | using CQRSShop.Domain.Aggregates;
3 | using CQRSShop.Domain.Exceptions;
4 | using CQRSShop.Infrastructure;
5 | using CQRSShop.Infrastructure.Exceptions;
6 |
7 | namespace CQRSShop.Domain.CommandHandlers
8 | {
9 | internal class BasketCommandHandler :
10 | IHandle,
11 | IHandle,
12 | IHandle,
13 | IHandle,
14 | IHandle
15 | {
16 | private readonly IDomainRepository _domainRepository;
17 |
18 | public BasketCommandHandler(IDomainRepository domainRepository)
19 | {
20 | _domainRepository = domainRepository;
21 | }
22 |
23 | public IAggregate Handle(CreateBasket command)
24 | {
25 | try
26 | {
27 | var basket = _domainRepository.GetById(command.Id);
28 | throw new BasketAlreadExistsException(command.Id);
29 | }
30 | catch (AggregateNotFoundException)
31 | {
32 | //Expect this
33 | }
34 | var customer = _domainRepository.GetById(command.CustomerId);
35 | return Basket.Create(command.Id, customer);
36 | }
37 |
38 | public IAggregate Handle(AddItemToBasket command)
39 | {
40 | var basket = _domainRepository.GetById(command.Id);
41 | var product = _domainRepository.GetById(command.ProductId);
42 | basket.AddItem(product, command.Quantity);
43 | return basket;
44 | }
45 |
46 | public IAggregate Handle(ProceedToCheckout command)
47 | {
48 | var basket = _domainRepository.GetById(command.Id);
49 | basket.ProceedToCheckout();
50 | return basket;
51 | }
52 |
53 | public IAggregate Handle(CheckoutBasket command)
54 | {
55 | var basket = _domainRepository.GetById(command.Id);
56 | basket.Checkout(command.ShippingAddress);
57 | return basket;
58 | }
59 |
60 | public IAggregate Handle(MakePayment command)
61 | {
62 | var basket = _domainRepository.GetById(command.Id);
63 | var order = basket.MakePayment(command.Payment);
64 | return order;
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Domain/CommandHandlers/CustomerCommandHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CQRSShop.Contracts.Commands;
3 | using CQRSShop.Domain.Aggregates;
4 | using CQRSShop.Domain.Exceptions;
5 | using CQRSShop.Infrastructure;
6 | using CQRSShop.Infrastructure.Exceptions;
7 |
8 | namespace CQRSShop.Domain.CommandHandlers
9 | {
10 | internal class CustomerCommandHandler :
11 | IHandle,
12 | IHandle
13 | {
14 | private readonly IDomainRepository _domainRepository;
15 |
16 | public CustomerCommandHandler(IDomainRepository domainRepository)
17 | {
18 | _domainRepository = domainRepository;
19 | }
20 |
21 | public IAggregate Handle(CreateCustomer command)
22 | {
23 | try
24 | {
25 | var customer = _domainRepository.GetById(command.Id);
26 | throw new CustomerAlreadyExistsException(command.Id);
27 | }
28 | catch (AggregateNotFoundException)
29 | {
30 | // We expect not to find anything
31 | }
32 | return Customer.Create(command.Id, command.Name);
33 | }
34 |
35 | public IAggregate Handle(MarkCustomerAsPreferred command)
36 | {
37 | var customer = _domainRepository.GetById(command.Id);
38 | customer.MakePreferred(command.Discount);
39 | return customer;
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Domain/CommandHandlers/OrderHandler.cs:
--------------------------------------------------------------------------------
1 | using CQRSShop.Contracts.Commands;
2 | using CQRSShop.Domain.Aggregates;
3 | using CQRSShop.Infrastructure;
4 |
5 | namespace CQRSShop.Domain.CommandHandlers
6 | {
7 | internal class OrderHandler :
8 | IHandle,
9 | IHandle,
10 | IHandle,
11 | IHandle
12 | {
13 | private readonly IDomainRepository _domainRepository;
14 |
15 | public OrderHandler(IDomainRepository domainRepository)
16 | {
17 | _domainRepository = domainRepository;
18 | }
19 |
20 | public IAggregate Handle(ApproveOrder command)
21 | {
22 | var order = _domainRepository.GetById(command.Id);
23 | order.Approve();
24 | return order;
25 | }
26 |
27 | public IAggregate Handle(StartShippingProcess command)
28 | {
29 | var order = _domainRepository.GetById(command.Id);
30 | order.StartShippingProcess();
31 | return order;
32 | }
33 |
34 | public IAggregate Handle(CancelOrder command)
35 | {
36 | var order = _domainRepository.GetById(command.Id);
37 | order.Cancel();
38 | return order;
39 | }
40 |
41 | public IAggregate Handle(ShipOrder command)
42 | {
43 | var order = _domainRepository.GetById(command.Id);
44 | order.ShipOrder();
45 | return order;
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Domain/CommandHandlers/ProductCommandHandler.cs:
--------------------------------------------------------------------------------
1 | using CQRSShop.Contracts.Commands;
2 | using CQRSShop.Domain.Aggregates;
3 | using CQRSShop.Domain.Exceptions;
4 | using CQRSShop.Infrastructure;
5 | using CQRSShop.Infrastructure.Exceptions;
6 |
7 | namespace CQRSShop.Domain.CommandHandlers
8 | {
9 | internal class ProductCommandHandler :
10 | IHandle
11 | {
12 | private readonly IDomainRepository _domainRepository;
13 |
14 | public ProductCommandHandler(IDomainRepository domainRepository)
15 | {
16 | _domainRepository = domainRepository;
17 | }
18 |
19 | public IAggregate Handle(CreateProduct command)
20 | {
21 | try
22 | {
23 | var product = _domainRepository.GetById(command.Id);
24 | throw new ProductAlreadyExistsException(command.Id);
25 | }
26 | catch (AggregateNotFoundException)
27 | {
28 | // We expect not to find anything
29 | }
30 | return Product.Create(command.Id, command.Name, command.Price);
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Domain/DomainEntry.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using CQRSShop.Contracts.Commands;
5 | using CQRSShop.Domain.CommandHandlers;
6 | using CQRSShop.Infrastructure;
7 |
8 | namespace CQRSShop.Domain
9 | {
10 | public class DomainEntry
11 | {
12 | private readonly CommandDispatcher _commandDispatcher;
13 |
14 | public DomainEntry(IDomainRepository domainRepository, IEnumerable> preExecutionPipe = null, IEnumerable> postExecutionPipe = null)
15 | {
16 | preExecutionPipe = preExecutionPipe ?? Enumerable.Empty>();
17 | postExecutionPipe = CreatePostExecutionPipe(postExecutionPipe);
18 | _commandDispatcher = CreateCommandDispatcher(domainRepository, preExecutionPipe, postExecutionPipe);
19 | }
20 |
21 | public void ExecuteCommand(TCommand command) where TCommand : ICommand
22 | {
23 | _commandDispatcher.ExecuteCommand(command);
24 | }
25 |
26 | private CommandDispatcher CreateCommandDispatcher(IDomainRepository domainRepository, IEnumerable> preExecutionPipe, IEnumerable> postExecutionPipe)
27 | {
28 | var commandDispatcher = new CommandDispatcher(domainRepository, preExecutionPipe, postExecutionPipe);
29 |
30 | var customerCommandHandler = new CustomerCommandHandler(domainRepository);
31 | commandDispatcher.RegisterHandler(customerCommandHandler);
32 | commandDispatcher.RegisterHandler(customerCommandHandler);
33 |
34 | var productCommandHandler = new ProductCommandHandler(domainRepository);
35 | commandDispatcher.RegisterHandler(productCommandHandler);
36 |
37 | var basketCommandHandler = new BasketCommandHandler(domainRepository);
38 | commandDispatcher.RegisterHandler(basketCommandHandler);
39 | commandDispatcher.RegisterHandler(basketCommandHandler);
40 | commandDispatcher.RegisterHandler(basketCommandHandler);
41 | commandDispatcher.RegisterHandler(basketCommandHandler);
42 | commandDispatcher.RegisterHandler(basketCommandHandler);
43 |
44 | var orderCommandHanler = new OrderHandler(domainRepository);
45 | commandDispatcher.RegisterHandler(orderCommandHanler);
46 | commandDispatcher.RegisterHandler(orderCommandHanler);
47 | commandDispatcher.RegisterHandler(orderCommandHanler);
48 | commandDispatcher.RegisterHandler(orderCommandHanler);
49 |
50 | return commandDispatcher;
51 | }
52 |
53 | private IEnumerable> CreatePostExecutionPipe(IEnumerable> postExecutionPipe)
54 | {
55 | if (postExecutionPipe != null)
56 | {
57 | foreach (var action in postExecutionPipe)
58 | {
59 | yield return action;
60 | }
61 | }
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/CQRSShop.Domain/Exceptions/CustomerAlreadyExistsException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace CQRSShop.Domain.Exceptions
4 | {
5 | public class CustomerAlreadyExistsException : DuplicateAggregateException
6 | {
7 | public CustomerAlreadyExistsException(Guid id) : base(id)
8 | {
9 |
10 | }
11 | }
12 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Domain/Exceptions/DuplicateAggregateException.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace CQRSShop.Domain.Exceptions
4 | {
5 | public abstract class DuplicateAggregateException : DomainException
6 | {
7 | protected DuplicateAggregateException(Guid id) : base(CreateMessage(id))
8 | {
9 |
10 | }
11 |
12 | private static string CreateMessage(Guid id)
13 | {
14 | return string.Format("Aggregate already exists with id {0}", id);
15 | }
16 | }
17 |
18 | public class ProductAlreadyExistsException : DuplicateAggregateException
19 | {
20 | public ProductAlreadyExistsException(Guid id) : base(id)
21 | {
22 | }
23 | }
24 |
25 | public class MissingAddressException : DomainException
26 | {
27 |
28 | }
29 |
30 | public class UnexpectedPaymentException : DomainException
31 | { }
32 |
33 | public class OrderCancelledException : DomainException
34 | { }
35 |
36 | public class ShippingStartedException : DomainException {}
37 |
38 | public abstract class DomainException : Exception
39 | {
40 | public DomainException()
41 | {
42 | }
43 |
44 | protected DomainException(string createMessage) : base(createMessage)
45 | {
46 | }
47 | }
48 |
49 | public class InvalidOrderState : DomainException { }
50 |
51 | public class BasketAlreadExistsException : DuplicateAggregateException
52 | {
53 | public BasketAlreadExistsException(Guid id) : base(id)
54 | {
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Domain/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("CQRSShop.Domain")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 | [assembly: AssemblyCompany("")]
12 | [assembly: AssemblyProduct("CQRSShop.Domain")]
13 | [assembly: AssemblyCopyright("Copyright © 2014")]
14 | [assembly: AssemblyTrademark("")]
15 | [assembly: AssemblyCulture("")]
16 |
17 | // Setting ComVisible to false makes the types in this assembly not visible
18 | // to COM components. If you need to access a type in this assembly from
19 | // COM, set the ComVisible attribute to true on that type.
20 | [assembly: ComVisible(false)]
21 |
22 | // The following GUID is for the ID of the typelib if this project is exposed to COM
23 | [assembly: Guid("6f266ab2-d0e6-4f09-bbf8-f69bdf108bbd")]
24 |
25 | // Version information for an assembly consists of the following four values:
26 | //
27 | // Major Version
28 | // Minor Version
29 | // Build Number
30 | // Revision
31 | //
32 | // You can specify all the values or you can default the Build and Revision Numbers
33 | // by using the '*' as shown below:
34 | // [assembly: AssemblyVersion("1.0.*")]
35 | [assembly: AssemblyVersion("1.0.0.0")]
36 | [assembly: AssemblyFileVersion("1.0.0.0")]
37 |
--------------------------------------------------------------------------------
/src/CQRSShop.Infrastructure/AggregateBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace CQRSShop.Infrastructure
5 | {
6 | public class AggregateBase : IAggregate
7 | {
8 | public int Version
9 | {
10 | get
11 | {
12 | return _version;
13 | }
14 | protected set
15 | {
16 | _version = value;
17 | }
18 | }
19 |
20 | public Guid Id { get; protected set; }
21 |
22 | private List _uncommitedEvents = new List();
23 | private Dictionary> _routes = new Dictionary>();
24 | private int _version = -1;
25 |
26 | public void RaiseEvent(IEvent @event)
27 | {
28 | ApplyEvent(@event);
29 | _uncommitedEvents.Add(@event);
30 | }
31 |
32 | protected void RegisterTransition(Action transition) where T : class
33 | {
34 | _routes.Add(typeof(T), o => transition(o as T));
35 | }
36 |
37 | public void ApplyEvent(IEvent @event)
38 | {
39 | var eventType = @event.GetType();
40 | if (_routes.ContainsKey(eventType))
41 | {
42 | _routes[eventType](@event);
43 | }
44 | Version++;
45 | }
46 |
47 | public IEnumerable UncommitedEvents()
48 | {
49 | return _uncommitedEvents;
50 | }
51 |
52 | public void ClearUncommitedEvents()
53 | {
54 | _uncommitedEvents.Clear();
55 | }
56 | }
57 | }
--------------------------------------------------------------------------------
/src/CQRSShop.Infrastructure/CQRSShop.Infrastructure.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | {57AE018A-BA49-471D-97D7-C4AD2040D4B0}
8 | Library
9 | Properties
10 | CQRSShop.Infrastructure
11 | CQRSShop.Infrastructure
12 | v4.5.1
13 | 512
14 | ..\
15 | true
16 |
17 |
18 | true
19 | full
20 | false
21 | bin\Debug\
22 | DEBUG;TRACE
23 | prompt
24 | 4
25 |
26 |
27 | pdbonly
28 | true
29 | bin\Release\
30 | TRACE
31 | prompt
32 | 4
33 |
34 |
35 |
36 | ..\packages\EventStore.Client.3.0.0-rc2\lib\net40\EventStore.ClientAPI.dll
37 |
38 |
39 | False
40 | ..\packages\Newtonsoft.Json.6.0.3\lib\net45\Newtonsoft.Json.dll
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
73 |
74 |
75 |
76 |
83 |
--------------------------------------------------------------------------------
/src/CQRSShop.Infrastructure/CommandDispatcher.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 |
5 | namespace CQRSShop.Infrastructure
6 | {
7 | public class CommandDispatcher
8 | {
9 | private Dictionary> _routes;
10 | private IDomainRepository _domainRepository;
11 | private readonly IEnumerable> _postExecutionPipe;
12 | private readonly IEnumerable> _preExecutionPipe;
13 |
14 | public CommandDispatcher(IDomainRepository domainRepository, IEnumerable> preExecutionPipe, IEnumerable> postExecutionPipe)
15 | {
16 | _domainRepository = domainRepository;
17 | _postExecutionPipe = postExecutionPipe;
18 | _preExecutionPipe = preExecutionPipe ?? Enumerable.Empty>();
19 | _routes = new Dictionary>();
20 | }
21 |
22 | public void RegisterHandler(IHandle handler) where TCommand : class, ICommand
23 | {
24 | _routes.Add(typeof (TCommand), command => handler.Handle(command as TCommand));
25 | }
26 |
27 | public void ExecuteCommand(TCommand command) where TCommand : ICommand
28 | {
29 | var commandType = command.GetType();
30 |
31 | RunPreExecutionPipe(command);
32 | if (!_routes.ContainsKey(commandType))
33 | {
34 | throw new ApplicationException("Missing handler for " + commandType.Name);
35 | }
36 | var aggregate = _routes[commandType](command);
37 | var savedEvents = _domainRepository.Save(aggregate);
38 | RunPostExecutionPipe(savedEvents);
39 | }
40 |
41 | private void RunPostExecutionPipe(IEnumerable