├── .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 |
--------------------------------------------------------------------------------