├── .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 [](https://www.nuget.org/packages/AmbientTasks/ "NuGet (releases)") [](https://www.myget.org/feed/ambienttasks/package/nuget/AmbientTasks "MyGet (prereleases)") [](https://gitter.im/Techsola/AmbientTasks "Chat on Gitter") [](https://github.com/Techsola/AmbientTasks/actions?query=workflow%3ACI "Build status") [](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