├── EventSourcingCQRS ├── wwwroot │ ├── js │ │ ├── site.min.js │ │ └── site.js │ ├── favicon.ico │ ├── lib │ │ ├── bootstrap │ │ │ ├── dist │ │ │ │ ├── fonts │ │ │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ │ │ └── glyphicons-halflings-regular.woff2 │ │ │ │ └── js │ │ │ │ │ └── npm.js │ │ │ ├── .bower.json │ │ │ └── LICENSE │ │ ├── jquery │ │ │ ├── .bower.json │ │ │ └── LICENSE.txt │ │ ├── jquery-validation │ │ │ ├── .bower.json │ │ │ ├── LICENSE.md │ │ │ └── dist │ │ │ │ ├── additional-methods.min.js │ │ │ │ └── jquery.validate.min.js │ │ └── jquery-validation-unobtrusive │ │ │ ├── .bower.json │ │ │ └── jquery.validate.unobtrusive.min.js │ ├── css │ │ ├── site.min.css │ │ └── site.css │ └── images │ │ ├── banner2.svg │ │ ├── banner1.svg │ │ ├── banner3.svg │ │ └── banner4.svg ├── .bowerrc ├── Views │ ├── _ViewStart.cshtml │ ├── _ViewImports.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ ├── _ValidationScriptsPartial.cshtml │ │ └── _Layout.cshtml │ └── Carts │ │ ├── IndexAsync.cshtml │ │ └── DetailsAsync.cshtml ├── .dockerignore ├── appsettings.json ├── Dockerfile ├── appsettings.Development.json ├── bower.json ├── Program.cs ├── bundleconfig.json ├── Models │ ├── CartIndexViewModel.cs │ └── CartDetailsViewModel.cs ├── EventSourcingCQRS.csproj ├── Controllers │ └── CartsController.cs └── Startup.cs ├── EventSourcingCQRS.Domain ├── AssemblyInfo.cs ├── Core │ ├── IAggregate.cs │ ├── IAggregateId.cs │ ├── IDomainEvent.cs │ ├── DomainEventBase.cs │ └── AggregateBase.cs ├── EventSourcingCQRS.Domain.csproj ├── PubSub │ ├── ITransientDomainEventPublisher.cs │ └── ITransientDomainEventSubscriber.cs ├── Persistence │ ├── EventStore │ │ ├── AppendResult.cs │ │ ├── Event.cs │ │ ├── IEventStore.cs │ │ └── EventStoreException.cs │ ├── IEventSourcingAggregate.cs │ ├── IRepository.cs │ └── EventSourcingRepository.cs ├── CartModule │ ├── CartException.cs │ ├── CartCreatedEvent.cs │ ├── ProductAddedEvent.cs │ ├── CartId.cs │ ├── CartItem.cs │ ├── ProductQuantityChangedEvent.cs │ └── Cart.cs ├── CustomerModule │ └── CustomerId.cs └── ProductModule │ └── ProductId.cs ├── EventSourcingCQRS.ReadModel ├── Common │ └── IReadEntity.cs ├── Product │ └── Product.cs ├── Customer │ └── Customer.cs ├── Persistence │ ├── IRepository.cs │ ├── IReadOnlyRepository.cs │ ├── RepositoryException.cs │ └── MongoDBRepository.cs ├── Cart │ ├── Cart.cs │ └── CartItem.cs └── EventSourcingCQRS.ReadModel.csproj ├── docker-compose.override.yml ├── EventSourcingCQRS.Domain.Tests ├── Utility │ ├── TestAggregateId.cs │ ├── TestDomainEvent.cs │ ├── TestAggregate.cs │ └── GenericAggregateBaseTest.cs ├── EventSourcingCQRS.Domain.Tests.csproj ├── EventSourcingRepositoryTest.cs └── CartTest.cs ├── docker-compose.ci.build.yml ├── EventSourcingCQRS.Application ├── Services │ ├── IDomainEventHandler.cs │ ├── ICartWriter.cs │ ├── ICartReader.cs │ ├── CartReader.cs │ └── CartWriter.cs ├── EventSourcingCQRS.Application.csproj ├── PubSub │ └── TransientDomainEventPubSub.cs └── Handlers │ └── CartUpdater.cs ├── EventSourcingCQRS.ReadModel.Tests ├── TestEntity.cs ├── EventSourcingCQRS.ReadModel.Tests.csproj ├── MongoDBTestBase.cs ├── MongoDBRepositoryTest.cs └── MongoDBTest.cs ├── docker-compose.yml ├── EventSourcingCQRS.Application.Tests ├── EventSourcingCQRS.Application.Tests.csproj └── TransientDomainEventPublisherTest.cs ├── EventSourcingCQRS.Domain.EventStore ├── EventSourcingCQRS.Domain.EventStore.csproj ├── PrivateSetterContractResolver.cs └── EventStoreEventStore.cs ├── EventSourcingCQRS.Domain.EventStore.Tests ├── EventSourcingCQRS.Domain.EventStore.Tests.csproj ├── JsonConvertTest.cs └── EventStoreTest.cs ├── docker-compose.dcproj ├── LICENSE ├── EventSourcingCQRS.sln └── .gitignore /EventSourcingCQRS/wwwroot/js/site.min.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /EventSourcingCQRS/.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "wwwroot/lib" 3 | } 4 | -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/js/site.js: -------------------------------------------------------------------------------- 1 | // Write your JavaScript code. 2 | -------------------------------------------------------------------------------- /EventSourcingCQRS/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /EventSourcingCQRS/.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !obj/Docker/publish/* 3 | !obj/Docker/empty/ 4 | -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VenomAV/EventSourcingCQRS/HEAD/EventSourcingCQRS/wwwroot/favicon.ico -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | [assembly: InternalsVisibleTo("EventSourcingCQRS.Domain.Tests")] -------------------------------------------------------------------------------- /EventSourcingCQRS/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using EventSourcingCQRS 2 | @using EventSourcingCQRS.Models 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /EventSourcingCQRS/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/Core/IAggregate.cs: -------------------------------------------------------------------------------- 1 | namespace EventSourcingCQRS.Domain.Core 2 | { 3 | public interface IAggregate 4 | { 5 | TId Id { get; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/Core/IAggregateId.cs: -------------------------------------------------------------------------------- 1 | namespace EventSourcingCQRS.Domain.Core 2 | { 3 | public interface IAggregateId 4 | { 5 | string IdAsString(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /EventSourcingCQRS.ReadModel/Common/IReadEntity.cs: -------------------------------------------------------------------------------- 1 | namespace EventSourcingCQRS.ReadModel.Common 2 | { 3 | public interface IReadEntity 4 | { 5 | string Id { get; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /EventSourcingCQRS/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM microsoft/aspnetcore:2.0 2 | ARG source 3 | WORKDIR /app 4 | EXPOSE 80 5 | COPY ${source:-obj/Docker/publish} . 6 | ENTRYPOINT ["dotnet", "EventSourcingCQRS.dll"] 7 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/EventSourcingCQRS.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VenomAV/EventSourcingCQRS/HEAD/EventSourcingCQRS/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VenomAV/EventSourcingCQRS/HEAD/EventSourcingCQRS/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VenomAV/EventSourcingCQRS/HEAD/EventSourcingCQRS/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VenomAV/EventSourcingCQRS/HEAD/EventSourcingCQRS/wwwroot/lib/bootstrap/dist/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /EventSourcingCQRS/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "IncludeScopes": false, 4 | "LogLevel": { 5 | "Default": "Debug", 6 | "System": "Information", 7 | "Microsoft": "Information" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /EventSourcingCQRS/bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asp.net", 3 | "private": true, 4 | "dependencies": { 5 | "bootstrap": "3.3.7", 6 | "jquery": "2.2.0", 7 | "jquery-validation": "1.14.0", 8 | "jquery-validation-unobtrusive": "3.2.6" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/PubSub/ITransientDomainEventPublisher.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace EventSourcingCQRS.Domain.PubSub 4 | { 5 | public interface ITransientDomainEventPublisher 6 | { 7 | Task PublishAsync(T publishedEvent); 8 | } 9 | } -------------------------------------------------------------------------------- /EventSourcingCQRS.ReadModel/Product/Product.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.ReadModel.Common; 2 | 3 | namespace EventSourcingCQRS.ReadModel.Product 4 | { 5 | public class Product : IReadEntity 6 | { 7 | public string Id { get; set; } 8 | 9 | public string Name { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/css/site.min.css: -------------------------------------------------------------------------------- 1 | body{padding-top:50px;padding-bottom:20px}.body-content{padding-left:15px;padding-right:15px}.carousel-caption p{font-size:20px;line-height:1.4}.carousel-inner .item img[src$=".svg"]{width:100%}#qrCode{margin:15px}@media screen and (max-width:767px){.carousel-caption{display:none}} -------------------------------------------------------------------------------- /EventSourcingCQRS.ReadModel/Customer/Customer.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.ReadModel.Common; 2 | 3 | namespace EventSourcingCQRS.ReadModel.Customer 4 | { 5 | public class Customer : IReadEntity 6 | { 7 | public string Id { get; set; } 8 | 9 | public string Name { get; set; } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docker-compose.override.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | mongo: 5 | ports: 6 | - "27017:27017" 7 | eventstore: 8 | ports: 9 | - "2113:2113" 10 | - "1113:1113" 11 | eventsourcingcqrs: 12 | environment: 13 | - ASPNETCORE_ENVIRONMENT=Development 14 | ports: 15 | - "80" 16 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain.Tests/Utility/TestAggregateId.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.Core; 2 | 3 | namespace EventSourcingCQRS.Domain.Tests.Utility 4 | { 5 | public class TestAggregateId : IAggregateId 6 | { 7 | public string IdAsString() 8 | { 9 | return ""; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docker-compose.ci.build.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | ci-build: 5 | image: microsoft/aspnetcore-build:1.0-1.1 6 | volumes: 7 | - .:/src 8 | working_dir: /src 9 | command: /bin/bash -c "dotnet restore ./EventSourcingCQRS.sln && dotnet publish ./EventSourcingCQRS.sln -c Release -o ./obj/Docker/publish" 10 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/PubSub/ITransientDomainEventSubscriber.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | 4 | namespace EventSourcingCQRS.Domain.PubSub 5 | { 6 | public interface ITransientDomainEventSubscriber 7 | { 8 | void Subscribe(Action handler); 9 | 10 | void Subscribe(Func handler); 11 | } 12 | } -------------------------------------------------------------------------------- /EventSourcingCQRS.Application/Services/IDomainEventHandler.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.Core; 2 | using System.Threading.Tasks; 3 | 4 | namespace EventSourcingCQRS.Application.Services 5 | { 6 | public interface IDomainEventHandler 7 | where TEvent: IDomainEvent 8 | { 9 | Task HandleAsync(TEvent @event); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/Persistence/EventStore/AppendResult.cs: -------------------------------------------------------------------------------- 1 | namespace EventSourcingCQRS.Domain.Persistence.EventStore 2 | { 3 | public class AppendResult 4 | { 5 | public AppendResult(long nextExpectedVersion) 6 | { 7 | NextExpectedVersion = nextExpectedVersion; 8 | } 9 | 10 | public long NextExpectedVersion { get; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EventSourcingCQRS.ReadModel/Persistence/IRepository.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.ReadModel.Common; 2 | using System.Threading.Tasks; 3 | 4 | namespace EventSourcingCQRS.ReadModel.Persistence 5 | { 6 | public interface IRepository : IReadOnlyRepository 7 | where T : IReadEntity 8 | { 9 | Task InsertAsync(T entity); 10 | 11 | Task UpdateAsync(T entity); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /EventSourcingCQRS.ReadModel/Cart/Cart.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.ReadModel.Common; 2 | 3 | namespace EventSourcingCQRS.ReadModel.Cart 4 | { 5 | public class Cart : IReadEntity 6 | { 7 | public string Id { get; set; } 8 | 9 | public int TotalItems { get; set; } 10 | 11 | public string CustomerId { get; set; } 12 | 13 | public string CustomerName { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain.Tests/Utility/TestDomainEvent.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.Core; 2 | 3 | namespace EventSourcingCQRS.Domain.Tests.Utility 4 | { 5 | public class TestDomainEvent : DomainEventBase 6 | { 7 | public override IDomainEvent WithAggregate(TestAggregateId aggregateId, long aggregateVersion) 8 | { 9 | return this; 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /EventSourcingCQRS.Application/Services/ICartWriter.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace EventSourcingCQRS.Application.Services 4 | { 5 | public interface ICartWriter 6 | { 7 | Task CreateAsync(string customerId); 8 | 9 | Task AddProductAsync(string cartId, string productId, int quantity); 10 | 11 | Task ChangeProductQuantityAsync(string cartId, string productId, int quantity); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Application/EventSourcingCQRS.Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /EventSourcingCQRS.ReadModel/EventSourcingCQRS.ReadModel.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /EventSourcingCQRS.ReadModel.Tests/TestEntity.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.ReadModel.Common; 2 | 3 | namespace EventSourcingCQRS.ReadModel.Tests 4 | { 5 | public class TestEntity : IReadEntity 6 | { 7 | private TestEntity() 8 | { 9 | 10 | } 11 | 12 | public TestEntity(string id) 13 | { 14 | Id = id; 15 | } 16 | 17 | public string Id { get; private set; } 18 | 19 | public int Quantity { get; set; } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /EventSourcingCQRS/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore; 2 | using Microsoft.AspNetCore.Hosting; 3 | 4 | namespace EventSourcingCQRS 5 | { 6 | public class Program 7 | { 8 | public static void Main(string[] args) 9 | { 10 | BuildWebHost(args).Run(); 11 | } 12 | 13 | public static IWebHost BuildWebHost(string[] args) => 14 | WebHost.CreateDefaultBuilder(args) 15 | .UseStartup() 16 | .Build(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/Persistence/IEventSourcingAggregate.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.Core; 2 | using System.Collections.Generic; 3 | 4 | namespace EventSourcingCQRS.Domain.Persistence 5 | { 6 | internal interface IEventSourcingAggregate 7 | { 8 | long Version { get; } 9 | void ApplyEvent(IDomainEvent @event, long version); 10 | IEnumerable> GetUncommittedEvents(); 11 | void ClearUncommittedEvents(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /EventSourcingCQRS.ReadModel/Persistence/IReadOnlyRepository.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.ReadModel.Common; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq.Expressions; 5 | using System.Threading.Tasks; 6 | 7 | namespace EventSourcingCQRS.ReadModel.Persistence 8 | { 9 | public interface IReadOnlyRepository 10 | where T : IReadEntity 11 | { 12 | Task> FindAllAsync(Expression> predicate); 13 | 14 | Task GetByIdAsync(string id); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | mongo: 5 | image: mongo 6 | networks: 7 | - eventsourcingcqrsnet 8 | eventstore: 9 | image: eventstore/eventstore 10 | networks: 11 | - eventsourcingcqrsnet 12 | eventsourcingcqrs: 13 | image: eventsourcingcqrs 14 | build: 15 | context: ./EventSourcingCQRS 16 | dockerfile: Dockerfile 17 | depends_on: 18 | - eventstore 19 | networks: 20 | - eventsourcingcqrsnet 21 | 22 | networks: 23 | eventsourcingcqrsnet: 24 | driver: bridge -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/Persistence/EventStore/Event.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.Core; 2 | 3 | namespace EventSourcingCQRS.Domain.Persistence.EventStore 4 | { 5 | public class Event 6 | { 7 | public Event(IDomainEvent domainEvent, long eventNumber) 8 | { 9 | DomainEvent = domainEvent; 10 | EventNumber = eventNumber; 11 | } 12 | 13 | public long EventNumber { get; } 14 | 15 | public IDomainEvent DomainEvent { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Application/Services/ICartReader.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.ReadModel.Cart; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq.Expressions; 5 | using System.Threading.Tasks; 6 | 7 | namespace EventSourcingCQRS.Application.Services 8 | { 9 | public interface ICartReader 10 | { 11 | Task GetByIdAsync(string id); 12 | 13 | Task> FindAllAsync(Expression> predicate); 14 | 15 | Task> GetItemsOfAsync(string cartId); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Application.Tests/EventSourcingCQRS.Application.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain.EventStore/EventSourcingCQRS.Domain.EventStore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/lib/bootstrap/dist/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/Persistence/EventStore/IEventStore.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.Core; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | 5 | namespace EventSourcingCQRS.Domain.Persistence.EventStore 6 | { 7 | public interface IEventStore 8 | { 9 | Task>> ReadEventsAsync(TAggregateId id) 10 | where TAggregateId: IAggregateId; 11 | 12 | Task AppendEventAsync(IDomainEvent @event) 13 | where TAggregateId: IAggregateId; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/CartModule/CartException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EventSourcingCQRS.Domain.CartModule 4 | { 5 | 6 | [Serializable] 7 | public class CartException : Exception 8 | { 9 | public CartException() { } 10 | public CartException(string message) : base(message) { } 11 | public CartException(string message, Exception inner) : base(message, inner) { } 12 | protected CartException( 13 | System.Runtime.Serialization.SerializationInfo info, 14 | System.Runtime.Serialization.StreamingContext context) : base(info, context) { } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /EventSourcingCQRS.ReadModel.Tests/EventSourcingCQRS.ReadModel.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /EventSourcingCQRS.ReadModel/Persistence/RepositoryException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EventSourcingCQRS.ReadModel.Persistence 4 | { 5 | 6 | [Serializable] 7 | public class RepositoryException : Exception 8 | { 9 | public RepositoryException() { } 10 | public RepositoryException(string message) : base(message) { } 11 | public RepositoryException(string message, Exception inner) : base(message, inner) { } 12 | protected RepositoryException( 13 | System.Runtime.Serialization.SerializationInfo info, 14 | System.Runtime.Serialization.StreamingContext context) : base(info, context) { } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/lib/jquery/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery", 3 | "main": "dist/jquery.js", 4 | "license": "MIT", 5 | "ignore": [ 6 | "package.json" 7 | ], 8 | "keywords": [ 9 | "jquery", 10 | "javascript", 11 | "browser", 12 | "library" 13 | ], 14 | "homepage": "https://github.com/jquery/jquery-dist", 15 | "version": "2.2.0", 16 | "_release": "2.2.0", 17 | "_resolution": { 18 | "type": "version", 19 | "tag": "2.2.0", 20 | "commit": "6fc01e29bdad0964f62ef56d01297039cdcadbe5" 21 | }, 22 | "_source": "git://github.com/jquery/jquery-dist.git", 23 | "_target": "2.2.0", 24 | "_originalSource": "jquery" 25 | } -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/Core/IDomainEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EventSourcingCQRS.Domain.Core 4 | { 5 | public interface IDomainEvent 6 | { 7 | /// 8 | /// The event identifier 9 | /// 10 | Guid EventId { get; } 11 | 12 | /// 13 | /// The identifier of the aggregate which has generated the event 14 | /// 15 | TAggregateId AggregateId { get; } 16 | 17 | /// 18 | /// The version of the aggregate when the event has been generated 19 | /// 20 | long AggregateVersion { get; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /EventSourcingCQRS.ReadModel.Tests/MongoDBTestBase.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Driver; 2 | using System; 3 | 4 | namespace EventSourcingCQRS.ReadModel.Tests 5 | { 6 | public class MongoDBTestBase : IDisposable 7 | { 8 | protected IMongoDatabase mongoDB; 9 | protected MongoClient client; 10 | 11 | public MongoDBTestBase(string database = null) 12 | { 13 | database = database ?? GetType().Name; 14 | client = new MongoClient("mongodb://localhost:27017"); 15 | mongoDB = client.GetDatabase(database); 16 | } 17 | 18 | public void Dispose() 19 | { 20 | client.DropDatabase(GetType().Name); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /EventSourcingCQRS/bundleconfig.json: -------------------------------------------------------------------------------- 1 | // Configure bundling and minification for the project. 2 | // More info at https://go.microsoft.com/fwlink/?LinkId=808241 3 | [ 4 | { 5 | "outputFileName": "wwwroot/css/site.min.css", 6 | // An array of relative input file paths. Globbing patterns supported 7 | "inputFiles": [ 8 | "wwwroot/css/site.css" 9 | ] 10 | }, 11 | { 12 | "outputFileName": "wwwroot/js/site.min.js", 13 | "inputFiles": [ 14 | "wwwroot/js/site.js" 15 | ], 16 | // Optionally specify minification options 17 | "minify": { 18 | "enabled": true, 19 | "renameLocals": true 20 | }, 21 | // Optionally generate .map file 22 | "sourceMap": false 23 | } 24 | ] 25 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain.Tests/EventSourcingCQRS.Domain.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain.EventStore.Tests/EventSourcingCQRS.Domain.EventStore.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /docker-compose.dcproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2.0 5 | Linux 6 | a6358068-e812-4129-91af-3f57c5d456a5 7 | True 8 | http://localhost:{ServicePort} 9 | eventsourcingcqrs 10 | 11 | 12 | 13 | 14 | docker-compose.yml 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | /* Wrapping element */ 7 | /* Set some basic padding to keep content from hitting the edges */ 8 | .body-content { 9 | padding-left: 15px; 10 | padding-right: 15px; 11 | } 12 | 13 | /* Carousel */ 14 | .carousel-caption p { 15 | font-size: 20px; 16 | line-height: 1.4; 17 | } 18 | 19 | /* Make .svg files in the carousel display properly in older browsers */ 20 | .carousel-inner .item img[src$=".svg"] { 21 | width: 100%; 22 | } 23 | 24 | /* QR code generator */ 25 | #qrCode { 26 | margin: 15px; 27 | } 28 | 29 | /* Hide/rearrange for smaller screens */ 30 | @media screen and (max-width: 767px) { 31 | /* Hide captions */ 32 | .carousel-caption { 33 | display: none; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain.Tests/Utility/TestAggregate.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.Core; 2 | using System.Collections.Generic; 3 | 4 | namespace EventSourcingCQRS.Domain.Tests.Utility 5 | { 6 | public class TestAggregate : AggregateBase 7 | { 8 | public readonly List> AppliedEvents = new List>(); 9 | 10 | private TestAggregate() 11 | { 12 | 13 | } 14 | 15 | public TestAggregate(params TestDomainEvent[] events) 16 | { 17 | foreach (var @event in events) 18 | { 19 | RaiseEvent(@event); 20 | } 21 | } 22 | 23 | public void Apply(TestDomainEvent @event) 24 | { 25 | AppliedEvents.Add(@event); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /EventSourcingCQRS/Models/CartIndexViewModel.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.ReadModel.Cart; 2 | using EventSourcingCQRS.ReadModel.Customer; 3 | using Microsoft.AspNetCore.Mvc.Rendering; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace EventSourcingCQRS.Models 8 | { 9 | public class CartIndexViewModel 10 | { 11 | public IEnumerable Carts { get; set; } 12 | 13 | public IEnumerable Customers { get; set; } 14 | 15 | public IEnumerable AvailableCustomers 16 | { 17 | get 18 | { 19 | return Customers.Select(x => new SelectListItem 20 | { 21 | Text = x.Name, 22 | Value = x.Id 23 | }) 24 | .ToList(); 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /EventSourcingCQRS/Models/CartDetailsViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using EventSourcingCQRS.ReadModel.Cart; 4 | using EventSourcingCQRS.ReadModel.Product; 5 | using Microsoft.AspNetCore.Mvc.Rendering; 6 | 7 | namespace EventSourcingCQRS.Models 8 | { 9 | public class CartDetailsViewModel 10 | { 11 | public Cart Cart { get; internal set; } 12 | 13 | public IEnumerable CartItems { get; internal set; } 14 | 15 | public IEnumerable Products { get; set; } 16 | 17 | public IEnumerable AvailableProducts 18 | { 19 | get 20 | { 21 | return Products.Select(x => new SelectListItem 22 | { 23 | Text = x.Name, 24 | Value = x.Id 25 | }) 26 | .ToList(); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /EventSourcingCQRS.ReadModel/Cart/CartItem.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.ReadModel.Common; 2 | 3 | namespace EventSourcingCQRS.ReadModel.Cart 4 | { 5 | public class CartItem : IReadEntity 6 | { 7 | public string Id { get; private set; } 8 | 9 | public string CartId { get; set; } 10 | 11 | public string ProductId { get; set; } 12 | 13 | public string ProductName { get; set; } 14 | 15 | public int Quantity { get; set; } 16 | 17 | public static CartItem CreateFor(string cartId, string productId) 18 | { 19 | return new CartItem 20 | { 21 | Id = IdFor(cartId, productId), 22 | CartId = cartId, 23 | ProductId = productId 24 | }; 25 | } 26 | 27 | public static string IdFor(string cartId, string productId) 28 | { 29 | return $"{productId}@{cartId}"; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /EventSourcingCQRS/Views/Shared/Error.cshtml: -------------------------------------------------------------------------------- 1 | @model ErrorViewModel 2 | @{ 3 | ViewData["Title"] = "Error"; 4 | } 5 | 6 |

