├── .gitignore ├── Pipelines.sln ├── demo └── App │ ├── App.csproj │ ├── Contexts.cs │ ├── Data.cs │ ├── Extensions.cs │ ├── Pipes.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── appsettings.Development.json │ └── appsettings.json ├── src └── Pipelines │ ├── AspNetCore │ ├── EndpointRouteBuilderExtensions.cs │ ├── PipelineMarkerService.cs │ ├── PipelineProviderExtensions.cs │ ├── PipelineStartupFilter.cs │ └── ServiceCollectionExtensions.cs │ ├── Core │ ├── ContextBase.cs │ ├── IAbortableContext.cs │ ├── ICancellableContext.cs │ ├── IPipeline.cs │ ├── IPipelineBuilder.cs │ ├── IPipelineBuilderFactory.cs │ ├── IPipelineProvider.cs │ ├── Pipe.cs │ ├── PipeDescriptorInfo.cs │ ├── Pipeline.cs │ ├── PipelineBuilder.cs │ ├── PipelineBuilderExtensions.cs │ ├── PipelineBuilderFactory.cs │ ├── PipelineDefaults.cs │ └── PipelineProvider.cs │ ├── DelegatePipe.cs │ ├── DelegatePipeRegistrationExtensions.cs │ ├── Guard.cs │ ├── PipeBase.cs │ ├── Pipelines.csproj │ ├── Properties │ └── AssemblyInfo.cs │ └── SubPipeline │ ├── LoopPipe.cs │ ├── LoopPipeRegistrationExtensions.cs │ └── SubContextBase.cs └── test └── Pipelines.Test ├── PipeDescriptorInfoFixture.cs ├── PipelineFixture.cs ├── Pipelines.Test.csproj └── PipleineBuilderFixture.cs /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | [Aa][Rr][Mm]/ 24 | [Aa][Rr][Mm]64/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | #[Ll]og/ 29 | 30 | # Visual Studio 2015/2017 cache/options directory 31 | .vs/ 32 | # Uncomment if you have tasks that create the project's static files in wwwroot 33 | #wwwroot/ 34 | 35 | # Visual Studio 2017 auto generated files 36 | Generated\ Files/ 37 | 38 | # MSTest test Results 39 | [Tt]est[Rr]esult*/ 40 | [Bb]uild[Ll]og.* 41 | 42 | # NUNIT 43 | *.VisualState.xml 44 | TestResult.xml 45 | 46 | # Build Results of an ATL Project 47 | [Dd]ebugPS/ 48 | [Rr]eleasePS/ 49 | dlldata.c 50 | 51 | # Benchmark Results 52 | BenchmarkDotNet.Artifacts/ 53 | 54 | # .NET Core 55 | project.lock.json 56 | project.fragment.lock.json 57 | artifacts/ 58 | 59 | # StyleCop 60 | StyleCopReport.xml 61 | 62 | # Files built by Visual Studio 63 | *_i.c 64 | *_p.c 65 | *_h.h 66 | *.ilk 67 | *.meta 68 | *.obj 69 | *.iobj 70 | *.pch 71 | *.pdb 72 | *.ipdb 73 | *.pgc 74 | *.pgd 75 | *.rsp 76 | *.sbr 77 | *.tlb 78 | *.tli 79 | *.tlh 80 | *.tmp 81 | *.tmp_proj 82 | *_wpftmp.csproj 83 | *.log 84 | *.vspscc 85 | *.vssscc 86 | .builds 87 | *.pidb 88 | *.svclog 89 | *.scc 90 | 91 | # Chutzpah Test files 92 | _Chutzpah* 93 | 94 | # Visual C++ cache files 95 | ipch/ 96 | *.aps 97 | *.ncb 98 | *.opendb 99 | *.opensdf 100 | *.sdf 101 | *.cachefile 102 | *.VC.db 103 | *.VC.VC.opendb 104 | 105 | # Visual Studio profiler 106 | *.psess 107 | *.vsp 108 | *.vspx 109 | *.sap 110 | 111 | # Visual Studio Trace Files 112 | *.e2e 113 | 114 | # TFS 2012 Local Workspace 115 | $tf/ 116 | 117 | # Guidance Automation Toolkit 118 | *.gpState 119 | 120 | # ReSharper is a .NET coding add-in 121 | _ReSharper*/ 122 | *.[Rr]e[Ss]harper 123 | *.DotSettings.user 124 | 125 | # JustCode is a .NET coding add-in 126 | .JustCode 127 | 128 | # TeamCity is a build add-in 129 | _TeamCity* 130 | 131 | # DotCover is a Code Coverage Tool 132 | *.dotCover 133 | 134 | # AxoCover is a Code Coverage Tool 135 | .axoCover/* 136 | !.axoCover/settings.json 137 | 138 | # Visual Studio code coverage results 139 | *.coverage 140 | *.coveragexml 141 | 142 | # NCrunch 143 | _NCrunch_* 144 | .*crunch*.local.xml 145 | nCrunchTemp_* 146 | 147 | # MightyMoose 148 | *.mm.* 149 | AutoTest.Net/ 150 | 151 | # Web workbench (sass) 152 | .sass-cache/ 153 | 154 | # Installshield output folder 155 | [Ee]xpress/ 156 | 157 | # DocProject is a documentation generator add-in 158 | DocProject/buildhelp/ 159 | DocProject/Help/*.HxT 160 | DocProject/Help/*.HxC 161 | DocProject/Help/*.hhc 162 | DocProject/Help/*.hhk 163 | DocProject/Help/*.hhp 164 | DocProject/Help/Html2 165 | DocProject/Help/html 166 | 167 | # Click-Once directory 168 | publish/ 169 | 170 | # Publish Web Output 171 | *.[Pp]ublish.xml 172 | *.azurePubxml 173 | # Note: Comment the next line if you want to checkin your web deploy settings, 174 | # but database connection strings (with potential passwords) will be unencrypted 175 | *.pubxml 176 | *.publishproj 177 | 178 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 179 | # checkin your Azure Web App publish settings, but sensitive information contained 180 | # in these scripts will be unencrypted 181 | PublishScripts/ 182 | 183 | # NuGet Packages 184 | *.nupkg 185 | # The packages folder can be ignored because of Package Restore 186 | **/[Pp]ackages/* 187 | # except build/, which is used as an MSBuild target. 188 | !**/[Pp]ackages/build/ 189 | # Uncomment if necessary however generally it will be regenerated when needed 190 | #!**/[Pp]ackages/repositories.config 191 | # NuGet v3's project.json files produces more ignorable files 192 | *.nuget.props 193 | *.nuget.targets 194 | 195 | # Microsoft Azure Build Output 196 | csx/ 197 | *.build.csdef 198 | 199 | # Microsoft Azure Emulator 200 | ecf/ 201 | rcf/ 202 | 203 | # Windows Store app package directories and files 204 | AppPackages/ 205 | BundleArtifacts/ 206 | Package.StoreAssociation.xml 207 | _pkginfo.txt 208 | *.appx 209 | 210 | # Visual Studio cache files 211 | # files ending in .cache can be ignored 212 | *.[Cc]ache 213 | # but keep track of directories ending in .cache 214 | !?*.[Cc]ache/ 215 | 216 | # Others 217 | ClientBin/ 218 | ~$* 219 | *~ 220 | *.dbmdl 221 | *.dbproj.schemaview 222 | *.jfm 223 | *.pfx 224 | *.publishsettings 225 | orleans.codegen.cs 226 | 227 | # Including strong name files can present a security risk 228 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 229 | #*.snk 230 | 231 | # Since there are multiple workflows, uncomment next line to ignore bower_components 232 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 233 | #bower_components/ 234 | 235 | # RIA/Silverlight projects 236 | Generated_Code/ 237 | 238 | # Backup & report files from converting an old project file 239 | # to a newer Visual Studio version. Backup files are not needed, 240 | # because we have git ;-) 241 | _UpgradeReport_Files/ 242 | Backup*/ 243 | UpgradeLog*.XML 244 | UpgradeLog*.htm 245 | ServiceFabricBackup/ 246 | *.rptproj.bak 247 | 248 | # SQL Server files 249 | *.mdf 250 | *.ldf 251 | *.ndf 252 | 253 | # Business Intelligence projects 254 | *.rdl.data 255 | *.bim.layout 256 | *.bim_*.settings 257 | *.rptproj.rsuser 258 | *- Backup*.rdl 259 | 260 | # Microsoft Fakes 261 | FakesAssemblies/ 262 | 263 | # GhostDoc plugin setting file 264 | *.GhostDoc.xml 265 | 266 | # Node.js Tools for Visual Studio 267 | .ntvs_analysis.dat 268 | node_modules/ 269 | 270 | # Visual Studio 6 build log 271 | *.plg 272 | 273 | # Visual Studio 6 workspace options file 274 | *.opt 275 | 276 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 277 | *.vbw 278 | 279 | # Visual Studio LightSwitch build output 280 | **/*.HTMLClient/GeneratedArtifacts 281 | **/*.DesktopClient/GeneratedArtifacts 282 | **/*.DesktopClient/ModelManifest.xml 283 | **/*.Server/GeneratedArtifacts 284 | **/*.Server/ModelManifest.xml 285 | _Pvt_Extensions 286 | 287 | # Paket dependency manager 288 | .paket/paket.exe 289 | paket-files/ 290 | 291 | # FAKE - F# Make 292 | .fake/ 293 | 294 | # JetBrains Rider 295 | .idea/ 296 | *.sln.iml 297 | 298 | # CodeRush personal settings 299 | .cr/personal 300 | 301 | # Python Tools for Visual Studio (PTVS) 302 | __pycache__/ 303 | *.pyc 304 | 305 | # Cake - Uncomment if you are using it 306 | # tools/** 307 | # !tools/packages.config 308 | 309 | # Tabs Studio 310 | *.tss 311 | 312 | # Telerik's JustMock configuration file 313 | *.jmconfig 314 | 315 | # BizTalk build output 316 | *.btp.cs 317 | *.btm.cs 318 | *.odx.cs 319 | *.xsd.cs 320 | 321 | # OpenCover UI analysis results 322 | OpenCover/ 323 | 324 | # Azure Stream Analytics local run output 325 | ASALocalRun/ 326 | 327 | # MSBuild Binary and Structured Log 328 | *.binlog 329 | 330 | # NVidia Nsight GPU debugger configuration file 331 | *.nvuser 332 | 333 | # MFractors (Xamarin productivity tool) working folder 334 | .mfractor/ 335 | 336 | # Local History for Visual Studio 337 | .localhistory/ 338 | 339 | # BeatPulse healthcheck temp database 340 | healthchecksdb 341 | *.bak 342 | 343 | # powershell output 344 | /PowerShell/src/DockerDeploy/output 345 | -------------------------------------------------------------------------------- /Pipelines.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.32112.339 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0A002CA3-7335-4148-B0E1-388DEAD195EC}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{513D0BF0-D1EE-4805-BB84-80AED1D12850}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pipelines", "src\Pipelines\Pipelines.csproj", "{BF90F9AA-5249-440A-A792-361A61907088}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Pipelines.Test", "test\Pipelines.Test\Pipelines.Test.csproj", "{8F8DEC45-5F4C-42F2-8F96-BC513109799A}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "demo", "demo", "{6B02B15D-4BF1-43BD-908E-A1E236A5FA49}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "App", "demo\App\App.csproj", "{5B6A927D-DF33-4D09-9242-C0525F7301A0}" 17 | EndProject 18 | Global 19 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 20 | Debug|Any CPU = Debug|Any CPU 21 | Release|Any CPU = Release|Any CPU 22 | EndGlobalSection 23 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 24 | {BF90F9AA-5249-440A-A792-361A61907088}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 25 | {BF90F9AA-5249-440A-A792-361A61907088}.Debug|Any CPU.Build.0 = Debug|Any CPU 26 | {BF90F9AA-5249-440A-A792-361A61907088}.Release|Any CPU.ActiveCfg = Release|Any CPU 27 | {BF90F9AA-5249-440A-A792-361A61907088}.Release|Any CPU.Build.0 = Release|Any CPU 28 | {8F8DEC45-5F4C-42F2-8F96-BC513109799A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {8F8DEC45-5F4C-42F2-8F96-BC513109799A}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {8F8DEC45-5F4C-42F2-8F96-BC513109799A}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {8F8DEC45-5F4C-42F2-8F96-BC513109799A}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {5B6A927D-DF33-4D09-9242-C0525F7301A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {5B6A927D-DF33-4D09-9242-C0525F7301A0}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {5B6A927D-DF33-4D09-9242-C0525F7301A0}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {5B6A927D-DF33-4D09-9242-C0525F7301A0}.Release|Any CPU.Build.0 = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(SolutionProperties) = preSolution 38 | HideSolutionNode = FALSE 39 | EndGlobalSection 40 | GlobalSection(NestedProjects) = preSolution 41 | {BF90F9AA-5249-440A-A792-361A61907088} = {0A002CA3-7335-4148-B0E1-388DEAD195EC} 42 | {8F8DEC45-5F4C-42F2-8F96-BC513109799A} = {513D0BF0-D1EE-4805-BB84-80AED1D12850} 43 | {5B6A927D-DF33-4D09-9242-C0525F7301A0} = {6B02B15D-4BF1-43BD-908E-A1E236A5FA49} 44 | EndGlobalSection 45 | GlobalSection(ExtensibilityGlobals) = postSolution 46 | SolutionGuid = {C441F3CB-B6B0-4868-8C33-8F5E8EC385CB} 47 | EndGlobalSection 48 | EndGlobal 49 | -------------------------------------------------------------------------------- /demo/App/App.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demo/App/Contexts.cs: -------------------------------------------------------------------------------- 1 | using Artech.Pipelines; 2 | 3 | namespace App 4 | { 5 | public sealed class StatePopulationContext : ContextBase 6 | { 7 | public StatePopulationData PopulationData { get; } 8 | public StatePopulationContext(StatePopulationData populationData) => PopulationData = populationData; 9 | } 10 | 11 | public sealed class ProvincePopulationContext : SubContextBase> 12 | { 13 | public string Province { get; private set; } = default!; 14 | public IDictionary Cities { get; private set; } = default!; 15 | public override void Initialize(StatePopulationContext parent, KeyValuePair item) 16 | { 17 | Province = item.Key; 18 | Cities = item.Value.Cities; 19 | base.Initialize(parent, item); 20 | } 21 | } 22 | 23 | public sealed class CityPopulationContext : SubContextBase> 24 | { 25 | public string City { get; private set; } = default!; 26 | public PopulationData PopulationData { get; private set; } = default!; 27 | public override void Initialize(ProvincePopulationContext parent, KeyValuePair item) 28 | { 29 | City = item.Key; 30 | PopulationData = item.Value; 31 | base.Initialize(parent, item); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /demo/App/Data.cs: -------------------------------------------------------------------------------- 1 | namespace App 2 | { 3 | public class PopulationData 4 | { 5 | public object Statistics { get; set; } = default!; 6 | } 7 | 8 | public class StatePopulationData 9 | { 10 | public IDictionary Provinces { get; set; } = default!; 11 | } 12 | 13 | public class ProvincePopulationData 14 | { 15 | public IDictionary Cities { get; set; } = default!; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demo/App/Extensions.cs: -------------------------------------------------------------------------------- 1 | using Artech.Pipelines; 2 | 3 | namespace App 4 | { 5 | public static class Extensions 6 | { 7 | public static IPipelineBuilder UseStatePipe(this IPipelineBuilder builder) 8 | where TPipe : Pipe 9 | => builder.Use(); 10 | public static IPipelineBuilder UseProvincePipe(this IPipelineBuilder builder) 11 | where TPipe : Pipe 12 | => builder.Use(); 13 | public static IPipelineBuilder UseCityPipe(this IPipelineBuilder builder) 14 | where TPipe : Pipe 15 | => builder.Use(); 16 | 17 | public static IPipelineBuilder ForEachProvince(this IPipelineBuilder builder, Action> setup) 18 | => builder.ForEach("For each province", it => it.PopulationData.Provinces, (_, _) => true, setup); 19 | public static IPipelineBuilder ForEachCity(this IPipelineBuilder builder, Action> setup) 20 | => builder.ForEach("For each city", it => it.Cities, (_, _) => true, setup); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /demo/App/Pipes.cs: -------------------------------------------------------------------------------- 1 | using Artech.Pipelines; 2 | 3 | namespace App 4 | { 5 | public sealed class FooStatePipe : PipeBase 6 | { 7 | public override string Description => "State Population Processor Foo"; 8 | protected override void Invoke(StatePopulationContext context) 9 | => Console.WriteLine("Foo: Process state population"); 10 | } 11 | public sealed class BarStatePipe : PipeBase 12 | { 13 | public override string Description => "State Population Processor Bar"; 14 | protected override void Invoke(StatePopulationContext context) 15 | => Console.WriteLine("Bar: Process state population"); 16 | } 17 | public sealed class BazStatePipe : PipeBase 18 | { 19 | public override string Description => "State Population Processor Baz"; 20 | protected override void Invoke(StatePopulationContext context) 21 | => Console.WriteLine("Baz: Process state population"); 22 | } 23 | 24 | public sealed class FooProvincePipe : PipeBase 25 | { 26 | public override string Description => "Province Population Processor Foo"; 27 | protected override void Invoke(ProvincePopulationContext context) 28 | => Console.WriteLine($"\tFoo: Process population of the province {context.Province}"); 29 | } 30 | 31 | public sealed class BarProvincePipe : PipeBase 32 | { 33 | public override string Description => "Province Population Processor Bar"; 34 | protected override void Invoke(ProvincePopulationContext context) 35 | => Console.WriteLine($"\tBar: Process population of the province {context.Province}"); 36 | 37 | } 38 | public sealed class BazProvincePipe : PipeBase 39 | { 40 | public override string Description => "Province Population Processor Baz"; 41 | protected override void Invoke(ProvincePopulationContext context) 42 | => Console.WriteLine($"\tBaz: Process population of the province {context.Province}"); 43 | } 44 | 45 | public sealed class FooCityPipe : PipeBase 46 | { 47 | public override string Description => "City Population Processor Foo"; 48 | protected override void Invoke(CityPopulationContext context) 49 | => Console.WriteLine($"\t\tFoo: Process population of the city {context.City}"); 50 | } 51 | 52 | public sealed class BarCityPipe : PipeBase 53 | { 54 | public override string Description => "City Population Processor Bar"; 55 | protected override void Invoke(CityPopulationContext context) 56 | => Console.WriteLine($"\t\tBar: Process population of the city {context.City}"); 57 | 58 | } 59 | 60 | public sealed class BazCityPipe : PipeBase 61 | { 62 | public override string Description => "City Population Processor Baz"; 63 | protected override void Invoke(CityPopulationContext context) 64 | => Console.WriteLine($"\t\tBaz: Process population of the city {context.City}"); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /demo/App/Program.cs: -------------------------------------------------------------------------------- 1 | using App; 2 | using Artech.Pipelines; 3 | 4 | var builder = WebApplication.CreateBuilder(args); 5 | builder.Services.AddPipelines(BuildPipelines); 6 | var app = builder.Build(); 7 | app.MapGet("/test", HandleRequest); 8 | app.Run(); 9 | 10 | static void BuildPipelines(IPipelineProvider pipelineProvider) 11 | { 12 | pipelineProvider.AddPipeline(name: "PopulationProcessor", setup: builder => builder 13 | .UseStatePipe() 14 | .UseStatePipe() 15 | .ForEachProvince(provinceBuilder => provinceBuilder 16 | .UseProvincePipe() 17 | .UseProvincePipe() 18 | .ForEachCity(cityBuilder => cityBuilder 19 | .UseCityPipe() 20 | .UseCityPipe() 21 | .UseCityPipe()) 22 | .UseProvincePipe()) 23 | .UseStatePipe()); 24 | } 25 | 26 | static async Task HandleRequest(HttpContext httpContext, IPipelineProvider provider, HttpResponse response) 27 | { 28 | Console.WriteLine("Execute PopulationProcessor pipeline"); 29 | var data = new StatePopulationData 30 | { 31 | Provinces = new Dictionary() 32 | }; 33 | data.Provinces.Add("Jiangsu", new ProvincePopulationData 34 | { 35 | Cities = new Dictionary 36 | { 37 | {"Suzhou", new PopulationData() }, 38 | {"Wuxi", new PopulationData() }, 39 | {"Changezhou", new PopulationData() }, 40 | } 41 | }); 42 | 43 | data.Provinces.Add("Shandong", new ProvincePopulationData 44 | { 45 | Cities = new Dictionary 46 | { 47 | {"Qingdao", new PopulationData() }, 48 | {"Jinan", new PopulationData() }, 49 | {"Yantai", new PopulationData() }, 50 | } 51 | }); 52 | 53 | data.Provinces.Add("Zhejiang", new ProvincePopulationData 54 | { 55 | Cities = new Dictionary 56 | { 57 | {"Hangzhou", new PopulationData() }, 58 | {"Ningbo", new PopulationData() }, 59 | {"Wenzhou", new PopulationData() }, 60 | } 61 | }); 62 | 63 | var context = new StatePopulationContext(data); 64 | 65 | var pipeline = provider.GetPipeline("PopulationProcessor"); 66 | var valueTask = pipeline.ProcessAsync(context); 67 | if (!valueTask.IsCompleted) 68 | { 69 | await valueTask; 70 | } 71 | return Results.Ok("OK"); 72 | } 73 | 74 | 75 | -------------------------------------------------------------------------------- /demo/App/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:44408", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "App": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "applicationUrl": "http://localhost:5201", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": true, 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /demo/App/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "DetailedErrors": true, 3 | "Logging": { 4 | "LogLevel": { 5 | "Default": "Information", 6 | "Microsoft.AspNetCore": "Warning" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /demo/App/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /src/Pipelines/AspNetCore/EndpointRouteBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Routing; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace Artech.Pipelines 7 | { 8 | /// 9 | /// Defines extension methods to register routing endpoints to export registered pipelines. 10 | /// 11 | public static class EndpointRouteBuilderExtensions 12 | { 13 | /// Registers routing endpoints to export registered pipelines. 14 | /// The . 15 | /// The current . 16 | public static IEndpointRouteBuilder MapPipelines(this IEndpointRouteBuilder endpointRouteBuilder) 17 | { 18 | var markerService = endpointRouteBuilder.ServiceProvider.GetService() ?? throw new InvalidOperationException("Pipeline based services are not registered."); 19 | endpointRouteBuilder.MapGet("pipelines", RenderAllPipelines); 20 | endpointRouteBuilder.MapGet("pipelines/{pipelineName}", RenderPipeline); 21 | return endpointRouteBuilder; 22 | } 23 | 24 | private static IResult RenderAllPipelines(IPipelineProvider pipelineProvider) 25 | { 26 | var pipelines = pipelineProvider.ExportAllPipelines(); 27 | var list = pipelines.Keys.Select(it => $"
  • {it}
  • "); 28 | var html = @$" 29 | 30 | Pipelines 31 | 32 |

    The following pipelines are registered:

    33 |
      34 | {string.Join(' ', list)} 35 |
    36 | 37 | "; 38 | return Results.Text(html, "text/html"); 39 | } 40 | 41 | private static IResult RenderPipeline(string pipelineName, IPipelineProvider pipelineProvider) 42 | { 43 | var pipelines = pipelineProvider.ExportAllPipelines(); 44 | if (!pipelines.TryGetValue(pipelineName, out var descriptor)) 45 | { 46 | return Results.NotFound(); 47 | } 48 | return Results.Text(descriptor.ToString()); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Pipelines/AspNetCore/PipelineMarkerService.cs: -------------------------------------------------------------------------------- 1 | namespace Artech.Pipelines 2 | { 3 | internal class PipelineMarkerService { } 4 | } 5 | -------------------------------------------------------------------------------- /src/Pipelines/AspNetCore/PipelineProviderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Flight.Extensions.Primitives; 2 | 3 | namespace Artech.Pipelines 4 | { 5 | /// 6 | /// Defines extension methods against . 7 | /// 8 | public static class PipelineProviderExtensions 9 | { 10 | /// Registers a default built pipeline. 11 | /// The type of the pipeline execution context. 12 | /// The used for pipeline provision. 13 | /// The delegate to built the pipeline to register. 14 | /// The current . 15 | public static IPipelineProvider AddPipeline(this IPipelineProvider pipelineProvider, Action> setup) 16 | { 17 | Guard.ArgumentNotNull(pipelineProvider, nameof(pipelineProvider)); 18 | return pipelineProvider.AddPipeline(PipelineDefaults.DefaultPipelineName, setup); 19 | } 20 | 21 | /// 22 | /// The the pipeline based on specified name. 23 | /// 24 | /// The type of the pipeline execution context. 25 | /// The used for pipeline provision. 26 | /// The pipeline's name. 27 | /// The representing the pipeline. 28 | public static IPipeline GetPipeline(this IPipelineProvider pipelineProvider, string name) 29 | { 30 | Guard.ArgumentNotNull(pipelineProvider, nameof(pipelineProvider)); 31 | if (pipelineProvider.TryGetPipeline(name, out var pipeline)) 32 | { 33 | return new Pipeline(pipeline!); 34 | } 35 | throw new InvalidOperationException($"The pipeline '{name}' is not registered."); 36 | } 37 | 38 | /// 39 | /// The the pipeline based on specified name. 40 | /// 41 | /// The type of the pipeline execution context. 42 | /// The used for pipeline provision. 43 | /// The representing the pipeline. 44 | public static IPipeline GetPipeline(this IPipelineProvider pipelineProvider) => pipelineProvider.GetPipeline(PipelineDefaults.DefaultPipelineName); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Pipelines/AspNetCore/PipelineStartupFilter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace Artech.Pipelines 6 | { 7 | internal class PipelineStartupFilter : IStartupFilter 8 | { 9 | private readonly Action _buildPipelines; 10 | 11 | public PipelineStartupFilter(Action buildPipelines) 12 | { 13 | _buildPipelines = buildPipelines ?? throw new ArgumentNullException(nameof(buildPipelines)); 14 | } 15 | 16 | public Action Configure(Action next) 17 | { 18 | return builder => { 19 | var pipelineProvider = builder.ApplicationServices.GetRequiredService(); 20 | _buildPipelines(pipelineProvider); 21 | builder.UseRouting(); 22 | builder.UseEndpoints(endpoints => endpoints.MapPipelines()); 23 | next(builder); 24 | }; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Pipelines/AspNetCore/ServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.DependencyInjection.Extensions; 4 | 5 | namespace Artech.Pipelines 6 | { 7 | /// 8 | /// Defines extension methods to register pipeline based services. 9 | /// 10 | public static class ServiceCollectionExtensions 11 | { 12 | /// Registers pipeline based services. 13 | /// The . 14 | /// The to build pipelines. 15 | /// The current . 16 | public static IServiceCollection AddPipelines(this IServiceCollection services, Action buildPipelines) 17 | { 18 | if (services == null) 19 | { 20 | throw new ArgumentNullException(nameof(services)); 21 | } 22 | if (buildPipelines == null) 23 | { 24 | throw new ArgumentNullException(nameof(buildPipelines)); 25 | } 26 | services.TryAdd(ServiceDescriptor.Singleton()); 27 | services.TryAdd(ServiceDescriptor.Singleton()); 28 | services.TryAddEnumerable(ServiceDescriptor.Singleton(Create)); 29 | services.TryAdd(ServiceDescriptor.Singleton()); 30 | services.TryAddSingleton(typeof(IPipeline<>), typeof(Pipeline<>)); 31 | return services; 32 | PipelineStartupFilter Create(IServiceProvider serviceProvider) => ActivatorUtilities.CreateInstance(serviceProvider, buildPipelines); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Pipelines/Core/ContextBase.cs: -------------------------------------------------------------------------------- 1 | namespace Artech.Pipelines 2 | { 3 | /// 4 | /// The base type of pipeline execution context. 5 | /// 6 | public abstract class ContextBase : IAbortableContext, ICancellableContext 7 | { 8 | /// Gets the properties. 9 | /// The properties. 10 | public IDictionary Properties { get; } = new Dictionary(); 11 | 12 | /// Gets a value indicating whether this pipeline execution is aborted. 13 | /// 14 | /// true if this pipeline execution is aborted; otherwise, false. 15 | public bool IsAborted { get; private set; } 16 | 17 | /// Gets the cancellation token. 18 | /// The cancellation token. 19 | public virtual CancellationToken CancellationToken => CancellationToken.None; 20 | 21 | /// Aborts this pipeline execution. 22 | public void Abort() => IsAborted = true; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Pipelines/Core/IAbortableContext.cs: -------------------------------------------------------------------------------- 1 | namespace Artech.Pipelines 2 | { 3 | /// 4 | /// Represents the abortable pipeline based execution context. 5 | /// 6 | public interface IAbortableContext 7 | { 8 | /// 9 | /// Gets a value indicating whether this pipeline execution is aborted. 10 | /// 11 | /// 12 | /// true if this pipeline execution is aborted; otherwise, false. 13 | /// 14 | bool IsAborted { get; } 15 | 16 | /// Aborts this pipeline execution. 17 | void Abort(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Pipelines/Core/ICancellableContext.cs: -------------------------------------------------------------------------------- 1 | namespace Artech.Pipelines 2 | { 3 | /// 4 | /// Represents the cancellable pipeline execution context. 5 | /// 6 | public interface ICancellableContext 7 | { 8 | /// Gets the cancellation token. 9 | /// The cancellation token. 10 | CancellationToken CancellationToken { get; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Pipelines/Core/IPipeline.cs: -------------------------------------------------------------------------------- 1 | namespace Artech.Pipelines 2 | { 3 | /// 4 | /// Represents a pipeline based on specified context. 5 | /// 6 | /// The type of pipeline execution context. 7 | public interface IPipeline 8 | { 9 | /// 10 | /// Execute the pipeline against specified execution context. 11 | /// 12 | /// Pipeline execution context. 13 | /// The to execute the pipeline against specified execution context. 14 | ValueTask ProcessAsync(TContext context); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Pipelines/Core/IPipelineBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace Artech.Pipelines 2 | { 3 | /// 4 | /// Represents a builder to build pipeline with registered pipes. 5 | /// 6 | /// The type of the pipeline execution context. 7 | public interface IPipelineBuilder 8 | { 9 | /// Gets the application services (application wide dependency injection container). 10 | /// The application services (application wide dependency injection container). 11 | IServiceProvider ApplicationServices { get; } 12 | 13 | /// Gets the properties. 14 | /// The properties. 15 | IDictionary Properties { get; } 16 | 17 | /// Add the specified pipe. 18 | /// The pipe to add. 19 | /// The current 20 | IPipelineBuilder Use(Pipe pipe); 21 | 22 | /// Builds the pipeline with added pipes. 23 | /// The exported pipeline descriptive information. 24 | /// The representing the built pipeline. 25 | Func Build(out PipeDescriptorInfo exportedPipeline); 26 | 27 | /// Creates the new . 28 | /// The type of the pipeline context. 29 | /// The created . 30 | IPipelineBuilder CreateNew(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Pipelines/Core/IPipelineBuilderFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Artech.Pipelines 2 | { 3 | /// 4 | /// Represents the factory to create . 5 | /// 6 | public interface IPipelineBuilderFactory 7 | { 8 | /// Creates the . 9 | /// The type of the pipeline execution context. 10 | /// The created . 11 | IPipelineBuilder Create(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Pipelines/Core/IPipelineProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Artech.Pipelines 2 | { 3 | /// 4 | /// Represents the provider to register and get named pipeline. 5 | /// 6 | public interface IPipelineProvider 7 | { 8 | /// Registers the built pipeline. 9 | /// The type of the pipeline execution context. 10 | /// The registration name of pipeline. 11 | /// The delegate to built the pipeline to register. 12 | /// The current . 13 | IPipelineProvider AddPipeline(string name, Action> setup); 14 | 15 | /// Tries to get the restered pipeline based on specified name. 16 | /// The type of the pipeline execution context. 17 | /// The registration name of pipeline. 18 | /// The representing the pipeline to get. 19 | /// A value indicating whether the specified pipeline exists. 20 | bool TryGetPipeline(string name, out Func? pipeline); 21 | 22 | /// Exports all pipelines. 23 | /// The dictionary containing all named pipeline based . 24 | IDictionary ExportAllPipelines(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Pipelines/Core/Pipe.cs: -------------------------------------------------------------------------------- 1 | namespace Artech.Pipelines 2 | { 3 | /// 4 | /// Represents a single pipe of which the pipeline is composed. 5 | /// 6 | /// 7 | public abstract class Pipe 8 | { 9 | /// 10 | /// The functional description. 11 | /// 12 | public abstract string Description { get; } 13 | 14 | /// 15 | /// Exports the pipe's descriptive information. 16 | /// 17 | /// The describing the next pipe in the pipeline. 18 | /// The describing the pipe. 19 | public virtual PipeDescriptorInfo Export(PipeDescriptorInfo next) => new(Description, next); 20 | 21 | /// 22 | /// Invoke the functional operation. 23 | /// 24 | /// The execution context. 25 | /// The delegate used to invoke the next pipe. 26 | /// The to invoke the functional operation. 27 | public abstract ValueTask InvokeAsync(TContext context, Func next); 28 | } 29 | } -------------------------------------------------------------------------------- /src/Pipelines/Core/PipeDescriptorInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | namespace Artech.Pipelines 4 | { 5 | /// 6 | /// Represents the pipe descriptive object. 7 | /// 8 | public class PipeDescriptorInfo 9 | { 10 | #region Fields 11 | private PipeDescriptorInfo? _next; 12 | private PipeDescriptorInfo? _sub; 13 | private string? _exportedDescription; 14 | #endregion 15 | 16 | #region Properties 17 | /// 18 | /// Gets the singleton describing the terminal pipe. 19 | /// 20 | public static PipeDescriptorInfo Terminal { get; } = new("Terminal"); 21 | 22 | /// 23 | /// Gets pipe descriptive information 24 | /// 25 | public string Description { get; } 26 | 27 | /// 28 | /// Gets the describing the next pipe in the pipeline. 29 | /// 30 | public PipeDescriptorInfo? Next { get => _next; set { _next = value; _exportedDescription = null; } } 31 | 32 | /// 33 | /// Gets or sets the describing the sub-pipeline. 34 | /// 35 | public PipeDescriptorInfo? SubPipeline { get => _sub; set { _sub = value; _exportedDescription = null; } } 36 | #endregion 37 | 38 | #region Constructors 39 | /// 40 | /// Creates a new . 41 | /// 42 | /// Pipe descriptive information. 43 | /// The describing the next pipe in the pipeline. 44 | public PipeDescriptorInfo(string description, PipeDescriptorInfo? next = null) 45 | { 46 | Description = description ?? throw new ArgumentNullException(nameof(description)); 47 | Next = next; 48 | } 49 | #endregion 50 | 51 | #region Public methods 52 | /// Converts to string. 53 | /// A that represents this instance. 54 | public override string ToString() 55 | { 56 | if (_exportedDescription != null) 57 | { 58 | return _exportedDescription; 59 | } 60 | var builder = new StringBuilder(); 61 | Format().ForEach(it => builder.AppendLine(it)); 62 | return builder.ToString(); 63 | } 64 | #endregion 65 | 66 | #region Private methods 67 | private List Format() 68 | { 69 | var exportedList = new List(); 70 | Walk(exportedList, 0, 1, this); 71 | return exportedList; 72 | 73 | static void Walk(List exported, int indent, int counter, PipeDescriptorInfo? current) 74 | { 75 | if (current == null || current == Terminal) 76 | { 77 | return; 78 | } 79 | exported.Add($"{new string('\t', indent)}{counter}.{current.Description}"); 80 | var subPipeline = current.SubPipeline; 81 | if (subPipeline != null) 82 | { 83 | foreach (var sub in subPipeline.Format()) 84 | { 85 | exported.Add($"{new string('\t', indent + 1)}{sub}"); 86 | } 87 | } 88 | Walk(exported, indent, counter + 1, current.Next); 89 | } 90 | } 91 | #endregion 92 | } 93 | } -------------------------------------------------------------------------------- /src/Pipelines/Core/Pipeline.cs: -------------------------------------------------------------------------------- 1 | using Flight.Extensions.Primitives; 2 | 3 | namespace Artech.Pipelines 4 | { 5 | internal sealed class Pipeline : IPipeline 6 | { 7 | private readonly Func _pipeline; 8 | public Pipeline(IPipelineProvider pipelineProvider) 9 | { 10 | Guard.ArgumentNotNull(pipelineProvider, nameof(pipelineProvider)); 11 | if (!pipelineProvider.TryGetPipeline(PipelineDefaults.DefaultPipelineName, out var pipeline)) 12 | { 13 | throw new InvalidOperationException($"The default pipeline specific to '{typeof(TContext)}' is not registered."); 14 | } 15 | _pipeline = pipeline!; 16 | } 17 | 18 | internal Pipeline(Func pipeline) => _pipeline = pipeline ?? throw new ArgumentNullException(nameof(pipeline)); 19 | 20 | public ValueTask ProcessAsync(TContext context) => _pipeline(context); 21 | } 22 | } -------------------------------------------------------------------------------- /src/Pipelines/Core/PipelineBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace Artech.Pipelines 4 | { 5 | internal class PipelineBuilder : IPipelineBuilder 6 | { 7 | #region Fields 8 | private readonly List> _pipes = new(); 9 | private Dictionary _properties = new(); 10 | #endregion 11 | 12 | #region Properties 13 | public IServiceProvider ApplicationServices { get; } 14 | public IDictionary Properties => _properties ??= new(); 15 | #endregion 16 | 17 | #region Constructors 18 | public PipelineBuilder(IServiceProvider? applicationServices = null) => ApplicationServices = applicationServices ?? new ServiceCollection().BuildServiceProvider(); 19 | #endregion 20 | 21 | #region Public methods 22 | public IPipelineBuilder Use(Pipe pipe) 23 | { 24 | _pipes.Add(pipe ?? throw new ArgumentNullException(nameof(pipe))); 25 | return this; 26 | } 27 | 28 | public IPipelineBuilder CreateNew() => new PipelineBuilder(ApplicationServices) { _properties = _properties }; 29 | 30 | public Func Build(out PipeDescriptorInfo exportedPipeline) 31 | { 32 | PipeDescriptorInfo nextDescriptor = PipeDescriptorInfo.Terminal; 33 | for (int index = _pipes.Count - 1; index > -1; index--) 34 | { 35 | var pipe = _pipes[index]; 36 | nextDescriptor = pipe.Export(nextDescriptor); 37 | } 38 | exportedPipeline = nextDescriptor; 39 | return PipelineBuilder.Build(_pipes); 40 | } 41 | #endregion 42 | 43 | #region Private methods 44 | private static Func Build(IList> pipes) 45 | { 46 | Func nextPipeline = _ => ValueTask.CompletedTask; 47 | for (int index = pipes.Count - 1; index > -1; index--) 48 | { 49 | var pipe = pipes[index]; 50 | var convertedPipe = Convert(pipe); 51 | nextPipeline = convertedPipe(nextPipeline); 52 | } 53 | return nextPipeline; 54 | 55 | static Func, Func> Convert(Pipe pipe) 56 | { 57 | return next => context => InvokeAsync(pipe, context, next); 58 | } 59 | 60 | static ValueTask InvokeAsync(Pipe pipe, TContext context, Func next) 61 | { 62 | if (context is ICancellableContext cancellableContext) 63 | { 64 | cancellableContext.CancellationToken.ThrowIfCancellationRequested(); 65 | } 66 | if (context is IAbortableContext abortableContext && abortableContext.IsAborted) 67 | { 68 | return ValueTask.CompletedTask; 69 | } 70 | return pipe.InvokeAsync(context, next); 71 | } 72 | } 73 | #endregion 74 | } 75 | } -------------------------------------------------------------------------------- /src/Pipelines/Core/PipelineBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace Artech.Pipelines 4 | { 5 | /// 6 | /// Defines extension methods to register pipe to specified 7 | /// 8 | public static class PipelineBuilderExtensions 9 | { 10 | /// Register specified type of pipe. 11 | /// The type of the pipeline execution context. 12 | /// The type of the pipe to register. 13 | /// The to builld pipeline with registered pipes. 14 | /// The extra arguments passed to pipe type's constructor. 15 | /// The current 16 | public static IPipelineBuilder Use(this IPipelineBuilder pipleineBuilder, params object[] arguments) where TPipe : Pipe 17 | { 18 | if (pipleineBuilder == null) 19 | { 20 | throw new ArgumentNullException(nameof(pipleineBuilder)); 21 | } 22 | var pipe = ActivatorUtilities.CreateInstance(pipleineBuilder.ApplicationServices, arguments); 23 | return pipleineBuilder.Use(pipe); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/Pipelines/Core/PipelineBuilderFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Artech.Pipelines 2 | { 3 | internal sealed class PipelineBuilderFactory : IPipelineBuilderFactory 4 | { 5 | private readonly IServiceProvider _applicationServices; 6 | public PipelineBuilderFactory(IServiceProvider applicationServices) => _applicationServices = applicationServices ?? throw new ArgumentNullException(nameof(applicationServices)); 7 | public IPipelineBuilder Create()=> new PipelineBuilder(_applicationServices); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Pipelines/Core/PipelineDefaults.cs: -------------------------------------------------------------------------------- 1 | namespace Artech.Pipelines 2 | { 3 | /// 4 | /// Defaults some defult settings. 5 | /// 6 | public static class PipelineDefaults 7 | { 8 | /// 9 | /// The registration name of default pipeline. 10 | /// 11 | public static readonly string DefaultPipelineName = "Default"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Pipelines/Core/PipelineProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace Artech.Pipelines 4 | { 5 | internal sealed class PipelineProvider : IPipelineProvider 6 | { 7 | #region Fields 8 | private readonly IPipelineBuilderFactory _pipelineBuilderFactory; 9 | private readonly Dictionary _pipelines = new(); 10 | private readonly Dictionary _exportedPipelines = new(); 11 | #endregion 12 | 13 | #region Constructors 14 | public PipelineProvider(IPipelineBuilderFactory pipelineBuilderFactory) 15 | => _pipelineBuilderFactory = pipelineBuilderFactory ?? throw new ArgumentNullException(nameof(pipelineBuilderFactory)); 16 | #endregion 17 | 18 | #region Public methods 19 | public IPipelineProvider AddPipeline(string name, Action> setup) 20 | { 21 | if (name == null) 22 | { 23 | throw new ArgumentNullException(nameof(name)); 24 | } 25 | if (string.IsNullOrWhiteSpace(name)) 26 | { 27 | throw new ArgumentException("Specified pipeline name cannot be white space.", nameof(name)); 28 | } 29 | var builder = _pipelineBuilderFactory.Create(); 30 | (setup ?? throw new ArgumentNullException(nameof(setup))).Invoke(builder); 31 | _pipelines[name] = builder.Build(out var descriptor); 32 | _exportedPipelines[name] = descriptor; 33 | return this; 34 | } 35 | 36 | public IDictionary ExportAllPipelines() => _exportedPipelines; 37 | 38 | public bool TryGetPipeline(string name, out Func? pipeline) 39 | { 40 | if (_pipelines.TryGetValue(name, out var value)) 41 | { 42 | if (value is Func result) 43 | { 44 | pipeline = result; 45 | return true; 46 | } 47 | throw new ArgumentException("The specified name does not match pipeline execution context type.", nameof(name)); 48 | } 49 | pipeline = null; 50 | return false; 51 | } 52 | #endregion 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Pipelines/DelegatePipe.cs: -------------------------------------------------------------------------------- 1 | namespace Artech.Pipelines 2 | { 3 | /// 4 | /// The pipe executing functional operation by invoking specified delegate. 5 | /// 6 | /// The type of the context. 7 | public sealed class DelegatePipe : Pipe 8 | { 9 | private readonly Func, ValueTask> _handler; 10 | 11 | /// Gets the description. 12 | /// The description. 13 | public override string Description { get; } 14 | 15 | /// Initializes a new instance of the class. 16 | /// The handler. 17 | /// The description. 18 | public DelegatePipe(Func, ValueTask> handler, string description) 19 | { 20 | _handler = handler ?? throw new ArgumentNullException(nameof(handler)); 21 | Description = description ?? throw new ArgumentNullException(nameof(description)); 22 | } 23 | 24 | /// Initializes a new instance of the class. 25 | /// The handler. 26 | /// The description. 27 | public DelegatePipe(Func, Task> handler, string description) 28 | { 29 | if (handler == null) 30 | { 31 | throw new ArgumentNullException(nameof(handler)); 32 | } 33 | _handler = async (context, next) => 34 | { 35 | await handler(context, next); 36 | await next(context); 37 | }; 38 | Description = description ?? throw new ArgumentNullException(nameof(description)); 39 | } 40 | 41 | /// 42 | /// Initializes a new instance of the class. 43 | /// 44 | /// The handler. 45 | /// The description. 46 | public DelegatePipe(Func handler, string description) 47 | { 48 | if (handler == null) 49 | { 50 | throw new ArgumentNullException(nameof(handler)); 51 | } 52 | _handler = async (context, next) => 53 | { 54 | await handler(context); 55 | await next(context); 56 | }; 57 | Description = description ?? throw new ArgumentNullException(nameof(description)); 58 | } 59 | 60 | /// 61 | /// Initializes a new instance of the class. 62 | /// 63 | /// The handler. 64 | /// The description. 65 | public DelegatePipe(Func handler, string description) 66 | { 67 | if (handler == null) 68 | { 69 | throw new ArgumentNullException(nameof(handler)); 70 | } 71 | _handler = async (context, next) => 72 | { 73 | await handler(context); 74 | await next(context); 75 | }; 76 | Description = description ?? throw new ArgumentNullException(nameof(description)); 77 | } 78 | 79 | /// Initializes a new instance of the class. 80 | /// The handler. 81 | /// The description. 82 | public DelegatePipe(Action handler, string description) 83 | { 84 | if (handler == null) 85 | { 86 | throw new ArgumentNullException(nameof(handler)); 87 | } 88 | _handler = (context, next) => 89 | { 90 | handler(context); 91 | return next(context); 92 | }; 93 | Description = description ?? throw new ArgumentNullException(nameof(description)); 94 | } 95 | 96 | /// 97 | /// Invoke the functional operation. 98 | /// 99 | /// The execution context. 100 | /// The delegate used to invoke the next pipe. 101 | /// 102 | /// The to invoke the functional operation. 103 | /// 104 | public override ValueTask InvokeAsync(TContext context, Func next) => _handler(context, next); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Pipelines/DelegatePipeRegistrationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Flight.Extensions.Primitives; 2 | 3 | namespace Artech.Pipelines 4 | { 5 | /// 6 | ///
    7 | ///
    8 | public static class DelegatePipeRegistrationExtensions 9 | { 10 | /// Registers a based on specified handler. 11 | /// The type of the context. 12 | /// The . 13 | /// The handler of the to register. 14 | /// The description of the to register. 15 | /// The current . 16 | public static IPipelineBuilder Use(this IPipelineBuilder builder, Func, ValueTask> handler, string description) 17 | { 18 | Guard.ArgumentNotNull(builder, nameof(builder)); 19 | Guard.ArgumentNotNull(handler, nameof(handler)); 20 | Guard.ArgumentNotNullOrWhiteSpace(description, nameof(description)); 21 | return builder.Use(new DelegatePipe(handler, description)); 22 | } 23 | 24 | /// Registers a based on specified handler. 25 | /// The type of the context. 26 | /// The . 27 | /// The handler of the to register. 28 | /// The description of the to register. 29 | /// The current . 30 | public static IPipelineBuilder Use(this IPipelineBuilder builder, Func handler, string description) 31 | { 32 | Guard.ArgumentNotNull(builder, nameof(builder)); 33 | Guard.ArgumentNotNull(handler, nameof(handler)); 34 | Guard.ArgumentNotNullOrWhiteSpace(description, nameof(description)); 35 | return builder.Use(new DelegatePipe(handler, description)); 36 | } 37 | 38 | /// Registers a based on specified handler. 39 | /// The type of the context. 40 | /// The . 41 | /// The handler of the to register. 42 | /// The description of the to register. 43 | /// The current . 44 | public static IPipelineBuilder Use(this IPipelineBuilder builder, Action handler, string description) 45 | { 46 | Guard.ArgumentNotNull(builder, nameof(builder)); 47 | Guard.ArgumentNotNull(handler, nameof(handler)); 48 | Guard.ArgumentNotNullOrWhiteSpace(description, nameof(description)); 49 | return builder.Use(new DelegatePipe(handler, description)); 50 | } 51 | 52 | /// Registers a based on specified handler. 53 | /// The type of the context. 54 | /// The . 55 | /// The handler of the to register. 56 | /// The description of the to register. 57 | /// The current . 58 | public static IPipelineBuilder Use(this IPipelineBuilder builder, Func, Task> handler, string description) 59 | { 60 | Guard.ArgumentNotNull(builder, nameof(builder)); 61 | Guard.ArgumentNotNull(handler, nameof(handler)); 62 | Guard.ArgumentNotNullOrWhiteSpace(description, nameof(description)); 63 | return builder.Use(new DelegatePipe(handler, description)); 64 | } 65 | 66 | /// Registers a based on specified handler. 67 | /// The type of the context. 68 | /// The . 69 | /// The handler of the to register. 70 | /// The description of the to register. 71 | /// The current . 72 | public static IPipelineBuilder Use(this IPipelineBuilder builder, Func handler, string description) 73 | { 74 | Guard.ArgumentNotNull(builder, nameof(builder)); 75 | Guard.ArgumentNotNull(handler, nameof(handler)); 76 | Guard.ArgumentNotNullOrWhiteSpace(description, nameof(description)); 77 | return builder.Use(new DelegatePipe(handler, description)); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Pipelines/Guard.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Flight.Extensions.Primitives 6 | { 7 | /// 8 | /// Defines argument validation based utility methods. 9 | /// 10 | public static class Guard 11 | { 12 | /// 13 | /// Ensures specified argument is not null. 14 | /// 15 | /// The type of argument to validate. 16 | /// The argument value. 17 | /// The argument name. 18 | /// The argument value. 19 | public static T ArgumentNotNull(T value, string name) where T:class 20 | { 21 | return value ?? throw new ArgumentNullException(name); 22 | } 23 | 24 | /// 25 | /// Ensures specified argument is not null or empty. 26 | /// 27 | /// The element type of argument collection to validate. 28 | /// The argument value. 29 | /// The argument name. 30 | /// The argument value. 31 | public static IEnumerable ArgumentNotNullOrEmpty(IEnumerable value, string name) 32 | { 33 | ArgumentNotNull(value, name); 34 | if (!value.Any()) 35 | { 36 | throw new ArgumentException($"Argument value cannot be an empty collection", name); 37 | } 38 | return value; 39 | } 40 | 41 | /// 42 | /// Ensures specified argument is not null or white space. 43 | /// 44 | /// The argument value. 45 | /// The argument name. 46 | /// The argument value. 47 | public static string ArgumentNotNullOrWhiteSpace(string value, string name) 48 | { 49 | if (value == null) 50 | { 51 | throw new ArgumentNullException(name); 52 | } 53 | if (string.IsNullOrWhiteSpace(value)) 54 | { 55 | throw new ArgumentException("Argument value cannot be null or a white space string.", name); 56 | } 57 | return value; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Pipelines/PipeBase.cs: -------------------------------------------------------------------------------- 1 | namespace Artech.Pipelines 2 | { 3 | /// 4 | /// A base class for synchronous pipe classes. 5 | /// 6 | /// The type of the context. 7 | /// 8 | public abstract class PipeBase : Pipe 9 | { 10 | /// 11 | /// Invoke the functional operation. 12 | /// 13 | /// The execution context. 14 | /// The delegate used to invoke the next pipe. 15 | /// 16 | /// The to invoke the functional operation. 17 | /// 18 | public override ValueTask InvokeAsync(TContext context, Func next) 19 | { 20 | Invoke(context); 21 | return next(context); 22 | } 23 | /// 24 | /// Invoke the functional operation synchronously. 25 | /// 26 | /// The execution context. 27 | protected abstract void Invoke(TContext context); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Pipelines/Pipelines.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | Artech.Pipelines 8 | Artech.Pipelines 9 | True 10 | 1.0.1 11 | 12 | 13 | 14 | True 15 | 16 | 17 | 18 | True 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Pipelines/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Pipelines.Test")] -------------------------------------------------------------------------------- /src/Pipelines/SubPipeline/LoopPipe.cs: -------------------------------------------------------------------------------- 1 | namespace Artech.Pipelines 2 | { 3 | /// 4 | /// A pipe which repeatedly execute its sub pipeline against each item of a collection from current context. 5 | /// 6 | /// The type of the pipeline execution context. 7 | /// The type of the sub pipeline execution context. 8 | /// The type of the element of collection. 9 | public sealed class LoopPipe : Pipe 10 | where TContext : ContextBase 11 | where TSubContext : SubContextBase, new() 12 | { 13 | #region Fields 14 | private readonly Func _subPipeline; 15 | private readonly Func> _collectionAccessor; 16 | private readonly Func _itemFilter; 17 | private readonly PipeDescriptorInfo _subPipelineDescriptor; 18 | private readonly string? _description; 19 | #endregion 20 | 21 | #region Properties 22 | /// The functional description. 23 | public override string Description => _description ?? $"For each {typeof(TItem)}"; 24 | 25 | #endregion 26 | 27 | #region Constructors 28 | /// Initializes a new instance of the class. 29 | /// The delegate used to get the collection to loop. 30 | /// The used to filter the iterated item. 31 | /// The representing the sub pipeline. 32 | /// The describing the sub pipeline descriptor. 33 | /// The descriptive information. 34 | public LoopPipe( 35 | Func> collectionAccessor, 36 | Func filter, 37 | Func subPipeline, 38 | PipeDescriptorInfo subPipelineDescriptor, 39 | string? description = null) 40 | { 41 | _collectionAccessor = collectionAccessor ?? throw new ArgumentNullException(nameof(collectionAccessor)); 42 | _subPipeline = subPipeline ?? throw new ArgumentNullException(nameof(subPipeline)); 43 | _itemFilter = filter ?? throw new ArgumentNullException(nameof(filter)); 44 | _subPipelineDescriptor = subPipelineDescriptor ?? throw new ArgumentNullException(nameof(subPipelineDescriptor)); 45 | _description = description; 46 | } 47 | #endregion 48 | 49 | #region Public methods 50 | /// Exports the pipe's descriptive information. 51 | /// The describing the next pipe in the pipeline. 52 | /// The describing the pipe. 53 | public override PipeDescriptorInfo Export(PipeDescriptorInfo next) 54 | { 55 | var pipeInfo = base.Export(next); 56 | pipeInfo.SubPipeline = _subPipelineDescriptor; 57 | return pipeInfo; 58 | } 59 | 60 | /// Invoke the functional operation. 61 | /// The execution context. 62 | /// The delegate used to invoke the next pipe. 63 | /// The ValueTask to invoke the functional operation. 64 | public override async ValueTask InvokeAsync(TContext context, Func next) 65 | { 66 | var collection = _collectionAccessor(context); 67 | foreach (var item in collection) 68 | { 69 | context.CancellationToken.ThrowIfCancellationRequested(); 70 | if (_itemFilter(context, item)) 71 | { 72 | var subContext = new TSubContext(); 73 | subContext.Initialize(context, item); 74 | await _subPipeline(subContext); 75 | } 76 | } 77 | await next(context); 78 | } 79 | #endregion 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Pipelines/SubPipeline/LoopPipeRegistrationExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Artech.Pipelines 2 | { 3 | /// 4 | /// Defines extension methods to register . 5 | /// 6 | public static class LoopPipeRegistrationExtensions 7 | { 8 | /// Registers . 9 | /// The type of the pipeline execution context. 10 | /// The type of the sub pipeline execution context. 11 | /// The type of the element of collection. 12 | /// The used to build pipeline with registered pipes. 13 | /// The descriptive information. 14 | /// The delegate to get collection to loop. 15 | /// The used to filter the iterated item. 16 | /// The sub pipeline setup. 17 | /// The current 18 | public static IPipelineBuilder ForEach( 19 | this IPipelineBuilder pipelineBuilder, 20 | string description, 21 | Func> collectionAccessor, 22 | Func filter, 23 | Action> subPipelineSetup) 24 | where TContext : ContextBase 25 | where TSubContext : SubContextBase, new() 26 | { 27 | var subBuilder = pipelineBuilder.CreateNew(); 28 | (subPipelineSetup ?? throw new ArgumentNullException(nameof(subPipelineSetup))).Invoke(subBuilder); 29 | var subPipeline = subBuilder.Build(out var subPipelineDescriptor); 30 | var pipe = new LoopPipe(collectionAccessor, filter, subPipeline, subPipelineDescriptor, description); 31 | return pipelineBuilder.Use(pipe); 32 | } 33 | 34 | /// Registers . 35 | /// The type of the pipeline execution context. 36 | /// The type of the sub pipeline execution context. 37 | /// The type of the element of collection. 38 | /// The used to build pipeline with registered pipes. 39 | /// The descriptive information. 40 | /// The delegate to get collection to loop. 41 | /// The sub pipeline setup. 42 | /// The current 43 | public static IPipelineBuilder ForEach( 44 | this IPipelineBuilder pipelineBuilder, 45 | string description, 46 | Func> collectionAccessor, 47 | Action> subPipelineSetup) 48 | where TContext : ContextBase 49 | where TSubContext : SubContextBase, new() 50 | { 51 | var subBuilder = pipelineBuilder.CreateNew(); 52 | (subPipelineSetup ?? throw new ArgumentNullException(nameof(subPipelineSetup))).Invoke(subBuilder); 53 | var subPipeline = subBuilder.Build(out var subPipelineDescriptor); 54 | var pipe = new LoopPipe(collectionAccessor, (_,_) => true, subPipeline, subPipelineDescriptor, description); 55 | return pipelineBuilder.Use(pipe); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Pipelines/SubPipeline/SubContextBase.cs: -------------------------------------------------------------------------------- 1 | namespace Artech.Pipelines 2 | { 3 | /// 4 | /// The base type of sub pipeline execution context. 5 | /// 6 | /// The type of the parent pipeline execution context. 7 | /// The type of the element of collection. 8 | public abstract class SubContextBase : ContextBase where TParentContext : ContextBase 9 | { 10 | /// Gets the parent pipeline execution context. 11 | /// The parent pipeline execution context. 12 | public TParentContext Parent { get; private set; } = default!; 13 | 14 | /// Gets the item of the collection to loop. 15 | /// The item of the collection to loop. 16 | public TItem Item { get; private set; } = default!; 17 | 18 | /// Gets the cancellation token. 19 | /// The cancellation token. 20 | public override CancellationToken CancellationToken => Parent?.CancellationToken ?? CancellationToken.None; 21 | 22 | /// Initializes the current pipeline execution context. 23 | /// The current pipeline execution context. 24 | /// The item of collection to loop. 25 | public virtual void Initialize(TParentContext parent, TItem item) 26 | { 27 | Parent = parent ?? throw new ArgumentNullException(nameof(parent)); 28 | Item = item; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/Pipelines.Test/PipeDescriptorInfoFixture.cs: -------------------------------------------------------------------------------- 1 | using Artech.Pipelines; 2 | using Xunit; 3 | 4 | namespace Pipelines.Test 5 | { 6 | public class PipeDescriptorInfoFixture 7 | { 8 | [Fact] 9 | public void Format() 10 | { 11 | var baz = new PipeDescriptorInfo("Baz", PipeDescriptorInfo.Terminal); 12 | var bar = new PipeDescriptorInfo("Bar", baz); 13 | 14 | var bar3 = new PipeDescriptorInfo("Bar3", PipeDescriptorInfo.Terminal); 15 | var bar2 = new PipeDescriptorInfo("Bar2", bar3); 16 | var bar1 = new PipeDescriptorInfo("Bar1", bar2); 17 | bar.SubPipeline = bar1; 18 | 19 | var foo = new PipeDescriptorInfo("Foo", bar); 20 | Assert.Equal("Foo\r\nBar\r\n\tBar1\r\n\tBar2\r\n\tBar3\r\nBaz\r\n", foo.ToString()); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /test/Pipelines.Test/PipelineFixture.cs: -------------------------------------------------------------------------------- 1 | //using Artech.Pipelines; 2 | //using System; 3 | //using System.Collections.Generic; 4 | //using System.Linq; 5 | //using System.Text; 6 | //using System.Threading.Tasks; 7 | //using Xunit; 8 | 9 | //namespace Pipelines.Test 10 | //{ 11 | // public class PipelineFixture 12 | // { 13 | // [Fact] 14 | // public async void PassThroughAsync() 15 | // { 16 | // var pipeline = new Pipeline(new List> { new Pipe("foo"), new Pipe("bar"), new Pipe("baz")}); 17 | // _list.Clear(); 18 | // await pipeline.PassThroughAsync(new Context()); 19 | // Assert.Equal(3, _list.Count); 20 | // Assert.Equal("foo", _list[0]); 21 | // Assert.Equal("bar", _list[1]); 22 | // Assert.Equal("baz", _list[2]); 23 | // } 24 | 25 | // private static List _list = new List(); 26 | // private class Context { } 27 | // private class Pipe : Pipe 28 | // { 29 | // public Pipe(string description)=> Description = description; 30 | // public override string Description { get; } 31 | // protected override ValueTask InvokeAsync(Context context, Pipeline next) 32 | // { 33 | // _list.Add(Description); 34 | // return next.PassThroughAsync(context); 35 | // } 36 | // } 37 | // } 38 | //} 39 | -------------------------------------------------------------------------------- /test/Pipelines.Test/Pipelines.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | false 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | all 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /test/Pipelines.Test/PipleineBuilderFixture.cs: -------------------------------------------------------------------------------- 1 | using Artech.Pipelines; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | 9 | namespace Pipelines.Test 10 | { 11 | public class PipleineBuilderFixture 12 | { 13 | [Fact] 14 | public async void Build() 15 | { 16 | var pipeline = new PipelineBuilder() 17 | .Use(new Pipe("foo")) 18 | .Use(new Pipe("bar")) 19 | .Use(new Pipe("baz")) 20 | .Build(out var descriptor); 21 | _list.Clear(); 22 | await pipeline(new Context()); 23 | Assert.Equal(3, _list.Count); 24 | Assert.Equal("foo", _list[0]); 25 | Assert.Equal("bar", _list[1]); 26 | Assert.Equal("baz", _list[2]); 27 | Assert.Equal("foo\r\nbar\r\nbaz\r\n", descriptor.ToString()); 28 | } 29 | 30 | private static List _list = new List(); 31 | private class Context { } 32 | private class Pipe : Pipe 33 | { 34 | public Pipe(string description) => Description = description; 35 | public override string Description { get; } 36 | 37 | public override ValueTask InvokeAsync(Context context, Func next) 38 | { 39 | _list.Add(Description); 40 | return next(context); 41 | } 42 | } 43 | } 44 | } 45 | --------------------------------------------------------------------------------