├── .gitignore ├── License.txt ├── README.md ├── lib └── nuget │ └── .gitignore ├── nuget ├── Enflow.Base │ ├── Enflow.Base.nuspec │ └── lib │ │ └── Enflow.Base.dll ├── NuGet.exe └── pack.cmd └── src ├── .nuget ├── NuGet.Config ├── NuGet.exe └── NuGet.targets ├── Enflow.Base.Test ├── Enflow.Base.Test.csproj ├── FlowableTests.cs ├── Properties │ └── AssemblyInfo.cs ├── StateRuleTests.cs ├── TestingImplementations.cs ├── WorkflowTests.cs └── packages.config ├── Enflow.Base ├── Enflow.Base.csproj ├── Exceptions │ ├── StateRuleException.cs │ └── WorkflowException.cs ├── Flowable.cs ├── ParameterReplacer.cs ├── Properties │ └── AssemblyInfo.cs ├── StateRule.cs └── Workflow.cs └── Enflow.sln /.gitignore: -------------------------------------------------------------------------------- 1 | # Build Folders (you can keep bin if you'd like, to store dlls and pdbs) 2 | [Bb]in/ 3 | [Oo]bj/ 4 | 5 | # mstest test results 6 | TestResults 7 | 8 | ## Ignore Visual Studio temporary files, build results, and 9 | ## files generated by popular Visual Studio add-ons. 10 | 11 | # User-specific files 12 | *.suo 13 | *.user 14 | *.sln.docstates 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Rr]elease/ 19 | x64/ 20 | *_i.c 21 | *_p.c 22 | *.ilk 23 | *.meta 24 | *.obj 25 | *.pch 26 | *.pdb 27 | *.pgc 28 | *.pgd 29 | *.rsp 30 | *.sbr 31 | *.tlb 32 | *.tli 33 | *.tlh 34 | *.tmp 35 | *.log 36 | *.vspscc 37 | *.vssscc 38 | .builds 39 | 40 | # Visual C++ cache files 41 | ipch/ 42 | *.aps 43 | *.ncb 44 | *.opensdf 45 | *.sdf 46 | 47 | # Visual Studio profiler 48 | *.psess 49 | *.vsp 50 | *.vspx 51 | 52 | # Guidance Automation Toolkit 53 | *.gpState 54 | 55 | # ReSharper is a .NET coding add-in 56 | _ReSharper* 57 | 58 | # NCrunch 59 | *.ncrunch* 60 | .*crunch*.local.xml 61 | 62 | # Installshield output folder 63 | [Ee]xpress 64 | 65 | # DocProject is a documentation generator add-in 66 | DocProject/buildhelp/ 67 | DocProject/Help/*.HxT 68 | DocProject/Help/*.HxC 69 | DocProject/Help/*.hhc 70 | DocProject/Help/*.hhk 71 | DocProject/Help/*.hhp 72 | DocProject/Help/Html2 73 | DocProject/Help/html 74 | 75 | # Click-Once directory 76 | publish 77 | 78 | # Publish Web Output 79 | *.Publish.xml 80 | 81 | # NuGet Packages Directory 82 | packages 83 | 84 | # Windows Azure Build Output 85 | csx 86 | *.build.csdef 87 | 88 | # Windows Store app package directory 89 | AppPackages/ 90 | 91 | # Others 92 | [Bb]in 93 | [Oo]bj 94 | sql 95 | TestResults 96 | [Tt]est[Rr]esult* 97 | *.Cache 98 | ClientBin 99 | [Ss]tyle[Cc]op.* 100 | ~$* 101 | *.dbmdl 102 | Generated_Code #added for RIA/Silverlight projects 103 | 104 | # Backup & report files from converting an old project file to a newer 105 | # Visual Studio version. Backup files are not needed, because we have git ;-) 106 | _UpgradeReport_Files/ 107 | Backup*/ 108 | UpgradeLog*.XML 109 | 110 | lib/nuget/* -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Joseph Phillips 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](https://dl.dropboxusercontent.com/u/35984366/enflow_logo3_m.png) 2 | 3 | Enflow is a simple library for workflows and state/business rules. It is a potential replacement for the _Unit of Work_ pattern popular in MVC applications, particularly where model state validation must accompany units of work. 4 | 5 | Usage is not limited to MVC. Enflow is a [Portable Class Library](http://msdn.microsoft.com/en-us/library/gg597391.aspx) (PCL) and works across multiple platforms. For more information, including usage in _Xamarin.Android_ and _Xamarin.iOS_, see [this blog post](http://slodge.blogspot.sk/2012/12/cross-platform-winrt-monodroid.html) (and [update](http://slodge.blogspot.ca/2013/04/my-current-pcl-setup-in-visual-studio.html)) by Stuart Lodge ([@slodge](https://twitter.com/slodge)). _Note: Currently experiencing a problem targeting Xamarin.Android and Xamarin.iOS. Working to resolve._ 6 | 7 | Enflow is available via [NuGet](https://nuget.org/packages/Enflow/). 8 | 9 | ### Models 10 | 11 | Note: It is no longer necessary to mark types with the ```IModel``` interface. This has been removed - Enflow is now much more versatile. 12 | 13 | ### State Rules 14 | 15 | Create rules based on any type and use the fluent API to create composite rules from atomic constituents. 16 | ```csharp 17 | 18 | public class Employee 19 | { 20 | public string Name { get; set; } 21 | public string Department { get; set; } 22 | public string Salary { get; set; } 23 | } 24 | 25 | public class MaxSalaryRule : StateRule 26 | { 27 | public override Expression> 28 | { 29 | get { return candidate => candidate.Salary < 40000; } 30 | } 31 | } 32 | 33 | public class InHrDepartmentRule : StateRule 34 | { 35 | public override Expression> 36 | { 37 | get { return candidate => candidate.Department == "Human Resources"; } 38 | } 39 | } 40 | 41 | // Compose a new rule from the others and describe it. 42 | var salaryRaiseRule = new MaxSalaryRule() 43 | .And(new InHrDepartmentRule()) 44 | .Describe("Employee must be in the HR deparment and have a salary less than $40,000."); 45 | 46 | // Candidates can then be checked against a rule to see if they satisfy it. 47 | var isEligible = salaryRaiseRule.IsSatified(someEmployee); 48 | 49 | // A rule can also be used to filter an IQueryable via the Predicate property. 50 | // Depending on the actual expression, this will also work with LINQ to Entities. 51 | var eligibleEmployees = Employees.Where(salaryRaiseRule.Predicate); 52 | ``` 53 | 54 | ### Workflows 55 | 56 | This is our new _Unit of Work_. Instantiate, passing in the rule to be validated. If the rule validation fails, a _StateRuleException_ will be thrown with the rule description as the message. 57 | ```csharp 58 | // Example incorporating a repository pattern. 59 | public class ApplySalaryRaise : Workflow 60 | { 61 | private readonly IRepository _repository; 62 | 63 | public ApplySalaryRaise(IStateRule rule, IRepository repository) : base(rule) 64 | { 65 | _repository = repository; 66 | } 67 | 68 | protected override Employee ExecuteWorkflow(Employee candidate) 69 | { 70 | candidate.Salary += 5000; 71 | _repository.Update(candidate); 72 | return candidate; 73 | } 74 | } 75 | 76 | // Some example candidates for our workflow. 77 | var eligibleEmployee = new Employee 78 | { 79 | Name = "John Smith", 80 | Department = "Human Resources", 81 | Salary = 10000; 82 | }; 83 | 84 | var ineligibleEmployee = new Employee 85 | { 86 | Name = "Tye Tarse", 87 | Department = "Board of Directors", 88 | Salary = 1000000 89 | }; 90 | 91 | // Give the candidate employee a $5000 raise if they're in HR and they earn less than $40k. 92 | var salaryRaiseWorflow = new ApplySalaryRaise(salaryRaiseRule, new EmployeeRepository()); 93 | 94 | // Input candidate will be granted the salary raise. 95 | salaryRaiseWorflow.Execute(eligibleEmployee); 96 | 97 | // This will throw a StateRuleException. 98 | salaryRaiseWorflow.Execute(ineligibleEmployee); 99 | ``` 100 | 101 | ### Flowable 102 | 103 | Flowable is a type amplification wrapper that allows fluent, declaritive use of Enflow. 104 | 105 | ```csharp 106 | // It is possible to chain workflows using flowable. 107 | // This would apply two pay increases as long as the salary remained under $40k. 108 | eligibleEmployee 109 | .AsFlowable() 110 | .Flow(salaryRaiseWorflow) 111 | .Flow(salaryRaiseWorflow); 112 | 113 | // State rule checks can also be applied directly to flowables. 114 | var canThisPersonGetMore = eligibleEmployee 115 | .AsFlowable() 116 | .Flow(salaryRaiseWorflow) 117 | .Satisfies(salaryRaiseRule); 118 | ``` 119 | 120 | An ```IWorkflow``` returns _T_ from its call to _Execute_. ```IWorkflow``` allows chaining together sequences of workflows that pass different types between them. This means complex workflows can be expressed as compositions of smaller ones, increasing modularity, re-use and testability. 121 | 122 | ```csharp 123 | public class Department 124 | { 125 | public string Name { get; set; } 126 | public int EmployeeCount { get; set; } 127 | public string Building { get; set; } 128 | } 129 | 130 | // These examples have the repository/persistence code omitted for clarity. 131 | 132 | public class MoveHrPersonToNewDepartment : Workflow 133 | { 134 | public Department Destination { get; set; } 135 | 136 | protected override Department ExecuteWorkflow(Employee candidate) 137 | { 138 | candidate.Department = Destination.Name; 139 | Destination.EmployeeCount++; 140 | return Destination; 141 | } 142 | } 143 | 144 | public class MoveToTheNewOffice : Workflow 145 | { 146 | protected override Department ExecuteWorkflow(Department candidate) 147 | { 148 | candidate.Building = "Uptown Office"; 149 | return candidate; 150 | } 151 | } 152 | 153 | var moveToBoardWorkflow = new MoveHrPersonToNewDepartment(new InHrDepartmentRule()) 154 | { 155 | // Something like this would come from the DB in a less contrived example, of course. 156 | Department = new Department 157 | { 158 | Name = "Board of Directors", 159 | EmployeeCount = 5, 160 | Building = "Downtown Office" 161 | } 162 | }; 163 | 164 | var moveDeptOfficeWorkflow = new MoveToTheNewOffice(); 165 | 166 | var employeeLocation = eligibleEmployee 167 | .AsFlowable() 168 | .Flow(salaryRaiseWorflow) 169 | .Flow(moveToBoardWorkflow) 170 | .Flow(moveDeptOfficeWorkflow) 171 | .Value 172 | .Building; // "Uptown Office" 173 | ``` 174 | 175 | ### The Workflow Factory 176 | 177 | Note: The workflow factory has been removed from Enflow. 178 | 179 | ### License 180 | 181 | Unless otherwise specified, all content in this repository is licensed as follows. 182 | 183 | The MIT License (MIT) 184 | 185 | __Copyright (c) 2013 Joseph Phillips__ 186 | 187 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 188 | 189 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 190 | 191 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 192 | -------------------------------------------------------------------------------- /lib/nuget/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /nuget/Enflow.Base/Enflow.Base.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Enflow 5 | 0.4.1 6 | Joseph Phillips 7 | Joseph Phillips 8 | https://github.com/SpiritMachine/Enflow/blob/master/License.txt 9 | https://github.com/SpiritMachine/Enflow 10 | https://dl.dropboxusercontent.com/u/35984366/enflow_logo3_sm.png 11 | false 12 | Enflow is a simple library for workflows and state rules. It aims to provide a simple, cross-platform unit-of-work facility. The state rule definitions can also be used to filter IQueryable and IEnumerable, including LINQ to Entities and other ORMs that can query by expressions. 13 | Workflow BusinessRules State Specification UnitOfWork PCL LINQ Expression 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /nuget/Enflow.Base/lib/Enflow.Base.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manadart/Enflow/3a98a63fbbee5b13056836b765d1cec5f084363c/nuget/Enflow.Base/lib/Enflow.Base.dll -------------------------------------------------------------------------------- /nuget/NuGet.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manadart/Enflow/3a98a63fbbee5b13056836b765d1cec5f084363c/nuget/NuGet.exe -------------------------------------------------------------------------------- /nuget/pack.cmd: -------------------------------------------------------------------------------- 1 | nuget pack Enflow.Base\Enflow.Base.nuspec -------------------------------------------------------------------------------- /src/.nuget/NuGet.Config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/.nuget/NuGet.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/manadart/Enflow/3a98a63fbbee5b13056836b765d1cec5f084363c/src/.nuget/NuGet.exe -------------------------------------------------------------------------------- /src/.nuget/NuGet.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(MSBuildProjectDirectory)\..\ 5 | 6 | 7 | false 8 | 9 | 10 | false 11 | 12 | 13 | true 14 | 15 | 16 | false 17 | 18 | 19 | 20 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | $([System.IO.Path]::Combine($(SolutionDir), ".nuget")) 31 | $([System.IO.Path]::Combine($(ProjectDir), "packages.config")) 32 | $([System.IO.Path]::Combine($(SolutionDir), "..\lib\nuget")) 33 | 34 | 35 | 36 | 37 | $(SolutionDir).nuget 38 | packages.config 39 | $([System.IO.Path]::Combine($(SolutionDir), "..\lib\nuget")) 40 | 41 | 42 | 43 | 44 | $(NuGetToolsPath)\nuget.exe 45 | @(PackageSource) 46 | 47 | "$(NuGetExePath)" 48 | mono --runtime=v4.0.30319 $(NuGetExePath) 49 | 50 | $(TargetDir.Trim('\\')) 51 | 52 | -RequireConsent 53 | 54 | $(NuGetCommand) install "$(PackagesConfig)" -source "$(PackageSources)" $(RequireConsentSwitch) -solutionDir "$(SolutionDir) " 55 | $(NuGetCommand) pack "$(ProjectPath)" -p Configuration=$(Configuration) -o "$(PackageOutputDir)" -symbols 56 | 57 | 58 | 59 | RestorePackages; 60 | $(BuildDependsOn); 61 | 62 | 63 | 64 | 65 | $(BuildDependsOn); 66 | BuildPackage; 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 89 | 90 | 93 | 94 | 95 | 96 | 98 | 99 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 150 | 151 | 152 | 153 | -------------------------------------------------------------------------------- /src/Enflow.Base.Test/Enflow.Base.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Debug 5 | AnyCPU 6 | 8.0.30703 7 | 2.0 8 | {130675EB-3D40-489B-B806-970EB0ED08D7} 9 | Library 10 | Properties 11 | Enflow.Base.Test 12 | Enflow.Base.Test 13 | v4.0 14 | 512 15 | ..\ 16 | true 17 | 18 | 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | ..\..\lib\nuget\NSubstitute.1.5.0.0\lib\NET40\NSubstitute.dll 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ..\..\lib\nuget\xunit.1.9.1\lib\net20\xunit.dll 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | Designer 60 | 61 | 62 | 63 | 64 | {297DE7BE-45CE-4C54-86EC-E999CBC5A7E9} 65 | Enflow.Base 66 | 67 | 68 | 69 | 70 | 77 | -------------------------------------------------------------------------------- /src/Enflow.Base.Test/FlowableTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Xunit; 3 | 4 | namespace Enflow.Base.Test 5 | { 6 | public class FlowableTests 7 | { 8 | [Fact] 9 | public void NullAsFlowableIsEmpty() 10 | { 11 | string wrapped = null; 12 | var flowableString = wrapped.AsFlowable(); 13 | Assert.False(flowableString.HasValue); 14 | } 15 | 16 | [Fact] 17 | public void CanExecuteChainedWorkflowsOfSameTypeViaFlow() 18 | { 19 | var workflow = new GuidCountIncrementWorkflow(); 20 | Assert.Equal(2, new GuidCount().AsFlowable().Flow(workflow).Flow(workflow).Value.Counter); 21 | } 22 | 23 | [Fact] 24 | public void CanExecuteChainedWorkflowsOfDifferentTypeViaFlow() 25 | { 26 | var incWorkflow = new GuidCountIncrementWorkflow(); 27 | var dupWorkflow = new GuidCountDuplicateWorkflow(); 28 | 29 | var results = new GuidCount().AsFlowable().Flow(incWorkflow).Flow(incWorkflow).Flow(dupWorkflow).Value; 30 | Assert.Equal(2, results.Count); 31 | Assert.True(results.All(r => r.Counter == 2)); 32 | } 33 | 34 | [Fact] 35 | public void CanTestStateRuleViaExtension() 36 | { 37 | Assert.True(new GuidCount().AsFlowable().Flow(new GuidCountIncrementWorkflow()).Satisfies(new LessThanTenCounterRule())); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Enflow.Base.Test/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("Adeptech.FluentWork.Base.Test")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("Geomatic Technologies Pty Ltd")] 12 | [assembly: AssemblyProduct("Adeptech.FluentWork.Base.Test")] 13 | [assembly: AssemblyCopyright("Copyright © Geomatic Technologies Pty Ltd 2013")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("a9935d0a-6bdf-452c-b6fa-94c73c5b7a37")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/Enflow.Base.Test/StateRuleTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using Xunit; 6 | 7 | namespace Enflow.Base.Test 8 | { 9 | public class PositiveCounterRule : StateRule 10 | { 11 | public override Expression> Predicate { get { return candidate => candidate.Counter > 0; } } 12 | } 13 | 14 | public class NegativeCounterRule : StateRule 15 | { 16 | public override Expression> Predicate { get { return candidate => candidate.Counter < 0; } } 17 | } 18 | 19 | public class StateRuleTests 20 | { 21 | [Fact] 22 | public void SimpleSatisfiedRuleReturnsTrue() 23 | { 24 | var model = new GuidCount(); 25 | model.Increment(); 26 | Assert.True(new PositiveCounterRule().IsSatisfied(model)); 27 | } 28 | 29 | [Fact] 30 | public void DescribeExtensionSetsDescription() 31 | { 32 | const string ruleDescription = "This rule enforces a counter > 0."; 33 | Assert.Equal(ruleDescription, new PositiveCounterRule().Describe(ruleDescription).Description); 34 | } 35 | 36 | [Fact] 37 | public void OrExtensionEvaluatesCorrectly() 38 | { 39 | var model = new GuidCount(); 40 | model.Increment(); 41 | Assert.True(new PositiveCounterRule().Or(new NegativeCounterRule()).IsSatisfied(model)); 42 | } 43 | 44 | [Fact] 45 | public void AndExtensionEvaluatesCorrectly() 46 | { 47 | var model = new GuidCount(); 48 | model.Increment(); 49 | Assert.False(new PositiveCounterRule().And(new NegativeCounterRule()).IsSatisfied(model)); 50 | } 51 | 52 | [Fact] 53 | public void NotExtensionEvaluesCorrectly() 54 | { 55 | var model = new GuidCount(); 56 | model.Increment(); 57 | Assert.False(new PositiveCounterRule().Not().IsSatisfied(model)); 58 | } 59 | 60 | [Fact] 61 | public void PredicateFiltersCorrectly() 62 | { 63 | var validCandidate = new GuidCount { Counter = 1 }; 64 | var invalidCandidate = new GuidCount { Counter = -1 }; 65 | 66 | var candidates = new List { validCandidate, invalidCandidate }.AsQueryable(); 67 | var filtered = candidates.Where(new PositiveCounterRule().Predicate).ToList(); 68 | 69 | Assert.Equal(1, filtered.Count); 70 | Assert.Equal(validCandidate.Id, filtered[0].Id); 71 | } 72 | 73 | [Fact] 74 | public void AndPredicateFiltersCorrectly() 75 | { 76 | var validCandidate = new GuidCount { Counter = 1 }; 77 | var invalidCandidate = new GuidCount { Counter = -1 }; 78 | 79 | var candidates = new List { validCandidate, invalidCandidate }.AsQueryable(); 80 | var filtered = candidates.Where(new PositiveCounterRule().And(new LessThanTenCounterRule()).Predicate).ToList(); 81 | 82 | Assert.Equal(1, filtered.Count); 83 | Assert.Equal(validCandidate.Id, filtered[0].Id); 84 | } 85 | 86 | [Fact] 87 | public void OrPredicateFiltersCorrectly() 88 | { 89 | var validCandidate = new GuidCount { Counter = 1 }; 90 | var invalidCandidate = new GuidCount { Counter = 20 }; 91 | 92 | var candidates = new List { validCandidate, invalidCandidate }.AsQueryable(); 93 | var filtered = candidates.Where(new NegativeCounterRule().Or(new LessThanTenCounterRule()).Predicate).ToList(); 94 | 95 | Assert.Equal(1, filtered.Count); 96 | Assert.Equal(validCandidate.Id, filtered[0].Id); 97 | } 98 | 99 | [Fact] 100 | public void NotPredicateFiltersCorrectly() 101 | { 102 | var validCandidate = new GuidCount { Counter = 1 }; 103 | var invalidCandidate = new GuidCount { Counter = -1 }; 104 | 105 | var candidates = new List { validCandidate, invalidCandidate }.AsQueryable(); 106 | var filtered = candidates.Where(new NegativeCounterRule().Not().Predicate).ToList(); 107 | 108 | Assert.Equal(1, filtered.Count); 109 | Assert.Equal(validCandidate.Id, filtered[0].Id); 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Enflow.Base.Test/TestingImplementations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | 5 | namespace Enflow.Base.Test 6 | { 7 | public class GuidCount 8 | { 9 | public Guid Id { get; private set; } 10 | public int Counter { get; set; } 11 | public void Increment() { Counter++; } 12 | 13 | public GuidCount() { Id = Guid.NewGuid(); } 14 | } 15 | 16 | public class LessThanTenCounterRule : StateRule 17 | { 18 | public override Expression> Predicate { get { return candidate => candidate.Counter < 10; } } 19 | } 20 | 21 | public class GuidCountIncrementWorkflow : Workflow 22 | { 23 | public GuidCountIncrementWorkflow() { } 24 | public GuidCountIncrementWorkflow(IStateRule preRule) : base(preRule) { } 25 | 26 | protected override GuidCount ExecuteWorkflow(GuidCount candidate) 27 | { 28 | candidate.Increment(); 29 | return candidate; 30 | } 31 | } 32 | 33 | public class GuidCountDuplicateWorkflow : Workflow> 34 | { 35 | public GuidCountDuplicateWorkflow() { } 36 | public GuidCountDuplicateWorkflow(IStateRule preRule) : base(preRule) { } 37 | 38 | protected override List ExecuteWorkflow(GuidCount candidate) 39 | { 40 | return new List { candidate, new GuidCount { Counter = candidate.Counter } }; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Enflow.Base.Test/WorkflowTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Xunit; 3 | using NSubstitute; 4 | 5 | namespace Enflow.Base.Test 6 | { 7 | public class WorkflowTests 8 | { 9 | [Fact] 10 | public void ExecutesWithoutBusinessRule() 11 | { 12 | var model = new GuidCount(); 13 | new GuidCountIncrementWorkflow().Execute(model); 14 | Assert.Equal(1, model.Counter); 15 | } 16 | 17 | [Fact] 18 | public void ValidatesPreBusinessRuleAndExecutes() 19 | { 20 | var model = new GuidCount(); 21 | 22 | var preRule = Substitute.For>(); 23 | preRule.IsSatisfied(Arg.Any()).Returns(true); 24 | 25 | new GuidCountIncrementWorkflow(preRule).Execute(model); 26 | 27 | Assert.Equal(1, model.Counter); 28 | } 29 | 30 | [Fact] 31 | public void TypeModWorkflowExecutesCorrectly() 32 | { 33 | var model = new GuidCount(); 34 | 35 | var preRule = Substitute.For>(); 36 | preRule.IsSatisfied(Arg.Any()).Returns(true); 37 | 38 | var result = new GuidCountDuplicateWorkflow(preRule).Execute(model); 39 | 40 | Assert.IsType>(result); 41 | Assert.Equal(2, result.Count); 42 | } 43 | 44 | [Fact] 45 | public void ThrowsErrorOnInvalidPreBusinessRuleAndDoesNotExecute() 46 | { 47 | var model = new GuidCount(); 48 | 49 | var preRule = Substitute.For>(); 50 | preRule.IsSatisfied(Arg.Any()).Returns(false); 51 | 52 | const string description = "This mock rule must be satisfied."; 53 | preRule.Description = description; 54 | 55 | var message = Assert.Throws(typeof(StateRuleException), () => new GuidCountIncrementWorkflow(preRule).Execute(model)).Message; 56 | Assert.Equal(description, message); 57 | Assert.Equal(0, model.Counter); 58 | } 59 | 60 | [Fact] 61 | public void WorkflowOfTAssignabletoIWorkflowOfT() 62 | { 63 | var workflow = new GuidCountIncrementWorkflow(Substitute.For>()); 64 | Assert.NotNull(workflow as IWorkflow); 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Enflow.Base.Test/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/Enflow.Base/Enflow.Base.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 10.0 6 | Debug 7 | AnyCPU 8 | {297DE7BE-45CE-4C54-86EC-E999CBC5A7E9} 9 | Library 10 | Properties 11 | Enflow.Base 12 | Enflow.Base 13 | v4.0 14 | Profile147 15 | 512 16 | {786C830F-07A1-408B-BD7F-6EE04809D6DB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 17 | 18 | 19 | true 20 | full 21 | false 22 | bin\Debug\ 23 | DEBUG;TRACE 24 | prompt 25 | 4 26 | 27 | 28 | pdbonly 29 | true 30 | bin\Release\ 31 | TRACE 32 | prompt 33 | 4 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 55 | -------------------------------------------------------------------------------- /src/Enflow.Base/Exceptions/StateRuleException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Enflow.Base 4 | { 5 | public class StateRuleException : Exception 6 | { 7 | public StateRuleException(string message) : base(message) { } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Enflow.Base/Exceptions/WorkflowException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Enflow.Base 4 | { 5 | public class WorkflowException : Exception 6 | { 7 | public WorkflowException(string message) : base(message) { } 8 | public WorkflowException(string message, Exception innerException) : base(message, innerException) { } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Enflow.Base/Flowable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Enflow.Base 4 | { 5 | public class Flowable 6 | { 7 | public static readonly Flowable Nothing = new Flowable(); 8 | 9 | public T Value { get; private set; } 10 | public bool HasValue { get; private set; } 11 | 12 | private Flowable() 13 | { 14 | HasValue = false; 15 | } 16 | 17 | public Flowable(T value) 18 | { 19 | Value = value; 20 | HasValue = true; 21 | } 22 | 23 | public bool Satisfies(IStateRule stateRule) 24 | { 25 | return HasValue && stateRule.IsSatisfied(Value); 26 | } 27 | } 28 | 29 | public static class FlowableExtensions 30 | { 31 | public static Flowable AsFlowable(this T value) 32 | { 33 | if (!(value is ValueType) && ReferenceEquals(value, null)) return Flowable.Nothing; 34 | return new Flowable(value); 35 | } 36 | 37 | public static Flowable Flow(this Flowable flowable, IWorkflow workflow) 38 | { 39 | return !flowable.HasValue ? Flowable.Nothing : workflow.Execute(flowable.Value).AsFlowable(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Enflow.Base/ParameterReplacer.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace Enflow.Base 4 | { 5 | public class ParameterReplacer : ExpressionVisitor 6 | { 7 | private readonly ParameterExpression _parameter; 8 | 9 | public ParameterReplacer(ParameterExpression parameter) { _parameter = parameter; } 10 | 11 | protected override Expression VisitParameter(ParameterExpression node) 12 | { 13 | return base.VisitParameter(_parameter); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Enflow.Base/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Resources; 2 | using System.Reflection; 3 | using System.Runtime.CompilerServices; 4 | using System.Runtime.InteropServices; 5 | 6 | // General Information about an assembly is controlled through the following 7 | // set of attributes. Change these attribute values to modify the information 8 | // associated with an assembly. 9 | [assembly: AssemblyTitle("Enflow.Base.pcl")] 10 | [assembly: AssemblyDescription("A simple library for workflows and state rules.")] 11 | [assembly: AssemblyConfiguration("")] 12 | [assembly: AssemblyCompany("Joseph Phillips")] 13 | [assembly: AssemblyProduct("Enflow.Base.pcl")] 14 | [assembly: AssemblyCopyright("Copyright © Joseph Phillips 2013")] 15 | [assembly: AssemblyTrademark("")] 16 | [assembly: AssemblyCulture("")] 17 | [assembly: NeutralResourcesLanguage("en")] 18 | 19 | // Version information for an assembly consists of the following four values: 20 | // 21 | // Major Version 22 | // Minor Version 23 | // Build Number 24 | // Revision 25 | // 26 | // You can specify all the values or you can default the Build and Revision Numbers 27 | // by using the '*' as shown below: 28 | // [assembly: AssemblyVersion("1.0.*")] 29 | [assembly: AssemblyVersion("0.4.1.0")] 30 | [assembly: AssemblyFileVersion("0.4.1.0")] 31 | -------------------------------------------------------------------------------- /src/Enflow.Base/StateRule.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * The inspiration for this code is the Wikipedia page on the Specification Pattern: 3 | * http://en.wikipedia.org/wiki/Specification_pattern 4 | * 5 | * It is shared under the Creative Commons Attribution-ShareAlike License. 6 | * The terms of use are stated here: 7 | * http://wikimediafoundation.org/wiki/Terms_of_Use 8 | * The license terms are available here: 9 | * http://creativecommons.org/licenses/by-sa/3.0/ 10 | * 11 | * An in depth look at the Specification Pattern is available in the excellent paper by Martin Fowler and Eric Evans: 12 | * http://martinfowler.com/apsupp/spec.pdf 13 | */ 14 | 15 | using System; 16 | using System.Linq.Expressions; 17 | 18 | namespace Enflow.Base 19 | { 20 | /// Interface for Enflow state rules 21 | /// 22 | public interface IStateRule 23 | { 24 | Expression> Predicate { get; } 25 | string Description { get; set; } 26 | bool IsSatisfied(T candidate); 27 | } 28 | 29 | public abstract class StateRule : IStateRule 30 | { 31 | public abstract Expression> Predicate { get; } 32 | public string Description { get; set; } 33 | public bool IsSatisfied(T candidate) { return Predicate.Compile().Invoke(candidate); } 34 | 35 | /// 36 | /// Ensures that parameter expressions refer to the same instance across all expressions that are combined to form the input expression. 37 | /// 38 | /// 39 | /// 40 | protected Expression> ReplaceParameter (Expression expression) 41 | { 42 | var parameterExpression = Expression.Parameter(typeof(T)); 43 | var body = new ParameterReplacer(parameterExpression).Visit(expression); 44 | return Expression.Lambda>(body, parameterExpression); 45 | } 46 | } 47 | 48 | /// Composite state rule where both input rules must be satisfied. 49 | /// 50 | public class AndStateRule : StateRule 51 | { 52 | private readonly IStateRule _ruleA; 53 | private readonly IStateRule _ruleB; 54 | 55 | internal AndStateRule(IStateRule ruleA, IStateRule ruleB) 56 | { 57 | _ruleA = ruleA; 58 | _ruleB = ruleB; 59 | } 60 | 61 | public override Expression> Predicate 62 | { 63 | get { return ReplaceParameter(Expression.And(_ruleA.Predicate.Body, _ruleB.Predicate.Body)); } 64 | } 65 | } 66 | 67 | /// Composite state rule where at least one of the input rules must be satisfied. 68 | /// 69 | public class OrStateRule : StateRule 70 | { 71 | private readonly IStateRule _ruleA; 72 | private readonly IStateRule _ruleB; 73 | 74 | internal OrStateRule(IStateRule ruleA, IStateRule ruleB) 75 | { 76 | _ruleA = ruleA; 77 | _ruleB = ruleB; 78 | } 79 | 80 | public override Expression> Predicate 81 | { 82 | get { return ReplaceParameter(Expression.Or(_ruleA.Predicate.Body, _ruleB.Predicate.Body)); } 83 | } 84 | } 85 | 86 | /// State rule that enforces a logical NOT of the input rule. 87 | /// 88 | public class NotStateRule : StateRule 89 | { 90 | private readonly IStateRule _rule; 91 | internal NotStateRule(IStateRule rule) { _rule = rule; } 92 | 93 | public override Expression> Predicate 94 | { 95 | get { return ReplaceParameter(Expression.Not(_rule.Predicate.Body)); } 96 | } 97 | } 98 | 99 | /// Facilitates the fluent API for composing state rules from atomic constituents. 100 | public static class StateRuleFluentExtensions 101 | { 102 | public static IStateRule And(this IStateRule ruleA, IStateRule ruleB) 103 | { 104 | return new AndStateRule(ruleA, ruleB); 105 | } 106 | 107 | public static IStateRule Or(this IStateRule ruleA, IStateRule ruleB) 108 | { 109 | return new OrStateRule(ruleA, ruleB); 110 | } 111 | 112 | public static IStateRule Not(this IStateRule rule) 113 | { 114 | return new NotStateRule(rule); 115 | } 116 | 117 | public static IStateRule Describe(this IStateRule rule, string description) 118 | { 119 | rule.Description = description; 120 | return rule; 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Enflow.Base/Workflow.cs: -------------------------------------------------------------------------------- 1 | namespace Enflow.Base 2 | { 3 | /// Interface for transitioning types through workflows. 4 | public interface IWorkflow 5 | { 6 | U Execute(T candidate); 7 | } 8 | 9 | public interface IWorkflow : IWorkflow { } 10 | 11 | public abstract class Workflow : IWorkflow 12 | { 13 | protected readonly IStateRule PreStateRule; 14 | 15 | protected Workflow() { } 16 | protected Workflow(IStateRule preStateRule) { PreStateRule = preStateRule; } 17 | 18 | /// Validates the pre-condition state rule and executes the workflow logic. 19 | /// 20 | public virtual U Execute(T candidate) 21 | { 22 | ValidateStateRule(candidate, PreStateRule); 23 | return ExecuteWorkflow(candidate); 24 | } 25 | 26 | /// Workflow logic not including pre-condition rule validation. 27 | /// 28 | protected abstract U ExecuteWorkflow(T candidate); 29 | 30 | private static void ValidateStateRule(T candidate, IStateRule rule) 31 | { 32 | if (rule == null) return; 33 | if (!rule.IsSatisfied(candidate)) throw new StateRuleException(rule.Description); 34 | } 35 | } 36 | 37 | public abstract class Workflow : Workflow, IWorkflow 38 | { 39 | protected Workflow() { } 40 | protected Workflow(IStateRule preStateRule) : base(preStateRule) { } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Enflow.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 11.00 3 | # Visual Studio 2010 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Enflow.Base.Test", "Enflow.Base.Test\Enflow.Base.Test.csproj", "{130675EB-3D40-489B-B806-970EB0ED08D7}" 5 | EndProject 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{44D526B0-0D81-4510-BDE0-7629C1E2480A}" 7 | ProjectSection(SolutionItems) = preProject 8 | .nuget\NuGet.Config = .nuget\NuGet.Config 9 | .nuget\NuGet.exe = .nuget\NuGet.exe 10 | .nuget\NuGet.targets = .nuget\NuGet.targets 11 | EndProjectSection 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Enflow.Base", "Enflow.Base\Enflow.Base.csproj", "{297DE7BE-45CE-4C54-86EC-E999CBC5A7E9}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {130675EB-3D40-489B-B806-970EB0ED08D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {130675EB-3D40-489B-B806-970EB0ED08D7}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {130675EB-3D40-489B-B806-970EB0ED08D7}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {130675EB-3D40-489B-B806-970EB0ED08D7}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {297DE7BE-45CE-4C54-86EC-E999CBC5A7E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {297DE7BE-45CE-4C54-86EC-E999CBC5A7E9}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {297DE7BE-45CE-4C54-86EC-E999CBC5A7E9}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {297DE7BE-45CE-4C54-86EC-E999CBC5A7E9}.Release|Any CPU.Build.0 = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(SolutionProperties) = preSolution 31 | HideSolutionNode = FALSE 32 | EndGlobalSection 33 | EndGlobal 34 | --------------------------------------------------------------------------------