Error.

7 |

An error occurred while processing your request.

8 | 9 | @if (Model.ShowRequestId) 10 | { 11 |

12 | Request ID: @Model.RequestId 13 |

14 | } 15 | 16 |

Development Mode

17 |

18 | Swapping to Development environment will display more detailed information about the error that occurred. 19 |

20 |

21 | Development environment should not be enabled in deployed applications, as it can result in sensitive information from exceptions being displayed to end users. For local debugging, development environment can be enabled by setting the ASPNETCORE_ENVIRONMENT environment variable to Development, and restarting the application. 22 |

23 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain.EventStore/PrivateSetterContractResolver.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Serialization; 4 | 5 | namespace EventSourcingCQRS.Domain.EventStore 6 | { 7 | public class PrivateSetterContractResolver : DefaultContractResolver 8 | { 9 | protected override JsonProperty CreateProperty( 10 | MemberInfo member, 11 | MemberSerialization memberSerialization) 12 | { 13 | var prop = base.CreateProperty(member, memberSerialization); 14 | 15 | if (!prop.Writable) 16 | { 17 | var property = member as PropertyInfo; 18 | if (property != null) 19 | { 20 | var hasPrivateSetter = property.GetSetMethod(true) != null; 21 | prop.Writable = hasPrivateSetter; 22 | } 23 | } 24 | 25 | return prop; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/Persistence/IRepository.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.Core; 2 | using System.Threading.Tasks; 3 | using System; 4 | 5 | namespace EventSourcingCQRS.Domain.Persistence 6 | { 7 | public interface IRepository 8 | where TAggregate: IAggregate 9 | { 10 | Task GetByIdAsync(TAggregateId id); 11 | 12 | Task SaveAsync(TAggregate aggregate); 13 | } 14 | 15 | 16 | [Serializable] 17 | public class RepositoryException : Exception 18 | { 19 | public RepositoryException() { } 20 | public RepositoryException(string message) : base(message) { } 21 | public RepositoryException(string message, Exception inner) : base(message, inner) { } 22 | protected RepositoryException( 23 | System.Runtime.Serialization.SerializationInfo info, 24 | System.Runtime.Serialization.StreamingContext context) : base(info, context) { } 25 | } 26 | } -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/CartModule/CartCreatedEvent.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.Core; 2 | using EventSourcingCQRS.Domain.CustomerModule; 3 | 4 | namespace EventSourcingCQRS.Domain.CartModule 5 | { 6 | public class CartCreatedEvent : DomainEventBase 7 | { 8 | CartCreatedEvent() 9 | { 10 | } 11 | 12 | internal CartCreatedEvent(CartId aggregateId, CustomerId customerId) : base(aggregateId) 13 | { 14 | CustomerId = customerId; 15 | } 16 | 17 | private CartCreatedEvent(CartId aggregateId, long aggregateVersion, CustomerId customerId) : base(aggregateId, aggregateVersion) 18 | { 19 | CustomerId = customerId; 20 | } 21 | 22 | public CustomerId CustomerId { get; private set; } 23 | 24 | public override IDomainEvent WithAggregate(CartId aggregateId, long aggregateVersion) 25 | { 26 | return new CartCreatedEvent(aggregateId, aggregateVersion, CustomerId); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/lib/jquery-validation/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-validation", 3 | "homepage": "http://jqueryvalidation.org/", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/jzaefferer/jquery-validation.git" 7 | }, 8 | "authors": [ 9 | "Jörn Zaefferer " 10 | ], 11 | "description": "Form validation made easy", 12 | "main": "dist/jquery.validate.js", 13 | "keywords": [ 14 | "forms", 15 | "validation", 16 | "validate" 17 | ], 18 | "license": "MIT", 19 | "ignore": [ 20 | "**/.*", 21 | "node_modules", 22 | "bower_components", 23 | "test", 24 | "demo", 25 | "lib" 26 | ], 27 | "dependencies": { 28 | "jquery": ">= 1.7.2" 29 | }, 30 | "version": "1.14.0", 31 | "_release": "1.14.0", 32 | "_resolution": { 33 | "type": "version", 34 | "tag": "1.14.0", 35 | "commit": "c1343fb9823392aa9acbe1c3ffd337b8c92fed48" 36 | }, 37 | "_source": "git://github.com/jzaefferer/jquery-validation.git", 38 | "_target": ">=1.8", 39 | "_originalSource": "jquery-validation" 40 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Andrea Vallotti 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. 22 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/CartModule/ProductAddedEvent.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.Core; 2 | using EventSourcingCQRS.Domain.ProductModule; 3 | 4 | namespace EventSourcingCQRS.Domain.CartModule 5 | { 6 | public class ProductAddedEvent : DomainEventBase 7 | { 8 | ProductAddedEvent() 9 | { 10 | } 11 | 12 | internal ProductAddedEvent(ProductId productId, int quantity) : base() 13 | { 14 | ProductId = productId; 15 | Quantity = quantity; 16 | } 17 | 18 | internal ProductAddedEvent(CartId aggregateId, long aggregateVersion, ProductId productId, int quantity) : base(aggregateId, aggregateVersion) 19 | { 20 | ProductId = productId; 21 | Quantity = quantity; 22 | } 23 | 24 | public ProductId ProductId { get; private set; } 25 | 26 | public int Quantity { get; private set; } 27 | 28 | public override IDomainEvent WithAggregate(CartId aggregateId, long aggregateVersion) 29 | { 30 | return new ProductAddedEvent(aggregateId, aggregateVersion, ProductId, Quantity); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/lib/bootstrap/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bootstrap", 3 | "description": "The most popular front-end framework for developing responsive, mobile first projects on the web.", 4 | "keywords": [ 5 | "css", 6 | "js", 7 | "less", 8 | "mobile-first", 9 | "responsive", 10 | "front-end", 11 | "framework", 12 | "web" 13 | ], 14 | "homepage": "http://getbootstrap.com", 15 | "license": "MIT", 16 | "moduleType": "globals", 17 | "main": [ 18 | "less/bootstrap.less", 19 | "dist/js/bootstrap.js" 20 | ], 21 | "ignore": [ 22 | "/.*", 23 | "_config.yml", 24 | "CNAME", 25 | "composer.json", 26 | "CONTRIBUTING.md", 27 | "docs", 28 | "js/tests", 29 | "test-infra" 30 | ], 31 | "dependencies": { 32 | "jquery": "1.9.1 - 3" 33 | }, 34 | "version": "3.3.7", 35 | "_release": "3.3.7", 36 | "_resolution": { 37 | "type": "version", 38 | "tag": "v3.3.7", 39 | "commit": "0b9c4a4007c44201dce9a6cc1a38407005c26c86" 40 | }, 41 | "_source": "https://github.com/twbs/bootstrap.git", 42 | "_target": "v3.3.7", 43 | "_originalSource": "bootstrap", 44 | "_direct": true 45 | } -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/lib/bootstrap/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2016 Twitter, Inc. 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /EventSourcingCQRS/EventSourcingCQRS.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0 5 | ..\docker-compose.dcproj 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/lib/jquery-validation/LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright Jörn Zaefferer 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /EventSourcingCQRS/Views/Shared/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 18 | 19 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/CartModule/CartId.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.Core; 2 | using System; 3 | 4 | namespace EventSourcingCQRS.Domain.CartModule 5 | { 6 | public class CartId : IAggregateId 7 | { 8 | private const string IdAsStringPrefix = "Cart-"; 9 | 10 | public Guid Id { get; private set; } 11 | 12 | private CartId(Guid id) 13 | { 14 | Id = id; 15 | } 16 | 17 | public CartId(string id) 18 | { 19 | Id = Guid.Parse(id.StartsWith(IdAsStringPrefix) ? id.Substring(IdAsStringPrefix.Length) : id); 20 | } 21 | 22 | public override string ToString() 23 | { 24 | return IdAsString(); 25 | } 26 | 27 | public override bool Equals(object obj) 28 | { 29 | return obj is CartId && Equals(Id, ((CartId)obj).Id); 30 | } 31 | 32 | public override int GetHashCode() 33 | { 34 | return Id.GetHashCode(); 35 | } 36 | 37 | public static CartId NewCartId() 38 | { 39 | return new CartId(Guid.NewGuid()); 40 | } 41 | 42 | public string IdAsString() 43 | { 44 | return $"{IdAsStringPrefix}{Id.ToString()}"; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/CustomerModule/CustomerId.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.Core; 2 | using System; 3 | 4 | namespace EventSourcingCQRS.Domain.CustomerModule 5 | { 6 | public class CustomerId : IAggregateId 7 | { 8 | private const string IdAsStringPrefix = "Customer-"; 9 | 10 | public Guid Id { get; private set; } 11 | 12 | private CustomerId(Guid id) 13 | { 14 | Id = id; 15 | } 16 | 17 | public CustomerId(string id) 18 | { 19 | Id = Guid.Parse(id.StartsWith(IdAsStringPrefix) ? id.Substring(IdAsStringPrefix.Length) : id); 20 | } 21 | 22 | public override string ToString() 23 | { 24 | return IdAsString(); 25 | } 26 | 27 | public override bool Equals(object obj) 28 | { 29 | return obj is CustomerId && Equals(Id, ((CustomerId)obj).Id); 30 | } 31 | 32 | public override int GetHashCode() 33 | { 34 | return Id.GetHashCode(); 35 | } 36 | 37 | public static CustomerId NewCustomerId() 38 | { 39 | return new CustomerId(Guid.NewGuid()); 40 | } 41 | 42 | public string IdAsString() 43 | { 44 | return $"{IdAsStringPrefix}{Id.ToString()}"; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/lib/jquery-validation-unobtrusive/.bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jquery-validation-unobtrusive", 3 | "version": "3.2.6", 4 | "homepage": "https://github.com/aspnet/jquery-validation-unobtrusive", 5 | "description": "Add-on to jQuery Validation to enable unobtrusive validation options in data-* attributes.", 6 | "main": [ 7 | "jquery.validate.unobtrusive.js" 8 | ], 9 | "ignore": [ 10 | "**/.*", 11 | "*.json", 12 | "*.md", 13 | "*.txt", 14 | "gulpfile.js" 15 | ], 16 | "keywords": [ 17 | "jquery", 18 | "asp.net", 19 | "mvc", 20 | "validation", 21 | "unobtrusive" 22 | ], 23 | "authors": [ 24 | "Microsoft" 25 | ], 26 | "license": "http://www.microsoft.com/web/webpi/eula/net_library_eula_enu.htm", 27 | "repository": { 28 | "type": "git", 29 | "url": "git://github.com/aspnet/jquery-validation-unobtrusive.git" 30 | }, 31 | "dependencies": { 32 | "jquery-validation": ">=1.8", 33 | "jquery": ">=1.8" 34 | }, 35 | "_release": "3.2.6", 36 | "_resolution": { 37 | "type": "version", 38 | "tag": "v3.2.6", 39 | "commit": "13386cd1b5947d8a5d23a12b531ce3960be1eba7" 40 | }, 41 | "_source": "git://github.com/aspnet/jquery-validation-unobtrusive.git", 42 | "_target": "3.2.6", 43 | "_originalSource": "jquery-validation-unobtrusive" 44 | } -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/CartModule/CartItem.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.ProductModule; 2 | 3 | namespace EventSourcingCQRS.Domain.CartModule 4 | { 5 | public class CartItem 6 | { 7 | public CartItem(ProductId productId, int quantity) 8 | { 9 | ProductId = productId; 10 | Quantity = quantity; 11 | } 12 | 13 | public ProductId ProductId { get; } 14 | 15 | public int Quantity { get; } 16 | 17 | public override bool Equals(object obj) 18 | { 19 | var castedObj = obj as CartItem; 20 | return castedObj != null && Equals(castedObj.ProductId, ProductId) 21 | && Equals(castedObj.Quantity, Quantity); 22 | } 23 | 24 | public override int GetHashCode() 25 | { 26 | var hashCode = 76325633; 27 | hashCode = hashCode * -1521134295 + ProductId.GetHashCode(); 28 | hashCode = hashCode * -1521134295 + Quantity.GetHashCode(); 29 | return hashCode; 30 | } 31 | 32 | public override string ToString() 33 | { 34 | return $"{{ ProductId: \"{ProductId}\", Quantity: {Quantity} }}"; 35 | } 36 | 37 | public CartItem WithQuantity(int quantity) 38 | { 39 | return new CartItem(ProductId, quantity); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Application/Services/CartReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Threading.Tasks; 5 | using EventSourcingCQRS.ReadModel.Cart; 6 | using EventSourcingCQRS.ReadModel.Persistence; 7 | 8 | namespace EventSourcingCQRS.Application.Services 9 | { 10 | public class CartReader : ICartReader 11 | { 12 | private readonly IReadOnlyRepository cartRepository; 13 | private readonly IReadOnlyRepository cartItemRepository; 14 | 15 | public CartReader(IReadOnlyRepository cartRepository, IReadOnlyRepository cartItemRepository) 16 | { 17 | this.cartRepository = cartRepository; 18 | this.cartItemRepository = cartItemRepository; 19 | } 20 | 21 | public async Task> FindAllAsync(Expression> predicate) 22 | { 23 | return await cartRepository.FindAllAsync(predicate); 24 | } 25 | 26 | public async Task GetByIdAsync(string id) 27 | { 28 | return await cartRepository.GetByIdAsync(id); 29 | } 30 | 31 | public async Task> GetItemsOfAsync(string cartId) 32 | { 33 | return await cartItemRepository.FindAllAsync(x => x.CartId == cartId); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/CartModule/ProductQuantityChangedEvent.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.Core; 2 | using EventSourcingCQRS.Domain.ProductModule; 3 | 4 | namespace EventSourcingCQRS.Domain.CartModule 5 | { 6 | public class ProductQuantityChangedEvent : DomainEventBase 7 | { 8 | ProductQuantityChangedEvent() 9 | { 10 | } 11 | 12 | internal ProductQuantityChangedEvent(ProductId productId, int oldQuantity, int newQuantity) : base() 13 | { 14 | ProductId = productId; 15 | OldQuantity = oldQuantity; 16 | NewQuantity = newQuantity; 17 | } 18 | 19 | private ProductQuantityChangedEvent(CartId aggregateId, long aggregateVersion, ProductId productId, 20 | int oldQuantity, int newQuantity) : base(aggregateId, aggregateVersion) 21 | { 22 | ProductId = productId; 23 | OldQuantity = oldQuantity; 24 | NewQuantity = newQuantity; 25 | } 26 | 27 | public ProductId ProductId { get; private set; } 28 | 29 | public int OldQuantity { get; private set; } 30 | 31 | public int NewQuantity { get; private set; } 32 | 33 | public override IDomainEvent WithAggregate(CartId aggregateId, long aggregateVersion) 34 | { 35 | return new ProductQuantityChangedEvent(aggregateId, aggregateVersion,ProductId, OldQuantity, NewQuantity); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/Core/DomainEventBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace EventSourcingCQRS.Domain.Core 5 | { 6 | public abstract class DomainEventBase : IDomainEvent, IEquatable> 7 | { 8 | protected DomainEventBase() 9 | { 10 | EventId = Guid.NewGuid(); 11 | } 12 | 13 | protected DomainEventBase(TAggregateId aggregateId) : this() 14 | { 15 | AggregateId = aggregateId; 16 | } 17 | 18 | protected DomainEventBase(TAggregateId aggregateId, long aggregateVersion) : this(aggregateId) 19 | { 20 | AggregateVersion = aggregateVersion; 21 | } 22 | 23 | public Guid EventId { get; private set; } 24 | 25 | public TAggregateId AggregateId { get; private set; } 26 | 27 | public long AggregateVersion { get; private set; } 28 | 29 | public override bool Equals(object obj) 30 | { 31 | return base.Equals(obj as DomainEventBase); 32 | } 33 | 34 | public bool Equals(DomainEventBase other) 35 | { 36 | return other != null && 37 | EventId.Equals(other.EventId); 38 | } 39 | 40 | public override int GetHashCode() 41 | { 42 | return 290933282 + EqualityComparer.Default.GetHashCode(EventId); 43 | } 44 | 45 | public abstract IDomainEvent WithAggregate(TAggregateId aggregateId, long aggregateVersion); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/ProductModule/ProductId.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.Core; 2 | using System; 3 | 4 | namespace EventSourcingCQRS.Domain.ProductModule 5 | { 6 | public class ProductId : IAggregateId 7 | { 8 | private const string IdAsStringPrefix = "Product-"; 9 | 10 | public Guid Id { get; private set; } 11 | 12 | private ProductId(Guid id) 13 | { 14 | Id = id; 15 | } 16 | 17 | public ProductId(string id) 18 | { 19 | Id = Guid.Parse(id.StartsWith(IdAsStringPrefix) ? id.Substring(IdAsStringPrefix.Length) : id); 20 | } 21 | 22 | public override string ToString() 23 | { 24 | return IdAsString(); 25 | } 26 | 27 | public override bool Equals(object obj) 28 | { 29 | return obj is ProductId && Equals(Id, ((ProductId)obj).Id); 30 | } 31 | 32 | public override int GetHashCode() 33 | { 34 | return Id.GetHashCode(); 35 | } 36 | 37 | public static ProductId NewProductId() 38 | { 39 | return new ProductId(Guid.NewGuid()); 40 | } 41 | 42 | public string IdAsString() 43 | { 44 | return $"{IdAsStringPrefix}{Id.ToString()}"; 45 | } 46 | 47 | public static bool operator !=(ProductId left, ProductId right) 48 | { 49 | return !(left == right); 50 | } 51 | 52 | public static bool operator ==(ProductId left, ProductId right) 53 | { 54 | return Equals(left?.Id, right?.Id); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain.EventStore.Tests/JsonConvertTest.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.CartModule; 2 | using EventSourcingCQRS.Domain.CustomerModule; 3 | using Newtonsoft.Json; 4 | using System; 5 | using Xunit; 6 | 7 | namespace EventSourcingCQRS.Domain.EventStore.Tests 8 | { 9 | [Trait("Type", "Unit")] 10 | public class JsonConvertTest 11 | { 12 | [Fact] 13 | public void DeserializeEvent() 14 | { 15 | var json = "{\"CustomerId\":{\"Id\":\"9154d569-0bf3-4c37-b588-ed49765c45dc\"},\"EventId\":\"3907f8bd-6b4b-4825-8a7c-b63800e7f28f\",\"AggregateId\":{\"Id\":\"23934182-ea29-4c99-8b4a-845c4b152153\"},\"AggregateVersion\":-1}"; 16 | 17 | var contractResolver = new PrivateSetterContractResolver(); 18 | JsonSerializerSettings settings = new JsonSerializerSettings { ContractResolver = contractResolver }; 19 | var type = Type.GetType("EventSourcingCQRS.Domain.CartModule.CartCreatedEvent, EventSourcingCQRS.Domain, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"); 20 | 21 | var @event = JsonConvert.DeserializeObject(json, type, settings); 22 | 23 | Assert.IsType(@event); 24 | Assert.Equal(new CustomerId("9154d569-0bf3-4c37-b588-ed49765c45dc"), ((CartCreatedEvent)@event).CustomerId); 25 | Assert.Equal(new CartId("23934182-ea29-4c99-8b4a-845c4b152153"), ((CartCreatedEvent)@event).AggregateId); 26 | Assert.Equal(new Guid("3907f8bd-6b4b-4825-8a7c-b63800e7f28f"), ((CartCreatedEvent)@event).EventId); 27 | Assert.Equal(-1, ((CartCreatedEvent)@event).AggregateVersion); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain.Tests/Utility/GenericAggregateBaseTest.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.Core; 2 | using EventSourcingCQRS.Domain.Persistence; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using Xunit; 7 | 8 | namespace EventSourcingCQRS.Domain.Tests.Utility 9 | { 10 | public abstract class GenericAggregateBaseTest 11 | where TAggregate : AggregateBase, IAggregate 12 | where TAggregateId : IAggregateId 13 | { 14 | protected void AssertSingleUncommittedEventOfType(TAggregate aggregate) 15 | where TEvent : IDomainEvent 16 | { 17 | var uncommittedEvents = GetUncommittedEventsOf(aggregate); 18 | 19 | Assert.Single(uncommittedEvents); 20 | Assert.IsType(uncommittedEvents.First()); 21 | } 22 | 23 | protected void AssertSingleUncommittedEvent(TAggregate aggregate, Action assertions) 24 | where TEvent : IDomainEvent 25 | { 26 | AssertSingleUncommittedEventOfType(aggregate); 27 | assertions((TEvent)((IEventSourcingAggregate)aggregate).GetUncommittedEvents().Single()); 28 | } 29 | 30 | protected void ClearUncommittedEvents(TAggregate aggregate) 31 | { 32 | ((IEventSourcingAggregate)aggregate).ClearUncommittedEvents(); 33 | } 34 | 35 | protected IEnumerable> GetUncommittedEventsOf(TAggregate aggregate) 36 | { 37 | return ((IEventSourcingAggregate)aggregate).GetUncommittedEvents(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/lib/jquery/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright jQuery Foundation and other contributors, https://jquery.org/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/jquery/jquery 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules and external directories are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. 37 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/Core/AggregateBase.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.Persistence; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace EventSourcingCQRS.Domain.Core 6 | { 7 | public abstract class AggregateBase : IAggregate, IEventSourcingAggregate 8 | { 9 | public const long NewAggregateVersion = -1; 10 | 11 | private readonly ICollection> _uncommittedEvents = new LinkedList>(); 12 | private long _version = NewAggregateVersion; 13 | 14 | public TId Id { get; protected set; } 15 | 16 | long IEventSourcingAggregate.Version => _version; 17 | 18 | void IEventSourcingAggregate.ApplyEvent(IDomainEvent @event, long version) 19 | { 20 | if (!_uncommittedEvents.Any(x => Equals(x.EventId, @event.EventId))) 21 | { 22 | ((dynamic)this).Apply((dynamic)@event); 23 | _version = version; 24 | } 25 | } 26 | 27 | void IEventSourcingAggregate.ClearUncommittedEvents() 28 | { 29 | _uncommittedEvents.Clear(); 30 | } 31 | 32 | IEnumerable> IEventSourcingAggregate.GetUncommittedEvents() 33 | { 34 | return _uncommittedEvents.AsEnumerable(); 35 | } 36 | 37 | protected void RaiseEvent(TEvent @event) 38 | where TEvent: DomainEventBase 39 | { 40 | IDomainEvent eventWithAggregate = @event.WithAggregate( 41 | Equals(Id, default(TId)) ? @event.AggregateId : Id, 42 | _version); 43 | 44 | ((IEventSourcingAggregate)this).ApplyEvent(eventWithAggregate, _version + 1); 45 | _uncommittedEvents.Add(eventWithAggregate); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/Persistence/EventStore/EventStoreException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace EventSourcingCQRS.Domain.Persistence.EventStore 4 | { 5 | [Serializable] 6 | public class EventStoreException : Exception 7 | { 8 | public EventStoreException() { } 9 | public EventStoreException(string message) : base(message) { } 10 | public EventStoreException(string message, Exception inner) : base(message, inner) { } 11 | protected EventStoreException( 12 | System.Runtime.Serialization.SerializationInfo info, 13 | System.Runtime.Serialization.StreamingContext context) : base(info, context) { } 14 | } 15 | 16 | 17 | [Serializable] 18 | public class EventStoreAggregateNotFoundException : EventStoreException 19 | { 20 | public EventStoreAggregateNotFoundException() { } 21 | public EventStoreAggregateNotFoundException(string message) : base(message) { } 22 | public EventStoreAggregateNotFoundException(string message, Exception inner) : base(message, inner) { } 23 | protected EventStoreAggregateNotFoundException( 24 | System.Runtime.Serialization.SerializationInfo info, 25 | System.Runtime.Serialization.StreamingContext context) : base(info, context) { } 26 | } 27 | 28 | 29 | [Serializable] 30 | public class EventStoreCommunicationException : EventStoreException 31 | { 32 | public EventStoreCommunicationException() { } 33 | public EventStoreCommunicationException(string message) : base(message) { } 34 | public EventStoreCommunicationException(string message, Exception inner) : base(message, inner) { } 35 | protected EventStoreCommunicationException( 36 | System.Runtime.Serialization.SerializationInfo info, 37 | System.Runtime.Serialization.StreamingContext context) : base(info, context) { } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain.EventStore.Tests/EventStoreTest.cs: -------------------------------------------------------------------------------- 1 | using EventStore.ClientAPI; 2 | using System; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace EventSourcingCQRS.Domain.EventStore.Tests 8 | { 9 | [Trait("Type", "Integration")] 10 | public class EventStoreTest : IDisposable 11 | { 12 | private IEventStoreConnection connection; 13 | private string stream; 14 | 15 | public EventStoreTest() 16 | { 17 | connection = EventStoreConnection.Create(new Uri("tcp://localhost:1113")); 18 | connection.ConnectAsync().Wait(); 19 | stream = Guid.NewGuid().ToString(); 20 | } 21 | 22 | [Fact] 23 | public async Task TestStreamDoesNotExists() 24 | { 25 | var events = await connection.ReadStreamEventsForwardAsync(stream, StreamPosition.Start, 1, false); 26 | 27 | Assert.Equal(SliceReadStatus.StreamNotFound, events.Status); 28 | } 29 | 30 | [Fact] 31 | public async Task TestStreamExists() 32 | { 33 | await AppendEventToStreamAsync(); 34 | 35 | var events = await connection.ReadStreamEventsForwardAsync(stream, StreamPosition.Start, 1, false); 36 | 37 | Assert.Equal(SliceReadStatus.Success, events.Status); 38 | Assert.Single(events.Events); 39 | } 40 | 41 | [Fact] 42 | public async Task TestPerformance() 43 | { 44 | for (int i = 0; i < 100; i++) 45 | { 46 | await connection.AppendToStreamAsync(stream, i - 1, 47 | new EventData(Guid.NewGuid(), "test", true, Encoding.UTF8.GetBytes("{}"), StreamMetadata.Create().AsJsonBytes())); 48 | } 49 | } 50 | 51 | private async Task AppendEventToStreamAsync() 52 | { 53 | await connection.AppendToStreamAsync(stream, ExpectedVersion.NoStream, 54 | new EventData(Guid.NewGuid(), "test", true, Encoding.UTF8.GetBytes("{}"), StreamMetadata.Create().AsJsonBytes())); 55 | } 56 | 57 | public void Dispose() 58 | { 59 | connection.DeleteStreamAsync(stream, ExpectedVersion.Any).Wait(); 60 | connection.Dispose(); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /EventSourcingCQRS.ReadModel/Persistence/MongoDBRepository.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.ReadModel.Common; 2 | using MongoDB.Driver; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq.Expressions; 6 | using System.Threading.Tasks; 7 | 8 | namespace EventSourcingCQRS.ReadModel.Persistence 9 | { 10 | public class MongoDBRepository : IRepository 11 | where T : IReadEntity 12 | { 13 | private readonly IMongoDatabase mongoDatabase; 14 | 15 | public MongoDBRepository(IMongoDatabase mongoDatabase) 16 | { 17 | this.mongoDatabase = mongoDatabase; 18 | } 19 | 20 | private string CollectionName => typeof(T).Name; 21 | 22 | public async Task> FindAllAsync(Expression> predicate) 23 | { 24 | var cursor = await mongoDatabase.GetCollection(CollectionName) 25 | .FindAsync(predicate); 26 | return cursor.ToEnumerable(); 27 | } 28 | 29 | public Task GetByIdAsync(string id) 30 | { 31 | return mongoDatabase.GetCollection(CollectionName) 32 | .Find(x => x.Id == id) 33 | .SingleAsync(); 34 | } 35 | 36 | public async Task InsertAsync(T entity) 37 | { 38 | try 39 | { 40 | await mongoDatabase.GetCollection(CollectionName) 41 | .InsertOneAsync(entity); 42 | } 43 | catch (MongoWriteException ex) 44 | { 45 | throw new RepositoryException($"Error inserting entity {entity.Id}", ex); 46 | } 47 | } 48 | 49 | public async Task UpdateAsync(T entity) 50 | { 51 | try 52 | { 53 | var result = await mongoDatabase.GetCollection(CollectionName) 54 | .ReplaceOneAsync(x => x.Id == entity.Id, entity); 55 | 56 | if (result.MatchedCount != 1) 57 | { 58 | throw new RepositoryException($"Missing entoty {entity.Id}"); 59 | } 60 | } 61 | catch (MongoWriteException ex) 62 | { 63 | throw new RepositoryException($"Error updating entity {entity.Id}", ex); 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Application/PubSub/TransientDomainEventPubSub.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.PubSub; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace EventSourcingCQRS.Application.PubSub 8 | { 9 | public class TransientDomainEventPubSub : IDisposable, ITransientDomainEventSubscriber, ITransientDomainEventPublisher 10 | { 11 | private static AsyncLocal>> handlers = new AsyncLocal>>(); 12 | 13 | public Dictionary> Handlers 14 | { 15 | get => handlers.Value ?? (handlers.Value = new Dictionary>()); 16 | } 17 | 18 | public TransientDomainEventPubSub() 19 | { 20 | } 21 | 22 | public void Dispose() 23 | { 24 | foreach (var handlersOfT in Handlers.Values) 25 | { 26 | handlersOfT.Clear(); 27 | } 28 | Handlers.Clear(); 29 | } 30 | 31 | public void Subscribe(Action handler) 32 | { 33 | GetHandlersOf().Add(handler); 34 | } 35 | 36 | public void Subscribe(Func handler) 37 | { 38 | GetHandlersOf().Add(handler); 39 | } 40 | 41 | public async Task PublishAsync(T publishedEvent) 42 | { 43 | foreach (var handler in GetHandlersOf()) 44 | { 45 | try 46 | { 47 | switch (handler) 48 | { 49 | case Action action: 50 | action(publishedEvent); 51 | break; 52 | case Func action: 53 | await action(publishedEvent); 54 | break; 55 | default: 56 | break; 57 | } 58 | } 59 | catch 60 | { 61 | //Logging 62 | } 63 | } 64 | } 65 | 66 | private ICollection GetHandlersOf() 67 | { 68 | return Handlers.GetValueOrDefault(typeof(T)) ?? (Handlers[typeof(T)] = new List()); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /EventSourcingCQRS/Views/Carts/IndexAsync.cshtml: -------------------------------------------------------------------------------- 1 | @model CartIndexViewModel 2 | 3 | @{ 4 | ViewData["Title"] = "Carts"; 5 | Layout = "~/Views/Shared/_Layout.cshtml"; 6 | } 7 | 8 |

@ViewData["Title"]

9 | 10 |

11 | 14 |

15 | 16 | 17 | 18 | 21 | 24 | 25 | 26 | 27 | 28 | @foreach (var item in Model.Carts) { 29 | 30 | 33 | 36 | 39 | 40 | } 41 | 42 |
19 | @Html.DisplayNameFor(model => model.Carts.First().CustomerName) 20 | 22 | @Html.DisplayNameFor(model => model.Carts.First().TotalItems) 23 |
31 | @Html.DisplayFor(modelItem => item.CustomerName) 32 | 34 | @Html.DisplayFor(modelItem => item.TotalItems) 35 | 37 | 38 |
43 | -------------------------------------------------------------------------------- /EventSourcingCQRS.ReadModel.Tests/MongoDBRepositoryTest.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.ReadModel.Persistence; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace EventSourcingCQRS.ReadModel.Tests 7 | { 8 | [Trait("Type", "Integration")] 9 | public class MongoDBRepositoryTest : MongoDBTestBase 10 | { 11 | private const string Id = "1"; 12 | 13 | private readonly MongoDBRepository sut; 14 | 15 | public MongoDBRepositoryTest() 16 | { 17 | sut = new MongoDBRepository(mongoDB); 18 | } 19 | 20 | [Fact] 21 | public async Task CanInsert() 22 | { 23 | await sut.InsertAsync(new TestEntity(Id) { Quantity = 3 }); 24 | 25 | var test = await sut.GetByIdAsync(Id); 26 | 27 | Assert.NotNull(test); 28 | Assert.Equal(Id, test.Id); 29 | } 30 | 31 | [Fact] 32 | public async Task CanUpdate() 33 | { 34 | await sut.InsertAsync(new TestEntity(Id) { Quantity = 3 }); 35 | await sut.UpdateAsync(new TestEntity(Id) { Quantity = 10 }); 36 | 37 | var test = await sut.GetByIdAsync(Id); 38 | 39 | Assert.NotNull(test); 40 | Assert.Equal(10, test.Quantity); 41 | } 42 | 43 | [Fact] 44 | public async Task CanFinWithCondition() 45 | { 46 | await sut.InsertAsync(new TestEntity("1") { Quantity = 10 }); 47 | await sut.InsertAsync(new TestEntity("2") { Quantity = 11 }); 48 | await sut.InsertAsync(new TestEntity("3") { Quantity = 12 }); 49 | await sut.InsertAsync(new TestEntity("4") { Quantity = 13 }); 50 | 51 | var list = (await sut.FindAllAsync(x => x.Quantity > 10 && x.Quantity < 13)).ToList(); 52 | 53 | Assert.Collection(list, x => Assert.Equal("2", x.Id), x => Assert.Equal("3", x.Id)); 54 | } 55 | 56 | [Fact] 57 | public async Task InsertTwoObjectWithSameIdThrowsException() 58 | { 59 | await sut.InsertAsync(new TestEntity(Id) { Quantity = 3 }); 60 | 61 | await Assert.ThrowsAsync(async () => await sut.InsertAsync(new TestEntity(Id) { Quantity = 10 })); 62 | } 63 | 64 | [Fact] 65 | public async Task UpdateMissingObjectThrowsException() 66 | { 67 | await sut.InsertAsync(new TestEntity("1") { Quantity = 3 }); 68 | 69 | await Assert.ThrowsAsync(async () => await sut.UpdateAsync(new TestEntity("2") { Quantity = 10 })); 70 | } 71 | 72 | [Fact] 73 | public async Task FindAllAsyncWorksWhenDBAndCollectionDoNotExist() 74 | { 75 | var list = (await sut.FindAllAsync(x => true)).ToList(); 76 | 77 | Assert.Empty(list); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/Persistence/EventSourcingRepository.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.Core; 2 | using System.Threading.Tasks; 3 | using EventSourcingCQRS.Domain.PubSub; 4 | using System.Reflection; 5 | using System; 6 | using EventSourcingCQRS.Domain.Persistence.EventStore; 7 | 8 | namespace EventSourcingCQRS.Domain.Persistence 9 | { 10 | public class EventSourcingRepository : IRepository 11 | where TAggregate : AggregateBase, IAggregate 12 | where TAggregateId : IAggregateId 13 | { 14 | private readonly IEventStore eventStore; 15 | private readonly ITransientDomainEventPublisher publisher; 16 | 17 | public EventSourcingRepository(IEventStore eventStore, ITransientDomainEventPublisher publisher) 18 | { 19 | this.eventStore = eventStore; 20 | this.publisher = publisher; 21 | } 22 | 23 | public async Task GetByIdAsync(TAggregateId id) 24 | { 25 | try 26 | { 27 | var aggregate = CreateEmptyAggregate(); 28 | IEventSourcingAggregate aggregatePersistence = aggregate; 29 | 30 | foreach (var @event in await eventStore.ReadEventsAsync(id)) 31 | { 32 | aggregatePersistence.ApplyEvent(@event.DomainEvent, @event.EventNumber); 33 | } 34 | return aggregate; 35 | } 36 | catch (EventStoreAggregateNotFoundException) 37 | { 38 | return null; 39 | } 40 | catch (EventStoreCommunicationException ex) 41 | { 42 | throw new RepositoryException("Unable to access persistence layer", ex); 43 | } 44 | } 45 | 46 | public async Task SaveAsync(TAggregate aggregate) 47 | { 48 | try 49 | { 50 | IEventSourcingAggregate aggregatePersistence = aggregate; 51 | 52 | foreach (var @event in aggregatePersistence.GetUncommittedEvents()) 53 | { 54 | await eventStore.AppendEventAsync(@event); 55 | await publisher.PublishAsync((dynamic)@event); 56 | } 57 | aggregatePersistence.ClearUncommittedEvents(); 58 | } 59 | catch (EventStoreCommunicationException ex) 60 | { 61 | throw new RepositoryException("Unable to access persistence layer", ex); 62 | } 63 | } 64 | 65 | private TAggregate CreateEmptyAggregate() 66 | { 67 | return (TAggregate)typeof(TAggregate) 68 | .GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, 69 | null, new Type[0], new ParameterModifier[0]) 70 | .Invoke(new object[0]); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain.Tests/EventSourcingRepositoryTest.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.Persistence; 2 | using EventSourcingCQRS.Domain.Persistence.EventStore; 3 | using EventSourcingCQRS.Domain.PubSub; 4 | using EventSourcingCQRS.Domain.Tests.Utility; 5 | using Moq; 6 | using System.Collections.Generic; 7 | using System.Threading.Tasks; 8 | using Xunit; 9 | 10 | namespace EventSourcingCQRS.Domain.Tests 11 | { 12 | [Trait("Type", "Unit")] 13 | public partial class EventSourcingRepositoryTest 14 | { 15 | private IRepository sut; 16 | private Mock eventStoreMock; 17 | private Mock domainEventPublisherMock; 18 | private static readonly TestAggregateId DefaultId = new TestAggregateId(); 19 | 20 | public EventSourcingRepositoryTest() 21 | { 22 | domainEventPublisherMock = new Mock(); 23 | eventStoreMock = new Mock(); 24 | sut = new EventSourcingRepository(eventStoreMock.Object, domainEventPublisherMock.Object); 25 | } 26 | 27 | [Fact] 28 | public async Task ShouldLoadAnAggregateAndApplyEventsAsync() 29 | { 30 | TestDomainEvent domainEvent = new TestDomainEvent(); 31 | eventStoreMock.Setup(x => x.ReadEventsAsync(DefaultId)) 32 | .ReturnsAsync(new List>() 33 | { 34 | new Event(domainEvent, 0) 35 | }); 36 | var aggregate = await sut.GetByIdAsync(DefaultId); 37 | 38 | Assert.NotNull(aggregate); 39 | Assert.Single(aggregate.AppliedEvents); 40 | Assert.Equal(domainEvent, aggregate.AppliedEvents[0]); 41 | } 42 | 43 | [Fact] 44 | public async Task ShouldPublishUncommittedEventsOnSaveAsync() 45 | { 46 | TestDomainEvent domainEvent = new TestDomainEvent(); 47 | var aggregate = new TestAggregate(domainEvent); 48 | 49 | eventStoreMock.Setup(x => x.AppendEventAsync(domainEvent)).ReturnsAsync(new AppendResult(1)); 50 | await sut.SaveAsync(aggregate); 51 | domainEventPublisherMock.Verify(x => x.PublishAsync(domainEvent)); 52 | } 53 | 54 | [Fact] 55 | public async Task ShouldReturnsNullWhenAggregateNotFoundOrDeletedAsync() 56 | { 57 | eventStoreMock.Setup(x => x.ReadEventsAsync(DefaultId)).Throws(); 58 | Assert.Null(await sut.GetByIdAsync(DefaultId)); 59 | } 60 | 61 | [Fact] 62 | public void ShouldThrowsExceptionWhenEventStoreHasCommunicationIssues() 63 | { 64 | eventStoreMock.Setup(x => x.ReadEventsAsync(DefaultId)).Throws(); 65 | Assert.ThrowsAsync(async () => { await sut.GetByIdAsync(DefaultId); }); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /EventSourcingCQRS/Controllers/CartsController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using EventSourcingCQRS.ReadModel.Persistence; 3 | using EventSourcingCQRS.ReadModel.Customer; 4 | using EventSourcingCQRS.ReadModel.Product; 5 | using System.Threading.Tasks; 6 | using EventSourcingCQRS.Models; 7 | using System.Linq; 8 | using EventSourcingCQRS.Application.Services; 9 | 10 | namespace EventSourcingCQRS.Controllers 11 | { 12 | 13 | public class CartsController : Controller 14 | { 15 | private readonly ICartReader cartReader; 16 | private readonly ICartWriter cartWriter; 17 | private readonly IReadOnlyRepository customerRepository; 18 | private readonly IReadOnlyRepository productRepository; 19 | 20 | public CartsController(ICartReader cartReader, ICartWriter cartWriter, 21 | IReadOnlyRepository customerRepository, IReadOnlyRepository productRepository) 22 | { 23 | this.cartReader = cartReader; 24 | this.cartWriter = cartWriter; 25 | this.customerRepository = customerRepository; 26 | this.productRepository = productRepository; 27 | } 28 | 29 | public async Task IndexAsync() 30 | { 31 | return View(new CartIndexViewModel 32 | { 33 | Carts = await cartReader.FindAllAsync(x => true), 34 | Customers = (await customerRepository.FindAllAsync(x => true)).ToList() 35 | }); 36 | } 37 | 38 | [HttpPost] 39 | public async Task CreateAsync(string customerId) 40 | { 41 | await cartWriter.CreateAsync(customerId); 42 | return RedirectToAction(nameof(IndexAsync)); 43 | } 44 | 45 | [Route("Carts/{id:length(41)}")] 46 | public async Task DetailsAsync(string id) 47 | { 48 | var viewModel = new CartDetailsViewModel 49 | { 50 | Cart = await cartReader.GetByIdAsync(id), 51 | CartItems = (await cartReader.GetItemsOfAsync(id)).ToList(), 52 | Products = (await productRepository.FindAllAsync(x => true)).ToList() 53 | }; 54 | return View(viewModel); 55 | } 56 | 57 | [HttpPost] 58 | [Route("Carts/{id:length(41)}/AddProduct")] 59 | public async Task AddProductAsync(string id, string productId, int quantity) 60 | { 61 | await cartWriter.AddProductAsync(id, productId, quantity); 62 | return RedirectToAction(nameof(DetailsAsync), new { id }); 63 | } 64 | 65 | [Route("Carts/{id:length(41)}/ChangeProductQuantity")] 66 | [HttpPost] 67 | public async Task ChangeProductQuantityAsync(string id, string productId, int quantity) 68 | { 69 | await cartWriter.ChangeProductQuantityAsync(id, productId, quantity); 70 | return RedirectToAction(nameof(DetailsAsync), new { id }); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /EventSourcingCQRS/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - EventSourcingCQRS 7 | 8 | 9 | 10 | 11 | 12 | 13 | 16 | 17 | 18 | 19 | 20 | 38 |
39 | @RenderBody() 40 |
41 |
42 |

© 2017 - EventSourcingCQRS

43 |
44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 58 | 64 | 65 | 66 | 67 | @RenderSection("Scripts", required: false) 68 | 69 | 70 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Application/Services/CartWriter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using EventSourcingCQRS.Domain.CartModule; 4 | using EventSourcingCQRS.Domain.Core; 5 | using EventSourcingCQRS.Domain.CustomerModule; 6 | using EventSourcingCQRS.Domain.Persistence; 7 | using EventSourcingCQRS.Domain.ProductModule; 8 | using EventSourcingCQRS.Domain.PubSub; 9 | 10 | namespace EventSourcingCQRS.Application.Services 11 | { 12 | public class CartWriter : ICartWriter 13 | { 14 | private readonly IRepository cartRepository; 15 | private readonly ITransientDomainEventSubscriber subscriber; 16 | private readonly IEnumerable> cartCreatedEventHandlers; 17 | private readonly IEnumerable> productAddedEventHandlers; 18 | private readonly IEnumerable> productQuantityChangedEventHandlers; 19 | 20 | public CartWriter(IRepository cartRepository, 21 | ITransientDomainEventSubscriber subscriber, 22 | IEnumerable> cartCreatedEventHandlers, 23 | IEnumerable> productAddedEventHandlers, 24 | IEnumerable> productQuantityChangedEventHandlers) 25 | { 26 | this.cartRepository = cartRepository; 27 | this.subscriber = subscriber; 28 | this.cartCreatedEventHandlers = cartCreatedEventHandlers; 29 | this.productAddedEventHandlers = productAddedEventHandlers; 30 | this.productQuantityChangedEventHandlers = productQuantityChangedEventHandlers; 31 | } 32 | 33 | public async Task AddProductAsync(string cartId, string productId, int quantity) 34 | { 35 | var cart = await cartRepository.GetByIdAsync(new CartId(cartId)); 36 | 37 | subscriber.Subscribe(async @event => await HandleAsync(productAddedEventHandlers, @event)); 38 | cart.AddProduct(new ProductId(productId), quantity); 39 | await cartRepository.SaveAsync(cart); 40 | } 41 | 42 | public async Task ChangeProductQuantityAsync(string cartId, string productId, int quantity) 43 | { 44 | var cart = await cartRepository.GetByIdAsync(new CartId(cartId)); 45 | 46 | subscriber.Subscribe(async @event => await HandleAsync(productQuantityChangedEventHandlers, @event)); 47 | cart.ChangeProductQuantity(new ProductId(productId), quantity); 48 | await cartRepository.SaveAsync(cart); 49 | } 50 | 51 | public async Task CreateAsync(string customerId) 52 | { 53 | var cart = new Cart(CartId.NewCartId(), new CustomerId(customerId)); 54 | 55 | subscriber.Subscribe(async @event => await HandleAsync(cartCreatedEventHandlers, @event)); 56 | await cartRepository.SaveAsync(cart); 57 | } 58 | 59 | public async Task HandleAsync(IEnumerable> handlers, T @event) 60 | where T : IDomainEvent 61 | { 62 | foreach (var handler in handlers) 63 | { 64 | await handler.HandleAsync(@event); 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Application/Handlers/CartUpdater.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.CartModule; 2 | using EventSourcingCQRS.ReadModel.Customer; 3 | using EventSourcingCQRS.ReadModel.Persistence; 4 | using EventSourcingCQRS.ReadModel.Product; 5 | using CartReadModel = EventSourcingCQRS.ReadModel.Cart.Cart; 6 | using CartItemReadModel = EventSourcingCQRS.ReadModel.Cart.CartItem; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using EventSourcingCQRS.Application.Services; 10 | 11 | namespace EventSourcingCQRS.Application.Handlers 12 | { 13 | public class CartUpdater : IDomainEventHandler, 14 | IDomainEventHandler, 15 | IDomainEventHandler 16 | { 17 | private readonly IReadOnlyRepository customerRepository; 18 | private readonly IReadOnlyRepository productRepository; 19 | private readonly IRepository cartRepository; 20 | private readonly IRepository cartItemRepository; 21 | 22 | public CartUpdater(IReadOnlyRepository customerRepository, 23 | IReadOnlyRepository productRepository, IRepository cartRepository, 24 | IRepository cartItemRepository) 25 | { 26 | this.customerRepository = customerRepository; 27 | this.productRepository = productRepository; 28 | this.cartRepository = cartRepository; 29 | this.cartItemRepository = cartItemRepository; 30 | } 31 | 32 | public async Task HandleAsync(CartCreatedEvent @event) 33 | { 34 | var customer = await customerRepository.GetByIdAsync(@event.CustomerId.IdAsString()); 35 | 36 | await cartRepository.InsertAsync(new CartReadModel 37 | { 38 | Id = @event.AggregateId.IdAsString(), 39 | CustomerId = customer.Id, 40 | CustomerName = customer.Name, 41 | TotalItems = 0 42 | }); 43 | } 44 | 45 | public async Task HandleAsync(ProductAddedEvent @event) 46 | { 47 | var product = await productRepository.GetByIdAsync(@event.ProductId.IdAsString()); 48 | var cart = await cartRepository.GetByIdAsync(@event.AggregateId.IdAsString()); 49 | var cartItem = CartItemReadModel.CreateFor(@event.AggregateId.IdAsString(), @event.ProductId.IdAsString()); 50 | 51 | cartItem.ProductName = product.Name; 52 | cartItem.Quantity = @event.Quantity; 53 | cart.TotalItems += @event.Quantity; 54 | await cartRepository.UpdateAsync(cart); 55 | await cartItemRepository.InsertAsync(cartItem); 56 | } 57 | 58 | public async Task HandleAsync(ProductQuantityChangedEvent @event) 59 | { 60 | var cartItemId = CartItemReadModel.IdFor(@event.AggregateId.IdAsString(), @event.ProductId.IdAsString()); 61 | var cartItem = (await cartItemRepository 62 | .FindAllAsync(x => x.Id == cartItemId)) 63 | .Single(); 64 | var cart = await cartRepository.GetByIdAsync(@event.AggregateId.IdAsString()); 65 | 66 | cart.TotalItems += @event.NewQuantity - @event.OldQuantity; 67 | cartItem.Quantity = @event.NewQuantity; 68 | 69 | await cartRepository.UpdateAsync(cart); 70 | await cartItemRepository.UpdateAsync(cartItem); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /EventSourcingCQRS.ReadModel.Tests/MongoDBTest.cs: -------------------------------------------------------------------------------- 1 | using MongoDB.Driver; 2 | using System; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Xunit; 6 | 7 | namespace EventSourcingCQRS.ReadModel.Tests 8 | { 9 | 10 | [Trait("Type", "Integration")] 11 | public class MongoDBTest : MongoDBTestBase 12 | { 13 | private const string CollectionName = "TestEntity"; 14 | 15 | [Fact] 16 | public async Task CanInsertAnItemInCollection() 17 | { 18 | string id = await InsertTestElementWithQuantity(5); 19 | var test = await mongoDB.GetCollection(CollectionName) 20 | .Find(x => x.Id == id) 21 | .SingleAsync(); 22 | 23 | Assert.NotNull(test); 24 | Assert.Equal(id, test.Id); 25 | Assert.Equal(5, test.Quantity); 26 | } 27 | 28 | [Fact] 29 | public async Task CanIncrementQuantity() 30 | { 31 | string id = await InsertTestElementWithQuantity(5); 32 | 33 | await mongoDB.GetCollection(CollectionName) 34 | .FindOneAndUpdateAsync(x => x.Id == id, Builders.Update.Inc(x => x.Quantity, 3)); 35 | 36 | var test = await mongoDB.GetCollection(CollectionName) 37 | .Find(x => x.Id == id) 38 | .SingleAsync(); 39 | 40 | Assert.Equal(8, test.Quantity); 41 | } 42 | 43 | [Fact] 44 | public async Task CanUpdateQuantity() 45 | { 46 | string id = await InsertTestElementWithQuantity(5); 47 | 48 | var test = await mongoDB.GetCollection(CollectionName) 49 | .Find(x => x.Id == id) 50 | .SingleAsync(); 51 | 52 | await mongoDB.GetCollection(CollectionName) 53 | .UpdateOneAsync(x => x.Id == id, Builders.Update.Set(x => x.Quantity, 8)); 54 | 55 | test = await mongoDB.GetCollection(CollectionName) 56 | .Find(x => x.Id == id) 57 | .SingleAsync(); 58 | 59 | Assert.Equal(8, test.Quantity); 60 | } 61 | 62 | [Fact] 63 | public async Task IdIsNotAutomaticallyAssigned() 64 | { 65 | await mongoDB.GetCollection(CollectionName) 66 | .InsertOneAsync(new TestEntity(null) 67 | { 68 | Quantity = 7 69 | }); 70 | 71 | var test = await mongoDB.GetCollection(CollectionName) 72 | .Find(FilterDefinition.Empty) 73 | .SingleAsync(); 74 | 75 | Assert.NotNull(test); 76 | Assert.Null(test.Id); 77 | } 78 | 79 | [Fact] 80 | public async Task ListExistingDBs() 81 | { 82 | await InsertTestElementWithQuantity(5); 83 | 84 | var databases = (await client.ListDatabasesAsync()).ToList(); 85 | var mongoDBTestDatabase = databases.SingleOrDefault(x => x["name"].AsString == GetType().Name); 86 | 87 | Assert.NotNull(mongoDBTestDatabase); 88 | } 89 | 90 | private async Task InsertTestElementWithQuantity(int quantity) 91 | { 92 | string id = Guid.NewGuid().ToString(); 93 | 94 | await mongoDB.GetCollection(CollectionName) 95 | .InsertOneAsync(new TestEntity(id) 96 | { 97 | Quantity = quantity 98 | }); 99 | return id; 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain/CartModule/Cart.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.Core; 2 | using System; 3 | using EventSourcingCQRS.Domain.CustomerModule; 4 | using EventSourcingCQRS.Domain.ProductModule; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace EventSourcingCQRS.Domain.CartModule 9 | { 10 | public class Cart : AggregateBase 11 | { 12 | public const int ProductQuantityThreshold = 50; 13 | 14 | //Needed for persistence purposes 15 | private Cart() 16 | { 17 | Items = new List(); 18 | } 19 | 20 | private CustomerId CustomerId { get; set; } 21 | 22 | private List Items { get; set; } 23 | 24 | public Cart(CartId cartId, CustomerId customerId) : this() 25 | { 26 | if (cartId == null) 27 | { 28 | throw new ArgumentNullException(nameof(cartId)); 29 | } 30 | if (customerId == null) 31 | { 32 | throw new ArgumentNullException(nameof(customerId)); 33 | } 34 | RaiseEvent(new CartCreatedEvent(cartId, customerId)); 35 | } 36 | 37 | public void AddProduct(ProductId productId, int quantity) 38 | { 39 | if (productId == null) 40 | { 41 | throw new ArgumentNullException(nameof(productId)); 42 | } 43 | if (ContainsProduct(productId)) 44 | { 45 | throw new CartException($"Product {productId} already added"); 46 | } 47 | CheckQuantity(productId, quantity); 48 | RaiseEvent(new ProductAddedEvent(productId, quantity)); 49 | } 50 | 51 | public void ChangeProductQuantity(ProductId productId, int quantity) 52 | { 53 | if (!ContainsProduct(productId)) 54 | { 55 | throw new CartException($"Product {productId} not found"); 56 | } 57 | CheckQuantity(productId, quantity); 58 | RaiseEvent(new ProductQuantityChangedEvent(productId, GetCartItemByProduct(productId).Quantity, quantity)); 59 | } 60 | 61 | public override string ToString() 62 | { 63 | return $"{{ Id: \"{Id}\", CustomerId: \"{CustomerId.IdAsString()}\", Items: [{string.Join(", ", Items.Select(x => x.ToString()))}] }}"; 64 | } 65 | 66 | internal void Apply(CartCreatedEvent ev) 67 | { 68 | Id = ev.AggregateId; 69 | CustomerId = ev.CustomerId; 70 | } 71 | 72 | internal void Apply(ProductAddedEvent ev) 73 | { 74 | Items.Add(new CartItem(ev.ProductId, ev.Quantity)); 75 | } 76 | 77 | internal void Apply(ProductQuantityChangedEvent ev) 78 | { 79 | var existingItem = Items.Single(x => x.ProductId == ev.ProductId); 80 | 81 | Items.Remove(existingItem); 82 | Items.Add(existingItem.WithQuantity(ev.NewQuantity)); 83 | } 84 | 85 | private bool ContainsProduct(ProductId productId) 86 | { 87 | return Items.Any(x => x.ProductId == productId); 88 | } 89 | 90 | private CartItem GetCartItemByProduct(ProductId productId) 91 | { 92 | return Items.Single(x => x.ProductId == productId); 93 | } 94 | 95 | private static void CheckQuantity(ProductId productId, int quantity) 96 | { 97 | if (quantity <= 0) 98 | { 99 | throw new ArgumentException("Quantity must be greater than zero", nameof(quantity)); 100 | } 101 | if (quantity > ProductQuantityThreshold) 102 | { 103 | throw new CartException($"Quantity for product {productId} must be less than or equal to {ProductQuantityThreshold}"); 104 | } 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain.EventStore/EventStoreEventStore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using EventSourcingCQRS.Domain.Core; 6 | using EventSourcingCQRS.Domain.Persistence.EventStore; 7 | using EventStore.ClientAPI; 8 | using EventStore.ClientAPI.Exceptions; 9 | using Newtonsoft.Json; 10 | 11 | namespace EventSourcingCQRS.Domain.EventStore 12 | { 13 | public class EventStoreEventStore : IEventStore 14 | { 15 | private readonly IEventStoreConnection connection; 16 | 17 | public EventStoreEventStore(IEventStoreConnection connection) 18 | { 19 | this.connection = connection; 20 | } 21 | 22 | public async Task>> ReadEventsAsync(TAggregateId id) 23 | where TAggregateId: IAggregateId 24 | { 25 | try 26 | { 27 | var ret = new List>(); 28 | StreamEventsSlice currentSlice; 29 | long nextSliceStart = StreamPosition.Start; 30 | 31 | do 32 | { 33 | currentSlice = await connection.ReadStreamEventsForwardAsync(id.IdAsString(), nextSliceStart, 200, false); 34 | if (currentSlice.Status != SliceReadStatus.Success) 35 | { 36 | throw new EventStoreAggregateNotFoundException($"Aggregate {id.IdAsString()} not found"); 37 | } 38 | nextSliceStart = currentSlice.NextEventNumber; 39 | foreach (var resolvedEvent in currentSlice.Events) 40 | { 41 | ret.Add(new Event(Deserialize(resolvedEvent.Event.EventType, resolvedEvent.Event.Data), resolvedEvent.Event.EventNumber)); 42 | } 43 | } while (!currentSlice.IsEndOfStream); 44 | 45 | return ret; 46 | } 47 | catch (EventStoreConnectionException ex) 48 | { 49 | throw new EventStoreCommunicationException($"Error while reading events for aggregate {id}", ex); 50 | } 51 | } 52 | 53 | public async Task AppendEventAsync(IDomainEvent @event) 54 | where TAggregateId : IAggregateId 55 | { 56 | try 57 | { 58 | var eventData = new EventData( 59 | @event.EventId, 60 | @event.GetType().AssemblyQualifiedName, 61 | true, 62 | Serialize(@event), 63 | Encoding.UTF8.GetBytes("{}")); 64 | 65 | var writeResult = await connection.AppendToStreamAsync( 66 | @event.AggregateId.IdAsString(), 67 | @event.AggregateVersion == AggregateBase.NewAggregateVersion ? ExpectedVersion.NoStream : @event.AggregateVersion, 68 | eventData); 69 | 70 | return new AppendResult(writeResult.NextExpectedVersion); 71 | } 72 | catch (EventStoreConnectionException ex) 73 | { 74 | throw new EventStoreCommunicationException($"Error while appending event {@event.EventId} for aggregate {@event.AggregateId}", ex); 75 | } 76 | } 77 | 78 | private IDomainEvent Deserialize(string eventType, byte[] data) 79 | { 80 | JsonSerializerSettings settings = new JsonSerializerSettings { ContractResolver = new PrivateSetterContractResolver() }; 81 | return (IDomainEvent)JsonConvert.DeserializeObject(Encoding.UTF8.GetString(data), Type.GetType(eventType), settings); 82 | } 83 | 84 | private byte[] Serialize(IDomainEvent @event) 85 | { 86 | return Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(@event)); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Domain.Tests/CartTest.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Domain.CartModule; 2 | using EventSourcingCQRS.Domain.CustomerModule; 3 | using EventSourcingCQRS.Domain.ProductModule; 4 | using EventSourcingCQRS.Domain.Tests.Utility; 5 | using Xunit; 6 | 7 | namespace EventSourcingCQRS.Domain.Tests 8 | { 9 | [Trait("Type", "Unit")] 10 | public class CartTest : GenericAggregateBaseTest 11 | { 12 | private static readonly CustomerId DefaultCustomerId = CustomerId.NewCustomerId(); 13 | private static readonly CartId DefaultCartId = CartId.NewCartId(); 14 | private static readonly ProductId DefaultProductId = ProductId.NewProductId(); 15 | 16 | [Fact] 17 | public void GivenNoCartExistsWhenCreateOneThenCartCreatedEvent() 18 | { 19 | var cart = new Cart(DefaultCartId, DefaultCustomerId); 20 | 21 | AssertSingleUncommittedEvent(cart, @event => 22 | { 23 | Assert.Equal(DefaultCartId, @event.AggregateId); 24 | Assert.Equal(DefaultCustomerId, @event.CustomerId); 25 | }); 26 | } 27 | 28 | [Fact] 29 | public void GivenACartWhenAddAProductThenProductAddedEvent() 30 | { 31 | var cart = new Cart(DefaultCartId, DefaultCustomerId); 32 | ClearUncommittedEvents(cart); 33 | 34 | cart.AddProduct(DefaultProductId, 2); 35 | 36 | AssertSingleUncommittedEvent(cart, @event => 37 | { 38 | Assert.Equal(DefaultProductId, @event.ProductId); 39 | Assert.Equal(2, @event.Quantity); 40 | Assert.Equal(DefaultCartId, @event.AggregateId); 41 | Assert.Equal(0, @event.AggregateVersion); 42 | }); 43 | } 44 | 45 | [Fact] 46 | public void GivenACartWithAProductWhenAddingTheSameProductThenThrowsCartException() 47 | { 48 | var cart = new Cart(DefaultCartId, DefaultCustomerId); 49 | 50 | cart.AddProduct(DefaultProductId, 2); 51 | ClearUncommittedEvents(cart); 52 | 53 | Assert.Throws(() => { cart.AddProduct(DefaultProductId, 1); }); 54 | Assert.Empty(GetUncommittedEventsOf(cart)); 55 | } 56 | 57 | [Fact] 58 | public void GivenACartWithAProductWhenChangingQuantityThenProductQuantityChangedEvent() 59 | { 60 | var cart = new Cart(DefaultCartId, DefaultCustomerId); 61 | 62 | cart.AddProduct(DefaultProductId, 2); 63 | ClearUncommittedEvents(cart); 64 | cart.ChangeProductQuantity(DefaultProductId, 3); 65 | AssertSingleUncommittedEvent(cart, @event => 66 | { 67 | Assert.Equal(DefaultProductId, @event.ProductId); 68 | Assert.Equal(2, @event.OldQuantity); 69 | Assert.Equal(3, @event.NewQuantity); 70 | Assert.Equal(DefaultCartId, @event.AggregateId); 71 | Assert.Equal(1, @event.AggregateVersion); 72 | }); 73 | } 74 | 75 | [Fact] 76 | public void GivenACartWhenChangingQuantityOfAMissingProductThenThrowsCartException() 77 | { 78 | var cart = new Cart(DefaultCartId, DefaultCustomerId); 79 | 80 | Assert.Throws(() => { cart.ChangeProductQuantity(DefaultProductId, 3); }); 81 | } 82 | 83 | [Fact] 84 | public void GivenAnEmptyCarWhenAddingAProductAndRequestedQuantityIsGreaterThanProductThresholdThenThrowsCartException() 85 | { 86 | var cart = new Cart(DefaultCartId, DefaultCustomerId); 87 | 88 | Assert.Throws(() => { cart.AddProduct(DefaultProductId, 51); }); 89 | } 90 | 91 | [Fact] 92 | public void GivenACartWithAProductWhenRequestedQuantityIsGreaterThanProductThresholdThenThrowsCartException() 93 | { 94 | var cart = new Cart(DefaultCartId, DefaultCustomerId); 95 | 96 | cart.AddProduct(DefaultProductId, 1); 97 | Assert.Throws(() => { cart.ChangeProductQuantity(DefaultProductId, 51); }); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /EventSourcingCQRS.Application.Tests/TransientDomainEventPublisherTest.cs: -------------------------------------------------------------------------------- 1 | using EventSourcingCQRS.Application.PubSub; 2 | using System; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace EventSourcingCQRS.Application.Tests 7 | { 8 | [Trait("Type", "Unit")] 9 | public class TransientDomainEventPublisherTest 10 | { 11 | private readonly Event publishedEvent = new Event(); 12 | private TransientDomainEventPubSub sut; 13 | 14 | public TransientDomainEventPublisherTest() 15 | { 16 | sut = new TransientDomainEventPubSub(); 17 | } 18 | 19 | [Fact] 20 | public async Task ShouldInvokeSubscriber() 21 | { 22 | //Arrange 23 | Event receivedEvent = null; 24 | sut.Subscribe(@event => 25 | { 26 | receivedEvent = @event; 27 | }); 28 | //Act 29 | await sut.PublishAsync(publishedEvent); 30 | //Assert 31 | Assert.Same(publishedEvent, receivedEvent); 32 | } 33 | 34 | [Fact] 35 | public async Task ShouldInvokeAllSubscribers() 36 | { 37 | //Arrange 38 | Event receivedEvent1 = null; 39 | Event receivedEvent2 = null; 40 | sut.Subscribe(@event => 41 | { 42 | receivedEvent1 = @event; 43 | }); 44 | sut.Subscribe(@event => 45 | { 46 | receivedEvent2 = @event; 47 | }); 48 | //Act 49 | await sut.PublishAsync(publishedEvent); 50 | //Assert 51 | Assert.Same(publishedEvent, receivedEvent1); 52 | Assert.Same(publishedEvent, receivedEvent2); 53 | } 54 | 55 | [Fact] 56 | public async Task ShouldNotInvokeSubscribersWhenTheEventIsNotOfTheExpectedType() 57 | { 58 | //Arrange 59 | Event receivedEvent = null; 60 | sut.Subscribe(@event => 61 | { 62 | receivedEvent = @event; 63 | }); 64 | //Act 65 | await sut.PublishAsync(new AnotherEvent()); 66 | //Assert 67 | Assert.Null(receivedEvent); 68 | } 69 | 70 | [Fact] 71 | public async Task ShouldNotInvokeSubscribersOnceDispose() 72 | { 73 | //Arrange 74 | Event receivedEvent = null; 75 | sut.Subscribe(@event => 76 | { 77 | receivedEvent = @event; 78 | }); 79 | //Act 80 | sut.Dispose(); 81 | await sut.PublishAsync(publishedEvent); 82 | //Assert 83 | Assert.Null(receivedEvent); 84 | } 85 | 86 | [Fact] 87 | public void ShouldNotInvokeSubscribersIfPublishIsOnADifferentScope() 88 | { 89 | Event receivedEvent = null; 90 | Task.WhenAll(Task.Run(() => 91 | { 92 | sut.Subscribe(@event => 93 | { 94 | receivedEvent = @event; 95 | }); 96 | }), 97 | Task.Run(async () => 98 | { 99 | await sut.PublishAsync(publishedEvent); 100 | }) 101 | ).Wait(); 102 | //Assert 103 | Assert.Null(receivedEvent); 104 | } 105 | 106 | [Fact] 107 | public async Task ShouldInvokeAllSubscribersWhenAnyThrowsException() 108 | { 109 | //Arrange 110 | Event receivedEvent2 = null; 111 | sut.Subscribe(@event => 112 | { 113 | throw new Exception("Failing operation"); 114 | }); 115 | sut.Subscribe(@event => 116 | { 117 | receivedEvent2 = @event; 118 | }); 119 | //Act 120 | await sut.PublishAsync(publishedEvent); 121 | //Assert 122 | Assert.Same(publishedEvent, receivedEvent2); 123 | } 124 | 125 | [Fact] 126 | public async Task ShouldWaitAsyncHandler() 127 | { 128 | //Arrange 129 | Event receivedEvent = null; 130 | sut.Subscribe(async @event => 131 | { 132 | await Task.Delay(100); 133 | receivedEvent = @event; 134 | }); 135 | //Act 136 | await sut.PublishAsync(publishedEvent); 137 | //Assert 138 | Assert.Same(publishedEvent, receivedEvent); 139 | } 140 | 141 | public class Event 142 | { 143 | } 144 | 145 | public class AnotherEvent 146 | { 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js: -------------------------------------------------------------------------------- 1 | /* 2 | ** Unobtrusive validation support library for jQuery and jQuery Validate 3 | ** Copyright (C) Microsoft Corporation. All rights reserved. 4 | */ 5 | !function(a){function e(a,e,n){a.rules[e]=n,a.message&&(a.messages[e]=a.message)}function n(a){return a.replace(/^\s+|\s+$/g,"").split(/\s*,\s*/g)}function t(a){return a.replace(/([!"#$%&'()*+,./:;<=>?@\[\\\]^`{|}~])/g,"\\$1")}function r(a){return a.substr(0,a.lastIndexOf(".")+1)}function i(a,e){return 0===a.indexOf("*.")&&(a=a.replace("*.",e)),a}function o(e,n){var r=a(this).find("[data-valmsg-for='"+t(n[0].name)+"']"),i=r.attr("data-valmsg-replace"),o=i?a.parseJSON(i)!==!1:null;r.removeClass("field-validation-valid").addClass("field-validation-error"),e.data("unobtrusiveContainer",r),o?(r.empty(),e.removeClass("input-validation-error").appendTo(r)):e.hide()}function d(e,n){var t=a(this).find("[data-valmsg-summary=true]"),r=t.find("ul");r&&r.length&&n.errorList.length&&(r.empty(),t.addClass("validation-summary-errors").removeClass("validation-summary-valid"),a.each(n.errorList,function(){a("
  • ").html(this.message).appendTo(r)}))}function s(e){var n=e.data("unobtrusiveContainer");if(n){var t=n.attr("data-valmsg-replace"),r=t?a.parseJSON(t):null;n.addClass("field-validation-valid").removeClass("field-validation-error"),e.removeData("unobtrusiveContainer"),r&&n.empty()}}function l(e){var n=a(this),t="__jquery_unobtrusive_validation_form_reset";if(!n.data(t)){n.data(t,!0);try{n.data("validator").resetForm()}finally{n.removeData(t)}n.find(".validation-summary-errors").addClass("validation-summary-valid").removeClass("validation-summary-errors"),n.find(".field-validation-error").addClass("field-validation-valid").removeClass("field-validation-error").removeData("unobtrusiveContainer").find(">*").removeData("unobtrusiveContainer")}}function m(e){var n=a(e),t=n.data(v),r=a.proxy(l,e),i=p.unobtrusive.options||{},m=function(n,t){var r=i[n];r&&a.isFunction(r)&&r.apply(e,t)};return t||(t={options:{errorClass:i.errorClass||"input-validation-error",errorElement:i.errorElement||"span",errorPlacement:function(){o.apply(e,arguments),m("errorPlacement",arguments)},invalidHandler:function(){d.apply(e,arguments),m("invalidHandler",arguments)},messages:{},rules:{},success:function(){s.apply(e,arguments),m("success",arguments)}},attachValidation:function(){n.off("reset."+v,r).on("reset."+v,r).validate(this.options)},validate:function(){return n.validate(),n.valid()}},n.data(v,t)),t}var u,p=a.validator,v="unobtrusiveValidation";p.unobtrusive={adapters:[],parseElement:function(e,n){var t,r,i,o=a(e),d=o.parents("form")[0];d&&(t=m(d),t.options.rules[e.name]=r={},t.options.messages[e.name]=i={},a.each(this.adapters,function(){var n="data-val-"+this.name,t=o.attr(n),s={};void 0!==t&&(n+="-",a.each(this.params,function(){s[this]=o.attr(n+this)}),this.adapt({element:e,form:d,message:t,params:s,rules:r,messages:i}))}),a.extend(r,{__dummy__:!0}),n||t.attachValidation())},parse:function(e){var n=a(e),t=n.parents().addBack().filter("form").add(n.find("form")).has("[data-val=true]");n.find("[data-val=true]").each(function(){p.unobtrusive.parseElement(this,!0)}),t.each(function(){var a=m(this);a&&a.attachValidation()})}},u=p.unobtrusive.adapters,u.add=function(a,e,n){return n||(n=e,e=[]),this.push({name:a,params:e,adapt:n}),this},u.addBool=function(a,n){return this.add(a,function(t){e(t,n||a,!0)})},u.addMinMax=function(a,n,t,r,i,o){return this.add(a,[i||"min",o||"max"],function(a){var i=a.params.min,o=a.params.max;i&&o?e(a,r,[i,o]):i?e(a,n,i):o&&e(a,t,o)})},u.addSingleVal=function(a,n,t){return this.add(a,[n||"val"],function(r){e(r,t||a,r.params[n])})},p.addMethod("__dummy__",function(a,e,n){return!0}),p.addMethod("regex",function(a,e,n){var t;return this.optional(e)?!0:(t=new RegExp(n).exec(a),t&&0===t.index&&t[0].length===a.length)}),p.addMethod("nonalphamin",function(a,e,n){var t;return n&&(t=a.match(/\W/g),t=t&&t.length>=n),t}),p.methods.extension?(u.addSingleVal("accept","mimtype"),u.addSingleVal("extension","extension")):u.addSingleVal("extension","extension","accept"),u.addSingleVal("regex","pattern"),u.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url"),u.addMinMax("length","minlength","maxlength","rangelength").addMinMax("range","min","max","range"),u.addMinMax("minlength","minlength").addMinMax("maxlength","minlength","maxlength"),u.add("equalto",["other"],function(n){var o=r(n.element.name),d=n.params.other,s=i(d,o),l=a(n.form).find(":input").filter("[name='"+t(s)+"']")[0];e(n,"equalTo",l)}),u.add("required",function(a){("INPUT"!==a.element.tagName.toUpperCase()||"CHECKBOX"!==a.element.type.toUpperCase())&&e(a,"required",!0)}),u.add("remote",["url","type","additionalfields"],function(o){var d={url:o.params.url,type:o.params.type||"GET",data:{}},s=r(o.element.name);a.each(n(o.params.additionalfields||o.element.name),function(e,n){var r=i(n,s);d.data[r]=function(){var e=a(o.form).find(":input").filter("[name='"+t(r)+"']");return e.is(":checkbox")?e.filter(":checked").val()||e.filter(":hidden").val()||"":e.is(":radio")?e.filter(":checked").val()||"":e.val()}}),e(o,"remote",d)}),u.add("password",["min","nonalphamin","regex"],function(a){a.params.min&&e(a,"minlength",a.params.min),a.params.nonalphamin&&e(a,"nonalphamin",a.params.nonalphamin),a.params.regex&&e(a,"regex",a.params.regex)}),a(function(){p.unobtrusive.parse(document)})}(jQuery); -------------------------------------------------------------------------------- /EventSourcingCQRS/Views/Carts/DetailsAsync.cshtml: -------------------------------------------------------------------------------- 1 | @model EventSourcingCQRS.Models.CartDetailsViewModel 2 | @{ 3 | ViewData["Title"] = "Cart detail"; 4 | Layout = "~/Views/Shared/_Layout.cshtml"; 5 | 6 | int index = 0; 7 | } 8 | 9 |

    @ViewData["Title"]

    10 | 11 |
    12 |
    13 | 14 |

    @Model.Cart.CustomerName

    15 |
    16 |
    17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | @foreach (var cartItem in Model.CartItems) 28 | { 29 | 30 | 33 | 34 | 35 | 41 | 42 | } 43 | 44 |
    #Product NameQuantity
    31 | @(++index) 32 | @cartItem.ProductName@cartItem.Quantity 36 | 40 |
    45 |
    46 |
    47 | 50 |
    51 |
    52 | 77 | 103 | @section Scripts { 104 | 113 | } -------------------------------------------------------------------------------- /EventSourcingCQRS.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2003 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcingCQRS", "EventSourcingCQRS\EventSourcingCQRS.csproj", "{61501D72-2100-42C8-BC11-95ACD109EB9D}" 7 | EndProject 8 | Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{A6358068-E812-4129-91AF-3F57C5D456A5}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcingCQRS.Domain", "EventSourcingCQRS.Domain\EventSourcingCQRS.Domain.csproj", "{26EA3D8C-7D19-44E6-B00F-9D9CBF10A695}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcingCQRS.Domain.Tests", "EventSourcingCQRS.Domain.Tests\EventSourcingCQRS.Domain.Tests.csproj", "{2510D09E-4F63-416D-B168-39CCE50EFB4C}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcingCQRS.Domain.EventStore", "EventSourcingCQRS.Domain.EventStore\EventSourcingCQRS.Domain.EventStore.csproj", "{46F0534F-B627-4B76-B887-6D0B594FFEB2}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcingCQRS.Domain.EventStore.Tests", "EventSourcingCQRS.Domain.EventStore.Tests\EventSourcingCQRS.Domain.EventStore.Tests.csproj", "{A4867FC5-06E4-494A-8707-5829B7A34B67}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcingCQRS.ReadModel", "EventSourcingCQRS.ReadModel\EventSourcingCQRS.ReadModel.csproj", "{2B62F256-66D9-4E3A-A2C9-520D2039FD27}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EventSourcingCQRS.ReadModel.Tests", "EventSourcingCQRS.ReadModel.Tests\EventSourcingCQRS.ReadModel.Tests.csproj", "{B38E0FEB-43B2-41AD-AA3F-53B8F4C7C718}" 21 | EndProject 22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventSourcingCQRS.Application", "EventSourcingCQRS.Application\EventSourcingCQRS.Application.csproj", "{F9F8DA44-C49F-4398-B9A6-76BCDCDDB1ED}" 23 | EndProject 24 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EventSourcingCQRS.Application.Tests", "EventSourcingCQRS.Application.Tests\EventSourcingCQRS.Application.Tests.csproj", "{AF725926-4CDE-4D77-9DD9-6984AAFC7DAD}" 25 | EndProject 26 | Global 27 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 28 | Debug|Any CPU = Debug|Any CPU 29 | Release|Any CPU = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 32 | {61501D72-2100-42C8-BC11-95ACD109EB9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {61501D72-2100-42C8-BC11-95ACD109EB9D}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {61501D72-2100-42C8-BC11-95ACD109EB9D}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {61501D72-2100-42C8-BC11-95ACD109EB9D}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {A6358068-E812-4129-91AF-3F57C5D456A5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {A6358068-E812-4129-91AF-3F57C5D456A5}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {A6358068-E812-4129-91AF-3F57C5D456A5}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {A6358068-E812-4129-91AF-3F57C5D456A5}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {26EA3D8C-7D19-44E6-B00F-9D9CBF10A695}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {26EA3D8C-7D19-44E6-B00F-9D9CBF10A695}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {26EA3D8C-7D19-44E6-B00F-9D9CBF10A695}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {26EA3D8C-7D19-44E6-B00F-9D9CBF10A695}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {2510D09E-4F63-416D-B168-39CCE50EFB4C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {2510D09E-4F63-416D-B168-39CCE50EFB4C}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {2510D09E-4F63-416D-B168-39CCE50EFB4C}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {2510D09E-4F63-416D-B168-39CCE50EFB4C}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {46F0534F-B627-4B76-B887-6D0B594FFEB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {46F0534F-B627-4B76-B887-6D0B594FFEB2}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {46F0534F-B627-4B76-B887-6D0B594FFEB2}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {46F0534F-B627-4B76-B887-6D0B594FFEB2}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {A4867FC5-06E4-494A-8707-5829B7A34B67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {A4867FC5-06E4-494A-8707-5829B7A34B67}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {A4867FC5-06E4-494A-8707-5829B7A34B67}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {A4867FC5-06E4-494A-8707-5829B7A34B67}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {2B62F256-66D9-4E3A-A2C9-520D2039FD27}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {2B62F256-66D9-4E3A-A2C9-520D2039FD27}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {2B62F256-66D9-4E3A-A2C9-520D2039FD27}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {2B62F256-66D9-4E3A-A2C9-520D2039FD27}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {B38E0FEB-43B2-41AD-AA3F-53B8F4C7C718}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {B38E0FEB-43B2-41AD-AA3F-53B8F4C7C718}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {B38E0FEB-43B2-41AD-AA3F-53B8F4C7C718}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {B38E0FEB-43B2-41AD-AA3F-53B8F4C7C718}.Release|Any CPU.Build.0 = Release|Any CPU 64 | {F9F8DA44-C49F-4398-B9A6-76BCDCDDB1ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 65 | {F9F8DA44-C49F-4398-B9A6-76BCDCDDB1ED}.Debug|Any CPU.Build.0 = Debug|Any CPU 66 | {F9F8DA44-C49F-4398-B9A6-76BCDCDDB1ED}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {F9F8DA44-C49F-4398-B9A6-76BCDCDDB1ED}.Release|Any CPU.Build.0 = Release|Any CPU 68 | {AF725926-4CDE-4D77-9DD9-6984AAFC7DAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 69 | {AF725926-4CDE-4D77-9DD9-6984AAFC7DAD}.Debug|Any CPU.Build.0 = Debug|Any CPU 70 | {AF725926-4CDE-4D77-9DD9-6984AAFC7DAD}.Release|Any CPU.ActiveCfg = Release|Any CPU 71 | {AF725926-4CDE-4D77-9DD9-6984AAFC7DAD}.Release|Any CPU.Build.0 = Release|Any CPU 72 | EndGlobalSection 73 | GlobalSection(SolutionProperties) = preSolution 74 | HideSolutionNode = FALSE 75 | EndGlobalSection 76 | GlobalSection(ExtensibilityGlobals) = postSolution 77 | SolutionGuid = {8970F73D-55B9-4EF3-8EBC-1A4E60B03428} 78 | EndGlobalSection 79 | EndGlobal 80 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | # NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | # .NET Core 46 | project.lock.json 47 | project.fragment.lock.json 48 | artifacts/ 49 | **/Properties/launchSettings.json 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # Visual Studio code coverage results 117 | *.coverage 118 | *.coveragexml 119 | 120 | # NCrunch 121 | _NCrunch_* 122 | .*crunch*.local.xml 123 | nCrunchTemp_* 124 | 125 | # MightyMoose 126 | *.mm.* 127 | AutoTest.Net/ 128 | 129 | # Web workbench (sass) 130 | .sass-cache/ 131 | 132 | # Installshield output folder 133 | [Ee]xpress/ 134 | 135 | # DocProject is a documentation generator add-in 136 | DocProject/buildhelp/ 137 | DocProject/Help/*.HxT 138 | DocProject/Help/*.HxC 139 | DocProject/Help/*.hhc 140 | DocProject/Help/*.hhk 141 | DocProject/Help/*.hhp 142 | DocProject/Help/Html2 143 | DocProject/Help/html 144 | 145 | # Click-Once directory 146 | publish/ 147 | 148 | # Publish Web Output 149 | *.[Pp]ublish.xml 150 | *.azurePubxml 151 | # TODO: Comment the next line if you want to checkin your web deploy settings 152 | # but database connection strings (with potential passwords) will be unencrypted 153 | *.pubxml 154 | *.publishproj 155 | 156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 157 | # checkin your Azure Web App publish settings, but sensitive information contained 158 | # in these scripts will be unencrypted 159 | PublishScripts/ 160 | 161 | # NuGet Packages 162 | *.nupkg 163 | # The packages folder can be ignored because of Package Restore 164 | **/packages/* 165 | # except build/, which is used as an MSBuild target. 166 | !**/packages/build/ 167 | # Uncomment if necessary however generally it will be regenerated when needed 168 | #!**/packages/repositories.config 169 | # NuGet v3's project.json files produces more ignorable files 170 | *.nuget.props 171 | *.nuget.targets 172 | 173 | # Microsoft Azure Build Output 174 | csx/ 175 | *.build.csdef 176 | 177 | # Microsoft Azure Emulator 178 | ecf/ 179 | rcf/ 180 | 181 | # Windows Store app package directories and files 182 | AppPackages/ 183 | BundleArtifacts/ 184 | Package.StoreAssociation.xml 185 | _pkginfo.txt 186 | 187 | # Visual Studio cache files 188 | # files ending in .cache can be ignored 189 | *.[Cc]ache 190 | # but keep track of directories ending in .cache 191 | !*.[Cc]ache/ 192 | 193 | # Others 194 | ClientBin/ 195 | ~$* 196 | *~ 197 | *.dbmdl 198 | *.dbproj.schemaview 199 | *.jfm 200 | *.pfx 201 | *.publishsettings 202 | orleans.codegen.cs 203 | 204 | # Since there are multiple workflows, uncomment next line to ignore bower_components 205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 206 | #bower_components/ 207 | 208 | # RIA/Silverlight projects 209 | Generated_Code/ 210 | 211 | # Backup & report files from converting an old project file 212 | # to a newer Visual Studio version. Backup files are not needed, 213 | # because we have git ;-) 214 | _UpgradeReport_Files/ 215 | Backup*/ 216 | UpgradeLog*.XML 217 | UpgradeLog*.htm 218 | 219 | # SQL Server files 220 | *.mdf 221 | *.ldf 222 | *.ndf 223 | 224 | # Business Intelligence projects 225 | *.rdl.data 226 | *.bim.layout 227 | *.bim_*.settings 228 | 229 | # Microsoft Fakes 230 | FakesAssemblies/ 231 | 232 | # GhostDoc plugin setting file 233 | *.GhostDoc.xml 234 | 235 | # Node.js Tools for Visual Studio 236 | .ntvs_analysis.dat 237 | node_modules/ 238 | 239 | # Typescript v1 declaration files 240 | typings/ 241 | 242 | # Visual Studio 6 build log 243 | *.plg 244 | 245 | # Visual Studio 6 workspace options file 246 | *.opt 247 | 248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 249 | *.vbw 250 | 251 | # Visual Studio LightSwitch build output 252 | **/*.HTMLClient/GeneratedArtifacts 253 | **/*.DesktopClient/GeneratedArtifacts 254 | **/*.DesktopClient/ModelManifest.xml 255 | **/*.Server/GeneratedArtifacts 256 | **/*.Server/ModelManifest.xml 257 | _Pvt_Extensions 258 | 259 | # Paket dependency manager 260 | .paket/paket.exe 261 | paket-files/ 262 | 263 | # FAKE - F# Make 264 | .fake/ 265 | 266 | # JetBrains Rider 267 | .idea/ 268 | *.sln.iml 269 | 270 | # CodeRush 271 | .cr/ 272 | 273 | # Python Tools for Visual Studio (PTVS) 274 | __pycache__/ 275 | *.pyc 276 | 277 | # Cake - Uncomment if you are using it 278 | # tools/** 279 | # !tools/packages.config 280 | 281 | # Telerik's JustMock configuration file 282 | *.jmconfig 283 | 284 | # BizTalk build output 285 | *.btp.cs 286 | *.btm.cs 287 | *.odx.cs 288 | *.xsd.cs 289 | -------------------------------------------------------------------------------- /EventSourcingCQRS/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using EventStore.ClientAPI; 7 | using EventSourcingCQRS.Domain.CartModule; 8 | using EventSourcingCQRS.Domain.Persistence; 9 | using EventSourcingCQRS.Domain.PubSub; 10 | using EventSourcingCQRS.Domain.EventStore; 11 | using EventSourcingCQRS.Domain.Persistence.EventStore; 12 | using MongoDB.Driver; 13 | using EventSourcingCQRS.Domain.Core; 14 | using EventSourcingCQRS.ReadModel.Persistence; 15 | using ReadCart = EventSourcingCQRS.ReadModel.Cart.Cart; 16 | using ReadCartItem = EventSourcingCQRS.ReadModel.Cart.CartItem; 17 | using System.Linq; 18 | using EventSourcingCQRS.ReadModel.Product; 19 | using System.Threading.Tasks; 20 | using EventSourcingCQRS.ReadModel.Customer; 21 | using EventSourcingCQRS.Application.Services; 22 | using EventSourcingCQRS.Application.PubSub; 23 | using EventSourcingCQRS.Application.Handlers; 24 | 25 | namespace EventSourcingCQRS 26 | { 27 | public class Startup 28 | { 29 | private const string ReadModelDBName = "ReadModel"; 30 | 31 | public Startup(IConfiguration configuration) 32 | { 33 | Configuration = configuration; 34 | } 35 | 36 | public IConfiguration Configuration { get; } 37 | 38 | // This method gets called by the runtime. Use this method to add services to the container. 39 | public void ConfigureServices(IServiceCollection services) 40 | { 41 | services.AddMvc(); 42 | services.AddSingleton(x => EventStoreConnection.Create(new Uri("tcp://eventstore:1113"))); 43 | services.AddTransient(); 44 | services.AddTransient(); 45 | services.AddTransient, EventSourcingRepository>(); 46 | services.AddSingleton(); 47 | services.AddSingleton(x => new MongoClient("mongodb://mongo:27017")); 48 | services.AddSingleton(x => x.GetService().GetDatabase(ReadModelDBName)); 49 | services.AddTransient, MongoDBRepository>(); 50 | services.AddTransient, MongoDBRepository>(); 51 | services.AddTransient, MongoDBRepository>(); 52 | services.AddTransient, MongoDBRepository>(); 53 | services.AddTransient, MongoDBRepository>(); 54 | services.AddTransient, MongoDBRepository>(); 55 | services.AddTransient, MongoDBRepository>(); 56 | services.AddTransient, MongoDBRepository>(); 57 | services.AddTransient, CartUpdater>(); 58 | services.AddTransient, CartUpdater>(); 59 | services.AddTransient, CartUpdater>(); 60 | services.AddTransient(); 61 | services.AddTransient(); 62 | } 63 | 64 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 65 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, 66 | IEventStoreConnection conn, IRepository productRepository, 67 | IRepository customerRepository, MongoClient mongoClient) 68 | { 69 | if (env.IsDevelopment()) 70 | { 71 | app.UseDeveloperExceptionPage(); 72 | app.UseBrowserLink(); 73 | } 74 | else 75 | { 76 | app.UseExceptionHandler("/Home/Error"); 77 | } 78 | 79 | app.UseStaticFiles(); 80 | 81 | app.UseMvc(routes => 82 | { 83 | routes.MapRoute( 84 | name: "default", 85 | template: "{controller=Carts}/{action=IndexAsync}/{id?}"); 86 | }); 87 | 88 | conn.ConnectAsync().Wait(); 89 | 90 | if (!productRepository.FindAllAsync(x => true).Result.Any() && 91 | !customerRepository.FindAllAsync(x => true).Result.Any()) 92 | { 93 | SeedReadModel(productRepository, customerRepository); 94 | } 95 | } 96 | 97 | private void SeedReadModel(IRepository productRepository, IRepository customerRepository) 98 | { 99 | var insertingProducts = new [] { 100 | new Product 101 | { 102 | Id = $"Product-{Guid.NewGuid().ToString()}", 103 | Name = "Laptop" 104 | }, 105 | new Product 106 | { 107 | Id = $"Product-{Guid.NewGuid().ToString()}", 108 | Name = "Smartphone" 109 | }, 110 | new Product 111 | { 112 | Id = $"Product-{Guid.NewGuid().ToString()}", 113 | Name = "Gaming PC" 114 | }, 115 | new Product 116 | { 117 | Id = $"Product-{Guid.NewGuid().ToString()}", 118 | Name = "Microwave oven" 119 | }, 120 | } 121 | .Select(x => productRepository.InsertAsync(x)); 122 | 123 | var insertingCustomers = new Customer[] { 124 | new Customer 125 | { 126 | Id = $"Customer-{Guid.NewGuid().ToString()}", 127 | Name = "Andrea" 128 | }, 129 | new Customer 130 | { 131 | Id = $"Customer-{Guid.NewGuid().ToString()}", 132 | Name = "Martina" 133 | }, 134 | } 135 | .Select(x => customerRepository.InsertAsync(x)); 136 | 137 | Task.WaitAll(insertingProducts.Union(insertingCustomers).ToArray()); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/images/banner2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/images/banner1.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/images/banner3.svg: -------------------------------------------------------------------------------- 1 | banner3b -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/images/banner4.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /EventSourcingCQRS/wwwroot/lib/jquery-validation/dist/additional-methods.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery Validation Plugin - v1.14.0 - 6/30/2015 2 | * http://jqueryvalidation.org/ 3 | * Copyright (c) 2015 Jörn Zaefferer; Licensed MIT */ 4 | !function(a){"function"==typeof define&&define.amd?define(["jquery","./jquery.validate.min"],a):a(jQuery)}(function(a){!function(){function b(a){return a.replace(/<.[^<>]*?>/g," ").replace(/ | /gi," ").replace(/[.(),;:!?%#$'\"_+=\/\-“”’]*/g,"")}a.validator.addMethod("maxWords",function(a,c,d){return this.optional(c)||b(a).match(/\b\w+\b/g).length<=d},a.validator.format("Please enter {0} words or less.")),a.validator.addMethod("minWords",function(a,c,d){return this.optional(c)||b(a).match(/\b\w+\b/g).length>=d},a.validator.format("Please enter at least {0} words.")),a.validator.addMethod("rangeWords",function(a,c,d){var e=b(a),f=/\b\w+\b/g;return this.optional(c)||e.match(f).length>=d[0]&&e.match(f).length<=d[1]},a.validator.format("Please enter between {0} and {1} words."))}(),a.validator.addMethod("accept",function(b,c,d){var e,f,g="string"==typeof d?d.replace(/\s/g,"").replace(/,/g,"|"):"image/*",h=this.optional(c);if(h)return h;if("file"===a(c).attr("type")&&(g=g.replace(/\*/g,".*"),c.files&&c.files.length))for(e=0;ec;c++)d=h-c,e=f.substring(c,c+1),g+=d*e;return g%11===0},"Please specify a valid bank account number"),a.validator.addMethod("bankorgiroaccountNL",function(b,c){return this.optional(c)||a.validator.methods.bankaccountNL.call(this,b,c)||a.validator.methods.giroaccountNL.call(this,b,c)},"Please specify a valid bank or giro account number"),a.validator.addMethod("bic",function(a,b){return this.optional(b)||/^([A-Z]{6}[A-Z2-9][A-NP-Z1-2])(X{3}|[A-WY-Z0-9][A-Z0-9]{2})?$/.test(a)},"Please specify a valid BIC code"),a.validator.addMethod("cifES",function(a){"use strict";var b,c,d,e,f,g,h=[];if(a=a.toUpperCase(),!a.match("((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)"))return!1;for(d=0;9>d;d++)h[d]=parseInt(a.charAt(d),10);for(c=h[2]+h[4]+h[6],e=1;8>e;e+=2)f=(2*h[e]).toString(),g=f.charAt(1),c+=parseInt(f.charAt(0),10)+(""===g?0:parseInt(g,10));return/^[ABCDEFGHJNPQRSUVW]{1}/.test(a)?(c+="",b=10-parseInt(c.charAt(c.length-1),10),a+=b,h[8].toString()===String.fromCharCode(64+b)||h[8].toString()===a.charAt(a.length-1)):!1},"Please specify a valid CIF number."),a.validator.addMethod("cpfBR",function(a){if(a=a.replace(/([~!@#$%^&*()_+=`{}\[\]\-|\\:;'<>,.\/? ])+/g,""),11!==a.length)return!1;var b,c,d,e,f=0;if(b=parseInt(a.substring(9,10),10),c=parseInt(a.substring(10,11),10),d=function(a,b){var c=10*a%11;return(10===c||11===c)&&(c=0),c===b},""===a||"00000000000"===a||"11111111111"===a||"22222222222"===a||"33333333333"===a||"44444444444"===a||"55555555555"===a||"66666666666"===a||"77777777777"===a||"88888888888"===a||"99999999999"===a)return!1;for(e=1;9>=e;e++)f+=parseInt(a.substring(e-1,e),10)*(11-e);if(d(f,b)){for(f=0,e=1;10>=e;e++)f+=parseInt(a.substring(e-1,e),10)*(12-e);return d(f,c)}return!1},"Please specify a valid CPF number"),a.validator.addMethod("creditcardtypes",function(a,b,c){if(/[^0-9\-]+/.test(a))return!1;a=a.replace(/\D/g,"");var d=0;return c.mastercard&&(d|=1),c.visa&&(d|=2),c.amex&&(d|=4),c.dinersclub&&(d|=8),c.enroute&&(d|=16),c.discover&&(d|=32),c.jcb&&(d|=64),c.unknown&&(d|=128),c.all&&(d=255),1&d&&/^(5[12345])/.test(a)?16===a.length:2&d&&/^(4)/.test(a)?16===a.length:4&d&&/^(3[47])/.test(a)?15===a.length:8&d&&/^(3(0[012345]|[68]))/.test(a)?14===a.length:16&d&&/^(2(014|149))/.test(a)?15===a.length:32&d&&/^(6011)/.test(a)?16===a.length:64&d&&/^(3)/.test(a)?16===a.length:64&d&&/^(2131|1800)/.test(a)?15===a.length:128&d?!0:!1},"Please enter a valid credit card number."),a.validator.addMethod("currency",function(a,b,c){var d,e="string"==typeof c,f=e?c:c[0],g=e?!0:c[1];return f=f.replace(/,/g,""),f=g?f+"]":f+"]?",d="^["+f+"([1-9]{1}[0-9]{0,2}(\\,[0-9]{3})*(\\.[0-9]{0,2})?|[1-9]{1}[0-9]{0,}(\\.[0-9]{0,2})?|0(\\.[0-9]{0,2})?|(\\.[0-9]{1,2})?)$",d=new RegExp(d),this.optional(b)||d.test(a)},"Please specify a valid currency"),a.validator.addMethod("dateFA",function(a,b){return this.optional(b)||/^[1-4]\d{3}\/((0?[1-6]\/((3[0-1])|([1-2][0-9])|(0?[1-9])))|((1[0-2]|(0?[7-9]))\/(30|([1-2][0-9])|(0?[1-9]))))$/.test(a)},a.validator.messages.date),a.validator.addMethod("dateITA",function(a,b){var c,d,e,f,g,h=!1,i=/^\d{1,2}\/\d{1,2}\/\d{4}$/;return i.test(a)?(c=a.split("/"),d=parseInt(c[0],10),e=parseInt(c[1],10),f=parseInt(c[2],10),g=new Date(Date.UTC(f,e-1,d,12,0,0,0)),h=g.getUTCFullYear()===f&&g.getUTCMonth()===e-1&&g.getUTCDate()===d?!0:!1):h=!1,this.optional(b)||h},a.validator.messages.date),a.validator.addMethod("dateNL",function(a,b){return this.optional(b)||/^(0?[1-9]|[12]\d|3[01])[\.\/\-](0?[1-9]|1[012])[\.\/\-]([12]\d)?(\d\d)$/.test(a)},a.validator.messages.date),a.validator.addMethod("extension",function(a,b,c){return c="string"==typeof c?c.replace(/,/g,"|"):"png|jpe?g|gif",this.optional(b)||a.match(new RegExp("\\.("+c+")$","i"))},a.validator.format("Please enter a value with a valid extension.")),a.validator.addMethod("giroaccountNL",function(a,b){return this.optional(b)||/^[0-9]{1,7}$/.test(a)},"Please specify a valid giro account number"),a.validator.addMethod("iban",function(a,b){if(this.optional(b))return!0;var c,d,e,f,g,h,i,j,k,l=a.replace(/ /g,"").toUpperCase(),m="",n=!0,o="",p="";if(c=l.substring(0,2),h={AL:"\\d{8}[\\dA-Z]{16}",AD:"\\d{8}[\\dA-Z]{12}",AT:"\\d{16}",AZ:"[\\dA-Z]{4}\\d{20}",BE:"\\d{12}",BH:"[A-Z]{4}[\\dA-Z]{14}",BA:"\\d{16}",BR:"\\d{23}[A-Z][\\dA-Z]",BG:"[A-Z]{4}\\d{6}[\\dA-Z]{8}",CR:"\\d{17}",HR:"\\d{17}",CY:"\\d{8}[\\dA-Z]{16}",CZ:"\\d{20}",DK:"\\d{14}",DO:"[A-Z]{4}\\d{20}",EE:"\\d{16}",FO:"\\d{14}",FI:"\\d{14}",FR:"\\d{10}[\\dA-Z]{11}\\d{2}",GE:"[\\dA-Z]{2}\\d{16}",DE:"\\d{18}",GI:"[A-Z]{4}[\\dA-Z]{15}",GR:"\\d{7}[\\dA-Z]{16}",GL:"\\d{14}",GT:"[\\dA-Z]{4}[\\dA-Z]{20}",HU:"\\d{24}",IS:"\\d{22}",IE:"[\\dA-Z]{4}\\d{14}",IL:"\\d{19}",IT:"[A-Z]\\d{10}[\\dA-Z]{12}",KZ:"\\d{3}[\\dA-Z]{13}",KW:"[A-Z]{4}[\\dA-Z]{22}",LV:"[A-Z]{4}[\\dA-Z]{13}",LB:"\\d{4}[\\dA-Z]{20}",LI:"\\d{5}[\\dA-Z]{12}",LT:"\\d{16}",LU:"\\d{3}[\\dA-Z]{13}",MK:"\\d{3}[\\dA-Z]{10}\\d{2}",MT:"[A-Z]{4}\\d{5}[\\dA-Z]{18}",MR:"\\d{23}",MU:"[A-Z]{4}\\d{19}[A-Z]{3}",MC:"\\d{10}[\\dA-Z]{11}\\d{2}",MD:"[\\dA-Z]{2}\\d{18}",ME:"\\d{18}",NL:"[A-Z]{4}\\d{10}",NO:"\\d{11}",PK:"[\\dA-Z]{4}\\d{16}",PS:"[\\dA-Z]{4}\\d{21}",PL:"\\d{24}",PT:"\\d{21}",RO:"[A-Z]{4}[\\dA-Z]{16}",SM:"[A-Z]\\d{10}[\\dA-Z]{12}",SA:"\\d{2}[\\dA-Z]{18}",RS:"\\d{18}",SK:"\\d{20}",SI:"\\d{15}",ES:"\\d{20}",SE:"\\d{20}",CH:"\\d{5}[\\dA-Z]{12}",TN:"\\d{20}",TR:"\\d{5}[\\dA-Z]{17}",AE:"\\d{3}\\d{16}",GB:"[A-Z]{4}\\d{14}",VG:"[\\dA-Z]{4}\\d{16}"},g=h[c],"undefined"!=typeof g&&(i=new RegExp("^[A-Z]{2}\\d{2}"+g+"$",""),!i.test(l)))return!1;for(d=l.substring(4,l.length)+l.substring(0,4),j=0;j9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?|0)7(?:[1345789]\d{2}|624)\s?\d{3}\s?\d{3})$/)},"Please specify a valid mobile number"),a.validator.addMethod("nieES",function(a){"use strict";return a=a.toUpperCase(),a.match("((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)")?/^[T]{1}/.test(a)?a[8]===/^[T]{1}[A-Z0-9]{8}$/.test(a):/^[XYZ]{1}/.test(a)?a[8]==="TRWAGMYFPDXBNJZSQVHLCKE".charAt(a.replace("X","0").replace("Y","1").replace("Z","2").substring(0,8)%23):!1:!1},"Please specify a valid NIE number."),a.validator.addMethod("nifES",function(a){"use strict";return a=a.toUpperCase(),a.match("((^[A-Z]{1}[0-9]{7}[A-Z0-9]{1}$|^[T]{1}[A-Z0-9]{8}$)|^[0-9]{8}[A-Z]{1}$)")?/^[0-9]{8}[A-Z]{1}$/.test(a)?"TRWAGMYFPDXBNJZSQVHLCKE".charAt(a.substring(8,0)%23)===a.charAt(8):/^[KLM]{1}/.test(a)?a[8]===String.fromCharCode(64):!1:!1},"Please specify a valid NIF number."),jQuery.validator.addMethod("notEqualTo",function(b,c,d){return this.optional(c)||!a.validator.methods.equalTo.call(this,b,c,d)},"Please enter a different value, values must not be the same."),a.validator.addMethod("nowhitespace",function(a,b){return this.optional(b)||/^\S+$/i.test(a)},"No white space please"),a.validator.addMethod("pattern",function(a,b,c){return this.optional(b)?!0:("string"==typeof c&&(c=new RegExp("^(?:"+c+")$")),c.test(a))},"Invalid format."),a.validator.addMethod("phoneNL",function(a,b){return this.optional(b)||/^((\+|00(\s|\s?\-\s?)?)31(\s|\s?\-\s?)?(\(0\)[\-\s]?)?|0)[1-9]((\s|\s?\-\s?)?[0-9]){8}$/.test(a)},"Please specify a valid phone number."),a.validator.addMethod("phoneUK",function(a,b){return a=a.replace(/\(|\)|\s+|-/g,""),this.optional(b)||a.length>9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?)|(?:\(?0))(?:\d{2}\)?\s?\d{4}\s?\d{4}|\d{3}\)?\s?\d{3}\s?\d{3,4}|\d{4}\)?\s?(?:\d{5}|\d{3}\s?\d{3})|\d{5}\)?\s?\d{4,5})$/)},"Please specify a valid phone number"),a.validator.addMethod("phoneUS",function(a,b){return a=a.replace(/\s+/g,""),this.optional(b)||a.length>9&&a.match(/^(\+?1-?)?(\([2-9]([02-9]\d|1[02-9])\)|[2-9]([02-9]\d|1[02-9]))-?[2-9]([02-9]\d|1[02-9])-?\d{4}$/)},"Please specify a valid phone number"),a.validator.addMethod("phonesUK",function(a,b){return a=a.replace(/\(|\)|\s+|-/g,""),this.optional(b)||a.length>9&&a.match(/^(?:(?:(?:00\s?|\+)44\s?|0)(?:1\d{8,9}|[23]\d{9}|7(?:[1345789]\d{8}|624\d{6})))$/)},"Please specify a valid uk phone number"),a.validator.addMethod("postalCodeCA",function(a,b){return this.optional(b)||/^[ABCEGHJKLMNPRSTVXY]\d[A-Z] \d[A-Z]\d$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postalcodeBR",function(a,b){return this.optional(b)||/^\d{2}.\d{3}-\d{3}?$|^\d{5}-?\d{3}?$/.test(a)},"Informe um CEP válido."),a.validator.addMethod("postalcodeIT",function(a,b){return this.optional(b)||/^\d{5}$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postalcodeNL",function(a,b){return this.optional(b)||/^[1-9][0-9]{3}\s?[a-zA-Z]{2}$/.test(a)},"Please specify a valid postal code"),a.validator.addMethod("postcodeUK",function(a,b){return this.optional(b)||/^((([A-PR-UWYZ][0-9])|([A-PR-UWYZ][0-9][0-9])|([A-PR-UWYZ][A-HK-Y][0-9])|([A-PR-UWYZ][A-HK-Y][0-9][0-9])|([A-PR-UWYZ][0-9][A-HJKSTUW])|([A-PR-UWYZ][A-HK-Y][0-9][ABEHMNPRVWXY]))\s?([0-9][ABD-HJLNP-UW-Z]{2})|(GIR)\s?(0AA))$/i.test(a)},"Please specify a valid UK postcode"),a.validator.addMethod("require_from_group",function(b,c,d){var e=a(d[1],c.form),f=e.eq(0),g=f.data("valid_req_grp")?f.data("valid_req_grp"):a.extend({},this),h=e.filter(function(){return g.elementValue(this)}).length>=d[0];return f.data("valid_req_grp",g),a(c).data("being_validated")||(e.data("being_validated",!0),e.each(function(){g.element(this)}),e.data("being_validated",!1)),h},a.validator.format("Please fill at least {0} of these fields.")),a.validator.addMethod("skip_or_fill_minimum",function(b,c,d){var e=a(d[1],c.form),f=e.eq(0),g=f.data("valid_skip")?f.data("valid_skip"):a.extend({},this),h=e.filter(function(){return g.elementValue(this)}).length,i=0===h||h>=d[0];return f.data("valid_skip",g),a(c).data("being_validated")||(e.data("being_validated",!0),e.each(function(){g.element(this)}),e.data("being_validated",!1)),i},a.validator.format("Please either skip these fields or fill at least {0} of them.")),a.validator.addMethod("stateUS",function(a,b,c){var d,e="undefined"==typeof c,f=e||"undefined"==typeof c.caseSensitive?!1:c.caseSensitive,g=e||"undefined"==typeof c.includeTerritories?!1:c.includeTerritories,h=e||"undefined"==typeof c.includeMilitary?!1:c.includeMilitary;return d=g||h?g&&h?"^(A[AEKLPRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$":g?"^(A[KLRSZ]|C[AOT]|D[CE]|FL|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEINOPST]|N[CDEHJMVY]|O[HKR]|P[AR]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$":"^(A[AEKLPRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$":"^(A[KLRZ]|C[AOT]|D[CE]|FL|GA|HI|I[ADLN]|K[SY]|LA|M[ADEINOST]|N[CDEHJMVY]|O[HKR]|PA|RI|S[CD]|T[NX]|UT|V[AT]|W[AIVY])$",d=f?new RegExp(d):new RegExp(d,"i"),this.optional(b)||d.test(a)},"Please specify a valid state"),a.validator.addMethod("strippedminlength",function(b,c,d){return a(b).text().length>=d},a.validator.format("Please enter at least {0} characters")),a.validator.addMethod("time",function(a,b){return this.optional(b)||/^([01]\d|2[0-3]|[0-9])(:[0-5]\d){1,2}$/.test(a)},"Please enter a valid time, between 00:00 and 23:59"),a.validator.addMethod("time12h",function(a,b){return this.optional(b)||/^((0?[1-9]|1[012])(:[0-5]\d){1,2}(\ ?[AP]M))$/i.test(a)},"Please enter a valid time in 12-hour am/pm format"),a.validator.addMethod("url2",function(a,b){return this.optional(b)||/^(https?|ftp):\/\/(((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:)*@)?(((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5]))|((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)*(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.?)(:\d*)?)(\/((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)+(\/(([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)*)*)?)?(\?((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|[\uE000-\uF8FF]|\/|\?)*)?(#((([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(%[\da-f]{2})|[!\$&'\(\)\*\+,;=]|:|@)|\/|\?)*)?$/i.test(a)},a.validator.messages.url),a.validator.addMethod("vinUS",function(a){if(17!==a.length)return!1;var b,c,d,e,f,g,h=["A","B","C","D","E","F","G","H","J","K","L","M","N","P","R","S","T","U","V","W","X","Y","Z"],i=[1,2,3,4,5,6,7,8,1,2,3,4,5,7,9,2,3,4,5,6,7,8,9],j=[8,7,6,5,4,3,2,10,0,9,8,7,6,5,4,3,2],k=0;for(b=0;17>b;b++){if(e=j[b],d=a.slice(b,b+1),8===b&&(g=d),isNaN(d)){for(c=0;c").attr("name",c.submitButton.name).val(a(c.submitButton).val()).appendTo(c.currentForm)),e=c.settings.submitHandler.call(c,c.currentForm,b),c.submitButton&&d.remove(),void 0!==e?e:!1):!0}return c.settings.debug&&b.preventDefault(),c.cancelSubmit?(c.cancelSubmit=!1,d()):c.form()?c.pendingRequest?(c.formSubmitted=!0,!1):d():(c.focusInvalid(),!1)})),c)},valid:function(){var b,c,d;return a(this[0]).is("form")?b=this.validate().form():(d=[],b=!0,c=a(this[0].form).validate(),this.each(function(){b=c.element(this)&&b,d=d.concat(c.errorList)}),c.errorList=d),b},rules:function(b,c){var d,e,f,g,h,i,j=this[0];if(b)switch(d=a.data(j.form,"validator").settings,e=d.rules,f=a.validator.staticRules(j),b){case"add":a.extend(f,a.validator.normalizeRule(c)),delete f.messages,e[j.name]=f,c.messages&&(d.messages[j.name]=a.extend(d.messages[j.name],c.messages));break;case"remove":return c?(i={},a.each(c.split(/\s/),function(b,c){i[c]=f[c],delete f[c],"required"===c&&a(j).removeAttr("aria-required")}),i):(delete e[j.name],f)}return g=a.validator.normalizeRules(a.extend({},a.validator.classRules(j),a.validator.attributeRules(j),a.validator.dataRules(j),a.validator.staticRules(j)),j),g.required&&(h=g.required,delete g.required,g=a.extend({required:h},g),a(j).attr("aria-required","true")),g.remote&&(h=g.remote,delete g.remote,g=a.extend(g,{remote:h})),g}}),a.extend(a.expr[":"],{blank:function(b){return!a.trim(""+a(b).val())},filled:function(b){return!!a.trim(""+a(b).val())},unchecked:function(b){return!a(b).prop("checked")}}),a.validator=function(b,c){this.settings=a.extend(!0,{},a.validator.defaults,b),this.currentForm=c,this.init()},a.validator.format=function(b,c){return 1===arguments.length?function(){var c=a.makeArray(arguments);return c.unshift(b),a.validator.format.apply(this,c)}:(arguments.length>2&&c.constructor!==Array&&(c=a.makeArray(arguments).slice(1)),c.constructor!==Array&&(c=[c]),a.each(c,function(a,c){b=b.replace(new RegExp("\\{"+a+"\\}","g"),function(){return c})}),b)},a.extend(a.validator,{defaults:{messages:{},groups:{},rules:{},errorClass:"error",validClass:"valid",errorElement:"label",focusCleanup:!1,focusInvalid:!0,errorContainer:a([]),errorLabelContainer:a([]),onsubmit:!0,ignore:":hidden",ignoreTitle:!1,onfocusin:function(a){this.lastActive=a,this.settings.focusCleanup&&(this.settings.unhighlight&&this.settings.unhighlight.call(this,a,this.settings.errorClass,this.settings.validClass),this.hideThese(this.errorsFor(a)))},onfocusout:function(a){this.checkable(a)||!(a.name in this.submitted)&&this.optional(a)||this.element(a)},onkeyup:function(b,c){var d=[16,17,18,20,35,36,37,38,39,40,45,144,225];9===c.which&&""===this.elementValue(b)||-1!==a.inArray(c.keyCode,d)||(b.name in this.submitted||b===this.lastElement)&&this.element(b)},onclick:function(a){a.name in this.submitted?this.element(a):a.parentNode.name in this.submitted&&this.element(a.parentNode)},highlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).addClass(c).removeClass(d):a(b).addClass(c).removeClass(d)},unhighlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).removeClass(c).addClass(d):a(b).removeClass(c).addClass(d)}},setDefaults:function(b){a.extend(a.validator.defaults,b)},messages:{required:"This field is required.",remote:"Please fix this field.",email:"Please enter a valid email address.",url:"Please enter a valid URL.",date:"Please enter a valid date.",dateISO:"Please enter a valid date ( ISO ).",number:"Please enter a valid number.",digits:"Please enter only digits.",creditcard:"Please enter a valid credit card number.",equalTo:"Please enter the same value again.",maxlength:a.validator.format("Please enter no more than {0} characters."),minlength:a.validator.format("Please enter at least {0} characters."),rangelength:a.validator.format("Please enter a value between {0} and {1} characters long."),range:a.validator.format("Please enter a value between {0} and {1}."),max:a.validator.format("Please enter a value less than or equal to {0}."),min:a.validator.format("Please enter a value greater than or equal to {0}.")},autoCreateRanges:!1,prototype:{init:function(){function b(b){var c=a.data(this.form,"validator"),d="on"+b.type.replace(/^validate/,""),e=c.settings;e[d]&&!a(this).is(e.ignore)&&e[d].call(c,this,b)}this.labelContainer=a(this.settings.errorLabelContainer),this.errorContext=this.labelContainer.length&&this.labelContainer||a(this.currentForm),this.containers=a(this.settings.errorContainer).add(this.settings.errorLabelContainer),this.submitted={},this.valueCache={},this.pendingRequest=0,this.pending={},this.invalid={},this.reset();var c,d=this.groups={};a.each(this.settings.groups,function(b,c){"string"==typeof c&&(c=c.split(/\s/)),a.each(c,function(a,c){d[c]=b})}),c=this.settings.rules,a.each(c,function(b,d){c[b]=a.validator.normalizeRule(d)}),a(this.currentForm).on("focusin.validate focusout.validate keyup.validate",":text, [type='password'], [type='file'], select, textarea, [type='number'], [type='search'], [type='tel'], [type='url'], [type='email'], [type='datetime'], [type='date'], [type='month'], [type='week'], [type='time'], [type='datetime-local'], [type='range'], [type='color'], [type='radio'], [type='checkbox']",b).on("click.validate","select, option, [type='radio'], [type='checkbox']",b),this.settings.invalidHandler&&a(this.currentForm).on("invalid-form.validate",this.settings.invalidHandler),a(this.currentForm).find("[required], [data-rule-required], .required").attr("aria-required","true")},form:function(){return this.checkForm(),a.extend(this.submitted,this.errorMap),this.invalid=a.extend({},this.errorMap),this.valid()||a(this.currentForm).triggerHandler("invalid-form",[this]),this.showErrors(),this.valid()},checkForm:function(){this.prepareForm();for(var a=0,b=this.currentElements=this.elements();b[a];a++)this.check(b[a]);return this.valid()},element:function(b){var c=this.clean(b),d=this.validationTargetFor(c),e=!0;return this.lastElement=d,void 0===d?delete this.invalid[c.name]:(this.prepareElement(d),this.currentElements=a(d),e=this.check(d)!==!1,e?delete this.invalid[d.name]:this.invalid[d.name]=!0),a(b).attr("aria-invalid",!e),this.numberOfInvalids()||(this.toHide=this.toHide.add(this.containers)),this.showErrors(),e},showErrors:function(b){if(b){a.extend(this.errorMap,b),this.errorList=[];for(var c in b)this.errorList.push({message:b[c],element:this.findByName(c)[0]});this.successList=a.grep(this.successList,function(a){return!(a.name in b)})}this.settings.showErrors?this.settings.showErrors.call(this,this.errorMap,this.errorList):this.defaultShowErrors()},resetForm:function(){a.fn.resetForm&&a(this.currentForm).resetForm(),this.submitted={},this.lastElement=null,this.prepareForm(),this.hideErrors();var b,c=this.elements().removeData("previousValue").removeAttr("aria-invalid");if(this.settings.unhighlight)for(b=0;c[b];b++)this.settings.unhighlight.call(this,c[b],this.settings.errorClass,"");else c.removeClass(this.settings.errorClass)},numberOfInvalids:function(){return this.objectLength(this.invalid)},objectLength:function(a){var b,c=0;for(b in a)c++;return c},hideErrors:function(){this.hideThese(this.toHide)},hideThese:function(a){a.not(this.containers).text(""),this.addWrapper(a).hide()},valid:function(){return 0===this.size()},size:function(){return this.errorList.length},focusInvalid:function(){if(this.settings.focusInvalid)try{a(this.findLastActive()||this.errorList.length&&this.errorList[0].element||[]).filter(":visible").focus().trigger("focusin")}catch(b){}},findLastActive:function(){var b=this.lastActive;return b&&1===a.grep(this.errorList,function(a){return a.element.name===b.name}).length&&b},elements:function(){var b=this,c={};return a(this.currentForm).find("input, select, textarea").not(":submit, :reset, :image, :disabled").not(this.settings.ignore).filter(function(){return!this.name&&b.settings.debug&&window.console&&console.error("%o has no name assigned",this),this.name in c||!b.objectLength(a(this).rules())?!1:(c[this.name]=!0,!0)})},clean:function(b){return a(b)[0]},errors:function(){var b=this.settings.errorClass.split(" ").join(".");return a(this.settings.errorElement+"."+b,this.errorContext)},reset:function(){this.successList=[],this.errorList=[],this.errorMap={},this.toShow=a([]),this.toHide=a([]),this.currentElements=a([])},prepareForm:function(){this.reset(),this.toHide=this.errors().add(this.containers)},prepareElement:function(a){this.reset(),this.toHide=this.errorsFor(a)},elementValue:function(b){var c,d=a(b),e=b.type;return"radio"===e||"checkbox"===e?this.findByName(b.name).filter(":checked").val():"number"===e&&"undefined"!=typeof b.validity?b.validity.badInput?!1:d.val():(c=d.val(),"string"==typeof c?c.replace(/\r/g,""):c)},check:function(b){b=this.validationTargetFor(this.clean(b));var c,d,e,f=a(b).rules(),g=a.map(f,function(a,b){return b}).length,h=!1,i=this.elementValue(b);for(d in f){e={method:d,parameters:f[d]};try{if(c=a.validator.methods[d].call(this,i,b,e.parameters),"dependency-mismatch"===c&&1===g){h=!0;continue}if(h=!1,"pending"===c)return void(this.toHide=this.toHide.not(this.errorsFor(b)));if(!c)return this.formatAndAdd(b,e),!1}catch(j){throw this.settings.debug&&window.console&&console.log("Exception occurred when checking element "+b.id+", check the '"+e.method+"' method.",j),j instanceof TypeError&&(j.message+=". Exception occurred when checking element "+b.id+", check the '"+e.method+"' method."),j}}if(!h)return this.objectLength(f)&&this.successList.push(b),!0},customDataMessage:function(b,c){return a(b).data("msg"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase())||a(b).data("msg")},customMessage:function(a,b){var c=this.settings.messages[a];return c&&(c.constructor===String?c:c[b])},findDefined:function(){for(var a=0;aWarning: No message defined for "+b.name+"")},formatAndAdd:function(b,c){var d=this.defaultMessage(b,c.method),e=/\$?\{(\d+)\}/g;"function"==typeof d?d=d.call(this,c.parameters,b):e.test(d)&&(d=a.validator.format(d.replace(e,"{$1}"),c.parameters)),this.errorList.push({message:d,element:b,method:c.method}),this.errorMap[b.name]=d,this.submitted[b.name]=d},addWrapper:function(a){return this.settings.wrapper&&(a=a.add(a.parent(this.settings.wrapper))),a},defaultShowErrors:function(){var a,b,c;for(a=0;this.errorList[a];a++)c=this.errorList[a],this.settings.highlight&&this.settings.highlight.call(this,c.element,this.settings.errorClass,this.settings.validClass),this.showLabel(c.element,c.message);if(this.errorList.length&&(this.toShow=this.toShow.add(this.containers)),this.settings.success)for(a=0;this.successList[a];a++)this.showLabel(this.successList[a]);if(this.settings.unhighlight)for(a=0,b=this.validElements();b[a];a++)this.settings.unhighlight.call(this,b[a],this.settings.errorClass,this.settings.validClass);this.toHide=this.toHide.not(this.toShow),this.hideErrors(),this.addWrapper(this.toShow).show()},validElements:function(){return this.currentElements.not(this.invalidElements())},invalidElements:function(){return a(this.errorList).map(function(){return this.element})},showLabel:function(b,c){var d,e,f,g=this.errorsFor(b),h=this.idOrName(b),i=a(b).attr("aria-describedby");g.length?(g.removeClass(this.settings.validClass).addClass(this.settings.errorClass),g.html(c)):(g=a("<"+this.settings.errorElement+">").attr("id",h+"-error").addClass(this.settings.errorClass).html(c||""),d=g,this.settings.wrapper&&(d=g.hide().show().wrap("<"+this.settings.wrapper+"/>").parent()),this.labelContainer.length?this.labelContainer.append(d):this.settings.errorPlacement?this.settings.errorPlacement(d,a(b)):d.insertAfter(b),g.is("label")?g.attr("for",h):0===g.parents("label[for='"+h+"']").length&&(f=g.attr("id").replace(/(:|\.|\[|\]|\$)/g,"\\$1"),i?i.match(new RegExp("\\b"+f+"\\b"))||(i+=" "+f):i=f,a(b).attr("aria-describedby",i),e=this.groups[b.name],e&&a.each(this.groups,function(b,c){c===e&&a("[name='"+b+"']",this.currentForm).attr("aria-describedby",g.attr("id"))}))),!c&&this.settings.success&&(g.text(""),"string"==typeof this.settings.success?g.addClass(this.settings.success):this.settings.success(g,b)),this.toShow=this.toShow.add(g)},errorsFor:function(b){var c=this.idOrName(b),d=a(b).attr("aria-describedby"),e="label[for='"+c+"'], label[for='"+c+"'] *";return d&&(e=e+", #"+d.replace(/\s+/g,", #")),this.errors().filter(e)},idOrName:function(a){return this.groups[a.name]||(this.checkable(a)?a.name:a.id||a.name)},validationTargetFor:function(b){return this.checkable(b)&&(b=this.findByName(b.name)),a(b).not(this.settings.ignore)[0]},checkable:function(a){return/radio|checkbox/i.test(a.type)},findByName:function(b){return a(this.currentForm).find("[name='"+b+"']")},getLength:function(b,c){switch(c.nodeName.toLowerCase()){case"select":return a("option:selected",c).length;case"input":if(this.checkable(c))return this.findByName(c.name).filter(":checked").length}return b.length},depend:function(a,b){return this.dependTypes[typeof a]?this.dependTypes[typeof a](a,b):!0},dependTypes:{"boolean":function(a){return a},string:function(b,c){return!!a(b,c.form).length},"function":function(a,b){return a(b)}},optional:function(b){var c=this.elementValue(b);return!a.validator.methods.required.call(this,c,b)&&"dependency-mismatch"},startRequest:function(a){this.pending[a.name]||(this.pendingRequest++,this.pending[a.name]=!0)},stopRequest:function(b,c){this.pendingRequest--,this.pendingRequest<0&&(this.pendingRequest=0),delete this.pending[b.name],c&&0===this.pendingRequest&&this.formSubmitted&&this.form()?(a(this.currentForm).submit(),this.formSubmitted=!1):!c&&0===this.pendingRequest&&this.formSubmitted&&(a(this.currentForm).triggerHandler("invalid-form",[this]),this.formSubmitted=!1)},previousValue:function(b){return a.data(b,"previousValue")||a.data(b,"previousValue",{old:null,valid:!0,message:this.defaultMessage(b,"remote")})},destroy:function(){this.resetForm(),a(this.currentForm).off(".validate").removeData("validator")}},classRuleSettings:{required:{required:!0},email:{email:!0},url:{url:!0},date:{date:!0},dateISO:{dateISO:!0},number:{number:!0},digits:{digits:!0},creditcard:{creditcard:!0}},addClassRules:function(b,c){b.constructor===String?this.classRuleSettings[b]=c:a.extend(this.classRuleSettings,b)},classRules:function(b){var c={},d=a(b).attr("class");return d&&a.each(d.split(" "),function(){this in a.validator.classRuleSettings&&a.extend(c,a.validator.classRuleSettings[this])}),c},normalizeAttributeRule:function(a,b,c,d){/min|max/.test(c)&&(null===b||/number|range|text/.test(b))&&(d=Number(d),isNaN(d)&&(d=void 0)),d||0===d?a[c]=d:b===c&&"range"!==b&&(a[c]=!0)},attributeRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)"required"===c?(d=b.getAttribute(c),""===d&&(d=!0),d=!!d):d=f.attr(c),this.normalizeAttributeRule(e,g,c,d);return e.maxlength&&/-1|2147483647|524288/.test(e.maxlength)&&delete e.maxlength,e},dataRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)d=f.data("rule"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase()),this.normalizeAttributeRule(e,g,c,d);return e},staticRules:function(b){var c={},d=a.data(b.form,"validator");return d.settings.rules&&(c=a.validator.normalizeRule(d.settings.rules[b.name])||{}),c},normalizeRules:function(b,c){return a.each(b,function(d,e){if(e===!1)return void delete b[d];if(e.param||e.depends){var f=!0;switch(typeof e.depends){case"string":f=!!a(e.depends,c.form).length;break;case"function":f=e.depends.call(c,c)}f?b[d]=void 0!==e.param?e.param:!0:delete b[d]}}),a.each(b,function(d,e){b[d]=a.isFunction(e)?e(c):e}),a.each(["minlength","maxlength"],function(){b[this]&&(b[this]=Number(b[this]))}),a.each(["rangelength","range"],function(){var c;b[this]&&(a.isArray(b[this])?b[this]=[Number(b[this][0]),Number(b[this][1])]:"string"==typeof b[this]&&(c=b[this].replace(/[\[\]]/g,"").split(/[\s,]+/),b[this]=[Number(c[0]),Number(c[1])]))}),a.validator.autoCreateRanges&&(null!=b.min&&null!=b.max&&(b.range=[b.min,b.max],delete b.min,delete b.max),null!=b.minlength&&null!=b.maxlength&&(b.rangelength=[b.minlength,b.maxlength],delete b.minlength,delete b.maxlength)),b},normalizeRule:function(b){if("string"==typeof b){var c={};a.each(b.split(/\s/),function(){c[this]=!0}),b=c}return b},addMethod:function(b,c,d){a.validator.methods[b]=c,a.validator.messages[b]=void 0!==d?d:a.validator.messages[b],c.length<3&&a.validator.addClassRules(b,a.validator.normalizeRule(b))},methods:{required:function(b,c,d){if(!this.depend(d,c))return"dependency-mismatch";if("select"===c.nodeName.toLowerCase()){var e=a(c).val();return e&&e.length>0}return this.checkable(c)?this.getLength(b,c)>0:b.length>0},email:function(a,b){return this.optional(b)||/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(a)},url:function(a,b){return this.optional(b)||/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})).?)(?::\d{2,5})?(?:[/?#]\S*)?$/i.test(a)},date:function(a,b){return this.optional(b)||!/Invalid|NaN/.test(new Date(a).toString())},dateISO:function(a,b){return this.optional(b)||/^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(a)},number:function(a,b){return this.optional(b)||/^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(a)},digits:function(a,b){return this.optional(b)||/^\d+$/.test(a)},creditcard:function(a,b){if(this.optional(b))return"dependency-mismatch";if(/[^0-9 \-]+/.test(a))return!1;var c,d,e=0,f=0,g=!1;if(a=a.replace(/\D/g,""),a.length<13||a.length>19)return!1;for(c=a.length-1;c>=0;c--)d=a.charAt(c),f=parseInt(d,10),g&&(f*=2)>9&&(f-=9),e+=f,g=!g;return e%10===0},minlength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||e>=d},maxlength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||d>=e},rangelength:function(b,c,d){var e=a.isArray(b)?b.length:this.getLength(b,c);return this.optional(c)||e>=d[0]&&e<=d[1]},min:function(a,b,c){return this.optional(b)||a>=c},max:function(a,b,c){return this.optional(b)||c>=a},range:function(a,b,c){return this.optional(b)||a>=c[0]&&a<=c[1]},equalTo:function(b,c,d){var e=a(d);return this.settings.onfocusout&&e.off(".validate-equalTo").on("blur.validate-equalTo",function(){a(c).valid()}),b===e.val()},remote:function(b,c,d){if(this.optional(c))return"dependency-mismatch";var e,f,g=this.previousValue(c);return this.settings.messages[c.name]||(this.settings.messages[c.name]={}),g.originalMessage=this.settings.messages[c.name].remote,this.settings.messages[c.name].remote=g.message,d="string"==typeof d&&{url:d}||d,g.old===b?g.valid:(g.old=b,e=this,this.startRequest(c),f={},f[c.name]=b,a.ajax(a.extend(!0,{mode:"abort",port:"validate"+c.name,dataType:"json",data:f,context:e.currentForm,success:function(d){var f,h,i,j=d===!0||"true"===d;e.settings.messages[c.name].remote=g.originalMessage,j?(i=e.formSubmitted,e.prepareElement(c),e.formSubmitted=i,e.successList.push(c),delete e.invalid[c.name],e.showErrors()):(f={},h=d||e.defaultMessage(c,"remote"),f[c.name]=g.message=a.isFunction(h)?h(b):h,e.invalid[c.name]=!0,e.showErrors(f)),g.valid=j,e.stopRequest(c,j)}},d)),"pending")}}});var b,c={};a.ajaxPrefilter?a.ajaxPrefilter(function(a,b,d){var e=a.port;"abort"===a.mode&&(c[e]&&c[e].abort(),c[e]=d)}):(b=a.ajax,a.ajax=function(d){var e=("mode"in d?d:a.ajaxSettings).mode,f=("port"in d?d:a.ajaxSettings).port;return"abort"===e?(c[f]&&c[f].abort(),c[f]=b.apply(this,arguments),c[f]):b.apply(this,arguments)})}); --------------------------------------------------------------------------------