├── .gitattributes
├── .github
├── renovate.json
└── workflows
│ ├── ci-build.yml
│ ├── lock.yml
│ └── release.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Images
└── main.png
├── LICENSE
├── README.md
├── src
├── Directory.build.props
├── Directory.build.targets
├── Fusillade.Tests
│ ├── API
│ │ ├── ApiApprovalTests.FusilladeTests.DotNet6_0.verified.txt
│ │ ├── ApiApprovalTests.FusilladeTests.DotNet7_0.verified.txt
│ │ ├── ApiApprovalTests.FusilladeTests.DotNet8_0.verified.txt
│ │ ├── ApiApprovalTests.FusilladeTests.Net4_7.verified.txt
│ │ ├── ApiApprovalTests.cs
│ │ └── ApiExtensions.cs
│ ├── Fusillade.Tests.csproj
│ ├── Http
│ │ ├── BaseHttpSchedulerSharedTests.cs
│ │ ├── HttpSchedulerCachingTests.cs
│ │ ├── HttpSchedulerSharedTests.cs
│ │ ├── TestHttpMessageHandler.cs
│ │ └── fixtures
│ │ │ └── ResponseWithETag
│ ├── IntegrationTestHelper.cs
│ └── NetCacheTests.cs
├── Fusillade.sln
├── Fusillade
│ ├── ConcatenateMixin.cs
│ ├── Fusillade.csproj
│ ├── IRequestCache.cs
│ ├── InflightRequest.cs
│ ├── LimitingHttpMessageHandler.cs
│ ├── NetCache.cs
│ ├── OfflineHttpMessageHandler.cs
│ ├── Priority.cs
│ └── RateLimitedHttpMessageHandler.cs
├── analyzers.ruleset
└── stylecop.json
└── version.json
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Catch all for anything we forgot. Add rules if you get CRLF to LF warnings.
2 | * text=auto
3 |
4 | # Text files that should be normalized to LF in odb.
5 | *.cs text eol=lf diff=csharp
6 | *.xaml text
7 | *.config text
8 | *.c text
9 | *.h text
10 | *.cpp text
11 | *.hpp text
12 |
13 | *.sln text
14 | *.csproj text
15 | *.vcxproj text
16 |
17 | *.md text
18 | *.tt text
19 | *.sh text
20 | *.ps1 text
21 | *.cmd text
22 | *.bat text
23 | *.markdown text
24 | *.msbuild text
25 |
26 |
27 | # Binary files that should not be normalized or diffed
28 | *.png binary
29 | *.jpg binary
30 | *.gif binary
31 | *.ico binary
32 | *.rc binary
33 |
34 | *.pfx binary
35 | *.snk binary
36 | *.dll binary
37 | *.exe binary
38 | *.lib binary
39 | *.exp binary
40 | *.pdb binary
41 | *.sdf binary
42 | *.7z binary
43 |
44 | # Generated file should just use CRLF, it's fiiine
45 | SolutionInfo.cs text eol=crlf diff=csharp
46 |
--------------------------------------------------------------------------------
/.github/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["local>reactiveui/.github:renovate"]
4 | }
--------------------------------------------------------------------------------
/.github/workflows/ci-build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | env:
10 | productNamespacePrefix: "Fusillade"
11 |
12 | jobs:
13 | build:
14 | uses: reactiveui/actions-common/.github/workflows/workflow-common-setup-and-build.yml@main
15 | with:
16 | configuration: Release
17 | productNamespacePrefix: "Fusilade"
18 | installWorkflows: false
19 |
--------------------------------------------------------------------------------
/.github/workflows/lock.yml:
--------------------------------------------------------------------------------
1 | name: 'Lock Threads'
2 |
3 | on:
4 | schedule:
5 | - cron: '0 0 * * *'
6 | workflow_dispatch:
7 |
8 | permissions:
9 | issues: write
10 | pull-requests: write
11 |
12 | concurrency:
13 | group: lock
14 |
15 | jobs:
16 | action:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: dessant/lock-threads@v5
20 | with:
21 | github-token: ${{ github.token }}
22 | issue-inactive-days: '14'
23 | pr-inactive-days: '14'
24 | issue-comment: >
25 | This issue has been automatically locked since there
26 | has not been any recent activity after it was closed.
27 | Please open a new issue for related bugs.
28 | pr-comment: >
29 | This pull request has been automatically locked since there
30 | has not been any recent activity after it was closed.
31 | Please open a new issue for related bugs.
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 |
7 | jobs:
8 | release:
9 | uses: reactiveui/actions-common/.github/workflows/workflow-common-release.yml@main
10 | with:
11 | configuration: Release
12 | productNamespacePrefix: "Fusillade"
13 | installWorkflows: false
14 | secrets:
15 | SIGN_CLIENT_USER_ID: ${{ secrets.SIGN_CLIENT_USER_ID }}
16 | SIGN_CLIENT_SECRET: ${{ secrets.SIGN_CLIENT_SECRET }}
17 | SIGN_CLIENT_CONFIG: ${{ secrets.SIGN_CLIENT_CONFIG }}
18 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }}
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 |
4 | # User-specific files
5 | *.suo
6 | *.user
7 | *.userosscache
8 | *.sln.docstates
9 |
10 | # User-specific files (MonoDevelop/Xamarin Studio)
11 | *.userprefs
12 |
13 | # Build results
14 | [Dd]ebug/
15 | [Dd]ebugPublic/
16 | [Rr]elease/
17 | [Rr]eleases/
18 | x64/
19 | x86/
20 | build/
21 | bld/
22 | [Bb]in/
23 | [Oo]bj/
24 |
25 | # Visual Studio 2015 cache/options directory
26 | .vs/
27 | # Uncomment if you have tasks that create the project's static files in wwwroot
28 | #wwwroot/
29 |
30 | # MSTest test Results
31 | [Tt]est[Rr]esult*/
32 | [Bb]uild[Ll]og.*
33 |
34 | # NUNIT
35 | *.VisualState.xml
36 | TestResult.xml
37 |
38 | # Build Results of an ATL Project
39 | [Dd]ebugPS/
40 | [Rr]eleasePS/
41 | dlldata.c
42 |
43 | # DNX
44 | *.lock.json
45 | artifacts/
46 | *.nuget.props
47 | *.nuget.targets
48 |
49 | *_i.c
50 | *_p.c
51 | *_i.h
52 | *.ilk
53 | *.meta
54 | *.obj
55 | *.pch
56 | *.pdb
57 | *.pgc
58 | *.pgd
59 | *.rsp
60 | *.sbr
61 | *.tlb
62 | *.tli
63 | *.tlh
64 | *.tmp
65 | *.tmp_proj
66 | *.log
67 | *.vspscc
68 | *.vssscc
69 | .builds
70 | *.pidb
71 | *.svclog
72 | *.scc
73 |
74 | # Chutzpah Test files
75 | _Chutzpah*
76 |
77 | # Visual C++ cache files
78 | ipch/
79 | *.aps
80 | *.ncb
81 | *.opendb
82 | *.opensdf
83 | *.sdf
84 | *.cachefile
85 |
86 | # Visual Studio profiler
87 | *.psess
88 | *.vsp
89 | *.vspx
90 | *.sap
91 |
92 | # TFS 2012 Local Workspace
93 | $tf/
94 |
95 | # Guidance Automation Toolkit
96 | *.gpState
97 |
98 | # ReSharper is a .NET coding add-in
99 | _ReSharper*/
100 | *.[Rr]e[Ss]harper
101 | *.DotSettings.user
102 |
103 | # JustCode is a .NET coding add-in
104 | .JustCode
105 |
106 | # TeamCity is a build add-in
107 | _TeamCity*
108 |
109 | # DotCover is a Code Coverage Tool
110 | *.dotCover
111 |
112 | # NCrunch
113 | _NCrunch_*
114 | .*crunch*.local.xml
115 | nCrunchTemp_*
116 |
117 | # MightyMoose
118 | *.mm.*
119 | AutoTest.Net/
120 |
121 | # Web workbench (sass)
122 | .sass-cache/
123 |
124 | # Installshield output folder
125 | [Ee]xpress/
126 |
127 | # DocProject is a documentation generator add-in
128 | DocProject/buildhelp/
129 | DocProject/Help/*.HxT
130 | DocProject/Help/*.HxC
131 | DocProject/Help/*.hhc
132 | DocProject/Help/*.hhk
133 | DocProject/Help/*.hhp
134 | DocProject/Help/Html2
135 | DocProject/Help/html
136 |
137 | # Click-Once directory
138 | publish/
139 |
140 | # Publish Web Output
141 | *.[Pp]ublish.xml
142 | *.azurePubxml
143 | # TODO: Comment the next line if you want to checkin your web deploy settings
144 | # but database connection strings (with potential passwords) will be unencrypted
145 | *.pubxml
146 | *.publishproj
147 |
148 | # NuGet Packages
149 | *.nupkg
150 | # The packages folder can be ignored because of Package Restore
151 | **/packages/*
152 | # except build/, which is used as an MSBuild target.
153 | !**/packages/build/
154 | # Uncomment if necessary however generally it will be regenerated when needed
155 | #!**/packages/repositories.config
156 |
157 | # Windows Azure Build Output
158 | csx/
159 | *.build.csdef
160 |
161 | # Windows Azure Emulator
162 | ecf/
163 | rcf/
164 |
165 | # Windows Store app package directory
166 | AppPackages/
167 | BundleArtifacts/
168 |
169 | # Visual Studio cache files
170 | # files ending in .cache can be ignored
171 | *.[Cc]ache
172 | # but keep track of directories ending in .cache
173 | !*.[Cc]ache/
174 |
175 | # Others
176 | ClientBin/
177 | ~$*
178 | *~
179 | *.dbmdl
180 | *.dbproj.schemaview
181 | *.pfx
182 | *.publishsettings
183 | node_modules/
184 | orleans.codegen.cs
185 |
186 | # RIA/Silverlight projects
187 | Generated_Code/
188 |
189 | # Backup & report files from converting an old project file
190 | # to a newer Visual Studio version. Backup files are not needed,
191 | # because we have git ;-)
192 | _UpgradeReport_Files/
193 | Backup*/
194 | UpgradeLog*.XML
195 | UpgradeLog*.htm
196 |
197 | # SQL Server files
198 | *.mdf
199 | *.ldf
200 |
201 | # Business Intelligence projects
202 | *.rdl.data
203 | *.bim.layout
204 | *.bim_*.settings
205 |
206 | # Microsoft Fakes
207 | FakesAssemblies/
208 |
209 | # GhostDoc plugin setting file
210 | *.GhostDoc.xml
211 |
212 | # Node.js Tools for Visual Studio
213 | .ntvs_analysis.dat
214 |
215 | # Visual Studio 6 build log
216 | *.plg
217 |
218 | # Visual Studio 6 workspace options file
219 | *.opt
220 |
221 | # Visual Studio LightSwitch build output
222 | **/*.HTMLClient/GeneratedArtifacts
223 | **/*.DesktopClient/GeneratedArtifacts
224 | **/*.DesktopClient/ModelManifest.xml
225 | **/*.Server/GeneratedArtifacts
226 | **/*.Server/ModelManifest.xml
227 | _Pvt_Extensions
228 |
229 | # Paket dependency manager
230 | .paket/paket.exe
231 |
232 | # FAKE - F# Make
233 | .fake/
234 |
235 | # Tools
236 | tools/
237 |
238 | # ReactiveUI
239 | artifacts/
240 | src/CommonAssemblyInfo.cs
241 | src/ReactiveUI.Events/Events_*.cs
242 | src/Fusillade.Tests/API/ApiApprovalTests.*.received.txt
243 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Code of Conduct
2 |
3 | As contributors and maintainers of this project, and in the interest of
4 | fostering an open and welcoming community, we pledge to respect all people who
5 | contribute through reporting issues, posting feature requests, updating
6 | documentation, submitting pull requests or patches, and other activities.
7 |
8 | We are committed to making participation in this project a harassment-free
9 | experience for everyone, regardless of level of experience, gender, gender
10 | identity and expression, sexual orientation, disability, personal appearance,
11 | body size, race, ethnicity, age, religion, or nationality.
12 |
13 | Examples of unacceptable behavior by participants include:
14 |
15 | * The use of sexualized language or imagery
16 | * Personal attacks
17 | * Trolling or insulting/derogatory comments
18 | * Public or private harassment
19 | * Publishing other's private information, such as physical or electronic
20 | addresses, without explicit permission
21 | * Other unethical or unprofessional conduct
22 |
23 | Project maintainers have the right and responsibility to remove, edit, or
24 | reject comments, commits, code, wiki edits, issues, and other contributions
25 | that are not aligned to this Code of Conduct, or to ban temporarily or
26 | permanently any contributor for other behaviors that they deem inappropriate,
27 | threatening, offensive, or harmful.
28 |
29 | By adopting this Code of Conduct, project maintainers commit themselves to
30 | fairly and consistently applying these principles to every aspect of managing
31 | this project. Project maintainers who do not follow or enforce the Code of
32 | Conduct may be permanently removed from the project team.
33 |
34 | This Code of Conduct applies both within project spaces and in public spaces
35 | when an individual is representing the project or its community.
36 |
37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
38 | reported by contacting a project maintainer at anais@anaisbetts.org. All
39 | complaints will be reviewed and investigated and will result in a response that
40 | is deemed necessary and appropriate to the circumstances. Maintainers are
41 | obligated to maintain confidentiality with regard to the reporter of an
42 | incident.
43 |
44 |
45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
46 | version 1.3.0, available at
47 | [http://contributor-covenant.org/version/1/3/0/][version]
48 |
49 | [homepage]: http://contributor-covenant.org
50 | [version]: http://contributor-covenant.org/version/1/3/0/
51 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Fusillade
2 |
3 | We'd love for you to contribute to our source code and to make Fusillade even better than it is
4 | today! Here are the guidelines we'd like you to follow:
5 |
6 | - [Code of Conduct](#coc)
7 | - [Question or Problem?](#question)
8 | - [Issues and Bugs](#issue)
9 | - [Feature Requests](#feature)
10 | - [Submission Guidelines](#submit)
11 | - [Coding Rules](#rules)
12 | - [Commit Message Guidelines](#commit)
13 |
14 | ## Code of Conduct
15 |
16 | Help us keep the project open and inclusive. Please read and follow our [Code of Conduct](CODE_OF_CONDUCT.md).
17 |
18 | ## Got a Question or Problem?
19 |
20 |
21 | ## Found an Issue?
22 |
23 | If you find a bug in the source code or a mistake in the documentation, you can help us by
24 | submitting an issue to our [GitHub Repository](https://github.com/reactiveui/Fusillade). Even better you can submit a Pull Request
25 | with a fix.
26 |
27 | **Please see the [Submission Guidelines](#submit) below.**
28 |
29 | ## Want a Feature?
30 |
31 | You can request a new feature by submitting an issue to our [GitHub Repository](https://github.com/paulcbetts/Fusillade). If you
32 | would like to implement a new feature then consider what kind of change it is:
33 |
34 | prevent duplication of work, and help you to craft the change so that it is successfully accepted
35 | into the project.
36 | * **Small Changes** can be crafted and submitted to the [GitHub Repository](https://github.com/reactiveui/Fusillade) as a Pull
37 | Request.
38 |
39 | ## Submission Guidelines
40 |
41 | ### Submitting an Issue
42 |
43 | If your issue appears to be a bug, and hasn't been reported, open a new issue. Help us to maximize
44 | the effort we can spend fixing issues and adding new features, by not reporting duplicate issues.
45 |
46 | Providing the following information will increase the chances of your issue being dealt with
47 | quickly:
48 |
49 | * **Overview of the Issue** - if an error is being thrown a stack trace helps
50 | * **Motivation for or Use Case** - explain why this is a bug for you
51 | * **Fusillade Version(s)** - is it a regression?
52 | * **Operating System** - is this a problem with all browsers or only specific ones?
53 | * **Reproduce the Error** - provide a example or an unambiguous set of steps.
54 | * **Related Issues** - has a similar issue been reported before?
55 | * **Suggest a Fix** - if you can't fix the bug yourself, perhaps you can point to what might be
56 | causing the problem (line of code or commit)
57 |
58 | **If you get help, help others. Good karma rulez!**
59 |
60 | ### Submitting a Pull Request
61 | Before you submit your pull request consider the following guidelines:
62 |
63 | * Search [GitHub](https://github.com/reactiveui/Fusillade/pulls) for an open or closed Pull Request
64 | that relates to your submission. You don't want to duplicate effort.
65 | * Make your changes in a new git branch:
66 |
67 | ```shell
68 | git checkout -b my-fix-branch master
69 | ```
70 |
71 | * Create your patch, **including appropriate test cases**.
72 | * Follow our [Coding Rules](#rules).
73 | * Run the test suite, as described below.
74 | * Commit your changes using a descriptive commit message that follows our
75 | [commit message conventions](#commit).
76 |
77 | ```shell
78 | git commit -a
79 | ```
80 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files.
81 |
82 | * Build your changes locally to ensure all the tests pass:
83 |
84 | ```shell
85 | build.cmd
86 | ```
87 |
88 | * Push your branch to GitHub:
89 |
90 | ```shell
91 | git push origin my-fix-branch
92 | ```
93 |
94 | In GitHub, send a pull request to `Fusillade:master`.
95 |
96 | If we suggest changes, then:
97 |
98 | * Make the required updates.
99 | * Re-run the test suite to ensure tests are still passing.
100 | * Commit your changes to your branch (e.g. `my-fix-branch`).
101 | * Push the changes to your GitHub repository (this will update your Pull Request).
102 |
103 | If the PR gets too outdated we may ask you to rebase and force push to update the PR:
104 |
105 | ```shell
106 | git rebase master -i
107 | git push origin my-fix-branch -f
108 | ```
109 |
110 | _WARNING: Squashing or reverting commits and force-pushing thereafter may remove GitHub comments
111 | on code that were previously made by you or others in your commits. Avoid any form of rebasing
112 | unless necessary._
113 |
114 | That's it! Thank you for your contribution!
115 |
116 | #### After your pull request is merged
117 |
118 | After your pull request is merged, you can safely delete your branch and pull the changes
119 | from the main (upstream) repository:
120 |
121 | * Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows:
122 |
123 | ```shell
124 | git push origin --delete my-fix-branch
125 | ```
126 |
127 | * Check out the master branch:
128 |
129 | ```shell
130 | git checkout master -f
131 | ```
132 |
133 | * Delete the local branch:
134 |
135 | ```shell
136 | git branch -D my-fix-branch
137 | ```
138 |
139 | * Update your master with the latest upstream version:
140 |
141 | ```shell
142 | git pull --ff upstream master
143 | ```
144 |
145 | ## Coding Rules
146 |
147 | To ensure consistency throughout the source code, keep these rules in mind as you are working:
148 |
149 | * All features or bug fixes **must be tested** by one or more unit tests.
150 | * All public API methods **must be documented** with XML documentation.
151 |
152 | ## Git Commit Guidelines
153 |
154 | Each commit message consists of a **header**, a **body** and a **footer**. The header has a special
155 | format that includes a **type** and a **subject**:
156 |
157 | ```
158 | :
159 |
160 |
161 |
162 | ```
163 |
164 | ### Revert
165 |
166 | If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit. In the body it should say: `This reverts commit .`, where the hash is the SHA of the commit being reverted.
167 |
168 | ### Type
169 |
170 | Must be one of the following:
171 |
172 | * **feat**: A new feature
173 | * **fix**: A bug fix
174 | * **docs**: Documentation only changes
175 | * **style**: Changes that do not affect the meaning of the code (white-space, formatting, missing
176 | semi-colons, etc)
177 | * **refactor**: A code change that neither fixes a bug nor adds a feature
178 | * **perf**: A code change that improves performance
179 | * **test**: Adding missing tests
180 | * **chore**: Changes to the build process or auxiliary tools and libraries such as documentation
181 | generation
182 |
183 | ### Subject
184 | The subject contains succinct description of the change:
185 |
186 | * use the imperative, present tense: "change" not "changed" nor "changes"
187 | * don't capitalize first letter
188 | * no dot (.) at the end
189 |
190 | ### Body
191 | Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
192 | The body should include the motivation for the change and contrast this with previous behavior.
193 |
194 | ### Footer
195 | The footer should contain any information about **Breaking Changes** and is also the place to
196 | reference GitHub issues that this commit **Closes**.
197 |
198 | **Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
199 |
--------------------------------------------------------------------------------
/Images/main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/reactiveui/Fusillade/74b78674fa38b42eac1640af51fd1ae5dcfaa288/Images/main.png
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Anaïs Betts
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
7 | the Software, and to permit persons to whom the Software is furnished to do so,
8 | subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | [](https://www.nuget.org/packages/fusillade)  [](https://codecov.io/gh/reactiveui/akavache)
3 |
4 |
5 |
6 |
7 |
8 |
9 | ## Fusillade: An opinionated HTTP library for Mobile Development
10 |
11 | Fusillade helps you to write more efficient code in mobile and desktop
12 | applications written in C#. Its design goals and feature set are inspired by
13 | [Volley](http://arnab.ch/blog/2013/08/asynchronous-http-requests-in-android-using-volley/)
14 | as well as [Picasso](http://square.github.io/picasso/).
15 |
16 | ### What even does this do for me?
17 |
18 | Fusillade is a set of HttpMessageHandlers (i.e. "drivers" for HttpClient) that
19 | make your mobile applications more efficient and responsive:
20 |
21 | * **Auto-deduplication of relevant requests** - if every instance of your TweetView
22 | class requests the same avatar image, Fusillade will only do *one* request
23 | and give the result to every instance. All `GET`, `HEAD`, and `OPTIONS` requests are deduplicated.
24 |
25 | * **Request Limiting** - Requests are always dispatched 4 at a time (the
26 | Volley default) - issue lots of requests without overwhelming the network
27 | connection.
28 |
29 | * **Request Prioritization** - background requests should run at a lower
30 | priority than requests initiated by the user, but actually *implementing*
31 | this is quite difficult. With a few changes to your app, you can hint to
32 | Fusillade which requests should skip to the front of the queue.
33 |
34 | * **Speculative requests** - On page load, many apps will try to speculatively
35 | cache data (i.e. try to pre-download data that the user *might* click on).
36 | Marking requests as speculative will allow requests until a certain data
37 | limit is reached, then cancel future requests (i.e. "Keep downloading data
38 | in the background until we've got 5MB of cached data")
39 |
40 | ### How do I use it?
41 |
42 | The easiest way to interact with Fusillade is via a class called `NetCache`,
43 | which has a number of built-in scenarios:
44 |
45 | ```cs
46 | public static class NetCache
47 | {
48 | // Use to fetch data into a cache when a page loads. Expect that
49 | // these requests will only get so far then give up and start failing
50 | public static HttpMessageHandler Speculative { get; set; }
51 |
52 | // Use for network requests that are running in the background
53 | public static HttpMessageHandler Background { get; set; }
54 |
55 | // Use for network requests that are fetching data that the user is
56 | // waiting on *right now*
57 | public static HttpMessageHandler UserInitiated { get; set; }
58 | }
59 | ```
60 |
61 | To use them, just create an `HttpClient` with the given handler:
62 |
63 | ```cs
64 | var client = new HttpClient(NetCache.UserInitiated);
65 | var response = await client.GetAsync("http://httpbin.org/get");
66 | var str = await response.Content.ReadAsStringAsync();
67 |
68 | Console.WriteLine(str);
69 | ```
70 |
71 | ### Where does it work?
72 |
73 | Everywhere! Fusillade is a Portable Library, it works on:
74 |
75 | * Xamarin.Android
76 | * Xamarin.iOS
77 | * Xamarin.Mac
78 | * Windows Desktop apps
79 | * WinRT / Windows Phone 8.1 apps
80 | * Windows Phone 8
81 |
82 | ### More on speculative requests
83 |
84 | Generally, on a mobile app, you'll want to *reset* the Speculative limit every
85 | time the app resumes from standby. How you do this depends on the platform,
86 | but in that callback, you need to call:
87 |
88 | ```cs
89 | NetCache.Speculative.ResetLimit(1048576 * 5/*MB*/);
90 | ```
91 |
92 | ### Offline Support
93 |
94 | Fusillade can optionally cache responses that it sees, then play them back to
95 | you when your app is offline (or you just want to speed up your app by fetching
96 | cached data). Here's how to set it up:
97 |
98 | * Implement the IRequestCache interface:
99 |
100 | ```cs
101 | public interface IRequestCache
102 | {
103 | ///
104 | /// Implement this method by saving the Body of the response. The
105 | /// response is already downloaded as a ByteArrayContent so you don't
106 | /// have to worry about consuming the stream.
107 | /// The originating request.
108 | /// The response whose body you should save.
109 | /// A unique key used to identify the request details.
110 | /// Cancellation token.
111 | /// Completion.
112 | Task Save(HttpRequestMessage request, HttpResponseMessage response, string key, CancellationToken ct);
113 |
114 | ///
115 | /// Implement this by loading the Body of the given request / key.
116 | ///
117 | /// The originating request.
118 | /// A unique key used to identify the request details,
119 | /// that was given in Save().
120 | /// Cancellation token.
121 | /// The Body of the given request, or null if the search
122 | /// completed successfully but the response was not found.
123 | Task Fetch(HttpRequestMessage request, string key, CancellationToken ct);
124 | }
125 | ```
126 |
127 | * Set an instance to `NetCache.RequestCache`, and make some requests:
128 |
129 | ```cs
130 | NetCache.RequestCache = new MyCoolCache();
131 |
132 | var client = new HttpClient(NetCache.UserInitiated);
133 | await client.GetStringAsync("https://httpbin.org/get");
134 | ```
135 |
136 | * Now you can use `NetCache.Offline` to get data even when the Internet is disconnected:
137 |
138 | ```cs
139 | // This will never actually make an HTTP request, it will either succeed via
140 | // reading from MyCoolCache, or return an HttpResponseMessage with a 503 Status code.
141 | var client = new HttpClient(NetCache.Offline);
142 | await client.GetStringAsync("https://httpbin.org/get");
143 | ```
144 |
145 | ### How do I use this with ModernHttpClient?
146 |
147 | Add this line to a **static constructor** of your app's startup class:
148 |
149 | ```cs
150 | using Splat;
151 |
152 | Locator.CurrentMutable.RegisterConstant(new NativeMessageHandler(), typeof(HttpMessageHandler));
153 | ```
154 |
155 | ### What do the priorities mean?
156 |
157 | The precedence is UserInitiated > Background > Speculative
158 | Which means that anything set as UserInitiate has a higher priority than Background or Speculative.
159 |
160 | Explicit is a special that allows to set an explicit value that can be higher, lower or in between any of the predefined cases.
161 |
162 | ```csharp
163 | var lowerThanSpeculative = new RateLimitedHttpMessageHandler(
164 | new HttpClientHandler(),
165 | Priority.Explicit,
166 | 9);
167 |
168 | var moreThanSpeculativeButLessThanBAckground = new RateLimitedHttpMessageHandler(
169 | new HttpClientHandler(),
170 | Priority.Explicit,
171 | 15);
172 |
173 | var doItBeforeEverythingElse = new RateLimitedHttpMessageHandler(
174 | new HttpClientHandler(),
175 | Priority.Explicit,
176 | 1000);
177 | ```
178 |
179 | ### Statics? That sucks! I like $OTHER_THING! Your priorities suck, I want to come up with my own scheme!
180 |
181 | `NetCache` is just a nice pre-canned default, the interesting code is in a
182 | class called `RateLimitedHttpMessageHandler`. You can create it explicitly and
183 | configure it as-needed.
184 |
185 | ### What's with the name?
186 |
187 | The word 'Fusillade' is a synonym for Volley :)
188 |
189 | ## Contribute
190 |
191 | Fusillade is developed under an OSI-approved open source license, making it freely usable and distributable, even for commercial use. Because of our Open Collective model for funding and transparency, we are able to funnel support and funds through to our contributors and community. We ❤ the people who are involved in this project, and we’d love to have you on board, especially if you are just getting started or have never contributed to open-source before.
192 |
193 | So here's to you, lovely person who wants to join us — this is how you can support us:
194 |
195 | * [Responding to questions on StackOverflow](https://stackoverflow.com/questions/tagged/fusillade)
196 | * [Passing on knowledge and teaching the next generation of developers](http://ericsink.com/entries/dont_use_rxui.html)
197 | * [Donations](https://reactiveui.net/donate) and [Corporate Sponsorships](https://reactiveui.net/sponsorship)
198 | * [Asking your employer to reciprocate and contribute to open-source](https://github.com/github/balanced-employee-ip-agreement)
199 | * Submitting documentation updates where you see fit or lacking.
200 | * Making contributions to the code base.
201 |
--------------------------------------------------------------------------------
/src/Directory.build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | $(NoWarn);VSX1000
5 | AnyCPU
6 | $(MSBuildProjectName.Contains('Tests'))
7 | git
8 | true
9 | $(MSBuildThisFileDirectory)analyzers.ruleset
10 | Embedded
11 | .NET Foundation and Contributors
12 | Copyright (c) .NET Foundation and Contributors
13 | main.png
14 | README.md
15 | MIT
16 |
17 | true
18 |
19 | true
20 |
21 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
22 |
23 | True
24 | latest
25 | preview
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/src/Directory.build.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 | $(AssemblyName) ($(TargetFramework))
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/Fusillade.Tests/API/ApiApprovalTests.FusilladeTests.DotNet6_0.verified.txt:
--------------------------------------------------------------------------------
1 | [assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v6.0", FrameworkDisplayName=".NET 6.0")]
2 | namespace Fusillade
3 | {
4 | public interface IRequestCache
5 | {
6 | System.Threading.Tasks.Task Fetch(System.Net.Http.HttpRequestMessage request, string key, System.Threading.CancellationToken ct);
7 | System.Threading.Tasks.Task Save(System.Net.Http.HttpRequestMessage request, System.Net.Http.HttpResponseMessage response, string key, System.Threading.CancellationToken ct);
8 | }
9 | public abstract class LimitingHttpMessageHandler : System.Net.Http.DelegatingHandler
10 | {
11 | protected LimitingHttpMessageHandler() { }
12 | protected LimitingHttpMessageHandler(System.Net.Http.HttpMessageHandler innerHandler) { }
13 | public abstract void ResetLimit(long? maxBytesToRead = default);
14 | }
15 | public static class NetCache
16 | {
17 | public static System.Net.Http.HttpMessageHandler Background { get; set; }
18 | public static System.Net.Http.HttpMessageHandler Offline { get; set; }
19 | public static Punchclock.OperationQueue OperationQueue { get; set; }
20 | public static Fusillade.IRequestCache? RequestCache { get; set; }
21 | public static Fusillade.LimitingHttpMessageHandler Speculative { get; set; }
22 | public static System.Net.Http.HttpMessageHandler UserInitiated { get; set; }
23 | }
24 | public class OfflineHttpMessageHandler : System.Net.Http.HttpMessageHandler
25 | {
26 | public OfflineHttpMessageHandler(System.Func>? retrieveBodyFunc) { }
27 | protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { }
28 | }
29 | public enum Priority
30 | {
31 | Explicit = 0,
32 | Speculative = 10,
33 | Background = 20,
34 | UserInitiated = 100,
35 | }
36 | public class RateLimitedHttpMessageHandler : Fusillade.LimitingHttpMessageHandler
37 | {
38 | public RateLimitedHttpMessageHandler(System.Net.Http.HttpMessageHandler handler, Fusillade.Priority basePriority, int priority = 0, long? maxBytesToRead = default, Punchclock.OperationQueue? opQueue = null, System.Func? cacheResultFunc = null) { }
39 | public override void ResetLimit(long? maxBytesToRead = default) { }
40 | protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { }
41 | public static string UniqueKeyForRequest(System.Net.Http.HttpRequestMessage request) { }
42 | }
43 | }
--------------------------------------------------------------------------------
/src/Fusillade.Tests/API/ApiApprovalTests.FusilladeTests.DotNet7_0.verified.txt:
--------------------------------------------------------------------------------
1 | [assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v7.0", FrameworkDisplayName=".NET 7.0")]
2 | namespace Fusillade
3 | {
4 | public interface IRequestCache
5 | {
6 | System.Threading.Tasks.Task Fetch(System.Net.Http.HttpRequestMessage request, string key, System.Threading.CancellationToken ct);
7 | System.Threading.Tasks.Task Save(System.Net.Http.HttpRequestMessage request, System.Net.Http.HttpResponseMessage response, string key, System.Threading.CancellationToken ct);
8 | }
9 | public abstract class LimitingHttpMessageHandler : System.Net.Http.DelegatingHandler
10 | {
11 | protected LimitingHttpMessageHandler() { }
12 | protected LimitingHttpMessageHandler(System.Net.Http.HttpMessageHandler innerHandler) { }
13 | public abstract void ResetLimit(long? maxBytesToRead = default);
14 | }
15 | public static class NetCache
16 | {
17 | public static System.Net.Http.HttpMessageHandler Background { get; set; }
18 | public static System.Net.Http.HttpMessageHandler Offline { get; set; }
19 | public static Punchclock.OperationQueue OperationQueue { get; set; }
20 | public static Fusillade.IRequestCache? RequestCache { get; set; }
21 | public static Fusillade.LimitingHttpMessageHandler Speculative { get; set; }
22 | public static System.Net.Http.HttpMessageHandler UserInitiated { get; set; }
23 | }
24 | public class OfflineHttpMessageHandler : System.Net.Http.HttpMessageHandler
25 | {
26 | public OfflineHttpMessageHandler(System.Func>? retrieveBodyFunc) { }
27 | protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { }
28 | }
29 | public enum Priority
30 | {
31 | Explicit = 0,
32 | Speculative = 10,
33 | Background = 20,
34 | UserInitiated = 100,
35 | }
36 | public class RateLimitedHttpMessageHandler : Fusillade.LimitingHttpMessageHandler
37 | {
38 | public RateLimitedHttpMessageHandler(System.Net.Http.HttpMessageHandler handler, Fusillade.Priority basePriority, int priority = 0, long? maxBytesToRead = default, Punchclock.OperationQueue? opQueue = null, System.Func? cacheResultFunc = null) { }
39 | public override void ResetLimit(long? maxBytesToRead = default) { }
40 | protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { }
41 | public static string UniqueKeyForRequest(System.Net.Http.HttpRequestMessage request) { }
42 | }
43 | }
--------------------------------------------------------------------------------
/src/Fusillade.Tests/API/ApiApprovalTests.FusilladeTests.DotNet8_0.verified.txt:
--------------------------------------------------------------------------------
1 | [assembly: System.Runtime.Versioning.TargetFramework(".NETCoreApp,Version=v8.0", FrameworkDisplayName=".NET 8.0")]
2 | namespace Fusillade
3 | {
4 | public interface IRequestCache
5 | {
6 | System.Threading.Tasks.Task Fetch(System.Net.Http.HttpRequestMessage request, string key, System.Threading.CancellationToken ct);
7 | System.Threading.Tasks.Task Save(System.Net.Http.HttpRequestMessage request, System.Net.Http.HttpResponseMessage response, string key, System.Threading.CancellationToken ct);
8 | }
9 | public abstract class LimitingHttpMessageHandler : System.Net.Http.DelegatingHandler
10 | {
11 | protected LimitingHttpMessageHandler() { }
12 | protected LimitingHttpMessageHandler(System.Net.Http.HttpMessageHandler innerHandler) { }
13 | public abstract void ResetLimit(long? maxBytesToRead = default);
14 | }
15 | public static class NetCache
16 | {
17 | public static System.Net.Http.HttpMessageHandler Background { get; set; }
18 | public static System.Net.Http.HttpMessageHandler Offline { get; set; }
19 | public static Punchclock.OperationQueue OperationQueue { get; set; }
20 | public static Fusillade.IRequestCache? RequestCache { get; set; }
21 | public static Fusillade.LimitingHttpMessageHandler Speculative { get; set; }
22 | public static System.Net.Http.HttpMessageHandler UserInitiated { get; set; }
23 | }
24 | public class OfflineHttpMessageHandler : System.Net.Http.HttpMessageHandler
25 | {
26 | public OfflineHttpMessageHandler(System.Func>? retrieveBodyFunc) { }
27 | protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { }
28 | }
29 | public enum Priority
30 | {
31 | Explicit = 0,
32 | Speculative = 10,
33 | Background = 20,
34 | UserInitiated = 100,
35 | }
36 | public class RateLimitedHttpMessageHandler : Fusillade.LimitingHttpMessageHandler
37 | {
38 | public RateLimitedHttpMessageHandler(System.Net.Http.HttpMessageHandler handler, Fusillade.Priority basePriority, int priority = 0, long? maxBytesToRead = default, Punchclock.OperationQueue? opQueue = null, System.Func? cacheResultFunc = null) { }
39 | public override void ResetLimit(long? maxBytesToRead = default) { }
40 | protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { }
41 | public static string UniqueKeyForRequest(System.Net.Http.HttpRequestMessage request) { }
42 | }
43 | }
--------------------------------------------------------------------------------
/src/Fusillade.Tests/API/ApiApprovalTests.FusilladeTests.Net4_7.verified.txt:
--------------------------------------------------------------------------------
1 | [assembly: System.Runtime.Versioning.TargetFramework(".NETStandard,Version=v2.0", FrameworkDisplayName=".NET Standard 2.0")]
2 | namespace Fusillade
3 | {
4 | public interface IRequestCache
5 | {
6 | System.Threading.Tasks.Task Fetch(System.Net.Http.HttpRequestMessage request, string key, System.Threading.CancellationToken ct);
7 | System.Threading.Tasks.Task Save(System.Net.Http.HttpRequestMessage request, System.Net.Http.HttpResponseMessage response, string key, System.Threading.CancellationToken ct);
8 | }
9 | public abstract class LimitingHttpMessageHandler : System.Net.Http.DelegatingHandler
10 | {
11 | protected LimitingHttpMessageHandler() { }
12 | protected LimitingHttpMessageHandler(System.Net.Http.HttpMessageHandler innerHandler) { }
13 | public abstract void ResetLimit(long? maxBytesToRead = default);
14 | }
15 | public static class NetCache
16 | {
17 | public static System.Net.Http.HttpMessageHandler Background { get; set; }
18 | public static System.Net.Http.HttpMessageHandler Offline { get; set; }
19 | public static Punchclock.OperationQueue OperationQueue { get; set; }
20 | public static Fusillade.IRequestCache? RequestCache { get; set; }
21 | public static Fusillade.LimitingHttpMessageHandler Speculative { get; set; }
22 | public static System.Net.Http.HttpMessageHandler UserInitiated { get; set; }
23 | }
24 | public class OfflineHttpMessageHandler : System.Net.Http.HttpMessageHandler
25 | {
26 | public OfflineHttpMessageHandler(System.Func>? retrieveBodyFunc) { }
27 | protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { }
28 | }
29 | public enum Priority
30 | {
31 | Explicit = 0,
32 | Speculative = 10,
33 | Background = 20,
34 | UserInitiated = 100,
35 | }
36 | public class RateLimitedHttpMessageHandler : Fusillade.LimitingHttpMessageHandler
37 | {
38 | public RateLimitedHttpMessageHandler(System.Net.Http.HttpMessageHandler handler, Fusillade.Priority basePriority, int priority = 0, long? maxBytesToRead = default, Punchclock.OperationQueue? opQueue = null, System.Func? cacheResultFunc = null) { }
39 | public override void ResetLimit(long? maxBytesToRead = default) { }
40 | protected override System.Threading.Tasks.Task SendAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken) { }
41 | public static string UniqueKeyForRequest(System.Net.Http.HttpRequestMessage request) { }
42 | }
43 | }
--------------------------------------------------------------------------------
/src/Fusillade.Tests/API/ApiApprovalTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System.Diagnostics.CodeAnalysis;
7 | using System.Threading.Tasks;
8 | using Xunit;
9 |
10 | namespace Fusillade.APITests
11 | {
12 | ///
13 | /// Tests for handling API approval.
14 | ///
15 | [ExcludeFromCodeCoverage]
16 | public class ApiApprovalTests
17 | {
18 | ///
19 | /// Tests to make sure the akavache project is approved.
20 | ///
21 | /// A representing the asynchronous unit test.
22 | [Fact]
23 | public Task FusilladeTests() => typeof(OfflineHttpMessageHandler).Assembly.CheckApproval(["Fusillade"]);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/Fusillade.Tests/API/ApiExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System;
7 | using System.Diagnostics.CodeAnalysis;
8 | using System.Reflection;
9 | using System.Runtime.CompilerServices;
10 | using System.Threading.Tasks;
11 | using PublicApiGenerator;
12 | using VerifyXunit;
13 |
14 | namespace Fusillade.APITests;
15 |
16 | ///
17 | /// A helper for doing API approvals.
18 | ///
19 | public static class ApiExtensions
20 | {
21 | ///
22 | /// Checks to make sure the API is approved.
23 | ///
24 | /// The assembly that is being checked.
25 | /// The namespaces.
26 | /// The caller file path.
27 | ///
28 | /// A Task.
29 | ///
30 | public static async Task CheckApproval(this Assembly assembly, string[] namespaces, [CallerFilePath] string filePath = "")
31 | {
32 | var generatorOptions = new ApiGeneratorOptions { AllowNamespacePrefixes = namespaces };
33 | var apiText = assembly.GeneratePublicApi(generatorOptions);
34 | var result = await Verifier.Verify(apiText, null, filePath)
35 | .UniqueForRuntimeAndVersion()
36 | .ScrubEmptyLines()
37 | .ScrubLines(l =>
38 | l.StartsWith("[assembly: AssemblyVersion(", StringComparison.InvariantCulture) ||
39 | l.StartsWith("[assembly: AssemblyFileVersion(", StringComparison.InvariantCulture) ||
40 | l.StartsWith("[assembly: AssemblyInformationalVersion(", StringComparison.InvariantCulture) ||
41 | l.StartsWith("[assembly: System.Reflection.AssemblyMetadata(", StringComparison.InvariantCulture));
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/Fusillade.Tests/Fusillade.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | net6.0;net8.0
4 | $(TargetFrameworks);net472
5 | $(NoWarn);1591;CA1707;SA1633
6 | false
7 | $(NoWarn);CA2000;CA1031;CA1307;CA1305
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Fusillade.Tests/Http/BaseHttpSchedulerSharedTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System.Net.Http;
7 | using Punchclock;
8 |
9 | namespace Fusillade.Tests
10 | {
11 | ///
12 | /// Checks to make sure the base http scheduler works.
13 | ///
14 | public class BaseHttpSchedulerSharedTests : HttpSchedulerSharedTests
15 | {
16 | ///
17 | protected override LimitingHttpMessageHandler CreateFixture(HttpMessageHandler innerHandler) =>
18 | new RateLimitedHttpMessageHandler(innerHandler, Priority.UserInitiated, opQueue: new OperationQueue(4));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Fusillade.Tests/Http/HttpSchedulerCachingTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Net;
10 | using System.Net.Http;
11 | using System.Net.Http.Headers;
12 | using System.Reactive.Linq;
13 | using System.Text;
14 | using System.Threading.Tasks;
15 | using Akavache;
16 | using Xunit;
17 |
18 | namespace Fusillade.Tests.Http
19 | {
20 | ///
21 | /// Checks to make sure that the http scheduler caches correctly.
22 | ///
23 | public class HttpSchedulerCachingTests
24 | {
25 | ///
26 | /// Checks to make sure that the caching functiosn are only called with content.
27 | ///
28 | /// A task to monitor the progress.
29 | [Fact]
30 | public async Task CachingFunctionShouldBeCalledWithContent()
31 | {
32 | var innerHandler = new TestHttpMessageHandler(_ =>
33 | {
34 | var ret = new HttpResponseMessage()
35 | {
36 | Content = new StringContent("foo", Encoding.UTF8),
37 | StatusCode = HttpStatusCode.OK,
38 | };
39 |
40 | ret.Headers.ETag = new EntityTagHeaderValue("\"worifjw\"");
41 | return Observable.Return(ret);
42 | });
43 |
44 | var contentResponses = new List();
45 |
46 | #if NET472
47 | var fixture = new RateLimitedHttpMessageHandler(innerHandler, Priority.UserInitiated, cacheResultFunc: async (rq, re, key, ct) => contentResponses.Add(await re.Content.ReadAsByteArrayAsync()));
48 | #else
49 | var fixture = new RateLimitedHttpMessageHandler(innerHandler, Priority.UserInitiated, cacheResultFunc: async (rq, re, key, ct) => contentResponses.Add(await re.Content.ReadAsByteArrayAsync(ct)));
50 | #endif
51 |
52 | var client = new HttpClient(fixture);
53 | var str = await client.GetStringAsync(new Uri("http://lol/bar"));
54 |
55 | Assert.Equal("foo", str);
56 | Assert.Equal(1, contentResponses.Count);
57 | Assert.Equal(3, contentResponses[0].Length);
58 | }
59 |
60 | ///
61 | /// Checks to make sure that the cache preserves the http headers.
62 | ///
63 | /// A task to monitor the progress.
64 | [Fact]
65 | public async Task CachingFunctionShouldPreserveHeaders()
66 | {
67 | var innerHandler = new TestHttpMessageHandler(_ =>
68 | {
69 | var ret = new HttpResponseMessage()
70 | {
71 | Content = new StringContent("foo", Encoding.UTF8),
72 | StatusCode = HttpStatusCode.OK,
73 | };
74 |
75 | ret.Headers.ETag = new("\"worifjw\"");
76 | return Observable.Return(ret);
77 | });
78 |
79 | var etagResponses = new List();
80 | var fixture = new RateLimitedHttpMessageHandler(innerHandler, Priority.UserInitiated, cacheResultFunc: (rq, re, key, ct) =>
81 | {
82 | etagResponses.Add(re.Headers.ETag.Tag);
83 | return Task.FromResult(true);
84 | });
85 |
86 | var client = new HttpClient(fixture);
87 | var resp = await client.GetAsync(new Uri("http://lol/bar"));
88 | Assert.Equal("\"worifjw\"", etagResponses[0]);
89 | }
90 |
91 | ///
92 | /// Does a round trip integration test.
93 | ///
94 | /// A task to monitor the progress.
95 | [Fact]
96 | public async Task RoundTripIntegrationTest()
97 | {
98 | var cache = new InMemoryBlobCache();
99 |
100 | var cachingHandler = new RateLimitedHttpMessageHandler(new HttpClientHandler(), Priority.UserInitiated, cacheResultFunc: async (rq, resp, key, ct) =>
101 | {
102 | #if NET472
103 | var data = await resp.Content.ReadAsByteArrayAsync();
104 | #else
105 | var data = await resp.Content.ReadAsByteArrayAsync(ct);
106 | #endif
107 | await cache.Insert(key, data);
108 | });
109 |
110 | var client = new HttpClient(cachingHandler);
111 | var origData = await client.GetStringAsync(new Uri("http://httpbin.org/get"));
112 |
113 | Assert.True(origData.Contains("origin"));
114 | Assert.Equal(1, (await cache.GetAllKeys()).Count());
115 |
116 | var offlineHandler = new OfflineHttpMessageHandler(async (rq, key, ct) => await cache.Get(key));
117 |
118 | client = new HttpClient(offlineHandler);
119 | var newData = await client.GetStringAsync(new Uri("http://httpbin.org/get"));
120 |
121 | Assert.Equal(origData, newData);
122 |
123 | bool shouldDie = true;
124 | try
125 | {
126 | await client.GetStringAsync(new Uri("http://httpbin.org/gzip"));
127 | }
128 | catch (Exception ex)
129 | {
130 | shouldDie = false;
131 | Console.WriteLine(ex);
132 | }
133 |
134 | Assert.False(shouldDie);
135 | }
136 |
137 | ///
138 | /// Checks that only relevant http methods are cached.
139 | ///
140 | /// The name of the method.
141 | /// If it should be cached or not.
142 | /// A task to monitor the progress.
143 | [Theory]
144 | [InlineData("GET", true)]
145 | [InlineData("HEAD", true)]
146 | [InlineData("OPTIONS", true)]
147 | [InlineData("POST", false)]
148 | [InlineData("DELETE", false)]
149 | [InlineData("PUT", false)]
150 | [InlineData("WHATEVER", false)]
151 | public async Task OnlyCacheRelevantMethods(string method, bool shouldCache)
152 | {
153 | var innerHandler = new TestHttpMessageHandler(_ =>
154 | {
155 | var ret = new HttpResponseMessage()
156 | {
157 | Content = new StringContent("foo", Encoding.UTF8),
158 | StatusCode = HttpStatusCode.OK,
159 | };
160 |
161 | return Observable.Return(ret);
162 | });
163 |
164 | var cached = false;
165 | var fixture = new RateLimitedHttpMessageHandler(innerHandler, Priority.UserInitiated, cacheResultFunc: (rq, re, key, ct) =>
166 | {
167 | cached = true;
168 | return Task.FromResult(true);
169 | });
170 |
171 | var client = new HttpClient(fixture);
172 | var request = new HttpRequestMessage(new(method), "http://lol/bar");
173 | await client.SendAsync(request);
174 |
175 | Assert.Equal(shouldCache, cached);
176 | }
177 | }
178 | }
179 |
--------------------------------------------------------------------------------
/src/Fusillade.Tests/Http/HttpSchedulerSharedTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Net;
10 | using System.Net.Http;
11 | using System.Net.Http.Headers;
12 | using System.Reactive;
13 | using System.Reactive.Concurrency;
14 | using System.Reactive.Linq;
15 | using System.Reactive.Subjects;
16 | using System.Reactive.Threading.Tasks;
17 | using System.Text;
18 | using System.Threading;
19 | using System.Threading.Tasks;
20 | using DynamicData;
21 | using Microsoft.Reactive.Testing;
22 | using ReactiveUI;
23 | using ReactiveUI.Testing;
24 | using Xunit;
25 |
26 | namespace Fusillade.Tests
27 | {
28 | ///
29 | /// Base class full of common requests.
30 | ///
31 | public abstract class HttpSchedulerSharedTests
32 | {
33 | ///
34 | /// Checks to make sure a dummy request is completed.
35 | ///
36 | /// A task to monitor the progress.
37 | [Fact]
38 | public async Task HttpSchedulerShouldCompleteADummyRequest()
39 | {
40 | var fixture = CreateFixture(new TestHttpMessageHandler(_ =>
41 | {
42 | var ret = new HttpResponseMessage()
43 | {
44 | Content = new StringContent("foo", Encoding.UTF8),
45 | StatusCode = HttpStatusCode.OK,
46 | };
47 |
48 | ret.Headers.ETag = new EntityTagHeaderValue("\"worifjw\"");
49 | return Observable.Return(ret);
50 | }));
51 |
52 | var client = new HttpClient(fixture)
53 | {
54 | BaseAddress = new Uri("http://example"),
55 | };
56 |
57 | var rq = new HttpRequestMessage(HttpMethod.Get, "/");
58 |
59 | var result = await client.SendAsync(rq).ToObservable()
60 | .Timeout(TimeSpan.FromSeconds(2.0), RxApp.TaskpoolScheduler);
61 |
62 | var bytes = await result.Content.ReadAsByteArrayAsync();
63 |
64 | Console.WriteLine(Encoding.UTF8.GetString(bytes));
65 | Assert.Equal(HttpStatusCode.OK, result.StatusCode);
66 | Assert.Equal(3 /*foo*/, bytes.Length);
67 | }
68 |
69 | ///
70 | /// Checks to make sure that the http scheduler doesn't do too much scheduling all at once.
71 | ///
72 | [Fact]
73 | public void HttpSchedulerShouldntScheduleLotsOfStuffAtOnce()
74 | {
75 | var blockedRqs = new Dictionary>();
76 | var scheduledCount = default(int);
77 | var completedCount = default(int);
78 |
79 | new TestScheduler().WithAsync(_ =>
80 | {
81 | var fixture = CreateFixture(new TestHttpMessageHandler(rq =>
82 | {
83 | scheduledCount++;
84 | var ret = new HttpResponseMessage()
85 | {
86 | Content = new StringContent("foo", Encoding.UTF8),
87 | StatusCode = HttpStatusCode.OK,
88 | };
89 |
90 | ret.Headers.ETag = new EntityTagHeaderValue("\"worifjw\"");
91 |
92 | blockedRqs[rq] = new Subject();
93 | return blockedRqs[rq].Select(_ => ret).Finally(() => completedCount++);
94 | }));
95 |
96 | var client = new HttpClient(fixture)
97 | {
98 | BaseAddress = new Uri("http://example")
99 | };
100 |
101 | var rqs =
102 | Enumerable
103 | .Range(0, 5)
104 | .Select(x => new HttpRequestMessage(HttpMethod.Get, "/" + x))
105 | .ToArray();
106 |
107 | rqs.ToObservable()
108 | .Select(rq => client.SendAsync(rq))
109 | .Merge()
110 | .ToObservableChangeSet()
111 | .ObserveOn(ImmediateScheduler.Instance)
112 | .Bind(out var results)
113 | .Subscribe();
114 |
115 | Assert.Equal(4, scheduledCount);
116 | Assert.Equal(0, completedCount);
117 |
118 | var firstSubj = blockedRqs.First().Value;
119 | firstSubj.OnNext(Unit.Default);
120 | firstSubj.OnCompleted();
121 |
122 | Assert.Equal(5, scheduledCount);
123 | Assert.Equal(1, completedCount);
124 |
125 | foreach (var v in blockedRqs.Values)
126 | {
127 | v.OnNext(Unit.Default);
128 | v.OnCompleted();
129 | }
130 |
131 | Assert.Equal(5, scheduledCount);
132 | Assert.Equal(5, completedCount);
133 |
134 | return Task.CompletedTask;
135 | });
136 | }
137 |
138 | ///
139 | /// Checks to make sure that the rate limited scheduler stops after content limit has been reached.
140 | ///
141 | /// A task to monitor the progress.
142 | [Fact]
143 | public async Task RateLimitedSchedulerShouldStopAfterContentLimitReached()
144 | {
145 | var fixture = CreateFixture(new TestHttpMessageHandler(_ =>
146 | {
147 | var ret = new HttpResponseMessage()
148 | {
149 | Content = new StringContent("foo", Encoding.UTF8),
150 | StatusCode = HttpStatusCode.OK,
151 | };
152 |
153 | ret.Headers.ETag = new EntityTagHeaderValue("\"worifjw\"");
154 | return Observable.Return(ret);
155 | }));
156 |
157 | var client = new HttpClient(fixture)
158 | {
159 | BaseAddress = new Uri("http://example"),
160 | };
161 |
162 | fixture.ResetLimit(5);
163 |
164 | // Under the limit => succeed
165 | var rq = new HttpRequestMessage(HttpMethod.Get, "/");
166 | var resp = await client.SendAsync(rq);
167 | Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
168 |
169 | // Crossing the limit => succeed
170 | rq = new HttpRequestMessage(HttpMethod.Get, "/");
171 | resp = await client.SendAsync(rq);
172 | Assert.Equal(HttpStatusCode.OK, resp.StatusCode);
173 |
174 | // Over the limit => cancelled
175 | rq = new HttpRequestMessage(HttpMethod.Get, "/");
176 | await Assert.ThrowsAsync(() => client.SendAsync(rq));
177 | }
178 |
179 | ///
180 | /// Tests to make sure that concurrent requests aren't debounced.
181 | ///
182 | /// A task to monitor the progress.
183 | [Fact]
184 | public async Task ConcurrentRequestsToTheSameResourceAreDebounced()
185 | {
186 | int messageCount = 0;
187 | Subject gate = new();
188 |
189 | var fixture = CreateFixture(new TestHttpMessageHandler(_ =>
190 | {
191 | var ret = new HttpResponseMessage()
192 | {
193 | Content = new StringContent("foo", Encoding.UTF8),
194 | StatusCode = HttpStatusCode.OK,
195 | };
196 |
197 | ret.Headers.ETag = new EntityTagHeaderValue("\"worifjw\"");
198 | messageCount++;
199 |
200 | return gate.Take(1).Select(__ => ret);
201 | }));
202 |
203 | var client = new HttpClient(fixture)
204 | {
205 | BaseAddress = new Uri("http://example"),
206 | };
207 |
208 | var rq1 = new HttpRequestMessage(HttpMethod.Get, "/");
209 | var rq2 = new HttpRequestMessage(HttpMethod.Get, "/");
210 |
211 | Assert.Equal(0, messageCount);
212 |
213 | var resp1Task = client.SendAsync(rq1);
214 | var resp2Task = client.SendAsync(rq2);
215 | Assert.Equal(1, messageCount);
216 |
217 | gate.OnNext(Unit.Default);
218 | gate.OnNext(Unit.Default);
219 |
220 | var resp1 = await resp1Task;
221 | var resp2 = await resp2Task;
222 |
223 | Assert.Equal(HttpStatusCode.OK, resp1.StatusCode);
224 | Assert.Equal(HttpStatusCode.OK, resp2.StatusCode);
225 | Assert.Equal(1, messageCount);
226 | }
227 |
228 | ///
229 | /// Checks to make sure that requests don't get unfairly cancelled.
230 | ///
231 | /// A task to monitor the progress.
232 | [Fact]
233 | public async Task DebouncedRequestsDontGetUnfairlyCancelled()
234 | {
235 | int messageCount = 0;
236 | Subject gate = new();
237 |
238 | var fixture = CreateFixture(new TestHttpMessageHandler(_ =>
239 | {
240 | var ret = new HttpResponseMessage()
241 | {
242 | Content = new StringContent("foo", Encoding.UTF8),
243 | StatusCode = HttpStatusCode.OK,
244 | };
245 |
246 | ret.Headers.ETag = new EntityTagHeaderValue("\"worifjw\"");
247 | messageCount++;
248 |
249 | return gate.Take(1).Select(__ => ret);
250 | }));
251 |
252 | var client = new HttpClient(fixture)
253 | {
254 | BaseAddress = new Uri("http://example"),
255 | };
256 |
257 | var rq1 = new HttpRequestMessage(HttpMethod.Get, "/");
258 | var rq2 = new HttpRequestMessage(HttpMethod.Get, "/");
259 |
260 | Assert.Equal(0, messageCount);
261 |
262 | /* NB: Here's the thing we're testing for
263 | *
264 | * When we issue concurrent requests to the same resource, one of them
265 | * will actually do the request, and one of them will wait on the other.
266 | * In this case, rq1 will do the request, and rq2 will just return
267 | * whatever rq1 will return.
268 | *
269 | * The key then, is to only truly cancel rq1 if both rq1 *and* rq2
270 | * are cancelled, but rq1 should *appear* to be cancelled */
271 | var cts = new CancellationTokenSource();
272 |
273 | var resp1Task = client.SendAsync(rq1, cts.Token);
274 | var resp2Task = client.SendAsync(rq2);
275 | Assert.Equal(1, messageCount);
276 |
277 | cts.Cancel();
278 |
279 | gate.OnNext(Unit.Default);
280 | gate.OnNext(Unit.Default);
281 |
282 | await Assert.ThrowsAsync(() => resp1Task);
283 | var resp2 = await resp2Task;
284 |
285 | Assert.Equal(HttpStatusCode.OK, resp2.StatusCode);
286 | Assert.Equal(1, messageCount);
287 | }
288 |
289 | ///
290 | /// Checks to make sure that different paths aren't debounced.
291 | ///
292 | /// A task to monitor the progress.
293 | [Fact]
294 | public async Task RequestsToDifferentPathsArentDebounced()
295 | {
296 | int messageCount = 0;
297 | Subject gate = new();
298 |
299 | var fixture = CreateFixture(new TestHttpMessageHandler(_ =>
300 | {
301 | var ret = new HttpResponseMessage()
302 | {
303 | Content = new StringContent("foo", Encoding.UTF8),
304 | StatusCode = HttpStatusCode.OK,
305 | };
306 |
307 | ret.Headers.ETag = new EntityTagHeaderValue("\"worifjw\"");
308 | messageCount++;
309 |
310 | return gate.Take(1).Select(__ => ret);
311 | }));
312 |
313 | var client = new HttpClient(fixture)
314 | {
315 | BaseAddress = new Uri("http://example"),
316 | };
317 |
318 | var rq1 = new HttpRequestMessage(HttpMethod.Get, "/foo");
319 | var rq2 = new HttpRequestMessage(HttpMethod.Get, "/bar");
320 |
321 | Assert.Equal(0, messageCount);
322 |
323 | var resp1Task = client.SendAsync(rq1);
324 | var resp2Task = client.SendAsync(rq2);
325 | Assert.Equal(2, messageCount);
326 |
327 | gate.OnNext(Unit.Default);
328 | gate.OnNext(Unit.Default);
329 |
330 | var resp1 = await resp1Task;
331 | var resp2 = await resp2Task;
332 |
333 | Assert.Equal(HttpStatusCode.OK, resp1.StatusCode);
334 | Assert.Equal(HttpStatusCode.OK, resp2.StatusCode);
335 | Assert.Equal(2, messageCount);
336 | }
337 |
338 | ///
339 | /// Tests if a debounce is fully cancelling requests.
340 | ///
341 | /// A task to monitor the progress.
342 | [Fact]
343 | public async Task FullyCancelledDebouncedRequestsGetForRealCancelled()
344 | {
345 | int messageCount = 0;
346 | int finalMessageCount = 0;
347 | Subject gate = new();
348 |
349 | var fixture = CreateFixture(new TestHttpMessageHandler(_ =>
350 | {
351 | var ret = new HttpResponseMessage()
352 | {
353 | Content = new StringContent("foo", Encoding.UTF8),
354 | StatusCode = HttpStatusCode.OK,
355 | };
356 |
357 | ret.Headers.ETag = new("\"worifjw\"");
358 | messageCount++;
359 |
360 | return gate.Take(1)
361 | .Do(__ => finalMessageCount++)
362 | .Select(__ => ret);
363 | }));
364 |
365 | var client = new HttpClient(fixture)
366 | {
367 | BaseAddress = new Uri("http://example"),
368 | };
369 |
370 | var rq1 = new HttpRequestMessage(HttpMethod.Get, "/");
371 | var rq2 = new HttpRequestMessage(HttpMethod.Get, "/");
372 |
373 | Assert.Equal(0, messageCount);
374 |
375 | /* NB: Here's the thing we're testing for
376 | *
377 | * When we issue concurrent requests to the same resource, one of them
378 | * will actually do the request, and one of them will wait on the other.
379 | * In this case, rq1 will do the request, and rq2 will just return
380 | * whatever rq1 will return.
381 | *
382 | * The key then, is to only truly cancel rq1 if both rq1 *and* rq2
383 | * are cancelled, but rq1 should *appear* to be cancelled. This test
384 | * cancels both requests then makes sure we actually cancel the
385 | * underlying result */
386 | var cts = new CancellationTokenSource();
387 |
388 | var resp1Task = client.SendAsync(rq1, cts.Token);
389 | var resp2Task = client.SendAsync(rq2, cts.Token);
390 | Assert.Equal(1, messageCount);
391 | Assert.Equal(0, finalMessageCount);
392 |
393 | cts.Cancel();
394 |
395 | gate.OnNext(Unit.Default);
396 | gate.OnNext(Unit.Default);
397 |
398 | await Assert.ThrowsAsync(() => resp1Task);
399 | await Assert.ThrowsAsync(() => resp2Task);
400 |
401 | Assert.Equal(1, messageCount);
402 | Assert.Equal(0, finalMessageCount);
403 | }
404 |
405 | ///
406 | /// Attempts to download a release from github to test the filters.
407 | ///
408 | /// A task to monitor the progress.
409 | [Fact]
410 | [Trait("Slow", "Very Yes")]
411 | public async Task DownloadARelease()
412 | {
413 | ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
414 |
415 | const string input = "https://github.com/akavache/Akavache/releases/download/3.2.0/Akavache.3.2.0.zip";
416 | var fixture = CreateFixture(new HttpClientHandler()
417 | {
418 | AllowAutoRedirect = true,
419 | MaxRequestContentBufferSize = 1048576 * 64,
420 | });
421 |
422 | var client = new HttpClient(fixture);
423 | var result = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, new Uri(input)));
424 | var bytes = await result.Content.ReadAsByteArrayAsync();
425 |
426 | Assert.True(result.IsSuccessStatusCode);
427 | Assert.Equal(8089690, bytes.Length);
428 | }
429 |
430 | ///
431 | /// Creates the test fixtures.
432 | ///
433 | /// The inner handler.
434 | /// The limiting handler.
435 | protected abstract LimitingHttpMessageHandler CreateFixture(HttpMessageHandler innerHandler = null);
436 | }
437 | }
438 |
--------------------------------------------------------------------------------
/src/Fusillade.Tests/Http/TestHttpMessageHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System;
7 | using System.Net.Http;
8 | using System.Reactive.Linq;
9 | using System.Reactive.Threading.Tasks;
10 | using System.Threading;
11 | using System.Threading.Tasks;
12 |
13 | namespace Fusillade.Tests
14 | {
15 | ///
16 | /// Tests the main http scheduler.
17 | ///
18 | ///
19 | /// Initializes a new instance of the class.
20 | ///
21 | /// Creates a http response.
22 | public class TestHttpMessageHandler(Func> createResult) : HttpMessageHandler
23 | {
24 | ///
25 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
26 | {
27 | if (cancellationToken.IsCancellationRequested)
28 | {
29 | return Observable.Throw(new OperationCanceledException()).ToTask();
30 | }
31 |
32 | return createResult(request).ToTask(cancellationToken);
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Fusillade.Tests/Http/fixtures/ResponseWithETag:
--------------------------------------------------------------------------------
1 | HTTP/1.1 200 OK
2 | Access-Control-Allow-Origin: *
3 | Content-Type: text/plain; charset=UTF-8
4 | Date: Tue, 24 Dec 2013 03:16:30 GMT
5 | Etag: "12345"
6 | Server: gunicorn/0.17.4
7 | Content-Length: 118
8 | Connection: keep-alive
9 |
10 | {
11 | "ETag": "12345",
12 | "Content-Type": "text/plain; charset=UTF-8",
13 | "Content-Length": "118",
14 | "Server": "httpbin"
15 | }
16 |
--------------------------------------------------------------------------------
/src/Fusillade.Tests/IntegrationTestHelper.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System;
7 | using System.IO;
8 | using System.Linq;
9 | using System.Net;
10 | using System.Net.Http;
11 | using System.Reactive.Linq;
12 | using System.Text;
13 |
14 | namespace Fusillade.Tests
15 | {
16 | ///
17 | /// A helper for performing integration tests.
18 | ///
19 | public static class IntegrationTestHelper
20 | {
21 | ///
22 | /// Combines together paths together and then gets a full path.
23 | ///
24 | /// The paths to combine.
25 | /// The string path.
26 | public static string GetPath(params string[] paths)
27 | {
28 | var ret = GetIntegrationTestRootDirectory();
29 | return new FileInfo(paths.Aggregate(ret, Path.Combine)).FullName;
30 | }
31 |
32 | ///
33 | /// Gets the root directory for the integration test.
34 | ///
35 | /// The path.
36 | public static string GetIntegrationTestRootDirectory()
37 | {
38 | // XXX: This is an evil hack, but it's okay for a unit test
39 | // We can't use Assembly.Location because unit test runners love
40 | // to move stuff to temp directories
41 | return Directory.GetParent(Directory.GetCurrentDirectory()).Parent.FullName;
42 | }
43 |
44 | ///
45 | /// Creates a response from a sample file with the data.
46 | ///
47 | /// The path to the file.
48 | /// The generated response.
49 | public static HttpResponseMessage GetResponse(params string[] paths)
50 | {
51 | var bytes = File.ReadAllBytes(GetPath(paths));
52 |
53 | // Find the body
54 | var bodyIndex = -1;
55 | for (bodyIndex = 0; bodyIndex < bytes.Length - 3; bodyIndex++)
56 | {
57 | if (bytes[bodyIndex] != 0x0D || bytes[bodyIndex + 1] != 0x0A ||
58 | bytes[bodyIndex + 2] != 0x0D || bytes[bodyIndex + 3] != 0x0A)
59 | {
60 | continue;
61 | }
62 |
63 | goto foundIt;
64 | }
65 |
66 | throw new Exception("Couldn't find response body");
67 |
68 | foundIt:
69 |
70 | var headerText = Encoding.UTF8.GetString(bytes, 0, bodyIndex);
71 | var lines = headerText.Split('\n');
72 | var statusCode = (HttpStatusCode)int.Parse(lines[0].Split(' ')[1]);
73 | var ret = new HttpResponseMessage(statusCode);
74 |
75 | ret.Content = new ByteArrayContent(bytes, bodyIndex + 2, bytes.Length - bodyIndex - 2);
76 |
77 | foreach (var line in lines.Skip(1))
78 | {
79 | var separatorIndex = line.IndexOf(':');
80 | var key = line.Substring(0, separatorIndex);
81 | var val = line.Substring(separatorIndex + 2).TrimEnd();
82 |
83 | if (string.IsNullOrWhiteSpace(line))
84 | {
85 | continue;
86 | }
87 |
88 | ret.Headers.TryAddWithoutValidation(key, val);
89 | ret.Content.Headers.TryAddWithoutValidation(key, val);
90 | }
91 |
92 | return ret;
93 | }
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/Fusillade.Tests/NetCacheTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using Xunit;
7 |
8 | namespace Fusillade.Tests
9 | {
10 | ///
11 | /// Checks to make sure that the NetCache operates correctly.
12 | ///
13 | [CollectionDefinition(nameof(NetCacheTests), DisableParallelization = true)]
14 | public class NetCacheTests
15 | {
16 | ///
17 | /// Verifies that we are registering the default handlers correctly.
18 | ///
19 | [Fact]
20 | public void DefaultValuesShouldBeRegistered()
21 | {
22 | Assert.NotNull(NetCache.Speculative);
23 | Assert.NotNull(NetCache.UserInitiated);
24 | Assert.NotNull(NetCache.Background);
25 | Assert.NotNull(NetCache.Offline);
26 | Assert.NotNull(NetCache.OperationQueue);
27 | Assert.Null(NetCache.RequestCache);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/Fusillade.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # 17
4 | VisualStudioVersion = 17.7.34031.279
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fusillade", "Fusillade\Fusillade.csproj", "{26493C47-6A4A-4F2A-9F92-046AA8CD95CC}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Fusillade.Tests", "Fusillade.Tests\Fusillade.Tests.csproj", "{BA0745E4-4566-4655-B83C-B4398F67DC39}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6B92A58B-DD1B-4AFF-A3CA-D51CBEB7B43D}"
11 | ProjectSection(SolutionItems) = preProject
12 | ..\.gitattributes = ..\.gitattributes
13 | ..\.gitignore = ..\.gitignore
14 | analyzers.ruleset = analyzers.ruleset
15 | ..\.github\workflows\ci-build.yml = ..\.github\workflows\ci-build.yml
16 | ..\CODE_OF_CONDUCT.md = ..\CODE_OF_CONDUCT.md
17 | ..\CONTRIBUTING.md = ..\CONTRIBUTING.md
18 | Directory.build.props = Directory.build.props
19 | Directory.build.targets = Directory.build.targets
20 | ..\LICENSE = ..\LICENSE
21 | ..\README.md = ..\README.md
22 | ..\.github\workflows\release.yml = ..\.github\workflows\release.yml
23 | stylecop.json = stylecop.json
24 | ..\version.json = ..\version.json
25 | EndProjectSection
26 | EndProject
27 | Global
28 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
29 | Debug|Any CPU = Debug|Any CPU
30 | Release|Any CPU = Release|Any CPU
31 | EndGlobalSection
32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
33 | {26493C47-6A4A-4F2A-9F92-046AA8CD95CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
34 | {26493C47-6A4A-4F2A-9F92-046AA8CD95CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
35 | {26493C47-6A4A-4F2A-9F92-046AA8CD95CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
36 | {26493C47-6A4A-4F2A-9F92-046AA8CD95CC}.Release|Any CPU.Build.0 = Release|Any CPU
37 | {BA0745E4-4566-4655-B83C-B4398F67DC39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38 | {BA0745E4-4566-4655-B83C-B4398F67DC39}.Debug|Any CPU.Build.0 = Debug|Any CPU
39 | {BA0745E4-4566-4655-B83C-B4398F67DC39}.Release|Any CPU.ActiveCfg = Release|Any CPU
40 | {BA0745E4-4566-4655-B83C-B4398F67DC39}.Release|Any CPU.Build.0 = Release|Any CPU
41 | EndGlobalSection
42 | GlobalSection(SolutionProperties) = preSolution
43 | HideSolutionNode = FALSE
44 | EndGlobalSection
45 | GlobalSection(ExtensibilityGlobals) = postSolution
46 | SolutionGuid = {96AB3D31-3E93-4E65-90D6-571A60C269E0}
47 | EndGlobalSection
48 | EndGlobal
49 |
--------------------------------------------------------------------------------
/src/Fusillade/ConcatenateMixin.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Linq;
9 | using System.Reactive.Linq;
10 | using System.Text;
11 |
12 | namespace Fusillade;
13 |
14 | internal static class ConcatenateMixin
15 | {
16 | public static string ConcatenateAll(this IEnumerable enumerables, Func selector, char separator = '|') =>
17 | enumerables.Aggregate(new StringBuilder(), (acc, x) =>
18 | {
19 | acc.Append(selector(x)).Append(separator);
20 | return acc;
21 | }).ToString();
22 | }
23 |
--------------------------------------------------------------------------------
/src/Fusillade/Fusillade.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | netstandard2.0;net6.0;net8.0
4 | fusillade
5 | enable
6 | CS8625;CS8604;CS8600;CS8614;CS8603;CS8618;CS8619
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/Fusillade/IRequestCache.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System.Net.Http;
7 | using System.Threading;
8 | using System.Threading.Tasks;
9 |
10 | namespace Fusillade;
11 |
12 | ///
13 | /// This Interface is a simple cache for HTTP requests - it is intentionally
14 | /// *not* designed to conform to HTTP caching rules since you most likely want
15 | /// to override those rules in a client app anyways.
16 | ///
17 | public interface IRequestCache
18 | {
19 | ///
20 | /// Implement this method by saving the Body of the response. The
21 | /// response is already downloaded as a ByteArrayContent so you don't
22 | /// have to worry about consuming the stream.
23 | ///
24 | /// The originating request.
25 | /// The response whose body you should save.
26 | /// A unique key used to identify the request details.
27 | /// Cancellation token.
28 | /// Completion.
29 | Task Save(HttpRequestMessage request, HttpResponseMessage response, string key, CancellationToken ct);
30 |
31 | ///
32 | /// Implement this by loading the Body of the given request / key.
33 | ///
34 | /// The originating request.
35 | /// A unique key used to identify the request details,
36 | /// that was given in Save().
37 | /// Cancellation token.
38 | /// The Body of the given request, or null if the search
39 | /// completed successfully but the response was not found.
40 | Task Fetch(HttpRequestMessage request, string key, CancellationToken ct);
41 | }
42 |
--------------------------------------------------------------------------------
/src/Fusillade/InflightRequest.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System;
7 | using System.Net.Http;
8 | using System.Reactive.Subjects;
9 | using System.Threading;
10 |
11 | namespace Fusillade;
12 |
13 | internal class InflightRequest(Action onFullyCancelled)
14 | {
15 | private int _refCount = 1;
16 |
17 | public AsyncSubject Response { get; protected set; } = new AsyncSubject();
18 |
19 | public void AddRef() => Interlocked.Increment(ref _refCount);
20 |
21 | public void Cancel()
22 | {
23 | if (Interlocked.Decrement(ref _refCount) <= 0)
24 | {
25 | onFullyCancelled();
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Fusillade/LimitingHttpMessageHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System.Net.Http;
7 |
8 | namespace Fusillade;
9 |
10 | ///
11 | /// Limiting HTTP schedulers only allow a certain number of bytes to be
12 | /// read before cancelling all future requests. This is designed for
13 | /// reading data that may or may not be used by the user later, in order
14 | /// to improve response times should the user later request the data.
15 | ///
16 | public abstract class LimitingHttpMessageHandler : DelegatingHandler
17 | {
18 | ///
19 | /// Initializes a new instance of the class.
20 | ///
21 | /// A inner handler we will call to get the data.
22 | protected LimitingHttpMessageHandler(HttpMessageHandler innerHandler)
23 | : base(innerHandler)
24 | {
25 | }
26 |
27 | ///
28 | /// Initializes a new instance of the class.
29 | ///
30 | protected LimitingHttpMessageHandler()
31 | {
32 | }
33 |
34 | ///
35 | /// Resets the total limit of bytes to read. This is usually called
36 | /// when the app resumes from suspend, to indicate that we should
37 | /// fetch another set of data.
38 | ///
39 | /// The maximum number of bytes to read.
40 | public abstract void ResetLimit(long? maxBytesToRead = null);
41 | }
42 |
--------------------------------------------------------------------------------
/src/Fusillade/NetCache.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System;
7 | using System.Diagnostics.CodeAnalysis;
8 | using System.Net.Http;
9 | using Punchclock;
10 | using Splat;
11 |
12 | namespace Fusillade;
13 |
14 | ///
15 | /// Handles caching for our Http requests.
16 | ///
17 | public static class NetCache
18 | {
19 | private static LimitingHttpMessageHandler speculative;
20 | [ThreadStatic]
21 | private static LimitingHttpMessageHandler? unitTestSpeculative;
22 | private static HttpMessageHandler userInitiated;
23 | [ThreadStatic]
24 | private static HttpMessageHandler? unitTestUserInitiated;
25 | private static HttpMessageHandler background;
26 | [ThreadStatic]
27 | private static HttpMessageHandler? unitTestBackground;
28 | private static HttpMessageHandler offline;
29 | [ThreadStatic]
30 | private static HttpMessageHandler? unitTestOffline;
31 | private static OperationQueue operationQueue = new(4);
32 | [ThreadStatic]
33 | private static OperationQueue? unitTestOperationQueue;
34 | private static IRequestCache? requestCache;
35 | [ThreadStatic]
36 | private static IRequestCache? unitTestRequestCache;
37 |
38 | ///
39 | /// Initializes static members of the class.
40 | ///
41 | static NetCache()
42 | {
43 | var innerHandler = Locator.Current.GetService() ?? new HttpClientHandler();
44 |
45 | // NB: In vNext this value will be adjusted based on the user's
46 | // network connection, but that requires us to go fully platformy
47 | // like Splat.
48 | speculative = new RateLimitedHttpMessageHandler(innerHandler, Priority.Speculative, 0, 1048576 * 5);
49 | userInitiated = new RateLimitedHttpMessageHandler(innerHandler, Priority.UserInitiated, 0);
50 | background = new RateLimitedHttpMessageHandler(innerHandler, Priority.Background, 0);
51 | offline = new OfflineHttpMessageHandler(null);
52 | }
53 |
54 | ///
55 | /// Gets or sets a handler of that allow a certain number of bytes to be
56 | /// read before cancelling all future requests. This is designed for
57 | /// reading data that may or may not be used by the user later, in order
58 | /// to improve response times should the user later request the data.
59 | ///
60 | public static LimitingHttpMessageHandler Speculative
61 | {
62 | get => unitTestSpeculative ?? Locator.Current.GetService("Speculative") ?? speculative;
63 | set
64 | {
65 | if (ModeDetector.InUnitTestRunner())
66 | {
67 | unitTestSpeculative = value;
68 | speculative ??= value;
69 | }
70 | else
71 | {
72 | speculative = value;
73 | }
74 | }
75 | }
76 |
77 | ///
78 | /// Gets or sets a scheduler that should be used for requests initiated by a user
79 | /// action such as clicking an item, they have the highest priority.
80 | ///
81 | public static HttpMessageHandler UserInitiated
82 | {
83 | get => unitTestUserInitiated ?? Locator.Current.GetService("UserInitiated") ?? userInitiated;
84 | set
85 | {
86 | if (ModeDetector.InUnitTestRunner())
87 | {
88 | unitTestUserInitiated = value;
89 | userInitiated ??= value;
90 | }
91 | else
92 | {
93 | userInitiated = value;
94 | }
95 | }
96 | }
97 |
98 | ///
99 | /// Gets or sets a scheduler that should be used for requests initiated in the
100 | /// background, and are scheduled at a lower priority.
101 | ///
102 | public static HttpMessageHandler Background
103 | {
104 | get => unitTestBackground ?? Locator.Current.GetService("Background") ?? background;
105 | set
106 | {
107 | if (ModeDetector.InUnitTestRunner())
108 | {
109 | unitTestBackground = value;
110 | background ??= value;
111 | }
112 | else
113 | {
114 | background = value;
115 | }
116 | }
117 | }
118 |
119 | ///
120 | /// Gets or sets a scheduler that fetches results solely from the cache specified in
121 | /// RequestCache.
122 | ///
123 | public static HttpMessageHandler Offline
124 | {
125 | get => unitTestOffline ?? Locator.Current.GetService("Offline") ?? offline;
126 | set
127 | {
128 | if (ModeDetector.InUnitTestRunner())
129 | {
130 | unitTestOffline = value;
131 | offline ??= value;
132 | }
133 | else
134 | {
135 | offline = value;
136 | }
137 | }
138 | }
139 |
140 | ///
141 | /// Gets or sets a scheduler that should be used for requests initiated in the
142 | /// operationQueue, and are scheduled at a lower priority. You don't
143 | /// need to mess with this.
144 | ///
145 | public static OperationQueue OperationQueue
146 | {
147 | get => unitTestOperationQueue ?? Locator.Current.GetService("OperationQueue") ?? operationQueue;
148 | set
149 | {
150 | if (ModeDetector.InUnitTestRunner())
151 | {
152 | unitTestOperationQueue = value;
153 | operationQueue ??= value;
154 | }
155 | else
156 | {
157 | operationQueue = value;
158 | }
159 | }
160 | }
161 |
162 | ///
163 | /// Gets or sets a request cache that if set indicates that HTTP handlers should save and load
164 | /// requests from a cached source.
165 | ///
166 | public static IRequestCache? RequestCache
167 | {
168 | get => unitTestRequestCache ?? requestCache;
169 | set
170 | {
171 | if (ModeDetector.InUnitTestRunner())
172 | {
173 | unitTestRequestCache = value;
174 | requestCache ??= value;
175 | }
176 | else
177 | {
178 | requestCache = value;
179 | }
180 | }
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/src/Fusillade/OfflineHttpMessageHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System;
7 | using System.Net;
8 | using System.Net.Http;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 |
12 | namespace Fusillade;
13 |
14 | ///
15 | /// A http handler that will make a response even if the HttpClient is offline.
16 | ///
17 | ///
18 | /// Initializes a new instance of the class.
19 | ///
20 | /// A function that will retrieve a body.
21 | public class OfflineHttpMessageHandler(Func>? retrieveBodyFunc) : HttpMessageHandler
22 | {
23 | ///
24 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
25 | {
26 | var retrieveBody = retrieveBodyFunc;
27 | if (retrieveBody == null && NetCache.RequestCache != null)
28 | {
29 | retrieveBody = NetCache.RequestCache.Fetch;
30 | }
31 |
32 | if (retrieveBody == null)
33 | {
34 | throw new Exception("Configure NetCache.RequestCache before calling this!");
35 | }
36 |
37 | var body = await retrieveBody(request, RateLimitedHttpMessageHandler.UniqueKeyForRequest(request), cancellationToken).ConfigureAwait(false);
38 | if (body == null)
39 | {
40 | return new HttpResponseMessage(HttpStatusCode.ServiceUnavailable);
41 | }
42 |
43 | var byteContent = new ByteArrayContent(body);
44 | return new HttpResponseMessage(HttpStatusCode.OK) { Content = byteContent };
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Fusillade/Priority.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | namespace Fusillade;
7 |
8 | ///
9 | /// This enumeration defines the default base priorities associated with the
10 | /// different NetCache instances.
11 | ///
12 | public enum Priority
13 | {
14 | ///
15 | /// This is a explicit task.
16 | ///
17 | Explicit = 0,
18 |
19 | ///
20 | /// A speculative priority where we aren't sure.
21 | ///
22 | Speculative = 10,
23 |
24 | ///
25 | /// This is background based task.
26 | ///
27 | Background = 20,
28 |
29 | ///
30 | /// This is a instance which is initiated by the user.
31 | ///
32 | UserInitiated = 100,
33 | }
34 |
--------------------------------------------------------------------------------
/src/Fusillade/RateLimitedHttpMessageHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2023 .NET Foundation and Contributors. All rights reserved.
2 | // Licensed to the .NET Foundation under one or more agreements.
3 | // The .NET Foundation licenses this file to you under the MIT license.
4 | // See the LICENSE file in the project root for full license information.
5 |
6 | using System;
7 | using System.Collections.Generic;
8 | using System.Diagnostics.CodeAnalysis;
9 | using System.Globalization;
10 | using System.IO;
11 | using System.Linq;
12 | using System.Net.Http;
13 | using System.Reactive.Linq;
14 | using System.Reactive.Threading.Tasks;
15 | using System.Text;
16 | using System.Threading;
17 | using System.Threading.Tasks;
18 | using Punchclock;
19 |
20 | namespace Fusillade;
21 |
22 | ///
23 | /// A http handler which will limit the rate at which we can read.
24 | ///
25 | ///
26 | /// Initializes a new instance of the class.
27 | ///
28 | /// The handler we are wrapping.
29 | /// The base priority of the request.
30 | /// The priority of the request.
31 | /// The maximum number of bytes we can read.
32 | /// The operation queue on which to run the operation.
33 | /// A method that is called if we need to get cached results.
34 | public class RateLimitedHttpMessageHandler(HttpMessageHandler handler, Priority basePriority, int priority = 0, long? maxBytesToRead = null, OperationQueue? opQueue = null, Func? cacheResultFunc = null) : LimitingHttpMessageHandler(handler)
35 | {
36 | private readonly int _priority = (int)basePriority + priority;
37 | private readonly Dictionary _inflightResponses = new();
38 | private long? _maxBytesToRead = maxBytesToRead;
39 |
40 | ///
41 | /// Generates a unique key for a .
42 | /// This assists with the caching.
43 | ///
44 | /// The request to generate a unique key for.
45 | /// The unique key.
46 | public static string UniqueKeyForRequest(HttpRequestMessage request)
47 | {
48 | if (request is null)
49 | {
50 | throw new ArgumentNullException(nameof(request));
51 | }
52 |
53 | var ret = new[]
54 | {
55 | request.RequestUri!.ToString(),
56 | request.Method.Method,
57 | request.Headers.Accept.ConcatenateAll(x => x.CharSet + x.MediaType),
58 | request.Headers.AcceptEncoding.ConcatenateAll(x => x.Value),
59 | (request.Headers.Referrer ?? new Uri("http://example")).AbsoluteUri,
60 | request.Headers.UserAgent.ConcatenateAll(x => x.Product != null ? x.Product.ToString() : x.Comment!),
61 | }.Aggregate(
62 | new StringBuilder(),
63 | (acc, x) =>
64 | {
65 | acc.AppendLine(x);
66 | return acc;
67 | });
68 |
69 | if (request.Headers.Authorization != null)
70 | {
71 | ret.Append(request.Headers.Authorization.Parameter).AppendLine(request.Headers.Authorization.Scheme);
72 | }
73 |
74 | return "HttpSchedulerCache_" + ret.ToString().GetHashCode().ToString("x", CultureInfo.InvariantCulture);
75 | }
76 |
77 | ///
78 | public override void ResetLimit(long? maxBytesToRead = null) => _maxBytesToRead = maxBytesToRead;
79 |
80 | ///
81 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
82 | {
83 | if (request is null)
84 | {
85 | throw new ArgumentNullException(nameof(request));
86 | }
87 |
88 | var method = request.Method;
89 | if (method != HttpMethod.Get && method != HttpMethod.Head && method != HttpMethod.Options)
90 | {
91 | return base.SendAsync(request, cancellationToken);
92 | }
93 |
94 | var cacheResult = cacheResultFunc;
95 | if (cacheResult == null && NetCache.RequestCache != null)
96 | {
97 | cacheResult = NetCache.RequestCache.Save;
98 | }
99 |
100 | if (_maxBytesToRead < 0)
101 | {
102 | var tcs = new TaskCompletionSource();
103 | #if NETSTANDARD2_0
104 | tcs.SetCanceled();
105 | #else
106 | tcs.SetCanceled(cancellationToken);
107 | #endif
108 | return tcs.Task;
109 | }
110 |
111 | var key = UniqueKeyForRequest(request);
112 | var realToken = new CancellationTokenSource();
113 | var ret = new InflightRequest(() =>
114 | {
115 | lock (_inflightResponses)
116 | {
117 | _inflightResponses.Remove(key);
118 | }
119 |
120 | realToken.Cancel();
121 | });
122 |
123 | lock (_inflightResponses)
124 | {
125 | if (_inflightResponses.ContainsKey(key))
126 | {
127 | var val = _inflightResponses[key];
128 | val.AddRef();
129 | cancellationToken.Register(val.Cancel);
130 |
131 | return val.Response.ToTask(cancellationToken);
132 | }
133 |
134 | _inflightResponses[key] = ret;
135 | }
136 |
137 | cancellationToken.Register(ret.Cancel);
138 |
139 | var queue = opQueue ?? NetCache.OperationQueue;
140 |
141 | queue.Enqueue(
142 | _priority,
143 | null!,
144 | async () =>
145 | {
146 | try
147 | {
148 | var resp = await base.SendAsync(request, realToken.Token).ConfigureAwait(false);
149 |
150 | if (_maxBytesToRead != null && resp.Content?.Headers.ContentLength != null)
151 | {
152 | _maxBytesToRead -= resp.Content.Headers.ContentLength;
153 | }
154 |
155 | if (cacheResult != null && resp.Content != null)
156 | {
157 | var ms = new MemoryStream();
158 | #if NET5_0_OR_GREATER
159 | var stream = await resp.Content.ReadAsStreamAsync(realToken.Token).ConfigureAwait(false);
160 | #else
161 | var stream = await resp.Content.ReadAsStreamAsync().ConfigureAwait(false);
162 | #endif
163 | await stream.CopyToAsync(ms, 32 * 1024, realToken.Token).ConfigureAwait(false);
164 |
165 | realToken.Token.ThrowIfCancellationRequested();
166 |
167 | var newResp = new HttpResponseMessage();
168 | foreach (var kvp in resp.Headers)
169 | {
170 | newResp.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value);
171 | }
172 |
173 | var newContent = new ByteArrayContent(ms.ToArray());
174 | foreach (var kvp in resp.Content.Headers)
175 | {
176 | newContent.Headers.TryAddWithoutValidation(kvp.Key, kvp.Value);
177 | }
178 |
179 | newResp.Content = newContent;
180 |
181 | resp = newResp;
182 | await cacheResult(request, resp, key, realToken.Token).ConfigureAwait(false);
183 | }
184 |
185 | return resp;
186 | }
187 | finally
188 | {
189 | lock (_inflightResponses)
190 | {
191 | _inflightResponses.Remove(key);
192 | }
193 | }
194 | },
195 | realToken.Token).ToObservable().Subscribe(ret.Response);
196 |
197 | return ret.Response.ToTask(cancellationToken);
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/src/analyzers.ruleset:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
231 |
232 |
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 |
--------------------------------------------------------------------------------
/src/stylecop.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
3 | "settings": {
4 | "indentation": {
5 | "useTabs": false,
6 | "indentationSize": 4
7 | },
8 | "documentationRules": {
9 | "documentExposedElements": true,
10 | "documentInternalElements": false,
11 | "documentPrivateElements": false,
12 | "documentInterfaces": true,
13 | "documentPrivateFields": false,
14 | "documentationCulture": "en-US",
15 | "companyName": ".NET Foundation and Contributors",
16 | "copyrightText": "Copyright (c) 2023 {companyName}. All rights reserved.\nLicensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the {licenseName} license.\nSee the {licenseFile} file in the project root for full license information.",
17 | "variables": {
18 | "licenseName": "MIT",
19 | "licenseFile": "LICENSE"
20 | },
21 | "xmlHeader": false
22 | },
23 | "layoutRules": {
24 | "newlineAtEndOfFile": "allow",
25 | "allowConsecutiveUsings": true
26 | },
27 | "maintainabilityRules": {
28 | "topLevelTypes": [
29 | "class",
30 | "interface",
31 | "struct",
32 | "enum",
33 | "delegate"
34 | ]
35 | },
36 | "orderingRules": {
37 | "usingDirectivesPlacement": "outsideNamespace",
38 | "systemUsingDirectivesFirst": true
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/version.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2.6",
3 | "publicReleaseRefSpec": [
4 | "^refs/heads/main", // we release out of master
5 | "^refs/heads/rel/\\d+\\.\\d+\\.\\d+" // we also release branches starting with rel/N.N.N
6 | ],
7 | "nugetPackageVersion":{
8 | "semVer": 2
9 | },
10 | "cloudBuild": {
11 | "setVersionVariables": true,
12 | "buildNumber": {
13 | "enabled": false
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------