├── .gitignore
├── HttpBatchHandler.sln
├── HttpBatchHandler.sln.DotSettings
├── LICENSE
├── NuGet.Config
├── README.md
├── azure-pipelines.yml
├── performance
├── batch.lua
├── payload.txt
└── run.sh
├── src
└── HttpBatchHandler
│ ├── AssemblyInfo.cs
│ ├── BatchMiddleware.cs
│ ├── BatchMiddlewareExtensions.cs
│ ├── Events
│ ├── BatchEndContext.cs
│ ├── BatchMiddlewareEvents.cs
│ ├── BatchMiddlewareOptions.cs
│ ├── BatchRequestExecutedContext.cs
│ ├── BatchRequestExecutingContext.cs
│ ├── BatchRequestPreparationContext.cs
│ └── BatchStartContext.cs
│ ├── HttpBatchHandler.csproj
│ ├── HttpRequestExtensions.cs
│ ├── HttpResponseExtensions.cs
│ ├── Multipart
│ ├── HttpApplicationContent.cs
│ ├── HttpApplicationMultipart.cs
│ ├── HttpApplicationRequestSection.cs
│ ├── HttpApplicationRequestSectionExtensions.cs
│ ├── HttpApplicationResponseSection.cs
│ ├── HttpApplicationResponseSectionExtensions.cs
│ ├── HttpContentExtensions.cs
│ ├── IMultipart.cs
│ ├── MultipartWriter.cs
│ └── SectionHelper.cs
│ ├── RequestState.cs
│ ├── ResponseFeature.cs
│ ├── StreamExtensions.cs
│ └── WriteOnlyResponseStream.cs
└── test
├── HttpBatchHandler.Benchmarks
├── HttpBatchHandler.Benchmarks.csproj
├── KestrelBenchmark.cs
├── MultipartReaderBenchmark.cs
├── MultipartWriterBenchmark.cs
├── Program.cs
├── Tools
│ ├── RandomPortHelper.cs
│ ├── Setup.cs
│ └── ValuesController.cs
└── payload.bin
├── HttpBatchHandler.Tests
├── BaseServerTests.cs
├── BaseWriterTests.cs
├── BatchMiddlewareTestFixture.cs
├── BatchMiddlewareTests.cs
├── HttpBatchHandler.Tests.csproj
├── MultipartContentTests.cs
├── MultipartParserTests.cs
├── MultipartRequest.txt
├── MultipartRequestPathBase.txt
├── MultipartResponse.txt
├── MultipartWriterTests.cs
├── RandomPortHelper.cs
├── ServerTestsWithPathBase.cs
├── ServerTestsWithoutPathBase.cs
├── TestFixture.cs
├── TestFixtureWithPathBase.cs
├── TestFixtureWithoutPathBase.cs
├── TestUtilities.cs
└── WriteOnlyResponseStreamTests.cs
└── HttpBatchHandler.Website
├── Controllers
└── ValuesController.cs
├── HttpBatchHandler.Website.csproj
├── Program.cs
├── Startup.cs
├── appsettings.Development.json
└── appsettings.json
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.suo
8 | *.user
9 | *.userosscache
10 | *.sln.docstates
11 |
12 | # User-specific files (MonoDevelop/Xamarin Studio)
13 | *.userprefs
14 |
15 | # Build results
16 | [Dd]ebug/
17 | [Dd]ebugPublic/
18 | [Rr]elease/
19 | [Rr]eleases/
20 | x64/
21 | x86/
22 | bld/
23 | [Bb]in/
24 | [Oo]bj/
25 | [Ll]og/
26 |
27 | # Visual Studio 2015 cache/options directory
28 | .vs/
29 | # Uncomment if you have tasks that create the project's static files in wwwroot
30 | #wwwroot/
31 |
32 | # MSTest test Results
33 | [Tt]est[Rr]esult*/
34 | [Bb]uild[Ll]og.*
35 |
36 | # NUNIT
37 | *.VisualState.xml
38 | TestResult.xml
39 |
40 | # Build Results of an ATL Project
41 | [Dd]ebugPS/
42 | [Rr]eleasePS/
43 | dlldata.c
44 |
45 | # .NET Core
46 | project.lock.json
47 | project.fragment.lock.json
48 | artifacts/
49 | **/Properties/launchSettings.json
50 |
51 | *_i.c
52 | *_p.c
53 | *_i.h
54 | *.ilk
55 | *.meta
56 | *.obj
57 | *.pch
58 | *.pdb
59 | *.pgc
60 | *.pgd
61 | *.rsp
62 | *.sbr
63 | *.tlb
64 | *.tli
65 | *.tlh
66 | *.tmp
67 | *.tmp_proj
68 | *.log
69 | *.vspscc
70 | *.vssscc
71 | .builds
72 | *.pidb
73 | *.svclog
74 | *.scc
75 |
76 | # Chutzpah Test files
77 | _Chutzpah*
78 |
79 | # Visual C++ cache files
80 | ipch/
81 | *.aps
82 | *.ncb
83 | *.opendb
84 | *.opensdf
85 | *.sdf
86 | *.cachefile
87 | *.VC.db
88 | *.VC.VC.opendb
89 |
90 | # Visual Studio profiler
91 | *.psess
92 | *.vsp
93 | *.vspx
94 | *.sap
95 |
96 | # TFS 2012 Local Workspace
97 | $tf/
98 |
99 | # Guidance Automation Toolkit
100 | *.gpState
101 |
102 | # ReSharper is a .NET coding add-in
103 | _ReSharper*/
104 | *.[Rr]e[Ss]harper
105 | *.DotSettings.user
106 |
107 | # JustCode is a .NET coding add-in
108 | .JustCode
109 |
110 | # TeamCity is a build add-in
111 | _TeamCity*
112 |
113 | # DotCover is a Code Coverage Tool
114 | *.dotCover
115 |
116 | # Visual Studio code coverage results
117 | *.coverage
118 | *.coveragexml
119 |
120 | # NCrunch
121 | _NCrunch_*
122 | .*crunch*.local.xml
123 | nCrunchTemp_*
124 |
125 | # MightyMoose
126 | *.mm.*
127 | AutoTest.Net/
128 |
129 | # Web workbench (sass)
130 | .sass-cache/
131 |
132 | # Installshield output folder
133 | [Ee]xpress/
134 |
135 | # DocProject is a documentation generator add-in
136 | DocProject/buildhelp/
137 | DocProject/Help/*.HxT
138 | DocProject/Help/*.HxC
139 | DocProject/Help/*.hhc
140 | DocProject/Help/*.hhk
141 | DocProject/Help/*.hhp
142 | DocProject/Help/Html2
143 | DocProject/Help/html
144 |
145 | # Click-Once directory
146 | publish/
147 |
148 | # Publish Web Output
149 | *.[Pp]ublish.xml
150 | *.azurePubxml
151 | # TODO: Comment the next line if you want to checkin your web deploy settings
152 | # but database connection strings (with potential passwords) will be unencrypted
153 | *.pubxml
154 | *.publishproj
155 |
156 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
157 | # checkin your Azure Web App publish settings, but sensitive information contained
158 | # in these scripts will be unencrypted
159 | PublishScripts/
160 |
161 | # NuGet Packages
162 | *.nupkg
163 | # The packages folder can be ignored because of Package Restore
164 | **/packages/*
165 | # except build/, which is used as an MSBuild target.
166 | !**/packages/build/
167 | # Uncomment if necessary however generally it will be regenerated when needed
168 | #!**/packages/repositories.config
169 | # NuGet v3's project.json files produces more ignorable files
170 | *.nuget.props
171 | *.nuget.targets
172 |
173 | # Microsoft Azure Build Output
174 | csx/
175 | *.build.csdef
176 |
177 | # Microsoft Azure Emulator
178 | ecf/
179 | rcf/
180 |
181 | # Windows Store app package directories and files
182 | AppPackages/
183 | BundleArtifacts/
184 | Package.StoreAssociation.xml
185 | _pkginfo.txt
186 |
187 | # Visual Studio cache files
188 | # files ending in .cache can be ignored
189 | *.[Cc]ache
190 | # but keep track of directories ending in .cache
191 | !*.[Cc]ache/
192 |
193 | # Others
194 | ClientBin/
195 | ~$*
196 | *~
197 | *.dbmdl
198 | *.dbproj.schemaview
199 | *.jfm
200 | *.pfx
201 | *.publishsettings
202 | orleans.codegen.cs
203 |
204 | # Since there are multiple workflows, uncomment next line to ignore bower_components
205 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
206 | #bower_components/
207 |
208 | # RIA/Silverlight projects
209 | Generated_Code/
210 |
211 | # Backup & report files from converting an old project file
212 | # to a newer Visual Studio version. Backup files are not needed,
213 | # because we have git ;-)
214 | _UpgradeReport_Files/
215 | Backup*/
216 | UpgradeLog*.XML
217 | UpgradeLog*.htm
218 |
219 | # SQL Server files
220 | *.mdf
221 | *.ldf
222 | *.ndf
223 |
224 | # Business Intelligence projects
225 | *.rdl.data
226 | *.bim.layout
227 | *.bim_*.settings
228 |
229 | # Microsoft Fakes
230 | FakesAssemblies/
231 |
232 | # GhostDoc plugin setting file
233 | *.GhostDoc.xml
234 |
235 | # Node.js Tools for Visual Studio
236 | .ntvs_analysis.dat
237 | node_modules/
238 |
239 | # Typescript v1 declaration files
240 | typings/
241 |
242 | # Visual Studio 6 build log
243 | *.plg
244 |
245 | # Visual Studio 6 workspace options file
246 | *.opt
247 |
248 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
249 | *.vbw
250 |
251 | # Visual Studio LightSwitch build output
252 | **/*.HTMLClient/GeneratedArtifacts
253 | **/*.DesktopClient/GeneratedArtifacts
254 | **/*.DesktopClient/ModelManifest.xml
255 | **/*.Server/GeneratedArtifacts
256 | **/*.Server/ModelManifest.xml
257 | _Pvt_Extensions
258 |
259 | # Paket dependency manager
260 | .paket/paket.exe
261 | paket-files/
262 |
263 | # FAKE - F# Make
264 | .fake/
265 |
266 | # JetBrains Rider
267 | .idea/
268 | *.sln.iml
269 |
270 | # CodeRush
271 | .cr/
272 |
273 | # Python Tools for Visual Studio (PTVS)
274 | __pycache__/
275 | *.pyc
276 |
277 | # Cake - Uncomment if you are using it
278 | # tools/**
279 | # !tools/packages.config
280 |
281 | # Telerik's JustMock configuration file
282 | *.jmconfig
283 |
284 | # BizTalk build output
285 | *.btp.cs
286 | *.btm.cs
287 | *.odx.cs
288 | *.xsd.cs
289 | /test/HttpBatchHandler.Website/Published
290 | /test/HttpBatchHandler.Benchmarks/BenchmarkDotNet.Artifacts/results
291 |
--------------------------------------------------------------------------------
/HttpBatchHandler.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.29001.49
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpBatchHandler", "src\HttpBatchHandler\HttpBatchHandler.csproj", "{4BB4B450-3407-4B95-A504-19C1A678B83C}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpBatchHandler.Tests", "test\HttpBatchHandler.Tests\HttpBatchHandler.Tests.csproj", "{9446A0ED-4D0C-463E-9395-0E22830E4DEA}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpBatchHandler.Website", "test\HttpBatchHandler.Website\HttpBatchHandler.Website.csproj", "{30ABB2A7-8028-4098-89AD-C83E08D3390D}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HttpBatchHandler.Benchmarks", "test\HttpBatchHandler.Benchmarks\HttpBatchHandler.Benchmarks.csproj", "{49F2296C-5C1F-4F7D-B52F-1530C52F004B}"
13 | EndProject
14 | Global
15 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
16 | Debug|Any CPU = Debug|Any CPU
17 | Release|Any CPU = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
20 | {4BB4B450-3407-4B95-A504-19C1A678B83C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {4BB4B450-3407-4B95-A504-19C1A678B83C}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {4BB4B450-3407-4B95-A504-19C1A678B83C}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {4BB4B450-3407-4B95-A504-19C1A678B83C}.Release|Any CPU.Build.0 = Release|Any CPU
24 | {9446A0ED-4D0C-463E-9395-0E22830E4DEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25 | {9446A0ED-4D0C-463E-9395-0E22830E4DEA}.Debug|Any CPU.Build.0 = Debug|Any CPU
26 | {9446A0ED-4D0C-463E-9395-0E22830E4DEA}.Release|Any CPU.ActiveCfg = Release|Any CPU
27 | {9446A0ED-4D0C-463E-9395-0E22830E4DEA}.Release|Any CPU.Build.0 = Release|Any CPU
28 | {30ABB2A7-8028-4098-89AD-C83E08D3390D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29 | {30ABB2A7-8028-4098-89AD-C83E08D3390D}.Debug|Any CPU.Build.0 = Debug|Any CPU
30 | {30ABB2A7-8028-4098-89AD-C83E08D3390D}.Release|Any CPU.ActiveCfg = Release|Any CPU
31 | {30ABB2A7-8028-4098-89AD-C83E08D3390D}.Release|Any CPU.Build.0 = Release|Any CPU
32 | {49F2296C-5C1F-4F7D-B52F-1530C52F004B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33 | {49F2296C-5C1F-4F7D-B52F-1530C52F004B}.Debug|Any CPU.Build.0 = Debug|Any CPU
34 | {49F2296C-5C1F-4F7D-B52F-1530C52F004B}.Release|Any CPU.ActiveCfg = Release|Any CPU
35 | {49F2296C-5C1F-4F7D-B52F-1530C52F004B}.Release|Any CPU.Build.0 = Release|Any CPU
36 | EndGlobalSection
37 | GlobalSection(SolutionProperties) = preSolution
38 | HideSolutionNode = FALSE
39 | EndGlobalSection
40 | GlobalSection(ExtensibilityGlobals) = postSolution
41 | SolutionGuid = {0661CE0D-AB2E-4736-A2D4-3A30E1F47295}
42 | EndGlobalSection
43 | EndGlobal
44 |
--------------------------------------------------------------------------------
/HttpBatchHandler.sln.DotSettings:
--------------------------------------------------------------------------------
1 |
2 | SUGGESTION
3 | SUGGESTION
4 | Required
5 | ExpressionBody
6 | ExpressionBody
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/NuGet.Config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HttpBatchHandler
2 | HttpBatchHandler for ASP .NET Core 3.0
3 |
4 | ## Currently working
5 | - Customization of batch handler by OnXXXEvents
6 | - HttpContent for batch requests (the old WebAPI 2.0 libraries should not be necessary anymore)
7 |
8 | ## TODO
9 | - Edge case error handling
10 | - The library should be probably split into two parts, one for the requests and one for the actual batchhandler
11 |
12 | ## Deprectaed
13 | See https://github.com/Tornhoof/HttpBatchHandler/issues/29 for thoughts about a better, less coupled approach.
14 |
--------------------------------------------------------------------------------
/azure-pipelines.yml:
--------------------------------------------------------------------------------
1 | # ASP.NET Core
2 | # Build and test ASP.NET Core projects targeting .NET Core.
3 | # Add steps that run tests, create a NuGet package, deploy, and more:
4 | # https://docs.microsoft.com/azure/devops/pipelines/languages/dotnet-core
5 |
6 | trigger:
7 | - master
8 |
9 | pool:
10 | vmImage: 'ubuntu-latest'
11 |
12 | variables:
13 | buildConfiguration: 'Release'
14 |
15 | steps:
16 | - task: DotNetCoreInstaller@1
17 | inputs:
18 | version: '3.0.100'
19 | - script: dotnet restore
20 | displayName: 'dotnet restore'
21 | - script: dotnet build --configuration $(buildConfiguration)
22 | - script: dotnet test -c Release test/HttpBatchHandler.Tests/HttpBatchHandler.Tests.csproj --logger trx --collect "Code coverage"
23 | displayName: 'dotnet test'
24 | - task: PublishTestResults@2
25 | inputs:
26 | testRunner: VSTest
27 | testResultsFiles: '**/*.trx'
28 | - task: CopyFiles@2
29 | inputs:
30 | SourceFolder: '$(Build.SourcesDirectory)'
31 | Contents: '**/$(buildConfiguration)/**/*.nupkg'
32 | TargetFolder: '$(Build.ArtifactStagingDirectory)'
33 | CleanTargetFolder: true
34 | flattenFolders: true
35 | - task: PublishBuildArtifacts@1
36 | inputs:
37 | PathtoPublish: '$(Build.ArtifactStagingDirectory)'
38 | ArtifactName: 'nuget'
39 | publishLocation: 'Container'
--------------------------------------------------------------------------------
/performance/batch.lua:
--------------------------------------------------------------------------------
1 | local open = io.open
2 | local function read_file(path)
3 | local file = open(path, "rb") -- r read mode and b binary mode
4 | if not file then return nil end
5 | local content = file:read "*a" -- *a or *all reads the whole file
6 | file:close()
7 | return content
8 | end
9 |
10 | wrk.method = "POST"
11 | wrk.body = read_file("payload.txt")
12 | wrk.headers["Content-Type"] = "multipart/mixed; boundary=\"batch_45cdcaaf-774f-40c6-8a12-dbb835d3132e\""
--------------------------------------------------------------------------------
/performance/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | ./wrk -t10 -c100 -d20 -s batch.lua http://192.168.128.221:5123/api/batch
3 |
--------------------------------------------------------------------------------
/src/HttpBatchHandler/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 |
3 | [assembly: InternalsVisibleTo("HttpBatchHandler.Tests")]
--------------------------------------------------------------------------------
/src/HttpBatchHandler/BatchMiddleware.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using HttpBatchHandler.Events;
4 | using HttpBatchHandler.Multipart;
5 | using Microsoft.AspNetCore.Authentication;
6 | using Microsoft.AspNetCore.Http;
7 | using Microsoft.AspNetCore.Http.Extensions;
8 | using Microsoft.AspNetCore.Http.Features;
9 | using Microsoft.AspNetCore.Http.Features.Authentication;
10 | using Microsoft.AspNetCore.WebUtilities;
11 | using Microsoft.Net.Http.Headers;
12 |
13 | namespace HttpBatchHandler
14 | {
15 | public class BatchMiddleware
16 | {
17 | private readonly IHttpContextFactory _factory;
18 | private readonly RequestDelegate _next;
19 | private readonly BatchMiddlewareOptions _options;
20 |
21 | public BatchMiddleware(RequestDelegate next, IHttpContextFactory factory, BatchMiddlewareOptions options)
22 | {
23 | _next = next;
24 | _factory = factory;
25 | _options = options;
26 | }
27 |
28 |
29 | public Task Invoke(HttpContext httpContext)
30 | {
31 | if (!httpContext.Request.Path.Equals(_options.Match))
32 | {
33 | return _next.Invoke(httpContext);
34 | }
35 |
36 | return InvokeBatchAsync(httpContext);
37 | }
38 |
39 | private FeatureCollection CreateDefaultFeatures(IFeatureCollection input)
40 | {
41 | var output = new FeatureCollection();
42 | output.Set(input.Get());
43 | output.Set(input.Get());
44 | output.Set(input.Get());
45 | output.Set(input.Get());
46 | output.Set(new ItemsFeature()); // per request?
47 | return output;
48 | }
49 |
50 | private async Task InvokeBatchAsync(HttpContext httpContext)
51 | {
52 | if (!httpContext.Request.IsMultiPartBatchRequest())
53 | {
54 | httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
55 | await httpContext.Response.WriteAsync("Invalid Content-Type.").ConfigureAwait(false);
56 | return;
57 | }
58 |
59 | var boundary = httpContext.Request.GetMultipartBoundary();
60 | if (string.IsNullOrEmpty(boundary))
61 | {
62 | httpContext.Response.StatusCode = StatusCodes.Status400BadRequest;
63 | await httpContext.Response.WriteAsync("Invalid boundary in Content-Type.").ConfigureAwait(false);
64 | return;
65 | }
66 |
67 | var startContext = new BatchStartContext
68 | {
69 | Request = httpContext.Request
70 | };
71 | var cancellationToken = httpContext.RequestAborted;
72 | await _options.Events.BatchStartAsync(startContext, cancellationToken).ConfigureAwait(false);
73 | Exception exception = null;
74 | var abort = false;
75 | var reader = new MultipartReader(boundary, httpContext.Request.Body);
76 | // PathString.StartsWithSegments that we use requires the base path to not end in a slash.
77 | var pathBase = httpContext.Request.PathBase;
78 | if (pathBase.HasValue && pathBase.Value.EndsWith("/"))
79 | {
80 | pathBase = new PathString(pathBase.Value.Substring(0, pathBase.Value.Length - 1));
81 | }
82 |
83 | using (var writer = new MultipartWriter("batch", Guid.NewGuid().ToString()))
84 | {
85 | try
86 | {
87 | HttpApplicationRequestSection section;
88 | while ((section = await reader
89 | .ReadNextHttpApplicationRequestSectionAsync(pathBase, httpContext.Request.IsHttps, cancellationToken)
90 | .ConfigureAwait(false)) != null)
91 | {
92 | httpContext.RequestAborted.ThrowIfCancellationRequested();
93 | var preparationContext = new BatchRequestPreparationContext
94 | {
95 | RequestFeature = section.RequestFeature,
96 | Features = CreateDefaultFeatures(httpContext.Features),
97 | State = startContext.State
98 | };
99 | await _options.Events.BatchRequestPreparationAsync(preparationContext, cancellationToken)
100 | .ConfigureAwait(false);
101 | using (var state =
102 | new RequestState(section.RequestFeature, _factory, preparationContext.Features))
103 | {
104 | using (httpContext.RequestAborted.Register(state.AbortRequest))
105 | {
106 | var executedContext = new BatchRequestExecutedContext
107 | {
108 | Request = state.Context.Request,
109 | State = startContext.State
110 | };
111 | try
112 | {
113 | var executingContext = new BatchRequestExecutingContext
114 | {
115 | Request = state.Context.Request,
116 | State = startContext.State
117 | };
118 | await _options.Events
119 | .BatchRequestExecutingAsync(executingContext, cancellationToken)
120 | .ConfigureAwait(false);
121 | await _next.Invoke(state.Context).ConfigureAwait(false);
122 | var response = await state.ResponseTaskAsync().ConfigureAwait(false);
123 | executedContext.Response = state.Context.Response;
124 | writer.Add(new HttpApplicationMultipart(response));
125 | }
126 | catch (Exception ex)
127 | {
128 | state.Abort(ex);
129 | executedContext.Exception = ex;
130 | }
131 | finally
132 | {
133 | await _options.Events.BatchRequestExecutedAsync(executedContext, cancellationToken)
134 | .ConfigureAwait(false);
135 | abort = executedContext.Abort;
136 | }
137 |
138 | if (abort)
139 | {
140 | break;
141 | }
142 | }
143 | }
144 | }
145 | }
146 | catch (Exception ex)
147 | {
148 | exception = ex;
149 | }
150 | finally
151 | {
152 | var endContext = new BatchEndContext
153 | {
154 | Exception = exception,
155 | State = startContext.State,
156 | IsAborted = abort,
157 | Response = httpContext.Response
158 | };
159 | if (endContext.Exception != null)
160 | {
161 | endContext.Response.StatusCode = StatusCodes.Status500InternalServerError;
162 | }
163 |
164 | await _options.Events.BatchEndAsync(endContext, cancellationToken).ConfigureAwait(false);
165 | if (!endContext.IsHandled)
166 | {
167 | httpContext.Response.Headers.Add(HeaderNames.ContentType, writer.ContentType);
168 | await writer.CopyToAsync(httpContext.Response.Body, cancellationToken).ConfigureAwait(false);
169 | }
170 | }
171 | }
172 | }
173 | }
174 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/BatchMiddlewareExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using HttpBatchHandler.Events;
3 | using Microsoft.AspNetCore.Builder;
4 | using Microsoft.AspNetCore.Http;
5 | using Microsoft.Extensions.DependencyInjection;
6 |
7 | namespace HttpBatchHandler
8 | {
9 | public static class BatchMiddlewareExtensions
10 | {
11 | public static IApplicationBuilder UseBatchMiddleware(this IApplicationBuilder builder) => builder.UseBatchMiddleware(null);
12 |
13 | public static IApplicationBuilder UseBatchMiddleware(this IApplicationBuilder builder,
14 | Action configurationAction)
15 | {
16 | var factory = builder.ApplicationServices.GetRequiredService();
17 | var options = new BatchMiddlewareOptions();
18 | configurationAction?.Invoke(options);
19 | return builder.UseMiddleware(factory, options);
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/Events/BatchEndContext.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.AspNetCore.Http;
3 |
4 | namespace HttpBatchHandler.Events
5 | {
6 | public class BatchEndContext
7 | {
8 | ///
9 | /// Possible exception
10 | ///
11 | public Exception Exception { get; set; }
12 |
13 | ///
14 | /// If not all requests were executed
15 | ///
16 | public bool IsAborted { get; set; }
17 |
18 | ///
19 | /// If true, then you need to populate the response yourself
20 | ///
21 | public bool IsHandled { get; set; } = false;
22 |
23 | ///
24 | /// The outgoing multipart response
25 | ///
26 | public HttpResponse Response { get; set; }
27 |
28 | ///
29 | /// State
30 | ///
31 | public object State { get; set; }
32 | }
33 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/Events/BatchMiddlewareEvents.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 |
5 | namespace HttpBatchHandler.Events
6 | {
7 | public class BatchMiddlewareEvents
8 | {
9 | ///
10 | /// After all batch request are executed
11 | ///
12 | public Func OnBatchEndAsync { get; set; } =
13 | (context, token) => Task.CompletedTask;
14 |
15 | ///
16 | /// Before an individual request is created
17 | ///
18 | public Func OnBatchPreparationAsync { get; set; } =
19 | (context, token) => Task.CompletedTask;
20 |
21 | ///
22 | /// After an individual request in a batch is executed
23 | ///
24 | public Func OnBatchRequestExecutedAsync { get; set; } =
25 | (context, token) => Task.CompletedTask;
26 |
27 | ///
28 | /// Before an individual request in a batch is executed
29 | ///
30 | public Func OnBatchRequestExecutingAsync { get; set; } =
31 | (context, token) => Task.CompletedTask;
32 |
33 | ///
34 | /// Before any request in a batch are executed
35 | ///
36 | public Func OnBatchStartAsync { get; set; } =
37 | (context, token) => Task.CompletedTask;
38 |
39 | ///
40 | /// After all batch request are executed
41 | ///
42 | public virtual Task BatchEndAsync(BatchEndContext context, CancellationToken cancellationToken = default) => OnBatchEndAsync(context, cancellationToken);
43 |
44 | ///
45 | /// After an individual request in a batch is executed
46 | ///
47 | public virtual Task BatchRequestExecutedAsync(BatchRequestExecutedContext context,
48 | CancellationToken cancellationToken = default) => OnBatchRequestExecutedAsync(context, cancellationToken);
49 |
50 | ///
51 | /// Before an individual request in a batch is executed
52 | ///
53 | public virtual Task BatchRequestExecutingAsync(BatchRequestExecutingContext context,
54 | CancellationToken cancellationToken = default) => OnBatchRequestExecutingAsync(context, cancellationToken);
55 |
56 | ///
57 | /// Before an individual request in a batch is executed
58 | ///
59 | public virtual Task BatchRequestPreparationAsync(BatchRequestPreparationContext context,
60 | CancellationToken cancellationToken = default) => OnBatchPreparationAsync(context, cancellationToken);
61 |
62 | ///
63 | /// Before any request in a batch are executed
64 | ///
65 | public virtual Task BatchStartAsync(BatchStartContext context, CancellationToken cancellationToken = default) => OnBatchStartAsync(context, cancellationToken);
66 | }
67 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/Events/BatchMiddlewareOptions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 |
3 | namespace HttpBatchHandler.Events
4 | {
5 | public class BatchMiddlewareOptions
6 | {
7 | ///
8 | /// Events
9 | ///
10 | public BatchMiddlewareEvents Events { get; set; } = new BatchMiddlewareEvents();
11 | ///
12 | /// Endpoint
13 | ///
14 | public PathString Match { get; set; } = "/api/batch";
15 | }
16 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/Events/BatchRequestExecutedContext.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.AspNetCore.Http;
3 |
4 | namespace HttpBatchHandler.Events
5 | {
6 | public class BatchRequestExecutedContext
7 | {
8 | ///
9 | /// Abort after this request?
10 | ///
11 | public bool Abort { get; set; }
12 |
13 | ///
14 | /// Exception
15 | ///
16 | public Exception Exception { get; set; }
17 |
18 | ///
19 | /// The individual HttpRequest
20 | ///
21 | public HttpRequest Request { get; set; }
22 |
23 | ///
24 | /// The individual HttpResponse
25 | ///
26 | public HttpResponse Response { get; set; }
27 |
28 | ///
29 | /// State
30 | ///
31 | public object State { get; set; }
32 | }
33 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/Events/BatchRequestExecutingContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 |
3 | namespace HttpBatchHandler.Events
4 | {
5 | public class BatchRequestExecutingContext
6 | {
7 | ///
8 | /// The individual request
9 | ///
10 | public HttpRequest Request { get; set; }
11 |
12 | ///
13 | /// State
14 | ///
15 | public object State { get; set; }
16 | }
17 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/Events/BatchRequestPreparationContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http.Features;
2 |
3 | namespace HttpBatchHandler.Events
4 | {
5 | public class BatchRequestPreparationContext
6 | {
7 | ///
8 | /// Features which should be in the httpContext
9 | ///
10 | public IFeatureCollection Features { get; set; }
11 |
12 | ///
13 | /// The individual request, prior to context creation
14 | ///
15 | public IHttpRequestFeature RequestFeature { get; set; }
16 |
17 | ///
18 | /// State
19 | ///
20 | public object State { get; set; }
21 | }
22 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/Events/BatchStartContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 |
3 | namespace HttpBatchHandler.Events
4 | {
5 | public class BatchStartContext
6 | {
7 | ///
8 | /// The incoming multipart request
9 | ///
10 | public HttpRequest Request { get; set; }
11 |
12 | ///
13 | /// State
14 | ///
15 | public object State { get; set; }
16 | }
17 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/HttpBatchHandler.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.0
5 | 0.8.1
6 | 0.8.1.0
7 | 0.8.1.0
8 | true
9 | HttpBatchHandler for ASP.NET Core 3.0 Contributors 2019
10 | HttpBatchHandler
11 | HttpBatchHandler for ASP.NET Core 3.0 Contributors
12 |
13 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
14 | HttpBatchHandler for ASP.NET Core 3.0+
15 | HttpBatchHandler for ASP.NET Core 3.0+
16 | Apache-2.0
17 | https://github.com/Tornhoof/HttpBatchHandler/
18 | https://github.com/Tornhoof/HttpBatchHandler
19 | git
20 | HttpBatchHandler
21 | An ASP.NET WebAPI 2 compatible BatchRequestHandler as described by:
22 | https://blogs.msdn.microsoft.com/webdev/2013/11/01/introducing-batch-support-in-web-api-and-web-api-odata
23 | Fix bad scheme propagation
24 | latest
25 | bin\$(Configuration)\netstandard2.0\HttpBatchHandler.xml
26 | 1701;1702;1705;1591
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/HttpBatchHandler/HttpRequestExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.AspNetCore.Http;
3 |
4 | namespace HttpBatchHandler
5 | {
6 | internal static class HttpRequestExtensions
7 | {
8 | public static bool IsMultiPartBatchRequest(this HttpRequest request) => request.ContentType?.StartsWith("multipart/", StringComparison.OrdinalIgnoreCase) ?? false;
9 | }
10 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/HttpResponseExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http;
2 |
3 | namespace HttpBatchHandler
4 | {
5 | internal static class HttpResponseExtensions
6 | {
7 | public static bool IsSuccessStatusCode(this HttpResponse response) => response.StatusCode >= 200 && response.StatusCode <= 299;
8 | }
9 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/Multipart/HttpApplicationContent.cs:
--------------------------------------------------------------------------------
1 | using System.IO;
2 | using System.Net;
3 | using System.Net.Http;
4 | using System.Net.Http.Headers;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 | using Microsoft.AspNetCore.WebUtilities;
8 |
9 | namespace HttpBatchHandler.Multipart
10 | {
11 | public class HttpApplicationContent : HttpContent
12 | {
13 | private static readonly char[] Crlf = "\r\n".ToCharArray();
14 | private readonly HttpRequestMessage _message;
15 |
16 | public HttpApplicationContent(HttpRequestMessage message)
17 | {
18 | _message = message;
19 | Headers.ContentType = new MediaTypeHeaderValue("application/http");
20 | Headers.ContentType.Parameters.Add(new NameValueHeaderValue("msgtype","request"));
21 | }
22 |
23 | protected override void Dispose(bool disposing)
24 | {
25 | if (disposing)
26 | {
27 | _message.Dispose();
28 | }
29 |
30 | base.Dispose(disposing);
31 | }
32 |
33 | protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context)
34 | {
35 | using (var sb = new HttpResponseStreamWriter(stream, Encoding.ASCII))
36 | {
37 | await sb.WriteAsync(_message.Method.Method).ConfigureAwait(false);
38 | await sb.WriteAsync(' ').ConfigureAwait(false);
39 | // ReSharper disable once ImpureMethodCallOnReadonlyValueField
40 | await sb.WriteAsync(_message.RequestUri.PathAndQuery).ConfigureAwait(false);
41 | await sb.WriteAsync(' ').ConfigureAwait(false);
42 | await sb.WriteAsync($"HTTP/{_message.Version}").ConfigureAwait(false);
43 | await sb.WriteAsync(Crlf).ConfigureAwait(false);
44 | await sb.WriteAsync($"Host: {_message.RequestUri.Authority}").ConfigureAwait(false);
45 | await sb.WriteAsync(Crlf).ConfigureAwait(false);
46 | foreach (var header in _message.Headers)
47 | {
48 | await sb.WriteAsync(header.Key).ConfigureAwait(false);
49 | await sb.WriteAsync(": ").ConfigureAwait(false);
50 | await sb.WriteAsync(string.Join(", ", header.Value)).ConfigureAwait(false);
51 | await sb.WriteAsync(Crlf).ConfigureAwait(false);
52 | }
53 |
54 | if (_message.Content?.Headers != null)
55 | {
56 | foreach (var header in _message.Content?.Headers)
57 | {
58 | await sb.WriteAsync(header.Key).ConfigureAwait(false);
59 | await sb.WriteAsync(": ").ConfigureAwait(false);
60 | await sb.WriteAsync(string.Join(", ", header.Value)).ConfigureAwait(false);
61 | await sb.WriteAsync(Crlf).ConfigureAwait(false);
62 | }
63 | }
64 |
65 | await sb.WriteAsync(Crlf).ConfigureAwait(false);
66 | }
67 |
68 | if (_message.Content != null)
69 | {
70 | using (var contentStream = await _message.Content.ReadAsStreamAsync().ConfigureAwait(false))
71 | {
72 | await contentStream.CopyToAsync(stream).ConfigureAwait(false);
73 | }
74 | }
75 | }
76 |
77 | protected override bool TryComputeLength(out long length)
78 | {
79 | length = 0;
80 | return false;
81 | }
82 | }
83 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/Multipart/HttpApplicationMultipart.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Text;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using Microsoft.AspNetCore.Http;
7 | using Microsoft.AspNetCore.WebUtilities;
8 |
9 | namespace HttpBatchHandler.Multipart
10 | {
11 | public class HttpApplicationMultipart : IMultipart
12 | {
13 | private static readonly char[] Crlf = "\r\n".ToCharArray();
14 | private readonly Stream _content;
15 | private readonly string _httpVersion;
16 | private readonly string _reasonPhrase;
17 |
18 | internal HttpApplicationMultipart(ResponseFeature responseFeature) : this(responseFeature.Protocol,
19 | responseFeature.StatusCode, responseFeature.ReasonPhrase, responseFeature.Stream, responseFeature.Headers)
20 | {
21 | }
22 |
23 | public HttpApplicationMultipart(string httpVersion, int statusCode, string reasonPhrase, Stream content,
24 | IHeaderDictionary headers)
25 | {
26 | _httpVersion = httpVersion;
27 | StatusCode = statusCode;
28 | _reasonPhrase = reasonPhrase;
29 | if (string.IsNullOrEmpty(_reasonPhrase))
30 | {
31 | _reasonPhrase = ReasonPhrases.GetReasonPhrase(statusCode);
32 | }
33 |
34 | _content = content;
35 | Headers = headers;
36 | }
37 |
38 | public IHeaderDictionary Headers { get; }
39 | public int StatusCode { get; }
40 |
41 | public void Dispose()
42 | {
43 | Dispose(true);
44 | GC.SuppressFinalize(this);
45 | }
46 |
47 | public async Task CopyToAsync(Stream stream, CancellationToken cancellationToken = default)
48 | {
49 | using (var sb = new HttpResponseStreamWriter(stream, Encoding.ASCII))
50 | {
51 | await sb.WriteAsync("Content-Type: application/http; msgtype=response").ConfigureAwait(false);
52 | await sb.WriteAsync(Crlf).ConfigureAwait(false);
53 | await sb.WriteAsync(Crlf).ConfigureAwait(false);
54 | await sb.WriteAsync(_httpVersion).ConfigureAwait(false);
55 | await sb.WriteAsync(' ').ConfigureAwait(false);
56 | // ReSharper disable once ImpureMethodCallOnReadonlyValueField
57 | await sb.WriteAsync(StatusCode.ToString()).ConfigureAwait(false);
58 | await sb.WriteAsync(' ').ConfigureAwait(false);
59 | await sb.WriteAsync(_reasonPhrase).ConfigureAwait(false);
60 | await sb.WriteAsync(Crlf).ConfigureAwait(false);
61 | foreach (var header in Headers)
62 | {
63 | await sb.WriteAsync(header.Key).ConfigureAwait(false);
64 | await sb.WriteAsync(": ").ConfigureAwait(false);
65 | await sb.WriteAsync(header.Value).ConfigureAwait(false);
66 | await sb.WriteAsync(Crlf).ConfigureAwait(false);
67 | }
68 |
69 | await sb.WriteAsync(Crlf).ConfigureAwait(false);
70 | await sb.FlushAsync().ConfigureAwait(false);
71 | }
72 |
73 | if (_content != null)
74 | {
75 | await _content.CopyToAsync(stream, cancellationToken).ConfigureAwait(false);
76 | }
77 | }
78 |
79 | protected virtual void Dispose(bool disposing)
80 | {
81 | if (disposing)
82 | {
83 | _content.Dispose();
84 | }
85 | }
86 |
87 | ~HttpApplicationMultipart()
88 | {
89 | Dispose(false);
90 | }
91 | }
92 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/Multipart/HttpApplicationRequestSection.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http.Features;
2 |
3 | namespace HttpBatchHandler.Multipart
4 | {
5 | public class HttpApplicationRequestSection
6 | {
7 | public IHttpRequestFeature RequestFeature { get; set; }
8 | }
9 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/Multipart/HttpApplicationRequestSectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using Microsoft.AspNetCore.Http;
7 | using Microsoft.AspNetCore.Http.Features;
8 | using Microsoft.AspNetCore.WebUtilities;
9 | using Microsoft.Extensions.Primitives;
10 | using Microsoft.Net.Http.Headers;
11 |
12 | namespace HttpBatchHandler.Multipart
13 | {
14 | public static class HttpApplicationRequestSectionExtensions
15 | {
16 | private static readonly char[] SpaceArray = {' '};
17 |
18 | public static async Task ReadNextHttpApplicationRequestSectionAsync(
19 | this MultipartReader reader, PathString pathBase = default, bool isHttps = false, CancellationToken cancellationToken = default)
20 | {
21 | var section = await reader.ReadNextSectionAsync(cancellationToken).ConfigureAwait(false);
22 | if (section == null)
23 | {
24 | return null; // if null we're done
25 | }
26 |
27 | var contentTypeHeader = MediaTypeHeaderValue.Parse(section.ContentType);
28 | if (!contentTypeHeader.MediaType.HasValue ||
29 | !contentTypeHeader.MediaType.Equals("application/http", StringComparison.OrdinalIgnoreCase))
30 | {
31 | throw new InvalidDataException("Invalid Content-Type.");
32 | }
33 |
34 | var param = contentTypeHeader.Parameters.SingleOrDefault(a =>
35 | a.Name.HasValue && a.Value.HasValue && a.Name.Equals("msgtype", StringComparison.OrdinalIgnoreCase) &&
36 | a.Value.Equals("request", StringComparison.OrdinalIgnoreCase));
37 | if (param == null)
38 | {
39 | throw new InvalidDataException("Invalid Content-Type.");
40 | }
41 |
42 | var bufferedStream = new BufferedReadStream(section.Body, SectionHelper.DefaultBufferSize);
43 | var requestLineParts = await ReadRequestLineAsync(bufferedStream, cancellationToken).ConfigureAwait(false);
44 | if (requestLineParts.Length != 3)
45 | {
46 | throw new InvalidDataException("Invalid request line.");
47 | }
48 |
49 | // Validation of the request line parts necessary?
50 | var headers = await SectionHelper.ReadHeadersAsync(bufferedStream, cancellationToken).ConfigureAwait(false);
51 | if (!headers.TryGetValue(HeaderNames.Host, out var hostHeader))
52 | {
53 | throw new InvalidDataException("No Host Header");
54 | }
55 |
56 | var uri = BuildUri(isHttps, hostHeader, requestLineParts[1]);
57 | var fullPath = PathString.FromUriComponent(uri);
58 | var feature = new HttpRequestFeature
59 | {
60 | Body = bufferedStream,
61 | Headers = new HeaderDictionary(headers),
62 | Method = requestLineParts[0],
63 | Protocol = requestLineParts[2],
64 | Scheme = uri.Scheme,
65 | QueryString = uri.Query
66 | };
67 | if (fullPath.StartsWithSegments(pathBase, out var remainder))
68 | {
69 | feature.PathBase = pathBase.Value;
70 | feature.Path = remainder.Value;
71 | }
72 | else
73 | {
74 | feature.PathBase = string.Empty;
75 | feature.Path = fullPath.Value;
76 | }
77 |
78 | return new HttpApplicationRequestSection
79 | {
80 | RequestFeature = feature
81 | };
82 | }
83 |
84 | private static Uri BuildUri(bool isHttps, StringValues hostHeader, string pathAndQuery)
85 | {
86 | if (hostHeader.Count != 1)
87 | {
88 | throw new InvalidOperationException("Invalid Host Header");
89 | }
90 |
91 | var hostString = new HostString(hostHeader.Single());
92 | if (!hostString.HasValue)
93 | {
94 | return null;
95 | }
96 |
97 | var scheme = isHttps ? Uri.UriSchemeHttps : Uri.UriSchemeHttp;
98 | var fullUri = $"{scheme}://{hostString.ToUriComponent()}{pathAndQuery}";
99 | var uri = new Uri(fullUri);
100 | return uri;
101 | }
102 |
103 |
104 | private static async Task ReadRequestLineAsync(BufferedReadStream stream,
105 | CancellationToken cancellationToken)
106 | {
107 | var line = await stream.ReadLineAsync(MultipartReader.DefaultHeadersLengthLimit, cancellationToken)
108 | .ConfigureAwait(false);
109 | return line.Split(SpaceArray);
110 | }
111 | }
112 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/Multipart/HttpApplicationResponseSection.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Http.Features;
2 |
3 | namespace HttpBatchHandler.Multipart
4 | {
5 | public class HttpApplicationResponseSection
6 | {
7 | public IHttpResponseFeature ResponseFeature { get; set; }
8 | }
9 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/Multipart/HttpApplicationResponseSectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using Microsoft.AspNetCore.Http;
7 | using Microsoft.AspNetCore.WebUtilities;
8 | using Microsoft.Net.Http.Headers;
9 |
10 | namespace HttpBatchHandler.Multipart
11 | {
12 | public static class HttpApplicationResponseSectionExtensions
13 | {
14 | private static readonly char[] SpaceArray = {' '};
15 |
16 | public static async Task ReadNextHttpApplicationResponseSectionAsync(
17 | this MultipartReader reader, CancellationToken cancellationToken = default)
18 | {
19 | var section = await reader.ReadNextSectionAsync(cancellationToken).ConfigureAwait(false);
20 | if (section == null)
21 | {
22 | return null; // if null we're done
23 | }
24 |
25 | var contentTypeHeader = MediaTypeHeaderValue.Parse(section.ContentType);
26 | if (!contentTypeHeader.MediaType.HasValue ||
27 | !contentTypeHeader.MediaType.Equals("application/http", StringComparison.OrdinalIgnoreCase))
28 | {
29 | throw new InvalidDataException("Invalid Content-Type.");
30 | }
31 |
32 | var param = contentTypeHeader.Parameters.SingleOrDefault(a =>
33 | a.Name.HasValue && a.Value.HasValue &&
34 | a.Name.Value.Equals("msgtype", StringComparison.OrdinalIgnoreCase) &&
35 | a.Value.Equals("response", StringComparison.OrdinalIgnoreCase));
36 | if (param == null)
37 | {
38 | throw new InvalidDataException("Invalid Content-Type.");
39 | }
40 |
41 | var bufferedStream = new BufferedReadStream(section.Body, SectionHelper.DefaultBufferSize);
42 | var responseLine = await ReadResponseLineAsync(bufferedStream, cancellationToken).ConfigureAwait(false);
43 | if (responseLine.Length != 3)
44 | {
45 | throw new InvalidDataException("Invalid request line.");
46 | }
47 |
48 | var headers = await SectionHelper.ReadHeadersAsync(bufferedStream, cancellationToken).ConfigureAwait(false);
49 | return new HttpApplicationResponseSection
50 | {
51 | ResponseFeature = new ResponseFeature(responseLine[0], int.Parse(responseLine[1]), responseLine[2], bufferedStream, new HeaderDictionary(headers))
52 | };
53 | }
54 |
55 |
56 | private static async Task ReadResponseLineAsync(BufferedReadStream stream,
57 | CancellationToken cancellationToken)
58 | {
59 | var line = await stream.ReadLineAsync(MultipartReader.DefaultHeadersLengthLimit, cancellationToken)
60 | .ConfigureAwait(false);
61 | return line.Split(SpaceArray, 3);
62 | }
63 | }
64 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/Multipart/HttpContentExtensions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Linq;
3 | using System.Net.Http;
4 | using System.Net.Http.Headers;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using Microsoft.AspNetCore.WebUtilities;
8 |
9 | namespace HttpBatchHandler.Multipart
10 | {
11 | public static class HttpContentExtensions
12 | {
13 | public static async Task ReadAsMultipartAsync(this HttpContent content,
14 | CancellationToken cancellationToken = default)
15 | {
16 | if (!content.Headers.IsMultipart())
17 | {
18 | return null;
19 | }
20 |
21 | var boundary = content.Headers.GetMultipartBoundary();
22 | if (string.IsNullOrEmpty(boundary))
23 | {
24 | return null;
25 | }
26 |
27 | var stream = await content.ReadAsStreamAsync().ConfigureAwait(false);
28 | var reader = new MultipartReader(boundary, stream);
29 | return reader;
30 | }
31 |
32 | private static string GetMultipartBoundary(this HttpContentHeaders headers)
33 | {
34 | if (headers == null)
35 | {
36 | throw new ArgumentNullException(nameof(headers));
37 | }
38 |
39 | if (headers.IsMultipart())
40 | {
41 | var boundaryParam = headers.ContentType.Parameters.FirstOrDefault(a =>
42 | string.Equals(a.Name, "boundary", StringComparison.OrdinalIgnoreCase));
43 | return boundaryParam?.Value.Trim('"');
44 | }
45 |
46 | return null;
47 | }
48 |
49 | private static bool IsMultipart(this HttpContentHeaders headers)
50 | {
51 | if (headers == null)
52 | {
53 | throw new ArgumentNullException(nameof(headers));
54 | }
55 |
56 | return headers.ContentType?.MediaType?.StartsWith("multipart/", StringComparison.OrdinalIgnoreCase) ??
57 | false;
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/Multipart/IMultipart.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | namespace HttpBatchHandler.Multipart
7 | {
8 | public interface IMultipart : IDisposable
9 | {
10 | Task CopyToAsync(Stream stream, CancellationToken cancellationToken = default);
11 | }
12 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/Multipart/MultipartWriter.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Text;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 |
8 | namespace HttpBatchHandler.Multipart
9 | {
10 | public class MultipartWriter : IDisposable
11 | {
12 | private readonly byte[] _endBoundary;
13 | private readonly Queue _parts = new Queue();
14 | private readonly byte[] _startBoundary;
15 | private readonly byte[] _crlf;
16 |
17 | public MultipartWriter(string subType, string boundary)
18 | {
19 | _startBoundary = Encoding.ASCII.GetBytes($"--{boundary}\r\n");
20 | _endBoundary = Encoding.ASCII.GetBytes($"--{boundary}--\r\n");
21 | _crlf = Encoding.ASCII.GetBytes("\r\n");
22 |
23 | ContentType = $"multipart/{subType}; boundary=\"{boundary}\"";
24 | }
25 |
26 | public string ContentType { get; }
27 |
28 | public void Dispose()
29 | {
30 | Dispose(true);
31 | GC.SuppressFinalize(this);
32 | }
33 |
34 | public void Add(IMultipart multipart)
35 | {
36 | _parts.Enqueue(multipart);
37 | }
38 |
39 | public async Task CopyToAsync(Stream stream, CancellationToken cancellationToken = default)
40 | {
41 | while (_parts.Count > 0)
42 | {
43 | using (var part = _parts.Dequeue())
44 | {
45 | await stream.WriteAsync(_startBoundary, 0, _startBoundary.Length, cancellationToken)
46 | .ConfigureAwait(false);
47 | await part.CopyToAsync(stream, cancellationToken).ConfigureAwait(false);
48 | await stream.WriteAsync(_crlf, 0, _crlf.Length, cancellationToken).ConfigureAwait(false);
49 | }
50 | }
51 |
52 | await stream.WriteAsync(_endBoundary, 0, _endBoundary.Length, cancellationToken).ConfigureAwait(false);
53 | }
54 |
55 | protected virtual void Dispose(bool disposing)
56 | {
57 | if (disposing)
58 | {
59 | while (_parts.Count > 0)
60 | {
61 | var part = _parts.Dequeue();
62 | part.Dispose();
63 | }
64 | }
65 | }
66 |
67 | ~MultipartWriter()
68 | {
69 | Dispose(false);
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/Multipart/SectionHelper.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.IO;
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using Microsoft.AspNetCore.WebUtilities;
6 | using Microsoft.Extensions.Primitives;
7 |
8 | namespace HttpBatchHandler.Multipart
9 | {
10 | internal static class SectionHelper
11 | {
12 | public const int DefaultBufferSize = 1024 * 4;
13 |
14 | public static async Task> ReadHeadersAsync(BufferedReadStream stream,
15 | CancellationToken cancellationToken = default)
16 | {
17 | var totalSize = 0;
18 | var accumulator = new KeyValueAccumulator();
19 | var line = await stream.ReadLineAsync(MultipartReader.DefaultHeadersLengthLimit - totalSize,
20 | cancellationToken).ConfigureAwait(false);
21 | while (!string.IsNullOrEmpty(line))
22 | {
23 | if (MultipartReader.DefaultHeadersLengthLimit - totalSize < line.Length)
24 | {
25 | throw new InvalidDataException(
26 | $"Multipart headers length limit {MultipartReader.DefaultHeadersLengthLimit} exceeded.");
27 | }
28 |
29 | totalSize += line.Length;
30 | var splitIndex = line.IndexOf(':');
31 | if (splitIndex <= 0)
32 | {
33 | throw new InvalidDataException($"Invalid header line: {line}");
34 | }
35 |
36 | var name = line.Substring(0, splitIndex);
37 | var value = line.Substring(splitIndex + 1, line.Length - splitIndex - 1).Trim();
38 | accumulator.Append(name, value);
39 | if (accumulator.KeyCount > MultipartReader.DefaultHeadersCountLimit)
40 | {
41 | throw new InvalidDataException(
42 | $"Multipart headers count limit {MultipartReader.DefaultHeadersCountLimit} exceeded.");
43 | }
44 |
45 | line = await stream.ReadLineAsync(MultipartReader.DefaultHeadersLengthLimit - totalSize,
46 | cancellationToken).ConfigureAwait(false);
47 | }
48 |
49 | return accumulator.GetResults();
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/RequestState.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using Microsoft.AspNetCore.Http;
5 | using Microsoft.AspNetCore.Http.Features;
6 |
7 | namespace HttpBatchHandler
8 | {
9 | internal class RequestState : IDisposable
10 | {
11 | private readonly IHttpContextFactory _factory;
12 | private readonly CancellationTokenSource _requestAbortedSource;
13 | private readonly ResponseFeature _responseFeature;
14 | private readonly WriteOnlyResponseStream _responseStream;
15 | private bool _pipelineFinished;
16 |
17 | internal RequestState(IHttpRequestFeature requestFeature, IHttpContextFactory factory,
18 | IFeatureCollection featureCollection)
19 | {
20 | _factory = factory;
21 | _requestAbortedSource = new CancellationTokenSource();
22 | _pipelineFinished = false;
23 |
24 | var contextFeatures = new FeatureCollection(featureCollection);
25 | contextFeatures.Set(requestFeature);
26 |
27 | _responseStream = new WriteOnlyResponseStream(AbortRequest);
28 | _responseFeature = new ResponseFeature(requestFeature.Protocol, 200, null, _responseStream, new HeaderDictionary()) {Abort = Abort};
29 | contextFeatures.Set(_responseFeature);
30 | contextFeatures.Set(new StreamResponseBodyFeature(_responseStream));
31 | var requestLifetimeFeature = new HttpRequestLifetimeFeature();
32 | contextFeatures.Set(requestLifetimeFeature);
33 | requestLifetimeFeature.RequestAborted = _requestAbortedSource.Token;
34 |
35 | Context = _factory.Create(contextFeatures);
36 | }
37 |
38 | public HttpContext Context { get; }
39 |
40 | public void Dispose()
41 | {
42 | Dispose(true);
43 | GC.SuppressFinalize(this);
44 | }
45 |
46 | protected virtual void Dispose(bool disposing)
47 | {
48 | if (disposing)
49 | {
50 | _factory.Dispose(Context);
51 | }
52 | }
53 |
54 | internal void Abort(Exception exception)
55 | {
56 | _pipelineFinished = true;
57 | _responseStream.Abort(exception);
58 | }
59 |
60 | internal void AbortRequest()
61 | {
62 | if (!_pipelineFinished)
63 | {
64 | _requestAbortedSource.Cancel();
65 | }
66 | }
67 |
68 | ///
69 | /// FireOnSendingHeadersAsync is a bit late here, the remaining middlewares are already fully processed, the testhost
70 | /// does it on the first body stream write, which is more logical
71 | /// but I'm not certain about the added complexity
72 | ///
73 | internal async Task ResponseTaskAsync()
74 | {
75 | _pipelineFinished = true;
76 | await _responseFeature.FireOnSendingHeadersAsync().ConfigureAwait(false);
77 | await _responseFeature.FireOnResponseCompletedAsync().ConfigureAwait(false);
78 | _responseStream.Complete();
79 | return _responseFeature;
80 | }
81 |
82 | ~RequestState()
83 | {
84 | Dispose(false);
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/src/HttpBatchHandler/ResponseFeature.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.IO.Pipelines;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using Microsoft.AspNetCore.Http;
7 | using Microsoft.AspNetCore.Http.Features;
8 |
9 | namespace HttpBatchHandler
10 | {
11 | internal class ResponseFeature : IHttpResponseFeature
12 | {
13 | private Func _responseCompletedAsync = () => Task.FromResult(true);
14 | private Func _responseStartingAsync = () => Task.FromResult(true);
15 |
16 | internal ResponseFeature(string protocol, int statusCode, string reasonPhrase, Stream content,
17 | IHeaderDictionary headers)
18 | {
19 | Protocol = protocol;
20 | StatusCode = statusCode;
21 | ReasonPhrase = reasonPhrase;
22 | Body = content;
23 | Headers = headers;
24 | }
25 |
26 | public Stream Stream => Body;
27 |
28 | public Action Abort { get; set; }
29 |
30 | public Stream Body { get; set; }
31 |
32 | public bool HasStarted { get; private set; }
33 |
34 | public IHeaderDictionary Headers { get; set; }
35 |
36 | public string Protocol { get; set; }
37 |
38 | public string ReasonPhrase { get; set; }
39 |
40 | public int StatusCode { get; set; }
41 |
42 | public void OnCompleted(Func