├── .dockerignore ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── comment-on-pr.yml │ └── dotnetcore.yml ├── .gitignore ├── .template.config └── template.json ├── .vscode └── tasks.json ├── CleanArchitecture.WorkerService.nuspec ├── CleanArchitecture.WorkerService.sln ├── Directory.Build.props ├── Directory.Packages.props ├── LICENSE ├── README.md ├── ef-migrations.txt ├── global.json ├── src ├── CleanArchitecture.Core │ ├── CleanArchitecture.Core.csproj │ ├── Entities │ │ ├── BaseEntity.cs │ │ └── UrlStatusHistory.cs │ ├── Interfaces │ │ ├── IEntryPointService.cs │ │ ├── IHttpService.cs │ │ ├── ILoggerAdapter.cs │ │ ├── IQueueReceiver.cs │ │ ├── IQueueSender.cs │ │ ├── IRepository.cs │ │ └── IUrlStatusChecker.cs │ ├── Services │ │ ├── EntryPointService.cs │ │ ├── IServiceLocator.cs │ │ ├── ServiceScopeFactoryLocator.cs │ │ └── UrlStatusChecker.cs │ └── Settings │ │ └── EntryPointSettings.cs ├── CleanArchitecture.Infrastructure │ ├── CleanArchitecture.Infrastructure.csproj │ ├── Data │ │ ├── AppDbContext.cs │ │ ├── Config │ │ │ ├── Constants.cs │ │ │ └── UrlStatusHistoryConfiguration.cs │ │ ├── EfRepository.cs │ │ └── Migrations │ │ │ ├── 20191101171448_InitialModel.Designer.cs │ │ │ ├── 20191101171448_InitialModel.cs │ │ │ └── AppDbContextModelSnapshot.cs │ ├── Http │ │ └── HttpService.cs │ ├── LoggerAdapter.cs │ ├── Messaging │ │ ├── InMemoryQueueReceiver.cs │ │ └── InMemoryQueueSender.cs │ └── ServiceCollectionSetupExtensions.cs └── CleanArchitecture.Worker │ ├── CleanArchitecture.Worker.csproj │ ├── Dockerfile │ ├── Program.cs │ ├── Worker.cs │ ├── WorkerSettings.cs │ ├── appsettings.Development.json │ └── appsettings.json └── tests └── CleanArchitecture.UnitTests ├── CleanArchitecture.UnitTests.csproj ├── Core └── EntryPointServiceExecuteAsync.cs └── Infrastructure └── EntryPointServiceExecuteAsync.cs /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ############################### 2 | # Core EditorConfig Options # 3 | ############################### 4 | root = true 5 | # All files 6 | [*] 7 | indent_style = space 8 | 9 | # XML project files 10 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] 11 | indent_size = 2 12 | 13 | # XML config files 14 | [*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] 15 | indent_size = 2 16 | 17 | # Code files 18 | [*.{cs,csx,vb,vbx}] 19 | indent_size = 2 20 | insert_final_newline = true 21 | charset = utf-8-bom 22 | ############################### 23 | # .NET Coding Conventions # 24 | ############################### 25 | [*.{cs,vb}] 26 | # Organize usings 27 | dotnet_sort_system_directives_first = true 28 | # this. preferences 29 | dotnet_style_qualification_for_field = false:silent 30 | dotnet_style_qualification_for_property = false:silent 31 | dotnet_style_qualification_for_method = false:silent 32 | dotnet_style_qualification_for_event = false:silent 33 | # Language keywords vs BCL types preferences 34 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 35 | dotnet_style_predefined_type_for_member_access = true:silent 36 | # Parentheses preferences 37 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 38 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 39 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 40 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 41 | # Modifier preferences 42 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 43 | dotnet_style_readonly_field = true:suggestion 44 | # Expression-level preferences 45 | dotnet_style_object_initializer = true:suggestion 46 | dotnet_style_collection_initializer = true:suggestion 47 | dotnet_style_explicit_tuple_names = true:suggestion 48 | dotnet_style_null_propagation = true:suggestion 49 | dotnet_style_coalesce_expression = true:suggestion 50 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent 51 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 52 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 53 | dotnet_style_prefer_auto_properties = true:silent 54 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 55 | dotnet_style_prefer_conditional_expression_over_return = true:silent 56 | ############################### 57 | # Naming Conventions # 58 | ############################### 59 | # Style Definitions 60 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 61 | # Use PascalCase for constant fields 62 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 63 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 64 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 65 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 66 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = * 67 | dotnet_naming_symbols.constant_fields.required_modifiers = const 68 | tab_width= 2 69 | dotnet_naming_rule.private_members_with_underscore.symbols = private_fields 70 | dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore 71 | dotnet_naming_rule.private_members_with_underscore.severity = suggestion 72 | dotnet_naming_symbols.private_fields.applicable_kinds = field 73 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private 74 | dotnet_naming_style.prefix_underscore.capitalization = camel_case 75 | dotnet_naming_style.prefix_underscore.required_prefix = _ 76 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 77 | end_of_line = crlf 78 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 79 | dotnet_style_prefer_compound_assignment = true:suggestion 80 | dotnet_style_prefer_simplified_interpolation = true:suggestion 81 | dotnet_style_namespace_match_folder = true:suggestion 82 | ############################### 83 | # IDE EditorConfig Settings # 84 | ############################### 85 | # IDE0005: Using directive is unnecessary. 86 | dotnet_diagnostic.IDE0005.severity = warning 87 | ############################### 88 | # C# Coding Conventions # 89 | ############################### 90 | [*.cs] 91 | # var preferences 92 | csharp_style_var_for_built_in_types = true:silent 93 | csharp_style_var_when_type_is_apparent = true:silent 94 | csharp_style_var_elsewhere = true:silent 95 | # Expression-bodied members 96 | csharp_style_expression_bodied_methods = false:silent 97 | csharp_style_expression_bodied_constructors = false:silent 98 | csharp_style_expression_bodied_operators = false:silent 99 | csharp_style_expression_bodied_properties = true:silent 100 | csharp_style_expression_bodied_indexers = true:silent 101 | csharp_style_expression_bodied_accessors = true:silent 102 | # Pattern matching preferences 103 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 104 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 105 | # Null-checking preferences 106 | csharp_style_throw_expression = true:suggestion 107 | csharp_style_conditional_delegate_call = true:suggestion 108 | # Modifier preferences 109 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 110 | # Expression-level preferences 111 | csharp_prefer_braces = true:silent 112 | csharp_style_deconstructed_variable_declaration = true:suggestion 113 | csharp_prefer_simple_default_expression = true:suggestion 114 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 115 | csharp_style_inlined_variable_declaration = true:suggestion 116 | # Namespaces 117 | csharp_style_namespace_declarations = file_scoped:warning 118 | ############################### 119 | # C# Formatting Rules # 120 | ############################### 121 | # New line preferences 122 | csharp_new_line_before_open_brace = all 123 | csharp_new_line_before_else = true 124 | csharp_new_line_before_catch = true 125 | csharp_new_line_before_finally = true 126 | csharp_new_line_before_members_in_object_initializers = true 127 | csharp_new_line_before_members_in_anonymous_types = true 128 | csharp_new_line_between_query_expression_clauses = true 129 | # Indentation preferences 130 | csharp_indent_case_contents = true 131 | csharp_indent_switch_labels = true 132 | csharp_indent_labels = flush_left 133 | # Space preferences 134 | csharp_space_after_cast = false 135 | csharp_space_after_keywords_in_control_flow_statements = true 136 | csharp_space_between_method_call_parameter_list_parentheses = false 137 | csharp_space_between_method_declaration_parameter_list_parentheses = false 138 | csharp_space_between_parentheses = false 139 | csharp_space_before_colon_in_inheritance_clause = true 140 | csharp_space_after_colon_in_inheritance_clause = true 141 | csharp_space_around_binary_operators = before_and_after 142 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 143 | csharp_space_between_method_call_name_and_opening_parenthesis = false 144 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 145 | # Wrapping preferences 146 | csharp_preserve_single_line_statements = true 147 | csharp_preserve_single_line_blocks = true 148 | csharp_using_directive_placement = outside_namespace:silent 149 | csharp_prefer_simple_using_statement = true:suggestion 150 | csharp_style_prefer_method_group_conversion = true:silent 151 | csharp_style_prefer_top_level_statements = true:silent 152 | csharp_style_expression_bodied_lambdas = true:silent 153 | csharp_style_expression_bodied_local_functions = false:silent 154 | csharp_style_prefer_null_check_over_type_check = true:suggestion 155 | csharp_style_prefer_local_over_anonymous_function = true:suggestion 156 | csharp_style_prefer_index_operator = true:suggestion 157 | csharp_style_prefer_range_operator = true:suggestion 158 | csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion 159 | csharp_style_prefer_tuple_swap = true:suggestion 160 | csharp_style_prefer_utf8_string_literals = true:suggestion 161 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion 162 | csharp_style_unused_value_expression_statement_preference = discard_variable:silent 163 | ############################### 164 | # VB Coding Conventions # 165 | ############################### 166 | [*.vb] 167 | # Modifier preferences 168 | visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/workflows/comment-on-pr.yml: -------------------------------------------------------------------------------- 1 | name: Comment on the Pull Request 2 | 3 | # read-write repo token 4 | # See: https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ 5 | on: 6 | workflow_run: 7 | workflows: [".NET Core"] 8 | types: 9 | - completed 10 | 11 | jobs: 12 | comment: 13 | runs-on: ubuntu-latest 14 | 15 | # Only comment on the PR if this is a PR event 16 | if: github.event.workflow_run.event == 'pull_request' 17 | 18 | steps: 19 | - name: Get the PR Number artifact 20 | uses: dawidd6/action-download-artifact@v2 21 | with: 22 | workflow: ${{ github.event.workflow_run.workflow_id }} 23 | workflow_conclusion: "" 24 | name: pr-number 25 | - name: Read PR Number into GitHub environment variables 26 | run: echo "PR_NUMBER=$(cat pr-number.txt)" >> $GITHUB_ENV 27 | - name: Confirm the PR Number (Debugging) 28 | run: echo $PR_NUMBER 29 | - name: Get the code coverage results file 30 | uses: dawidd6/action-download-artifact@v2 31 | with: 32 | workflow: ${{ github.event.workflow_run.workflow_id }} 33 | workflow_conclusion: "" 34 | name: code-coverage-results 35 | - name: Add Coverage PR Comment 36 | uses: marocchino/sticky-pull-request-comment@v2 37 | with: 38 | number: ${{ env.PR_NUMBER }} 39 | recreate: true 40 | path: code-coverage-results.md -------------------------------------------------------------------------------- /.github/workflows/dotnetcore.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ main ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | # Write permission needed to add PR comment 16 | permissions: 17 | pull-requests: write 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Setup .NET Core 22 | uses: actions/setup-dotnet@v3 23 | with: 24 | dotnet-version: '8.x' 25 | - name: Install dependencies 26 | run: dotnet restore 27 | - name: Build 28 | run: dotnet build --configuration Release --no-restore 29 | 30 | # See https://josh-ops.com/posts/github-code-coverage/ 31 | # Add coverlet.collector nuget package to test project - 'dotnet add package coverlet 32 | - name: Test 33 | run: dotnet test --no-restore --verbosity normal --collect:"XPlat Code Coverage" --logger trx --results-directory coverage 34 | 35 | - name: Copy Coverage To Predictable Location 36 | # run: cp coverage/*/coverage.cobertura.xml coverage/coverage.cobertura.xml 37 | run: find coverage -type f -name coverage.cobertura.xml -exec cp -p {} coverage/coverage.cobertura.xml \; 38 | 39 | - name: Code Coverage Summary Report 40 | uses: irongut/CodeCoverageSummary@v1.3.0 41 | # uses: joshjohanning/CodeCoverageSummary@v1.0.2 42 | with: 43 | filename: coverage/coverage.cobertura.xml 44 | badge: true 45 | format: 'markdown' 46 | output: 'both' 47 | 48 | - name: Upload code coverage results artifact 49 | uses: actions/upload-artifact@v3 50 | if: success() || failure() 51 | with: 52 | name: code-coverage-results 53 | path: code-coverage-results.md 54 | retention-days: 1 55 | 56 | - name: Save the PR number in an artifact 57 | if: github.event_name == 'pull_request' && (success() || failure()) 58 | shell: bash 59 | env: 60 | PR_NUMBER: ${{ github.event.number }} 61 | run: echo $PR_NUMBER > pr-number.txt 62 | 63 | - name: Upload the PR number 64 | uses: actions/upload-artifact@v3 65 | if: github.event_name == 'pull_request' && (success() || failure()) 66 | with: 67 | name: pr-number 68 | path: ./pr-number.txt 69 | retention-days: 1 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | codealike.json 332 | -------------------------------------------------------------------------------- /.template.config /template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/template", 3 | "author": "Steve Smith @ardalis", 4 | "classifications": [ 5 | "Web","ASP.NET","Clean Architecture", "Worker Service" 6 | ], 7 | "tags": { 8 | "language": "C#", 9 | "type":"project" 10 | }, 11 | "identity": "Ardalis.CleanArchitecture.WorkerService.Template", 12 | "name": "ASP.NET Core Clean Architecture Solution for Worker Service", 13 | "shortName": "clean-arch-worker-svc", 14 | "sourceName": "CleanArchitecture.WorkerService.sln", 15 | "preferNameDirectory": true 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "command": "dotnet", 9 | "type": "shell", 10 | "args": [ 11 | "build", 12 | // Ask dotnet build to generate full paths for file names. 13 | "/property:GenerateFullPaths=true", 14 | // Do not generate summary otherwise it leads to duplicate errors in Problems panel 15 | "/consoleloggerparameters:NoSummary" 16 | ], 17 | "group": { 18 | "kind":"build", 19 | "isDefault": true 20 | }, 21 | "presentation": { 22 | "reveal": "silent" 23 | }, 24 | "problemMatcher": "$msCompile" 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /CleanArchitecture.WorkerService.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Ardalis.CleanArchitecture.WorkerService.Template 5 | ASP.NET Core Clean Architecture Solution for Worker Service 6 | 9.1.2 7 | Steve Smith 8 | 9 | The Clean Architecture Solution Template for Worker service popularized by Steve @ardalis Smith. 10 | 11 | en-US 12 | MIT 13 | https://github.com/ardalis/CleanArchitecture.WorkerService 14 | 15 | Fixes MimeKit disonnect issue. 16 | 17 | 18 | 19 | 20 | Web ASP.NET "Clean Architecture" ddd domain-driven-design clean-architecture clean architecture ardalis SOLID 21 | ./content/icon.png 22 | README.md 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /CleanArchitecture.WorkerService.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.4.33122.133 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.Core", "src\CleanArchitecture.Core\CleanArchitecture.Core.csproj", "{142D1A23-9B0F-45F4-A07C-DDAB86582766}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{103BC25E-93A0-485E-8C74-77D2CB4F0AA1}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{2B6A6F0C-5CF1-4A47-84EF-05665CDCE0ED}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.UnitTests", "tests\CleanArchitecture.UnitTests\CleanArchitecture.UnitTests.csproj", "{8B71D2C0-882B-499C-8537-2885EF26A058}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.Infrastructure", "src\CleanArchitecture.Infrastructure\CleanArchitecture.Infrastructure.csproj", "{57D03E58-4CF6-48C9-AE51-45122A6DE029}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CleanArchitecture.Worker", "src\CleanArchitecture.Worker\CleanArchitecture.Worker.csproj", "{2163398E-7732-4F63-B490-6DFEF8A5E619}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sln", "sln", "{2DD960C4-7461-4547-9983-3E3034D2B0C6}" 19 | ProjectSection(SolutionItems) = preProject 20 | .editorconfig = .editorconfig 21 | Directory.Build.props = Directory.Build.props 22 | Directory.Packages.props = Directory.Packages.props 23 | ef-migrations.txt = ef-migrations.txt 24 | global.json = global.json 25 | README.md = README.md 26 | EndProjectSection 27 | EndProject 28 | Global 29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 30 | Debug|Any CPU = Debug|Any CPU 31 | Release|Any CPU = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 34 | {142D1A23-9B0F-45F4-A07C-DDAB86582766}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {142D1A23-9B0F-45F4-A07C-DDAB86582766}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {142D1A23-9B0F-45F4-A07C-DDAB86582766}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {142D1A23-9B0F-45F4-A07C-DDAB86582766}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {8B71D2C0-882B-499C-8537-2885EF26A058}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {8B71D2C0-882B-499C-8537-2885EF26A058}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {8B71D2C0-882B-499C-8537-2885EF26A058}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {8B71D2C0-882B-499C-8537-2885EF26A058}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {57D03E58-4CF6-48C9-AE51-45122A6DE029}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {57D03E58-4CF6-48C9-AE51-45122A6DE029}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {57D03E58-4CF6-48C9-AE51-45122A6DE029}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {57D03E58-4CF6-48C9-AE51-45122A6DE029}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {2163398E-7732-4F63-B490-6DFEF8A5E619}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {2163398E-7732-4F63-B490-6DFEF8A5E619}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {2163398E-7732-4F63-B490-6DFEF8A5E619}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {2163398E-7732-4F63-B490-6DFEF8A5E619}.Release|Any CPU.Build.0 = Release|Any CPU 50 | EndGlobalSection 51 | GlobalSection(SolutionProperties) = preSolution 52 | HideSolutionNode = FALSE 53 | EndGlobalSection 54 | GlobalSection(NestedProjects) = preSolution 55 | {142D1A23-9B0F-45F4-A07C-DDAB86582766} = {103BC25E-93A0-485E-8C74-77D2CB4F0AA1} 56 | {8B71D2C0-882B-499C-8537-2885EF26A058} = {2B6A6F0C-5CF1-4A47-84EF-05665CDCE0ED} 57 | {57D03E58-4CF6-48C9-AE51-45122A6DE029} = {103BC25E-93A0-485E-8C74-77D2CB4F0AA1} 58 | {2163398E-7732-4F63-B490-6DFEF8A5E619} = {103BC25E-93A0-485E-8C74-77D2CB4F0AA1} 59 | EndGlobalSection 60 | GlobalSection(ExtensibilityGlobals) = postSolution 61 | SolutionGuid = {35EF64A7-2C17-4EB0-B9FB-D9C7B21AEDC0} 62 | EndGlobalSection 63 | EndGlobal 64 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | -------------------------------------------------------------------------------- /Directory.Packages.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Steve Smith 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CleanArchitecture.WorkerService 2 | 3 | A solution template using Clean Architecture for building a .NET 7.0 Worker Service. 4 | 5 | ## Give a Star! :star: 6 | 7 | If you like or are using this project to learn or start your solution, please give it a star. Thanks! 8 | 9 | ## Credits 10 | 11 | Big thanks to [all of the great contributors to this project](https://github.com/ardalis/CleanArchitecture.WorkerService/graphs/contributors)! 12 | 13 | ## Build Status 14 | 15 | [![.NET Core](https://github.com/ardalis/CleanArchitecture.WorkerService/actions/workflows/dotnetcore.yml/badge.svg)](https://github.com/ardalis/CleanArchitecture.WorkerService/actions/workflows/dotnetcore.yml) 16 | 17 | ## Getting Started 18 | 19 | Clone or download the repository. 20 | 21 | Install the ef core cli tools `dotnet tool install --global dotnet-ef`. If you already have an old version, first try `dotnet tool update --global dotnet-ef --version 6.0.0-*`, if that doesn't work, see [Updating Ef Core Cli](https://github.com/aspnet/EntityFrameworkCore/issues/14016#issuecomment-487308603) First, delete C:\Users\{yourUser}\.dotnet\tools\.store\dotnet-ef tool. 22 | 23 | This app is currently configured to run against a localdb SQL Server instance. To initialized the database, you will need to run this command in the /src/CleanArchitecture.Worker folder: 24 | 25 | ```powershell 26 | dotnet ef database update -c appdbcontext -p ../CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj -s CleanArchitecture.Worker.csproj 27 | ``` 28 | 29 | Check the connection string in `appsettings.json` in the CleanArchitecture.Worker project to verify its details if you have problems. 30 | 31 | Open the solution in Visual Studio and run it with ctrl-F5 (the CleanArchitecture.Worker project should be the startup project) or in the console go to the `src/CleanArchitecture.Worker` folder and run `dotnet run`. 32 | 33 | On startup the app queues up 10 URLs to hit (google.com) and you should see it make 10 requests and save them to the database and then do nothing, logging each second. 34 | 35 | ## Using this for your own worker service 36 | 37 | To use this as a template for your own worker server projects, make the following changes: 38 | 39 | - Rename CleanArchitecture to YourAppName or YourCompany.YourAppName 40 | - Configure the connection string to your database if you're using one 41 | - Replace InMemory queue implementations with Azure, AWS, Rabbit, etc. actual queues you're using 42 | - Remove UrlStatusHistory and related services and interfaces 43 | 44 | ## References 45 | 46 | - [Clean Architecture template for ASP.NET Core solutions](https://github.com/ardalis/CleanArchitecture) 47 | - [Creating a Clean Architecture Worker Service Template](https://www.youtube.com/watch?v=_jfnnAMNb94) ([Twitch](https://twitch.tv/ardalis) Stream 1) 48 | - [Creating a Clean Architecture Worker Service Template](https://www.youtube.com/watch?v=Nttt33GoTXg) ([Twitch](https://twitch.tv/ardalis) Stream 2) 49 | 50 | Useful Pluralsight courses: 51 | - [SOLID Principles of Object Oriented Design](https://www.pluralsight.com/courses/principles-oo-design) 52 | - [SOLID Principles for C# Developers](https://www.pluralsight.com/courses/csharp-solid-principles) 53 | - [Domain-Driven Design Fundamentals](https://www.pluralsight.com/courses/domain-driven-design-fundamentals) 54 | -------------------------------------------------------------------------------- /ef-migrations.txt: -------------------------------------------------------------------------------- 1 | -- run from CleanArchitecture.Worker project folder 2 | dotnet ef migrations add InitialModel --context appdbcontext -p ../CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj -s CleanArchitecture.Worker.csproj -o Data/Migrations 3 | 4 | dotnet ef database update -c appdbcontext -p ../CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj -s CleanArchitecture.Worker.csproj 5 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "7.0.100", 4 | "rollForward": "latestMajor" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Core/CleanArchitecture.Core.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Core/Entities/BaseEntity.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Core.Entities; 2 | 3 | public abstract class BaseEntity 4 | { 5 | public int Id { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Core/Entities/UrlStatusHistory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanArchitecture.Core.Entities; 4 | 5 | /// 6 | /// Tracks the status attempts to periodically GET a URL 7 | /// 8 | public class UrlStatusHistory : BaseEntity 9 | { 10 | public string Uri { get; set; } 11 | public DateTime RequestDateUtc { get; } = DateTime.UtcNow; 12 | public int StatusCode { get; set; } 13 | public string RequestId { get; set; } 14 | 15 | public override string ToString() 16 | { 17 | return $"Fetched {Uri} at {RequestDateUtc.ToLocalTime()} with status code {StatusCode}."; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Core/Interfaces/IEntryPointService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace CleanArchitecture.Core.Interfaces; 4 | 5 | public interface IEntryPointService 6 | { 7 | Task ExecuteAsync(); 8 | } 9 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Core/Interfaces/IHttpService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace CleanArchitecture.Core.Interfaces; 4 | 5 | public interface IHttpService 6 | { 7 | Task GetUrlResponseStatusCodeAsync(string url); 8 | } 9 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Core/Interfaces/ILoggerAdapter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CleanArchitecture.Core.Interfaces; 4 | 5 | // Helps if you need to confirm logging is happening 6 | // https://ardalis.com/testing-logging-in-aspnet-core 7 | public interface ILoggerAdapter 8 | { 9 | void LogInformation(string message, params object[] args); 10 | void LogError(Exception ex, string message, params object[] args); 11 | } 12 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Core/Interfaces/IQueueReceiver.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace CleanArchitecture.Core.Interfaces; 4 | 5 | public interface IQueueReceiver 6 | { 7 | Task GetMessageFromQueue(string queueName); 8 | } 9 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Core/Interfaces/IQueueSender.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace CleanArchitecture.Core.Interfaces; 4 | 5 | public interface IQueueSender 6 | { 7 | Task SendMessageToQueue(string message, string queueName); 8 | } 9 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Core/Interfaces/IRepository.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Core.Entities; 2 | using System.Collections.Generic; 3 | 4 | namespace CleanArchitecture.Core.Interfaces; 5 | 6 | public interface IRepository 7 | { 8 | T GetById(int id) where T : BaseEntity; 9 | List List() where T : BaseEntity; 10 | T Add(T entity) where T : BaseEntity; 11 | void Update(T entity) where T : BaseEntity; 12 | void Delete(T entity) where T : BaseEntity; 13 | } 14 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Core/Interfaces/IUrlStatusChecker.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Core.Entities; 2 | using System.Threading.Tasks; 3 | 4 | namespace CleanArchitecture.Core.Interfaces; 5 | 6 | public interface IUrlStatusChecker 7 | { 8 | Task CheckUrlAsync(string url, string requestId); 9 | } 10 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Core/Services/EntryPointService.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Core.Interfaces; 2 | using CleanArchitecture.Core.Settings; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace CleanArchitecture.Core.Services; 8 | 9 | /// 10 | /// An example service that performs business logic 11 | /// 12 | public class EntryPointService(ILoggerAdapter _logger, 13 | EntryPointSettings _settings, 14 | IQueueReceiver _queueReceiver, 15 | IServiceLocator _serviceScopeFactoryLocator, 16 | IUrlStatusChecker _urlStatusChecker) : IEntryPointService 17 | { 18 | public async Task ExecuteAsync() 19 | { 20 | _logger.LogInformation("{service} running at: {time}", nameof(EntryPointService), DateTimeOffset.Now); 21 | try 22 | { 23 | // EF Requires a scope so we are creating one per execution here 24 | using var scope = _serviceScopeFactoryLocator.CreateScope(); 25 | var repository = 26 | scope.ServiceProvider 27 | .GetService(); 28 | 29 | // read from the queue 30 | string message = await _queueReceiver.GetMessageFromQueue(_settings.ReceivingQueueName); 31 | if (string.IsNullOrEmpty(message)) return; 32 | 33 | // check 1 URL in the message 34 | var statusHistory = await _urlStatusChecker.CheckUrlAsync(message, ""); 35 | 36 | // record HTTP status / response time / maybe existence of keyword in database 37 | repository.Add(statusHistory); 38 | 39 | _logger.LogInformation(statusHistory.ToString()); 40 | } 41 | #pragma warning disable CA1031 // Do not catch general exception types 42 | catch (Exception ex) 43 | { 44 | _logger.LogError(ex, $"{nameof(EntryPointService)}.{nameof(ExecuteAsync)} threw an exception."); 45 | // TODO: Decide if you want to re-throw which will crash the worker service 46 | //throw; 47 | } 48 | #pragma warning restore CA1031 // Do not catch general exception types 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Core/Services/IServiceLocator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | 4 | namespace CleanArchitecture.Core.Services; 5 | 6 | public interface IServiceLocator : IDisposable 7 | { 8 | IServiceScope CreateScope(); 9 | T Get(); 10 | } 11 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Core/Services/ServiceScopeFactoryLocator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace CleanArchitecture.Core.Services; 4 | 5 | /// 6 | /// A wrapper around ServiceScopeFactory to make it easier to fake out with MOQ. 7 | /// 8 | /// 9 | public sealed class ServiceScopeFactoryLocator(IServiceScopeFactory _factory) : IServiceLocator 10 | { 11 | private IServiceScope _scope; 12 | 13 | public T Get() 14 | { 15 | CreateScope(); 16 | 17 | return _scope.ServiceProvider.GetService(); 18 | } 19 | 20 | public IServiceScope CreateScope() 21 | { 22 | // if (_scope == null) comment this out to avoid {"Cannot access a disposed object.\r\nObject name: 'IServiceProvider'."} 23 | _scope = _factory.CreateScope(); 24 | return _scope; 25 | } 26 | 27 | public void Dispose() 28 | { 29 | _scope?.Dispose(); 30 | _scope = null; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Core/Services/UrlStatusChecker.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | using CleanArchitecture.Core.Entities; 3 | using CleanArchitecture.Core.Interfaces; 4 | using System.Threading.Tasks; 5 | 6 | namespace CleanArchitecture.Core.Services; 7 | 8 | /// 9 | /// A simple service that fetches a URL and returns a UrlStatusHistory instance with the result 10 | /// 11 | public class UrlStatusChecker(IHttpService _httpService) : IUrlStatusChecker 12 | { 13 | public async Task CheckUrlAsync(string url, string requestId) 14 | { 15 | Guard.Against.NullOrWhiteSpace(url, nameof(url)); 16 | 17 | var statusCode = await _httpService.GetUrlResponseStatusCodeAsync(url); 18 | 19 | return new UrlStatusHistory 20 | { 21 | RequestId = requestId, 22 | StatusCode = statusCode, 23 | Uri = url 24 | }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Core/Settings/EntryPointSettings.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Core.Settings; 2 | 3 | /// 4 | /// An example settings class used to configure a service 5 | /// 6 | public class EntryPointSettings 7 | { 8 | public string ReceivingQueueName { get; set; } 9 | public string SendingQueueName { get; set; } 10 | } 11 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/CleanArchitecture.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Data/AppDbContext.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Core.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using System.Reflection; 4 | 5 | namespace CleanArchitecture.Infrastructure.Data; 6 | 7 | public class AppDbContext : DbContext 8 | { 9 | public AppDbContext(DbContextOptions options) : base(options) 10 | { 11 | } 12 | 13 | public DbSet UrlStatusHistories { get; set; } 14 | 15 | protected override void OnModelCreating(ModelBuilder modelBuilder) 16 | { 17 | base.OnModelCreating(modelBuilder); 18 | modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Data/Config/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Infrastructure.Data.Config; 2 | 3 | public static class Constants 4 | { 5 | public const int DEFAULT_URI_LENGTH = 1024; 6 | } 7 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Data/Config/UrlStatusHistoryConfiguration.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Core.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace CleanArchitecture.Infrastructure.Data.Config; 6 | 7 | public class UrlStatusHistoryConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.Property(ush => ush.RequestDateUtc) 12 | .IsRequired(); 13 | builder.Property(ush => ush.Id) 14 | .IsRequired(); 15 | builder.Property(ush => ush.StatusCode) 16 | .IsRequired(); 17 | builder.Property(ush => ush.Uri) 18 | .HasMaxLength(Constants.DEFAULT_URI_LENGTH) 19 | .IsRequired(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Data/EfRepository.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Core.Entities; 2 | using CleanArchitecture.Core.Interfaces; 3 | using Microsoft.EntityFrameworkCore; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace CleanArchitecture.Infrastructure.Data; 8 | 9 | /// 10 | /// A simple repository implementation for EF Core 11 | /// If you don't want changes to be saved immediately, add a SaveChanges method to the interface 12 | /// and remove the calls to _dbContext.SaveChanges from the Add/Update/Delete methods 13 | /// 14 | public class EfRepository : IRepository 15 | { 16 | private readonly AppDbContext _dbContext; 17 | 18 | public EfRepository(AppDbContext dbContext) 19 | { 20 | _dbContext = dbContext; 21 | } 22 | 23 | public T GetById(int id) where T : BaseEntity 24 | { 25 | return _dbContext.Set().SingleOrDefault(e => e.Id == id); 26 | } 27 | 28 | public List List() where T : BaseEntity 29 | { 30 | return _dbContext.Set().ToList(); 31 | } 32 | 33 | public T Add(T entity) where T : BaseEntity 34 | { 35 | _dbContext.Set().Add(entity); 36 | _dbContext.SaveChanges(); 37 | 38 | return entity; 39 | } 40 | 41 | public void Delete(T entity) where T : BaseEntity 42 | { 43 | _dbContext.Set().Remove(entity); 44 | _dbContext.SaveChanges(); 45 | } 46 | 47 | public void Update(T entity) where T : BaseEntity 48 | { 49 | _dbContext.Entry(entity).State = EntityState.Modified; 50 | _dbContext.SaveChanges(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Data/Migrations/20191101171448_InitialModel.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using CleanArchitecture.Infrastructure.Data; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 9 | 10 | namespace CleanArchitecture.Infrastructure.Data.Migrations 11 | { 12 | [DbContext(typeof(AppDbContext))] 13 | [Migration("20191101171448_InitialModel")] 14 | partial class InitialModel 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder 20 | .HasAnnotation("ProductVersion", "3.0.0") 21 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 22 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 23 | 24 | modelBuilder.Entity("CleanArchitecture.Core.Entities.UrlStatusHistory", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd() 28 | .HasColumnType("int") 29 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 30 | 31 | b.Property("RequestDateUtc") 32 | .HasColumnType("datetime2"); 33 | 34 | b.Property("RequestId") 35 | .HasColumnType("nvarchar(max)"); 36 | 37 | b.Property("StatusCode") 38 | .HasColumnType("int"); 39 | 40 | b.Property("Uri") 41 | .IsRequired() 42 | .HasColumnType("nvarchar(1024)") 43 | .HasMaxLength(1024); 44 | 45 | b.HasKey("Id"); 46 | 47 | b.ToTable("UrlStatusHistories"); 48 | }); 49 | #pragma warning restore 612, 618 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Data/Migrations/20191101171448_InitialModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Migrations; 3 | 4 | namespace CleanArchitecture.Infrastructure.Data.Migrations 5 | { 6 | public partial class InitialModel : Migration 7 | { 8 | protected override void Up(MigrationBuilder migrationBuilder) 9 | { 10 | migrationBuilder.CreateTable( 11 | name: "UrlStatusHistories", 12 | columns: table => new 13 | { 14 | Id = table.Column(nullable: false) 15 | .Annotation("SqlServer:Identity", "1, 1"), 16 | Uri = table.Column(maxLength: 1024, nullable: false), 17 | RequestDateUtc = table.Column(nullable: false), 18 | StatusCode = table.Column(nullable: false), 19 | RequestId = table.Column(nullable: true) 20 | }, 21 | constraints: table => 22 | { 23 | table.PrimaryKey("PK_UrlStatusHistories", x => x.Id); 24 | }); 25 | } 26 | 27 | protected override void Down(MigrationBuilder migrationBuilder) 28 | { 29 | migrationBuilder.DropTable( 30 | name: "UrlStatusHistories"); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Data/Migrations/AppDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using CleanArchitecture.Infrastructure.Data; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 8 | 9 | namespace CleanArchitecture.Infrastructure.Data.Migrations 10 | { 11 | [DbContext(typeof(AppDbContext))] 12 | partial class AppDbContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "3.0.0") 19 | .HasAnnotation("Relational:MaxIdentifierLength", 128) 20 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 21 | 22 | modelBuilder.Entity("CleanArchitecture.Core.Entities.UrlStatusHistory", b => 23 | { 24 | b.Property("Id") 25 | .ValueGeneratedOnAdd() 26 | .HasColumnType("int") 27 | .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); 28 | 29 | b.Property("RequestDateUtc") 30 | .HasColumnType("datetime2"); 31 | 32 | b.Property("RequestId") 33 | .HasColumnType("nvarchar(max)"); 34 | 35 | b.Property("StatusCode") 36 | .HasColumnType("int"); 37 | 38 | b.Property("Uri") 39 | .IsRequired() 40 | .HasColumnType("nvarchar(1024)") 41 | .HasMaxLength(1024); 42 | 43 | b.HasKey("Id"); 44 | 45 | b.ToTable("UrlStatusHistories"); 46 | }); 47 | #pragma warning restore 612, 618 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Http/HttpService.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Core.Interfaces; 2 | using System.Net.Http; 3 | using System.Threading.Tasks; 4 | 5 | namespace CleanArchitecture.Infrastructure.Http; 6 | 7 | /// 8 | /// An implementation of IHttpService using HttpClient 9 | /// 10 | public class HttpService : IHttpService 11 | { 12 | public async Task GetUrlResponseStatusCodeAsync(string url) 13 | { 14 | using var client = new HttpClient(); 15 | var result = await client.GetAsync(url); 16 | 17 | return (int)result.StatusCode; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/LoggerAdapter.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Core.Interfaces; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | 5 | namespace CleanArchitecture.Infrastructure; 6 | 7 | /// 8 | /// An ILoggerAdapter implementation that uses Microsoft.Extensions.Logging 9 | /// 10 | /// 11 | public class LoggerAdapter(ILogger _logger) : ILoggerAdapter 12 | { 13 | public void LogError(Exception ex, string message, params object[] args) 14 | { 15 | _logger.LogError(ex, message, args); 16 | } 17 | 18 | public void LogInformation(string message, params object[] args) 19 | { 20 | _logger.LogInformation(message, args); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Messaging/InMemoryQueueReceiver.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.GuardClauses; 2 | using CleanArchitecture.Core.Interfaces; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace CleanArchitecture.Infrastructure.Messaging; 7 | 8 | /// 9 | /// A simple implementation using the built-in Queue type and a single static instance. 10 | /// 11 | public class InMemoryQueueReceiver : IQueueReceiver 12 | { 13 | public static Queue MessageQueue = new Queue(); 14 | 15 | public async Task GetMessageFromQueue(string queueName) 16 | { 17 | await Task.CompletedTask; // just so async is allowed 18 | Guard.Against.NullOrWhiteSpace(queueName, nameof(queueName)); 19 | if (MessageQueue.Count == 0) return null; 20 | return MessageQueue.Dequeue(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/Messaging/InMemoryQueueSender.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Core.Interfaces; 2 | using System.Threading.Tasks; 3 | 4 | namespace CleanArchitecture.Infrastructure.Messaging; 5 | 6 | /// 7 | /// A simple implementation using the built-in Queue type 8 | /// 9 | public class InMemoryQueueSender : IQueueSender 10 | { 11 | public async Task SendMessageToQueue(string message, string queueName) 12 | { 13 | await Task.CompletedTask; // just so async is allowed 14 | InMemoryQueueReceiver.MessageQueue.Enqueue(message); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Infrastructure/ServiceCollectionSetupExtensions.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Core.Interfaces; 2 | using CleanArchitecture.Core.Services; 3 | using CleanArchitecture.Infrastructure.Data; 4 | using CleanArchitecture.Infrastructure.Http; 5 | using CleanArchitecture.Infrastructure.Messaging; 6 | using Microsoft.EntityFrameworkCore; 7 | using Microsoft.Extensions.Configuration; 8 | using Microsoft.Extensions.DependencyInjection; 9 | 10 | namespace CleanArchitecture.Infrastructure; 11 | 12 | public static class ServiceCollectionSetupExtensions 13 | { 14 | public static void AddDbContext(this IServiceCollection services, IConfiguration configuration) => 15 | services.AddDbContext(options => 16 | options.UseSqlServer( 17 | configuration.GetConnectionString("DefaultConnection"))); 18 | 19 | public static void AddRepositories(this IServiceCollection services) => 20 | services.AddScoped(); 21 | 22 | public static void AddMessageQueues(this IServiceCollection services) 23 | { 24 | services.AddSingleton(); 25 | services.AddSingleton(); 26 | } 27 | 28 | public static void AddUrlCheckingServices(this IServiceCollection services) 29 | { 30 | services.AddTransient(); 31 | services.AddTransient(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Worker/CleanArchitecture.Worker.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | dotnet-CleanArchitecture.Worker-AF0DD401-BB78-4FCC-8186-3E275E001A54 5 | Linux 6 | ..\.. 7 | True 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Worker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base 2 | WORKDIR /app 3 | 4 | FROM mcr.microsoft.com/dotnet/runtime:8.0 AS build 5 | WORKDIR /src 6 | COPY ["src/CleanArchitecture.Worker/CleanArchitecture.Worker.csproj", "src/CleanArchitecture.Worker/"] 7 | RUN dotnet restore "src/CleanArchitecture.Worker/CleanArchitecture.Worker.csproj" 8 | COPY . . 9 | WORKDIR "/src/src/CleanArchitecture.Worker" 10 | RUN dotnet build "CleanArchitecture.Worker.csproj" -c Release -o /app/build 11 | 12 | FROM build AS publish 13 | RUN dotnet publish "CleanArchitecture.Worker.csproj" -c Release -o /app/publish 14 | 15 | FROM base AS final 16 | WORKDIR /app 17 | COPY --from=publish /app/publish . 18 | ENTRYPOINT ["dotnet", "CleanArchitecture.Worker.dll"] -------------------------------------------------------------------------------- /src/CleanArchitecture.Worker/Program.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Core.Interfaces; 2 | using CleanArchitecture.Core.Services; 3 | using CleanArchitecture.Infrastructure; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using CleanArchitecture.Core.Settings; 8 | 9 | namespace CleanArchitecture.Worker; 10 | 11 | public class Program 12 | { 13 | public static void Main(string[] args) 14 | { 15 | var host = CreateHostBuilder(args).Build(); 16 | 17 | // seed some queue messages 18 | var queueSender = (IQueueSender)host.Services.GetRequiredService(typeof(IQueueSender)); 19 | for (int i = 0; i < 10; i++) 20 | { 21 | queueSender.SendMessageToQueue("https://google.com", "urlcheck"); 22 | } 23 | 24 | host.Run(); 25 | } 26 | 27 | public static IHostBuilder CreateHostBuilder(string[] args) => 28 | Host.CreateDefaultBuilder(args) 29 | .ConfigureServices((hostContext, services) => 30 | { 31 | services.AddSingleton(typeof(ILoggerAdapter<>), typeof(LoggerAdapter<>)); 32 | services.AddSingleton(); 33 | services.AddSingleton(); 34 | 35 | // Infrastructure.ContainerSetup 36 | services.AddMessageQueues(); 37 | services.AddDbContext(hostContext.Configuration); 38 | services.AddRepositories(); 39 | services.AddUrlCheckingServices(); 40 | 41 | var workerSettings = new WorkerSettings(); 42 | hostContext.Configuration.Bind(nameof(WorkerSettings), workerSettings); 43 | services.AddSingleton(workerSettings); 44 | 45 | var entryPointSettings = new EntryPointSettings(); 46 | hostContext.Configuration.Bind(nameof(EntryPointSettings), entryPointSettings); 47 | services.AddSingleton(entryPointSettings); 48 | 49 | services.AddHostedService(); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Worker/Worker.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using CleanArchitecture.Core.Interfaces; 5 | using Microsoft.Extensions.Hosting; 6 | 7 | namespace CleanArchitecture.Worker; 8 | 9 | /// 10 | /// The Worker is a BackgroundService that is executed periodically 11 | /// It should not contain any business logic but should call an entrypoint service that 12 | /// execute once per time period. 13 | /// 14 | public class Worker(ILoggerAdapter _logger, 15 | IEntryPointService _entryPointService, 16 | WorkerSettings _settings) : BackgroundService 17 | { 18 | protected override async Task ExecuteAsync(CancellationToken stoppingToken) 19 | { 20 | _logger.LogInformation("CleanArchitecture.Worker service starting at: {time}", DateTimeOffset.Now); 21 | while (!stoppingToken.IsCancellationRequested) 22 | { 23 | await _entryPointService.ExecuteAsync(); 24 | await Task.Delay(_settings.DelayMilliseconds, stoppingToken); 25 | } 26 | _logger.LogInformation("CleanArchitecture.Worker service stopping at: {time}", DateTimeOffset.Now); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Worker/WorkerSettings.cs: -------------------------------------------------------------------------------- 1 | namespace CleanArchitecture.Worker; 2 | 3 | /// 4 | /// A settings class used to configure the delay between executions. 5 | /// 6 | public class WorkerSettings 7 | { 8 | public int DelayMilliseconds { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Worker/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Debug", 5 | "System": "Information", 6 | "Microsoft": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/CleanArchitecture.Worker/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=CleanArchitectureWorkerService;Trusted_Connection=True;MultipleActiveResultSets=true" 4 | }, 5 | "Logging": { 6 | "LogLevel": { 7 | "Default": "Information", 8 | "Microsoft": "Warning", 9 | "Microsoft.Hosting.Lifetime": "Information" 10 | } 11 | }, 12 | "EntryPointSettings": { 13 | "ReceivingQueueName": "receivingQueue", 14 | "SendingQueueName": "sendingQueue" 15 | }, 16 | "WorkerSettings": { 17 | "DelayMilliseconds": "1000" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/CleanArchitecture.UnitTests/CleanArchitecture.UnitTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | all 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/CleanArchitecture.UnitTests/Core/EntryPointServiceExecuteAsync.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Core.Interfaces; 2 | using CleanArchitecture.Core.Services; 3 | using CleanArchitecture.Core.Settings; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Moq; 6 | using System; 7 | using System.Threading.Tasks; 8 | using Xunit; 9 | 10 | namespace CleanArchitecture.UnitTests; 11 | 12 | public class EntryPointServiceExecuteAsync 13 | { 14 | private static (EntryPointService, Mock>, Mock, Mock, Mock) Factory() 15 | { 16 | var logger = new Mock>(); 17 | var settings = new EntryPointSettings 18 | { 19 | ReceivingQueueName = "testQueue" 20 | }; 21 | var queueReceiver = new Mock(); 22 | var serviceLocator = new Mock(); 23 | var urlStatusChecker = new Mock(); 24 | 25 | // maybe a tuple later on 26 | var repository = SetupCreateScope(serviceLocator); 27 | 28 | var service = new EntryPointService(logger.Object, settings, queueReceiver.Object, serviceLocator.Object, urlStatusChecker.Object); 29 | return (service, logger, queueReceiver, serviceLocator, repository); 30 | } 31 | 32 | private static Mock SetupCreateScope(Mock serviceLocator) 33 | { 34 | var fakeScope = new Mock(); 35 | serviceLocator.Setup(sl => sl.CreateScope()) 36 | .Returns(fakeScope.Object); 37 | 38 | var serviceProvider = new Mock(); 39 | fakeScope.Setup(s => s.ServiceProvider) 40 | .Returns(serviceProvider.Object); 41 | 42 | return SetupCustomInjection(serviceProvider); 43 | } 44 | 45 | private static Mock SetupCustomInjection(Mock serviceProvider) 46 | { 47 | // GetRequiredService is an extension method, but GetService is not 48 | var repository = new Mock(); 49 | serviceProvider.Setup(sp => sp.GetService(typeof(IRepository))) 50 | .Returns(repository.Object); 51 | 52 | // return a tuple as you have more dependencies 53 | return repository; 54 | } 55 | 56 | [Fact] 57 | public async Task LogsExceptionsEncountered() 58 | { 59 | var (service, logger, queueReceiver, _, _) = Factory(); 60 | queueReceiver.Setup(qr => qr.GetMessageFromQueue(It.IsAny())) 61 | .ThrowsAsync(new Exception("Boom!")); 62 | 63 | await service.ExecuteAsync(); 64 | 65 | logger.Verify(l => l.LogError(It.IsAny(), It.IsAny()), Times.Once); 66 | } 67 | 68 | [Fact] 69 | public async Task MessageWasRetrievedFromTheQueue() 70 | { 71 | // example of getting inside of the CreateScope 72 | var (service, _, queueReceiver, _, _) = Factory(); 73 | 74 | await service.ExecuteAsync(); 75 | 76 | queueReceiver.Verify(qr => qr.GetMessageFromQueue("testQueue"), Times.Once); 77 | } 78 | 79 | [Fact] 80 | public async Task MessageWasRetrievedFromTheQueue_WorksManyTimes() 81 | { 82 | // simulate multiple runs, but doesn't actually make the disposed object exception happen. 83 | // avoid {"Cannot access a disposed object.\r\nObject name: 'IServiceProvider'."} 84 | var (service, _, _, _, _) = Factory(); 85 | await service.ExecuteAsync(); 86 | await service.ExecuteAsync(); 87 | await service.ExecuteAsync(); 88 | await service.ExecuteAsync(); 89 | var (service2, _, _, _, _) = Factory(); 90 | await service2.ExecuteAsync(); 91 | var (service3, _, _, _, _) = Factory(); 92 | await service3.ExecuteAsync(); 93 | 94 | Assert.True(true); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/CleanArchitecture.UnitTests/Infrastructure/EntryPointServiceExecuteAsync.cs: -------------------------------------------------------------------------------- 1 | using CleanArchitecture.Infrastructure.Messaging; 2 | using System; 3 | using System.Threading.Tasks; 4 | using Xunit; 5 | 6 | namespace CleanArchitecture.UnitTests; 7 | 8 | public class InMemoryQueueReceiverGetMessageFromQueue 9 | { 10 | private readonly InMemoryQueueReceiver _receiver = new InMemoryQueueReceiver(); 11 | [Fact] 12 | public async Task ThrowsNullExceptionGivenNullQueuename() 13 | { 14 | var ex = await Assert.ThrowsAsync(() => _receiver.GetMessageFromQueue(null)); 15 | } 16 | 17 | [Fact] 18 | public async Task ThrowsArgumentExceptionGivenEmptyQueuename() 19 | { 20 | var ex = await Assert.ThrowsAsync(() => _receiver.GetMessageFromQueue(String.Empty)); 21 | } 22 | } 23 | --------------------------------------------------------------------------------