├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── Src └── Xer.DomainDriven │ ├── AggregateRoot.Configuration.cs │ ├── AggregateRoot.cs │ ├── DomainEventStream.cs │ ├── Entity.cs │ ├── Exceptions │ ├── AggregateRootNotFoundException.cs │ └── DomainEventNotAppliedException.cs │ ├── IAggregateRoot.cs │ ├── IDomainEvent.cs │ ├── IDomainEventPublisher.cs │ ├── IDomainEventStream.cs │ ├── IEntity.cs │ ├── Repositories │ ├── IAggregateRootRepository.cs │ ├── InMemoryAggregateRootRepository.cs │ └── PublishingAggregateRootRepository.cs │ ├── ValueObject.HashCode.cs │ ├── ValueObject.cs │ └── Xer.DomainDriven.csproj ├── Tests └── Xer.DomainDriven.Tests │ ├── AggregateRootTests.cs │ ├── DomainEventStreamTests.cs │ ├── Entities │ ├── DomainEvents.cs │ ├── TestAggregateRoot.cs │ └── TestVaueObject.cs │ ├── InMemoryAggregateRootRepositoryTests.cs │ ├── ValueObjectTests.cs │ └── Xer.DomainDriven.Tests.csproj ├── Tools └── packages.config ├── Xer.DomainDriven.sln ├── appveyor.yml ├── build.cake ├── build.ps1 └── build.sh /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | 263 | # VS Code 264 | .vscode/ 265 | 266 | [Tt]ools/** 267 | ![Tt]ools/packages.config 268 | BuildArtifacts/** -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Joel Jeremy Marquez 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xer.DomainDriven 2 | Domain Driven Design entities and marker interfaces. 3 | 4 | Below are super basic examples on how to use the DDD types: 5 | 6 | Aggregate Root: 7 | ```csharp 8 | public class OrderAggregateRoot : AggregateRoot 9 | { 10 | private readonly List _lineItems; 11 | 12 | public OrderAggregateRoot (Guid orderId, IEnumerable lineItems) 13 | : base(orderId) // Pass ID to base. 14 | { 15 | _lineItems = lineItems.ToList(); 16 | } 17 | 18 | public void Approve() 19 | { 20 | // Apply domain events to be committed and published by repository. 21 | // Include line item IDs in event. 22 | ApplyDomainEvent(new OrderApprovedEvent(Id, _lineItems.Select(li => li.Id))); 23 | } 24 | 25 | public void Cancel() 26 | { 27 | // Apply domain events to be committed and published by repository. 28 | // Include line item IDs in event. 29 | ApplyDomainEvent(new OrderCancelledEvent(Id, _lineItems.Select(li => li.Id))); 30 | } 31 | } 32 | ``` 33 | 34 | Entity: 35 | ```csharp 36 | public class LineItem : Entity 37 | { 38 | public string ProductName { get; } 39 | public string Description { get; } 40 | public Price Price { get; } 41 | 42 | public LineItem(Guid lineItemId, string productName, string description, Price price) 43 | : base(lineItemId) // Pass ID to base. 44 | { 45 | ProductName = productName; 46 | Description = description; 47 | Price = price; 48 | } 49 | 50 | public void ApplyDiscount(decimal discountPercentage) 51 | { 52 | Price = Price.ApplyDiscount(discountPercentage); 53 | } 54 | } 55 | ``` 56 | 57 | Value Object: 58 | ```csharp 59 | public class Price : ValueObject 60 | { 61 | public decimal Amount { get; } 62 | public string Currency { get; } 63 | 64 | public Price(decimal amount, string currency) 65 | { 66 | Amount = amount; 67 | Currency = currency; 68 | } 69 | 70 | public Price AdjustAmount(decimal newAmount) 71 | { 72 | // Value objects should be immutable, so create a new instance. 73 | return new Price(newAmount, Currency); 74 | } 75 | 76 | public Price ApplyDiscount(decimal discountPercentage) 77 | { 78 | decimal discountedAmount = Price.Amount - (Price.Amount * discountPercentage); 79 | // Value objects should be immutable, so create a new instance. 80 | return new Price(discountedAmount, Currency); 81 | } 82 | 83 | public override bool ValueEquals(Price other) 84 | { 85 | return Amount == other.Amount && 86 | Currency == other.Currency; 87 | } 88 | 89 | public override HashCode GenerateHashCode() 90 | { 91 | return HashCode.New.Combine(Amount) 92 | .Combine(Currency); 93 | } 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /Src/Xer.DomainDriven/AggregateRoot.Configuration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Xer.DomainDriven.Exceptions; 4 | 5 | namespace Xer.DomainDriven 6 | { 7 | /// 8 | /// Represents an entity that serves as a "root" of an aggregate - which is the term for a set/group of entities 9 | /// and value objects that needs to maintain consistency as a whole. 10 | /// 11 | /// 12 | /// An aggregate root is responsible for protecting the invariants of an aggregate. 13 | /// Hence, all operations to change the state of an aggregate must go through the aggregate root. 14 | /// 15 | public abstract partial class AggregateRoot 16 | { 17 | protected interface IApplyActionConfiguration 18 | { 19 | /// 20 | /// Register an apply action to execute whenever a domain event is applied to the aggregate root. 21 | /// 22 | /// Type of domain event to apply. 23 | IApplyActionSelector Apply() where TDomainEvent : class, IDomainEvent; 24 | 25 | /// 26 | /// Require that an applier is registered for any domain events. 27 | /// 28 | void RequireApplyActions(); 29 | 30 | /// 31 | /// Set the action that executes whenever a domain event is successfully applied. 32 | /// 33 | /// Action to executes whenever a domain event is successfully applied. 34 | void OnApplySuccess(Action onApplySuccess); 35 | } 36 | 37 | protected interface IApplyActionSelector where TDomainEvent : class, IDomainEvent 38 | { 39 | /// 40 | /// Set apply action to apply the domain event. 41 | /// 42 | /// Action to apply the domain event. 43 | void With(Action applyAction); 44 | } 45 | 46 | protected interface IApplyActionResolver 47 | { 48 | /// 49 | /// Resolve apply action to execute for the domain event. 50 | /// 51 | /// Domain event to apply. 52 | /// 53 | /// This exception might be thrown if 54 | /// method was invoked and no registered domain event applier was found. 55 | /// 56 | /// Action that applies the domain event to the aggregate. Otherwise, null. 57 | Action ResolveApplyActionFor(IDomainEvent domainEvent); 58 | } 59 | 60 | private class ApplyActionConfiguration : IApplyActionConfiguration, IApplyActionResolver 61 | { 62 | #region Declarations 63 | 64 | private readonly Dictionary> _applyActionByDomainEventType = new Dictionary>(); 65 | private bool _requireDomainEventAppliers = false; 66 | private Action _onApplySuccess = (e) => { }; 67 | 68 | #endregion Declarations 69 | 70 | #region IApplyActionConfiguration Implementation 71 | 72 | /// 73 | /// Register an apply action to execute whenever a domain event is applied to the aggregate root. 74 | /// 75 | /// Type of domain event to apply. 76 | public IApplyActionSelector Apply() where TDomainEvent : class, IDomainEvent 77 | { 78 | return new ApplyActionSelector(this); 79 | } 80 | 81 | /// 82 | /// Require that an applier is registered for all domain events. 83 | /// 84 | public void RequireApplyActions() 85 | { 86 | _requireDomainEventAppliers = true; 87 | } 88 | 89 | /// 90 | /// Set the action that executes each time a domain event is successfully applied. 91 | /// This overrides the any previously set action delegate. 92 | /// 93 | /// Action that executes each time a domain event is successfully applied. 94 | public void OnApplySuccess(Action onDomainEventApplied) 95 | { 96 | _onApplySuccess = onDomainEventApplied ?? throw new ArgumentNullException(nameof(onDomainEventApplied)); 97 | } 98 | 99 | #endregion IApplyActionConfiguration Implementation 100 | 101 | #region IApplyActionResolver Implementation 102 | 103 | /// 104 | /// Resolve action to execute for the applied domain event. 105 | /// 106 | /// Domain event to apply. 107 | /// 108 | /// This exception will be thrown if 109 | /// was invoked and no domain event applier can be resolved for the given domain event. 110 | /// 111 | /// Action that applies the domain event to the aggregate. Otherwise, null. 112 | public Action ResolveApplyActionFor(IDomainEvent domainEvent) 113 | { 114 | if (domainEvent == null) 115 | { 116 | throw new ArgumentNullException(nameof(domainEvent)); 117 | } 118 | 119 | Type domainEventType = domainEvent.GetType(); 120 | 121 | bool found = _applyActionByDomainEventType.TryGetValue(domainEventType, out Action applyAction); 122 | if (!found && _requireDomainEventAppliers) 123 | { 124 | throw new DomainEventNotAppliedException(domainEvent, 125 | $@"{GetType().Name} has no registered apply action for domain event of type {domainEventType.Name}. 126 | Configure domain event apply actions by calling {nameof(Configure)} method in constructor."); 127 | } 128 | 129 | return applyAction; 130 | } 131 | 132 | #endregion IApplyActionResolver Implementation 133 | 134 | #region Methods 135 | 136 | /// 137 | /// Register action to be executed for the domain event. 138 | /// 139 | /// Type of domain event to apply. 140 | /// Action to apply the domain event to the aggregate. 141 | public void RegisterApplyAction(Action applyAction) where TDomainEvent : class, IDomainEvent 142 | { 143 | if (applyAction == null) 144 | { 145 | throw new ArgumentNullException(nameof(applyAction)); 146 | } 147 | 148 | Type domainEventType = typeof(TDomainEvent); 149 | 150 | if (_applyActionByDomainEventType.ContainsKey(domainEventType)) 151 | { 152 | throw new InvalidOperationException($"A apply action for {domainEventType.Name} has already been registered."); 153 | } 154 | 155 | Action apply = (e) => 156 | { 157 | TDomainEvent domainEvent = e as TDomainEvent; 158 | if (domainEvent == null) 159 | { 160 | throw new ArgumentException( 161 | $@"An invalid domain event was provided to the apply action. 162 | Expected domain event is of type {typeof(TDomainEvent).Name} but {e.GetType().Name} was provided."); 163 | } 164 | 165 | applyAction.Invoke(domainEvent); 166 | 167 | _onApplySuccess.Invoke(domainEvent); 168 | }; 169 | 170 | _applyActionByDomainEventType.Add(domainEventType, apply); 171 | } 172 | 173 | #endregion Methods 174 | } 175 | 176 | private class ApplyActionSelector : IApplyActionSelector where TDomainEvent : class, IDomainEvent 177 | { 178 | private readonly ApplyActionConfiguration _configuration; 179 | 180 | /// 181 | /// Constructor. 182 | /// 183 | /// Apply action configuration. 184 | public ApplyActionSelector(ApplyActionConfiguration configuration) 185 | { 186 | _configuration = configuration; 187 | } 188 | 189 | /// 190 | /// Set apply action tp apply the domain event. 191 | /// 192 | /// Action to apply the domain event. 193 | public void With(Action applyAction) 194 | { 195 | if (applyAction == null) 196 | { 197 | throw new ArgumentNullException(nameof(applyAction)); 198 | } 199 | 200 | _configuration.RegisterApplyAction(applyAction); 201 | } 202 | } 203 | } 204 | } -------------------------------------------------------------------------------- /Src/Xer.DomainDriven/AggregateRoot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using Xer.DomainDriven.Exceptions; 6 | 7 | namespace Xer.DomainDriven 8 | { 9 | /// 10 | /// Represents an entity that serves as a "root" of an aggregate - which is the term for a set/group of entities 11 | /// and value objects that needs to maintain consistency as a whole. 12 | /// 13 | /// 14 | /// An aggregate root is responsible for protecting the invariants of an aggregate. 15 | /// Hence, all operations to change the state of an aggregate must go through the aggregate root. 16 | /// 17 | public abstract partial class AggregateRoot : Entity, IAggregateRoot 18 | { 19 | #region Declarations 20 | 21 | private readonly Queue _domainEventsForCommit = new Queue(); 22 | private readonly ApplyActionConfiguration _applyActionConfiguration = new ApplyActionConfiguration(); 23 | 24 | #endregion Declarations 25 | 26 | #region Properties 27 | 28 | /// 29 | /// Snapshot of the current domain events that are marked for commit. 30 | /// 31 | /// Readonly collection of domain events that are marked for commit. 32 | protected IDomainEventStream DomainEventsForCommit => new DomainEventStream(Id, _domainEventsForCommit); 33 | 34 | #endregion Properties 35 | 36 | #region Constructors 37 | 38 | /// 39 | /// Constructor. 40 | /// 41 | /// Id of aggregate root. 42 | public AggregateRoot(Guid aggregateRootId) 43 | : this(aggregateRootId, DateTimeOffset.UtcNow, DateTimeOffset.UtcNow) 44 | { 45 | } 46 | 47 | /// 48 | /// Constructor. 49 | /// 50 | /// Id of aggregate root. 51 | /// Created date. 52 | /// Updated date. 53 | public AggregateRoot(Guid aggregateRootId, DateTimeOffset created, DateTimeOffset updated) 54 | : base(aggregateRootId, created, updated) 55 | { 56 | _applyActionConfiguration.OnApplySuccess((e) => 57 | { 58 | // Update this aggregate root's updated property 59 | // based on the applied domain event's timestamp. 60 | Updated = e.TimeStamp; 61 | }); 62 | } 63 | 64 | #endregion Constructors 65 | 66 | #region IAggregateRoot implementation 67 | 68 | // Note: These methods have been implemented explicitly to avoid cluttering public API. 69 | 70 | /// 71 | /// Get an event stream of all the uncommitted domain events applied to the aggregate. 72 | /// 73 | /// Stream of uncommitted domain events. 74 | IDomainEventStream IAggregateRoot.GetDomainEventsMarkedForCommit() 75 | { 76 | return new DomainEventStream(Id, DomainEventsForCommit); 77 | } 78 | 79 | // 80 | // Clear all internally tracked domain events. 81 | // 82 | void IAggregateRoot.MarkDomainEventsAsCommitted() 83 | { 84 | _domainEventsForCommit.Clear(); 85 | } 86 | 87 | #endregion IAggregateRoot implementation 88 | 89 | #region Protected Methods 90 | 91 | /// 92 | /// Setup apply actions configuration. 93 | /// 94 | /// Apply action configuration. 95 | protected void Configure(Action configuration) 96 | { 97 | if (configuration == null) 98 | { 99 | throw new ArgumentNullException(nameof(configuration)); 100 | } 101 | 102 | configuration.Invoke(_applyActionConfiguration); 103 | } 104 | 105 | /// 106 | /// Apply domain event to this aggregate root and mark domain event for commit. 107 | /// 108 | /// Type of domain event to apply. 109 | /// Instance of domain event to apply. 110 | protected virtual void ApplyDomainEvent(TDomainEvent domainEvent) where TDomainEvent : IDomainEvent 111 | { 112 | if (domainEvent == null) 113 | { 114 | throw new ArgumentNullException(nameof(domainEvent)); 115 | } 116 | 117 | // Invoke and track the event to save to event store. 118 | InvokeDomainEventApplier(domainEvent); 119 | } 120 | 121 | /// 122 | /// Apply domain event to this aggregate root wihtout marking domain event for commit. 123 | /// 124 | /// Type of domain event to replay. 125 | /// Instance of domain event to replay. 126 | protected virtual void ReplayDomainEvent(TDomainEvent domainEvent) where TDomainEvent : IDomainEvent 127 | { 128 | if (domainEvent == null) 129 | { 130 | throw new ArgumentNullException(nameof(domainEvent)); 131 | } 132 | 133 | // Invoke and track the event to save to event store. 134 | InvokeDomainEventApplier(domainEvent, markDomainEventForCommit: false); 135 | } 136 | 137 | #endregion Protected Methods 138 | 139 | #region Functions 140 | 141 | /// 142 | /// Invoke the registered action to handle the domain event. 143 | /// 144 | /// Type of the domain event to handle. 145 | /// Domain event instance to handle. 146 | /// True, if domain event should be marked/tracked for commit. Otherwise, false - which means domain event should just be replayed. 147 | private void InvokeDomainEventApplier(TDomainEvent domainEvent, bool markDomainEventForCommit = true) where TDomainEvent : IDomainEvent 148 | { 149 | Action applyAction = _applyActionConfiguration.ResolveApplyActionFor(domainEvent); 150 | if (applyAction == null) 151 | { 152 | return; 153 | } 154 | 155 | try 156 | { 157 | applyAction.Invoke(domainEvent); 158 | 159 | if (markDomainEventForCommit) 160 | { 161 | MarkAppliedDomainEventForCommit(domainEvent); 162 | } 163 | } 164 | catch (Exception ex) 165 | { 166 | throw new DomainEventNotAppliedException(domainEvent, 167 | $"Exception occured while {GetType().Name} [Id = {Id}] was trying to apply domain event of type {domainEvent.GetType().Name}.", 168 | ex); 169 | } 170 | } 171 | 172 | /// 173 | /// Add domain event to list of tracked domain events. 174 | /// 175 | /// Domain event instance to track. 176 | private void MarkAppliedDomainEventForCommit(IDomainEvent domainEvent) 177 | { 178 | _domainEventsForCommit.Enqueue(domainEvent); 179 | } 180 | 181 | #endregion Functions 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /Src/Xer.DomainDriven/DomainEventStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace Xer.DomainDriven 7 | { 8 | /// 9 | /// Represents a type that holds a collection/stream of domain events. 10 | /// 11 | public class DomainEventStream : IDomainEventStream, IEnumerable 12 | { 13 | #region Declarations 14 | 15 | private readonly IDomainEvent[] _domainEvents; 16 | 17 | #endregion Declarations 18 | 19 | #region Properties 20 | 21 | /// 22 | /// Id of the aggregate root which owns this stream. 23 | /// 24 | public Guid AggregateRootId { get; } 25 | 26 | /// 27 | /// Get number of domain events in the stream. 28 | /// 29 | public int DomainEventCount { get; } 30 | 31 | #endregion Properties 32 | 33 | #region Constructors 34 | 35 | /// 36 | /// Constructor to create an empty stream for the aggregate. 37 | /// 38 | /// ID of the aggregate root. 39 | public DomainEventStream(Guid aggreggateRootId) 40 | { 41 | AggregateRootId = aggreggateRootId; 42 | _domainEvents = new IDomainEvent[0]; 43 | } 44 | 45 | /// 46 | /// Constructs a new instance of a read-only stream. 47 | /// 48 | /// Id of the aggregate root which owns this stream. 49 | /// Domain events. 50 | public DomainEventStream(Guid aggregateRootId, IEnumerable domainEvents) 51 | { 52 | if (domainEvents == null) 53 | { 54 | throw new ArgumentNullException(nameof(domainEvents)); 55 | } 56 | 57 | _domainEvents = domainEvents.ToArray(); 58 | 59 | AggregateRootId = aggregateRootId; 60 | DomainEventCount = _domainEvents.Length; 61 | } 62 | 63 | #endregion Constructors 64 | 65 | /// 66 | /// Creates a new domain event stream which has the appended domain event. 67 | /// 68 | /// Domain event to append to the domain event stream. 69 | /// New instance of domain event stream with the appended domain event. 70 | public DomainEventStream AppendDomainEvent(IDomainEvent domainEventToAppend) 71 | { 72 | if (domainEventToAppend == null) 73 | { 74 | throw new ArgumentNullException(nameof(domainEventToAppend)); 75 | } 76 | 77 | if (!AggregateRootId.Equals(domainEventToAppend.AggregateRootId)) 78 | { 79 | throw new InvalidOperationException("Cannot append domain event belonging to a different aggregate root."); 80 | } 81 | 82 | return new DomainEventStream(AggregateRootId, this.Concat(new[] { domainEventToAppend })); 83 | } 84 | 85 | /// 86 | /// Creates a new domain event stream which has the appended domain event stream. 87 | /// 88 | /// Domain event stream to append to this domain event stream. 89 | /// New instance of domain event stream with the appended domain event stream. 90 | public DomainEventStream AppendDomainEventStream(IDomainEventStream streamToAppend) 91 | { 92 | if (streamToAppend == null) 93 | { 94 | throw new ArgumentNullException(nameof(streamToAppend)); 95 | } 96 | 97 | if (!AggregateRootId.Equals(streamToAppend.AggregateRootId)) 98 | { 99 | throw new InvalidOperationException("Cannot append domain events belonging to a different aggregate root."); 100 | } 101 | 102 | return new DomainEventStream(AggregateRootId, this.Concat(streamToAppend)); 103 | } 104 | 105 | /// 106 | /// Get enumerator. 107 | /// 108 | /// Enumerator which yields domain events until iterated upon. 109 | public IEnumerator GetEnumerator() 110 | { 111 | foreach (IDomainEvent domainEvent in _domainEvents) 112 | { 113 | yield return domainEvent; 114 | } 115 | } 116 | 117 | /// 118 | /// Get enumerator. 119 | /// 120 | /// Enumerator which yields domain events until iterated upon. 121 | IEnumerator IEnumerable.GetEnumerator() 122 | { 123 | foreach (IDomainEvent domainEvent in _domainEvents) 124 | { 125 | yield return domainEvent; 126 | } 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /Src/Xer.DomainDriven/Entity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Xer.DomainDriven 4 | { 5 | /// 6 | /// Represents a type that is distinguishable by a unique ID. 7 | /// 8 | /// 9 | /// Two entities that has the same ID are to be considered equal regardless of the state of their properties. 10 | /// 11 | public abstract class Entity : IEntity 12 | { 13 | #region Properties 14 | 15 | /// 16 | /// Unique ID. 17 | /// 18 | public Guid Id { get; protected set; } 19 | 20 | /// 21 | /// Date when entitity was created. 22 | /// This will default to if no value has been provided in constructor. 23 | /// 24 | public DateTimeOffset Created { get; protected set; } 25 | 26 | /// 27 | /// Date when entity was last updated. 28 | /// This will default to if no value has been provided in constructor. 29 | /// 30 | public DateTimeOffset Updated { get; protected set; } 31 | 32 | #endregion Properties 33 | 34 | #region Constructors 35 | 36 | /// 37 | /// Constructor. 38 | /// 39 | /// 40 | /// This will set and properties to . 41 | /// 42 | /// ID of entity. 43 | public Entity(Guid entityId) 44 | { 45 | Id = entityId; 46 | Created = DateTimeOffset.UtcNow; 47 | Updated = DateTimeOffset.UtcNow; 48 | } 49 | 50 | /// 51 | /// Constructor. 52 | /// 53 | /// ID of entity. 54 | /// Created date. 55 | /// Updated date. 56 | public Entity(Guid entityId, DateTimeOffset created, DateTimeOffset updated) 57 | { 58 | Id = entityId; 59 | Created = created; 60 | Updated = updated; 61 | } 62 | 63 | #endregion Constructors 64 | 65 | #region Methods 66 | 67 | /// 68 | /// Check if entity has the same identity as this entity instance. 69 | /// 70 | /// Entity. 71 | /// True if entities have the same identity. Otherwise, false. 72 | public virtual bool IsSameAs(IEntity entity) 73 | { 74 | if (entity == null) 75 | { 76 | throw new ArgumentNullException(nameof(entity)); 77 | } 78 | 79 | return Id == entity.Id; 80 | } 81 | 82 | #endregion Methods 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Src/Xer.DomainDriven/Exceptions/AggregateRootNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Xer.DomainDriven.Exceptions 4 | { 5 | /// 6 | /// Represents an exception that indicates that an aggregate root was not found. 7 | /// 8 | public class AggregateRootNotFoundException : Exception 9 | { 10 | /// 11 | /// ID of aggregate root that was not found. 12 | /// 13 | public Guid AggregateRootId { get; } 14 | 15 | /// 16 | /// Constructor. 17 | /// 18 | /// ID of aggregate root that was not found. 19 | public AggregateRootNotFoundException(Guid aggregateRootId) 20 | : this(aggregateRootId, $"Aggregate root with ID {aggregateRootId} does not exist.") 21 | { 22 | } 23 | 24 | /// 25 | /// Constructor. 26 | /// 27 | /// ID of aggregate root that was not found. 28 | /// Exception message. 29 | public AggregateRootNotFoundException(Guid aggregateRootId, string message) : base(message) 30 | { 31 | AggregateRootId = aggregateRootId; 32 | } 33 | 34 | /// 35 | /// Constructor. 36 | /// 37 | /// ID of aggregate root that was not found. 38 | /// Exception message. 39 | /// Inner exception. 40 | public AggregateRootNotFoundException(Guid aggregateRootId, string message, Exception innerException) : base(message, innerException) 41 | { 42 | AggregateRootId = aggregateRootId; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /Src/Xer.DomainDriven/Exceptions/DomainEventNotAppliedException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Xer.DomainDriven.Exceptions 4 | { 5 | /// 6 | /// Represents an exception that indicates that a domain event was attempted to be applied but failed. 7 | /// 8 | public class DomainEventNotAppliedException : Exception 9 | { 10 | /// 11 | /// ID of the aggregate root to whom the domain event was being applied. 12 | /// 13 | public Guid AggregateRootId { get; } 14 | 15 | /// 16 | /// Domain event that failed to tbe applied. 17 | /// 18 | public IDomainEvent DomainEvent { get; } 19 | 20 | /// 21 | /// Constructor. 22 | /// 23 | /// Domain event that failed to tbe applied. 24 | public DomainEventNotAppliedException(IDomainEvent domainEvent) 25 | : this(domainEvent, string.Empty) 26 | { 27 | } 28 | 29 | /// 30 | /// Constructor. 31 | /// 32 | /// Domain event that failed to be applied. 33 | /// Exception message. 34 | public DomainEventNotAppliedException(IDomainEvent domainEvent, string message) 35 | : this(domainEvent, message, null) 36 | { 37 | } 38 | 39 | /// 40 | /// Constructor. 41 | /// 42 | /// Domain event that failed to be applied. 43 | /// Exception message. 44 | /// Inner exception. 45 | public DomainEventNotAppliedException(IDomainEvent domainEvent, string message, Exception innerException) 46 | : base(message, innerException) 47 | { 48 | AggregateRootId = domainEvent.AggregateRootId; 49 | DomainEvent = domainEvent; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Src/Xer.DomainDriven/IAggregateRoot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Xer.DomainDriven 4 | { 5 | /// 6 | /// Represents an entity that serves as a "root" of an aggregate - which is the term for a set/group of entities 7 | /// and value objects that needs to maintain consistency as a whole. 8 | /// 9 | /// 10 | /// An aggregate root is responsible for protecting the invariants of an aggregate. 11 | /// Hence, all operations to change the state of an aggregate must go through the aggregate root. 12 | /// 13 | public interface IAggregateRoot : IEntity 14 | { 15 | /// 16 | /// Get an event stream of all the uncommitted domain events applied to the aggregate root. 17 | /// 18 | /// Stream of uncommitted domain events. 19 | IDomainEventStream GetDomainEventsMarkedForCommit(); 20 | 21 | // 22 | // Mark all internally tracked domain events as committed. 23 | // 24 | void MarkDomainEventsAsCommitted(); 25 | } 26 | } -------------------------------------------------------------------------------- /Src/Xer.DomainDriven/IDomainEvent.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Xer.DomainDriven 4 | { 5 | /// 6 | /// Represents an event that has occurred within a domain. 7 | /// 8 | public interface IDomainEvent 9 | { 10 | /// 11 | /// Id of the aggregate root. 12 | /// 13 | Guid AggregateRootId { get; } 14 | 15 | /// 16 | /// Timestamp. 17 | /// 18 | DateTimeOffset TimeStamp { get; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Src/Xer.DomainDriven/IDomainEventPublisher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace Xer.DomainDriven 7 | { 8 | /// 9 | /// Represents a type that publishes domain events. 10 | /// 11 | public interface IDomainEventPublisher 12 | { 13 | /// 14 | /// Publish domain events. 15 | /// 16 | /// Domain events. 17 | /// Cancellation token. 18 | /// Asynchronous task. 19 | Task PublishAsync(IDomainEventStream domainEvents, CancellationToken cancellationToken = default(CancellationToken)); 20 | } 21 | } -------------------------------------------------------------------------------- /Src/Xer.DomainDriven/IDomainEventStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Xer.DomainDriven 5 | { 6 | /// 7 | /// Represents a type that holds a collection/stream of domain events. 8 | /// 9 | public interface IDomainEventStream : IEnumerable 10 | { 11 | /// 12 | /// Id of the aggregate root which owns this stream. 13 | /// 14 | Guid AggregateRootId { get; } 15 | 16 | /// 17 | /// Get number of domain events in the stream. 18 | /// 19 | int DomainEventCount { get; } 20 | } 21 | } -------------------------------------------------------------------------------- /Src/Xer.DomainDriven/IEntity.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Xer.DomainDriven 4 | { 5 | /// 6 | /// Represents a type that is distinguishable by a unique ID. 7 | /// 8 | /// 9 | /// Two entities that has the same ID are to be considered equal regardless of the state of their properties. 10 | /// 11 | public interface IEntity 12 | { 13 | /// 14 | /// Unique identifier. 15 | /// 16 | Guid Id { get; } 17 | 18 | /// 19 | /// Date when entity was created. 20 | /// 21 | DateTimeOffset Created { get; } 22 | 23 | /// 24 | /// Date when entity was last updated. 25 | /// 26 | DateTimeOffset Updated { get; } 27 | } 28 | } -------------------------------------------------------------------------------- /Src/Xer.DomainDriven/Repositories/IAggregateRootRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Xer.DomainDriven.Repositories 6 | { 7 | /// 8 | /// Represents a type that knows how to retrieve and save an aggregate root. 9 | /// 10 | /// Type of aggregate root that this repository stores. 11 | public interface IAggregateRootRepository where TAggregateRoot : IAggregateRoot 12 | { 13 | /// 14 | /// Save aggregate root. 15 | /// 16 | /// Aggregate root. 17 | /// Cancellation token. 18 | /// Asynchronous task. 19 | Task SaveAsync(TAggregateRoot aggregateRoot, CancellationToken cancellationToken = default(CancellationToken)); 20 | 21 | /// 22 | /// Get aggregate root by ID. 23 | /// 24 | /// Aggregate root ID. 25 | /// Cancellation token. 26 | /// Instance of aggregate root. 27 | Task GetByIdAsync(Guid aggregateRootId, CancellationToken cancellationToken = default(CancellationToken)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Src/Xer.DomainDriven/Repositories/InMemoryAggregateRootRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Xer.DomainDriven.Exceptions; 7 | 8 | namespace Xer.DomainDriven.Repositories 9 | { 10 | /// 11 | /// Represents an implementation of aggregate root repository which uses an in-memory storage mechanism. 12 | /// 13 | /// Type of aggregate root that this repository stores. 14 | public class InMemoryAggregateRootRepository : IAggregateRootRepository 15 | where TAggregateRoot : IAggregateRoot 16 | { 17 | #region Declarations 18 | 19 | private static readonly Task CompletedTask = Task.FromResult(true); 20 | private List _aggregateRoots = new List(); 21 | 22 | private readonly bool _throwIfAggregateRootIsNotFound; 23 | 24 | #endregion Declarations 25 | 26 | #region Constructors 27 | 28 | /// 29 | /// Default constructor. 30 | /// 31 | public InMemoryAggregateRootRepository() 32 | : this(false) 33 | { 34 | 35 | } 36 | 37 | /// 38 | /// Constructor. 39 | /// 40 | /// True if repository should throw if aggregate root does not exist. Otherwise, false. 41 | public InMemoryAggregateRootRepository(bool throwIfAggregateRootIsNotFound) 42 | { 43 | _throwIfAggregateRootIsNotFound = throwIfAggregateRootIsNotFound; 44 | } 45 | 46 | #endregion Constructors 47 | 48 | #region IAggregateRootRepository Implementation 49 | 50 | /// 51 | /// Get aggregate root by ID. 52 | /// 53 | /// Aggregate root ID. 54 | /// Cancellation token. 55 | /// Instance of aggregate root. 56 | public Task GetByIdAsync(Guid aggregateRootId, CancellationToken cancellationToken = default(CancellationToken)) 57 | { 58 | TAggregateRoot aggregateRoot = _aggregateRoots.FirstOrDefault(a => a.Id.Equals(aggregateRootId)); 59 | 60 | if (aggregateRoot == null && _throwIfAggregateRootIsNotFound) 61 | { 62 | return TaskFromException(new AggregateRootNotFoundException(aggregateRootId, $"Aggregate root with ID {aggregateRootId} was not found.")); 63 | } 64 | 65 | return Task.FromResult(aggregateRoot); 66 | } 67 | 68 | /// 69 | /// Save aggregate root. 70 | /// 71 | /// Aggregate root. 72 | /// Cancellation token. 73 | /// Asynchronous task. 74 | public Task SaveAsync(TAggregateRoot aggregateRoot, CancellationToken cancellationToken = default(CancellationToken)) 75 | { 76 | _aggregateRoots.RemoveAll(a => a.Id == aggregateRoot.Id); 77 | _aggregateRoots.Add(aggregateRoot); 78 | 79 | return CompletedTask; 80 | } 81 | 82 | #endregion IAggregateRootRepository Implementation 83 | 84 | #region Functions 85 | 86 | /// 87 | /// Create a task with exception. 88 | /// 89 | /// Exception. 90 | /// Faulted task containing the exception. 91 | private static Task TaskFromException(Exception ex) 92 | { 93 | TaskCompletionSource tcs = new TaskCompletionSource(); 94 | tcs.TrySetException(ex); 95 | return tcs.Task; 96 | } 97 | 98 | #endregion Functions 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Src/Xer.DomainDriven/Repositories/PublishingAggregateRootRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Xer.DomainDriven.Repositories 6 | { 7 | /// 8 | /// Represents a aggregate root repository decorator that publishes all uncommitted domain events 9 | /// after the decorated aggregate root repository saves to the storage mechanism. 10 | /// 11 | /// Type of aggregate root that this repository stores. 12 | 13 | public class PublishingAggregateRootRepository : IAggregateRootRepository 14 | where TAggregateRoot : IAggregateRoot 15 | { 16 | private readonly IAggregateRootRepository _decoratedRepository; 17 | private readonly IDomainEventPublisher _domainEventPublisher; 18 | 19 | /// 20 | /// Constructor. 21 | /// 22 | /// Aggregate root repository to decorate. 23 | /// Domain event publisher. 24 | public PublishingAggregateRootRepository(IAggregateRootRepository repositoryToDecorate, 25 | IDomainEventPublisher domainEventPublisher) 26 | { 27 | _decoratedRepository = repositoryToDecorate; 28 | _domainEventPublisher = domainEventPublisher; 29 | } 30 | 31 | /// 32 | /// Get aggregate root by ID. 33 | /// 34 | /// Aggregate root ID. 35 | /// Cancellation token. 36 | /// Instance of aggregate root. 37 | public Task GetByIdAsync(Guid aggregateRootId, CancellationToken cancellationToken = default(CancellationToken)) 38 | => _decoratedRepository.GetByIdAsync(aggregateRootId, cancellationToken); 39 | 40 | /// 41 | /// Save aggregate root and publish uncommitted domain events. 42 | /// 43 | /// Aggregate root. 44 | /// Cancellation token. 45 | /// Asynchronous task. 46 | public async Task SaveAsync(TAggregateRoot aggregateRoot, CancellationToken cancellationToken = default(CancellationToken)) 47 | { 48 | // Get a copy of domain events marked for commit. 49 | IDomainEventStream domainEventsCopy = aggregateRoot.GetDomainEventsMarkedForCommit(); 50 | 51 | // Save aggregate root. 52 | await _decoratedRepository.SaveAsync(aggregateRoot); 53 | 54 | // Publish after saving. 55 | await _domainEventPublisher.PublishAsync(domainEventsCopy); 56 | 57 | // Clear domain events after publishing. 58 | aggregateRoot.MarkDomainEventsAsCommitted(); 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /Src/Xer.DomainDriven/ValueObject.HashCode.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Xer.DomainDriven 6 | { 7 | /// 8 | /// Represents a type that is distinguishable only by the state of its properties. 9 | /// 10 | /// 11 | /// Two value objects that has the same properties are to be considered equal. 12 | /// Value objects should be implemented as immutable. Hence, all modifications to 13 | /// a value object should return a new instance with the updated properties. 14 | /// 15 | /// Type of the derived class. 16 | public abstract partial class ValueObject : IEquatable where TSelf : class 17 | { 18 | /// 19 | /// Represents a hash code. 20 | /// 21 | protected struct HashCode 22 | { 23 | private readonly int _value; 24 | 25 | /// 26 | /// Constructor. 27 | /// 28 | /// Hash code value. 29 | public HashCode(int value) => _value = value; 30 | 31 | /// 32 | /// A new HashCode that has an initial value. 33 | /// 34 | public static HashCode New => new HashCode(19); 35 | 36 | /// 37 | /// Combine current value with the hash code of the provided object. 38 | /// 39 | /// Object to get the hash code to be combined. 40 | /// Type of object. 41 | /// New instance of HashCode that contains the combined hash code value. 42 | public HashCode Combine(T obj) 43 | { 44 | int hashCode = EqualityComparer.Default.GetHashCode(obj); 45 | return unchecked(new HashCode((_value * 31) + hashCode)); 46 | } 47 | 48 | /// 49 | /// Implicit conversion from HashCode to int. 50 | /// 51 | /// 52 | public static implicit operator int(HashCode hash) => hash._value; 53 | 54 | /// 55 | /// Returns this HashCode instance's value. 56 | /// 57 | /// This HashCode instance's value. 58 | public override int GetHashCode() => _value; 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /Src/Xer.DomainDriven/ValueObject.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Xer.DomainDriven 4 | { 5 | /// 6 | /// Represents a type that is distinguishable only by the state of its properties. 7 | /// 8 | /// 9 | /// Two value objects that has the same properties are to be considered equal. 10 | /// Value objects should be implemented as immutable. Hence, all modifications to 11 | /// a value object should return a new instance with the updated properties. 12 | /// 13 | /// Type of the derived class. 14 | public abstract partial class ValueObject : IEquatable where TSelf : class 15 | { 16 | #region Protected Methods 17 | 18 | /// 19 | /// Generate a HashCode by passing all the value object's fields to the HashCode's constructor. 20 | /// 21 | /// Instance of HashCode. 22 | protected abstract HashCode GenerateHashCode(); 23 | 24 | /// 25 | /// Compare equality by value. 26 | /// 27 | /// Other instance. 28 | /// True if objects should be treated as equal by value. Otherwise, false. 29 | protected abstract bool ValueEquals(TSelf other); 30 | 31 | #endregion Protected Methods 32 | 33 | #region Equality Operators 34 | 35 | /// 36 | /// Equals method. 37 | /// 38 | /// Other object. 39 | /// True, if objects are determined as equal. Otherwise, false. 40 | public override bool Equals(object obj) 41 | { 42 | return Equals(obj as TSelf); 43 | } 44 | 45 | /// 46 | /// Equals method. 47 | /// 48 | /// Other object. 49 | /// True, if objects are determined as equal. Otherwise, false. 50 | public bool Equals(TSelf other) 51 | { 52 | if (other == null) 53 | return false; 54 | 55 | if (ReferenceEquals(this, other)) 56 | return true; 57 | 58 | if (this.GetType() != other.GetType()) 59 | return false; 60 | 61 | return ValueEquals(other); 62 | } 63 | 64 | /// 65 | /// Equals operator. 66 | /// 67 | /// First value object. 68 | /// Second value object. 69 | /// True, if value objects are determined as equal. Otherwise, false. 70 | public static bool operator ==(ValueObject obj1, ValueObject obj2) 71 | { 72 | if (ReferenceEquals(obj1, null) && ReferenceEquals(obj2, null)) 73 | { 74 | return true; 75 | } 76 | 77 | if (!ReferenceEquals(obj1, null)) 78 | { 79 | return obj1.Equals(obj2); 80 | } 81 | 82 | return false; 83 | } 84 | 85 | /// 86 | /// Inequality operator. 87 | /// 88 | /// First value object. 89 | /// Second value object. 90 | /// True, if value objects are determined as NOT equal. Otherwise, false. 91 | public static bool operator !=(ValueObject obj1, ValueObject obj2) 92 | { 93 | return !(obj1 == obj2); 94 | } 95 | 96 | /// 97 | /// Generate a hash code. 98 | /// 99 | /// Hash code. 100 | public override int GetHashCode() 101 | { 102 | return GenerateHashCode(); 103 | } 104 | 105 | #endregion Equality Operators 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /Src/Xer.DomainDriven/Xer.DomainDriven.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard1.0 5 | jeyjeyemem;xerprojects-contributors; 6 | Domain driven design components and marker interfaces. 7 | Copyright (c) XerProjects contributors 8 | https://github.com/XerProjects/Xer.DomainDriven/blob/master/LICENSE 9 | https://github.com/XerProjects/Xer.DomainDriven 10 | domain-driven-design;ddd; 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Tests/Xer.DomainDriven.Tests/AggregateRootTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FluentAssertions; 4 | using Xer.DomainDriven.Exceptions; 5 | using Xer.DomainDriven.Tests.Entities; 6 | using Xunit; 7 | 8 | namespace Xer.DomainDriven.Tests 9 | { 10 | public class AggregateRootTests 11 | { 12 | #region ApplyDomainEventMethod 13 | 14 | public class ApplyDomainEventMethod 15 | { 16 | [Fact] 17 | public void ShouldApplyDomainEvent() 18 | { 19 | // Given. 20 | var aggregateRoot = new TestAggregateRoot(Guid.NewGuid()); 21 | Guid changeId = Guid.NewGuid(); 22 | // When. 23 | aggregateRoot.ChangeMe(changeId); 24 | 25 | // Then. 26 | aggregateRoot.HasHandledChangeId(changeId).Should().BeTrue(); 27 | } 28 | 29 | [Fact] 30 | public void ShouldPropagateIfDomainEventApplierMethodThrowsAnException() 31 | { 32 | var aggregateRoot = new TestAggregateRoot(Guid.NewGuid()); 33 | aggregateRoot.Invoking(a => a.ThrowAnException()).Should().Throw(); 34 | } 35 | 36 | [Fact] 37 | public void ShouldChangeUpdatedPropertyWhenADomainEventIsAppliedByDefault() 38 | { 39 | // Given. 40 | var aggregateRoot = new TestAggregateRoot(Guid.NewGuid()); 41 | DateTimeOffset aggregateRootUpdated = aggregateRoot.Updated; 42 | 43 | // When. 44 | aggregateRoot.ChangeMe(Guid.NewGuid()); 45 | 46 | // Then. 47 | aggregateRoot.Updated.Should().NotBe(aggregateRootUpdated); 48 | } 49 | 50 | [Fact] 51 | public void ShouldNotThrowIfNoDomainEventApplierIsRegistered() 52 | { 53 | // Aggregate root with no configured domain event appliers. 54 | var aggregateRoot = new DefaultAggregateRoot(Guid.NewGuid()); 55 | aggregateRoot.Invoking(ar => ar.ChangeMe(Guid.NewGuid())) 56 | .Should().NotThrow(); 57 | } 58 | 59 | [Fact] 60 | public void ShouldThrowIfNoDomainEventApplierIsRegisteredButIsRequired() 61 | { 62 | var aggregateRoot = new ApplierRequiredAggregateRoot(Guid.NewGuid()); 63 | aggregateRoot.Invoking(ar => ar.ChangeMe(Guid.NewGuid())) 64 | .Should().ThrowExactly(); 65 | } 66 | } 67 | 68 | #endregion ApplyDomainEventMethod 69 | 70 | #region GetUncommittedDomainEventsMethod 71 | 72 | public class GetDomainEventsMarkedForCommitMethod 73 | { 74 | [Fact] 75 | public void ShouldIncludeAppliedDomainEvent() 76 | { 77 | // Given. 78 | TestAggregateRoot aggregateRoot = new TestAggregateRoot(Guid.NewGuid()); 79 | 80 | // When. 81 | // Apply 3 domain events 82 | aggregateRoot.ChangeMe(Guid.NewGuid()); 83 | aggregateRoot.ChangeMe(Guid.NewGuid()); 84 | aggregateRoot.ChangeMe(Guid.NewGuid()); 85 | 86 | // Then. 87 | IAggregateRoot explicitCast = aggregateRoot; 88 | explicitCast.GetDomainEventsMarkedForCommit().Should().HaveCount(3); 89 | } 90 | } 91 | 92 | #endregion GetDomainEventsMarkedForCommitMethod 93 | 94 | #region MarkDomainEventsAsCommittedMethod 95 | 96 | public class ClearUncommitedDomainEventsMethod 97 | { 98 | [Fact] 99 | public void ShouldRemoveAllAppliedDomainEvents() 100 | { 101 | TestAggregateRoot aggregateRoot = new TestAggregateRoot(Guid.NewGuid()); 102 | 103 | // Apply 3 domain events 104 | aggregateRoot.ChangeMe(Guid.NewGuid()); 105 | aggregateRoot.ChangeMe(Guid.NewGuid()); 106 | aggregateRoot.ChangeMe(Guid.NewGuid()); 107 | 108 | // Check 109 | IAggregateRoot explicitCast = aggregateRoot; 110 | explicitCast.GetDomainEventsMarkedForCommit().Should().HaveCount(3); 111 | 112 | // Clear 113 | explicitCast.MarkDomainEventsAsCommitted(); 114 | explicitCast.GetDomainEventsMarkedForCommit().Should().HaveCount(0); 115 | } 116 | } 117 | 118 | #endregion MarkDomainEventsAsCommittedMethod 119 | } 120 | } -------------------------------------------------------------------------------- /Tests/Xer.DomainDriven.Tests/DomainEventStreamTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Xer.DomainDriven.Tests.Entities; 4 | using Xunit; 5 | 6 | namespace Xer.DomainDriven.Tests 7 | { 8 | public class DomainEventStreamTests 9 | { 10 | #region AppendDomainEventMethod 11 | 12 | public class AppendDomainEventMethod 13 | { 14 | [Fact] 15 | public void ShouldAppendDomainEventToEndOfStream() 16 | { 17 | TestAggregateRoot aggregateRoot = new TestAggregateRoot(Guid.NewGuid()); 18 | 19 | var stream = new DomainEventStream(aggregateRoot.Id); 20 | 21 | stream.Should().HaveCount(0); 22 | 23 | var aggregateRootDomainEvent = new AggregateRootChangedDomainEvent(aggregateRoot.Id, Guid.NewGuid()); 24 | IDomainEventStream result = stream.AppendDomainEvent(aggregateRootDomainEvent); 25 | 26 | result.Should().HaveCount(1); 27 | } 28 | 29 | [Fact] 30 | public void ShouldThrowIfAggregateRootIdDoesNotMatch() 31 | { 32 | TestAggregateRoot aggregateRoot1 = new TestAggregateRoot(Guid.NewGuid()); 33 | TestAggregateRoot aggregateRoot2 = new TestAggregateRoot(Guid.NewGuid()); 34 | 35 | var aggregateRoot1Stream = new DomainEventStream(aggregateRoot1.Id); 36 | 37 | aggregateRoot1Stream.Should().HaveCount(0); 38 | 39 | var aggregateRoot2DomainEvent = new AggregateRootChangedDomainEvent(aggregateRoot2.Id, Guid.NewGuid()); 40 | 41 | // Append domain event of aggregate 2 to stream of aggregate 1. 42 | aggregateRoot1Stream.Invoking(s => s.AppendDomainEvent(aggregateRoot2DomainEvent)).Should().Throw(); 43 | } 44 | 45 | [Fact] 46 | public void ShouldThrowIfStreamToAppendIsNull() 47 | { 48 | DomainEventStream stream = new DomainEventStream(Guid.NewGuid()); 49 | stream.Invoking(s => s.AppendDomainEvent(null)).Should().Throw(); 50 | } 51 | } 52 | 53 | #endregion AppendDomainEventMethod 54 | 55 | #region AppendDomainEventStreamMethod 56 | 57 | public class AppendDomainEventStreamMethod 58 | { 59 | [Fact] 60 | public void ShouldAppendDomainEventsToEndOfStream() 61 | { 62 | TestAggregateRoot aggregateRoot = new TestAggregateRoot(Guid.NewGuid()); 63 | IAggregateRoot explicitCast = aggregateRoot; 64 | 65 | // Apply 3 domain events. 66 | aggregateRoot.ChangeMe(Guid.NewGuid()); 67 | aggregateRoot.ChangeMe(Guid.NewGuid()); 68 | aggregateRoot.ChangeMe(Guid.NewGuid()); 69 | 70 | DomainEventStream stream1 = (DomainEventStream)explicitCast.GetDomainEventsMarkedForCommit(); 71 | 72 | // Clear domain events. 73 | explicitCast.MarkDomainEventsAsCommitted(); 74 | 75 | // Apply 3 domain events. 76 | aggregateRoot.ChangeMe(Guid.NewGuid()); 77 | aggregateRoot.ChangeMe(Guid.NewGuid()); 78 | aggregateRoot.ChangeMe(Guid.NewGuid()); 79 | 80 | DomainEventStream stream2 = (DomainEventStream)explicitCast.GetDomainEventsMarkedForCommit(); 81 | 82 | // Append 2 streams. 83 | DomainEventStream result = stream1.AppendDomainEventStream(stream2); 84 | 85 | result.Should().HaveCount(6); 86 | } 87 | 88 | [Fact] 89 | public void ShouldThrowIfAggregateRootIdDoesNotMatch() 90 | { 91 | TestAggregateRoot aggregateRoot1 = new TestAggregateRoot(Guid.NewGuid()); 92 | TestAggregateRoot aggregateRoot2 = new TestAggregateRoot(Guid.NewGuid()); 93 | IAggregateRoot aggregateRoot1ExplicitCast = aggregateRoot1; 94 | IAggregateRoot aggregateRoot2ExplicitCast = aggregateRoot2; 95 | 96 | // Apply 3 domain events. 97 | aggregateRoot1.ChangeMe(Guid.NewGuid()); 98 | aggregateRoot1.ChangeMe(Guid.NewGuid()); 99 | aggregateRoot1.ChangeMe(Guid.NewGuid()); 100 | 101 | DomainEventStream stream1 = (DomainEventStream)aggregateRoot1ExplicitCast.GetDomainEventsMarkedForCommit(); 102 | 103 | // Apply 3 domain events. 104 | aggregateRoot2.ChangeMe(Guid.NewGuid()); 105 | aggregateRoot2.ChangeMe(Guid.NewGuid()); 106 | aggregateRoot2.ChangeMe(Guid.NewGuid()); 107 | 108 | DomainEventStream stream2 = (DomainEventStream)aggregateRoot2ExplicitCast.GetDomainEventsMarkedForCommit(); 109 | 110 | // Append 2 streams. 111 | stream1.Invoking(s1 => s1.AppendDomainEventStream(stream2)).Should().Throw(); 112 | } 113 | 114 | [Fact] 115 | public void ShouldThrowIfStreamToAppendIsNull() 116 | { 117 | DomainEventStream stream = new DomainEventStream(Guid.NewGuid()); 118 | stream.Invoking(s => s.AppendDomainEventStream(null)).Should().Throw(); 119 | } 120 | } 121 | 122 | #endregion AppendDomainEventStreamMethod 123 | 124 | #region DomainEventCount 125 | 126 | public class DomainEventCountProperty 127 | { 128 | [Fact] 129 | public void ShouldBeEqualToNumberOfDomainEvents() 130 | { 131 | TestAggregateRoot aggregateRoot = new TestAggregateRoot(Guid.NewGuid()); 132 | 133 | // 3 domain events. 134 | var domainEvent1 = new AggregateRootChangedDomainEvent(aggregateRoot.Id, Guid.NewGuid()); 135 | var domainEvent2 = new AggregateRootChangedDomainEvent(aggregateRoot.Id, Guid.NewGuid()); 136 | var domainEvent3 = new AggregateRootChangedDomainEvent(aggregateRoot.Id, Guid.NewGuid()); 137 | 138 | DomainEventStream stream = new DomainEventStream(aggregateRoot.Id, new[] 139 | { 140 | domainEvent1, domainEvent2, domainEvent3 141 | }); 142 | 143 | stream.DomainEventCount.Should().Be(3); 144 | } 145 | } 146 | 147 | #endregion DomainEventCount 148 | } 149 | } -------------------------------------------------------------------------------- /Tests/Xer.DomainDriven.Tests/Entities/DomainEvents.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Xer.DomainDriven.Tests.Entities 4 | { 5 | public class AggregateRootChangedDomainEvent : IDomainEvent 6 | { 7 | public Guid ChangeId { get; } 8 | 9 | public Guid AggregateRootId { get; } 10 | 11 | public DateTimeOffset TimeStamp { get; } = DateTimeOffset.UtcNow; 12 | 13 | public AggregateRootChangedDomainEvent(Guid aggregateRootId, Guid changeId) 14 | { 15 | AggregateRootId = aggregateRootId; 16 | ChangeId = changeId; 17 | } 18 | } 19 | 20 | public class ExceptionCausingDomainEvent : IDomainEvent 21 | { 22 | public Guid AggregateRootId { get; } 23 | 24 | public DateTimeOffset TimeStamp { get; } = DateTimeOffset.UtcNow; 25 | 26 | public ExceptionCausingDomainEvent(Guid aggregateRootId) 27 | { 28 | AggregateRootId = aggregateRootId; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Tests/Xer.DomainDriven.Tests/Entities/TestAggregateRoot.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Xer.DomainDriven.Tests.Entities 5 | { 6 | public class TestAggregateRoot : AggregateRoot 7 | { 8 | private readonly List _handledChangeIDs = new List(); 9 | 10 | public IReadOnlyCollection HandledChangeIDs => _handledChangeIDs.AsReadOnly(); 11 | 12 | public TestAggregateRoot(Guid aggregateRootId) 13 | : this(aggregateRootId, DateTime.UtcNow, DateTime.UtcNow) 14 | { 15 | } 16 | 17 | public TestAggregateRoot(Guid aggregateRootId, DateTime createdUtc, DateTime updatedUtc) 18 | : base(aggregateRootId, createdUtc, updatedUtc) 19 | { 20 | Configure(c => 21 | { 22 | c.RequireApplyActions(); 23 | c.Apply().With(OnTestAggregateRootChangedEvent); 24 | c.Apply().With(OnExceptionCausingDomainEvent); 25 | }); 26 | } 27 | 28 | public void ChangeMe(Guid changeId) 29 | { 30 | ApplyDomainEvent(new AggregateRootChangedDomainEvent(Id, changeId)); 31 | } 32 | 33 | public void ThrowAnException() 34 | { 35 | ApplyDomainEvent(new ExceptionCausingDomainEvent(Id)); 36 | } 37 | 38 | public bool HasHandledChangeId(Guid changeId) 39 | { 40 | return _handledChangeIDs.Contains(changeId); 41 | } 42 | 43 | private void OnExceptionCausingDomainEvent(ExceptionCausingDomainEvent obj) 44 | { 45 | throw new Exception("ExceptionCausingDomainEvent"); 46 | } 47 | 48 | private void OnTestAggregateRootChangedEvent(AggregateRootChangedDomainEvent domainEvent) 49 | { 50 | _handledChangeIDs.Add(domainEvent.ChangeId); 51 | } 52 | } 53 | 54 | public class ApplierRequiredAggregateRoot : AggregateRoot 55 | { 56 | public ApplierRequiredAggregateRoot(Guid aggregateRootId) 57 | : this(aggregateRootId, DateTime.UtcNow, DateTime.UtcNow) 58 | { 59 | } 60 | 61 | public ApplierRequiredAggregateRoot(Guid aggregateRootId, DateTime created, DateTime updated) 62 | : base(aggregateRootId, created, updated) 63 | { 64 | Configure(c => 65 | { 66 | c.RequireApplyActions(); 67 | // No domain event applier was registered. 68 | }); 69 | } 70 | 71 | public void ChangeMe(Guid changeId) 72 | { 73 | ApplyDomainEvent(new AggregateRootChangedDomainEvent(Id, changeId)); 74 | } 75 | } 76 | 77 | // Aggregate root with no configured domain event appliers. 78 | public class DefaultAggregateRoot : AggregateRoot 79 | { 80 | public DefaultAggregateRoot(Guid aggregateRootId) 81 | : this(aggregateRootId, DateTime.UtcNow, DateTime.UtcNow) 82 | { 83 | } 84 | 85 | public DefaultAggregateRoot(Guid aggregateRootId, DateTime created, DateTime updated) 86 | : base(aggregateRootId, created, updated) 87 | { 88 | } 89 | 90 | public void ChangeMe(Guid changeId) 91 | { 92 | ApplyDomainEvent(new AggregateRootChangedDomainEvent(Id, changeId)); 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /Tests/Xer.DomainDriven.Tests/Entities/TestVaueObject.cs: -------------------------------------------------------------------------------- 1 | namespace Xer.DomainDriven.Tests.Entities 2 | { 3 | public class TestValueObject : ValueObject 4 | { 5 | public string Data { get; } 6 | public int Number { get; } 7 | 8 | public TestValueObject(string data, int number) 9 | { 10 | Data = data; 11 | Number = number; 12 | } 13 | 14 | protected override bool ValueEquals(TestValueObject other) 15 | { 16 | return Data == other.Data && 17 | Number == other.Number; 18 | } 19 | 20 | protected override HashCode GenerateHashCode() 21 | { 22 | return HashCode.New.Combine(Number).Combine(Data); 23 | } 24 | } 25 | 26 | public class TestValueObjectSecond : TestValueObject 27 | { 28 | public TestValueObjectSecond(string data, int number) 29 | : base(data, number) 30 | { 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Tests/Xer.DomainDriven.Tests/InMemoryAggregateRootRepositoryTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using FluentAssertions; 4 | using Xer.DomainDriven.Repositories; 5 | using Xer.DomainDriven.Tests.Entities; 6 | using Xunit; 7 | 8 | namespace Xer.DomainDriven.Tests 9 | { 10 | public class InMemoryAggregateRootRepositoryTests 11 | { 12 | public class SaveAyncMethod 13 | { 14 | [Fact] 15 | public async Task ShouldSaveAggregateRoot() 16 | { 17 | var aggregateRoot = new TestAggregateRoot(Guid.NewGuid()); 18 | 19 | var repository = new InMemoryAggregateRootRepository(); 20 | await repository.SaveAsync(aggregateRoot); 21 | 22 | var result = await repository.GetByIdAsync(aggregateRoot.Id); 23 | 24 | result.Should().Be(aggregateRoot); 25 | } 26 | 27 | [Fact] 28 | public async Task ShouldReplaceExistingAggregateRoots() 29 | { 30 | var aggregateRoot1 = new TestAggregateRoot(Guid.NewGuid()); 31 | var aggregateRoot2 = new TestAggregateRoot(Guid.NewGuid()); 32 | 33 | var repository = new InMemoryAggregateRootRepository(); 34 | await repository.SaveAsync(aggregateRoot1); 35 | await repository.SaveAsync(aggregateRoot2); 36 | 37 | var result = await repository.GetByIdAsync(aggregateRoot2.Id); 38 | 39 | result.Should().Be(aggregateRoot2); 40 | } 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /Tests/Xer.DomainDriven.Tests/ValueObjectTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Xer.DomainDriven.Tests.Entities; 4 | using Xunit; 5 | 6 | namespace Xer.DomainDriven.Tests 7 | { 8 | public class ValueObjectTests 9 | { 10 | public class Equality 11 | { 12 | [Fact] 13 | public void EqualityOperatorShouldBeTrueIfValueObjectsMatchByValue() 14 | { 15 | TestValueObject valueObject1 = new TestValueObject("Test", 123); 16 | TestValueObject valueObject2 = new TestValueObject("Test", 123); 17 | 18 | (valueObject1 == valueObject2).Should().BeTrue(); 19 | } 20 | 21 | [Fact] 22 | public void EqualityOperatorShouldBeTrueIfValueObjectsAreTheSameReference() 23 | { 24 | TestValueObject valueObject1 = new TestValueObject("Test", 123); 25 | TestValueObject sameReference = valueObject1; 26 | 27 | (valueObject1 == sameReference).Should().BeTrue(); 28 | } 29 | 30 | [Fact] 31 | public void EqualityOperatorShouldBeFalseIfValueObjectsDoNotMatchByValue() 32 | { 33 | TestValueObject valueObject1 = new TestValueObject("Test", 123); 34 | TestValueObject valueObject2 = new TestValueObject("Test2", 1234); 35 | 36 | (valueObject1 == valueObject2).Should().BeFalse(); 37 | } 38 | 39 | [Fact] 40 | public void EqualityOperatorShouldBeFalseIfComparedWithNull() 41 | { 42 | TestValueObject valueObject1 = new TestValueObject("Test", 123); 43 | TestValueObject valueObject2 = null; 44 | 45 | (valueObject1 == valueObject2).Should().BeFalse(); 46 | } 47 | 48 | [Fact] 49 | public void EqualsShouldBeTrueIfValueObjectsMatchByValue() 50 | { 51 | TestValueObject valueObject1 = new TestValueObject("Test", 123); 52 | TestValueObject valueObject2 = new TestValueObject("Test", 123); 53 | 54 | valueObject1.Equals(valueObject2).Should().BeTrue(); 55 | } 56 | 57 | [Fact] 58 | public void EqualsShouldBeTrueIfValueObjectsAreTheSameReference() 59 | { 60 | TestValueObject valueObject1 = new TestValueObject("Test", 123); 61 | TestValueObject sameReference = valueObject1; 62 | 63 | valueObject1.Equals(sameReference).Should().BeTrue(); 64 | } 65 | 66 | [Fact] 67 | public void EqualsShouldNotBeTrueIfValueObjectsDoNotMatchByValue() 68 | { 69 | TestValueObject valueObject1 = new TestValueObject("Test", 123); 70 | TestValueObject valueObject2 = new TestValueObject("Test2", 1234); 71 | 72 | valueObject1.Equals(valueObject2).Should().BeFalse(); 73 | } 74 | 75 | [Fact] 76 | public void EqualsOperatorShouldNotBeTrueIfComparedWithNull() 77 | { 78 | TestValueObject valueObject1 = new TestValueObject("Test", 123); 79 | TestValueObject valueObject2 = null; 80 | 81 | valueObject1.Equals(valueObject2).Should().BeFalse(); 82 | } 83 | 84 | [Fact] 85 | public void ObjectEqualsShouldBeTrueIfValueObjectsMatchByValue() 86 | { 87 | TestValueObject valueObject1 = new TestValueObject("Test", 123); 88 | TestValueObject valueObject2 = new TestValueObject("Test", 123); 89 | 90 | valueObject1.Equals((object)valueObject2).Should().BeTrue(); 91 | } 92 | 93 | [Fact] 94 | public void ObjectEqualsShouldBeTrueIfValueObjectsAreTheSameReference() 95 | { 96 | TestValueObject valueObject1 = new TestValueObject("Test", 123); 97 | TestValueObject sameReference = valueObject1; 98 | 99 | valueObject1.Equals((object)sameReference).Should().BeTrue(); 100 | } 101 | 102 | [Fact] 103 | public void ObjectEqualsShouldNotBeTrueIfValueObjectsDoNotMatchByValue() 104 | { 105 | TestValueObject valueObject1 = new TestValueObject("Test", 123); 106 | TestValueObject valueObject2 = new TestValueObject("Test2", 1234); 107 | 108 | valueObject1.Equals((object)valueObject2).Should().BeFalse(); 109 | } 110 | 111 | [Fact] 112 | public void ObjectEqualsOperatorShouldNotBeTrueIfComparedWithNull() 113 | { 114 | TestValueObject valueObject1 = new TestValueObject("Test", 123); 115 | TestValueObject valueObject2 = null; 116 | 117 | valueObject1.Equals((object)valueObject2).Should().BeFalse(); 118 | } 119 | 120 | [Fact] 121 | public void ShouldNotBeEqualIfValueObjectsMatchByValueButDifferentType() 122 | { 123 | var valueObject1 = new TestValueObject("Test", 123); 124 | var valueObject2 = new TestValueObjectSecond("Test", 123); 125 | 126 | // Same value, should be equal. 127 | valueObject1.Should().NotBe(valueObject2); 128 | } 129 | } 130 | 131 | public class GetHashCodeMethod 132 | { 133 | [Fact] 134 | public void ShouldBeSameForTheSameInstance() 135 | { 136 | TestValueObject valueObject1 = new TestValueObject("Test", 123); 137 | 138 | int hashCode1 = valueObject1.GetHashCode(); 139 | int hashCode2 = valueObject1.GetHashCode(); 140 | 141 | hashCode1.Should().Be(hashCode2); 142 | } 143 | 144 | [Fact] 145 | public void ShouldBeSameForTheDifferentInstancesWithSameValues() 146 | { 147 | TestValueObject valueObject1 = new TestValueObject("Test", 123); 148 | TestValueObject valueObject2 = new TestValueObject("Test", 123); 149 | 150 | int hashCode1 = valueObject1.GetHashCode(); 151 | int hashCode2 = valueObject2.GetHashCode(); 152 | 153 | hashCode1.Should().Be(hashCode2); 154 | } 155 | 156 | [Fact] 157 | public void ShouldNotBeSameForTheDifferentInstancesWithDifferentValues() 158 | { 159 | TestValueObject valueObject1 = new TestValueObject("Test", 123); 160 | TestValueObject valueObject2 = new TestValueObject("Test2", 1234); 161 | 162 | int hashCode1 = valueObject1.GetHashCode(); 163 | int hashCode2 = valueObject2.GetHashCode(); 164 | 165 | hashCode1.Should().NotBe(hashCode2); 166 | } 167 | } 168 | } 169 | } -------------------------------------------------------------------------------- /Tests/Xer.DomainDriven.Tests/Xer.DomainDriven.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 | 22 | -------------------------------------------------------------------------------- /Tools/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Xer.DomainDriven.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xer.DomainDriven", "Src\Xer.DomainDriven\Xer.DomainDriven.csproj", "{B158F4B9-C157-411A-85FB-3F538432A92F}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{77122373-243D-48F3-A0DF-84F83D9467B2}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Xer.DomainDriven.Tests", "Tests\Xer.DomainDriven.Tests\Xer.DomainDriven.Tests.csproj", "{544D41FE-8042-40CC-A068-4ED39E519D9D}" 11 | EndProject 12 | Global 13 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 14 | Debug|Any CPU = Debug|Any CPU 15 | Debug|x64 = Debug|x64 16 | Debug|x86 = Debug|x86 17 | Release|Any CPU = Release|Any CPU 18 | Release|x64 = Release|x64 19 | Release|x86 = Release|x86 20 | EndGlobalSection 21 | GlobalSection(SolutionProperties) = preSolution 22 | HideSolutionNode = FALSE 23 | EndGlobalSection 24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 25 | {B158F4B9-C157-411A-85FB-3F538432A92F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {B158F4B9-C157-411A-85FB-3F538432A92F}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {B158F4B9-C157-411A-85FB-3F538432A92F}.Debug|x64.ActiveCfg = Debug|x64 28 | {B158F4B9-C157-411A-85FB-3F538432A92F}.Debug|x64.Build.0 = Debug|x64 29 | {B158F4B9-C157-411A-85FB-3F538432A92F}.Debug|x86.ActiveCfg = Debug|x86 30 | {B158F4B9-C157-411A-85FB-3F538432A92F}.Debug|x86.Build.0 = Debug|x86 31 | {B158F4B9-C157-411A-85FB-3F538432A92F}.Release|Any CPU.ActiveCfg = Release|Any CPU 32 | {B158F4B9-C157-411A-85FB-3F538432A92F}.Release|Any CPU.Build.0 = Release|Any CPU 33 | {B158F4B9-C157-411A-85FB-3F538432A92F}.Release|x64.ActiveCfg = Release|x64 34 | {B158F4B9-C157-411A-85FB-3F538432A92F}.Release|x64.Build.0 = Release|x64 35 | {B158F4B9-C157-411A-85FB-3F538432A92F}.Release|x86.ActiveCfg = Release|x86 36 | {B158F4B9-C157-411A-85FB-3F538432A92F}.Release|x86.Build.0 = Release|x86 37 | {544D41FE-8042-40CC-A068-4ED39E519D9D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {544D41FE-8042-40CC-A068-4ED39E519D9D}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {544D41FE-8042-40CC-A068-4ED39E519D9D}.Debug|x64.ActiveCfg = Debug|x64 40 | {544D41FE-8042-40CC-A068-4ED39E519D9D}.Debug|x64.Build.0 = Debug|x64 41 | {544D41FE-8042-40CC-A068-4ED39E519D9D}.Debug|x86.ActiveCfg = Debug|x86 42 | {544D41FE-8042-40CC-A068-4ED39E519D9D}.Debug|x86.Build.0 = Debug|x86 43 | {544D41FE-8042-40CC-A068-4ED39E519D9D}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {544D41FE-8042-40CC-A068-4ED39E519D9D}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {544D41FE-8042-40CC-A068-4ED39E519D9D}.Release|x64.ActiveCfg = Release|x64 46 | {544D41FE-8042-40CC-A068-4ED39E519D9D}.Release|x64.Build.0 = Release|x64 47 | {544D41FE-8042-40CC-A068-4ED39E519D9D}.Release|x86.ActiveCfg = Release|x86 48 | {544D41FE-8042-40CC-A068-4ED39E519D9D}.Release|x86.Build.0 = Release|x86 49 | EndGlobalSection 50 | GlobalSection(NestedProjects) = preSolution 51 | {544D41FE-8042-40CC-A068-4ED39E519D9D} = {77122373-243D-48F3-A0DF-84F83D9467B2} 52 | EndGlobalSection 53 | EndGlobal 54 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | pull_requests: 3 | do_not_increment_build_number: true 4 | image: Visual Studio 2017 5 | nuget: 6 | disable_publish_on_pr: true 7 | build_script: 8 | - ps: .\build.ps1 9 | test: off 10 | artifacts: 11 | - path: .\BuildArtifacts\*.nupkg 12 | name: NuGet 13 | skip_commits: 14 | message: /^\[nobuild\](.*)?/ # Skip build if commit message starts with [nobuild] 15 | deploy: 16 | - provider: NuGet 17 | server: https://www.myget.org/F/xerprojects-ci/api/v2/package 18 | api_key: 19 | secure: u04sQwcw2Dg6ymwifBf1PoYRwo6HrQOsEagB7IQYvwRPt+10tyFcrbuKq7Az34Oz 20 | skip_symbols: true 21 | on: 22 | branch: /(^dev$|^master$|^release[-/](.*)|^hotfix[-/](.*))/ # Branches: dev, master, release-*, release/*, hotfix-*, hotfix/* 23 | - provider: NuGet 24 | name: production 25 | skip_symbols: true 26 | api_key: 27 | secure: 1fEVy/0Jgny/LKUOQC75fofhRjEfpAaVV0Y8u3nH+oKdmrPFFFEF1swA+iS0W0rV 28 | on: 29 | branch: master 30 | appveyor_repo_tag: true # Only deploy to NuGet if a tag is found. -------------------------------------------------------------------------------- /build.cake: -------------------------------------------------------------------------------- 1 | /////////////////////////////////////////////////////////////////////////////// 2 | // ADDINS/TOOLS 3 | /////////////////////////////////////////////////////////////////////////////// 4 | #tool "nuget:?package=GitVersion.CommandLine" 5 | #addin nuget:?package=Cake.Git 6 | 7 | /////////////////////////////////////////////////////////////////////////////// 8 | // ARGUMENTS 9 | /////////////////////////////////////////////////////////////////////////////// 10 | 11 | var target = Argument("target", "Default"); 12 | var configuration = Argument("configuration", "Release"); 13 | 14 | /////////////////////////////////////////////////////////////////////////////// 15 | // GLOBAL VARIABLES 16 | /////////////////////////////////////////////////////////////////////////////// 17 | 18 | var solutions = GetFiles("./**/*.sln"); 19 | var projects = GetFiles("./**/*.csproj").Select(x => x.GetDirectory()); 20 | BuildParameters buildParameters; 21 | 22 | /////////////////////////////////////////////////////////////////////////////// 23 | // SETUP / TEARDOWN 24 | /////////////////////////////////////////////////////////////////////////////// 25 | 26 | Setup(context => 27 | { 28 | buildParameters = new BuildParameters(Context); 29 | 30 | // Executed BEFORE the first task. 31 | Information("Xer.DomainDriven"); 32 | Information("==========================================================================================="); 33 | Information("Git Version"); 34 | Information("Semver: {0}", buildParameters.GitVersion.LegacySemVerPadded); 35 | Information("Major minor patch: {0}", buildParameters.GitVersion.MajorMinorPatch); 36 | Information("Assembly: {0}", buildParameters.GitVersion.AssemblySemVer); 37 | Information("Informational: {0}", buildParameters.GitVersion.InformationalVersion); 38 | if (DirectoryExists(buildParameters.BuildArtifactsDirectory)) 39 | { 40 | // Cleanup build artifacts. 41 | Information($"Cleaning up {buildParameters.BuildArtifactsDirectory} directory."); 42 | DeleteDirectory(buildParameters.BuildArtifactsDirectory, new DeleteDirectorySettings { Recursive = true }); 43 | } 44 | Information("==========================================================================================="); 45 | }); 46 | 47 | Teardown(context => 48 | { 49 | // Executed AFTER the last task. 50 | Information("Finished running tasks."); 51 | }); 52 | 53 | /////////////////////////////////////////////////////////////////////////////// 54 | // TASK DEFINITIONS 55 | /////////////////////////////////////////////////////////////////////////////// 56 | 57 | Task("Clean") 58 | .Description("Cleans all directories that are used during the build process.") 59 | .Does(() => 60 | { 61 | if (projects.Count() == 0) 62 | { 63 | Information("No projects found."); 64 | return; 65 | } 66 | 67 | // Clean solution directories. 68 | foreach (var project in projects) 69 | { 70 | Information("Cleaning {0}", project); 71 | DotNetCoreClean(project.FullPath); 72 | } 73 | }); 74 | 75 | Task("Restore") 76 | .Description("Restores all the NuGet packages that are used by the specified solution.") 77 | .Does(() => 78 | { 79 | if (solutions.Count() == 0) 80 | { 81 | Information("No solutions found."); 82 | return; 83 | } 84 | 85 | var settings = new DotNetCoreRestoreSettings 86 | { 87 | ArgumentCustomization = args => buildParameters.AppendVersionArguments(args) 88 | }; 89 | 90 | // Restore all NuGet packages. 91 | foreach (var solution in solutions) 92 | { 93 | Information("Restoring {0}...", solution); 94 | 95 | DotNetCoreRestore(solution.FullPath, settings); 96 | } 97 | }); 98 | 99 | Task("Build") 100 | .Description("Builds all the different parts of the project.") 101 | .IsDependentOn("Clean") 102 | .IsDependentOn("Restore") 103 | .Does(() => 104 | { 105 | if (solutions.Count() == 0) 106 | { 107 | Information("No solutions found."); 108 | return; 109 | } 110 | 111 | var settings = new DotNetCoreBuildSettings 112 | { 113 | Configuration = configuration, 114 | ArgumentCustomization = args => buildParameters.AppendVersionArguments(args) 115 | }; 116 | 117 | // Build all solutions. 118 | foreach (var solution in solutions) 119 | { 120 | Information("Building {0}", solution); 121 | 122 | DotNetCoreBuild(solution.FullPath, settings); 123 | } 124 | }); 125 | 126 | Task("Test") 127 | .Description("Execute all unit test projects.") 128 | .IsDependentOn("Build") 129 | .Does(() => 130 | { 131 | var projects = GetFiles("./Tests/**/*.Tests.csproj"); 132 | 133 | if (projects.Count == 0) 134 | { 135 | Information("No test projects found."); 136 | return; 137 | } 138 | 139 | var settings = new DotNetCoreTestSettings 140 | { 141 | Configuration = configuration, 142 | NoBuild = true, 143 | }; 144 | 145 | foreach (var project in projects) 146 | { 147 | DotNetCoreTest(project.FullPath, settings); 148 | } 149 | }); 150 | 151 | Task("Pack") 152 | .Description("Create NuGet packages.") 153 | .IsDependentOn("Test") 154 | .Does(() => 155 | { 156 | var projects = GetFiles("./Src/**/*.csproj"); 157 | 158 | if (projects.Count() == 0) 159 | { 160 | Information("No projects found."); 161 | return; 162 | } 163 | 164 | var settings = new DotNetCorePackSettings 165 | { 166 | OutputDirectory = buildParameters.BuildArtifactsDirectory, 167 | Configuration = configuration, 168 | NoBuild = true, 169 | ArgumentCustomization = args => buildParameters.AppendVersionArguments(args) 170 | }; 171 | 172 | foreach (var project in projects) 173 | { 174 | DotNetCorePack(project.ToString(), settings); 175 | } 176 | }); 177 | 178 | /////////////////////////////////////////////////////////////////////////////// 179 | // TARGETS 180 | /////////////////////////////////////////////////////////////////////////////// 181 | 182 | Task("Default") 183 | .Description("This is the default task which will be ran if no specific target is passed in.") 184 | .IsDependentOn("Pack") 185 | .IsDependentOn("Test") 186 | .IsDependentOn("Build") 187 | .IsDependentOn("Restore") 188 | .IsDependentOn("Clean"); 189 | 190 | /////////////////////////////////////////////////////////////////////////////// 191 | // EXECUTION 192 | /////////////////////////////////////////////////////////////////////////////// 193 | 194 | RunTarget(target); 195 | 196 | public class BuildParameters 197 | { 198 | private ICakeContext _context; 199 | private GitVersion _gitVersion; 200 | 201 | public BuildParameters(ICakeContext context) 202 | { 203 | _context = context; 204 | _gitVersion = context.GitVersion(); 205 | } 206 | 207 | public GitVersion GitVersion => _gitVersion; 208 | 209 | public string BuildArtifactsDirectory => "./BuildArtifacts"; 210 | 211 | public ProcessArgumentBuilder AppendVersionArguments(ProcessArgumentBuilder args) => args 212 | .Append("/p:Version={0}", GitVersion.LegacySemVerPadded) 213 | .Append("/p:AssemblyVersion={0}", GitVersion.MajorMinorPatch) 214 | .Append("/p:FileVersion={0}", GitVersion.MajorMinorPatch) 215 | .Append("/p:AssemblyInformationalVersion={0}", GitVersion.InformationalVersion); 216 | } 217 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | ########################################################################## 2 | # This is the Cake bootstrapper script for PowerShell. 3 | # This file was downloaded from https://github.com/cake-build/resources 4 | # Feel free to change this file to fit your needs. 5 | ########################################################################## 6 | 7 | <# 8 | 9 | .SYNOPSIS 10 | This is a Powershell script to bootstrap a Cake build. 11 | 12 | .DESCRIPTION 13 | This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) 14 | and execute your Cake build script with the parameters you provide. 15 | 16 | .PARAMETER Script 17 | The build script to execute. 18 | .PARAMETER Target 19 | The build script target to run. 20 | .PARAMETER Configuration 21 | The build configuration to use. 22 | .PARAMETER Verbosity 23 | Specifies the amount of information to be displayed. 24 | .PARAMETER ShowDescription 25 | Shows description about tasks. 26 | .PARAMETER DryRun 27 | Performs a dry run. 28 | .PARAMETER Experimental 29 | Uses the nightly builds of the Roslyn script engine. 30 | .PARAMETER Mono 31 | Uses the Mono Compiler rather than the Roslyn script engine. 32 | .PARAMETER SkipToolPackageRestore 33 | Skips restoring of packages. 34 | .PARAMETER ScriptArgs 35 | Remaining arguments are added here. 36 | 37 | .LINK 38 | https://cakebuild.net 39 | 40 | #> 41 | 42 | [CmdletBinding()] 43 | Param( 44 | [string]$Script = "build.cake", 45 | [string]$Target, 46 | [string]$Configuration, 47 | [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] 48 | [string]$Verbosity, 49 | [switch]$ShowDescription, 50 | [Alias("WhatIf", "Noop")] 51 | [switch]$DryRun, 52 | [switch]$Experimental, 53 | [switch]$Mono, 54 | [switch]$SkipToolPackageRestore, 55 | [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] 56 | [string[]]$ScriptArgs 57 | ) 58 | 59 | [Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null 60 | function MD5HashFile([string] $filePath) 61 | { 62 | if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf)) 63 | { 64 | return $null 65 | } 66 | 67 | [System.IO.Stream] $file = $null; 68 | [System.Security.Cryptography.MD5] $md5 = $null; 69 | try 70 | { 71 | $md5 = [System.Security.Cryptography.MD5]::Create() 72 | $file = [System.IO.File]::OpenRead($filePath) 73 | return [System.BitConverter]::ToString($md5.ComputeHash($file)) 74 | } 75 | finally 76 | { 77 | if ($file -ne $null) 78 | { 79 | $file.Dispose() 80 | } 81 | } 82 | } 83 | 84 | function GetProxyEnabledWebClient 85 | { 86 | $wc = New-Object System.Net.WebClient 87 | $proxy = [System.Net.WebRequest]::GetSystemWebProxy() 88 | $proxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials 89 | $wc.Proxy = $proxy 90 | return $wc 91 | } 92 | 93 | Write-Host "Preparing to run build script..." 94 | 95 | if(!$PSScriptRoot){ 96 | $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent 97 | } 98 | 99 | $TOOLS_DIR = Join-Path $PSScriptRoot "tools" 100 | $ADDINS_DIR = Join-Path $TOOLS_DIR "Addins" 101 | $MODULES_DIR = Join-Path $TOOLS_DIR "Modules" 102 | $NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" 103 | $CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" 104 | $NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" 105 | $PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" 106 | $PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" 107 | $ADDINS_PACKAGES_CONFIG = Join-Path $ADDINS_DIR "packages.config" 108 | $MODULES_PACKAGES_CONFIG = Join-Path $MODULES_DIR "packages.config" 109 | 110 | # Make sure tools folder exists 111 | if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { 112 | Write-Verbose -Message "Creating tools directory..." 113 | New-Item -Path $TOOLS_DIR -Type directory | out-null 114 | } 115 | 116 | # Make sure that packages.config exist. 117 | if (!(Test-Path $PACKAGES_CONFIG)) { 118 | Write-Verbose -Message "Downloading packages.config..." 119 | try { 120 | $wc = GetProxyEnabledWebClient 121 | $wc.DownloadFile("https://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch { 122 | Throw "Could not download packages.config." 123 | } 124 | } 125 | 126 | # Try find NuGet.exe in path if not exists 127 | if (!(Test-Path $NUGET_EXE)) { 128 | Write-Verbose -Message "Trying to find nuget.exe in PATH..." 129 | $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_ -PathType Container) } 130 | $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 131 | if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { 132 | Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." 133 | $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName 134 | } 135 | } 136 | 137 | # Try download NuGet.exe if not exists 138 | if (!(Test-Path $NUGET_EXE)) { 139 | Write-Verbose -Message "Downloading NuGet.exe..." 140 | try { 141 | $wc = GetProxyEnabledWebClient 142 | $wc.DownloadFile($NUGET_URL, $NUGET_EXE) 143 | } catch { 144 | Throw "Could not download NuGet.exe." 145 | } 146 | } 147 | 148 | # Save nuget.exe path to environment to be available to child processed 149 | $ENV:NUGET_EXE = $NUGET_EXE 150 | 151 | # Restore tools from NuGet? 152 | if(-Not $SkipToolPackageRestore.IsPresent) { 153 | Push-Location 154 | Set-Location $TOOLS_DIR 155 | 156 | # Check for changes in packages.config and remove installed tools if true. 157 | [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG) 158 | if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or 159 | ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { 160 | Write-Verbose -Message "Missing or changed package.config hash..." 161 | Get-ChildItem -Exclude packages.config,nuget.exe,Cake.Bakery | 162 | Remove-Item -Recurse 163 | } 164 | 165 | Write-Verbose -Message "Restoring tools from NuGet..." 166 | $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" 167 | 168 | if ($LASTEXITCODE -ne 0) { 169 | Throw "An error occurred while restoring NuGet tools." 170 | } 171 | else 172 | { 173 | $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" 174 | } 175 | Write-Verbose -Message ($NuGetOutput | out-string) 176 | 177 | Pop-Location 178 | } 179 | 180 | # Restore addins from NuGet 181 | if (Test-Path $ADDINS_PACKAGES_CONFIG) { 182 | Push-Location 183 | Set-Location $ADDINS_DIR 184 | 185 | Write-Verbose -Message "Restoring addins from NuGet..." 186 | $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$ADDINS_DIR`"" 187 | 188 | if ($LASTEXITCODE -ne 0) { 189 | Throw "An error occurred while restoring NuGet addins." 190 | } 191 | 192 | Write-Verbose -Message ($NuGetOutput | out-string) 193 | 194 | Pop-Location 195 | } 196 | 197 | # Restore modules from NuGet 198 | if (Test-Path $MODULES_PACKAGES_CONFIG) { 199 | Push-Location 200 | Set-Location $MODULES_DIR 201 | 202 | Write-Verbose -Message "Restoring modules from NuGet..." 203 | $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$MODULES_DIR`"" 204 | 205 | if ($LASTEXITCODE -ne 0) { 206 | Throw "An error occurred while restoring NuGet modules." 207 | } 208 | 209 | Write-Verbose -Message ($NuGetOutput | out-string) 210 | 211 | Pop-Location 212 | } 213 | 214 | # Make sure that Cake has been installed. 215 | if (!(Test-Path $CAKE_EXE)) { 216 | Throw "Could not find Cake.exe at $CAKE_EXE" 217 | } 218 | 219 | 220 | 221 | # Build Cake arguments 222 | $cakeArguments = @("$Script"); 223 | if ($Target) { $cakeArguments += "-target=$Target" } 224 | if ($Configuration) { $cakeArguments += "-configuration=$Configuration" } 225 | if ($Verbosity) { $cakeArguments += "-verbosity=$Verbosity" } 226 | if ($ShowDescription) { $cakeArguments += "-showdescription" } 227 | if ($DryRun) { $cakeArguments += "-dryrun" } 228 | if ($Experimental) { $cakeArguments += "-experimental" } 229 | if ($Mono) { $cakeArguments += "-mono" } 230 | $cakeArguments += $ScriptArgs 231 | 232 | # Start Cake 233 | Write-Host "Running build script..." 234 | &$CAKE_EXE $cakeArguments 235 | exit $LASTEXITCODE 236 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ########################################################################## 4 | # This is the Cake bootstrapper script for Linux and OS X. 5 | # This file was downloaded from https://github.com/cake-build/resources 6 | # Feel free to change this file to fit your needs. 7 | ########################################################################## 8 | 9 | # Define directories. 10 | SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 11 | TOOLS_DIR=$SCRIPT_DIR/tools 12 | ADDINS_DIR=$TOOLS_DIR/Addins 13 | MODULES_DIR=$TOOLS_DIR/Modules 14 | NUGET_EXE=$TOOLS_DIR/nuget.exe 15 | CAKE_EXE=$TOOLS_DIR/Cake/Cake.exe 16 | PACKAGES_CONFIG=$TOOLS_DIR/packages.config 17 | PACKAGES_CONFIG_MD5=$TOOLS_DIR/packages.config.md5sum 18 | ADDINS_PACKAGES_CONFIG=$ADDINS_DIR/packages.config 19 | MODULES_PACKAGES_CONFIG=$MODULES_DIR/packages.config 20 | 21 | # Define md5sum or md5 depending on Linux/OSX 22 | MD5_EXE= 23 | if [[ "$(uname -s)" == "Darwin" ]]; then 24 | MD5_EXE="md5 -r" 25 | else 26 | MD5_EXE="md5sum" 27 | fi 28 | 29 | # Define default arguments. 30 | SCRIPT="build.cake" 31 | CAKE_ARGUMENTS=() 32 | 33 | # Parse arguments. 34 | for i in "$@"; do 35 | case $1 in 36 | -s|--script) SCRIPT="$2"; shift ;; 37 | --) shift; CAKE_ARGUMENTS+=("$@"); break ;; 38 | *) CAKE_ARGUMENTS+=("$1") ;; 39 | esac 40 | shift 41 | done 42 | 43 | # Make sure the tools folder exist. 44 | if [ ! -d "$TOOLS_DIR" ]; then 45 | mkdir "$TOOLS_DIR" 46 | fi 47 | 48 | # Make sure that packages.config exist. 49 | if [ ! -f "$TOOLS_DIR/packages.config" ]; then 50 | echo "Downloading packages.config..." 51 | curl -Lsfo "$TOOLS_DIR/packages.config" https://cakebuild.net/download/bootstrapper/packages 52 | if [ $? -ne 0 ]; then 53 | echo "An error occurred while downloading packages.config." 54 | exit 1 55 | fi 56 | fi 57 | 58 | # Download NuGet if it does not exist. 59 | if [ ! -f "$NUGET_EXE" ]; then 60 | echo "Downloading NuGet..." 61 | curl -Lsfo "$NUGET_EXE" https://dist.nuget.org/win-x86-commandline/latest/nuget.exe 62 | if [ $? -ne 0 ]; then 63 | echo "An error occurred while downloading nuget.exe." 64 | exit 1 65 | fi 66 | fi 67 | 68 | # Restore tools from NuGet. 69 | pushd "$TOOLS_DIR" >/dev/null 70 | if [ ! -f "$PACKAGES_CONFIG_MD5" ] || [ "$( cat "$PACKAGES_CONFIG_MD5" | sed 's/\r$//' )" != "$( $MD5_EXE "$PACKAGES_CONFIG" | awk '{ print $1 }' )" ]; then 71 | find . -type d ! -name . ! -name 'Cake.Bakery' | xargs rm -rf 72 | fi 73 | 74 | mono "$NUGET_EXE" install -ExcludeVersion 75 | if [ $? -ne 0 ]; then 76 | echo "Could not restore NuGet tools." 77 | exit 1 78 | fi 79 | 80 | $MD5_EXE "$PACKAGES_CONFIG" | awk '{ print $1 }' >| "$PACKAGES_CONFIG_MD5" 81 | 82 | popd >/dev/null 83 | 84 | # Restore addins from NuGet. 85 | if [ -f "$ADDINS_PACKAGES_CONFIG" ]; then 86 | pushd "$ADDINS_DIR" >/dev/null 87 | 88 | mono "$NUGET_EXE" install -ExcludeVersion 89 | if [ $? -ne 0 ]; then 90 | echo "Could not restore NuGet addins." 91 | exit 1 92 | fi 93 | 94 | popd >/dev/null 95 | fi 96 | 97 | # Restore modules from NuGet. 98 | if [ -f "$MODULES_PACKAGES_CONFIG" ]; then 99 | pushd "$MODULES_DIR" >/dev/null 100 | 101 | mono "$NUGET_EXE" install -ExcludeVersion 102 | if [ $? -ne 0 ]; then 103 | echo "Could not restore NuGet modules." 104 | exit 1 105 | fi 106 | 107 | popd >/dev/null 108 | fi 109 | 110 | # Make sure that Cake has been installed. 111 | if [ ! -f "$CAKE_EXE" ]; then 112 | echo "Could not find Cake.exe at '$CAKE_EXE'." 113 | exit 1 114 | fi 115 | 116 | # Start Cake 117 | exec mono "$CAKE_EXE" $SCRIPT "${CAKE_ARGUMENTS[@]}" 118 | --------------------------------------------------------------------------------