├── .editorconfig ├── .github └── workflows │ └── CI.yml ├── .gitignore ├── AmbientTasks.sln ├── AmbientTasks.sln.DotSettings ├── CHANGELOG.md ├── LICENSE.txt ├── Readme.md ├── build.ps1 ├── build ├── CiServerIntegration.ps1 ├── Get-DetectedCiVersion.ps1 ├── SignTool.ps1 └── ValidateMetadata.ps1 └── src ├── AmbientTasks.Tests ├── .editorconfig ├── AmbientTasks.Tests.csproj ├── AmbientTasksAddFuncOverloadTests.cs ├── AmbientTasksPostTests.cs ├── AmbientTasksTests.cs ├── CallbackWatcher.cs ├── On.cs ├── PreventExecutionContextLeaksAttribute.cs ├── RequireOnAllTestMethodsAttribute.cs ├── SynchronizationContextAssert.cs └── Utils.cs ├── AmbientTasks.snk ├── AmbientTasks ├── AmbientTasks.AmbientTaskContext.cs ├── AmbientTasks.PostClosure.cs ├── AmbientTasks.cs └── AmbientTasks.csproj └── Directory.Build.props /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | [*.cs] 9 | indent_style = space 10 | indent_size = 4 11 | 12 | [*.{sln,*proj,dotsettings}] 13 | charset = utf-8-bom 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | [*] 19 | csharp_indent_case_contents_when_block = false 20 | dotnet_style_collection_initializer = true:silent 21 | csharp_style_conditional_delegate_call = true:error 22 | csharp_style_deconstructed_variable_declaration = true:silent 23 | dotnet_style_object_initializer = true:silent 24 | dotnet_sort_system_directives_first = true 25 | dotnet_code_quality_unused_parameters = all:silent 26 | dotnet_style_explicit_tuple_names = true:error 27 | dotnet_style_predefined_type_for_locals_parameters_members = true:error 28 | dotnet_style_predefined_type_for_member_access = true:error 29 | dotnet_style_readonly_field = true:error 30 | csharp_style_var_elsewhere = true:silent 31 | csharp_style_var_for_built_in_types = true:silent 32 | csharp_style_var_when_type_is_apparent = true:silent 33 | 34 | # Override ReSharper defaults 35 | csharp_space_after_cast = false 36 | resharper_csharp_space_within_single_line_array_initializer_braces = true # https://www.jetbrains.com/help/resharper/EditorConfig_CSHARP_SpacesPageSchema.html#resharper_csharp_space_within_single_line_array_initializer_braces 37 | 38 | # The first matching rule wins, more specific rules at the top 39 | # dotnet_naming_rule.*.symbols does not yet support a comma-separated list https://github.com/dotnet/roslyn/issues/20891 40 | # dotnet_naming_symbols.*.applicable_kinds does not yet support namespace, type_parameter or local https://github.com/dotnet/roslyn/issues/18121 41 | 42 | dotnet_naming_style.interfaces.required_prefix = I 43 | dotnet_naming_style.interfaces.capitalization = pascal_case # Needed or VS ignores all naming rules https://github.com/dotnet/roslyn/issues/20895 44 | 45 | dotnet_naming_symbols.interfaces.applicable_kinds = interface 46 | dotnet_naming_rule.interfaces.severity = error 47 | dotnet_naming_rule.interfaces.symbols = interfaces 48 | dotnet_naming_rule.interfaces.style = interfaces 49 | 50 | 51 | dotnet_naming_style.pascal_case.capitalization = pascal_case 52 | 53 | dotnet_naming_symbols.namespaces_types_and_non_field_members.applicable_kinds = namespace, class, struct, enum, interface, delegate, type_parameter, method, property, event 54 | dotnet_naming_rule.namespaces_types_and_non_field_members.severity = warning 55 | dotnet_naming_rule.namespaces_types_and_non_field_members.symbols = namespaces_types_and_non_field_members 56 | dotnet_naming_rule.namespaces_types_and_non_field_members.style = pascal_case 57 | 58 | dotnet_naming_symbols.non_private_fields.applicable_kinds = field 59 | dotnet_naming_symbols.non_private_fields.applicable_accessibilities = public, protected, protected_internal, internal 60 | dotnet_naming_rule.non_private_fields.severity = warning 61 | dotnet_naming_rule.non_private_fields.symbols = non_private_fields 62 | dotnet_naming_rule.non_private_fields.style = pascal_case 63 | 64 | dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field 65 | dotnet_naming_symbols.static_readonly_fields.required_modifiers = static, readonly 66 | dotnet_naming_rule.static_readonly_fields.severity = warning 67 | dotnet_naming_rule.static_readonly_fields.symbols = static_readonly_fields 68 | dotnet_naming_rule.static_readonly_fields.style = pascal_case 69 | 70 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 71 | dotnet_naming_symbols.constant_fields.required_modifiers = const 72 | dotnet_naming_rule.constant_fields.severity = warning 73 | dotnet_naming_rule.constant_fields.symbols = constant_fields 74 | dotnet_naming_rule.constant_fields.style = pascal_case 75 | 76 | 77 | dotnet_naming_style.camel_case.capitalization = camel_case 78 | 79 | dotnet_naming_symbols.other_fields_parameters_and_locals.applicable_kinds = field, parameter, local 80 | dotnet_naming_rule.other_fields_parameters_and_locals.severity = warning 81 | dotnet_naming_rule.other_fields_parameters_and_locals.symbols = other_fields_parameters_and_locals 82 | dotnet_naming_rule.other_fields_parameters_and_locals.style = camel_case 83 | 84 | 85 | # .NET diagnostic configuration 86 | 87 | # CS8509: The switch expression does not handle all possible inputs (it is not exhaustive). 88 | dotnet_diagnostic.CS8509.severity = silent 89 | # CS8524: The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. 90 | dotnet_diagnostic.CS8524.severity = silent 91 | 92 | # IDE0005: Using directive is unnecessary. 93 | dotnet_diagnostic.IDE0005.severity = warning 94 | 95 | # CA1304: Specify CultureInfo 96 | dotnet_diagnostic.CA1304.severity = warning 97 | 98 | # CA1305: Specify IFormatProvider 99 | dotnet_diagnostic.CA1305.severity = warning 100 | 101 | # CA1310: Specify StringComparison for correctness 102 | dotnet_diagnostic.CA1310.severity = warning 103 | 104 | # CA1825: Avoid zero-length array allocations 105 | dotnet_diagnostic.CA1825.severity = warning 106 | 107 | # CA2016: Forward the 'CancellationToken' parameter to methods that take one 108 | dotnet_diagnostic.CA2016.severity = warning 109 | 110 | # CA2208: Instantiate argument exceptions correctly 111 | dotnet_diagnostic.CA2208.severity = warning 112 | 113 | # CA2211: Non-constant fields should not be visible 114 | dotnet_diagnostic.CA2211.severity = warning 115 | 116 | # CA2219: Do not raise exceptions in finally clauses 117 | dotnet_diagnostic.CA2219.severity = warning 118 | 119 | # CA2231: Overload operator equals on overriding value type Equals 120 | dotnet_diagnostic.CA2231.severity = warning 121 | 122 | # CA1806: Do not ignore method results 123 | dotnet_diagnostic.CA1806.severity = silent 124 | 125 | # CA1816: Dispose methods should call SuppressFinalize 126 | dotnet_diagnostic.CA1816.severity = none 127 | 128 | # CA1822: Mark members as static 129 | dotnet_diagnostic.CA1822.severity = silent 130 | 131 | # CA1826: Do not use Enumerable methods on indexable collections 132 | dotnet_diagnostic.CA1826.severity = silent 133 | 134 | # CA1834: Consider using 'StringBuilder.Append(char)' when applicable 135 | dotnet_diagnostic.CA1834.severity = silent 136 | 137 | # CA1806: Do not ignore method results 138 | dotnet_diagnostic.CA1806.severity = silent 139 | 140 | # CA2245: Do not assign a property to itself 141 | dotnet_diagnostic.CA2245.severity = silent 142 | 143 | # CA2201: Do not raise reserved exception types 144 | dotnet_diagnostic.CA2201.severity = warning 145 | 146 | # CA1805: Do not initialize unnecessarily 147 | dotnet_diagnostic.CA1805.severity = warning 148 | 149 | # CA1725: Parameter names should match base declaration 150 | dotnet_diagnostic.CA1725.severity = warning 151 | 152 | # IDE0001: Simplify Names 153 | dotnet_diagnostic.IDE0001.severity = warning 154 | 155 | # CA2215: Dispose methods should call base class dispose 156 | dotnet_diagnostic.CA2215.severity = warning 157 | 158 | # IDE0059: Unnecessary assignment of a value 159 | dotnet_diagnostic.IDE0059.severity = warning 160 | 161 | # CA1031: Do not catch general exception types 162 | dotnet_diagnostic.CA1031.severity = warning 163 | 164 | # CA1303: Do not pass literals as localized parameters 165 | dotnet_diagnostic.CA1303.severity = none 166 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ '*' ] 6 | pull_request: 7 | 8 | jobs: 9 | CI: 10 | 11 | runs-on: windows-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 # Needed in order for tags to be available so prereleases autoincrement the version 17 | 18 | - name: Build and test 19 | run: ./build.ps1 20 | env: 21 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 22 | 23 | - name: Publish to MyGet 24 | if: github.ref == 'refs/heads/main' 25 | run: dotnet nuget push artifacts\Packages\AmbientTasks.*.nupkg --source https://www.myget.org/F/ambienttasks/api/v3/index.json --api-key ${{ secrets.MYGET_API_KEY }} 26 | 27 | - name: Upload packages artifact 28 | if: always() 29 | uses: actions/upload-artifact@v2 30 | with: 31 | name: Packages 32 | path: artifacts/Packages 33 | 34 | - name: Upload test results artifact 35 | if: always() 36 | uses: actions/upload-artifact@v2 37 | with: 38 | name: Test results 39 | path: artifacts/Test results 40 | 41 | - name: Upload logs artifact 42 | if: always() 43 | uses: actions/upload-artifact@v2 44 | with: 45 | name: Logs 46 | path: artifacts/Logs 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tools/ 2 | 3 | # AltCover 4 | __Saved/ 5 | 6 | 7 | ## Ignore Visual Studio temporary files, build results, and 8 | ## files generated by popular Visual Studio add-ons. 9 | ## 10 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 11 | 12 | # User-specific files 13 | *.rsuser 14 | *.suo 15 | *.user 16 | *.userosscache 17 | *.sln.docstates 18 | 19 | # User-specific files (MonoDevelop/Xamarin Studio) 20 | *.userprefs 21 | 22 | # Mono auto generated files 23 | mono_crash.* 24 | 25 | # Build results 26 | [Dd]ebug/ 27 | [Dd]ebugPublic/ 28 | [Rr]elease/ 29 | [Rr]eleases/ 30 | x64/ 31 | x86/ 32 | [Aa][Rr][Mm]/ 33 | [Aa][Rr][Mm]64/ 34 | bld/ 35 | [Bb]in/ 36 | [Oo]bj/ 37 | [Ll]og/ 38 | 39 | # Visual Studio 2015/2017 cache/options directory 40 | .vs/ 41 | # Uncomment if you have tasks that create the project's static files in wwwroot 42 | #wwwroot/ 43 | 44 | # Visual Studio 2017 auto generated files 45 | Generated\ Files/ 46 | 47 | # MSTest test Results 48 | [Tt]est[Rr]esult*/ 49 | [Bb]uild[Ll]og.* 50 | 51 | # NUnit 52 | *.VisualState.xml 53 | TestResult.xml 54 | nunit-*.xml 55 | 56 | # Build Results of an ATL Project 57 | [Dd]ebugPS/ 58 | [Rr]eleasePS/ 59 | dlldata.c 60 | 61 | # Benchmark Results 62 | BenchmarkDotNet.Artifacts/ 63 | 64 | # .NET Core 65 | project.lock.json 66 | project.fragment.lock.json 67 | artifacts/ 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # JustCode is a .NET coding add-in 136 | .JustCode 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Visual Studio code coverage results 149 | *.coverage 150 | *.coveragexml 151 | 152 | # NCrunch 153 | _NCrunch_* 154 | .*crunch*.local.xml 155 | nCrunchTemp_* 156 | 157 | # MightyMoose 158 | *.mm.* 159 | AutoTest.Net/ 160 | 161 | # Web workbench (sass) 162 | .sass-cache/ 163 | 164 | # Installshield output folder 165 | [Ee]xpress/ 166 | 167 | # DocProject is a documentation generator add-in 168 | DocProject/buildhelp/ 169 | DocProject/Help/*.HxT 170 | DocProject/Help/*.HxC 171 | DocProject/Help/*.hhc 172 | DocProject/Help/*.hhk 173 | DocProject/Help/*.hhp 174 | DocProject/Help/Html2 175 | DocProject/Help/html 176 | 177 | # Click-Once directory 178 | publish/ 179 | 180 | # Publish Web Output 181 | *.[Pp]ublish.xml 182 | *.azurePubxml 183 | # Note: Comment the next line if you want to checkin your web deploy settings, 184 | # but database connection strings (with potential passwords) will be unencrypted 185 | *.pubxml 186 | *.publishproj 187 | 188 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 189 | # checkin your Azure Web App publish settings, but sensitive information contained 190 | # in these scripts will be unencrypted 191 | PublishScripts/ 192 | 193 | # NuGet Packages 194 | *.nupkg 195 | # NuGet Symbol Packages 196 | *.snupkg 197 | # The packages folder can be ignored because of Package Restore 198 | **/[Pp]ackages/* 199 | # except build/, which is used as an MSBuild target. 200 | !**/[Pp]ackages/build/ 201 | # Uncomment if necessary however generally it will be regenerated when needed 202 | #!**/[Pp]ackages/repositories.config 203 | # NuGet v3's project.json files produces more ignorable files 204 | *.nuget.props 205 | *.nuget.targets 206 | 207 | # Microsoft Azure Build Output 208 | csx/ 209 | *.build.csdef 210 | 211 | # Microsoft Azure Emulator 212 | ecf/ 213 | rcf/ 214 | 215 | # Windows Store app package directories and files 216 | AppPackages/ 217 | BundleArtifacts/ 218 | Package.StoreAssociation.xml 219 | _pkginfo.txt 220 | *.appx 221 | *.appxbundle 222 | *.appxupload 223 | 224 | # Visual Studio cache files 225 | # files ending in .cache can be ignored 226 | *.[Cc]ache 227 | # but keep track of directories ending in .cache 228 | !?*.[Cc]ache/ 229 | 230 | # Others 231 | ClientBin/ 232 | ~$* 233 | *~ 234 | *.dbmdl 235 | *.dbproj.schemaview 236 | *.jfm 237 | *.pfx 238 | *.publishsettings 239 | orleans.codegen.cs 240 | 241 | # Including strong name files can present a security risk 242 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 243 | #*.snk 244 | 245 | # Since there are multiple workflows, uncomment next line to ignore bower_components 246 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 247 | #bower_components/ 248 | 249 | # RIA/Silverlight projects 250 | Generated_Code/ 251 | 252 | # Backup & report files from converting an old project file 253 | # to a newer Visual Studio version. Backup files are not needed, 254 | # because we have git ;-) 255 | _UpgradeReport_Files/ 256 | Backup*/ 257 | UpgradeLog*.XML 258 | UpgradeLog*.htm 259 | ServiceFabricBackup/ 260 | *.rptproj.bak 261 | 262 | # SQL Server files 263 | *.mdf 264 | *.ldf 265 | *.ndf 266 | 267 | # Business Intelligence projects 268 | *.rdl.data 269 | *.bim.layout 270 | *.bim_*.settings 271 | *.rptproj.rsuser 272 | *- [Bb]ackup.rdl 273 | *- [Bb]ackup ([0-9]).rdl 274 | *- [Bb]ackup ([0-9][0-9]).rdl 275 | 276 | # Microsoft Fakes 277 | FakesAssemblies/ 278 | 279 | # GhostDoc plugin setting file 280 | *.GhostDoc.xml 281 | 282 | # Node.js Tools for Visual Studio 283 | .ntvs_analysis.dat 284 | node_modules/ 285 | 286 | # Visual Studio 6 build log 287 | *.plg 288 | 289 | # Visual Studio 6 workspace options file 290 | *.opt 291 | 292 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 293 | *.vbw 294 | 295 | # Visual Studio LightSwitch build output 296 | **/*.HTMLClient/GeneratedArtifacts 297 | **/*.DesktopClient/GeneratedArtifacts 298 | **/*.DesktopClient/ModelManifest.xml 299 | **/*.Server/GeneratedArtifacts 300 | **/*.Server/ModelManifest.xml 301 | _Pvt_Extensions 302 | 303 | # Paket dependency manager 304 | .paket/paket.exe 305 | paket-files/ 306 | 307 | # FAKE - F# Make 308 | .fake/ 309 | 310 | # CodeRush personal settings 311 | .cr/personal 312 | 313 | # Python Tools for Visual Studio (PTVS) 314 | __pycache__/ 315 | *.pyc 316 | 317 | # Cake - Uncomment if you are using it 318 | # tools/** 319 | # !tools/packages.config 320 | 321 | # Tabs Studio 322 | *.tss 323 | 324 | # Telerik's JustMock configuration file 325 | *.jmconfig 326 | 327 | # BizTalk build output 328 | *.btp.cs 329 | *.btm.cs 330 | *.odx.cs 331 | *.xsd.cs 332 | 333 | # OpenCover UI analysis results 334 | OpenCover/ 335 | 336 | # Azure Stream Analytics local run output 337 | ASALocalRun/ 338 | 339 | # MSBuild Binary and Structured Log 340 | *.binlog 341 | 342 | # NVidia Nsight GPU debugger configuration file 343 | *.nvuser 344 | 345 | # MFractors (Xamarin productivity tool) working folder 346 | .mfractor/ 347 | 348 | # Local History for Visual Studio 349 | .localhistory/ 350 | 351 | # BeatPulse healthcheck temp database 352 | healthchecksdb 353 | 354 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 355 | MigrationBackup/ 356 | -------------------------------------------------------------------------------- /AmbientTasks.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29009.5 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AmbientTasks", "src\AmbientTasks\AmbientTasks.csproj", "{6B8E3C03-084B-4E5F-9615-B2D38CE0B851}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1F814447-A38B-4929-8D3B-7C58790C6F8F}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | src\Directory.Build.props = src\Directory.Build.props 12 | EndProjectSection 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AmbientTasks.Tests", "src\AmbientTasks.Tests\AmbientTasks.Tests.csproj", "{17A09C86-8841-4761-B688-440BDA347D5F}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {6B8E3C03-084B-4E5F-9615-B2D38CE0B851}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {6B8E3C03-084B-4E5F-9615-B2D38CE0B851}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {6B8E3C03-084B-4E5F-9615-B2D38CE0B851}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {6B8E3C03-084B-4E5F-9615-B2D38CE0B851}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {17A09C86-8841-4761-B688-440BDA347D5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {17A09C86-8841-4761-B688-440BDA347D5F}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {17A09C86-8841-4761-B688-440BDA347D5F}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {17A09C86-8841-4761-B688-440BDA347D5F}.Release|Any CPU.Build.0 = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(SolutionProperties) = preSolution 32 | HideSolutionNode = FALSE 33 | EndGlobalSection 34 | GlobalSection(ExtensibilityGlobals) = postSolution 35 | SolutionGuid = {97110545-7D9A-40DB-9A0E-52EEE1D4A871} 36 | EndGlobalSection 37 | EndGlobal 38 | -------------------------------------------------------------------------------- /AmbientTasks.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.1] - 2021-01-10 9 | 10 | ### Changed 11 | 12 | - Debug symbols are no longer in the NuGet package and are now published to the NuGet symbol location that is built in to Visual Studio. See the readme to load debug symbols for prerelease builds from MyGet. 13 | 14 | ## [1.0.0] - 2020-02-01 15 | 16 | ### Added 17 | 18 | - Initial release, targeting .NET Standard 2.0. Ability to track a `Task`, invoke a `Func`, post a synchronous or async callback to the current or specified synchronization context, and wait for all of the above. 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2019–2021 Technology Solutions Associates, LLC 4 | 5 | All rights reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # AmbientTasks [![NuGet badge](https://img.shields.io/nuget/v/AmbientTasks)](https://www.nuget.org/packages/AmbientTasks/ "NuGet (releases)") [![MyGet badge](https://img.shields.io/myget/ambienttasks/vpre/AmbientTasks.svg?label=myget)](https://www.myget.org/feed/ambienttasks/package/nuget/AmbientTasks "MyGet (prereleases)") [![Gitter badge](https://img.shields.io/gitter/room/Techsola/AmbientTasks)](https://gitter.im/Techsola/AmbientTasks "Chat on Gitter") [![Build status badge](https://github.com/Techsola/AmbientTasks/workflows/CI/badge.svg)](https://github.com/Techsola/AmbientTasks/actions?query=workflow%3ACI "Build status") [![codecov badge](https://codecov.io/gh/Techsola/AmbientTasks/branch/main/graph/badge.svg)](https://codecov.io/gh/Techsola/AmbientTasks "Test coverage") 2 | 3 | All notable changes are documented in [CHANGELOG.md](CHANGELOG.md). 4 | 5 | Enables scoped completion tracking and error handling of tasks as an alternative to fire-and-forget and `async void`. Easy to produce and consume, and test-friendly. 6 | 7 | Benefits: 8 | 9 | - Avoids `async void` which, while being semantically correct for top-level event handlers, [is very easy to misuse](https://msdn.microsoft.com/en-us/magazine/jj991977.aspx). 10 | 11 | - Avoids fire-and-forget (`async Task` but ignoring the task). This comes with its own pitfalls, leaking the exception to `TaskScheduler.UnobservedTaskException` or never discovering a defect due to suppressing exceptions. 12 | 13 | - Test code can use a simple API to know exactly how long to wait for asynchronous processes triggered by non-async APIs before doing a final assert. 14 | 15 | - Exceptions are no longer missed in test code due to the test not waiting long enough or the exception being unhandled on a thread pool thread. 16 | 17 | - Unhandled task exceptions are sent to a chosen global handler immediately rather than waiting until the next garbage collection (arbitrarily far in the future) finalizes an orphaned task and triggers `TaskScheduler.UnobservedTaskException`. 18 | 19 | ## Example 1 (view model) 20 | 21 | When the UI picker bound to `SelectedFooId` changes the property, the displayed label bound to `SelectedFooName` should update to reflect information about the selection. 22 | 23 | (See the [How to use](#how-to-use) section to see what you’d probably want to add to your `Program.Main`.) 24 | 25 | ```cs 26 | public class ViewModel 27 | { 28 | private int selectedFooId; 29 | 30 | public int SelectedFooId 31 | { 32 | get => selectedFooId; 33 | set 34 | { 35 | if (selectedFooId == value) return; 36 | selectedFooId = value; 37 | OnPropertyChanged(); 38 | 39 | // Start task without waiting for it 40 | AmbientTasks.Add(UpdateSelectedFooNameAsync(selectedFooId)); 41 | } 42 | } 43 | 44 | // Never use async void (or fire-and-forget which is in the same spirit) 45 | private async Task UpdateSelectedFooNameAsync(int fooId) 46 | { 47 | SelectedFooName = null; 48 | 49 | var foo = await LoadFooAsync(fooId); 50 | if (selectedFooId != fooId) return; 51 | 52 | // Update UI 53 | SelectedFooName = foo.Name; 54 | } 55 | } 56 | ``` 57 | 58 | ### Test code 59 | 60 | ```cs 61 | [Test] 62 | public void Changing_selected_ID_loads_and_updates_selected_name() 63 | { 64 | // Set up a delay 65 | var vm = new ViewModel(...); 66 | 67 | vm.SelectedFooId = 42; 68 | 69 | await AmbientTasks.WaitAllAsync(); 70 | Assert.That(vm.SelectedFooName, Is.EqualTo("Some name")); 71 | } 72 | ``` 73 | 74 | ## Example 2 (form) 75 | 76 | (See the [How to use](#how-to-use) section to see what you’d probably want to add to your `Program.Main`.) 77 | 78 | ```cs 79 | public class MainForm 80 | { 81 | private void FooComboBox_GotFocus(object sender, EventArgs e) 82 | { 83 | // Due to idiosyncrasies of the third-party control, ShowPopup doesn’t work properly when called 84 | // during the processing of this event. The recommendation is usually to queue ShowPopup to happen 85 | // right after the event is no longer being handled via Control.BeginInvoke or similar. 86 | 87 | // Use AmbientTasks.Post rather than: 88 | // - Control.BeginInvoke 89 | // - SynchronizationContext.Post 90 | // - await Task.Yield() (requires async void event handler) 91 | 92 | // This way, your tests know how long to wait and exceptions are automatically propagated to them. 93 | AmbientTasks.Post(() => FooComboBox.ShowPopup()); 94 | } 95 | } 96 | ``` 97 | 98 | ### Test code 99 | 100 | ```cs 101 | [Test] 102 | public void Foo_combo_box_opens_when_it_receives_focus() 103 | { 104 | var form = new MainForm(...); 105 | form.Show(); 106 | 107 | WindowsFormsUtils.RunWithMessagePump(async () => 108 | { 109 | form.FooComboBox.Focus(); 110 | 111 | await AmbientTasks.WaitAllAsync(); 112 | Assert.That(form.FooComboBox.IsPopupOpen, Is.True); 113 | }); 114 | } 115 | ``` 116 | 117 | ## How to use 118 | 119 | If your application has a top-level exception handler which grabs diagnostics or displays a prompt to send logs or restart, you’ll want to add this to the top of `Program.Main`: 120 | 121 | ```cs 122 | AmbientTasks.BeginContext(ex => GlobalExceptionHandler(ex)); 123 | ``` 124 | 125 | Any failure in a task passed to `AmbientTasks.Add` will be immediately handled there rather than throwing the exception on a background thread or synchronization context. 126 | 127 | Use `AmbientTasks.Add` and `Post` any time a non-async call starts off an asynchronous or queued procedure. (See the example section.) This includes replacing fire-and-forget by passing the task to `AmbientTasks.Add` and replacing `async void` by changing it to `void` and moving the awaits into an `async Task` method or lambda. For example: 128 | 129 | ##### Before 130 | 131 | ```cs 132 | private async void SomeEventHandler(object sender, EventArgs e) 133 | { 134 | // Update UI 135 | 136 | var info = await GetInfoAsync(...); 137 | 138 | // Update UI using info 139 | } 140 | ``` 141 | 142 | ##### After 143 | 144 | ```cs 145 | private void SomeEventHandler(object sender, EventArgs e) 146 | { 147 | // Update UI 148 | 149 | AmbientTasks.Add(async () => 150 | { 151 | var info = await GetInfoAsync(...); 152 | 153 | // Update UI using info 154 | }); 155 | } 156 | ``` 157 | 158 | Finally, await `AmbientTasks.WaitAllAsync()` in your test code whenever `AmbientTasks.Add` is used. This gets the timing right and routes any background exceptions to the responsible test. 159 | 160 | It could potentially make sense to delay the application exit until `AmbientTasks.WaitAllAsync()` completes, too, depending on your needs. 161 | 162 | ## Debugging into AmbientTasks source 163 | 164 | Stepping into AmbientTasks source code, pausing the debugger while execution is inside AmbientTasks code and seeing the source, and setting breakpoints in AmbientTasks all require loading symbols for AmbientTasks. To do this in Visual Studio: 165 | 166 | 1. Go to Debug > Options, and uncheck ‘Enable Just My Code.’ (It’s a good idea to reenable this as soon as you’re finished with the task that requires debugging into a specific external library.) 167 | ℹ *Before* doing this, because Visual Studio can become unresponsive when attempting to load symbols for absolutely everything, I recommend going to Debugging > Symbols within the Options window and selecting ‘Load only specified modules.’ 168 | 169 | 2. If you are using a version that was released to nuget.org, enable the built-in ‘NuGet.org Symbol Server’ symbol location. 170 | If you are using a prerelease version of AmbientTasks package, go to Debugging > Symbols within the Options window and add this as a new symbol location: `https://www.myget.org/F/ambienttasks/api/v2/symbolpackage/` 171 | 172 | 3. If ‘Load only specified modules’ is selected in Options > Debugging > Symbols, you will have to explicitly tell Visual Studio to load symbols for AmbientTasks. One way to do this while debugging is to go to Debug > Windows > Modules and right-click on AmbientTasks. Select ‘Load Symbols’ if you only want to do it for the current debugging session. Select ‘Always Load Automatically’ if you want to load symbols now and also add the file name to a list so that Visual Studio loads AmbientTasks symbols in all future debug sessions when Just My Code is disabled. 173 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | Param( 2 | [switch] $Release, 3 | [string] $SigningCertThumbprint, 4 | [string] $TimestampServer 5 | ) 6 | 7 | $ErrorActionPreference = 'Stop' 8 | 9 | # Options 10 | $configuration = 'Release' 11 | $artifactsDir = Join-Path (Resolve-Path .) 'artifacts' 12 | $packagesDir = Join-Path $artifactsDir 'Packages' 13 | $testResultsDir = Join-Path $artifactsDir 'Test results' 14 | $logsDir = Join-Path $artifactsDir 'Logs' 15 | 16 | # Detection 17 | . $PSScriptRoot\build\Get-DetectedCiVersion.ps1 18 | $versionInfo = Get-DetectedCiVersion -Release:$Release 19 | Update-CiServerBuildName $versionInfo.ProductVersion 20 | Write-Host "Building using version $($versionInfo.ProductVersion)" 21 | 22 | $dotnetArgs = @( 23 | '--configuration', $configuration 24 | '/p:RepositoryCommit=' + $versionInfo.CommitHash 25 | '/p:Version=' + $versionInfo.ProductVersion 26 | '/p:PackageVersion=' + $versionInfo.PackageVersion 27 | '/p:FileVersion=' + $versionInfo.FileVersion 28 | '/p:ContinuousIntegrationBuild=' + ($env:CI -or $env:TF_BUILD) 29 | ) 30 | 31 | # Build 32 | dotnet build /bl:"$logsDir\build.binlog" @dotnetArgs 33 | if ($LastExitCode) { exit 1 } 34 | 35 | if ($SigningCertThumbprint) { 36 | . build\SignTool.ps1 37 | SignTool $SigningCertThumbprint $TimestampServer ( 38 | Get-ChildItem src\AmbientTasks\bin\$configuration -Recurse -Include AmbientTasks.dll) 39 | } 40 | 41 | # Pack 42 | Remove-Item -Recurse -Force $packagesDir -ErrorAction Ignore 43 | 44 | . build\ValidateMetadata.ps1 45 | ValidateMetadata $versionInfo.ProductVersion -Release:$Release 46 | 47 | dotnet pack --no-build --output $packagesDir /bl:"$logsDir\pack.binlog" @dotnetArgs 48 | if ($LastExitCode) { exit 1 } 49 | 50 | if ($SigningCertThumbprint) { 51 | # Waiting for 'dotnet sign' to become available (https://github.com/NuGet/Home/issues/7939) 52 | $nuget = 'tools\nuget.exe' 53 | if (-not (Test-Path $nuget)) { 54 | New-Item -ItemType Directory -Force -Path tools 55 | 56 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 57 | Invoke-WebRequest -Uri https://dist.nuget.org/win-x86-commandline/latest/nuget.exe -OutFile $nuget 58 | } 59 | 60 | # Workaround for https://github.com/NuGet/Home/issues/10446 61 | foreach ($extension in 'nupkg', 'snupkg') { 62 | & $nuget sign $packagesDir\*.$extension -CertificateFingerprint $SigningCertThumbprint -Timestamper $TimestampServer 63 | } 64 | } 65 | 66 | # Test 67 | Remove-Item -Recurse -Force $testResultsDir -ErrorAction Ignore 68 | 69 | dotnet test --no-build --configuration $configuration --logger trx --results-directory $testResultsDir /p:AltCover=true /p:AltCoverXmlReport="$testResultsDir\coverage.xml" /p:AltCoverAssemblyExcludeFilter=AmbientTasks.Tests /p:AltCoverVerbosity=Warning /bl:"$logsDir\test.binlog" 70 | if ($LastExitCode) { $testsFailed = true } 71 | 72 | if ($env:CODECOV_TOKEN) { 73 | dotnet tool install Codecov.Tool --tool-path tools 74 | $codecov = 'tools\codecov' 75 | 76 | foreach ($coverageFile in Get-ChildItem "$testResultsDir\coverage.*.xml") { 77 | $tfm = $coverageFile.Name.Substring( 78 | 'coverage.'.Length, 79 | $coverageFile.Name.Length - 'coverage.'.Length - '.xml'.Length) 80 | 81 | & $codecov --name $tfm --file $coverageFile --token $env:CODECOV_TOKEN 82 | if ($LastExitCode) { exit 1 } 83 | } 84 | } 85 | 86 | if ($testsFailed) { exit 1 } 87 | -------------------------------------------------------------------------------- /build/CiServerIntegration.ps1: -------------------------------------------------------------------------------- 1 | class BuildMetadata { 2 | [int] $BuildNumber 3 | [System.Nullable[int]] $PullRequestNumber 4 | [string] $BranchName 5 | } 6 | 7 | function Get-BuildMetadata { 8 | $metadata = [BuildMetadata]::new() 9 | 10 | if ($env:TF_BUILD) { 11 | $metadata.BuildNumber = $env:Build_BuildId 12 | $metadata.PullRequestNumber = $env:System_PullRequest_PullRequestNumber 13 | $metadata.BranchName = $env:Build_SourceBranchName 14 | } 15 | elseif ($env:GITHUB_ACTIONS) { 16 | $metadata.BuildNumber = $env:GITHUB_RUN_NUMBER 17 | 18 | if ($env:GITHUB_REF.StartsWith('refs/pull/')) { 19 | $trimmedRef = $env:GITHUB_REF.Substring('refs/pull/'.Length) 20 | $metadata.PullRequestNumber = $trimmedRef.Substring(0, $trimmedRef.IndexOf('/')) 21 | $metadata.BranchName = $env:GITHUB_BASE_REF 22 | } elseif ($env:GITHUB_REF.StartsWith('refs/heads/')) { 23 | $metadata.BranchName = $env:GITHUB_REF.Substring('refs/heads/'.Length) 24 | } 25 | } 26 | elseif ($env:CI) { 27 | throw 'Build metadata detection is not implemented for this CI server.' 28 | } 29 | 30 | return $metadata 31 | } 32 | 33 | function Update-CiServerBuildName([Parameter(Mandatory=$true)] [string] $BuildName) { 34 | if ($env:TF_BUILD) { 35 | Write-Output "##vso[build.updatebuildnumber]$BuildName" 36 | } 37 | elseif ($env:GITHUB_ACTIONS) { 38 | # GitHub Actions does not appear to have a way to dynamically update the name/number of a workflow run. 39 | } 40 | elseif ($env:CI) { 41 | throw 'Build name updating is not implemented for this CI server.' 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /build/Get-DetectedCiVersion.ps1: -------------------------------------------------------------------------------- 1 | . $PSScriptRoot\CiServerIntegration.ps1 2 | 3 | function Get-VersionPrefixFromTags { 4 | function Get-VersionPrefix([Parameter(Mandatory=$true)] [string] $Tag) { 5 | # Start the search at index 6, skipping 1 for the `v` and 5 because no valid semantic version can have a suffix sooner than `N.N.N`. 6 | $suffixStart = $Tag.IndexOfAny(('-', '+'), 6) 7 | 8 | return [version] $( 9 | if ($suffixStart -eq -1) { 10 | $Tag.Substring(1) 11 | } else { 12 | $Tag.Substring(1, $suffixStart - 1) 13 | }) 14 | } 15 | 16 | $currentTags = @(git tag --list v* --points-at head --sort=-v:refname) 17 | if ($currentTags.Count -gt 0) { 18 | # Head is tagged, so the tag is the intended CI version for this build. 19 | return Get-VersionPrefix $currentTags[0] 20 | } 21 | 22 | $previousTags = @(git tag --list v* --sort=-v:refname) 23 | if ($previousTags.Count -gt 0) { 24 | # Head is not tagged, so it would be greater than the most recent tagged version. 25 | $previousVersion = Get-VersionPrefix $previousTags[0] 26 | return [version]::new($previousVersion.Major, $previousVersion.Minor, $previousVersion.Build + 1) 27 | } 28 | 29 | # No release has been tagged, so the initial version should be whatever the source files currently contain. 30 | } 31 | 32 | function XmlPeek( 33 | [Parameter(Mandatory=$true)] [string] $FilePath, 34 | [Parameter(Mandatory=$true)] [string] $XPath, 35 | [HashTable] $NamespaceUrisByPrefix 36 | ) { 37 | $document = [xml](Get-Content $FilePath) 38 | $namespaceManager = [System.Xml.XmlNamespaceManager]::new($document.NameTable) 39 | 40 | if ($null -ne $NamespaceUrisByPrefix) { 41 | foreach ($prefix in $NamespaceUrisByPrefix.Keys) { 42 | $namespaceManager.AddNamespace($prefix, $NamespaceUrisByPrefix[$prefix]); 43 | } 44 | } 45 | 46 | return $document.SelectSingleNode($XPath, $namespaceManager).Value 47 | } 48 | 49 | class VersionInfo { 50 | [string] $CommitHash 51 | [string] $ProductVersion 52 | [string] $PackageVersion 53 | [string] $FileVersion 54 | } 55 | 56 | function Get-DetectedCiVersion([switch] $Release) { 57 | $versionPrefix = [version](XmlPeek 'src\AmbientTasks\AmbientTasks.csproj' '/Project/PropertyGroup/Version/text()') 58 | $minVersionPrefix = Get-VersionPrefixFromTags 59 | if ($versionPrefix -lt $minVersionPrefix) { $versionPrefix = $minVersionPrefix } 60 | 61 | $buildMetadata = Get-BuildMetadata 62 | $buildNumber = $buildMetadata.BuildNumber 63 | 64 | $versionInfo = [VersionInfo]::new() 65 | $versionInfo.CommitHash = (git rev-parse head) 66 | $versionInfo.ProductVersion = $versionPrefix 67 | $versionInfo.PackageVersion = $versionPrefix 68 | $versionInfo.FileVersion = $versionPrefix 69 | 70 | if (!$buildNumber) { 71 | if ($Release) { throw 'Cannot release without a build number.' } 72 | } 73 | else { 74 | $shortCommitHash = (git rev-parse --short=8 head) 75 | 76 | if ($Release) { 77 | $versionInfo.ProductVersion += "+build.$buildNumber.commit.$shortCommitHash" 78 | } 79 | elseif ($buildMetadata.PullRequestNumber) { 80 | $versionInfo.ProductVersion += "-$buildNumber.pr.$($buildMetadata.PullRequestNumber)" 81 | $versionInfo.PackageVersion += "-$buildNumber.pr.$($buildMetadata.PullRequestNumber)" 82 | } 83 | elseif ($buildMetadata.BranchName -ne 'main') { 84 | $prereleaseSegment = $buildMetadata.BranchName -replace '[^a-zA-Z0-9]+', '-' 85 | 86 | $versionInfo.ProductVersion += "-$buildNumber.$prereleaseSegment" 87 | $versionInfo.PackageVersion += "-$buildNumber.$prereleaseSegment" 88 | } 89 | else { 90 | $versionInfo.ProductVersion += "-ci.$buildNumber+commit.$shortCommitHash" 91 | $versionInfo.PackageVersion += "-ci.$buildNumber" 92 | } 93 | 94 | $versionInfo.FileVersion += ".$buildNumber" 95 | } 96 | 97 | return $versionInfo 98 | } 99 | -------------------------------------------------------------------------------- /build/SignTool.ps1: -------------------------------------------------------------------------------- 1 | function Find-SignTool { 2 | $sdk = Get-ChildItem 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Microsoft SDKs\Windows' | 3 | ForEach-Object { Get-ItemProperty $_.PSPath } | 4 | Where-Object InstallationFolder -ne $null | 5 | Sort-Object { [version]$_.ProductVersion } | 6 | Select-Object -Last 1 7 | 8 | if (!$sdk) { throw 'Cannot find a Windows SDK installation that has signtool.exe.' } 9 | 10 | $version = [version]$sdk.ProductVersion; 11 | $major = $version.Major; 12 | $minor = [Math]::Max($version.Minor, 0); 13 | $build = [Math]::Max($version.Build, 0); 14 | $revision = [Math]::Max($version.Revision, 0); 15 | 16 | return Join-Path $sdk.InstallationFolder "bin\$major.$minor.$build.$revision\x64\signtool.exe" 17 | } 18 | 19 | function SignTool( 20 | [Parameter(Mandatory=$true)] [string] $CertificateThumbprint, 21 | [Parameter(Mandatory=$true)] [string] $TimestampServer, 22 | [Parameter(Mandatory=$true)] [string[]] $Files 23 | ) { 24 | & (Find-SignTool) sign /sha1 $CertificateThumbprint /fd SHA256 /tr $TimestampServer @Files 25 | if ($LastExitCode) { exit 1 } 26 | } 27 | -------------------------------------------------------------------------------- /build/ValidateMetadata.ps1: -------------------------------------------------------------------------------- 1 | function ValidateMetadata( 2 | [Parameter(Mandatory=$true)] [string] $ProductVersion, 3 | [switch] $Release 4 | ) { 5 | $lastReleasedVersion = XmlPeek src\AmbientTasks\AmbientTasks.csproj '/Project/PropertyGroup/Version/text()' 6 | 7 | if ($Release) { 8 | $productVersionWithoutBuildMetadata = $ProductVersion.Substring(0, $ProductVersion.IndexOf('+')) 9 | if ($lastReleasedVersion -ne $productVersionWithoutBuildMetadata) { 10 | throw 'The version must be updated in the .csproj to do a release build.' 11 | } 12 | } 13 | 14 | $changelogHeaderLines = Select-String -Path CHANGELOG.md -Pattern ('## [' + $lastReleasedVersion + ']') -SimpleMatch 15 | if ($changelogHeaderLines.Count -ne 1) { 16 | throw "There must be exactly one entry in CHANGELOG.md for version $lastReleasedVersion." 17 | } 18 | 19 | $urlAnchor = $changelogHeaderLines[0].Line.Substring('## '.Length).Replace(' ', '-') -replace '[^-\w]', '' 20 | $requiredReleaseNotesLink = "https://github.com/Techsola/AmbientTasks/blob/v$lastReleasedVersion/CHANGELOG.md#$urlAnchor" 21 | $packageReleaseNotes = XmlPeek src\AmbientTasks\AmbientTasks.csproj '/Project/PropertyGroup/PackageReleaseNotes/text()' 22 | 23 | if (-not $packageReleaseNotes.Contains($requiredReleaseNotesLink)) { 24 | throw 'Package release notes in .csproj must contain this URL: ' + $requiredReleaseNotesLink 25 | } 26 | 27 | if ($packageReleaseNotes.Length -ne $packageReleaseNotes.Trim().Length) { 28 | throw 'Package release notes must not begin or end with whitespace.' 29 | } 30 | 31 | foreach ($line in $packageReleaseNotes.Split(@("`r`n", "`r", "`n"), [StringSplitOptions]::None)) { 32 | if ([string]::IsNullOrWhiteSpace($line)) { 33 | if ($line.Length -ne 0) { 34 | throw 'Package release notes must not have whitespace-only lines.' 35 | } 36 | } elseif ($line.Length -ne $line.TrimEnd().Length -and $line.Length -ne $line.TrimEnd().Length + 2) { 37 | throw 'Package release notes must not have trailing whitespace.' 38 | } elseif ($line.Length -ne $line.TrimStart().Length) { 39 | throw 'Package release notes must not be indented.' 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/AmbientTasks.Tests/.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | 3 | # CA1031: Do not catch general exception types 4 | dotnet_diagnostic.CA1031.severity = none 5 | 6 | # CA2201: Do not raise reserved exception types 7 | dotnet_diagnostic.CA2201.severity = none 8 | -------------------------------------------------------------------------------- /src/AmbientTasks.Tests/AmbientTasks.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0;net472 5 | Techsola 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/AmbientTasks.Tests/AmbientTasksAddFuncOverloadTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using NUnit.Framework; 4 | using Shouldly; 5 | 6 | namespace Techsola 7 | { 8 | public static class AmbientTasksAddFuncOverloadTests 9 | { 10 | [Test] 11 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 12 | public static void Adding_null_func_is_noop() 13 | { 14 | AmbientTasks.Add((Func?)null); 15 | 16 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 17 | } 18 | 19 | [Test] 20 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 21 | public static void Func_returning_null_task_is_noop() 22 | { 23 | AmbientTasks.Add(() => null!); 24 | 25 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 26 | } 27 | 28 | [Test] 29 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 30 | public static void Func_returning_synchronously_successfully_completed_task_is_noop() 31 | { 32 | AmbientTasks.Add(() => Task.CompletedTask); 33 | 34 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 35 | } 36 | 37 | [Test] 38 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 39 | public static void Func_returning_synchronously_canceled_task_is_noop() 40 | { 41 | var source = new TaskCompletionSource(); 42 | source.SetCanceled(); 43 | AmbientTasks.Add(() => source.Task); 44 | 45 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 46 | } 47 | 48 | [Test] 49 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 50 | public static void Func_throwing_OperationCanceledException_is_noop() 51 | { 52 | AmbientTasks.Add(() => throw new OperationCanceledException()); 53 | 54 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 55 | } 56 | 57 | [Test] 58 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 59 | public static void Func_throwing_TaskCanceledException_is_noop() 60 | { 61 | AmbientTasks.Add(() => throw new TaskCanceledException()); 62 | 63 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 64 | } 65 | 66 | [Test] 67 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 68 | public static void BeginContext_handler_receives_exception_thrown_from_func() 69 | { 70 | var exception = new Exception(); 71 | var watcher = new CallbackWatcher(); 72 | 73 | AmbientTasks.BeginContext(ex => 74 | { 75 | watcher.OnCallback(); 76 | ex.ShouldBeSameAs(exception); 77 | }); 78 | 79 | using (watcher.ExpectCallback()) 80 | AmbientTasks.Add(() => throw exception); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/AmbientTasks.Tests/AmbientTasksPostTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using NUnit.Framework; 5 | using Shouldly; 6 | 7 | namespace Techsola 8 | { 9 | public static class AmbientTasksPostTests 10 | { 11 | public enum PostOverload 12 | { 13 | Action, 14 | SendOrPostCallback, 15 | AsyncAction 16 | } 17 | 18 | private static void AmbientTasksPost(PostOverload overload, Action action) 19 | { 20 | switch (overload) 21 | { 22 | case PostOverload.Action: 23 | AmbientTasks.Post(action); 24 | break; 25 | case PostOverload.SendOrPostCallback: 26 | AmbientTasks.Post(state => ((Action)state!).Invoke(), state: action); 27 | break; 28 | case PostOverload.AsyncAction: 29 | AmbientTasks.Post(() => 30 | { 31 | action.Invoke(); 32 | return Task.CompletedTask; 33 | }); 34 | break; 35 | default: 36 | throw new NotImplementedException(); 37 | } 38 | } 39 | 40 | [Test] 41 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 42 | public static void Passed_delegate_may_be_null([Values] PostOverload overload) 43 | { 44 | switch (overload) 45 | { 46 | case PostOverload.Action: 47 | AmbientTasks.Post((Action?)null); 48 | break; 49 | case PostOverload.SendOrPostCallback: 50 | AmbientTasks.Post(null, state: null); 51 | break; 52 | case PostOverload.AsyncAction: 53 | AmbientTasks.Post((Func?)null); 54 | break; 55 | default: 56 | throw new NotImplementedException(); 57 | } 58 | } 59 | 60 | [Test] 61 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 62 | public static void Passed_synchronization_context_may_not_be_null([Values] PostOverload overload) 63 | { 64 | switch (overload) 65 | { 66 | case PostOverload.Action: 67 | Should.Throw( 68 | () => AmbientTasks.Post(synchronizationContext: null!, (Action?)null)) 69 | .ParamName.ShouldBe("synchronizationContext"); 70 | break; 71 | case PostOverload.SendOrPostCallback: 72 | Should.Throw( 73 | () => AmbientTasks.Post(synchronizationContext: null!, null, state: null)) 74 | .ParamName.ShouldBe("synchronizationContext"); 75 | break; 76 | case PostOverload.AsyncAction: 77 | Should.Throw( 78 | () => AmbientTasks.Post(synchronizationContext: null!, (Func?)null)) 79 | .ParamName.ShouldBe("synchronizationContext"); 80 | break; 81 | default: 82 | throw new NotImplementedException(); 83 | } 84 | } 85 | 86 | [Test] 87 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 88 | public static void Passed_synchronization_context_is_used_for_post([Values] PostOverload overload) 89 | { 90 | using (SynchronizationContextAssert.ExpectSinglePost(postedAction => { })) 91 | { 92 | var contextToUse = SynchronizationContext.Current!; 93 | 94 | using (SynchronizationContextAssert.ExpectNoPost()) 95 | { 96 | switch (overload) 97 | { 98 | case PostOverload.Action: 99 | AmbientTasks.Post(contextToUse, () => { }); 100 | break; 101 | case PostOverload.SendOrPostCallback: 102 | AmbientTasks.Post(contextToUse, state => { }, state: null); 103 | break; 104 | case PostOverload.AsyncAction: 105 | AmbientTasks.Post(contextToUse, () => Task.CompletedTask); 106 | break; 107 | default: 108 | throw new NotImplementedException(); 109 | } 110 | } 111 | } 112 | } 113 | 114 | [Test] 115 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 116 | public static async Task Executes_asynchronously_if_current_synchronization_context_is_null([Values] PostOverload overload) 117 | { 118 | var source = new TaskCompletionSource(); 119 | 120 | using (Utils.WithTemporarySynchronizationContext(null)) 121 | { 122 | AmbientTasksPost(overload, () => source.SetResult(Thread.CurrentThread)); 123 | 124 | if (source.Task.Status == TaskStatus.RanToCompletion) 125 | { 126 | var threadUsed = source.Task.Result; 127 | threadUsed.ShouldNotBeSameAs(Thread.CurrentThread); 128 | } 129 | } 130 | 131 | await source.Task; 132 | } 133 | 134 | [Test] 135 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 136 | public static void Exception_from_SynchronizationContext_post_before_invoking_delegate_is_not_handled_when_there_is_no_BeginContext_handler([Values] PostOverload overload) 137 | { 138 | var exception = new Exception(); 139 | 140 | using (SynchronizationContextAssert.ExpectSinglePost(postedAction => throw exception)) 141 | { 142 | Should.Throw(() => AmbientTasksPost(overload, () => { })).ShouldBeSameAs(exception); 143 | } 144 | } 145 | 146 | [Test] 147 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 148 | public static void Exception_from_SynchronizationContext_post_before_invoking_delegate_is_not_handled_when_there_is_a_BeginContext_handler([Values] PostOverload overload) 149 | { 150 | AmbientTasks.BeginContext(ex => { }); 151 | 152 | var exception = new Exception(); 153 | 154 | using (SynchronizationContextAssert.ExpectSinglePost(postedAction => throw exception)) 155 | { 156 | Should.Throw(() => AmbientTasksPost(overload, () => { })).ShouldBeSameAs(exception); 157 | } 158 | } 159 | 160 | [Test] 161 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 162 | public static void Exception_from_SynchronizationContext_post_after_invoking_delegate_is_not_handled_when_there_is_no_BeginContext_handler([Values] PostOverload overload) 163 | { 164 | var exception = new Exception(); 165 | 166 | using (SynchronizationContextAssert.ExpectSinglePost(postedAction => 167 | { 168 | postedAction.Invoke(); 169 | throw exception; 170 | })) 171 | { 172 | Should.Throw(() => AmbientTasksPost(overload, () => { })).ShouldBeSameAs(exception); 173 | } 174 | } 175 | 176 | [Test] 177 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 178 | public static void Exception_from_SynchronizationContext_post_after_invoking_delegate_is_not_handled_when_there_is_a_BeginContext_handler([Values] PostOverload overload) 179 | { 180 | AmbientTasks.BeginContext(ex => { }); 181 | 182 | var exception = new Exception(); 183 | 184 | using (SynchronizationContextAssert.ExpectSinglePost(postedAction => 185 | { 186 | postedAction.Invoke(); 187 | throw exception; 188 | })) 189 | { 190 | Should.Throw(() => AmbientTasksPost(overload, () => { })).ShouldBeSameAs(exception); 191 | } 192 | } 193 | 194 | [Test] 195 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 196 | public static void Exception_from_user_delegate_is_thrown_on_SynchronizationContext_when_there_is_no_BeginContext_handler([Values] PostOverload overload) 197 | { 198 | var exception = new Exception(); 199 | 200 | using (SynchronizationContextAssert.ExpectSinglePost(postedAction => 201 | { 202 | Should.Throw(postedAction).ShouldBeSameAs(exception); 203 | })) 204 | { 205 | AmbientTasksPost(overload, () => throw exception); 206 | } 207 | } 208 | 209 | [Test] 210 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 211 | public static void Exception_from_user_delegate_is_not_thrown_on_SynchronizationContext_when_there_is_a_BeginContext_handler([Values] PostOverload overload) 212 | { 213 | AmbientTasks.BeginContext(ex => { }); 214 | 215 | using (SynchronizationContextAssert.ExpectSinglePost(postedAction => postedAction.Invoke())) 216 | { 217 | AmbientTasksPost(overload, () => throw new Exception()); 218 | } 219 | } 220 | 221 | [Test] 222 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 223 | public static void Exception_from_user_delegate_is_handled_by_BeginContext_handler([Values] PostOverload overload) 224 | { 225 | var exception = new Exception(); 226 | var watcher = new CallbackWatcher(); 227 | 228 | AmbientTasks.BeginContext(ex => 229 | { 230 | watcher.OnCallback(); 231 | ex.ShouldBeSameAs(exception); 232 | }); 233 | 234 | using (SynchronizationContextAssert.ExpectSinglePost(postedAction => postedAction.Invoke())) 235 | { 236 | using (watcher.ExpectCallback()) 237 | AmbientTasksPost(overload, () => throw exception); 238 | } 239 | } 240 | 241 | [Test] 242 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 243 | public static void Exception_from_user_delegate_is_in_task_from_next_call_to_WaitAllAsync_when_there_is_no_BeginContext_handler([Values] PostOverload overload) 244 | { 245 | var exception = new Exception(); 246 | 247 | using (SynchronizationContextAssert.ExpectSinglePost(postedAction => postedAction.Invoke())) 248 | { 249 | Should.Throw(() => AmbientTasksPost(overload, () => throw exception)); 250 | } 251 | 252 | var waitAllTask = AmbientTasks.WaitAllAsync(); 253 | waitAllTask.Status.ShouldBe(TaskStatus.Faulted); 254 | 255 | var aggregateException = waitAllTask.Exception!.InnerExceptions.ShouldHaveSingleItem().ShouldBeOfType(); 256 | 257 | aggregateException.InnerExceptions.ShouldHaveSingleItem().ShouldBeSameAs(exception); 258 | } 259 | 260 | [Test] 261 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 262 | public static void Exception_from_user_delegate_is_not_in_task_from_next_call_to_WaitAllAsync_when_there_is_a_BeginContext_handler([Values] PostOverload overload) 263 | { 264 | AmbientTasks.BeginContext(ex => { }); 265 | 266 | using (SynchronizationContextAssert.ExpectSinglePost(postedAction => postedAction.Invoke())) 267 | { 268 | AmbientTasksPost(overload, () => throw new Exception()); 269 | } 270 | 271 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 272 | } 273 | 274 | [Test] 275 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 276 | public static void WaitAllAsync_waits_for_SynchronizationContext_Post_to_invoke_delegate_before_completing([Values] PostOverload overload) 277 | { 278 | var postedAction = (Action?)null; 279 | 280 | using (SynchronizationContextAssert.ExpectSinglePost(p => postedAction = p)) 281 | { 282 | AmbientTasksPost(overload, () => { }); 283 | } 284 | 285 | var waitAllTask = AmbientTasks.WaitAllAsync(); 286 | waitAllTask.IsCompleted.ShouldBeFalse(); 287 | 288 | postedAction.ShouldNotBeNull(); 289 | postedAction!.Invoke(); 290 | 291 | waitAllTask.Status.ShouldBe(TaskStatus.RanToCompletion); 292 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 293 | } 294 | 295 | [Test] 296 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 297 | public static void WaitAllAsync_does_not_complete_until_async_task_added_by_user_delegate_completes([Values] PostOverload overload) 298 | { 299 | var source = new TaskCompletionSource(); 300 | var postedAction = (Action?)null; 301 | 302 | using (SynchronizationContextAssert.ExpectSinglePost(p => postedAction = p)) 303 | { 304 | AmbientTasksPost(overload, () => 305 | { 306 | AmbientTasks.Add(source.Task); 307 | }); 308 | } 309 | 310 | var waitAllTask = AmbientTasks.WaitAllAsync(); 311 | waitAllTask.IsCompleted.ShouldBeFalse(); 312 | 313 | postedAction.ShouldNotBeNull(); 314 | postedAction!.Invoke(); 315 | 316 | waitAllTask.IsCompleted.ShouldBeFalse(); 317 | 318 | source.SetResult(null); 319 | 320 | waitAllTask.Status.ShouldBe(TaskStatus.RanToCompletion); 321 | } 322 | 323 | [Test] 324 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 325 | public static void WaitAllAsync_does_not_complete_until_async_task_returned_by_user_delegate_completes() 326 | { 327 | var source = new TaskCompletionSource(); 328 | var postedAction = (Action?)null; 329 | 330 | using (SynchronizationContextAssert.ExpectSinglePost(p => postedAction = p)) 331 | { 332 | AmbientTasks.Post(() => source.Task); 333 | } 334 | 335 | var waitAllTask = AmbientTasks.WaitAllAsync(); 336 | waitAllTask.IsCompleted.ShouldBeFalse(); 337 | 338 | postedAction.ShouldNotBeNull(); 339 | postedAction!.Invoke(); 340 | 341 | waitAllTask.IsCompleted.ShouldBeFalse(); 342 | 343 | source.SetResult(null); 344 | 345 | waitAllTask.Status.ShouldBe(TaskStatus.RanToCompletion); 346 | } 347 | 348 | [Test] 349 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 350 | public static void Delegate_is_abandoned_if_SynchronizationContext_post_throws_before_invoking_delegate([Values] PostOverload overload) 351 | { 352 | var postedAction = (Action?)null; 353 | 354 | using (SynchronizationContextAssert.ExpectSinglePost(p => 355 | { 356 | postedAction = p; 357 | throw new Exception(); 358 | })) 359 | { 360 | Should.Throw(() => AmbientTasksPost(overload, () => Assert.Fail("The delegate should be abandoned."))); 361 | } 362 | 363 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 364 | 365 | postedAction.ShouldNotBeNull(); 366 | postedAction!.Invoke(); 367 | 368 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 369 | } 370 | 371 | [Test] 372 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 373 | public static void Subsequent_invocations_by_SynchronizationContext_are_ignored_when_successful([Values] PostOverload overload) 374 | { 375 | var postedAction = (Action?)null; 376 | var callCount = 0; 377 | 378 | using (SynchronizationContextAssert.ExpectSinglePost(p => 379 | { 380 | postedAction = p; 381 | })) 382 | { 383 | AmbientTasksPost(overload, () => callCount++); 384 | } 385 | 386 | callCount.ShouldBe(0); 387 | postedAction.ShouldNotBeNull(); 388 | postedAction!.Invoke(); 389 | callCount.ShouldBe(1); 390 | 391 | postedAction.Invoke(); 392 | 393 | callCount.ShouldBe(1); 394 | } 395 | 396 | [Test] 397 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 398 | public static void Subsequent_invocations_by_SynchronizationContext_are_ignored_when_user_code_throws([Values] PostOverload overload) 399 | { 400 | var postedAction = (Action?)null; 401 | var callCount = 0; 402 | 403 | using (SynchronizationContextAssert.ExpectSinglePost(p => 404 | { 405 | postedAction = p; 406 | })) 407 | { 408 | AmbientTasksPost(overload, () => 409 | { 410 | callCount++; 411 | throw new Exception(); 412 | }); 413 | } 414 | 415 | callCount.ShouldBe(0); 416 | Should.Throw(postedAction!); 417 | callCount.ShouldBe(1); 418 | 419 | postedAction.ShouldNotBeNull(); 420 | postedAction!.Invoke(); 421 | 422 | callCount.ShouldBe(1); 423 | } 424 | } 425 | } 426 | -------------------------------------------------------------------------------- /src/AmbientTasks.Tests/AmbientTasksTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using NUnit.Framework; 6 | using Shouldly; 7 | 8 | namespace Techsola 9 | { 10 | public static class AmbientTasksTests 11 | { 12 | [Test] 13 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 14 | public static void Adding_synchronously_faulted_task_with_no_context_throws_AggregateException_synchronously() 15 | { 16 | var exception = new Exception(); 17 | 18 | var aggregateException = Should.Throw( 19 | () => AmbientTasks.Add(Task.FromException(exception))); 20 | 21 | aggregateException.InnerExceptions.ShouldBe(new[] { exception }); 22 | } 23 | 24 | [Test] 25 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 26 | public static void Adding_synchronously_faulted_task_with_no_context_throws_AggregateException_on_current_SynchronizationContext() 27 | { 28 | var exception = new Exception(); 29 | 30 | using (SynchronizationContextAssert.ExpectSinglePost(postedAction => 31 | { 32 | var aggregateException = Should.Throw(postedAction); 33 | 34 | aggregateException.InnerExceptions.ShouldBe(new[] { exception }); 35 | })) 36 | { 37 | AmbientTasks.Add(Task.FromException(exception)); 38 | } 39 | } 40 | 41 | [Test] 42 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 43 | public static void Adding_synchronously_faulted_task_with_multiple_exceptions_and_no_context_throws_AggregateException_synchronously_with_all_exceptions() 44 | { 45 | var exceptions = new[] 46 | { 47 | new Exception(), 48 | new Exception() 49 | }; 50 | 51 | var source = new TaskCompletionSource(); 52 | source.SetException(exceptions); 53 | 54 | var aggregateException = Should.Throw(() => AmbientTasks.Add(source.Task)); 55 | 56 | aggregateException.InnerExceptions.ShouldBe(exceptions); 57 | } 58 | 59 | [Test] 60 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 61 | public static void Adding_synchronously_faulted_task_with_multiple_exceptions_and_no_context_throws_AggregateException_on_current_SynchronizationContexts() 62 | { 63 | var exceptions = new[] 64 | { 65 | new Exception(), 66 | new Exception() 67 | }; 68 | 69 | var source = new TaskCompletionSource(); 70 | source.SetException(exceptions); 71 | 72 | using (SynchronizationContextAssert.ExpectSinglePost(postedAction => 73 | { 74 | var aggregateException = Should.Throw(postedAction); 75 | 76 | aggregateException.InnerExceptions.ShouldBe(exceptions); 77 | })) 78 | { 79 | AmbientTasks.Add(source.Task); 80 | } 81 | } 82 | 83 | [Test] 84 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 85 | public static void WaitAllAsync_should_fault_after_adding_synchronously_failed_task_with_no_context() 86 | { 87 | Should.Throw( 88 | () => AmbientTasks.Add(Task.FromException(new Exception()))); 89 | 90 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.Faulted); 91 | } 92 | 93 | [Test] 94 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 95 | public static void WaitAllAsync_should_reset_after_fault_is_displayed_in_previously_returned_task() 96 | { 97 | Should.Throw( 98 | () => AmbientTasks.Add(Task.FromException(new Exception()))); 99 | 100 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.Faulted); 101 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 102 | } 103 | 104 | [Test] 105 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 106 | public static void WaitAllAsync_should_fault_after_adding_synchronously_failed_task_with_no_context_when_on_SynchronizationContext() 107 | { 108 | using (SynchronizationContextAssert.ExpectSinglePost(postedAction => { })) 109 | { 110 | AmbientTasks.Add(Task.FromException(new Exception())); 111 | 112 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.Faulted); 113 | } 114 | } 115 | 116 | [Test] 117 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 118 | public static void Adding_null_task_is_noop() 119 | { 120 | AmbientTasks.Add((Task?)null); 121 | 122 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 123 | } 124 | 125 | [Test] 126 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 127 | public static void Adding_synchronously_successfully_completed_task_is_noop() 128 | { 129 | AmbientTasks.Add(Task.CompletedTask); 130 | 131 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 132 | } 133 | 134 | [Test] 135 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 136 | public static void Adding_synchronously_canceled_task_is_noop() 137 | { 138 | var source = new TaskCompletionSource(); 139 | source.SetCanceled(); 140 | AmbientTasks.Add(source.Task); 141 | 142 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 143 | } 144 | 145 | [Test] 146 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 147 | public static void WaitAllAsync_waits_for_added_task_with_no_context_to_succeed() 148 | { 149 | var source = new TaskCompletionSource(); 150 | AmbientTasks.Add(source.Task); 151 | 152 | var waitAllTask = AmbientTasks.WaitAllAsync(); 153 | waitAllTask.IsCompleted.ShouldBeFalse(); 154 | 155 | source.SetResult(null); 156 | waitAllTask.Status.ShouldBe(TaskStatus.RanToCompletion); 157 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 158 | } 159 | 160 | [Test] 161 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 162 | public static void WaitAllAsync_waits_for_added_task_with_no_context_to_cancel() 163 | { 164 | var source = new TaskCompletionSource(); 165 | AmbientTasks.Add(source.Task); 166 | 167 | var waitAllTask = AmbientTasks.WaitAllAsync(); 168 | waitAllTask.IsCompleted.ShouldBeFalse(); 169 | 170 | source.SetCanceled(); 171 | waitAllTask.Status.ShouldBe(TaskStatus.RanToCompletion); 172 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 173 | } 174 | 175 | [Test] 176 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 177 | public static void WaitAllAsync_does_not_reset_until_all_tasks_have_completed() 178 | { 179 | var source1 = new TaskCompletionSource(); 180 | var source2 = new TaskCompletionSource(); 181 | 182 | AmbientTasks.Add(source1.Task); 183 | var waitTaskSeenAfterFirstAdd = AmbientTasks.WaitAllAsync(); 184 | waitTaskSeenAfterFirstAdd.IsCompleted.ShouldBeFalse(); 185 | 186 | // Should not reset on next call 187 | AmbientTasks.WaitAllAsync().ShouldBeSameAs(waitTaskSeenAfterFirstAdd); 188 | 189 | AmbientTasks.Add(source2.Task); 190 | AmbientTasks.WaitAllAsync().ShouldBeSameAs(waitTaskSeenAfterFirstAdd); 191 | 192 | // Should not reset on next call 193 | AmbientTasks.WaitAllAsync().ShouldBeSameAs(waitTaskSeenAfterFirstAdd); 194 | 195 | source1.SetResult(null); 196 | AmbientTasks.WaitAllAsync().ShouldBeSameAs(waitTaskSeenAfterFirstAdd); 197 | 198 | // Should not reset on next call 199 | AmbientTasks.WaitAllAsync().ShouldBeSameAs(waitTaskSeenAfterFirstAdd); 200 | 201 | source2.SetResult(null); 202 | waitTaskSeenAfterFirstAdd.Status.ShouldBe(TaskStatus.RanToCompletion); 203 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 204 | } 205 | 206 | [Test] 207 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 208 | public static void WaitAllAsync_waits_for_added_task_with_no_context_to_fault() 209 | { 210 | var source = new TaskCompletionSource(); 211 | AmbientTasks.Add(source.Task); 212 | 213 | var waitAllTask = AmbientTasks.WaitAllAsync(); 214 | waitAllTask.IsCompleted.ShouldBeFalse(); 215 | 216 | source.SetException(new Exception()); 217 | waitAllTask.Status.ShouldBe(TaskStatus.Faulted); 218 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 219 | } 220 | 221 | [Test] 222 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 223 | public static void WaitAllAsync_waits_for_added_task_with_no_context_and_throws_exception_on_current_SynchronizationContext() 224 | { 225 | var source = new TaskCompletionSource(); 226 | var exception = new Exception(); 227 | 228 | using (SynchronizationContextAssert.ExpectSinglePost(postedAction => 229 | { 230 | var aggregateException = Should.Throw(postedAction); 231 | 232 | aggregateException.InnerExceptions.ShouldBe(new[] { exception }); 233 | })) 234 | { 235 | AmbientTasks.Add(source.Task); 236 | 237 | var waitAllTask = AmbientTasks.WaitAllAsync(); 238 | waitAllTask.IsCompleted.ShouldBeFalse(); 239 | 240 | source.SetException(exception); 241 | 242 | waitAllTask.Status.ShouldBe(TaskStatus.Faulted); 243 | } 244 | } 245 | 246 | [Test] 247 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 248 | public static void WaitAllAsync_resets_after_overlapping_tasks_fail() 249 | { 250 | var source1 = new TaskCompletionSource(); 251 | var source2 = new TaskCompletionSource(); 252 | 253 | AmbientTasks.Add(source1.Task); 254 | AmbientTasks.Add(source2.Task); 255 | 256 | var waitAllTaskBeforeAllExceptions = AmbientTasks.WaitAllAsync(); 257 | 258 | source1.SetException(new Exception()); 259 | source2.SetException(new Exception()); 260 | 261 | waitAllTaskBeforeAllExceptions.Status.ShouldBe(TaskStatus.Faulted); 262 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 263 | } 264 | 265 | [Test] 266 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 267 | public static async Task WaitAllAsync_should_throw_AggregateException_when_awaited() 268 | { 269 | var source = new TaskCompletionSource(); 270 | AmbientTasks.Add(source.Task); 271 | 272 | var waitAllTask = AmbientTasks.WaitAllAsync(); 273 | 274 | var exception = new Exception(); 275 | source.SetException(exception); 276 | 277 | var aggregateException = await Should.ThrowAsync(waitAllTask); 278 | aggregateException.InnerExceptions.ShouldBe(new[] { exception }); 279 | } 280 | 281 | [Test] 282 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 283 | public static void WaitAllAsync_does_not_return_with_fault_from_added_test_until_last_task_completes() 284 | { 285 | var source1 = new TaskCompletionSource(); 286 | var source2 = new TaskCompletionSource(); 287 | var source3 = new TaskCompletionSource(); 288 | 289 | AmbientTasks.Add(source1.Task); 290 | AmbientTasks.Add(source2.Task); 291 | AmbientTasks.Add(source3.Task); 292 | 293 | var waitAllTask = AmbientTasks.WaitAllAsync(); 294 | 295 | source1.SetException(new Exception()); 296 | waitAllTask.IsCompleted.ShouldBeFalse(); 297 | AmbientTasks.WaitAllAsync().ShouldBeSameAs(waitAllTask); 298 | 299 | source2.SetResult(null); 300 | waitAllTask.IsCompleted.ShouldBeFalse(); 301 | AmbientTasks.WaitAllAsync().ShouldBeSameAs(waitAllTask); 302 | 303 | source3.SetResult(null); 304 | waitAllTask.Status.ShouldBe(TaskStatus.Faulted); 305 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 306 | } 307 | 308 | [Test] 309 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 310 | public static void WaitAllAsync_should_have_single_AggregateException_with_all_exceptions_from_each_task() 311 | { 312 | var task1Exceptions = new[] { new Exception("Task 1 exception 1"), new Exception("Task 1 exception 2") }; 313 | var task2Exceptions = new[] { new Exception("Task 2 exception 1"), new Exception("Task 2 exception 2") }; 314 | var source1 = new TaskCompletionSource(); 315 | var source2 = new TaskCompletionSource(); 316 | AmbientTasks.Add(source1.Task); 317 | AmbientTasks.Add(source2.Task); 318 | 319 | var waitAllTask = AmbientTasks.WaitAllAsync(); 320 | 321 | source1.SetException(task1Exceptions); 322 | source2.SetException(task2Exceptions); 323 | 324 | waitAllTask.Status.ShouldBe(TaskStatus.Faulted); 325 | 326 | var aggregateException = waitAllTask.Exception!.InnerExceptions.ShouldHaveSingleItem().ShouldBeOfType(); 327 | 328 | aggregateException.InnerExceptions.ShouldBe(task1Exceptions.Concat(task2Exceptions)); 329 | } 330 | 331 | [Test] 332 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 333 | public static void WaitAllAsync_should_have_single_AggregateException_with_all_exceptions_from_each_task_all_faulted_synchronously() 334 | { 335 | var task1Exceptions = new[] { new Exception("Task 1 exception 1"), new Exception("Task 1 exception 2") }; 336 | var task2Exceptions = new[] { new Exception("Task 2 exception 1"), new Exception("Task 2 exception 2") }; 337 | var source1 = new TaskCompletionSource(); 338 | var source2 = new TaskCompletionSource(); 339 | AmbientTasks.Add(source1.Task); 340 | AmbientTasks.Add(source2.Task); 341 | 342 | source1.SetException(task1Exceptions); 343 | source2.SetException(task2Exceptions); 344 | 345 | var waitAllTask = AmbientTasks.WaitAllAsync(); 346 | 347 | waitAllTask.Status.ShouldBe(TaskStatus.Faulted); 348 | 349 | var aggregateException = waitAllTask.Exception!.InnerExceptions.ShouldHaveSingleItem().ShouldBeOfType(); 350 | 351 | aggregateException.InnerExceptions.ShouldBe(task1Exceptions.Concat(task2Exceptions)); 352 | } 353 | 354 | [Test] 355 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 356 | public static void SynchronizationContext_that_throws_on_post_does_not_prevent_WaitAllAsync_completion() 357 | { 358 | var source = new TaskCompletionSource(); 359 | 360 | using (SynchronizationContextAssert.ExpectSinglePost(postedAction => throw new Exception())) 361 | { 362 | AmbientTasks.Add(source.Task); 363 | 364 | var waitAllTask = AmbientTasks.WaitAllAsync(); 365 | source.SetException(new Exception()); 366 | waitAllTask.Status.ShouldBe(TaskStatus.Faulted); 367 | } 368 | } 369 | 370 | [Test] 371 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 372 | public static void BeginContext_handler_receives_exceptions_from_synchronously_faulted_tasks() 373 | { 374 | var exception = new Exception(); 375 | var watcher = new CallbackWatcher(); 376 | 377 | AmbientTasks.BeginContext(ex => 378 | { 379 | watcher.OnCallback(); 380 | ex.ShouldBeSameAs(exception); 381 | }); 382 | 383 | using (watcher.ExpectCallback()) 384 | AmbientTasks.Add(Task.FromException(exception)); 385 | } 386 | 387 | [Test] 388 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 389 | public static void BeginContext_handler_receives_exceptions_from_asynchronously_faulted_tasks() 390 | { 391 | var source = new TaskCompletionSource(); 392 | var exception = new Exception(); 393 | var watcher = new CallbackWatcher(); 394 | 395 | AmbientTasks.BeginContext(ex => 396 | { 397 | watcher.OnCallback(); 398 | ex.ShouldBeSameAs(exception); 399 | }); 400 | 401 | AmbientTasks.Add(source.Task); 402 | 403 | using (watcher.ExpectCallback()) 404 | source.SetException(exception); 405 | } 406 | 407 | [Test] 408 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 409 | public static void Handled_exceptions_do_not_appear_in_a_subsequent_call_to_WaitAllAsync() 410 | { 411 | AmbientTasks.BeginContext(handler => { }); 412 | 413 | AmbientTasks.Add(Task.FromException(new Exception())); 414 | 415 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.RanToCompletion); 416 | } 417 | 418 | [Test] 419 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 420 | public static void Handled_exceptions_do_not_appear_in_task_returned_from_WaitAllAsync_while_waiting_for_task() 421 | { 422 | var source = new TaskCompletionSource(); 423 | var exception = new Exception(); 424 | 425 | AmbientTasks.BeginContext(handler => { }); 426 | 427 | AmbientTasks.Add(source.Task); 428 | 429 | var waitAllTask = AmbientTasks.WaitAllAsync(); 430 | waitAllTask.IsCompleted.ShouldBeFalse(); 431 | 432 | source.SetException(exception); 433 | 434 | waitAllTask.Status.ShouldBe(TaskStatus.RanToCompletion); 435 | } 436 | 437 | [Test] 438 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 439 | public static void Handler_should_be_called_one_additional_time_to_handle_its_own_exception() 440 | { 441 | var taskException = new Exception(); 442 | var handlerException = new Exception(); 443 | var watcher = new CallbackWatcher(); 444 | 445 | AmbientTasks.BeginContext(ex => 446 | { 447 | watcher.OnCallback(out var callCount); 448 | 449 | if (callCount == 1) throw handlerException; 450 | 451 | ex.ShouldBe(handlerException); 452 | }); 453 | 454 | using (watcher.ExpectCallback(count: 2)) 455 | AmbientTasks.Add(Task.FromException(taskException)); 456 | 457 | var waitAllTask = AmbientTasks.WaitAllAsync(); 458 | 459 | waitAllTask.Status.ShouldBe(TaskStatus.Faulted); 460 | 461 | var aggregateException = waitAllTask.Exception!.InnerExceptions.ShouldHaveSingleItem().ShouldBeOfType(); 462 | 463 | aggregateException.InnerExceptions.ShouldHaveSingleItem().ShouldBeSameAs(taskException); 464 | } 465 | 466 | [Test] 467 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 468 | public static void WaitAllAsync_should_have_single_AggregateException_with_all_three_exceptions_when_handler_throws_exception_twice() 469 | { 470 | var taskException = new Exception(); 471 | var handlerException1 = new Exception(); 472 | var handlerException2 = new Exception(); 473 | var watcher = new CallbackWatcher(); 474 | 475 | AmbientTasks.BeginContext(ex => 476 | { 477 | watcher.OnCallback(out var callCount); 478 | 479 | throw callCount == 1 ? handlerException1 : handlerException2; 480 | }); 481 | 482 | using (watcher.ExpectCallback(count: 2)) 483 | AmbientTasks.Add(Task.FromException(taskException)); 484 | 485 | var waitAllTask = AmbientTasks.WaitAllAsync(); 486 | 487 | waitAllTask.Status.ShouldBe(TaskStatus.Faulted); 488 | var aggregateException = waitAllTask.Exception!.InnerExceptions.ShouldHaveSingleItem().ShouldBeOfType(); 489 | 490 | aggregateException.InnerExceptions.ShouldBe(new[] { taskException, handlerException1, handlerException2 }); 491 | } 492 | 493 | [Test] 494 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 495 | public static void BeginContext_handler_is_not_executed_using_synchronization_context() 496 | { 497 | using (SynchronizationContextAssert.ExpectNoPost()) 498 | { 499 | AmbientTasks.BeginContext(ex => { }); 500 | 501 | AmbientTasks.Add(Task.FromException(new Exception())); 502 | 503 | using (Utils.WithTemporarySynchronizationContext(null)) 504 | { 505 | AmbientTasks.Add(Task.FromException(new Exception())); 506 | } 507 | } 508 | } 509 | 510 | [Test] 511 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 512 | public static void BeginContext_handler_is_replaced_with_next_call() 513 | { 514 | AmbientTasks.BeginContext(ex => Assert.Fail("This handler should not be called.")); 515 | 516 | var watcher = new CallbackWatcher(); 517 | AmbientTasks.BeginContext(ex => watcher.OnCallback()); 518 | 519 | using (watcher.ExpectCallback()) 520 | AmbientTasks.Add(Task.FromException(new Exception())); 521 | } 522 | 523 | [Test] 524 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 525 | public static void BeginContext_handler_can_be_removed() 526 | { 527 | AmbientTasks.BeginContext(ex => Assert.Fail("This handler should not be called.")); 528 | 529 | AmbientTasks.BeginContext(); 530 | 531 | var exception = new Exception(); 532 | var aggregateException = Should.Throw( 533 | () => AmbientTasks.Add(Task.FromException(exception))); 534 | 535 | aggregateException.InnerExceptions.ShouldHaveSingleItem().ShouldBeSameAs(exception); 536 | 537 | AmbientTasks.WaitAllAsync().Status.ShouldBe(TaskStatus.Faulted); 538 | } 539 | 540 | [Test] 541 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 542 | public static async Task BeginContext_handler_flows_into_Task_Run() 543 | { 544 | var watcher = new CallbackWatcher(); 545 | AmbientTasks.BeginContext(ex => watcher.OnCallback()); 546 | 547 | await Task.Run(() => 548 | { 549 | using (watcher.ExpectCallback()) 550 | AmbientTasks.Add(Task.FromException(new Exception())); 551 | }); 552 | } 553 | 554 | [Test] 555 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 556 | public static async Task BeginContext_handler_flows_into_new_thread() 557 | { 558 | var source = new TaskCompletionSource(); 559 | var watcher = new CallbackWatcher(); 560 | 561 | var thread = new Thread(() => 562 | { 563 | try 564 | { 565 | using (watcher.ExpectCallback()) 566 | AmbientTasks.Add(Task.FromException(new Exception())); 567 | 568 | source.SetResult(null); 569 | } 570 | catch (Exception ex) 571 | { 572 | source.SetException(ex); 573 | } 574 | }); 575 | 576 | AmbientTasks.BeginContext(ex => watcher.OnCallback()); 577 | 578 | thread.Start(); 579 | 580 | await source.Task; 581 | } 582 | 583 | [Test] 584 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 585 | public static async Task BeginContext_handler_flows_into_ThreadPool_QueueUserWorkItem() 586 | { 587 | var watcher = new CallbackWatcher(); 588 | AmbientTasks.BeginContext(ex => watcher.OnCallback()); 589 | 590 | var source = new TaskCompletionSource(); 591 | 592 | ThreadPool.QueueUserWorkItem(_ => 593 | { 594 | try 595 | { 596 | using (watcher.ExpectCallback()) 597 | AmbientTasks.Add(Task.FromException(new Exception())); 598 | 599 | source.SetResult(null); 600 | } 601 | catch (Exception ex) 602 | { 603 | source.SetException(ex); 604 | } 605 | }, null); 606 | 607 | await source.Task; 608 | } 609 | 610 | [Test] 611 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 612 | public static async Task BeginContext_handler_flows_across_await() 613 | { 614 | var watcher = new CallbackWatcher(); 615 | AmbientTasks.BeginContext(ex => watcher.OnCallback()); 616 | 617 | await Task.Yield(); 618 | 619 | using (watcher.ExpectCallback()) 620 | AmbientTasks.Add(Task.FromException(new Exception())); 621 | } 622 | 623 | [Test] 624 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 625 | public static async Task BeginContext_handler_does_not_flow_from_inner_method_back_to_outer_method_after_await() 626 | { 627 | var watcher = new CallbackWatcher(); 628 | AmbientTasks.BeginContext(ex => watcher.OnCallback()); 629 | 630 | await InnerFunction(); 631 | 632 | using (watcher.ExpectCallback()) 633 | AmbientTasks.Add(Task.FromException(new Exception())); 634 | 635 | static async Task InnerFunction() 636 | { 637 | await Task.Yield(); 638 | AmbientTasks.BeginContext(ex => Assert.Fail()); 639 | } 640 | } 641 | 642 | [Test] 643 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 644 | public static void BeginContext_handler_can_be_called_recursively() 645 | { 646 | var exception1 = new Exception(); 647 | var exception2 = new Exception(); 648 | 649 | var depth = 0; 650 | var maxDepth = 0; 651 | 652 | AmbientTasks.BeginContext(ex => 653 | { 654 | depth++; 655 | if (maxDepth < depth) maxDepth = depth; 656 | try 657 | { 658 | if (ex == exception1) AmbientTasks.Add(Task.FromException(exception2)); 659 | } 660 | finally 661 | { 662 | depth--; 663 | } 664 | }); 665 | 666 | AmbientTasks.Add(Task.FromException(exception1)); 667 | 668 | maxDepth.ShouldBe(2); 669 | } 670 | 671 | [Test] 672 | [PreventExecutionContextLeaks] // Workaround for https://github.com/nunit/nunit/issues/3283 673 | public static void SynchronizationContext_that_throws_on_post_adds_exception_to_WaitAllAsync() 674 | { 675 | var source = new TaskCompletionSource(); 676 | 677 | var postException = new Exception(); 678 | var taskException = new Exception(); 679 | 680 | using (SynchronizationContextAssert.ExpectSinglePost(postedAction => throw postException)) 681 | { 682 | AmbientTasks.Add(source.Task); 683 | 684 | var waitAllTask = AmbientTasks.WaitAllAsync(); 685 | source.SetException(taskException); 686 | waitAllTask.Status.ShouldBe(TaskStatus.Faulted); 687 | 688 | var aggregateException = waitAllTask.Exception!.InnerExceptions.ShouldHaveSingleItem().ShouldBeOfType(); 689 | aggregateException.InnerExceptions.ShouldBe(new[] { taskException, postException }); 690 | } 691 | } 692 | } 693 | } 694 | -------------------------------------------------------------------------------- /src/AmbientTasks.Tests/CallbackWatcher.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | 4 | namespace Techsola 5 | { 6 | /// 7 | /// Provides idiomatic callback-based assertions. 8 | /// 9 | /// 10 | /// 11 | /// var watcher = new CallbackWatcher(); 12 | /// 13 | /// systemUnderTest.PropertyChanged += (sender, e) => watcher.OnCallback(); 14 | /// 15 | /// using (watcher.ExpectCallback()) 16 | /// { 17 | /// systemUnderTest.SomeProperty = 42; 18 | /// } // Fails if PropertyChanged did not fire 19 | /// 20 | /// systemUnderTest.SomeProperty = 42; // Fails if PropertyChanged fires 21 | /// 22 | /// 23 | public sealed class CallbackWatcher 24 | { 25 | private int expectedCount; 26 | private int actualCount; 27 | 28 | /// 29 | /// Begins expecting callbacks. When the returned scope is disposed, stops expecting callbacks and throws 30 | /// if was not called the expected number of times between beginning and ending. 31 | /// 32 | /// 33 | /// The number of times that is expected to be called before the returned scope is disposed. 34 | /// 35 | /// Thrown when is less than 1. 36 | /// Thrown when the returned scope from the previous call has not been disposed. 37 | /// Thrown when was not called the expected number of times between beginning and ending. 38 | public IDisposable ExpectCallback(int count = 1) 39 | { 40 | if (count < 1) 41 | throw new ArgumentOutOfRangeException(nameof(count), expectedCount, "Expected callback count must be greater than or equal to one."); 42 | 43 | if (expectedCount != 0) 44 | throw new InvalidOperationException($"The previous {nameof(ExpectCallback)} scope must be disposed before calling again."); 45 | 46 | expectedCount = count; 47 | actualCount = 0; 48 | 49 | return On.Dispose(() => 50 | { 51 | try 52 | { 53 | if (actualCount < expectedCount) 54 | Assert.Fail($"Expected {(expectedCount == 1 ? "a single call" : expectedCount + " calls")}, but there {(actualCount == 1 ? "was" : "were")} {actualCount}."); 55 | } 56 | finally 57 | { 58 | expectedCount = 0; 59 | } 60 | }); 61 | } 62 | 63 | /// 64 | /// Call this from the callback being tested. 65 | /// Throws if this watcher is not currently expecting a callback 66 | /// (see ) or if the expected number of callbacks has been exceeded. 67 | /// 68 | /// 69 | /// Thrown when this watcher is not currently expecting a callback (see ) 70 | /// or if the expected number of callbacks has been exceeded. 71 | /// 72 | public void OnCallback() => OnCallback(out _); 73 | 74 | /// 75 | /// Call this from the callback being tested. 76 | /// Throws if this watcher is not currently expecting a callback 77 | /// (see ) or if the expected number of callbacks has been exceeded. 78 | /// 79 | /// 80 | /// Thrown when this watcher is not currently expecting a callback (see ) 81 | /// or if the expected number of callbacks has been exceeded. 82 | /// 83 | public void OnCallback(out int callCount) 84 | { 85 | actualCount++; 86 | callCount = actualCount; 87 | 88 | if (actualCount > expectedCount) 89 | { 90 | Assert.Fail(expectedCount == 0 91 | ? "Expected no callback." 92 | : $"Expected {(expectedCount == 1 ? "a single call" : expectedCount + " calls")}, but there were more."); 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/AmbientTasks.Tests/On.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace Techsola 5 | { 6 | internal static class On 7 | { 8 | public static IDisposable Dispose(Action action) => new OnDisposeAction(action); 9 | 10 | private sealed class OnDisposeAction : IDisposable 11 | { 12 | private Action? action; 13 | 14 | public OnDisposeAction(Action action) 15 | { 16 | this.action = action ?? throw new ArgumentNullException(nameof(action)); 17 | } 18 | 19 | public void Dispose() => Interlocked.Exchange(ref action, null)?.Invoke(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/AmbientTasks.Tests/PreventExecutionContextLeaksAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading; 5 | using NUnit.Framework.Interfaces; 6 | using NUnit.Framework.Internal; 7 | using NUnit.Framework.Internal.Commands; 8 | using Techsola; 9 | 10 | [assembly: RequireOnAllTestMethods(typeof(PreventExecutionContextLeaksAttribute))] 11 | 12 | namespace Techsola 13 | { 14 | /// 15 | /// Workaround for https://github.com/nunit/nunit/issues/3283. 16 | /// 17 | [AttributeUsage(AttributeTargets.Method)] 18 | public sealed class PreventExecutionContextLeaksAttribute : Attribute, IWrapSetUpTearDown 19 | { 20 | public TestCommand Wrap(TestCommand command) => new ExecuteInIsolatedExecutionContextCommand(command); 21 | 22 | private sealed class ExecuteInIsolatedExecutionContextCommand : DelegatingTestCommand 23 | { 24 | public ExecuteInIsolatedExecutionContextCommand(TestCommand innerCommand) : base(innerCommand) 25 | { 26 | } 27 | 28 | [DebuggerNonUserCode] 29 | public override TestResult Execute(TestExecutionContext context) 30 | { 31 | using var copy = ExecutionContext.Capture()?.CreateCopy() 32 | ?? throw new NotImplementedException(); 33 | 34 | var returnValue = new StrongBox(); 35 | ExecutionContext.Run(copy, Execute, state: (context, returnValue)); 36 | return returnValue.Value!; 37 | } 38 | 39 | [DebuggerNonUserCode] 40 | private void Execute(object? state) 41 | { 42 | var (context, returnValue) = ((TestExecutionContext, StrongBox))state!; 43 | 44 | returnValue.Value = innerCommand.Execute(context); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/AmbientTasks.Tests/RequireOnAllTestMethodsAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NUnit.Framework; 3 | using NUnit.Framework.Interfaces; 4 | using System.Linq; 5 | 6 | namespace Techsola 7 | { 8 | [AttributeUsage(AttributeTargets.Assembly | AttributeTargets.Class | AttributeTargets.Method)] 9 | public sealed class RequireOnAllTestMethodsAttribute : Attribute, ITestAction 10 | { 11 | private readonly Type[] attributeTypes; 12 | 13 | public RequireOnAllTestMethodsAttribute(params Type[] attributeTypes) 14 | { 15 | this.attributeTypes = attributeTypes; 16 | } 17 | 18 | public ActionTargets Targets => ActionTargets.Test; 19 | 20 | public void BeforeTest(ITest test) 21 | { 22 | var missingTypes = attributeTypes 23 | .Except( 24 | from data in test.Method!.MethodInfo.GetCustomAttributesData() 25 | select data.AttributeType) 26 | .ToList(); 27 | 28 | if (missingTypes.Any()) 29 | { 30 | Assert.Fail("The test method is missing " + string.Join(", ", missingTypes) + "."); 31 | } 32 | } 33 | 34 | public void AfterTest(ITest test) 35 | { 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/AmbientTasks.Tests/SynchronizationContextAssert.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using NUnit.Framework; 4 | 5 | namespace Techsola 6 | { 7 | public static class SynchronizationContextAssert 8 | { 9 | public static IDisposable ExpectNoPost() 10 | { 11 | var context = new MockSynchronizationContext(testPostedAction: null); 12 | 13 | return Utils.WithTemporarySynchronizationContext(context); 14 | } 15 | 16 | public static IDisposable ExpectSinglePost(Action testPostedAction) 17 | { 18 | if (testPostedAction is null) throw new ArgumentNullException(nameof(testPostedAction)); 19 | 20 | var context = new MockSynchronizationContext(testPostedAction); 21 | var withTempContext = Utils.WithTemporarySynchronizationContext(context); 22 | 23 | return On.Dispose(() => 24 | { 25 | withTempContext.Dispose(); 26 | if (!context.ReceivedPost) Assert.Fail("Expected a call to SynchronizationContext.Post."); 27 | }); 28 | } 29 | 30 | private sealed class MockSynchronizationContext : SynchronizationContext 31 | { 32 | private readonly Action? testPostedAction; 33 | 34 | public MockSynchronizationContext(Action? testPostedAction) 35 | { 36 | this.testPostedAction = testPostedAction; 37 | } 38 | 39 | public bool ReceivedPost { get; private set; } 40 | 41 | public override void Send(SendOrPostCallback d, object? state) 42 | { 43 | Assert.Fail("Expected no call to SynchronizationContext.Send."); 44 | } 45 | 46 | public override void Post(SendOrPostCallback d, object? state) 47 | { 48 | if (testPostedAction is null) Assert.Fail("Expected no calls to SynchronizationContext.Post."); 49 | if (ReceivedPost) Assert.Fail("Expected no more than one call to SynchronizationContext.Post."); 50 | ReceivedPost = true; 51 | 52 | testPostedAction!.Invoke(() => d.Invoke(state)); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/AmbientTasks.Tests/Utils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace Techsola 5 | { 6 | internal static class Utils 7 | { 8 | public static IDisposable WithTemporarySynchronizationContext(SynchronizationContext? context) 9 | { 10 | var originalSynchronizationContext = SynchronizationContext.Current; 11 | SynchronizationContext.SetSynchronizationContext(context); 12 | 13 | return On.Dispose(() => 14 | { 15 | if (SynchronizationContext.Current == context) 16 | SynchronizationContext.SetSynchronizationContext(originalSynchronizationContext); 17 | }); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/AmbientTasks.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Techsola/AmbientTasks/d8385b92b00239252ac8aeb7753ff9d2d50f1a5e/src/AmbientTasks.snk -------------------------------------------------------------------------------- /src/AmbientTasks/AmbientTasks.AmbientTaskContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace Techsola 8 | { 9 | partial class AmbientTasks 10 | { 11 | private sealed class AmbientTaskContext 12 | { 13 | private readonly Action? exceptionHandler; 14 | 15 | /// 16 | /// Doubles as a lockable object for all access to , , 17 | /// and . 18 | /// 19 | private readonly List bufferedExceptions = new List(); 20 | private int currentTaskCount; 21 | private TaskCompletionSource? waitAllSource; 22 | 23 | public AmbientTaskContext(Action? exceptionHandler) 24 | { 25 | this.exceptionHandler = exceptionHandler; 26 | } 27 | 28 | [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "All exceptions are caught and passed to the appropriate handler by design.")] 29 | public bool RecordAndTrySuppress(IReadOnlyCollection exceptions) 30 | { 31 | if (exceptionHandler is null) 32 | { 33 | lock (bufferedExceptions) 34 | { 35 | // Don't wrap yet. WaitAllAsync will wrap in AggregateException after collecting unhandled exceptions. 36 | bufferedExceptions.AddRange(exceptions); 37 | } 38 | 39 | return false; 40 | } 41 | 42 | try 43 | { 44 | // The top-level handler should not normally care about the exception type, so conditionally 45 | // wrapping should not be a pit of failure like it is in situations where you need to catch specific 46 | // exceptions. 47 | exceptionHandler.Invoke(exceptions.Count == 1 48 | ? exceptions.Single() 49 | : new AggregateException(exceptions)); 50 | } 51 | catch (Exception handlerException) 52 | { 53 | lock (bufferedExceptions) 54 | { 55 | // Don't wrap yet. WaitAllAsync will wrap in AggregateException after collecting unhandled exceptions. 56 | bufferedExceptions.AddRange(exceptions); 57 | } 58 | 59 | try 60 | { 61 | exceptionHandler.Invoke(handlerException); 62 | } 63 | catch (Exception secondHandlerException) 64 | { 65 | lock (bufferedExceptions) 66 | { 67 | bufferedExceptions.Add(handlerException); 68 | bufferedExceptions.Add(secondHandlerException); 69 | } 70 | } 71 | } 72 | 73 | return true; 74 | } 75 | 76 | public void StartTask() 77 | { 78 | lock (bufferedExceptions) 79 | { 80 | currentTaskCount = checked(currentTaskCount + 1); 81 | } 82 | } 83 | 84 | public void EndTask() 85 | { 86 | TaskCompletionSource? sourceToComplete; 87 | Exception[] endingExceptions; 88 | 89 | lock (bufferedExceptions) 90 | { 91 | var newCount = currentTaskCount - 1; 92 | if (newCount < 0) throw new InvalidOperationException($"More calls to {nameof(EndTask)} than {nameof(StartTask)}."); 93 | currentTaskCount = newCount; 94 | if (newCount > 0) return; 95 | 96 | sourceToComplete = waitAllSource; 97 | if (sourceToComplete is null) return; // No one is waiting 98 | waitAllSource = null; 99 | 100 | endingExceptions = bufferedExceptions.ToArray(); 101 | bufferedExceptions.Clear(); 102 | } 103 | 104 | // Do not set the source inside the lock. Arbitrary user continuations may have been set on 105 | // sourceToComplete.Task since it was previously returned from WaitAllAsync, and executing arbitrary 106 | // user code within a lock is a very bad idea. 107 | if (endingExceptions.Any()) 108 | sourceToComplete.SetException(new AggregateException(endingExceptions)); 109 | else 110 | sourceToComplete.SetResult(null); 111 | } 112 | 113 | public Task WaitAllAsync() 114 | { 115 | lock (bufferedExceptions) 116 | { 117 | if (waitAllSource != null) return waitAllSource.Task; 118 | 119 | if (currentTaskCount > 0) 120 | { 121 | waitAllSource = new TaskCompletionSource(); 122 | return waitAllSource.Task; 123 | } 124 | 125 | if (bufferedExceptions.Any()) 126 | { 127 | var source = new TaskCompletionSource(); 128 | source.SetException(new AggregateException(bufferedExceptions)); 129 | bufferedExceptions.Clear(); 130 | return source.Task; 131 | } 132 | } 133 | 134 | return Task.CompletedTask; 135 | } 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/AmbientTasks/AmbientTasks.PostClosure.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | 3 | namespace Techsola 4 | { 5 | public static partial class AmbientTasks 6 | { 7 | private sealed class PostClosure 8 | { 9 | private int wasInvoked; 10 | 11 | public AmbientTaskContext Context { get; } 12 | public TState State { get; } 13 | 14 | public PostClosure(AmbientTaskContext context, TState state) 15 | { 16 | Context = context; 17 | State = state; 18 | } 19 | 20 | public bool TryClaimInvocation() => Interlocked.Exchange(ref wasInvoked, 1) == 0; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AmbientTasks/AmbientTasks.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Runtime.ExceptionServices; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Techsola 9 | { 10 | /// 11 | /// Enables scoped completion tracking and error handling of tasks as an alternative to fire-and-forget and async 12 | /// void. Easy to produce and consume, and test-friendly. 13 | /// 14 | public static partial class AmbientTasks 15 | { 16 | private static readonly AsyncLocal Context = new AsyncLocal(); 17 | 18 | private static AmbientTaskContext CurrentContext => Context.Value ??= new AmbientTaskContext(exceptionHandler: null); 19 | 20 | /// 21 | /// 22 | /// Replaces the current async-local scope with a new scope which has its own exception handler and isolated set 23 | /// of tracked tasks. 24 | /// 25 | /// If is , exceptions will be left uncaught. In 26 | /// the case of tracked objects, the exception will be rethrown on the synchronization 27 | /// context which began tracking it. 28 | /// 29 | /// 30 | public static void BeginContext(Action? exceptionHandler = null) 31 | { 32 | Context.Value = new AmbientTaskContext(exceptionHandler); 33 | } 34 | 35 | /// 36 | /// Waits until all tracked tasks are complete. Any exceptions that were not handled, including exceptions 37 | /// thrown by an exception handler, will be included as inner exceptions of the 38 | /// property. 39 | /// 40 | public static Task WaitAllAsync() => CurrentContext.WaitAllAsync(); 41 | 42 | /// 43 | /// 44 | /// Begins tracking a so that any exception is handled and so that 45 | /// waits for its completion. 46 | /// 47 | /// 48 | /// Once passed to this method, a task’s exception will never be unobserved. If the task faults or is already 49 | /// faulted and an exception handler is currently registered (see ), the handler will 50 | /// receive the task’s . If no handler has been registered, the will be rethrown on the that was current 52 | /// when was called. (If there was no synchronization context, it will be rethrown 53 | /// immediately by a continuation requesting .) 54 | /// 55 | /// 56 | public static void Add(Task? task) 57 | { 58 | if (task is null) return; 59 | 60 | switch (task.Status) 61 | { 62 | case TaskStatus.Canceled: 63 | case TaskStatus.RanToCompletion: 64 | break; 65 | 66 | case TaskStatus.Faulted: 67 | OnTaskCompleted(task, state: (CurrentContext, SynchronizationContext.Current, taskWasStarted: false)); 68 | break; 69 | 70 | default: 71 | var context = CurrentContext; 72 | context.StartTask(); 73 | task.ContinueWith( 74 | OnTaskCompleted, 75 | state: (context, SynchronizationContext.Current, taskWasStarted: true), 76 | CancellationToken.None, 77 | TaskContinuationOptions.ExecuteSynchronously, 78 | TaskScheduler.Default); 79 | break; 80 | } 81 | } 82 | 83 | private static void OnTaskCompleted(Task completedTask, object? state) 84 | { 85 | var (context, addSynchronizationContext, taskWasStarted) = ((AmbientTaskContext, SynchronizationContext, bool))state!; 86 | try 87 | { 88 | if (completedTask.IsFaulted) 89 | { 90 | if (!context.RecordAndTrySuppress(completedTask.Exception!.InnerExceptions)) 91 | { 92 | var exceptionInfo = ExceptionDispatchInfo.Capture(completedTask.Exception); 93 | 94 | if (addSynchronizationContext is null) 95 | { 96 | OnTaskFaultWithoutHandler(exceptionInfo); 97 | } 98 | else 99 | { 100 | try 101 | { 102 | addSynchronizationContext.Post(OnTaskFaultWithoutHandler, state: exceptionInfo); 103 | } 104 | catch (Exception postException) when (context.RecordAndTrySuppress(new[] { postException })) 105 | { 106 | } 107 | } 108 | } 109 | } 110 | } 111 | finally 112 | { 113 | if (taskWasStarted) context.EndTask(); 114 | } 115 | } 116 | 117 | private static void OnTaskFaultWithoutHandler(object? state) 118 | { 119 | ((ExceptionDispatchInfo)state!).Throw(); 120 | } 121 | 122 | /// 123 | /// 124 | /// Invokes a delegate immediately and begins tracking the returned so that any exception is 125 | /// handled and so that waits for its completion. If the delegate throws rather than 126 | /// returning a task, the exception is wrapped in a instance and treated the same as if the 127 | /// task had been returned from the delegate. 128 | /// 129 | /// 130 | /// Once passed to this method, a task’s exception will never be unobserved. If the task faults or is already 131 | /// faulted and an exception handler is currently registered (see ), the handler will 132 | /// receive the task’s . If no handler has been registered, the will be rethrown on the that was current 134 | /// when was called. (If there was no synchronization context, it will be rethrown 135 | /// immediately by a continuation requesting .) 136 | /// 137 | /// 138 | public static void Add(Func? functionToInvoke) 139 | { 140 | if (functionToInvoke is null) return; 141 | 142 | Add(InvokeWithExceptionsWrapped(functionToInvoke)); 143 | } 144 | 145 | [SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exceptions must be wrapped.")] 146 | private static Task InvokeWithExceptionsWrapped(Func function) 147 | { 148 | try 149 | { 150 | return function.Invoke(); 151 | } 152 | catch (OperationCanceledException) 153 | { 154 | var source = new TaskCompletionSource(); 155 | source.SetCanceled(); 156 | return source.Task; 157 | } 158 | catch (Exception ex) 159 | { 160 | return Task.FromException(ex); 161 | } 162 | } 163 | 164 | /// 165 | /// 166 | /// Executes the specified delegate on the current while tracking so that 167 | /// any exception is handled and so that waits for its completion. 168 | /// 169 | /// 170 | /// A default is installed if the current one is . 171 | /// 172 | /// 173 | /// If an exception handler has been registered (see ), any exception will be caught 174 | /// and routed to the handler instead of . If no handler has been registered, the 175 | /// exception will not be caught even though it will be recorded and thrown by . 176 | /// 177 | /// 178 | public static void Post(SendOrPostCallback? d, object? state) 179 | { 180 | // Install a default synchronization context if one does not exist 181 | Post(AsyncOperationManager.SynchronizationContext, d, state); 182 | } 183 | 184 | /// 185 | /// 186 | /// Executes the specified delegate on the current while tracking so that 187 | /// any exception is handled and so that waits for its completion. 188 | /// 189 | /// 190 | /// is thrown if is . 192 | /// 193 | /// 194 | /// If an exception handler has been registered (see ), any exception will be caught 195 | /// and routed to the handler instead of . If no handler has been registered, the 196 | /// exception will not be caught even though it will be recorded and thrown by . 197 | /// 198 | /// 199 | public static void Post(SynchronizationContext synchronizationContext, SendOrPostCallback? d, object? state) 200 | { 201 | if (synchronizationContext is null) 202 | throw new ArgumentNullException(nameof(synchronizationContext)); 203 | 204 | if (d is null) return; 205 | 206 | var context = CurrentContext; 207 | context.StartTask(); 208 | 209 | var closure = new PostClosure<(SendOrPostCallback, object?)>(context, (d, state)); 210 | 211 | var postReturned = false; 212 | try 213 | { 214 | synchronizationContext.Post(OnPost, closure); 215 | postReturned = true; 216 | } 217 | finally 218 | { 219 | if (!postReturned && closure.TryClaimInvocation()) 220 | { 221 | context.EndTask(); 222 | } 223 | } 224 | } 225 | 226 | private static void OnPost(object? state) 227 | { 228 | var closure = (PostClosure<(SendOrPostCallback d, object? state)>)state!; 229 | if (!closure.TryClaimInvocation()) return; 230 | try 231 | { 232 | closure.State.d.Invoke(closure.State.state); 233 | } 234 | catch (Exception ex) when (closure.Context.RecordAndTrySuppress(new[] { ex })) 235 | { 236 | } 237 | finally 238 | { 239 | closure.Context.EndTask(); 240 | } 241 | } 242 | 243 | /// 244 | /// 245 | /// Executes the specified delegate on the current while tracking so that 246 | /// any exception is handled and so that waits for its completion. 247 | /// 248 | /// 249 | /// A default is installed if the current one is . 250 | /// 251 | /// 252 | /// If an exception handler has been registered (see ), any exception will be caught 253 | /// and routed to the handler instead of . If no handler has been registered, the 254 | /// exception will not be caught even though it will be recorded and thrown by . 255 | /// 256 | /// 257 | public static void Post(Action? postCallbackAction) 258 | { 259 | // Install a default synchronization context if one does not exist 260 | Post(AsyncOperationManager.SynchronizationContext, postCallbackAction); 261 | } 262 | 263 | /// 264 | /// 265 | /// Executes the specified delegate on the current while tracking so that 266 | /// any exception is handled and so that waits for its completion. 267 | /// 268 | /// 269 | /// is thrown if is . 271 | /// 272 | /// 273 | /// If an exception handler has been registered (see ), any exception will be caught 274 | /// and routed to the handler instead of . If no handler has been registered, the 275 | /// exception will not be caught even though it will be recorded and thrown by . 276 | /// 277 | /// 278 | public static void Post(SynchronizationContext synchronizationContext, Action? postCallbackAction) 279 | { 280 | if (synchronizationContext is null) 281 | throw new ArgumentNullException(nameof(synchronizationContext)); 282 | 283 | if (postCallbackAction is null) return; 284 | 285 | var context = CurrentContext; 286 | context.StartTask(); 287 | 288 | var closure = new PostClosure(context, postCallbackAction); 289 | 290 | var postReturned = false; 291 | try 292 | { 293 | synchronizationContext.Post(OnPostAction, closure); 294 | postReturned = true; 295 | } 296 | finally 297 | { 298 | if (!postReturned && closure.TryClaimInvocation()) 299 | { 300 | context.EndTask(); 301 | } 302 | } 303 | } 304 | 305 | private static void OnPostAction(object? state) 306 | { 307 | var closure = (PostClosure)state!; 308 | if (!closure.TryClaimInvocation()) return; 309 | try 310 | { 311 | closure.State.Invoke(); 312 | } 313 | catch (Exception ex) when (closure.Context.RecordAndTrySuppress(new[] { ex })) 314 | { 315 | } 316 | finally 317 | { 318 | closure.Context.EndTask(); 319 | } 320 | } 321 | 322 | /// 323 | /// 324 | /// Executes the specified delegate on the current while tracking so that 325 | /// any exception is handled and so that waits for completion of the returned task. 326 | /// 327 | /// 328 | /// A default is installed if the current one is . 329 | /// 330 | /// 331 | /// If an exception handler has been registered (see ), any exception will be caught 332 | /// and routed to the handler instead of . If no handler has been registered, the 333 | /// exception will not be caught even though it will be recorded and thrown by . 334 | /// 335 | /// 336 | public static void Post(Func? postCallbackAsyncAction) 337 | { 338 | // Install a default synchronization context if one does not exist 339 | Post(AsyncOperationManager.SynchronizationContext, postCallbackAsyncAction); 340 | } 341 | 342 | /// 343 | /// 344 | /// Executes the specified delegate on the current while tracking so that 345 | /// any exception is handled and so that waits for completion of the returned task. 346 | /// 347 | /// 348 | /// is thrown if is . 350 | /// 351 | /// 352 | /// If an exception handler has been registered (see ), any exception will be caught 353 | /// and routed to the handler instead of . If no handler has been registered, the 354 | /// exception will not be caught even though it will be recorded and thrown by . 355 | /// 356 | /// 357 | public static void Post(SynchronizationContext synchronizationContext, Func? postCallbackAsyncAction) 358 | { 359 | if (synchronizationContext is null) 360 | throw new ArgumentNullException(nameof(synchronizationContext)); 361 | 362 | if (postCallbackAsyncAction is null) return; 363 | 364 | var context = CurrentContext; 365 | context.StartTask(); 366 | 367 | var closure = new PostClosure>(context, postCallbackAsyncAction); 368 | 369 | var postReturned = false; 370 | try 371 | { 372 | synchronizationContext.Post(OnPostAsyncAction, closure); 373 | postReturned = true; 374 | } 375 | finally 376 | { 377 | if (!postReturned && closure.TryClaimInvocation()) 378 | { 379 | context.EndTask(); 380 | } 381 | } 382 | } 383 | 384 | private static void OnPostAsyncAction(object? state) 385 | { 386 | var closure = (PostClosure>)state!; 387 | if (!closure.TryClaimInvocation()) return; 388 | 389 | try 390 | { 391 | Add(closure.State.Invoke()); 392 | } 393 | catch (Exception ex) when (closure.Context.RecordAndTrySuppress(new[] { ex })) 394 | { 395 | } 396 | finally 397 | { 398 | closure.Context.EndTask(); 399 | } 400 | } 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /src/AmbientTasks/AmbientTasks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | Techsola 6 | true 7 | true 8 | ..\AmbientTasks.snk 9 | 10 | 1.0.1 11 | Debug symbols are no longer in the NuGet package and are now published to the NuGet symbol location that is built in to Visual Studio. See the readme to load debug symbols for prerelease builds from MyGet. 12 | 13 | (https://github.com/Techsola/AmbientTasks/blob/v1.0.1/CHANGELOG.md#101---2021-01-10) 14 | Technology Solutions Associates 15 | Copyright © 2019–2021 Technology Solutions Associates 16 | MIT 17 | https://github.com/Techsola/AmbientTasks 18 | https://github.com/Techsola/AmbientTasks 19 | git 20 | async task void post background ambient scope wait track handle error 21 | Enables scoped completion tracking and error handling of tasks as an alternative to fire-and-forget and async void. Easy to produce and consume, and test-friendly. 22 | true 23 | true 24 | snupkg 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | latest 5 | enable 6 | latest 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | --------------------------------------------------------------------------------