├── .gitattributes
├── .gitignore
├── Benchmark
├── Benchmark.csproj
├── DoNothingStartupService.cs
├── DummyService.cs
├── Program.cs
└── appsettings.json
├── LICENSE
├── README.md
├── ReleaseNotes.md
├── RunMethodsSequentially
├── IStartupServiceToRunSequentially.cs
├── LockAndRunCode
│ ├── DatabaseHelpers.cs
│ ├── FileSystemDoesDirectoryExist.cs
│ ├── FileSystemLockAndRunJob.cs
│ ├── GetLockAndThenRunHostedService.cs
│ ├── GetLockAndThenRunServices.cs
│ ├── IGetLockAndThenRunServices.cs
│ ├── ILockAndRunJob.cs
│ ├── IPreLockTest.cs
│ ├── JobRunner.cs
│ ├── NoLockAndRunJob.cs
│ ├── NoLockPreLockTest.cs
│ ├── PostGreSqlDoesDatabaseExist.cs
│ ├── PostGreSqlLockAndRunJob.cs
│ ├── SqlServerDoesDatabaseExist.cs
│ ├── SqlServerLockAndRunJob.cs
│ └── TryLockVersion.cs
├── RegisterRunMethodsSequentiallyTester.cs
├── RunMethodsSequentially.csproj
├── RunMethodsSequentiallyNuGetIcon.png
├── RunSequentiallyException.cs
├── RunSequentiallyOptions.cs
└── StartupExtentions.cs
├── RunStartupMethodsSequentially.sln
├── Test
├── EfCore
│ ├── CommonNameDateTime.cs
│ ├── NameDateTime.cs
│ └── TestDbContext.cs
├── Helpers
│ ├── ParallelExtensions.cs
│ ├── RegisterRunHelpers.cs
│ ├── RegisterTestLogger.cs
│ └── SqlServerHelpers.cs
├── ServicesToCall
│ ├── SqlServerEnsureCreatedDatabaseOnly.cs
│ ├── SqlServerMigrateDbContextService.cs
│ ├── UpdateDatabase1.cs
│ ├── UpdateDatabase2.cs
│ ├── UpdateDatabaseUseScoped1.cs
│ ├── UpdateDatabaseUseScoped2.cs
│ ├── UpdateThatTakes800Milliseconds1.cs
│ ├── UpdateThatTakes800Milliseconds2.cs
│ ├── UpdateWithDelay.cs
│ ├── UpdateWithNegativeOrderNum.cs
│ ├── UpdateWithPositiveOrderNum.cs
│ └── UpdateWithZeroOrderNum.cs
├── Test.csproj
├── TestData
│ └── DummyFile.txt
├── UnitTests
│ ├── TestFileSystemVersion.cs
│ ├── TestLogging.cs
│ ├── TestNoLockVersion.cs
│ ├── TestPostGreSql.cs
│ ├── TestPostGreSqlLocks.cs
│ ├── TestRegisterRunMethodsSequentiallyTester.cs
│ ├── TestSqlServerHelpers.cs
│ ├── TestSqlServerLocks.cs
│ └── TestSqlServerParallelLocks.cs
└── appsettings.json
└── WebSiteRunSequentially
├── Controllers
└── HomeController.cs
├── Database
├── CommonNameDateTime.cs
├── NameDateTime.cs
└── WebSiteDbContext.cs
├── Models
├── CommonLogsDto.cs
└── ErrorViewModel.cs
├── Program.cs
├── Properties
├── ServiceDependencies
│ └── WebSiteRunSequentially - Web Deploy
│ │ ├── mssql1.arm.json
│ │ └── profile.arm.json
├── launchSettings.json
├── serviceDependencies.WebSiteRunSequentially - Web Deploy.json
└── serviceDependencies.json
├── StartupServices
├── StartupServiceEnsureCreated.cs
├── StartupServiceSeedDatabase.cs
└── StartupServiceThrowException.cs
├── Views
├── Home
│ ├── Index.cshtml
│ └── Privacy.cshtml
├── Shared
│ ├── Error.cshtml
│ ├── _Layout.cshtml
│ ├── _Layout.cshtml.css
│ └── _ValidationScriptsPartial.cshtml
├── _ViewImports.cshtml
└── _ViewStart.cshtml
├── WebSiteRunSequentially.csproj
├── appsettings.Development.json
├── appsettings.json
└── wwwroot
├── css
└── site.css
├── favicon.ico
├── js
└── site.js
└── lib
├── bootstrap
├── LICENSE
└── dist
│ ├── css
│ ├── bootstrap-grid.css
│ ├── bootstrap-grid.css.map
│ ├── bootstrap-grid.min.css
│ ├── bootstrap-grid.min.css.map
│ ├── bootstrap-grid.rtl.css
│ ├── bootstrap-grid.rtl.css.map
│ ├── bootstrap-grid.rtl.min.css
│ ├── bootstrap-grid.rtl.min.css.map
│ ├── bootstrap-reboot.css
│ ├── bootstrap-reboot.css.map
│ ├── bootstrap-reboot.min.css
│ ├── bootstrap-reboot.min.css.map
│ ├── bootstrap-reboot.rtl.css
│ ├── bootstrap-reboot.rtl.css.map
│ ├── bootstrap-reboot.rtl.min.css
│ ├── bootstrap-reboot.rtl.min.css.map
│ ├── bootstrap-utilities.css
│ ├── bootstrap-utilities.css.map
│ ├── bootstrap-utilities.min.css
│ ├── bootstrap-utilities.min.css.map
│ ├── bootstrap-utilities.rtl.css
│ ├── bootstrap-utilities.rtl.css.map
│ ├── bootstrap-utilities.rtl.min.css
│ ├── bootstrap-utilities.rtl.min.css.map
│ ├── bootstrap.css
│ ├── bootstrap.css.map
│ ├── bootstrap.min.css
│ ├── bootstrap.min.css.map
│ ├── bootstrap.rtl.css
│ ├── bootstrap.rtl.css.map
│ ├── bootstrap.rtl.min.css
│ └── bootstrap.rtl.min.css.map
│ └── js
│ ├── bootstrap.bundle.js
│ ├── bootstrap.bundle.js.map
│ ├── bootstrap.bundle.min.js
│ ├── bootstrap.bundle.min.js.map
│ ├── bootstrap.esm.js
│ ├── bootstrap.esm.js.map
│ ├── bootstrap.esm.min.js
│ ├── bootstrap.esm.min.js.map
│ ├── bootstrap.js
│ ├── bootstrap.js.map
│ ├── bootstrap.min.js
│ └── bootstrap.min.js.map
├── jquery-validation-unobtrusive
├── LICENSE.txt
├── jquery.validate.unobtrusive.js
└── jquery.validate.unobtrusive.min.js
├── jquery-validation
├── LICENSE.md
└── dist
│ ├── additional-methods.js
│ ├── additional-methods.min.js
│ ├── jquery.validate.js
│ └── jquery.validate.min.js
└── jquery
├── LICENSE.txt
└── dist
├── jquery.js
├── jquery.min.js
└── jquery.min.map
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
5 |
6 | ###############################################################################
7 | # Set default behavior for command prompt diff.
8 | #
9 | # This is need for earlier builds of msysgit that does not have it on by
10 | # default for csharp files.
11 | # Note: This is only used by command line
12 | ###############################################################################
13 | #*.cs diff=csharp
14 |
15 | ###############################################################################
16 | # Set the merge driver for project and solution files
17 | #
18 | # Merging from the command prompt will add diff markers to the files if there
19 | # are conflicts (Merging from VS is not affected by the settings below, in VS
20 | # the diff markers are never inserted). Diff markers may cause the following
21 | # file extensions to fail to load in VS. An alternative would be to treat
22 | # these files as binary and thus will always conflict and require user
23 | # intervention with every merge. To do so, just uncomment the entries below
24 | ###############################################################################
25 | #*.sln merge=binary
26 | #*.csproj merge=binary
27 | #*.vbproj merge=binary
28 | #*.vcxproj merge=binary
29 | #*.vcproj merge=binary
30 | #*.dbproj merge=binary
31 | #*.fsproj merge=binary
32 | #*.lsproj merge=binary
33 | #*.wixproj merge=binary
34 | #*.modelproj merge=binary
35 | #*.sqlproj merge=binary
36 | #*.wwaproj merge=binary
37 |
38 | ###############################################################################
39 | # behavior for image files
40 | #
41 | # image files are treated as binary by default.
42 | ###############################################################################
43 | #*.jpg binary
44 | #*.png binary
45 | #*.gif binary
46 |
47 | ###############################################################################
48 | # diff behavior for common document formats
49 | #
50 | # Convert binary document formats to text before diffing them. This feature
51 | # is only available from the command line. Turn it on by uncommenting the
52 | # entries below.
53 | ###############################################################################
54 | #*.doc diff=astextplain
55 | #*.DOC diff=astextplain
56 | #*.docx diff=astextplain
57 | #*.DOCX diff=astextplain
58 | #*.dot diff=astextplain
59 | #*.DOT diff=astextplain
60 | #*.pdf diff=astextplain
61 | #*.PDF diff=astextplain
62 | #*.rtf diff=astextplain
63 | #*.RTF diff=astextplain
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Oo]ut/
33 | [Ll]og/
34 | [Ll]ogs/
35 |
36 | # Visual Studio 2015/2017 cache/options directory
37 | .vs/
38 | # Uncomment if you have tasks that create the project's static files in wwwroot
39 | #wwwroot/
40 |
41 | # Visual Studio 2017 auto generated files
42 | Generated\ Files/
43 |
44 | # MSTest test Results
45 | [Tt]est[Rr]esult*/
46 | [Bb]uild[Ll]og.*
47 |
48 | # NUnit
49 | *.VisualState.xml
50 | TestResult.xml
51 | nunit-*.xml
52 |
53 | # Build Results of an ATL Project
54 | [Dd]ebugPS/
55 | [Rr]eleasePS/
56 | dlldata.c
57 |
58 | # Benchmark Results
59 | BenchmarkDotNet.Artifacts/
60 |
61 | # .NET Core
62 | project.lock.json
63 | project.fragment.lock.json
64 | artifacts/
65 |
66 | # ASP.NET Scaffolding
67 | ScaffoldingReadMe.txt
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 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio LightSwitch build output
298 | **/*.HTMLClient/GeneratedArtifacts
299 | **/*.DesktopClient/GeneratedArtifacts
300 | **/*.DesktopClient/ModelManifest.xml
301 | **/*.Server/GeneratedArtifacts
302 | **/*.Server/ModelManifest.xml
303 | _Pvt_Extensions
304 |
305 | # Paket dependency manager
306 | .paket/paket.exe
307 | paket-files/
308 |
309 | # FAKE - F# Make
310 | .fake/
311 |
312 | # CodeRush personal settings
313 | .cr/personal
314 |
315 | # Python Tools for Visual Studio (PTVS)
316 | __pycache__/
317 | *.pyc
318 |
319 | # Cake - Uncomment if you are using it
320 | # tools/**
321 | # !tools/packages.config
322 |
323 | # Tabs Studio
324 | *.tss
325 |
326 | # Telerik's JustMock configuration file
327 | *.jmconfig
328 |
329 | # BizTalk build output
330 | *.btp.cs
331 | *.btm.cs
332 | *.odx.cs
333 | *.xsd.cs
334 |
335 | # OpenCover UI analysis results
336 | OpenCover/
337 |
338 | # Azure Stream Analytics local run output
339 | ASALocalRun/
340 |
341 | # MSBuild Binary and Structured Log
342 | *.binlog
343 |
344 | # NVidia Nsight GPU debugger configuration file
345 | *.nvuser
346 |
347 | # MFractors (Xamarin productivity tool) working folder
348 | .mfractor/
349 |
350 | # Local History for Visual Studio
351 | .localhistory/
352 |
353 | # BeatPulse healthcheck temp database
354 | healthchecksdb
355 |
356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
357 | MigrationBackup/
358 |
359 | # Ionide (cross platform F# VS Code tools) working folder
360 | .ionide/
361 |
362 | # Fody - auto-generated XML schema
363 | FodyWeavers.xsd
--------------------------------------------------------------------------------
/Benchmark/Benchmark.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net6.0
6 | enable
7 | enable
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/Benchmark/DoNothingStartupService.cs:
--------------------------------------------------------------------------------
1 | using RunMethodsSequentially;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 |
8 | namespace Benchmark
9 | {
10 | public class DoNothingStartupService : IStartupServiceToRunSequentially
11 | {
12 | public int OrderNum { get; }
13 | public ValueTask ApplyYourChangeAsync(IServiceProvider scopedServices)
14 | {
15 | return ValueTask.CompletedTask;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/Benchmark/DummyService.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading.Tasks;
6 |
7 | namespace Benchmark
8 | {
9 | public class DummyService : IDummyService
10 | {
11 | }
12 |
13 | public interface IDummyService { }
14 | }
15 |
--------------------------------------------------------------------------------
/Benchmark/Program.cs:
--------------------------------------------------------------------------------
1 | using Benchmark;
2 | using BenchmarkDotNet.Attributes;
3 | using BenchmarkDotNet.Running;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using RunMethodsSequentially;
7 | using RunMethodsSequentially.LockAndRunCode;
8 | using Test.EfCore;
9 | using Test.ServicesToCall;
10 | using TestSupport.EfHelpers;
11 |
12 | public class Program
13 | {
14 | static void Main(string[] args) => BenchmarkRunner.Run();
15 |
16 | private TestDbContext _context;
17 | private IGetLockAndThenRunServices _lockAndRun;
18 |
19 | [GlobalSetup]
20 | public void Setup()
21 | {
22 |
23 | var options = this.CreateUniqueClassOptions();
24 | _context = new TestDbContext(options);
25 | _context.Database.EnsureClean();
26 | }
27 |
28 | [IterationSetup]
29 | public void SetupServices()
30 | {
31 | var services = new ServiceCollection();
32 | services.AddDbContext(dbOptions =>
33 | dbOptions.UseSqlServer(_context.Database.GetConnectionString()));
34 | var options = services.RegisterRunMethodsSequentially(options =>
35 | {
36 | options.RegisterAsHostedService = false;
37 | options.AddSqlServerLockAndRunMethods(_context.Database.GetConnectionString());
38 | //options.AddRunMethodsWithoutLock();
39 | //options.RegisterServiceToRunInJob();
40 | options.RegisterServiceToRunInJob();
41 | options.RegisterServiceToRunInJob();
42 | //options.RegisterServiceToRunInJob();
43 | //options.RegisterServiceToRunInJob();
44 | });
45 |
46 | for (int i = 0; i < 200; i++)
47 | {
48 | services.AddTransient();
49 | }
50 |
51 | var serviceProvider = services.BuildServiceProvider();
52 | _lockAndRun = serviceProvider.GetRequiredService();
53 | }
54 |
55 | [Benchmark]
56 | public async Task RunLockAndRun()
57 | {
58 | await _lockAndRun.LockAndLoadAsync();
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Benchmark/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "ConnectionStrings": {
3 | "UnitTestConnection": "Server=(localdb)\\mssqllocaldb;Database=RunSequentiallyBenchmark-Test;Trusted_Connection=True;MultipleActiveResultSets=true",
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/ReleaseNotes.md:
--------------------------------------------------------------------------------
1 | # Release Notes
2 |
3 | ## 2.0.1
4 |
5 | - Microsoft.Data.SqlClient updated to because of vulnerable in current version
6 | - Updated DistributedLock parts to fix the .NET 8 Postgres bug - see https://github.com/npgsql/npgsql/issues/5143
7 |
8 | ## 2.0.0
9 |
10 | - Changed framework to netstandard2.1 to work with any version of .NET
11 |
12 | ## 1.3.1
13 |
14 | - Update of various NuGet packages, especially the DistributedLock packages that fixes some problems
15 |
16 | ## 1.3.0
17 |
18 | - New Feature: Added a class to test that your startup code will run properly
19 | - New Feature: Added logging so that you can see what RunMethodsSequentially did on host startup
20 |
21 | ## 1.2.0
22 |
23 | - BREAKING CHANGE: `RunMethodWhileInLockAsync()` in old interface `IServiceToCallWhileInLock` changed to `ApplyYourChangeAsync(IServiceProvider scopedServices)`. This provides a another way to obtain services instead of constructor injection
24 | - BREAKING CHANGE: `OrderNum` added to old interface `IServiceToCallWhileInLock` to define the order your startup services are run
25 | - BREAKING CHANGE: interface `IServiceToCallWhileInLock` renamed to `IStartupServiceToRunSequentially`
26 | - BREAKING CHANGE: Deleted `WhatOrderToRunIn` attribute as `OrderNum` now added to `IStartupServiceToRunSequentially` interface
27 | - New feature: Added `AddRunMethodsWithoutLock` version as a way of turning off locking if not needed
28 |
29 | ## 1.1.0
30 |
31 | - Added the optional WhatOrderToRunIn attribute to allow you to define the order in which the IServiceToCallWhileInLock services are run
32 |
33 | ## 1.0.0
34 |
35 | - First release: Supports SQL Server and PostgreSQL (other databases available)
--------------------------------------------------------------------------------
/RunMethodsSequentially/IStartupServiceToRunSequentially.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading.Tasks;
6 |
7 | namespace RunMethodsSequentially
8 | {
9 | ///
10 | /// This defines the service that you want run within a locked state
11 | ///
12 | public interface IStartupServiceToRunSequentially
13 | {
14 | ///
15 | /// This defines the order in which your startup services are run.
16 | /// Note that startup services with the same will
17 | /// be run in the order that they were registered with the
18 | ///
19 | public int OrderNum { get; }
20 |
21 | ///
22 | /// This method will be called within the RunMethodsSequentially JobRunner
23 | ///
24 | /// The RunMethodsSequentially JobRunner will provide a scoped service provider,
25 | /// which allows you to obtain services more efficiently, but be aware the scoped service is used by every
26 | /// startup services you register. But be aware that sharing the scoped service this might cause issues
27 | /// whn using EF Core if
28 | ///
29 | ///
30 | ValueTask ApplyYourChangeAsync(IServiceProvider scopedServices);
31 | }
32 | }
--------------------------------------------------------------------------------
/RunMethodsSequentially/LockAndRunCode/DatabaseHelpers.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using Microsoft.Data.SqlClient;
5 | using Npgsql;
6 |
7 | namespace RunMethodsSequentially.LockAndRunCode
8 | {
9 | public static class DatabaseHelpers
10 | {
11 | public static string GetDatabaseNameFromSqlServerConnectionString(this string connectionString)
12 | {
13 | var builder = new SqlConnectionStringBuilder(connectionString);
14 | return builder.InitialCatalog;
15 | }
16 |
17 | public static string GetDatabaseNameFromPostgreSqlConnectionString(this string connectionString)
18 | {
19 | var builder = new NpgsqlConnectionStringBuilder(connectionString);
20 | return builder.Database;
21 | }
22 |
23 | }
24 | }
--------------------------------------------------------------------------------
/RunMethodsSequentially/LockAndRunCode/FileSystemDoesDirectoryExist.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System.IO;
5 | using System.Threading.Tasks;
6 |
7 | namespace RunMethodsSequentially.LockAndRunCode
8 | {
9 | ///
10 | /// This checks that the given filepath leads to a FileSystem directory
11 | ///
12 | public class FileSystemDoesDirectoryExist : IPreLockTest
13 | {
14 | private readonly string _directoryFilePath;
15 |
16 | public FileSystemDoesDirectoryExist(string directoryFilePath)
17 | {
18 | _directoryFilePath = directoryFilePath;
19 | }
20 |
21 | public ValueTask CheckLockResourceExistsAsync()
22 | {
23 | return new ValueTask(Directory.Exists(_directoryFilePath));
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/RunMethodsSequentially/LockAndRunCode/FileSystemLockAndRunJob.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.IO;
6 | using System.Threading.Tasks;
7 | using Medallion.Threading.FileSystem;
8 | using Microsoft.Extensions.DependencyInjection;
9 |
10 | namespace RunMethodsSequentially.LockAndRunCode
11 | {
12 | ///
13 | /// This the DistributedLock FileSystem to lock on a global directory available to all instances of the application
14 | ///
15 | public class FileSystemLockAndRunJob : ILockAndRunJob
16 | {
17 | private readonly string _directoryFilePath;
18 | private readonly RunSequentiallyOptions _options;
19 |
20 | public string ResourceName { get; }
21 |
22 | public FileSystemLockAndRunJob(RunSequentiallyOptions options, string directoryFilePath)
23 | {
24 | _options = options;
25 | _directoryFilePath = directoryFilePath;
26 |
27 | ResourceName = $"Looking for directory at {directoryFilePath}";
28 | }
29 |
30 | public async Task LockAndRunMethodsAsync(IServiceProvider serviceProvider)
31 | {
32 | using var scope = serviceProvider.CreateScope();
33 | var scopedServices = scope.ServiceProvider;
34 | var lockFileDirectory = new DirectoryInfo(_directoryFilePath);
35 | var distributedLock = new FileDistributedLock(lockFileDirectory, _options.GlobalLockName);
36 | await using (await distributedLock.AcquireAsync(
37 | TimeSpan.FromSeconds(_options.DefaultLockTimeoutInSeconds)))
38 | {
39 | await scopedServices.RunJobAsync();
40 | }
41 |
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/RunMethodsSequentially/LockAndRunCode/GetLockAndThenRunHostedService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using Microsoft.Extensions.Hosting;
8 |
9 | namespace RunMethodsSequentially.LockAndRunCode
10 | {
11 | public class GetLockAndThenRunHostedService : IHostedService
12 | {
13 | private readonly IGetLockAndThenRunServices _service;
14 |
15 | public GetLockAndThenRunHostedService(IServiceProvider serviceProvider)
16 | {
17 | _service = new GetLockAndThenRunServices(serviceProvider);
18 | }
19 |
20 | public async Task StartAsync(CancellationToken cancellationToken)
21 | {
22 | var success = await _service.LockAndLoadAsync();
23 | }
24 |
25 | ///
26 | /// Not used
27 | ///
28 | ///
29 | ///
30 | public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
31 | }
32 | }
--------------------------------------------------------------------------------
/RunMethodsSequentially/LockAndRunCode/GetLockAndThenRunServices.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using Microsoft.Extensions.Logging;
9 |
10 | namespace RunMethodsSequentially.LockAndRunCode
11 | {
12 | ///
13 | /// This applies the registered lock types and tries each in order until it has a lock.
14 | /// If no lock can be found then it throws an exception.
15 | /// If it gets a lock, then it will execute the startup services that you have registered
16 | ///
17 | ///
18 | public class GetLockAndThenRunServices : IGetLockAndThenRunServices
19 | {
20 | private readonly IServiceProvider _serviceProvider;
21 |
22 | public GetLockAndThenRunServices(IServiceProvider serviceProvider)
23 | {
24 | _serviceProvider = serviceProvider;
25 | }
26 |
27 | ///
28 | /// This uses the PreLockCheck to check the the resource to be locked exists.
29 | /// If the resource does exist it calls the code to lock and run the methods.
30 | /// If no resource is found, then it throws an exception
31 | ///
32 | ///
33 | public async Task LockAndLoadAsync()
34 | {
35 | using var scope = _serviceProvider.CreateScope();
36 | var scopedServices = scope.ServiceProvider;
37 | var logger = scopedServices.GetRequiredService>();
38 |
39 | var options = scopedServices.GetRequiredService();
40 | if (!options.LockVersionsInOrder.Any())
41 | throw new RunSequentiallyException(
42 | $"You must register at least one lock service when registering {nameof(StartupExtensions.RegisterRunMethodsSequentially)}, " +
43 | $"for instance services.{nameof(StartupExtensions.RegisterRunMethodsSequentially)}(options => options.{nameof(StartupExtensions.AddSqlServerLockAndRunMethods)}(connectionString))...");
44 | foreach (var lockVersion in options.LockVersionsInOrder)
45 | {
46 | if (await lockVersion.PreLockCheck.CheckLockResourceExistsAsync())
47 | {
48 | logger.LogInformation("The {0} exists and will be locked.", lockVersion.LockAndRunClass.ResourceName);
49 |
50 | //The resource to lock on is there, so lock and run the methods and exit
51 | await lockVersion.LockAndRunClass.LockAndRunMethodsAsync(scopedServices);
52 | return true;
53 | }
54 | //else resource wasn't available so try another lock version
55 | logger.LogInformation("The {0} was not found.", lockVersion.LockAndRunClass.ResourceName);
56 | }
57 |
58 | //Failed to find any resource to lock, so return a useful exception
59 | var listOfMissingResources = string.Join(Environment.NewLine,
60 | options.LockVersionsInOrder.Select(x => x.LockAndRunClass.ResourceName));
61 | throw new RunSequentiallyException(
62 | "No resource were found to lock, so could not run the registered services. The resources tried are" +
63 | Environment.NewLine + listOfMissingResources);
64 | }
65 | }
66 | }
--------------------------------------------------------------------------------
/RunMethodsSequentially/LockAndRunCode/IGetLockAndThenRunServices.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System.Threading.Tasks;
5 |
6 | namespace RunMethodsSequentially.LockAndRunCode
7 | {
8 | public interface IGetLockAndThenRunServices
9 | {
10 | ///
11 | /// This uses the PreLockCheck to check the the resource to be locked exists.
12 | /// If the resource does exist it calls the code to lock and run the methods.
13 | /// If no resource is found, then it throws an exception
14 | ///
15 | /// return true if successful
16 | Task LockAndLoadAsync();
17 | }
18 | }
--------------------------------------------------------------------------------
/RunMethodsSequentially/LockAndRunCode/ILockAndRunJob.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading.Tasks;
6 |
7 | namespace RunMethodsSequentially.LockAndRunCode
8 | {
9 | ///
10 | /// This defines the code that will try to lock with the global resource
11 | /// and if the lock is successful it will then run your registered startup services within that lock
12 | ///
13 | public interface ILockAndRunJob
14 | {
15 | ///
16 | /// This contains the name of the resource that this version is looking for
17 | ///
18 | string ResourceName { get; }
19 |
20 | ///
21 | /// This is the method that will lock the resource and then run the registered services
22 | ///
23 | ///
24 | ///
25 | Task LockAndRunMethodsAsync(IServiceProvider serviceProvider);
26 | }
27 | }
--------------------------------------------------------------------------------
/RunMethodsSequentially/LockAndRunCode/IPreLockTest.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System.Threading.Tasks;
5 |
6 | namespace RunMethodsSequentially.LockAndRunCode
7 | {
8 | ///
9 | /// This defines the code that checks that the global resource you want to lock exists
10 | ///
11 | public interface IPreLockTest
12 | {
13 | ValueTask CheckLockResourceExistsAsync();
14 | }
15 | }
--------------------------------------------------------------------------------
/RunMethodsSequentially/LockAndRunCode/JobRunner.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using Microsoft.Extensions.Logging;
9 |
10 | namespace RunMethodsSequentially.LockAndRunCode
11 | {
12 | public static class JobRunner
13 | {
14 | ///
15 | /// This runs the registered services once a lock has been achieved
16 | ///
17 | /// NOTE: This is provided as a scopedServices
18 | public static async Task RunJobAsync(this IServiceProvider scopedServices)
19 | {
20 | var logger = scopedServices.GetRequiredService>();
21 |
22 | var servicesToRun = scopedServices.GetServices().ToArray();
23 | if (!servicesToRun.Any())
24 | throw new RunSequentiallyException(
25 | "You have not registered any services to run when the lock is active. " +
26 | $"Use the {nameof(StartupExtensions.RegisterServiceToRunInJob)} extension method to register the services you want run during a lock.");
27 | var duplicates = servicesToRun.GroupBy(x => x.GetType())
28 | .Where(x => x.Count() > 1).ToArray();
29 | if (duplicates.Any())
30 | throw new RunSequentiallyException(
31 | $"Some of your services registered by {nameof(StartupExtensions.RegisterServiceToRunInJob)} extension method are duplicates. They are: "+
32 | string.Join(", ", duplicates.Select(x => x.Key.Name)));
33 |
34 | //This orders the startup services by their OrderNum and then execute them
35 | foreach (var serviceToRun in servicesToRun.OrderBy(service => service.OrderNum))
36 | {
37 | await serviceToRun.ApplyYourChangeAsync(scopedServices);
38 | logger.LogInformation("The startup service class [{0}] was successfully executed.", serviceToRun.GetType().Name);
39 | }
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/RunMethodsSequentially/LockAndRunCode/NoLockAndRunJob.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.IO;
6 | using System.Threading.Tasks;
7 | using Medallion.Threading.FileSystem;
8 | using Medallion.Threading.SqlServer;
9 | using Microsoft.Extensions.DependencyInjection;
10 |
11 | namespace RunMethodsSequentially.LockAndRunCode
12 | {
13 | public class NoLockAndRunJob : ILockAndRunJob
14 | {
15 | public string ResourceName { get; } = "No locking applied";
16 |
17 | public async Task LockAndRunMethodsAsync(IServiceProvider serviceProvider)
18 | {
19 | using var scope = serviceProvider.CreateScope();
20 | var scopedServices = scope.ServiceProvider;
21 | await scopedServices.RunJobAsync();
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/RunMethodsSequentially/LockAndRunCode/NoLockPreLockTest.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System.Threading.Tasks;
5 |
6 | namespace RunMethodsSequentially.LockAndRunCode
7 | {
8 | public class NoLockPreLockTest : IPreLockTest
9 | {
10 | public ValueTask CheckLockResourceExistsAsync()
11 | {
12 | return new ValueTask(true);
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/RunMethodsSequentially/LockAndRunCode/PostGreSqlDoesDatabaseExist.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System.Threading.Tasks;
5 | using Npgsql;
6 |
7 | namespace RunMethodsSequentially.LockAndRunCode
8 | {
9 | ///
10 | /// This checks that a PostgreSQL database defined in its connection string exists
11 | ///
12 | public class PostgreSqlDoesDatabaseExist : IPreLockTest
13 | {
14 | private readonly string _connectionString;
15 |
16 | public PostgreSqlDoesDatabaseExist(string connectionString)
17 | {
18 | _connectionString = connectionString;
19 | }
20 |
21 | public async ValueTask CheckLockResourceExistsAsync()
22 | {
23 | //Thanks to phil_rawlings for his stack overflow answer https://stackoverflow.com/a/20032567/1434764
24 | var builder = new NpgsqlConnectionStringBuilder(_connectionString);
25 | var databaseToLookFor = builder.Database;
26 | builder.Database = "postgres";
27 | var newConnectionString = builder.ToString();
28 |
29 | using (NpgsqlConnection conn = new NpgsqlConnection(newConnectionString))
30 | {
31 | conn.Open();
32 | string cmdText = $"SELECT 1 FROM pg_database WHERE datname='{databaseToLookFor}'";
33 | using (NpgsqlCommand cmd = new NpgsqlCommand(cmdText, conn))
34 | {
35 | var result = await cmd.ExecuteScalarAsync();
36 | return result != null;
37 | }
38 | }
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/RunMethodsSequentially/LockAndRunCode/PostGreSqlLockAndRunJob.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading.Tasks;
6 | using Medallion.Threading.Postgres;
7 | using Microsoft.Extensions.DependencyInjection;
8 |
9 | namespace RunMethodsSequentially.LockAndRunCode
10 | {
11 | ///
12 | /// This the DistributedLock Postgres to lock on a global Postgres database available to all instances of the application
13 | ///
14 | public class PostgreSqlLockAndRunJob : ILockAndRunJob
15 | {
16 | private readonly string _connectionString;
17 | private readonly RunSequentiallyOptions _options;
18 |
19 | public string ResourceName { get; }
20 |
21 | public PostgreSqlLockAndRunJob(RunSequentiallyOptions options, string connectionString)
22 | {
23 | _options = options;
24 | _connectionString = connectionString;
25 |
26 | ResourceName = $"PostgreSQL database with name [{connectionString.GetDatabaseNameFromPostgreSqlConnectionString()}]";
27 | }
28 |
29 | public async Task LockAndRunMethodsAsync(IServiceProvider serviceProvider)
30 | {
31 | using var scope = serviceProvider.CreateScope();
32 | var scopedServices = scope.ServiceProvider;
33 | var distributedLock = new PostgresDistributedLock(new PostgresAdvisoryLockKey(_options.GlobalLockName, allowHashing: true), _connectionString);
34 | await using (await distributedLock.AcquireAsync(
35 | TimeSpan.FromSeconds(_options.DefaultLockTimeoutInSeconds)))
36 | {
37 | await scopedServices.RunJobAsync();
38 | }
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------
/RunMethodsSequentially/LockAndRunCode/SqlServerDoesDatabaseExist.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System.Threading.Tasks;
5 | using Microsoft.Data.SqlClient;
6 |
7 | namespace RunMethodsSequentially.LockAndRunCode
8 | {
9 | ///
10 | /// This checks that a SQL Server database defined in its connection string exists
11 | ///
12 | public class SqlServerDoesDatabaseExist : IPreLockTest
13 | {
14 | private readonly string _connectionString;
15 |
16 | public SqlServerDoesDatabaseExist(string connectionString)
17 | {
18 | _connectionString = connectionString;
19 | }
20 |
21 | public async ValueTask CheckLockResourceExistsAsync()
22 | {
23 | var builder = new SqlConnectionStringBuilder(_connectionString);
24 | var databaseName = builder.InitialCatalog;
25 | builder.InitialCatalog = "";
26 | var newConnectionString = builder.ToString();
27 |
28 | using (var myConn = new SqlConnection(newConnectionString))
29 | {
30 | var command = $"SELECT COUNT(*) FROM sys.databases WHERE [Name] = '{databaseName}'";
31 | var myCommand = new SqlCommand(command, myConn);
32 | myConn.Open();
33 | return ((int)await myCommand.ExecuteScalarAsync()) == 1;
34 | }
35 | }
36 | }
37 | }
--------------------------------------------------------------------------------
/RunMethodsSequentially/LockAndRunCode/SqlServerLockAndRunJob.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading.Tasks;
6 | using Medallion.Threading.SqlServer;
7 | using Microsoft.Extensions.DependencyInjection;
8 |
9 | namespace RunMethodsSequentially.LockAndRunCode
10 | {
11 | ///
12 | /// This the DistributedLock SQL Server to lock on a global SQL Server database available to all instances of the application
13 | ///
14 | public class SqlServerLockAndRunJob : ILockAndRunJob
15 | {
16 | private readonly string _connectionString;
17 | private readonly RunSequentiallyOptions _options;
18 |
19 | public string ResourceName { get; }
20 |
21 | public SqlServerLockAndRunJob(RunSequentiallyOptions options, string connectionString)
22 | {
23 | _options = options;
24 | _connectionString = connectionString;
25 |
26 | ResourceName = $"SQL Server database with name [{connectionString.GetDatabaseNameFromSqlServerConnectionString()}]";
27 | }
28 |
29 | public async Task LockAndRunMethodsAsync(IServiceProvider serviceProvider)
30 | {
31 | using var scope = serviceProvider.CreateScope();
32 | var scopedServices = scope.ServiceProvider;
33 | var distributedLock = new SqlDistributedLock(_options.GlobalLockName, _connectionString);
34 | await using (await distributedLock.AcquireAsync(
35 | TimeSpan.FromSeconds(_options.DefaultLockTimeoutInSeconds)))
36 | {
37 | await scopedServices.RunJobAsync();
38 | }
39 |
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/RunMethodsSequentially/LockAndRunCode/TryLockVersion.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | namespace RunMethodsSequentially.LockAndRunCode
5 | {
6 | public class TryLockVersion
7 | {
8 | public TryLockVersion(IPreLockTest preLockTask, ILockAndRunJob lockAndRunClass)
9 | {
10 | LockAndRunClass = lockAndRunClass;
11 | PreLockCheck = preLockTask;
12 | }
13 |
14 | public IPreLockTest PreLockCheck { get; }
15 | public ILockAndRunJob LockAndRunClass { get; }
16 | }
17 | }
--------------------------------------------------------------------------------
/RunMethodsSequentially/RegisterRunMethodsSequentiallyTester.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using Microsoft.Extensions.DependencyInjection;
5 | using Microsoft.Extensions.Hosting;
6 | using RunMethodsSequentially.LockAndRunCode;
7 | using System.IO;
8 | using System.Threading.Tasks;
9 |
10 | namespace RunMethodsSequentially
11 | {
12 | ///
13 | /// This class will test your use of the
14 | /// by registering to the services and then running the code which would be run on the startup of your application
15 | ///
16 | public class RegisterRunMethodsSequentiallyTester
17 | {
18 | ///
19 | /// You need to register the with its options
20 | /// as found in your startup code.
21 | /// You also need to register any services, such as your application's DbContext, that your startup services need
22 | ///
23 | public ServiceCollection Services { get; } = new ServiceCollection();
24 |
25 | ///
26 | /// If you are using the then you can use this path to a directory
27 | ///
28 | public string LockFolderPath { get; } = Directory.GetCurrentDirectory();
29 |
30 | ///
31 | /// Run this to check that your with its options work
32 | ///
33 | ///
34 | public async Task RunHostStartupCodeAsync()
35 | {
36 | Services.AddLogging();
37 | var serviceProvider = Services.BuildServiceProvider();
38 | var options = serviceProvider.GetRequiredService();
39 | if (options.RegisterAsHostedService)
40 | {
41 | var lockAndRun = serviceProvider.GetRequiredService();
42 | await lockAndRun.StartAsync(default);
43 | }
44 | else
45 | {
46 | var lockAndRun = serviceProvider.GetRequiredService();
47 | await lockAndRun.LockAndLoadAsync();
48 | }
49 | }
50 |
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/RunMethodsSequentially/RunMethodsSequentially.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.1
5 | true
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Net.RunMethodsSequentially
21 | 2.0.1
22 | Jon P Smith
23 | Runs updates to single resources, e.g. a database, on startup of an application that has multiple instances
24 | false
25 |
26 | - Microsoft.Data.SqlClient updated to because of vulnerable in current version
27 | - Updated DistributedLock parts to fix the .NET 8 Postgres bug - see https://github.com/npgsql/npgsql/issues/5143
28 |
29 | Copyright (c) 2021 Jon P Smith. Licenced under MIT licence
30 | Entity Framework Core, ASP.NET Core
31 | true
32 | true
33 | https://github.com/JonPSmith/RunStartupMethodsSequentially
34 | https://github.com/JonPSmith/RunStartupMethodsSequentially
35 | RunMethodsSequentiallyNuGetIcon.png
36 | MIT
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/RunMethodsSequentially/RunMethodsSequentiallyNuGetIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/JonPSmith/RunStartupMethodsSequentially/b3562cd17161b40139fdb1d026ad20ffc720e4ac/RunMethodsSequentially/RunMethodsSequentiallyNuGetIcon.png
--------------------------------------------------------------------------------
/RunMethodsSequentially/RunSequentiallyException.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 |
6 | namespace RunMethodsSequentially
7 | {
8 | public class RunSequentiallyException : Exception
9 | {
10 | public RunSequentiallyException(string message)
11 | : base(message) {}
12 | }
13 | }
--------------------------------------------------------------------------------
/RunMethodsSequentially/RunSequentiallyOptions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System.Collections.Generic;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using Microsoft.Extensions.Hosting;
7 | using Medallion.Threading;
8 | using RunMethodsSequentially.LockAndRunCode;
9 |
10 | namespace RunMethodsSequentially
11 | {
12 | ///
13 | /// This contains varius
14 | ///
15 | public class RunSequentiallyOptions
16 | {
17 | ///
18 | /// This defines how the is registered in the dependency injection provider
19 | /// Default is true, with registers the code as a
20 | /// If false, it is registered as transient service found by the
21 | ///
22 | public bool RegisterAsHostedService { get; set; } = true;
23 |
24 | ///
25 | /// This contains the name of the global lock, default is .
26 | /// Useful to know in case you want to use other services
27 | ///
28 | public string GlobalLockName { get; set; } = nameof(RunMethodsSequentially);
29 |
30 | ///
31 | /// This defines how long it will try to obtain a lock, defaults to 100 seconds
32 | /// When you have `NNN` multiple instances the total time taken for the ALL of your
33 | /// startup services to run must be less that `DefaultLockTimeoutInSeconds / NNN`.
34 | ///
35 | public int DefaultLockTimeoutInSeconds { get; set; } = 100;
36 |
37 | ///
38 | /// This contains a series of Lock Versions. It will start with the first one and only steps the next
39 | /// Lock Version(s) if the resource isn't found.
40 | ///
41 | public ICollection LockVersionsInOrder { get; } = new List();
42 |
43 | internal RunSequentiallyOptions(IServiceCollection services)
44 | {
45 | Services = services;
46 | }
47 |
48 | internal IServiceCollection Services { get; }
49 | }
50 | }
--------------------------------------------------------------------------------
/RunMethodsSequentially/StartupExtentions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using RunMethodsSequentially.LockAndRunCode;
7 |
8 | namespace RunMethodsSequentially
9 | {
10 | public static class StartupExtensions
11 | {
12 | ///
13 | /// This registers the RunMethodsSequentially feature into your DI services,
14 | /// and you then register how you want to lock the your global resource(s) via the options.
15 | /// NOTE: By default the RunMethodsSequentially code will be registered as a IHostedService,
16 | /// which is correct for usage in a ASP.NET Core. If you aren't usinh ASP.NET Core, then see the
17 | /// docs on how to register RunMethodsSequentially as a normal service
18 | ///
19 | ///
20 | ///
21 | ///
22 | public static RunSequentiallyOptions RegisterRunMethodsSequentially(this IServiceCollection services,
23 | Action optionsAction = null)
24 | {
25 | var options = new RunSequentiallyOptions(services);
26 | optionsAction?.Invoke(options);
27 |
28 | services.AddSingleton(options);
29 | if (options.RegisterAsHostedService)
30 | services.AddHostedService();
31 | else
32 | services.AddTransient();
33 |
34 | return options;
35 | }
36 |
37 | ///
38 | /// This will lock using a SQL Server database. If the SQL Server database hasn't been created yet
39 | /// it will pass onto the next lock type, e.g.
40 | ///
41 | ///
42 | /// The connection string to the SQL Server database
43 | public static void AddSqlServerLockAndRunMethods(this RunSequentiallyOptions options,
44 | string connectionString)
45 | {
46 | if (connectionString == null) throw new ArgumentNullException(nameof(connectionString));
47 |
48 | options.LockVersionsInOrder.Add(new TryLockVersion(
49 | new SqlServerDoesDatabaseExist(connectionString),
50 | new SqlServerLockAndRunJob(options, connectionString)));
51 | }
52 |
53 | ///
54 | /// This will lock using a PostgreSQL database. If the PostgreSQL database hasn't been created yet
55 | /// it will pass onto the next lock type, e.g.
56 | ///
57 | ///
58 | /// The connection string to the PostgreSQL database
59 | public static void AddPostgreSqlLockAndRunMethods(this RunSequentiallyOptions options,
60 | string connectionString)
61 | {
62 | if (connectionString == null) throw new ArgumentNullException(nameof(connectionString));
63 |
64 | options.LockVersionsInOrder.Add(new TryLockVersion(
65 | new PostgreSqlDoesDatabaseExist(connectionString),
66 | new PostgreSqlLockAndRunJob(options, connectionString)));
67 | }
68 |
69 | ///
70 | /// This will lock on a filesytem directory in your running application, e.g.
71 | /// in ASP.NET Core the wwwroot directory. If it can't find the directory it will pass onto
72 | /// the next lock type. If there isn't a next lock type it will fail
73 | ///
74 | ///
75 | /// The filepath to a global directory accessable by all the instances of your app
76 | public static void AddFileSystemLockAndRunMethods(this RunSequentiallyOptions options,
77 | string directoryFilePath)
78 | {
79 | if (directoryFilePath == null) throw new ArgumentNullException(nameof(directoryFilePath));
80 |
81 | options.LockVersionsInOrder.Add(new TryLockVersion(
82 | new FileSystemDoesDirectoryExist(directoryFilePath),
83 | new FileSystemLockAndRunJob(options, directoryFilePath)));
84 | }
85 |
86 | ///
87 | /// This just runs the your startup services without locking anything.
88 | /// This is useful if you are only running one instance of your application
89 | /// NOTE: Locking a database is fast (1 ms local SQL Server), but no lock only takes 50 us
90 | ///
91 | ///
92 | public static void AddRunMethodsWithoutLock(this RunSequentiallyOptions options)
93 | {
94 | options.LockVersionsInOrder.Add(new TryLockVersion(
95 | new NoLockPreLockTest(),
96 | new NoLockAndRunJob()));
97 | }
98 |
99 | ///
100 | /// This method allows you to register your startup services, i.e. classes that inherit the
101 | /// interface called .
102 | /// NOTE that the order in which your startup services are registered defines the order they are executed,
103 | /// BUT a value of not zero overides the order of execution
104 | ///
105 | ///
106 | ///
107 | ///
108 | public static RunSequentiallyOptions RegisterServiceToRunInJob(this RunSequentiallyOptions options)
109 | where TService : class, IStartupServiceToRunSequentially
110 | {
111 | options.Services.AddTransient();
112 | return options;
113 | }
114 | }
115 | }
--------------------------------------------------------------------------------
/RunStartupMethodsSequentially.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.1.31911.260
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RunMethodsSequentially", "RunMethodsSequentially\RunMethodsSequentially.csproj", "{D148E104-C2E8-452F-86AC-9D1E638D4564}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test", "Test\Test.csproj", "{725DA932-179F-4939-A37D-5A5B2FBDE538}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F8C8DA71-3EE9-4B12-AB5B-C1C3F9C9692E}"
11 | ProjectSection(SolutionItems) = preProject
12 | LICENSE = LICENSE
13 | README.md = README.md
14 | ReleaseNotes.md = ReleaseNotes.md
15 | EndProjectSection
16 | EndProject
17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Benchmark", "Benchmark\Benchmark.csproj", "{0C305AB8-9B4F-45AF-B37C-248AB20F896A}"
18 | EndProject
19 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebSiteRunSequentially", "WebSiteRunSequentially\WebSiteRunSequentially.csproj", "{0B4E489C-2B28-46D2-BD35-F77BFB550858}"
20 | EndProject
21 | Global
22 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
23 | Debug|Any CPU = Debug|Any CPU
24 | Debug|x64 = Debug|x64
25 | Debug|x86 = Debug|x86
26 | Release|Any CPU = Release|Any CPU
27 | Release|x64 = Release|x64
28 | Release|x86 = Release|x86
29 | EndGlobalSection
30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
31 | {D148E104-C2E8-452F-86AC-9D1E638D4564}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32 | {D148E104-C2E8-452F-86AC-9D1E638D4564}.Debug|Any CPU.Build.0 = Debug|Any CPU
33 | {D148E104-C2E8-452F-86AC-9D1E638D4564}.Debug|x64.ActiveCfg = Debug|Any CPU
34 | {D148E104-C2E8-452F-86AC-9D1E638D4564}.Debug|x64.Build.0 = Debug|Any CPU
35 | {D148E104-C2E8-452F-86AC-9D1E638D4564}.Debug|x86.ActiveCfg = Debug|Any CPU
36 | {D148E104-C2E8-452F-86AC-9D1E638D4564}.Debug|x86.Build.0 = Debug|Any CPU
37 | {D148E104-C2E8-452F-86AC-9D1E638D4564}.Release|Any CPU.ActiveCfg = Release|Any CPU
38 | {D148E104-C2E8-452F-86AC-9D1E638D4564}.Release|Any CPU.Build.0 = Release|Any CPU
39 | {D148E104-C2E8-452F-86AC-9D1E638D4564}.Release|x64.ActiveCfg = Release|Any CPU
40 | {D148E104-C2E8-452F-86AC-9D1E638D4564}.Release|x64.Build.0 = Release|Any CPU
41 | {D148E104-C2E8-452F-86AC-9D1E638D4564}.Release|x86.ActiveCfg = Release|Any CPU
42 | {D148E104-C2E8-452F-86AC-9D1E638D4564}.Release|x86.Build.0 = Release|Any CPU
43 | {725DA932-179F-4939-A37D-5A5B2FBDE538}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
44 | {725DA932-179F-4939-A37D-5A5B2FBDE538}.Debug|Any CPU.Build.0 = Debug|Any CPU
45 | {725DA932-179F-4939-A37D-5A5B2FBDE538}.Debug|x64.ActiveCfg = Debug|Any CPU
46 | {725DA932-179F-4939-A37D-5A5B2FBDE538}.Debug|x64.Build.0 = Debug|Any CPU
47 | {725DA932-179F-4939-A37D-5A5B2FBDE538}.Debug|x86.ActiveCfg = Debug|Any CPU
48 | {725DA932-179F-4939-A37D-5A5B2FBDE538}.Debug|x86.Build.0 = Debug|Any CPU
49 | {725DA932-179F-4939-A37D-5A5B2FBDE538}.Release|Any CPU.ActiveCfg = Release|Any CPU
50 | {725DA932-179F-4939-A37D-5A5B2FBDE538}.Release|Any CPU.Build.0 = Release|Any CPU
51 | {725DA932-179F-4939-A37D-5A5B2FBDE538}.Release|x64.ActiveCfg = Release|Any CPU
52 | {725DA932-179F-4939-A37D-5A5B2FBDE538}.Release|x64.Build.0 = Release|Any CPU
53 | {725DA932-179F-4939-A37D-5A5B2FBDE538}.Release|x86.ActiveCfg = Release|Any CPU
54 | {725DA932-179F-4939-A37D-5A5B2FBDE538}.Release|x86.Build.0 = Release|Any CPU
55 | {0C305AB8-9B4F-45AF-B37C-248AB20F896A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
56 | {0C305AB8-9B4F-45AF-B37C-248AB20F896A}.Debug|Any CPU.Build.0 = Debug|Any CPU
57 | {0C305AB8-9B4F-45AF-B37C-248AB20F896A}.Debug|x64.ActiveCfg = Debug|Any CPU
58 | {0C305AB8-9B4F-45AF-B37C-248AB20F896A}.Debug|x64.Build.0 = Debug|Any CPU
59 | {0C305AB8-9B4F-45AF-B37C-248AB20F896A}.Debug|x86.ActiveCfg = Debug|Any CPU
60 | {0C305AB8-9B4F-45AF-B37C-248AB20F896A}.Debug|x86.Build.0 = Debug|Any CPU
61 | {0C305AB8-9B4F-45AF-B37C-248AB20F896A}.Release|Any CPU.ActiveCfg = Release|Any CPU
62 | {0C305AB8-9B4F-45AF-B37C-248AB20F896A}.Release|Any CPU.Build.0 = Release|Any CPU
63 | {0C305AB8-9B4F-45AF-B37C-248AB20F896A}.Release|x64.ActiveCfg = Release|Any CPU
64 | {0C305AB8-9B4F-45AF-B37C-248AB20F896A}.Release|x64.Build.0 = Release|Any CPU
65 | {0C305AB8-9B4F-45AF-B37C-248AB20F896A}.Release|x86.ActiveCfg = Release|Any CPU
66 | {0C305AB8-9B4F-45AF-B37C-248AB20F896A}.Release|x86.Build.0 = Release|Any CPU
67 | {0B4E489C-2B28-46D2-BD35-F77BFB550858}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
68 | {0B4E489C-2B28-46D2-BD35-F77BFB550858}.Debug|Any CPU.Build.0 = Debug|Any CPU
69 | {0B4E489C-2B28-46D2-BD35-F77BFB550858}.Debug|x64.ActiveCfg = Debug|Any CPU
70 | {0B4E489C-2B28-46D2-BD35-F77BFB550858}.Debug|x64.Build.0 = Debug|Any CPU
71 | {0B4E489C-2B28-46D2-BD35-F77BFB550858}.Debug|x86.ActiveCfg = Debug|Any CPU
72 | {0B4E489C-2B28-46D2-BD35-F77BFB550858}.Debug|x86.Build.0 = Debug|Any CPU
73 | {0B4E489C-2B28-46D2-BD35-F77BFB550858}.Release|Any CPU.ActiveCfg = Release|Any CPU
74 | {0B4E489C-2B28-46D2-BD35-F77BFB550858}.Release|Any CPU.Build.0 = Release|Any CPU
75 | {0B4E489C-2B28-46D2-BD35-F77BFB550858}.Release|x64.ActiveCfg = Release|Any CPU
76 | {0B4E489C-2B28-46D2-BD35-F77BFB550858}.Release|x64.Build.0 = Release|Any CPU
77 | {0B4E489C-2B28-46D2-BD35-F77BFB550858}.Release|x86.ActiveCfg = Release|Any CPU
78 | {0B4E489C-2B28-46D2-BD35-F77BFB550858}.Release|x86.Build.0 = Release|Any CPU
79 | EndGlobalSection
80 | GlobalSection(SolutionProperties) = preSolution
81 | HideSolutionNode = FALSE
82 | EndGlobalSection
83 | GlobalSection(ExtensibilityGlobals) = postSolution
84 | SolutionGuid = {82DB0431-F2E7-4DFA-82D0-EAE671F47118}
85 | EndGlobalSection
86 | EndGlobal
87 |
--------------------------------------------------------------------------------
/Test/EfCore/CommonNameDateTime.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 |
6 | namespace Test.EfCore
7 | {
8 | public class CommonNameDateTime
9 | {
10 | public int Id { get; set; }
11 | public string Name { get; set; }
12 | public int Stage { get; set; }
13 | public DateTime DateTimeUtc { get; set; }
14 |
15 | public override string ToString()
16 | {
17 | return $"Name: {Name}, Stage: {Stage}, DateTimeUtc: {DateTimeUtc:O}";
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/Test/EfCore/NameDateTime.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 |
6 | namespace Test.EfCore
7 | {
8 | public class NameDateTime
9 | {
10 | public int Id { get; set; }
11 | public string Name { get; set; }
12 | public DateTime DateTimeUtc { get; set; }
13 |
14 | public override string ToString()
15 | {
16 | return $"Name: {Name}, DateTimeUtc: {DateTimeUtc:O}";
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/Test/EfCore/TestDbContext.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using Microsoft.EntityFrameworkCore;
5 |
6 | namespace Test.EfCore
7 | {
8 | public class TestDbContext : DbContext
9 | {
10 | public TestDbContext(DbContextOptions options)
11 | : base(options) { }
12 |
13 | public DbSet NameDateTimes { get; set; }
14 | public DbSet CommonNameDateTimes { get; set; }
15 | }
16 | }
--------------------------------------------------------------------------------
/Test/Helpers/ParallelExtensions.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Threading.Tasks;
7 | using System.Threading.Tasks.Dataflow;
8 |
9 | namespace Test.Helpers
10 | {
11 | //NOTE Parallel.ForEach doesn't handle async
12 | //I found a very useful article https://medium.com/@alex.puiu/parallel-foreach-async-in-c-36756f8ebe62
13 | //From this I decided the AsyncParallelForEach approach, which can run async methods in paralle
14 | public static class ParallelExtensions
15 | {
16 | public static async IAsyncEnumerable NumTimesAsyncEnumerable(this int numTimes)
17 | {
18 | for (int i = 1; i <= numTimes; i++)
19 | {
20 | yield return i;
21 | }
22 | }
23 |
24 |
25 | public static async Task AsyncParallelForEach(this IAsyncEnumerable source, Func body, int maxDegreeOfParallelism = DataflowBlockOptions.Unbounded, TaskScheduler scheduler = null)
26 | {
27 | var options = new ExecutionDataflowBlockOptions
28 | {
29 | MaxDegreeOfParallelism = maxDegreeOfParallelism
30 | };
31 | if (scheduler != null)
32 | options.TaskScheduler = scheduler;
33 |
34 | var block = new ActionBlock(body, options);
35 |
36 | await foreach (var item in source)
37 | block.Post(item);
38 |
39 | block.Complete();
40 | await block.Completion;
41 | }
42 | }
43 | }
--------------------------------------------------------------------------------
/Test/Helpers/RegisterRunHelpers.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using Microsoft.EntityFrameworkCore;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using Microsoft.Extensions.Logging;
8 | using RunMethodsSequentially;
9 | using RunMethodsSequentially.LockAndRunCode;
10 | using Test.EfCore;
11 | using TestSupport.EfHelpers;
12 | using TestSupport.Helpers;
13 |
14 | namespace Test.Helpers
15 | {
16 | public static class RegisterRunHelpers
17 | {
18 | public static ServiceCollection SetupSqlServerRunMethodsSequentially(this TestDbContext context,
19 | Action optionsAction = null)
20 | {
21 | var services = new ServiceCollection();
22 | services.AddDbContext(dbOptions =>
23 | dbOptions.UseSqlServer(context.Database.GetConnectionString()));
24 | var options = services.RegisterRunMethodsSequentially(options =>
25 | {
26 | options.RegisterAsHostedService = false;
27 | options.AddSqlServerLockAndRunMethods(context.Database.GetConnectionString());
28 | });
29 | optionsAction?.Invoke(options);
30 |
31 | return services;
32 | }
33 |
34 | public static ServiceCollection SetupPostgreSqlRunMethodsSequentially(this TestDbContext context,
35 | Action optionsAction = null)
36 | {
37 | var services = new ServiceCollection();
38 | services.AddDbContext(dbOptions =>
39 | dbOptions.UseNpgsql(context.Database.GetConnectionString()));
40 | var options = services.RegisterRunMethodsSequentially(options =>
41 | {
42 | options.RegisterAsHostedService = false;
43 | options.AddPostgreSqlLockAndRunMethods(context.Database.GetConnectionString());
44 | });
45 | optionsAction?.Invoke(options);
46 |
47 | return services;
48 | }
49 |
50 | public static ServiceCollection SetupNoLockRunMethodsSequentially(this TestDbContext context,
51 | Action optionsAction = null)
52 | {
53 | var services = new ServiceCollection();
54 | services.AddDbContext(dbOptions =>
55 | dbOptions.UseSqlServer(context.Database.GetConnectionString()));
56 | var options = services.RegisterRunMethodsSequentially(options =>
57 | {
58 | options.RegisterAsHostedService = false;
59 | options.AddRunMethodsWithoutLock();
60 | });
61 | optionsAction?.Invoke(options);
62 |
63 | return services;
64 | }
65 |
66 | public static ServiceCollection SetupFileSystemLockMethodsSequentially(this TestDbContext context,
67 | Action optionsAction = null)
68 | {
69 | var services = new ServiceCollection();
70 | services.AddDbContext(dbOptions =>
71 | dbOptions.UseSqlServer(context.Database.GetConnectionString()));
72 | var options = services.RegisterRunMethodsSequentially(options =>
73 | {
74 | options.RegisterAsHostedService = false;
75 | options.AddFileSystemLockAndRunMethods(TestData.GetTestDataDir());
76 | });
77 | optionsAction?.Invoke(options);
78 |
79 | return services;
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/Test/Helpers/RegisterTestLogger.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.Extensions.Logging;
3 | using RunMethodsSequentially.LockAndRunCode;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 | using TestSupport.EfHelpers;
10 |
11 | namespace Test.Helpers
12 | {
13 | public class RegisterTestLogger
14 | {
15 | public List Logs = new List();
16 |
17 | public RegisterTestLogger(ServiceCollection services)
18 | {
19 | services.AddSingleton>(
20 | new Logger(new LoggerFactory(new[] { new MyLoggerProviderActionOut(Logs.Add) })));
21 | }
22 |
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Test/Helpers/SqlServerHelpers.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using Microsoft.Data.SqlClient;
6 | using TestSupport.EfHelpers;
7 |
8 | namespace Test.Helpers
9 | {
10 | public static class SqlServerHelpers
11 | {
12 | public static bool DoesDatabaseExist(this string connectionString)
13 | {
14 | var builder = new SqlConnectionStringBuilder(connectionString);
15 | var databaseName = builder.InitialCatalog;
16 | builder.InitialCatalog = "";
17 | var newConnectionString = builder.ToString();
18 |
19 | return newConnectionString.ExecuteRowCount("sys.databases", $"WHERE [Name] = '{databaseName}'") == 1;
20 | }
21 |
22 | public static void DeleteDatabase(this string connectionString)
23 | {
24 | var builder = new SqlConnectionStringBuilder(connectionString);
25 | var databaseName = builder.InitialCatalog;
26 | builder.InitialCatalog = "";
27 | var newConnectionString = builder.ToString();
28 |
29 | if (newConnectionString.ExecuteRowCount("sys.databases", $"WHERE [Name] = '{databaseName}'") == 1)
30 | newConnectionString.ExecuteNonQuery("DROP DATABASE [" + databaseName + "]");
31 | if (newConnectionString.ExecuteRowCount("sys.databases", $"WHERE [Name] = '{databaseName}'") == 1)
32 | //it failed
33 | throw new InvalidOperationException($"Failed to deleted {databaseName}. Did you have SSMS open or something?");
34 | }
35 |
36 | public static void CreateDatabase(this string connectionString)
37 | {
38 | var builder = new SqlConnectionStringBuilder(connectionString);
39 | var databaseName = builder.InitialCatalog;
40 | builder.InitialCatalog = "";
41 | var newConnectionString = builder.ToString();
42 |
43 | if (newConnectionString.ExecuteRowCount("sys.databases", $"WHERE [Name] = '{databaseName}'") == 1)
44 | throw new InvalidOperationException($"There is a data of that name already.");
45 |
46 | newConnectionString.ExecuteNonQuery("CREATE DATABASE [" + databaseName + "]");
47 | }
48 | }
49 | }
--------------------------------------------------------------------------------
/Test/ServicesToCall/SqlServerEnsureCreatedDatabaseOnly.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading.Tasks;
6 | using RunMethodsSequentially;
7 | using Test.EfCore;
8 |
9 | namespace Test.ServicesToCall
10 | {
11 | public class SqlServerEnsureCreatedDatabaseOnly : IStartupServiceToRunSequentially
12 | {
13 | private readonly TestDbContext _context;
14 |
15 | public SqlServerEnsureCreatedDatabaseOnly(TestDbContext context)
16 | {
17 | _context = context;
18 | }
19 |
20 | public int OrderNum { get; }
21 |
22 | public async ValueTask ApplyYourChangeAsync(IServiceProvider scopedServices)
23 | {
24 | await _context.Database.EnsureCreatedAsync();
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/Test/ServicesToCall/SqlServerMigrateDbContextService.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading.Tasks;
6 | using Microsoft.EntityFrameworkCore;
7 | using RunMethodsSequentially;
8 | using Test.EfCore;
9 |
10 | namespace Test.ServicesToCall
11 | {
12 | public class SqlServerMigrateDbContextService : IStartupServiceToRunSequentially
13 | {
14 | private readonly TestDbContext _context;
15 |
16 | public SqlServerMigrateDbContextService(TestDbContext context)
17 | {
18 | _context = context;
19 | }
20 | public int OrderNum { get; }
21 |
22 | public async ValueTask ApplyYourChangeAsync(IServiceProvider scopedServices)
23 | {
24 | await _context.Database.MigrateAsync();
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/Test/ServicesToCall/UpdateDatabase1.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading.Tasks;
6 | using Microsoft.EntityFrameworkCore;
7 | using RunMethodsSequentially;
8 | using Test.EfCore;
9 |
10 | namespace Test.ServicesToCall
11 | {
12 | public class UpdateDatabase1 : IStartupServiceToRunSequentially
13 | {
14 |
15 | private readonly TestDbContext _context;
16 |
17 | public UpdateDatabase1(TestDbContext context)
18 | {
19 | _context = context;
20 | }
21 |
22 | public int OrderNum { get; }
23 |
24 | public async ValueTask ApplyYourChangeAsync(IServiceProvider scopedServices)
25 | {
26 | var startTime = DateTime.UtcNow;
27 | //add a new entry
28 | _context.Add(new NameDateTime
29 | { Name = nameof(UpdateDatabase1), DateTimeUtc = startTime });
30 |
31 | //add/update the common class
32 | var commonEntity = await _context.CommonNameDateTimes.SingleOrDefaultAsync();
33 | if (commonEntity == null)
34 | _context.Add(new CommonNameDateTime
35 | { Name = nameof(UpdateDatabase1), DateTimeUtc = startTime });
36 | else
37 | {
38 | commonEntity.DateTimeUtc = startTime;
39 | }
40 |
41 | await _context.SaveChangesAsync();
42 | }
43 | }
44 | }
--------------------------------------------------------------------------------
/Test/ServicesToCall/UpdateDatabase2.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading.Tasks;
6 | using Microsoft.EntityFrameworkCore;
7 | using RunMethodsSequentially;
8 | using Test.EfCore;
9 |
10 | namespace Test.ServicesToCall
11 | {
12 | public class UpdateDatabase2 : IStartupServiceToRunSequentially
13 | {
14 | private readonly TestDbContext _context;
15 |
16 | public UpdateDatabase2(TestDbContext context)
17 | {
18 | _context = context;
19 | }
20 | public int OrderNum { get; }
21 |
22 | public async ValueTask ApplyYourChangeAsync(IServiceProvider scopedServices)
23 | {
24 | var startTime = DateTime.UtcNow;
25 | //add a new entry
26 | _context.Add(new NameDateTime
27 | { Name = nameof(UpdateDatabase1), DateTimeUtc = startTime });
28 |
29 | //add/update the common class
30 | var commonEntity = await _context.CommonNameDateTimes.SingleOrDefaultAsync();
31 | if (commonEntity == null)
32 | _context.Add(new CommonNameDateTime
33 | { Name = nameof(UpdateDatabase1), DateTimeUtc = startTime });
34 | else
35 | {
36 | commonEntity.DateTimeUtc = startTime;
37 | }
38 |
39 | await _context.SaveChangesAsync();
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/Test/ServicesToCall/UpdateDatabaseUseScoped1.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading.Tasks;
6 | using Microsoft.EntityFrameworkCore;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using RunMethodsSequentially;
9 | using Test.EfCore;
10 |
11 | namespace Test.ServicesToCall
12 | {
13 | public class UpdateDatabaseUseScoped1 : IStartupServiceToRunSequentially
14 | {
15 | public int OrderNum { get; }
16 |
17 | public async ValueTask ApplyYourChangeAsync(IServiceProvider scopedServices)
18 | {
19 | var context = scopedServices.GetRequiredService();
20 |
21 | var startTime = DateTime.UtcNow;
22 | //add a new entry
23 | context.Add(new NameDateTime
24 | { Name = nameof(UpdateDatabase1), DateTimeUtc = startTime });
25 |
26 | //add/update the common class
27 | var commonEntity = await context.CommonNameDateTimes.SingleOrDefaultAsync();
28 | if (commonEntity == null)
29 | context.Add(new CommonNameDateTime
30 | { Name = nameof(UpdateDatabase1), DateTimeUtc = startTime });
31 | else
32 | {
33 | commonEntity.DateTimeUtc = startTime;
34 | }
35 |
36 | await context.SaveChangesAsync();
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/Test/ServicesToCall/UpdateDatabaseUseScoped2.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading.Tasks;
6 | using Microsoft.EntityFrameworkCore;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using RunMethodsSequentially;
9 | using Test.EfCore;
10 |
11 | namespace Test.ServicesToCall
12 | {
13 | public class UpdateDatabaseUseScoped2 : IStartupServiceToRunSequentially
14 | {
15 | public int OrderNum { get; }
16 |
17 | public async ValueTask ApplyYourChangeAsync(IServiceProvider scopedServices)
18 | {
19 | var context = scopedServices.GetRequiredService();
20 |
21 | var startTime = DateTime.UtcNow;
22 | //add a new entry
23 | context.Add(new NameDateTime
24 | { Name = nameof(UpdateDatabase1), DateTimeUtc = startTime });
25 |
26 | //add/update the common class
27 | var commonEntity = await context.CommonNameDateTimes.SingleOrDefaultAsync();
28 | if (commonEntity == null)
29 | context.Add(new CommonNameDateTime
30 | { Name = nameof(UpdateDatabase1), DateTimeUtc = startTime });
31 | else
32 | {
33 | commonEntity.DateTimeUtc = startTime;
34 | }
35 |
36 | await context.SaveChangesAsync();
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/Test/ServicesToCall/UpdateThatTakes800Milliseconds1.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading.Tasks;
6 | using RunMethodsSequentially;
7 |
8 | namespace Test.ServicesToCall
9 | {
10 | public class UpdateThatTakes800Milliseconds1 : IStartupServiceToRunSequentially
11 | {
12 | public int OrderNum { get; }
13 |
14 | public async ValueTask ApplyYourChangeAsync(IServiceProvider scopedServices)
15 | {
16 | await Task.Delay(800);
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/Test/ServicesToCall/UpdateThatTakes800Milliseconds2.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading.Tasks;
6 | using RunMethodsSequentially;
7 |
8 | namespace Test.ServicesToCall
9 | {
10 | public class UpdateThatTakes800Milliseconds2 : IStartupServiceToRunSequentially
11 | {
12 | public string GlobalLockName { get; } = nameof(RunMethodsSequentially);
13 |
14 | public int OrderNum { get; }
15 |
16 | public async ValueTask ApplyYourChangeAsync(IServiceProvider scopedServices)
17 | {
18 | await Task.Delay(800);
19 | }
20 | }
21 | }
--------------------------------------------------------------------------------
/Test/ServicesToCall/UpdateWithDelay.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading.Tasks;
6 | using Microsoft.EntityFrameworkCore;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using RunMethodsSequentially;
9 | using Test.EfCore;
10 |
11 | namespace Test.ServicesToCall
12 | {
13 | public class UpdateWithDelay : IStartupServiceToRunSequentially
14 | {
15 | public int OrderNum { get; }
16 |
17 | public async ValueTask ApplyYourChangeAsync(IServiceProvider scopedServices)
18 | {
19 | var context = scopedServices.GetRequiredService();
20 |
21 | var commonEntity = await context.CommonNameDateTimes.SingleAsync();
22 |
23 | context.Add(new NameDateTime { Name = "Read", DateTimeUtc = commonEntity.DateTimeUtc });
24 | await context.SaveChangesAsync();
25 |
26 | await Task.Delay(100);
27 | commonEntity.DateTimeUtc = DateTime.UtcNow;
28 | commonEntity.Name = Guid.NewGuid().ToString();
29 | context.Add(new NameDateTime { Name = "Write", DateTimeUtc = commonEntity.DateTimeUtc });
30 |
31 | await context.SaveChangesAsync();
32 | }
33 | }
34 | }
--------------------------------------------------------------------------------
/Test/ServicesToCall/UpdateWithNegativeOrderNum.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading.Tasks;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using RunMethodsSequentially;
8 | using Test.EfCore;
9 |
10 | namespace Test.ServicesToCall
11 | {
12 | public class UpdateWithNegativeOrderNum : IStartupServiceToRunSequentially
13 | {
14 | public int OrderNum { get; } = -1;
15 |
16 | public async ValueTask ApplyYourChangeAsync(IServiceProvider scopedServices)
17 | {
18 | var context = scopedServices.GetRequiredService();
19 |
20 | context.Add(new NameDateTime { Name = $"OrderNum = -1", DateTimeUtc = DateTime.UtcNow });
21 | await context.SaveChangesAsync();
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/Test/ServicesToCall/UpdateWithPositiveOrderNum.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading.Tasks;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using RunMethodsSequentially;
8 | using Test.EfCore;
9 |
10 | namespace Test.ServicesToCall
11 | {
12 | public class UpdateWithPositiveOrderNum : IStartupServiceToRunSequentially
13 | {
14 | public int OrderNum { get; } = 1;
15 |
16 | public async ValueTask ApplyYourChangeAsync(IServiceProvider scopedServices)
17 | {
18 | var context = scopedServices.GetRequiredService();
19 |
20 | context.Add(new NameDateTime { Name = $"OrderNum = +1", DateTimeUtc = DateTime.UtcNow });
21 | await context.SaveChangesAsync();
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/Test/ServicesToCall/UpdateWithZeroOrderNum.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Threading.Tasks;
6 | using Microsoft.Extensions.DependencyInjection;
7 | using RunMethodsSequentially;
8 | using Test.EfCore;
9 |
10 | namespace Test.ServicesToCall
11 | {
12 | public class UpdateWithZeroOrderNum : IStartupServiceToRunSequentially
13 | {
14 | private readonly TestDbContext context;
15 |
16 | public UpdateWithZeroOrderNum(TestDbContext context)
17 | {
18 | context = context;
19 | }
20 |
21 | public int OrderNum { get; }
22 |
23 | public async ValueTask ApplyYourChangeAsync(IServiceProvider scopedServices)
24 | {
25 | var context = scopedServices.GetRequiredService();
26 |
27 | context.Add(new NameDateTime { Name = $"No OrderNum", DateTimeUtc = DateTime.UtcNow });
28 | await context.SaveChangesAsync();
29 | }
30 | }
31 | }
--------------------------------------------------------------------------------
/Test/Test.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | runtime; build; native; contentfiles; analyzers; buildtransitive
17 | all
18 |
19 |
20 | runtime; build; native; contentfiles; analyzers; buildtransitive
21 | all
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/Test/TestData/DummyFile.txt:
--------------------------------------------------------------------------------
1 | This file is there to make sure the TestData directory it there.
2 |
--------------------------------------------------------------------------------
/Test/UnitTests/TestFileSystemVersion.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using RunMethodsSequentially;
9 | using RunMethodsSequentially.LockAndRunCode;
10 | using Test.EfCore;
11 | using Test.Helpers;
12 | using Test.ServicesToCall;
13 | using TestSupport.EfHelpers;
14 | using Xunit;
15 | using Xunit.Abstractions;
16 | using Xunit.Extensions.AssertExtensions;
17 |
18 | namespace Test.UnitTests
19 | {
20 | public class TestFileSystemVersion
21 | {
22 | private readonly ITestOutputHelper _output;
23 |
24 | public TestFileSystemVersion(ITestOutputHelper output)
25 | {
26 | _output = output;
27 | }
28 |
29 | [Fact]
30 | public async Task TestFileSystemLockRunOneService()
31 | {
32 | //SETUP
33 | var dbOptions = this.CreateUniqueClassOptions();
34 | using var context = new TestDbContext(dbOptions);
35 | context.Database.EnsureClean();
36 |
37 | var services = context.SetupFileSystemLockMethodsSequentially(
38 | options => options.RegisterServiceToRunInJob());
39 | var testLogger = new RegisterTestLogger(services);
40 | var serviceProvider = services.BuildServiceProvider();
41 | var lockAndRun = serviceProvider.GetRequiredService();
42 |
43 | //ATTEMPT
44 | using (new TimeThings(_output))
45 | await lockAndRun.LockAndLoadAsync();
46 |
47 | //VERIFY
48 | var entry = context.NameDateTimes.Single();
49 | entry.DateTimeUtc.ShouldBeInRange(DateTime.UtcNow.AddMilliseconds(-500), DateTime.UtcNow);
50 | entry.Name.ShouldEqual(nameof(UpdateDatabase1));
51 | var common = context.CommonNameDateTimes.Single();
52 | common.DateTimeUtc.ShouldEqual(entry.DateTimeUtc);
53 | }
54 |
55 | [Fact]
56 | public async Task TestFileSystemLockOrderedByWhatOrderToRunIn()
57 | {
58 | //SETUP
59 | var dbOptions = this.CreateUniqueClassOptions();
60 | using var context = new TestDbContext(dbOptions);
61 | context.Database.EnsureClean();
62 |
63 | var services = context.SetupFileSystemLockMethodsSequentially(
64 | options =>
65 | {
66 | options.RegisterServiceToRunInJob();
67 | options.RegisterServiceToRunInJob();
68 | options.RegisterServiceToRunInJob();
69 | });
70 | var testLogger = new RegisterTestLogger(services);
71 | var serviceProvider = services.BuildServiceProvider();
72 | var lockAndRun = serviceProvider.GetRequiredService();
73 |
74 | //ATTEMPT
75 | await lockAndRun.LockAndLoadAsync();
76 |
77 | //VERIFY
78 | var entries = context.NameDateTimes.OrderBy(x => x.Id).ToList();
79 | entries.Select(x => x.Name).ShouldEqual(new[] { "OrderNum = -1", "No OrderNum", "OrderNum = +1" });
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/Test/UnitTests/TestLogging.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using Microsoft.Extensions.Hosting;
9 | using RunMethodsSequentially;
10 | using RunMethodsSequentially.LockAndRunCode;
11 | using Test.EfCore;
12 | using Test.Helpers;
13 | using Test.ServicesToCall;
14 | using TestSupport.EfHelpers;
15 | using TestSupport.Helpers;
16 | using Xunit;
17 | using Xunit.Abstractions;
18 | using Xunit.Extensions.AssertExtensions;
19 |
20 | namespace Test.UnitTests
21 | {
22 | public class TestLogging
23 | {
24 | private readonly ITestOutputHelper _output;
25 |
26 | public TestLogging(ITestOutputHelper output)
27 | {
28 | _output = output;
29 | }
30 |
31 | [Fact]
32 | public async Task TestLoggingRunOneService()
33 | {
34 | //SETUP
35 | var dbOptions = this.CreateUniqueClassOptions();
36 | using var context = new TestDbContext(dbOptions);
37 | context.Database.EnsureClean();
38 |
39 | var services = context.SetupSqlServerRunMethodsSequentially(
40 | options => options.RegisterServiceToRunInJob());
41 | var testLogger = new RegisterTestLogger(services);
42 | var serviceProvider = services.BuildServiceProvider();
43 | var lockAndRun = serviceProvider.GetRequiredService();
44 |
45 | //ATTEMPT
46 | using (new TimeThings(_output))
47 | await lockAndRun.LockAndLoadAsync();
48 |
49 | //VERIFY
50 | foreach(var log in testLogger.Logs)
51 | {
52 | _output.WriteLine(log.Message);
53 | }
54 | testLogger.Logs.Count.ShouldEqual(2);
55 | var i = 0;
56 | testLogger.Logs[i++].Message.ShouldEqual("The SQL Server database with name [RunMethodsSequentially-Test_TestLogging] exists and will be locked.");
57 | testLogger.Logs[i++].Message.ShouldEqual("The startup service class [UpdateDatabase1] was successfully executed.");
58 | }
59 |
60 | [Fact]
61 | public async Task TestLoggingRunTwoServices()
62 | {
63 | //SETUP
64 | var dbOptions = this.CreateUniqueClassOptions();
65 | using var context = new TestDbContext(dbOptions);
66 | context.Database.EnsureClean();
67 |
68 | var services = context.SetupSqlServerRunMethodsSequentially(
69 | options =>
70 | {
71 | options.RegisterServiceToRunInJob();
72 | options.RegisterServiceToRunInJob();
73 | });
74 | var testLogger = new RegisterTestLogger(services);
75 | var serviceProvider = services.BuildServiceProvider();
76 | var lockAndRun = serviceProvider.GetRequiredService();
77 |
78 | //ATTEMPT
79 | await lockAndRun.LockAndLoadAsync();
80 |
81 | //VERIFY
82 | foreach (var log in testLogger.Logs)
83 | {
84 | _output.WriteLine(log.Message);
85 | }
86 | testLogger.Logs.Count.ShouldEqual(3);
87 | var i = 0;
88 | testLogger.Logs[i++].Message.ShouldEqual("The SQL Server database with name [RunMethodsSequentially-Test_TestLogging] exists and will be locked.");
89 | testLogger.Logs[i++].Message.ShouldEqual("The startup service class [UpdateDatabase1] was successfully executed.");
90 | testLogger.Logs[i++].Message.ShouldEqual("The startup service class [UpdateDatabase2] was successfully executed.");
91 | }
92 |
93 | }
94 | }
--------------------------------------------------------------------------------
/Test/UnitTests/TestNoLockVersion.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using RunMethodsSequentially;
9 | using RunMethodsSequentially.LockAndRunCode;
10 | using Test.EfCore;
11 | using Test.Helpers;
12 | using Test.ServicesToCall;
13 | using TestSupport.EfHelpers;
14 | using Xunit;
15 | using Xunit.Abstractions;
16 | using Xunit.Extensions.AssertExtensions;
17 |
18 | namespace Test.UnitTests
19 | {
20 | public class TestNoLockVersion
21 | {
22 | private readonly ITestOutputHelper _output;
23 |
24 | public TestNoLockVersion(ITestOutputHelper output)
25 | {
26 | _output = output;
27 | }
28 |
29 | [Fact]
30 | public async Task TestNoLockRunOneService()
31 | {
32 | //SETUP
33 | var dbOptions = this.CreateUniqueClassOptions();
34 | using var context = new TestDbContext(dbOptions);
35 | context.Database.EnsureClean();
36 |
37 | var services = context.SetupNoLockRunMethodsSequentially(
38 | options => options.RegisterServiceToRunInJob());
39 | var testLogger = new RegisterTestLogger(services);
40 | var serviceProvider = services.BuildServiceProvider();
41 | var lockAndRun = serviceProvider.GetRequiredService();
42 |
43 | //ATTEMPT
44 | using (new TimeThings(_output))
45 | await lockAndRun.LockAndLoadAsync();
46 |
47 | //VERIFY
48 | var entry = context.NameDateTimes.Single();
49 | entry.DateTimeUtc.ShouldBeInRange(DateTime.UtcNow.AddMilliseconds(-500), DateTime.UtcNow);
50 | entry.Name.ShouldEqual(nameof(UpdateDatabase1));
51 | var common = context.CommonNameDateTimes.Single();
52 | common.DateTimeUtc.ShouldEqual(entry.DateTimeUtc);
53 | }
54 |
55 | [Fact]
56 | public async Task TestNoLockRunOrderedByWhatOrderToRunIn()
57 | {
58 | //SETUP
59 | var dbOptions = this.CreateUniqueClassOptions();
60 | using var context = new TestDbContext(dbOptions);
61 | context.Database.EnsureClean();
62 |
63 | var services = context.SetupNoLockRunMethodsSequentially(
64 | options =>
65 | {
66 | options.RegisterServiceToRunInJob();
67 | options.RegisterServiceToRunInJob();
68 | options.RegisterServiceToRunInJob();
69 | });
70 | var testLogger = new RegisterTestLogger(services);
71 | var serviceProvider = services.BuildServiceProvider();
72 | var lockAndRun = serviceProvider.GetRequiredService();
73 |
74 | //ATTEMPT
75 | await lockAndRun.LockAndLoadAsync();
76 |
77 | //VERIFY
78 | var entries = context.NameDateTimes.OrderBy(x => x.Id).ToList();
79 | entries.Select(x => x.Name).ShouldEqual(new[] { "OrderNum = -1", "No OrderNum", "OrderNum = +1" });
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/Test/UnitTests/TestPostGreSql.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System.Collections.Generic;
5 | using System.Threading.Tasks;
6 | using Microsoft.EntityFrameworkCore;
7 | using Npgsql;
8 | using Respawn;
9 | using RunMethodsSequentially.LockAndRunCode;
10 | using Test.EfCore;
11 | using Test.Helpers;
12 | using TestSupport.Attributes;
13 | using TestSupport.EfHelpers;
14 | using TestSupport.Helpers;
15 | using Xunit;
16 | using Xunit.Abstractions;
17 | using Xunit.Extensions.AssertExtensions;
18 |
19 | namespace Test.UnitTests
20 | {
21 | public class TestPostgreSql
22 | {
23 | private readonly ITestOutputHelper _output;
24 |
25 | public TestPostgreSql(ITestOutputHelper output)
26 | {
27 | _output = output;
28 | }
29 |
30 | [Fact]
31 | public void TestCreatePostgreUniqueDatabaseOptions()
32 | {
33 | //SETUP
34 | var options = this.CreatePostgreSqlUniqueClassOptions();
35 | using var context = new TestDbContext(options);
36 |
37 | //ATTEMPT
38 | var connectionString = context.Database.GetConnectionString();
39 |
40 | //VERIFY
41 | connectionString.ShouldEqual("Host=127.0.0.1;Port=5432;Database=RunStartup-Test_TestPostgreSql;Username=postgres;Password=LetMeIn");
42 | }
43 |
44 | [Fact]
45 | public async Task TestDeleteCreateDatabaseEfCore()
46 | {
47 | //SETUP
48 | var logs = new List();
49 | var options = this.CreatePostgreSqlUniqueClassOptionsWithLogTo(log => logs.Add(log));
50 | using var context = new TestDbContext(options);
51 | using(new TimeThings(_output, "Possible delete db"))
52 | context.Database.EnsureDeleted();
53 |
54 | var service = new PostgreSqlDoesDatabaseExist(context.Database.GetConnectionString());
55 |
56 | //ATTEMPT
57 | var noDb = await service.CheckLockResourceExistsAsync();
58 | using (new TimeThings(_output, "Create db"))
59 | context.Database.EnsureCreated();
60 | var hasDb = await service.CheckLockResourceExistsAsync();
61 |
62 | //VERIFY
63 | noDb.ShouldBeFalse();
64 | hasDb.ShouldBeTrue();
65 | }
66 |
67 | [RunnableInDebugOnly]
68 | public void TestDeleteAllTestDatabases()
69 | {
70 | //SETUP
71 |
72 | //ATTEMPT
73 | using (new TimeThings(_output, "Deleted dbs"))
74 | {
75 | var numDeleted = DatabaseTidyHelper.DeleteAllPostgreSqlTestDatabases();
76 | _output.WriteLine($"Deleted {numDeleted} dbs");
77 | }
78 |
79 | //VERIFY
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/Test/UnitTests/TestPostGreSqlLocks.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using Microsoft.Extensions.Hosting;
9 | using RunMethodsSequentially;
10 | using RunMethodsSequentially.LockAndRunCode;
11 | using Test.EfCore;
12 | using Test.Helpers;
13 | using Test.ServicesToCall;
14 | using TestSupport.EfHelpers;
15 | using TestSupport.Helpers;
16 | using Xunit;
17 | using Xunit.Abstractions;
18 | using Xunit.Extensions.AssertExtensions;
19 |
20 | namespace Test.UnitTests
21 | {
22 | public class TestPostgreSqlLocks
23 | {
24 | private readonly ITestOutputHelper _output;
25 |
26 | public TestPostgreSqlLocks(ITestOutputHelper output)
27 | {
28 | _output = output;
29 | }
30 |
31 | [Fact]
32 | public void TestRegisterRunMethodsSequentiallyHostedService()
33 | {
34 | //SETUP
35 | var connectionString = this.GetUniquePostgreSqlConnectionString();
36 | var services = new ServiceCollection();
37 |
38 | //ATTEMPT
39 | services.RegisterRunMethodsSequentially(options =>
40 | {
41 | options.AddPostgreSqlLockAndRunMethods(connectionString);
42 | });
43 |
44 | //VERIFY
45 | var serviceProvider = services.BuildServiceProvider();
46 | var options = serviceProvider.GetRequiredService();
47 | options.LockVersionsInOrder.Count.ShouldEqual(1);
48 | options.LockVersionsInOrder.Single().LockAndRunClass.ResourceName.ShouldEqual(
49 | $"PostgreSQL database with name [{connectionString.GetDatabaseNameFromPostgreSqlConnectionString()}]");
50 | serviceProvider.GetService().ShouldNotBeNull();
51 | }
52 |
53 | [Fact]
54 | public void TestRegisterRunMethodsSequentiallyNormalService()
55 | {
56 | //SETUP
57 | var connectionString = this.GetUniquePostgreSqlConnectionString();
58 | var services = new ServiceCollection();
59 |
60 | //ATTEMPT
61 | services.RegisterRunMethodsSequentially(options =>
62 | {
63 | options.RegisterAsHostedService = false;
64 | options.AddPostgreSqlLockAndRunMethods(connectionString);
65 | });
66 |
67 | //VERIFY
68 | var serviceProvider = services.BuildServiceProvider();
69 | var options = serviceProvider.GetRequiredService();
70 | options.LockVersionsInOrder.Count.ShouldEqual(1);
71 | options.LockVersionsInOrder.Single().LockAndRunClass.ResourceName.ShouldEqual(
72 | $"PostgreSQL database with name [{connectionString.GetDatabaseNameFromPostgreSqlConnectionString()}]");
73 | serviceProvider.GetService().ShouldNotBeNull();
74 | }
75 |
76 | [Fact]
77 | public async Task TestLockPostgreDatabaseAndRunOneService()
78 | {
79 | //SETUP
80 | var dbOptions = this.CreatePostgreSqlUniqueClassOptions();
81 | using var context = new TestDbContext(dbOptions);
82 | context.Database.EnsureClean();
83 |
84 | var services = context.SetupPostgreSqlRunMethodsSequentially(
85 | options => options.RegisterServiceToRunInJob());
86 | var testLogger = new RegisterTestLogger(services);
87 | var serviceProvider = services.BuildServiceProvider();
88 | var lockAndRun = serviceProvider.GetRequiredService();
89 |
90 | //ATTEMPT
91 | await lockAndRun.LockAndLoadAsync();
92 |
93 | //VERIFY
94 | var entry = context.NameDateTimes.Single();
95 | entry.DateTimeUtc.ShouldBeInRange(DateTime.UtcNow.AddSeconds(-5), DateTime.UtcNow);
96 | entry.Name.ShouldEqual(nameof(UpdateDatabase1));
97 | var common = context.CommonNameDateTimes.Single();
98 | common.DateTimeUtc.ShouldEqual(entry.DateTimeUtc);
99 | }
100 |
101 | [Fact]
102 | public async Task TestLockPostgreDatabaseAndRunTwoServices()
103 | {
104 | //SETUP
105 | var dbOptions = this.CreatePostgreSqlUniqueClassOptions();
106 | using var context = new TestDbContext(dbOptions);
107 | context.Database.EnsureClean();
108 |
109 | var services = context.SetupPostgreSqlRunMethodsSequentially(
110 | options =>
111 | {
112 | options.RegisterServiceToRunInJob();
113 | options.RegisterServiceToRunInJob();
114 | });
115 | var testLogger = new RegisterTestLogger(services);
116 | var serviceProvider = services.BuildServiceProvider();
117 | var lockAndRun = serviceProvider.GetRequiredService();
118 |
119 | //ATTEMPT
120 | await lockAndRun.LockAndLoadAsync();
121 |
122 | //VERIFY
123 | var utcNow = DateTime.UtcNow;
124 | var entries = context.NameDateTimes.OrderBy(x => x.Id).ToList();
125 | entries[0].DateTimeUtc.ShouldBeInRange(DateTime.UtcNow.AddSeconds(-5), utcNow);
126 | entries[1].DateTimeUtc.ShouldBeInRange(entries[0].DateTimeUtc, utcNow);
127 | var common = context.CommonNameDateTimes.Single();
128 | common.DateTimeUtc.ShouldEqual(entries[1].DateTimeUtc);
129 | }
130 |
131 | [Fact]
132 | public async Task TestLockPostgreDatabaseNoDatabaseToStartWith()
133 | {
134 | //SETUP
135 | var dbOptions = this.CreatePostgreSqlUniqueClassOptions();
136 | using var context = new TestDbContext(dbOptions);
137 | context.Database.EnsureDeleted();
138 |
139 | var services = context.SetupPostgreSqlRunMethodsSequentially(
140 | options =>
141 | {
142 | options.AddFileSystemLockAndRunMethods(TestData.GetTestDataDir());
143 | options.RegisterServiceToRunInJob();
144 | options.RegisterServiceToRunInJob();
145 | });
146 | var testLogger = new RegisterTestLogger(services);
147 | var serviceProvider = services.BuildServiceProvider();
148 | var lockAndRun = serviceProvider.GetRequiredService();
149 |
150 | //ATTEMPT
151 | await lockAndRun.LockAndLoadAsync();
152 |
153 | //VERIFY
154 | var entry = context.NameDateTimes.Single();
155 | entry.DateTimeUtc.ShouldBeInRange(DateTime.UtcNow.AddSeconds(-5), DateTime.UtcNow);
156 | entry.Name.ShouldEqual(nameof(UpdateDatabase1));
157 | var common = context.CommonNameDateTimes.Single();
158 | common.DateTimeUtc.ShouldEqual(entry.DateTimeUtc);
159 | }
160 |
161 | //------------------------------------------------------
162 | //Check errors
163 |
164 | [Fact]
165 | public async Task TestLockPostgreDatabase_NoDatabase()
166 | {
167 | //SETUP
168 | var dbOptions = this.CreatePostgreSqlUniqueClassOptions();
169 | using var context = new TestDbContext(dbOptions);
170 | context.Database.EnsureDeleted();
171 |
172 | var services = context.SetupPostgreSqlRunMethodsSequentially(
173 | options => options.RegisterServiceToRunInJob());
174 | var testLogger = new RegisterTestLogger(services);
175 | var serviceProvider = services.BuildServiceProvider();
176 | var lockAndRun = serviceProvider.GetRequiredService();
177 |
178 | //ATTEMPT
179 | var ex = await Assert.ThrowsAsync(async () => await lockAndRun.LockAndLoadAsync());
180 |
181 | //VERIFY
182 | _output.WriteLine(ex.Message);
183 | ex.Message.ShouldStartWith("No resource were found to lock, so could not run the registered services. ");
184 | }
185 |
186 | [Fact]
187 | public async Task TestLockPostgreDatabase_NoServicesFound()
188 | {
189 | //SETUP
190 | var dbOptions = this.CreatePostgreSqlUniqueClassOptions();
191 | using var context = new TestDbContext(dbOptions);
192 | context.Database.EnsureClean();
193 |
194 | var services = context.SetupPostgreSqlRunMethodsSequentially();
195 | var testLogger = new RegisterTestLogger(services);
196 | var serviceProvider = services.BuildServiceProvider();
197 | var lockAndRun = serviceProvider.GetRequiredService();
198 |
199 | //ATTEMPT
200 | var ex = await Assert.ThrowsAsync(async () => await lockAndRun.LockAndLoadAsync());
201 |
202 | //VERIFY
203 | _output.WriteLine(ex.Message);
204 | ex.Message.ShouldStartWith("You have not registered any services to run when the lock is active.");
205 | }
206 |
207 | [Fact]
208 | public async Task TestLockPostgreDatabase_DuplicateServices()
209 | {
210 | //SETUP
211 | var dbOptions = this.CreatePostgreSqlUniqueClassOptions();
212 | using var context = new TestDbContext(dbOptions);
213 | context.Database.EnsureClean();
214 |
215 | var services = context.SetupPostgreSqlRunMethodsSequentially(
216 | options =>
217 | {
218 | options.RegisterServiceToRunInJob();
219 | options.RegisterServiceToRunInJob();
220 | });
221 | var testLogger = new RegisterTestLogger(services);
222 | var serviceProvider = services.BuildServiceProvider();
223 | var lockAndRun = serviceProvider.GetRequiredService();
224 |
225 | //ATTEMPT
226 | var ex = await Assert.ThrowsAsync(async () => await lockAndRun.LockAndLoadAsync());
227 |
228 | //VERIFY
229 | _output.WriteLine(ex.Message);
230 | ex.Message.ShouldStartWith("Some of your services registered by RegisterServiceToRunInJob extension method are duplicates.");
231 | }
232 |
233 | [Fact]
234 | public async Task TestLockPostgreDatabase_NoRegisteredLockService()
235 | {
236 | //SETUP
237 | var services = new ServiceCollection();
238 | services.RegisterRunMethodsSequentially(options =>
239 | {
240 | options.RegisterAsHostedService = false;
241 | //options.AddLockSqlServerAndRunMethods(context.Database.GetConnectionString());
242 | });
243 | var testLogger = new RegisterTestLogger(services);
244 | var serviceProvider = services.BuildServiceProvider();
245 | var lockAndRun = serviceProvider.GetRequiredService();
246 |
247 | //ATTEMPT
248 | var ex = await Assert.ThrowsAsync(async () => await lockAndRun.LockAndLoadAsync());
249 |
250 | //VERIFY
251 | _output.WriteLine(ex.Message);
252 | ex.Message.ShouldStartWith("You must register at least one lock service when registering");
253 | }
254 | }
255 | }
--------------------------------------------------------------------------------
/Test/UnitTests/TestRegisterRunMethodsSequentiallyTester.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 | using Microsoft.EntityFrameworkCore;
8 | using Microsoft.Extensions.DependencyInjection;
9 | using RunMethodsSequentially;
10 | using Test.EfCore;
11 | using Test.ServicesToCall;
12 | using TestSupport.EfHelpers;
13 | using WebSiteRunSequentially.Database;
14 | using WebSiteRunSequentially.StartupServices;
15 | using Xunit;
16 | using Xunit.Abstractions;
17 | using Xunit.Extensions.AssertExtensions;
18 |
19 | namespace Test.UnitTests
20 | {
21 | public class TestRegisterRunMethodsSequentiallyTester
22 | {
23 | [Fact]
24 | public async Task ExampleTester()
25 | {
26 | //SETUP
27 | var dbOptions = this.CreateUniqueClassOptions();
28 | using var context = new WebSiteDbContext(dbOptions);
29 | context.Database.EnsureClean();
30 |
31 | var builder = new RegisterRunMethodsSequentiallyTester();
32 |
33 | //ATTEMPT
34 | //Copy your setup in your Program here
35 | //---------------------------------------------------------------
36 | var connectionString = context.Database.GetConnectionString(); //CHANGED
37 | var lockFolder = builder.LockFolderPath; //CHANGED
38 |
39 | builder.Services.AddDbContext(options =>
40 | options.UseSqlServer(connectionString));
41 |
42 | builder.Services.RegisterRunMethodsSequentially(options =>
43 | {
44 | options.AddSqlServerLockAndRunMethods(connectionString);
45 | options.AddFileSystemLockAndRunMethods(lockFolder);
46 | })
47 | .RegisterServiceToRunInJob()
48 | .RegisterServiceToRunInJob();
49 | //----------------------------------------------------------------
50 |
51 | //VERIFY
52 | await builder.RunHostStartupCodeAsync();
53 | context.CommonNameDateTimes.Single().DateTimeUtc
54 | .ShouldBeInRange(DateTime.UtcNow.AddSeconds(-1), DateTime.UtcNow);
55 | }
56 |
57 | [Fact]
58 | public async Task TestRunTwoServiceOk()
59 | {
60 | //SETUP
61 | var dbOptions = this.CreateUniqueClassOptions();
62 | using var context = new TestDbContext(dbOptions);
63 | context.Database.EnsureClean();
64 |
65 | var builder = new RegisterRunMethodsSequentiallyTester();
66 |
67 | //ATTEMPT
68 | //Copy your setup in your Program here
69 | //---------------------------------------------------------------
70 | builder.Services.AddDbContext(options =>
71 | options.UseSqlServer(context.Database.GetConnectionString()));
72 | builder.Services.RegisterRunMethodsSequentially(options =>
73 | {
74 | options.AddSqlServerLockAndRunMethods(context.Database.GetConnectionString());
75 | options.AddFileSystemLockAndRunMethods(builder.LockFolderPath);
76 | })
77 | .RegisterServiceToRunInJob()
78 | .RegisterServiceToRunInJob();
79 | //----------------------------------------------------------------
80 |
81 | //VERIFY
82 | await builder.RunHostStartupCodeAsync();
83 | }
84 |
85 | [Fact]
86 | public async Task TestRunTwoServiceNoDatabaseOk()
87 | {
88 | //SETUP
89 | var dbOptions = this.CreateUniqueClassOptions();
90 | using var context = new TestDbContext(dbOptions);
91 | context.Database.EnsureDeleted();
92 |
93 | var builder = new RegisterRunMethodsSequentiallyTester();
94 |
95 | //ATTEMPT
96 | //Copy your setup in your Program here
97 | //---------------------------------------------------------------
98 | builder.Services.AddDbContext(options =>
99 | options.UseSqlServer(context.Database.GetConnectionString()));
100 | builder.Services.RegisterRunMethodsSequentially(options =>
101 | {
102 | options.AddSqlServerLockAndRunMethods(context.Database.GetConnectionString());
103 | options.AddFileSystemLockAndRunMethods(builder.LockFolderPath);
104 | })
105 | .RegisterServiceToRunInJob()
106 | .RegisterServiceToRunInJob();
107 | //----------------------------------------------------------------
108 |
109 | //VERIFY
110 | await builder.RunHostStartupCodeAsync();
111 | }
112 |
113 | [Fact]
114 | public async Task TestRunTwoServiceRegisterAsHostedServiceOk()
115 | {
116 | //SETUP
117 | var dbOptions = this.CreateUniqueClassOptions();
118 | using var context = new TestDbContext(dbOptions);
119 | context.Database.EnsureClean();
120 |
121 | var builder = new RegisterRunMethodsSequentiallyTester();
122 |
123 | //ATTEMPT
124 | //Copy your setup in your Program here
125 | //---------------------------------------------------------------
126 | builder.Services.AddDbContext(options =>
127 | options.UseSqlServer(context.Database.GetConnectionString()));
128 | builder.Services.RegisterRunMethodsSequentially(options =>
129 | {
130 | options.RegisterAsHostedService = false;
131 | options.AddSqlServerLockAndRunMethods(context.Database.GetConnectionString());
132 | options.AddFileSystemLockAndRunMethods(builder.LockFolderPath);
133 | })
134 | .RegisterServiceToRunInJob()
135 | .RegisterServiceToRunInJob();
136 | //----------------------------------------------------------------
137 |
138 | //VERIFY
139 | await builder.RunHostStartupCodeAsync();
140 | }
141 |
142 | [Fact]
143 | public async Task TestRunTwoServiceThrowsException()
144 | {
145 | //SETUP
146 | var dbOptions = this.CreateUniqueClassOptions();
147 | using var context = new TestDbContext(dbOptions);
148 | context.Database.EnsureClean();
149 | var lockFolder = System.IO.Directory.GetCurrentDirectory();
150 |
151 | var builder = new RegisterRunMethodsSequentiallyTester();
152 | //Copy your setup in your Program here
153 | //---------------------------------------------------------------
154 | builder.Services.AddDbContext(options =>
155 | options.UseSqlServer(context.Database.GetConnectionString()));
156 | builder.Services.RegisterRunMethodsSequentially(options =>
157 | {
158 | options.AddSqlServerLockAndRunMethods(context.Database.GetConnectionString());
159 | options.AddFileSystemLockAndRunMethods(builder.LockFolderPath);
160 | })
161 | .RegisterServiceToRunInJob()
162 | .RegisterServiceToRunInJob(); //!!!!!!!!!!!!!!!!!! DUPLICATE !!!!!!!!!!!!!!!
163 | //----------------------------------------------------------------
164 |
165 | //ATTEMPT
166 | var ex = await Assert.ThrowsAsync< RunSequentiallyException>(async () => await builder.RunHostStartupCodeAsync());
167 |
168 | //VERIFY
169 | ex.Message.ShouldEqual("Some of your services registered by RegisterServiceToRunInJob extension method are duplicates. They are: UpdateDatabase1");
170 | }
171 |
172 | }
173 | }
--------------------------------------------------------------------------------
/Test/UnitTests/TestSqlServerHelpers.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using Test.Helpers;
5 | using TestSupport.Helpers;
6 | using Xunit;
7 | using Xunit.Extensions.AssertExtensions;
8 |
9 | namespace Test.UnitTests
10 | {
11 | public class TestSqlServerHelpers
12 | {
13 |
14 | [Fact]
15 | public void TestCheckCreateDelete()
16 | {
17 | //SETUP
18 | var connectionString = this.GetUniqueDatabaseConnectionString();
19 | if (connectionString.DoesDatabaseExist())
20 | connectionString.DeleteDatabase();
21 | connectionString.DoesDatabaseExist().ShouldBeFalse();
22 |
23 | //ATTEMPT - VERIFY
24 | connectionString.CreateDatabase();
25 | connectionString.DoesDatabaseExist().ShouldBeTrue();
26 | connectionString.DeleteDatabase();
27 | connectionString.DoesDatabaseExist().ShouldBeFalse();
28 | }
29 | }
30 | }
--------------------------------------------------------------------------------
/Test/UnitTests/TestSqlServerLocks.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Linq;
6 | using System.Threading.Tasks;
7 | using Microsoft.Extensions.DependencyInjection;
8 | using Microsoft.Extensions.Hosting;
9 | using RunMethodsSequentially;
10 | using RunMethodsSequentially.LockAndRunCode;
11 | using Test.EfCore;
12 | using Test.Helpers;
13 | using Test.ServicesToCall;
14 | using TestSupport.EfHelpers;
15 | using TestSupport.Helpers;
16 | using Xunit;
17 | using Xunit.Abstractions;
18 | using Xunit.Extensions.AssertExtensions;
19 |
20 | namespace Test.UnitTests
21 | {
22 | public class TestSqlServerLocks
23 | {
24 | private readonly ITestOutputHelper _output;
25 |
26 | public TestSqlServerLocks(ITestOutputHelper output)
27 | {
28 | _output = output;
29 | }
30 |
31 | [Fact]
32 | public void ExampleRegisterRunMethodsSequentially()
33 | {
34 | //SETUP
35 | var connectionString = this.GetUniqueDatabaseConnectionString();
36 | var services = new ServiceCollection();
37 |
38 | //ATTEMPT
39 | services.RegisterRunMethodsSequentially(options =>
40 | {
41 | options.AddSqlServerLockAndRunMethods(connectionString);
42 | options.AddFileSystemLockAndRunMethods(TestData.GetTestDataDir());
43 | }).RegisterServiceToRunInJob()
44 | .RegisterServiceToRunInJob();
45 |
46 | //VERIFY
47 | var serviceProvider = services.BuildServiceProvider();
48 | var options = serviceProvider.GetRequiredService();
49 | options.LockVersionsInOrder.Count.ShouldEqual(2);
50 | options.LockVersionsInOrder.First().LockAndRunClass.ResourceName.ShouldEqual(
51 | $"SQL Server database with name [{connectionString.GetDatabaseNameFromSqlServerConnectionString()}]");
52 | options.LockVersionsInOrder.Last().LockAndRunClass.ResourceName.ShouldEqual(
53 | @"Looking for directory at C:\Users\JonPSmith\source\repos\RunStartupMethodsSequentially\Test\TestData");
54 | serviceProvider.GetService().ShouldNotBeNull();
55 | }
56 |
57 | [Fact]
58 | public void TestRegisterRunMethodsSequentiallyHostedService()
59 | {
60 | //SETUP
61 | var connectionString = this.GetUniqueDatabaseConnectionString();
62 | var services = new ServiceCollection();
63 |
64 | //ATTEMPT
65 | services.RegisterRunMethodsSequentially(options =>
66 | {
67 | options.AddSqlServerLockAndRunMethods(connectionString);
68 | });
69 |
70 | //VERIFY
71 | var serviceProvider = services.BuildServiceProvider();
72 | var options = serviceProvider.GetRequiredService();
73 | options.LockVersionsInOrder.Count.ShouldEqual(1);
74 | options.LockVersionsInOrder.Single().LockAndRunClass.ResourceName.ShouldEqual(
75 | $"SQL Server database with name [{connectionString.GetDatabaseNameFromSqlServerConnectionString()}]");
76 | serviceProvider.GetService().ShouldNotBeNull();
77 | }
78 |
79 | [Fact]
80 | public void TestRegisterRunMethodsSequentiallyNormalService()
81 | {
82 | //SETUP
83 | var connectionString = this.GetUniqueDatabaseConnectionString();
84 | var services = new ServiceCollection();
85 |
86 | //ATTEMPT
87 | services.RegisterRunMethodsSequentially(options =>
88 | {
89 | options.RegisterAsHostedService = false;
90 | options.AddSqlServerLockAndRunMethods(connectionString);
91 | });
92 |
93 | //VERIFY
94 | var serviceProvider = services.BuildServiceProvider();
95 | var options = serviceProvider.GetRequiredService();
96 | options.LockVersionsInOrder.Count.ShouldEqual(1);
97 | options.LockVersionsInOrder.Single().LockAndRunClass.ResourceName.ShouldEqual(
98 | $"SQL Server database with name [{connectionString.GetDatabaseNameFromSqlServerConnectionString()}]");
99 | serviceProvider.GetService().ShouldNotBeNull();
100 | }
101 |
102 | [Fact]
103 | public async Task TestLockSqlDatabaseAndRunOneService()
104 | {
105 | //SETUP
106 | var dbOptions = this.CreateUniqueClassOptions();
107 | using var context = new TestDbContext(dbOptions);
108 | context.Database.EnsureClean();
109 |
110 | var services = context.SetupSqlServerRunMethodsSequentially(
111 | options => options.RegisterServiceToRunInJob());
112 | var testLogger = new RegisterTestLogger(services);
113 | var serviceProvider = services.BuildServiceProvider();
114 | var lockAndRun = serviceProvider.GetRequiredService();
115 |
116 | //ATTEMPT
117 | using (new TimeThings(_output))
118 | await lockAndRun.LockAndLoadAsync();
119 |
120 | //VERIFY
121 | var entry = context.NameDateTimes.Single();
122 | entry.DateTimeUtc.ShouldBeInRange(DateTime.UtcNow.AddMilliseconds(-500), DateTime.UtcNow);
123 | entry.Name.ShouldEqual(nameof(UpdateDatabase1));
124 | var common = context.CommonNameDateTimes.Single();
125 | common.DateTimeUtc.ShouldEqual(entry.DateTimeUtc);
126 | }
127 |
128 | [Fact]
129 | public async Task TestLockSqlDatabaseAndRunTwoServices()
130 | {
131 | //SETUP
132 | var dbOptions = this.CreateUniqueClassOptions();
133 | using var context = new TestDbContext(dbOptions);
134 | context.Database.EnsureClean();
135 |
136 | var services = context.SetupSqlServerRunMethodsSequentially(
137 | options =>
138 | {
139 | options.RegisterServiceToRunInJob();
140 | options.RegisterServiceToRunInJob();
141 | });
142 | var testLogger = new RegisterTestLogger(services);
143 | var serviceProvider = services.BuildServiceProvider();
144 | var lockAndRun = serviceProvider.GetRequiredService();
145 |
146 | //ATTEMPT
147 | await lockAndRun.LockAndLoadAsync();
148 |
149 | //VERIFY
150 | var utcNow = DateTime.UtcNow;
151 | var entries = context.NameDateTimes.OrderBy(x => x.Id).ToList();
152 | entries[0].DateTimeUtc.ShouldBeInRange(DateTime.UtcNow.AddMilliseconds(-500), utcNow);
153 | entries[1].DateTimeUtc.ShouldBeInRange(entries[0].DateTimeUtc, utcNow);
154 | var common = context.CommonNameDateTimes.Single();
155 | common.DateTimeUtc.ShouldEqual(entries[1].DateTimeUtc);
156 | }
157 |
158 |
159 | [Fact]
160 | public async Task TestLockSqlDatabaseAndRunOrderedByWhatOrderToRunIn()
161 | {
162 | //SETUP
163 | var dbOptions = this.CreateUniqueClassOptions();
164 | using var context = new TestDbContext(dbOptions);
165 | context.Database.EnsureClean();
166 |
167 | var services = context.SetupSqlServerRunMethodsSequentially(
168 | options =>
169 | {
170 | options.RegisterServiceToRunInJob();
171 | options.RegisterServiceToRunInJob();
172 | options.RegisterServiceToRunInJob();
173 | });
174 | var testLogger = new RegisterTestLogger(services);
175 | var serviceProvider = services.BuildServiceProvider();
176 | var lockAndRun = serviceProvider.GetRequiredService();
177 |
178 | //ATTEMPT
179 | await lockAndRun.LockAndLoadAsync();
180 |
181 | //VERIFY
182 | var entries = context.NameDateTimes.OrderBy(x => x.Id).ToList();
183 | entries.Select(x => x.Name).ShouldEqual(new[] { "OrderNum = -1", "No OrderNum", "OrderNum = +1" });
184 | }
185 |
186 | [Fact]
187 | public async Task TestLockSqlDatabaseNoDatabaseToStartWith()
188 | {
189 | //SETUP
190 | var dbOptions = this.CreateUniqueClassOptions();
191 | using var context = new TestDbContext(dbOptions);
192 | context.Database.EnsureDeleted();
193 |
194 | var services = context.SetupSqlServerRunMethodsSequentially(
195 | options =>
196 | {
197 | options.AddFileSystemLockAndRunMethods(TestData.GetTestDataDir());
198 | options.RegisterServiceToRunInJob();
199 | options.RegisterServiceToRunInJob();
200 | });
201 | var testLogger = new RegisterTestLogger(services);
202 | var serviceProvider = services.BuildServiceProvider();
203 | var lockAndRun = serviceProvider.GetRequiredService();
204 |
205 | //ATTEMPT
206 | await lockAndRun.LockAndLoadAsync();
207 |
208 | //VERIFY
209 | var entry = context.NameDateTimes.Single();
210 | entry.DateTimeUtc.ShouldBeInRange(DateTime.UtcNow.AddMilliseconds(-500), DateTime.UtcNow);
211 | entry.Name.ShouldEqual(nameof(UpdateDatabase1));
212 | var common = context.CommonNameDateTimes.Single();
213 | common.DateTimeUtc.ShouldEqual(entry.DateTimeUtc);
214 | }
215 |
216 | //------------------------------------------------------
217 | //Check errors
218 |
219 | [Fact]
220 | public async Task TestLockSqlDatabase_NoDatabase()
221 | {
222 | //SETUP
223 | var dbOptions = this.CreateUniqueClassOptions();
224 | using var context = new TestDbContext(dbOptions);
225 | context.Database.EnsureDeleted();
226 |
227 | var services = context.SetupSqlServerRunMethodsSequentially(
228 | options => options.RegisterServiceToRunInJob());
229 | var testLogger = new RegisterTestLogger(services);
230 | var serviceProvider = services.BuildServiceProvider();
231 | var lockAndRun = serviceProvider.GetRequiredService();
232 |
233 | //ATTEMPT
234 | var ex = await Assert.ThrowsAsync(async () => await lockAndRun.LockAndLoadAsync());
235 |
236 | //VERIFY
237 | _output.WriteLine(ex.Message);
238 | ex.Message.ShouldStartWith("No resource were found to lock, so could not run the registered services. ");
239 | }
240 |
241 | [Fact]
242 | public async Task TestLockSqlDatabase_NoServicesFound()
243 | {
244 | //SETUP
245 | var dbOptions = this.CreateUniqueClassOptions();
246 | using var context = new TestDbContext(dbOptions);
247 | context.Database.EnsureClean();
248 |
249 | var services = context.SetupSqlServerRunMethodsSequentially();
250 | var testLogger = new RegisterTestLogger(services);
251 | var serviceProvider = services.BuildServiceProvider();
252 | var lockAndRun = serviceProvider.GetRequiredService();
253 |
254 | //ATTEMPT
255 | var ex = await Assert.ThrowsAsync(async () => await lockAndRun.LockAndLoadAsync());
256 |
257 | //VERIFY
258 | _output.WriteLine(ex.Message);
259 | ex.Message.ShouldStartWith("You have not registered any services to run when the lock is active.");
260 | }
261 |
262 | [Fact]
263 | public async Task TestLockSqlDatabase_DuplicateServices()
264 | {
265 | //SETUP
266 | var dbOptions = this.CreateUniqueClassOptions();
267 | using var context = new TestDbContext(dbOptions);
268 | context.Database.EnsureClean();
269 |
270 | var services = context.SetupSqlServerRunMethodsSequentially(
271 | options =>
272 | {
273 | options.RegisterServiceToRunInJob();
274 | options.RegisterServiceToRunInJob();
275 | });
276 | var testLogger = new RegisterTestLogger(services);
277 | var serviceProvider = services.BuildServiceProvider();
278 | var lockAndRun = serviceProvider.GetRequiredService();
279 |
280 | //ATTEMPT
281 | var ex = await Assert.ThrowsAsync(async () => await lockAndRun.LockAndLoadAsync());
282 |
283 | //VERIFY
284 | _output.WriteLine(ex.Message);
285 | ex.Message.ShouldStartWith("Some of your services registered by RegisterServiceToRunInJob extension method are duplicates.");
286 | }
287 |
288 | [Fact]
289 | public async Task TestLockSqlDatabase_NoRegisteredLockService()
290 | {
291 | //SETUP
292 | var services = new ServiceCollection();
293 | services.RegisterRunMethodsSequentially(options =>
294 | {
295 | options.RegisterAsHostedService = false;
296 | //options.AddLockSqlServerAndRunMethods(context.Database.GetConnectionString());
297 | });
298 | var testLogger = new RegisterTestLogger(services);
299 | var serviceProvider = services.BuildServiceProvider();
300 | var lockAndRun = serviceProvider.GetRequiredService();
301 |
302 | //ATTEMPT
303 | var ex = await Assert.ThrowsAsync(async () => await lockAndRun.LockAndLoadAsync());
304 |
305 | //VERIFY
306 | _output.WriteLine(ex.Message);
307 | ex.Message.ShouldStartWith("You must register at least one lock service when registering");
308 | }
309 |
310 |
311 | //--------------------------------------------------------------
312 | //private method
313 |
314 |
315 | }
316 | }
--------------------------------------------------------------------------------
/Test/UnitTests/TestSqlServerParallelLocks.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 | using System.Collections.Concurrent;
6 | using System.IO;
7 | using System.Linq;
8 | using System.Threading.Tasks;
9 | using Medallion.Threading.FileSystem;
10 | using Medallion.Threading.SqlServer;
11 | using Microsoft.EntityFrameworkCore;
12 | using Microsoft.Extensions.DependencyInjection;
13 | using RunMethodsSequentially;
14 | using RunMethodsSequentially.LockAndRunCode;
15 | using Test.EfCore;
16 | using Test.Helpers;
17 | using Test.ServicesToCall;
18 | using TestSupport.EfHelpers;
19 | using TestSupport.Helpers;
20 | using Xunit;
21 | using Xunit.Abstractions;
22 | using Xunit.Extensions.AssertExtensions;
23 |
24 | namespace Test.UnitTests
25 | {
26 |
27 | //NOTE Parallel.ForEach doesn't handle async
28 | //I found a very useful article https://medium.com/@alex.puiu/parallel-foreach-async-in-c-36756f8ebe62
29 | //From this I decided the AsyncParallelForEach approach, which can run async methods in parallel
30 | public class TestSqlServerParallelLocks
31 | {
32 | private readonly ITestOutputHelper _output;
33 |
34 | public TestSqlServerParallelLocks(ITestOutputHelper output)
35 | {
36 | _output = output;
37 | }
38 |
39 | [Fact]
40 | public async Task TestAsyncParallelForEachNoLocking()
41 | {
42 | //SETUP
43 | var dbOptions = this.CreateUniqueClassOptions();
44 | using var context = new TestDbContext(dbOptions);
45 | context.Database.EnsureClean();
46 |
47 | var logs = new ConcurrentStack();
48 |
49 | async Task TaskAsync(int num)
50 | {
51 | logs.Push($"S{num}: {DateTime.UtcNow:O}");
52 | await Task.Delay(100);
53 | logs.Push($"E{num}: {DateTime.UtcNow:O}");
54 | }
55 |
56 | //ATTEMPT
57 | await 3.NumTimesAsyncEnumerable().AsyncParallelForEach(TaskAsync);
58 |
59 | //VERIFY
60 | foreach (var log in logs.Reverse())
61 | {
62 | _output.WriteLine(log);
63 | }
64 | logs.Reverse().Select(x => x.First()).ShouldEqual(new []{'S', 'S', 'S', 'E', 'E', 'E', });
65 | }
66 |
67 | [Fact]
68 | public async Task TestAsyncParallelForEachWithSqlLocking()
69 | {
70 | //SETUP
71 | var dbOptions = this.CreateUniqueClassOptions();
72 | using var context = new TestDbContext(dbOptions);
73 | context.Database.EnsureClean();
74 |
75 | var logs = new ConcurrentStack();
76 |
77 | async Task TaskAsync(int i)
78 | {
79 | var distributedLock = new SqlDistributedLock("MyLock", context.Database.GetConnectionString());
80 | await using (await distributedLock.AcquireAsync())
81 | {
82 | logs.Push($"S{i}: {DateTime.UtcNow:O}");
83 | await Task.Delay(100);
84 | logs.Push($"E{i}: {DateTime.UtcNow:O}");
85 | }
86 | }
87 |
88 | //ATTEMPT
89 | await 3.NumTimesAsyncEnumerable().AsyncParallelForEach(TaskAsync);
90 |
91 | //VERIFY
92 | foreach (var log in logs.Reverse())
93 | {
94 | _output.WriteLine(log);
95 | }
96 | logs.Reverse().Select(x => x.First()).ShouldEqual(new[] { 'S', 'E', 'S', 'E', 'S', 'E', });
97 | }
98 |
99 | [Fact]
100 | public async Task TestAsyncParallelForEachWithFileSystemLocking()
101 | {
102 | //SETUP
103 | var dbOptions = this.CreateUniqueClassOptions();
104 | using var context = new TestDbContext(dbOptions);
105 | context.Database.EnsureClean();
106 |
107 | var logs = new ConcurrentStack();
108 |
109 | async Task TaskAsync(int i)
110 | {
111 | var lockFileDirectory = new DirectoryInfo(TestData.GetTestDataDir());
112 | var distributedLock = new FileDistributedLock(lockFileDirectory, "MyLock");
113 | await using (await distributedLock.AcquireAsync())
114 | {
115 | logs.Push($"S{i}: {DateTime.UtcNow:O}");
116 | await Task.Delay(100);
117 | logs.Push($"E{i}: {DateTime.UtcNow:O}");
118 | }
119 |
120 | return i;
121 | }
122 |
123 | //ATTEMPT
124 | await 3.NumTimesAsyncEnumerable().AsyncParallelForEach(TaskAsync);
125 |
126 | //VERIFY
127 | foreach (var log in logs.Reverse())
128 | {
129 | _output.WriteLine(log);
130 | }
131 | logs.Reverse().Select(x => x.First()).ShouldEqual(new[] { 'S', 'E', 'S', 'E', 'S', 'E', });
132 | }
133 |
134 | [Fact]
135 | public async Task TestLockSqlDatabaseAndRunOneService()
136 | {
137 | //SETUP
138 | var dbOptions = this.CreateUniqueClassOptions();
139 | using var context = new TestDbContext(dbOptions);
140 | context.Database.EnsureClean();
141 |
142 | context.Add(new CommonNameDateTime { Name = "start", DateTimeUtc = DateTime.UtcNow });
143 | await context.SaveChangesAsync();
144 |
145 | async Task TaskAsync(int i1)
146 | {
147 | using var localContext = new TestDbContext(dbOptions);
148 | var services = localContext.SetupSqlServerRunMethodsSequentially(
149 | options => options.RegisterServiceToRunInJob());
150 | var testLogger = new RegisterTestLogger(services);
151 | var serviceProvider = services.BuildServiceProvider();
152 | var lockAndRun = serviceProvider.GetRequiredService();
153 | await lockAndRun.LockAndLoadAsync();
154 | }
155 |
156 | //ATTEMPT
157 | await 3.NumTimesAsyncEnumerable().AsyncParallelForEach(TaskAsync);
158 |
159 | //VERIFY
160 | var entities = context.NameDateTimes.ToList();
161 | foreach (var entity in entities)
162 | {
163 | _output.WriteLine(entity.ToString());
164 | }
165 | entities.Select(x => x.Name).ShouldEqual(new[] { "Read", "Write", "Read", "Write", "Read", "Write", });
166 | for (int i = 1; i < entities.Count; i++)
167 | {
168 | (entities[i].DateTimeUtc >= entities[i - 1].DateTimeUtc).ShouldBeTrue(i.ToString());
169 | }
170 | }
171 |
172 | [Fact]
173 | public async Task TestLockSqlDatabase_Timeout()
174 | {
175 | //SETUP
176 | var dbOptions = this.CreateUniqueClassOptions();
177 | using var context = new TestDbContext(dbOptions);
178 | context.Database.EnsureClean();
179 |
180 | context.Add(new CommonNameDateTime { Name = "start", DateTimeUtc = DateTime.UtcNow });
181 | await context.SaveChangesAsync();
182 |
183 | async Task TaskAsync(int i1)
184 | {
185 | var services = context.SetupSqlServerRunMethodsSequentially(options =>
186 | {
187 | options.DefaultLockTimeoutInSeconds = 1;
188 | options.RegisterServiceToRunInJob();
189 | options.RegisterServiceToRunInJob();
190 | });
191 | var testLogger = new RegisterTestLogger(services);
192 | var serviceProvider = services.BuildServiceProvider();
193 | var lockAndRun = serviceProvider.GetRequiredService();
194 | await lockAndRun.LockAndLoadAsync();
195 | }
196 |
197 | //ATTEMPT
198 | var ex = await Assert.ThrowsAsync(async () => await 3.NumTimesAsyncEnumerable().AsyncParallelForEach(TaskAsync));
199 |
200 | //VERIFY
201 | _output.WriteLine(ex.Message);
202 | ex.Message.ShouldEqual("Timeout exceeded when trying to acquire the lock");
203 | }
204 |
205 | }
206 | }
--------------------------------------------------------------------------------
/Test/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "ConnectionStrings": {
3 | "UnitTestConnection": "Server=(localdb)\\mssqllocaldb;Database=RunMethodsSequentially-Test;Trusted_Connection=True;MultipleActiveResultSets=true",
4 | "PostgreSqlConnection": "Host=127.0.0.1;Port=5432;Database=RunStartup-Test;Username=postgres;Password=LetMeIn"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/WebSiteRunSequentially/Controllers/HomeController.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using System.Diagnostics;
3 | using WebSiteRunSequentially.Database;
4 | using WebSiteRunSequentially.Models;
5 |
6 | namespace WebSiteRunSequentially.Controllers
7 | {
8 | public class HomeController : Controller
9 | {
10 | private readonly ILogger _logger;
11 |
12 | public HomeController(ILogger logger)
13 | {
14 | _logger = logger;
15 | }
16 |
17 | public IActionResult Index([FromServices] WebSiteDbContext context)
18 | {
19 | var common = context.CommonNameDateTimes.SingleOrDefault();
20 | var logs = context.NameDateTimes.OrderByDescending(x => x.DateTimeUtc).ToList();
21 |
22 | return View(new CommonLogsDto(common, logs));
23 | }
24 |
25 | public IActionResult DelLogs([FromServices] WebSiteDbContext context)
26 | {
27 | context.RemoveRange(context.NameDateTimes);
28 | context.SaveChanges();
29 |
30 | return RedirectToAction("Index");
31 | }
32 |
33 | public IActionResult Privacy()
34 | {
35 | _logger.LogInformation("The Privacy link has been clicked");
36 | return View();
37 | }
38 |
39 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
40 | public IActionResult Error()
41 | {
42 | return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
43 | }
44 | }
45 | }
--------------------------------------------------------------------------------
/WebSiteRunSequentially/Database/CommonNameDateTime.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | namespace WebSiteRunSequentially.Database
5 | {
6 | public class CommonNameDateTime
7 | {
8 | public int Id { get; set; }
9 | public string Name { get; set; }
10 | public int Stage { get; set; }
11 | public DateTime DateTimeUtc { get; set; }
12 |
13 | public override string ToString()
14 | {
15 | return $"Name: {Name}, Stage: {Stage}, DateTimeUtc: {DateTimeUtc:O}";
16 | }
17 | }
18 | }
--------------------------------------------------------------------------------
/WebSiteRunSequentially/Database/NameDateTime.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using System;
5 |
6 | namespace WebSiteRunSequentially.Database
7 | {
8 | public class NameDateTime
9 | {
10 | public int Id { get; set; }
11 | public string Name { get; set; }
12 | public DateTime DateTimeUtc { get; set; }
13 |
14 | public override string ToString()
15 | {
16 | return $"Name: {Name}, DateTimeUtc: {DateTimeUtc:O}";
17 | }
18 | }
19 | }
--------------------------------------------------------------------------------
/WebSiteRunSequentially/Database/WebSiteDbContext.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2021 Jon P Smith, GitHub: JonPSmith, web: http://www.thereformedprogrammer.net/
2 | // Licensed under MIT license. See License.txt in the project root for license information.
3 |
4 | using Microsoft.EntityFrameworkCore;
5 |
6 | namespace WebSiteRunSequentially.Database
7 | {
8 | public class WebSiteDbContext : DbContext
9 | {
10 | public WebSiteDbContext(DbContextOptions options)
11 | : base(options) { }
12 |
13 | public DbSet NameDateTimes { get; set; }
14 | public DbSet CommonNameDateTimes { get; set; }
15 | }
16 | }
--------------------------------------------------------------------------------
/WebSiteRunSequentially/Models/CommonLogsDto.cs:
--------------------------------------------------------------------------------
1 | using WebSiteRunSequentially.Database;
2 |
3 | namespace WebSiteRunSequentially.Models
4 | {
5 | public class CommonLogsDto
6 | {
7 | public CommonNameDateTime Common { get; }
8 | public List Logs { get; }
9 |
10 | public CommonLogsDto(CommonNameDateTime common, List logs)
11 | {
12 | Common = common;
13 | Logs = logs;
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/WebSiteRunSequentially/Models/ErrorViewModel.cs:
--------------------------------------------------------------------------------
1 | namespace WebSiteRunSequentially.Models
2 | {
3 | public class ErrorViewModel
4 | {
5 | public string? RequestId { get; set; }
6 |
7 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
8 | }
9 | }
--------------------------------------------------------------------------------
/WebSiteRunSequentially/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using RunMethodsSequentially;
3 | using WebSiteRunSequentially.Database;
4 | using WebSiteRunSequentially.StartupServices;
5 |
6 | var builder = WebApplication.CreateBuilder(args);
7 |
8 | // Add services to the container.
9 | builder.Services.AddControllersWithViews();
10 |
11 | var connectionString = builder.Configuration
12 | .GetConnectionString("DefaultConnection");
13 | var lockFolder = builder.Environment.WebRootPath;
14 |
15 | builder.Services.AddDbContext(options =>
16 | options.UseSqlServer(connectionString));
17 |
18 | builder.Services.RegisterRunMethodsSequentially(options =>
19 | {
20 | options.AddSqlServerLockAndRunMethods(connectionString);
21 | options.AddFileSystemLockAndRunMethods(lockFolder);
22 | })
23 | .RegisterServiceToRunInJob()
24 | //.RegisterServiceToRunInJob() //Checks that an Exception will stop the application.
25 | .RegisterServiceToRunInJob();
26 |
27 | var app = builder.Build();
28 |
29 | // Configure the HTTP request pipeline.
30 | if (!app.Environment.IsDevelopment())
31 | {
32 | app.UseExceptionHandler("/Home/Error");
33 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
34 | app.UseHsts();
35 | }
36 |
37 | app.UseHttpsRedirection();
38 | app.UseStaticFiles();
39 |
40 | app.UseRouting();
41 |
42 | app.UseAuthorization();
43 |
44 | app.MapControllerRoute(
45 | name: "default",
46 | pattern: "{controller=Home}/{action=Index}/{id?}");
47 |
48 | app.Run();
49 |
--------------------------------------------------------------------------------
/WebSiteRunSequentially/Properties/ServiceDependencies/WebSiteRunSequentially - Web Deploy/mssql1.arm.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
3 | "contentVersion": "1.0.0.0",
4 | "parameters": {
5 | "resourceGroupName": {
6 | "type": "string",
7 | "defaultValue": "UkSouth-dev",
8 | "metadata": {
9 | "_parameterType": "resourceGroup",
10 | "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking."
11 | }
12 | },
13 | "resourceGroupLocation": {
14 | "type": "string",
15 | "defaultValue": "uksouth",
16 | "metadata": {
17 | "_parameterType": "location",
18 | "description": "Location of the resource group. Resource groups could have different location than resources."
19 | }
20 | },
21 | "resourceLocation": {
22 | "type": "string",
23 | "defaultValue": "[parameters('resourceGroupLocation')]",
24 | "metadata": {
25 | "_parameterType": "location",
26 | "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there."
27 | }
28 | }
29 | },
30 | "resources": [
31 | {
32 | "type": "Microsoft.Resources/resourceGroups",
33 | "name": "[parameters('resourceGroupName')]",
34 | "location": "[parameters('resourceGroupLocation')]",
35 | "apiVersion": "2019-10-01"
36 | },
37 | {
38 | "type": "Microsoft.Resources/deployments",
39 | "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat('RunMethodsSequentially-WebSiteRunSequentially', subscription().subscriptionId)))]",
40 | "resourceGroup": "[parameters('resourceGroupName')]",
41 | "apiVersion": "2019-10-01",
42 | "dependsOn": [
43 | "[parameters('resourceGroupName')]"
44 | ],
45 | "properties": {
46 | "mode": "Incremental",
47 | "template": {
48 | "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
49 | "contentVersion": "1.0.0.0",
50 | "resources": [
51 | {
52 | "kind": "v12.0",
53 | "location": "[parameters('resourceLocation')]",
54 | "name": "efcore5bookapp",
55 | "type": "Microsoft.Sql/servers",
56 | "apiVersion": "2017-10-01-preview"
57 | },
58 | {
59 | "sku": {
60 | "name": "Basic",
61 | "tier": "Basic",
62 | "capacity": 5
63 | },
64 | "kind": "v12.0,user",
65 | "location": "[parameters('resourceLocation')]",
66 | "name": "efcore5bookapp/RunMethodsSequentially-WebSiteRunSequentially",
67 | "type": "Microsoft.Sql/servers/databases",
68 | "apiVersion": "2017-10-01-preview",
69 | "dependsOn": [
70 | "efcore5bookapp"
71 | ]
72 | }
73 | ]
74 | }
75 | }
76 | }
77 | ],
78 | "metadata": {
79 | "_dependencyType": "mssql.azure"
80 | }
81 | }
--------------------------------------------------------------------------------
/WebSiteRunSequentially/Properties/ServiceDependencies/WebSiteRunSequentially - Web Deploy/profile.arm.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
3 | "contentVersion": "1.0.0.0",
4 | "metadata": {
5 | "_dependencyType": "compute.appService.windows"
6 | },
7 | "parameters": {
8 | "resourceGroupName": {
9 | "type": "string",
10 | "defaultValue": "UkSouth-dev",
11 | "metadata": {
12 | "description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking."
13 | }
14 | },
15 | "resourceGroupLocation": {
16 | "type": "string",
17 | "defaultValue": "uksouth",
18 | "metadata": {
19 | "description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support."
20 | }
21 | },
22 | "resourceName": {
23 | "type": "string",
24 | "defaultValue": "WebSiteRunSequentially",
25 | "metadata": {
26 | "description": "Name of the main resource to be created by this template."
27 | }
28 | },
29 | "resourceLocation": {
30 | "type": "string",
31 | "defaultValue": "[parameters('resourceGroupLocation')]",
32 | "metadata": {
33 | "description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there."
34 | }
35 | }
36 | },
37 | "variables": {
38 | "appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
39 | "appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]"
40 | },
41 | "resources": [
42 | {
43 | "type": "Microsoft.Resources/resourceGroups",
44 | "name": "[parameters('resourceGroupName')]",
45 | "location": "[parameters('resourceGroupLocation')]",
46 | "apiVersion": "2019-10-01"
47 | },
48 | {
49 | "type": "Microsoft.Resources/deployments",
50 | "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
51 | "resourceGroup": "[parameters('resourceGroupName')]",
52 | "apiVersion": "2019-10-01",
53 | "dependsOn": [
54 | "[parameters('resourceGroupName')]"
55 | ],
56 | "properties": {
57 | "mode": "Incremental",
58 | "template": {
59 | "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
60 | "contentVersion": "1.0.0.0",
61 | "resources": [
62 | {
63 | "location": "[parameters('resourceLocation')]",
64 | "name": "[parameters('resourceName')]",
65 | "type": "Microsoft.Web/sites",
66 | "apiVersion": "2015-08-01",
67 | "tags": {
68 | "[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty"
69 | },
70 | "dependsOn": [
71 | "[variables('appServicePlan_ResourceId')]"
72 | ],
73 | "kind": "app",
74 | "properties": {
75 | "name": "[parameters('resourceName')]",
76 | "kind": "app",
77 | "httpsOnly": true,
78 | "reserved": false,
79 | "serverFarmId": "[variables('appServicePlan_ResourceId')]",
80 | "siteConfig": {
81 | "metadata": [
82 | {
83 | "name": "CURRENT_STACK",
84 | "value": "dotnetcore"
85 | }
86 | ]
87 | }
88 | },
89 | "identity": {
90 | "type": "SystemAssigned"
91 | }
92 | },
93 | {
94 | "location": "[parameters('resourceLocation')]",
95 | "name": "[variables('appServicePlan_name')]",
96 | "type": "Microsoft.Web/serverFarms",
97 | "apiVersion": "2015-08-01",
98 | "sku": {
99 | "name": "S1",
100 | "tier": "Standard",
101 | "family": "S",
102 | "size": "S1"
103 | },
104 | "properties": {
105 | "name": "[variables('appServicePlan_name')]"
106 | }
107 | }
108 | ]
109 | }
110 | }
111 | }
112 | ]
113 | }
--------------------------------------------------------------------------------
/WebSiteRunSequentially/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "iisSettings": {
3 | "windowsAuthentication": false,
4 | "anonymousAuthentication": true,
5 | "iisExpress": {
6 | "applicationUrl": "http://localhost:39953",
7 | "sslPort": 44359
8 | }
9 | },
10 | "profiles": {
11 | "WebSiteRunSequentially": {
12 | "commandName": "Project",
13 | "dotnetRunMessages": true,
14 | "launchBrowser": true,
15 | "applicationUrl": "https://localhost:7181;http://localhost:5181",
16 | "environmentVariables": {
17 | "ASPNETCORE_ENVIRONMENT": "Development"
18 | }
19 | },
20 | "IIS Express": {
21 | "commandName": "IISExpress",
22 | "launchBrowser": true,
23 | "environmentVariables": {
24 | "ASPNETCORE_ENVIRONMENT": "Development"
25 | }
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/WebSiteRunSequentially/Properties/serviceDependencies.WebSiteRunSequentially - Web Deploy.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "mssql1": {
4 | "secretStore": "AzureAppSettings",
5 | "resourceId": "/subscriptions/[parameters('subscriptionId')]/resourceGroups/[parameters('resourceGroupName')]/providers/Microsoft.Sql/servers/efcore5bookapp/databases/RunMethodsSequentially-WebSiteRunSequentially",
6 | "type": "mssql.azure",
7 | "connectionId": "ConnectionStrings:DefaultConnection"
8 | }
9 | }
10 | }
--------------------------------------------------------------------------------
/WebSiteRunSequentially/Properties/serviceDependencies.json:
--------------------------------------------------------------------------------
1 | {
2 | "dependencies": {
3 | "mssql1": {
4 | "type": "mssql",
5 | "connectionId": "ConnectionStrings:DefaultConnection"
6 | }
7 | }
8 | }
--------------------------------------------------------------------------------
/WebSiteRunSequentially/StartupServices/StartupServiceEnsureCreated.cs:
--------------------------------------------------------------------------------
1 | using RunMethodsSequentially;
2 | using WebSiteRunSequentially.Database;
3 |
4 | namespace WebSiteRunSequentially.StartupServices
5 | {
6 | public class StartupServiceEnsureCreated : IStartupServiceToRunSequentially
7 | {
8 | public int OrderNum { get; }
9 | public async ValueTask ApplyYourChangeAsync(IServiceProvider scopedServices)
10 | {
11 | var testDbContext = scopedServices.GetRequiredService();
12 | await testDbContext.Database.EnsureCreatedAsync();
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/WebSiteRunSequentially/StartupServices/StartupServiceSeedDatabase.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using RunMethodsSequentially;
3 | using WebSiteRunSequentially.Database;
4 |
5 | namespace WebSiteRunSequentially.StartupServices
6 | {
7 | public class StartupServiceSeedDatabase : IStartupServiceToRunSequentially
8 | {
9 | public int OrderNum { get; }
10 | public async ValueTask ApplyYourChangeAsync(IServiceProvider scopedServices)
11 | {
12 | var context = scopedServices.GetRequiredService();
13 |
14 | var startTime = DateTime.UtcNow;
15 |
16 | var commonEntity = await context.CommonNameDateTimes.SingleOrDefaultAsync();
17 | if (commonEntity == null)
18 | {
19 | context.Add(new CommonNameDateTime{ Name = "New", Stage = 1, DateTimeUtc = startTime });
20 | context.Add(new NameDateTime { Name = "No common entity found. Created a new common and set its Stage to 1", DateTimeUtc = startTime});
21 | }
22 | else if (commonEntity.DateTimeUtc < startTime.AddMinutes(-5))
23 | {
24 | var timeDiff = startTime - commonEntity.DateTimeUtc;
25 | commonEntity.Name = "Reset";
26 | commonEntity.DateTimeUtc = startTime;
27 | commonEntity.Stage = 1;
28 | context.Add(new NameDateTime { Name = $"Common entity found, but was {timeDiff.ToString(@"hh\:mm\:ss")} old. Updated common to Stage to 1", DateTimeUtc = startTime });
29 | }
30 | else
31 | {
32 | commonEntity.Name = "Updated";
33 | commonEntity.DateTimeUtc = startTime;
34 | commonEntity.Stage = commonEntity.Stage + 1;
35 | context.Add(new NameDateTime { Name = $"Common entity found. Updated common to Stage to {commonEntity.Stage}", DateTimeUtc = startTime });
36 | }
37 |
38 | await context.SaveChangesAsync();
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/WebSiteRunSequentially/StartupServices/StartupServiceThrowException.cs:
--------------------------------------------------------------------------------
1 | using RunMethodsSequentially;
2 | using WebSiteRunSequentially.Database;
3 |
4 | namespace WebSiteRunSequentially.StartupServices
5 | {
6 | public class StartupServiceThrowException : IStartupServiceToRunSequentially
7 | {
8 | public int OrderNum { get; }
9 | public async ValueTask ApplyYourChangeAsync(IServiceProvider scopedServices)
10 | {
11 | throw new Exception("This should stop the application.");
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/WebSiteRunSequentially/Views/Home/Index.cshtml:
--------------------------------------------------------------------------------
1 | @model CommonLogsDto
2 | @{
3 | ViewData["Title"] = "Home Page";
4 | }
5 |
6 |
7 |
Test of using RunStartupMethodsSequentially
8 |
9 |
10 |
11 | This web app is designed to test that two start services, 1. migrate and 2. seed a database is run seperately when using the
12 | RunStartupMethodsSequentially library.
13 |
14 |
15 | When this web app is deployed to mutiple instances it will update an single entity (referred to as "Common") , and another entity (referred to as "Logs")
16 | provides a log of what happened. From this information you can check that each instance runs the the startup services.
17 | For instance if you had three instances of this apllication running, then you should get something like this.
18 |
19 |
Common entity
20 |
State: Updated, Stage = 3, time = ...
21 |
Losg - newest first
22 |
23 |
Common entity found. Updated common to Stage to 3
24 |
Common entity found. Updated common to Stage to 2
25 |
No common entity found. Created a new common and set its Stage to 1
26 |
27 |
28 |
NOTE: Timings of when each instance runs on Azure is variable.
29 |
30 | See the page below for the current results.
31 |
32 |
33 |
18 | Swapping to Development environment will display more detailed information about the error that occurred.
19 |
20 |
21 | The Development environment shouldn't be enabled for deployed applications.
22 | It can result in displaying sensitive information from exceptions to end users.
23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
24 | and restarting the app.
25 |