├── .editorconfig ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── build.yml ├── .gitignore ├── .idea └── .idea.Foundatio.RabbitMQ │ └── .idea │ ├── .gitignore │ ├── encodings.xml │ ├── indexLayout.xml │ ├── misc.xml │ └── vcs.xml ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── Foundatio.RabbitMQ.sln ├── LICENSE.txt ├── NuGet.config ├── README.md ├── build ├── Dockerfile ├── Foundatio.snk ├── common.props ├── foundatio-icon.png └── rabbitmq_delayed_message_exchange-4.0.2.ez ├── docker-compose.yml ├── global.json ├── samples ├── Directory.Build.props ├── Foundatio.RabbitMQ.Publish │ ├── Foundatio.RabbitMQ.Publish.csproj │ ├── MyMessage.cs │ └── Program.cs └── Foundatio.RabbitMQ.Subscribe │ ├── Foundatio.RabbitMQ.Subscribe.csproj │ └── Program.cs ├── src ├── Directory.Build.props └── Foundatio.RabbitMQ │ ├── Extensions │ └── TaskExtensions.cs │ ├── Foundatio.RabbitMQ.csproj │ └── Messaging │ ├── AcknowledgementStrategy.cs │ ├── RabbitMQMessageBus.cs │ └── RabbitMQMessageBusOptions.cs └── tests ├── Directory.Build.props └── Foundatio.RabbitMQ.Tests ├── Foundatio.RabbitMQ.Tests.csproj ├── Messaging ├── RabbitMqMessageBusDelayedExchangeTests.cs ├── RabbitMqMessageBusTestBase.cs └── RabbitMqMessageBusTests.cs └── Properties └── AssemblyInfo.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org (https://github.com/dotnet/runtime/blob/main/.editorconfig) 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Default settings: 7 | # A newline ending every file 8 | # Use 4 spaces as indentation 9 | [*] 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 4 13 | trim_trailing_whitespace = true 14 | 15 | [*.json] 16 | insert_final_newline = false 17 | 18 | # Generated code 19 | [*{_AssemblyInfo.cs,.notsupported.cs,AsmOffsets.cs}] 20 | generated_code = true 21 | 22 | # C# files 23 | [*.cs] 24 | # New line preferences 25 | csharp_new_line_before_open_brace = all 26 | csharp_new_line_before_else = true 27 | csharp_new_line_before_catch = true 28 | csharp_new_line_before_finally = true 29 | csharp_new_line_before_members_in_object_initializers = true 30 | csharp_new_line_before_members_in_anonymous_types = true 31 | csharp_new_line_between_query_expression_clauses = true 32 | 33 | # Indentation preferences 34 | csharp_indent_block_contents = true 35 | csharp_indent_braces = false 36 | csharp_indent_case_contents = true 37 | csharp_indent_case_contents_when_block = true 38 | csharp_indent_switch_labels = true 39 | csharp_indent_labels = one_less_than_current 40 | 41 | # Modifier preferences 42 | csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async:suggestion 43 | 44 | # avoid this. unless absolutely necessary 45 | dotnet_style_qualification_for_field = false:error 46 | dotnet_style_qualification_for_property = false:error 47 | dotnet_style_qualification_for_method = false:error 48 | dotnet_style_qualification_for_event = false:error 49 | 50 | # Types: use keywords instead of BCL types, and permit var only when the type is clear 51 | csharp_style_var_for_built_in_types = false:suggestion 52 | csharp_style_var_when_type_is_apparent = false:none 53 | csharp_style_var_elsewhere = true:suggestion 54 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 55 | dotnet_style_predefined_type_for_member_access = false:suggestion 56 | 57 | # name all constant fields using PascalCase 58 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 59 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 60 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 61 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 62 | dotnet_naming_symbols.constant_fields.required_modifiers = const 63 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 64 | 65 | # static fields should have s_ prefix 66 | dotnet_naming_rule.static_fields_should_have_prefix.severity = false:none 67 | dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields 68 | dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style 69 | dotnet_naming_symbols.static_fields.applicable_kinds = field 70 | dotnet_naming_symbols.static_fields.required_modifiers = static 71 | dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected 72 | dotnet_naming_style.static_prefix_style.required_prefix = s_ 73 | dotnet_naming_style.static_prefix_style.capitalization = camel_case 74 | 75 | # internal and private fields should be _camelCase 76 | dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion 77 | dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields 78 | dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style 79 | dotnet_naming_symbols.private_internal_fields.applicable_kinds = field 80 | dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal 81 | dotnet_naming_style.camel_case_underscore_style.required_prefix = _ 82 | dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case 83 | 84 | # Code style defaults 85 | csharp_using_directive_placement = outside_namespace:suggestion 86 | dotnet_sort_system_directives_first = true 87 | csharp_prefer_braces = true:silent 88 | csharp_preserve_single_line_blocks = true:none 89 | csharp_preserve_single_line_statements = false:none 90 | csharp_prefer_static_local_function = true:suggestion 91 | csharp_prefer_simple_using_statement = false:none 92 | csharp_style_prefer_switch_expression = true:suggestion 93 | dotnet_style_readonly_field = true:suggestion 94 | 95 | # Expression-level preferences 96 | dotnet_style_object_initializer = true:suggestion 97 | dotnet_style_collection_initializer = true:suggestion 98 | dotnet_style_explicit_tuple_names = true:suggestion 99 | dotnet_style_coalesce_expression = true:suggestion 100 | dotnet_style_null_propagation = true:suggestion 101 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 102 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 103 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 104 | dotnet_style_prefer_auto_properties = true:suggestion 105 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 106 | dotnet_style_prefer_conditional_expression_over_return = true:silent 107 | csharp_prefer_simple_default_expression = true:suggestion 108 | 109 | # Expression-bodied members 110 | csharp_style_expression_bodied_methods = true:silent 111 | csharp_style_expression_bodied_constructors = true:silent 112 | csharp_style_expression_bodied_operators = true:silent 113 | csharp_style_expression_bodied_properties = true:silent 114 | csharp_style_expression_bodied_indexers = true:silent 115 | csharp_style_expression_bodied_accessors = true:silent 116 | csharp_style_expression_bodied_lambdas = true:silent 117 | csharp_style_expression_bodied_local_functions = true:silent 118 | 119 | # Pattern matching 120 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 121 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 122 | csharp_style_inlined_variable_declaration = true:suggestion 123 | 124 | # Null checking preferences 125 | csharp_style_throw_expression = true:suggestion 126 | csharp_style_conditional_delegate_call = true:suggestion 127 | 128 | # Other features 129 | csharp_style_prefer_index_operator = false:none 130 | csharp_style_prefer_range_operator = false:none 131 | csharp_style_pattern_local_over_anonymous_function = false:none 132 | 133 | # Space preferences 134 | csharp_space_after_cast = false 135 | csharp_space_after_colon_in_inheritance_clause = true 136 | csharp_space_after_comma = true 137 | csharp_space_after_dot = false 138 | csharp_space_after_keywords_in_control_flow_statements = true 139 | csharp_space_after_semicolon_in_for_statement = true 140 | csharp_space_around_binary_operators = before_and_after 141 | csharp_space_around_declaration_statements = do_not_ignore 142 | csharp_space_before_colon_in_inheritance_clause = true 143 | csharp_space_before_comma = false 144 | csharp_space_before_dot = false 145 | csharp_space_before_open_square_brackets = false 146 | csharp_space_before_semicolon_in_for_statement = false 147 | csharp_space_between_empty_square_brackets = false 148 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 149 | csharp_space_between_method_call_name_and_opening_parenthesis = false 150 | csharp_space_between_method_call_parameter_list_parentheses = false 151 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 152 | csharp_space_between_method_declaration_name_and_open_parenthesis = false 153 | csharp_space_between_method_declaration_parameter_list_parentheses = false 154 | csharp_space_between_parentheses = false 155 | csharp_space_between_square_brackets = false 156 | 157 | # Custom 158 | csharp_style_namespace_declarations = file_scoped:error 159 | dotnet_diagnostic.IDE0005.severity = error # Using directive is unnecessary. 160 | 161 | # C++ Files 162 | [*.{cpp,h,in}] 163 | curly_bracket_next_line = true 164 | indent_brace_style = Allman 165 | 166 | # Xml project files 167 | [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] 168 | indent_size = 2 169 | 170 | [*.{csproj,vbproj,proj,nativeproj,locproj}] 171 | charset = utf-8 172 | 173 | # Xml build files 174 | [*.builds] 175 | indent_size = 2 176 | 177 | # Xml files 178 | [*.{xml,stylecop,resx,ruleset}] 179 | indent_size = 2 180 | 181 | # Xml config files 182 | [*.{props,targets,config,nuspec}] 183 | indent_size = 2 184 | 185 | # YAML config files 186 | [*.{yml,yaml}] 187 | indent_size = 2 188 | 189 | # Shell scripts 190 | [*.sh] 191 | end_of_line = lf 192 | [*.{cmd,bat}] 193 | end_of_line = crlf 194 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: exceptionless 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: nuget 5 | directory: "/" 6 | schedule: 7 | interval: weekly 8 | 9 | - package-ecosystem: docker 10 | directory: "/" 11 | schedule: 12 | interval: quarterly 13 | 14 | - package-ecosystem: "docker-compose" 15 | directory: "/" 16 | schedule: 17 | interval: quarterly 18 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | uses: FoundatioFx/Foundatio/.github/workflows/build-workflow.yml@main 7 | secrets: inherit 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific files 2 | *.suo 3 | *.user 4 | 5 | # Build results 6 | [Bb]in/ 7 | [Oo]bj/ 8 | artifacts 9 | .vs/ 10 | 11 | # MSTest test Results 12 | [Tt]est[Rr]esult*/ 13 | 14 | # ReSharper is a .NET coding add-in 15 | _ReSharper*/ 16 | *.[Rr]e[Ss]harper 17 | *.DotSettings.user 18 | 19 | # JustCode is a .NET coding addin-in 20 | .JustCode 21 | 22 | # DotCover is a Code Coverage Tool 23 | *.dotCover 24 | 25 | # NCrunch 26 | _NCrunch_* 27 | .*crunch*.local.xml 28 | 29 | # NuGet Packages 30 | *.nupkg 31 | 32 | .DS_Store 33 | 34 | # Rider 35 | 36 | # User specific 37 | **/.idea/**/workspace.xml 38 | **/.idea/**/tasks.xml 39 | **/.idea/shelf/* 40 | **/.idea/dictionaries 41 | 42 | # Sensitive or high-churn files 43 | **/.idea/**/dataSources/ 44 | **/.idea/**/dataSources.ids 45 | **/.idea/**/dataSources.xml 46 | **/.idea/**/dataSources.local.xml 47 | **/.idea/**/sqlDataSources.xml 48 | **/.idea/**/dynamic.xml 49 | 50 | # Rider 51 | # Rider auto-generates .iml files, and contentModel.xml 52 | **/.idea/**/*.iml 53 | **/.idea/**/contentModel.xml 54 | **/.idea/**/modules.xml -------------------------------------------------------------------------------- /.idea/.idea.Foundatio.RabbitMQ/.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Rider ignored files 5 | /.idea.Foundatio.RabbitMQ.iml 6 | /modules.xml 7 | /contentModel.xml 8 | /projectSettingsUpdater.xml 9 | # Editor-based HTTP Client requests 10 | /httpRequests/ 11 | -------------------------------------------------------------------------------- /.idea/.idea.Foundatio.RabbitMQ/.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/.idea.Foundatio.RabbitMQ/.idea/indexLayout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/.idea.Foundatio.RabbitMQ/.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/.idea.Foundatio.RabbitMQ/.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Publish", 6 | "type": "coreclr", 7 | "request": "launch", 8 | "program": "${workspaceFolder}/samples/Foundatio.RabbitMQ.Publish/bin/Debug/net8.0/Foundatio.RabbitMQ.Publish.dll", 9 | "args": [], 10 | "cwd": "${workspaceFolder}/samples/Foundatio.RabbitMQ.Publish", 11 | "requireExactSource": false, 12 | "console": "integratedTerminal", 13 | "stopAtEntry": false, 14 | "internalConsoleOptions": "openOnSessionStart" 15 | }, 16 | { 17 | "name": "Subscribe", 18 | "type": "coreclr", 19 | "request": "launch", 20 | "program": "${workspaceFolder}/samples/Foundatio.RabbitMQ.Subscribe/bin/Debug/net8.0/Foundatio.RabbitMQ.Subscribe.dll", 21 | "args": [], 22 | "cwd": "${workspaceFolder}/samples/Foundatio.RabbitMQ.Subscribe", 23 | "requireExactSource": false, 24 | "console": "integratedTerminal", 25 | "stopAtEntry": false, 26 | "internalConsoleOptions": "openOnSessionStart" 27 | }, 28 | { 29 | "name": ".NET Core Attach", 30 | "type": "coreclr", 31 | "request": "attach", 32 | "processId": "${command:pickProcess}" 33 | } 34 | ], 35 | "compounds": [ 36 | { 37 | "name": "Pub/Sub Sample", 38 | "preLaunchTask": "build", 39 | "configurations": [ 40 | "Subscribe", 41 | "Publish" 42 | ] 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Muxer", 4 | "Xunit" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "group": { 9 | "kind": "build", 10 | "isDefault": true 11 | }, 12 | "args": [ 13 | "build", 14 | "${workspaceFolder}/Foundatio.RabbitMQ.sln", 15 | "/p:GenerateFullPaths=true" 16 | ], 17 | "problemMatcher": "$msCompile" 18 | }, 19 | { 20 | "label": "test", 21 | "command": "dotnet", 22 | "type": "process", 23 | "group": { 24 | "kind": "test", 25 | "isDefault": true 26 | }, 27 | "args": [ 28 | "test", 29 | "${workspaceFolder}/tests/Foundatio.RabbitMQ.Tests/Foundatio.RabbitMQ.Tests.csproj", 30 | "/p:GenerateFullPaths=true" 31 | ], 32 | "problemMatcher": "$msCompile" 33 | }, 34 | { 35 | "label": "pack", 36 | "command": "dotnet pack -c Release -o ${workspaceFolder}/artifacts", 37 | "type": "shell", 38 | "problemMatcher": [] 39 | }, 40 | { 41 | "label": "docker: rabbitmq", 42 | "command": "docker compose up", 43 | "type": "shell", 44 | "isBackground": true, 45 | "group": "test", 46 | "problemMatcher": [] 47 | } 48 | ] 49 | } -------------------------------------------------------------------------------- /Foundatio.RabbitMQ.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26923.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{70515E66-DAF8-4D18-8F8F-8A2934171AA9}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4E12E4C1-2EC6-4EC7-8956-AF2721DA4ECE}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | build\common.props = build\common.props 12 | README.md = README.md 13 | docker-compose.yml = docker-compose.yml 14 | tests\Directory.Build.props = tests\Directory.Build.props 15 | build\Dockerfile = build\Dockerfile 16 | EndProjectSection 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Foundatio.RabbitMQ", "src\Foundatio.RabbitMQ\Foundatio.RabbitMQ.csproj", "{EAE3607D-73A1-4D02-BDAA-24A37DDA15CB}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Foundatio.RabbitMQ.Tests", "tests\Foundatio.RabbitMQ.Tests\Foundatio.RabbitMQ.Tests.csproj", "{9832E1F4-C826-4704-890A-00F330BC5913}" 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{AA4CFEA3-A476-4B1C-91C4-C219C6DFC2C2}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Foundatio.RabbitMQ.Publish", "samples\Foundatio.RabbitMQ.Publish\Foundatio.RabbitMQ.Publish.csproj", "{9C984561-184A-4708-82C0-A9ADA2D13E02}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Foundatio.RabbitMQ.Subscribe", "samples\Foundatio.RabbitMQ.Subscribe\Foundatio.RabbitMQ.Subscribe.csproj", "{D2C982EB-E879-4F5C-BD9E-17445EBA1E26}" 27 | EndProject 28 | Global 29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 30 | Debug|Any CPU = Debug|Any CPU 31 | Debug|x64 = Debug|x64 32 | Debug|x86 = Debug|x86 33 | Release|Any CPU = Release|Any CPU 34 | Release|x64 = Release|x64 35 | Release|x86 = Release|x86 36 | EndGlobalSection 37 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 38 | {EAE3607D-73A1-4D02-BDAA-24A37DDA15CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {EAE3607D-73A1-4D02-BDAA-24A37DDA15CB}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {EAE3607D-73A1-4D02-BDAA-24A37DDA15CB}.Debug|x64.ActiveCfg = Debug|Any CPU 41 | {EAE3607D-73A1-4D02-BDAA-24A37DDA15CB}.Debug|x64.Build.0 = Debug|Any CPU 42 | {EAE3607D-73A1-4D02-BDAA-24A37DDA15CB}.Debug|x86.ActiveCfg = Debug|Any CPU 43 | {EAE3607D-73A1-4D02-BDAA-24A37DDA15CB}.Debug|x86.Build.0 = Debug|Any CPU 44 | {EAE3607D-73A1-4D02-BDAA-24A37DDA15CB}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {EAE3607D-73A1-4D02-BDAA-24A37DDA15CB}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {EAE3607D-73A1-4D02-BDAA-24A37DDA15CB}.Release|x64.ActiveCfg = Release|Any CPU 47 | {EAE3607D-73A1-4D02-BDAA-24A37DDA15CB}.Release|x64.Build.0 = Release|Any CPU 48 | {EAE3607D-73A1-4D02-BDAA-24A37DDA15CB}.Release|x86.ActiveCfg = Release|Any CPU 49 | {EAE3607D-73A1-4D02-BDAA-24A37DDA15CB}.Release|x86.Build.0 = Release|Any CPU 50 | {9832E1F4-C826-4704-890A-00F330BC5913}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {9832E1F4-C826-4704-890A-00F330BC5913}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {9832E1F4-C826-4704-890A-00F330BC5913}.Debug|x64.ActiveCfg = Debug|Any CPU 53 | {9832E1F4-C826-4704-890A-00F330BC5913}.Debug|x64.Build.0 = Debug|Any CPU 54 | {9832E1F4-C826-4704-890A-00F330BC5913}.Debug|x86.ActiveCfg = Debug|Any CPU 55 | {9832E1F4-C826-4704-890A-00F330BC5913}.Debug|x86.Build.0 = Debug|Any CPU 56 | {9832E1F4-C826-4704-890A-00F330BC5913}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {9832E1F4-C826-4704-890A-00F330BC5913}.Release|Any CPU.Build.0 = Release|Any CPU 58 | {9832E1F4-C826-4704-890A-00F330BC5913}.Release|x64.ActiveCfg = Release|Any CPU 59 | {9832E1F4-C826-4704-890A-00F330BC5913}.Release|x64.Build.0 = Release|Any CPU 60 | {9832E1F4-C826-4704-890A-00F330BC5913}.Release|x86.ActiveCfg = Release|Any CPU 61 | {9832E1F4-C826-4704-890A-00F330BC5913}.Release|x86.Build.0 = Release|Any CPU 62 | {9C984561-184A-4708-82C0-A9ADA2D13E02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 63 | {9C984561-184A-4708-82C0-A9ADA2D13E02}.Debug|Any CPU.Build.0 = Debug|Any CPU 64 | {9C984561-184A-4708-82C0-A9ADA2D13E02}.Debug|x64.ActiveCfg = Debug|Any CPU 65 | {9C984561-184A-4708-82C0-A9ADA2D13E02}.Debug|x64.Build.0 = Debug|Any CPU 66 | {9C984561-184A-4708-82C0-A9ADA2D13E02}.Debug|x86.ActiveCfg = Debug|Any CPU 67 | {9C984561-184A-4708-82C0-A9ADA2D13E02}.Debug|x86.Build.0 = Debug|Any CPU 68 | {9C984561-184A-4708-82C0-A9ADA2D13E02}.Release|Any CPU.ActiveCfg = Release|Any CPU 69 | {9C984561-184A-4708-82C0-A9ADA2D13E02}.Release|Any CPU.Build.0 = Release|Any CPU 70 | {9C984561-184A-4708-82C0-A9ADA2D13E02}.Release|x64.ActiveCfg = Release|Any CPU 71 | {9C984561-184A-4708-82C0-A9ADA2D13E02}.Release|x64.Build.0 = Release|Any CPU 72 | {9C984561-184A-4708-82C0-A9ADA2D13E02}.Release|x86.ActiveCfg = Release|Any CPU 73 | {9C984561-184A-4708-82C0-A9ADA2D13E02}.Release|x86.Build.0 = Release|Any CPU 74 | {D2C982EB-E879-4F5C-BD9E-17445EBA1E26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 75 | {D2C982EB-E879-4F5C-BD9E-17445EBA1E26}.Debug|Any CPU.Build.0 = Debug|Any CPU 76 | {D2C982EB-E879-4F5C-BD9E-17445EBA1E26}.Debug|x64.ActiveCfg = Debug|Any CPU 77 | {D2C982EB-E879-4F5C-BD9E-17445EBA1E26}.Debug|x64.Build.0 = Debug|Any CPU 78 | {D2C982EB-E879-4F5C-BD9E-17445EBA1E26}.Debug|x86.ActiveCfg = Debug|Any CPU 79 | {D2C982EB-E879-4F5C-BD9E-17445EBA1E26}.Debug|x86.Build.0 = Debug|Any CPU 80 | {D2C982EB-E879-4F5C-BD9E-17445EBA1E26}.Release|Any CPU.ActiveCfg = Release|Any CPU 81 | {D2C982EB-E879-4F5C-BD9E-17445EBA1E26}.Release|Any CPU.Build.0 = Release|Any CPU 82 | {D2C982EB-E879-4F5C-BD9E-17445EBA1E26}.Release|x64.ActiveCfg = Release|Any CPU 83 | {D2C982EB-E879-4F5C-BD9E-17445EBA1E26}.Release|x64.Build.0 = Release|Any CPU 84 | {D2C982EB-E879-4F5C-BD9E-17445EBA1E26}.Release|x86.ActiveCfg = Release|Any CPU 85 | {D2C982EB-E879-4F5C-BD9E-17445EBA1E26}.Release|x86.Build.0 = Release|Any CPU 86 | EndGlobalSection 87 | GlobalSection(SolutionProperties) = preSolution 88 | HideSolutionNode = FALSE 89 | EndGlobalSection 90 | GlobalSection(NestedProjects) = preSolution 91 | {9832E1F4-C826-4704-890A-00F330BC5913} = {70515E66-DAF8-4D18-8F8F-8A2934171AA9} 92 | {9C984561-184A-4708-82C0-A9ADA2D13E02} = {AA4CFEA3-A476-4B1C-91C4-C219C6DFC2C2} 93 | {D2C982EB-E879-4F5C-BD9E-17445EBA1E26} = {AA4CFEA3-A476-4B1C-91C4-C219C6DFC2C2} 94 | EndGlobalSection 95 | GlobalSection(ExtensibilityGlobals) = postSolution 96 | SolutionGuid = {2D850136-CBAA-4910-B467-4F1243DB267E} 97 | EndGlobalSection 98 | EndGlobal 99 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Foundatio](https://raw.githubusercontent.com/FoundatioFx/Foundatio/master/media/foundatio-dark-bg.svg#gh-dark-mode-only "Foundatio")![Foundatio](https://raw.githubusercontent.com/FoundatioFx/Foundatio/master/media/foundatio.svg#gh-light-mode-only "Foundatio") 2 | 3 | [![Build status](https://github.com/FoundatioFx/Foundatio.RabbitMQ/workflows/Build/badge.svg)](https://github.com/FoundatioFx/Foundatio.RabbitMQ/actions) 4 | [![NuGet Version](http://img.shields.io/nuget/v/Foundatio.RabbitMQ.svg?style=flat)](https://www.nuget.org/packages/Foundatio.RabbitMQ/) 5 | [![feedz.io](https://img.shields.io/badge/endpoint.svg?url=https%3A%2F%2Ff.feedz.io%2Ffoundatio%2Ffoundatio%2Fshield%2FFoundatio.RabbitMQ%2Flatest)](https://f.feedz.io/foundatio/foundatio/packages/Foundatio.RabbitMQ/latest/download) 6 | [![Discord](https://img.shields.io/discord/715744504891703319)](https://discord.gg/6HxgFCx) 7 | 8 | Pluggable foundation blocks for building loosely coupled distributed apps. 9 | 10 | - [Caching](#caching) 11 | - [Queues](#queues) 12 | - [Locks](#locks) 13 | - [Messaging](#messaging) 14 | - [Jobs](#jobs) 15 | - [File Storage](#file-storage) 16 | - [Metrics](#metrics) 17 | 18 | Includes implementations in Redis, Azure, AWS, RabbitMQ, Kafka and in memory (for development). 19 | 20 | ## Why Foundatio? 21 | 22 | When building several big cloud applications we found a lack of great solutions (that's not to say there isn't solutions out there) for many key pieces to building scalable distributed applications while keeping the development experience simple. Here are a few examples of why we built and use Foundatio: 23 | 24 | - Wanted to build against abstract interfaces so that we could easily change implementations. 25 | - Wanted the blocks to be dependency injection friendly. 26 | - Caching: We were initially using an open source Redis cache client but then it turned into a commercial product with high licensing costs. Not only that, but there weren't any in memory implementations so every developer was required to set up and configure Redis. 27 | - Message Bus: We initially looked at [NServiceBus](http://particular.net/nservicebus) (great product) but it had high licensing costs (they have to eat too) but was not OSS friendly. We also looked into [MassTransit](http://masstransit-project.com/) (another great product) but found Azure support lacking at the time and local set up a pain (for in memory). We wanted a simple message bus that just worked locally or in the cloud. 28 | - Storage: We couldn't find any existing project that was decoupled and supported in memory, file storage or Azure Blob Storage. 29 | 30 | To summarize, if you want pain free development and testing while allowing your app to scale, use Foundatio! 31 | 32 | ## Implementations 33 | 34 | - [Redis](https://github.com/FoundatioFx/Foundatio.Redis) - Caching, Storage, Queues, Messaging, Locks, Metrics 35 | - [Azure Storage](https://github.com/FoundatioFx/Foundatio.AzureStorage) - Storage, Queues 36 | - [Azure ServiceBus](https://github.com/FoundatioFx/Foundatio.AzureServiceBus) - Queues, Messaging 37 | - [AWS](https://github.com/FoundatioFx/Foundatio.AWS) - Storage, Queues, Metrics 38 | - [Kafka](https://github.com/FoundatioFx/Foundatio.Kafka) - Messaging 39 | - [RabbitMQ](https://github.com/FoundatioFx/Foundatio.RabbitMQ) - Messaging 40 | - [Minio](https://github.com/FoundatioFx/Foundatio.Minio) - Storage 41 | - [Aliyun](https://github.com/FoundatioFx/Foundatio.Aliyun) - Storage 42 | - [SshNet](https://github.com/FoundatioFx/Foundatio.Storage.SshNet) - Storage 43 | 44 | ## Getting Started (Development) 45 | 46 | [Foundatio can be installed](https://www.nuget.org/packages?q=Foundatio) via the [NuGet package manager](https://docs.nuget.org/consume/Package-Manager-Dialog). If you need help, please [open an issue](https://github.com/FoundatioFx/Foundatio/issues/new) or join our [Discord](https://discord.gg/6HxgFCx) chat room. We’re always here to help if you have any questions! 47 | 48 | **This section is for development purposes only! If you are trying to use the Foundatio libraries, please get them from NuGet.** 49 | 50 | 1. You will need to have [Visual Studio Code](https://code.visualstudio.com) installed. 51 | 2. Open the `Foundatio.sln` Visual Studio solution file. 52 | 53 | ## Using Foundatio 54 | 55 | The sections below contain a small subset of what's possible with Foundatio. We recommend taking a peek at the source code for more information. Please let us know if you have any questions or need assistance! 56 | 57 | ### [Caching](https://github.com/FoundatioFx/Foundatio/tree/master/src/Foundatio/Caching) 58 | 59 | Caching allows you to store and access data lightning fast, saving you exspensive operations to create or get data. We provide four different cache implementations that derive from the [`ICacheClient` interface](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Caching/ICacheClient.cs): 60 | 61 | 1. [InMemoryCacheClient](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Caching/InMemoryCacheClient.cs): An in memory cache client implementation. This cache implementation is only valid for the lifetime of the process. It's worth noting that the in memory cache client has the ability to cache the last X items via the `MaxItems` property. We use this in [Exceptionless](https://github.com/exceptionless/Exceptionless) to only [keep the last 250 resolved geoip results](https://github.com/exceptionless/Exceptionless/blob/master/src/Exceptionless.Core/Geo/MaxMindGeoIpService.cs). 62 | 2. [HybridCacheClient](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Caching/HybridCacheClient.cs): This cache implementation uses both an `ICacheClient` and the `InMemoryCacheClient` and uses an `IMessageBus` to keep the cache in sync across processes. This can lead to **huge wins in performance** as you are saving a serialization operation and a call to the remote cache if the item exists in the local cache. 63 | 3. [RedisCacheClient](https://github.com/FoundatioFx/Foundatio.Redis/blob/master/src/Foundatio.Redis/Cache/RedisCacheClient.cs): A Redis cache client implementation. 64 | 4. [RedisHybridCacheClient](https://github.com/FoundatioFx/Foundatio.Redis/blob/master/src/Foundatio.Redis/Cache/RedisHybridCacheClient.cs): An implementation of `HybridCacheClient` that uses the `RedisCacheClient` as `ICacheClient` and the `RedisMessageBus` as `IMessageBus`. 65 | 5. [ScopedCacheClient](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Caching/ScopedCacheClient.cs): This cache implementation takes an instance of `ICacheClient` and a string `scope`. The scope is prefixed onto every cache key. This makes it really easy to scope all cache keys and remove them with ease. 66 | 67 | #### Sample 68 | 69 | ```csharp 70 | using Foundatio.Caching; 71 | 72 | ICacheClient cache = new InMemoryCacheClient(); 73 | await cache.SetAsync("test", 1); 74 | var value = await cache.GetAsync("test"); 75 | ``` 76 | 77 | ### [Queues](https://github.com/FoundatioFx/Foundatio/tree/master/src/Foundatio/Queues) 78 | 79 | Queues offer First In, First Out (FIFO) message delivery. We provide four different queue implementations that derive from the [`IQueue` interface](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Queues/IQueue.cs): 80 | 81 | 1. [InMemoryQueue](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Queues/InMemoryQueue.cs): An in memory queue implementation. This queue implementation is only valid for the lifetime of the process. 82 | 2. [RedisQueue](https://github.com/FoundatioFx/Foundatio.Redis/blob/master/src/Foundatio.Redis/Queues/RedisQueue.cs): An Redis queue implementation. 83 | 3. [AzureServiceBusQueue](https://github.com/FoundatioFx/Foundatio.AzureServiceBus/blob/master/src/Foundatio.AzureServiceBus/Queues/AzureServiceBusQueue.cs): An Azure Service Bus Queue implementation. 84 | 4. [AzureStorageQueue](https://github.com/FoundatioFx/Foundatio.AzureStorage/blob/master/src/Foundatio.AzureStorage/Queues/AzureStorageQueue.cs): An Azure Storage Queue implementation. 85 | 5. [SQSQueue](https://github.com/FoundatioFx/Foundatio.AWS/blob/master/src/Foundatio.AWS/Queues/SQSQueue.cs): An AWS SQS implementation. 86 | 87 | #### Sample 88 | 89 | ```csharp 90 | using Foundatio.Queues; 91 | 92 | IQueue queue = new InMemoryQueue(); 93 | 94 | await queue.EnqueueAsync(new SimpleWorkItem { 95 | Data = "Hello" 96 | }); 97 | 98 | var workItem = await queue.DequeueAsync(); 99 | ``` 100 | 101 | ### [Locks](https://github.com/FoundatioFx/Foundatio/tree/master/src/Foundatio/Lock) 102 | 103 | Locks ensure a resource is only accessed by one consumer at any given time. We provide two different locking implementations that derive from the [`ILockProvider` interface](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Lock/ILockProvider.cs): 104 | 105 | 1. [CacheLockProvider](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Lock/CacheLockProvider.cs): A lock implementation that uses cache to communicate between processes. 106 | 2. [ThrottlingLockProvider](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Lock/ThrottlingLockProvider.cs): A lock implementation that only allows a certain amount of locks through. You could use this to throttle api calls to some external service and it will throttle them across all processes asking for that lock. 107 | 3. [ScopedLockProvider](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Lock/ScopedLockProvider.cs): This lock implementation takes an instance of `ILockProvider` and a string `scope`. The scope is prefixed onto every lock key. This makes it really easy to scope all locks and release them with ease. 108 | 109 | It's worth noting that all lock providers take a `ICacheClient`. This allows you to ensure your code locks properly across machines. 110 | 111 | #### Sample 112 | 113 | ```csharp 114 | using Foundatio.Lock; 115 | 116 | ILockProvider locker = new CacheLockProvider(new InMemoryCacheClient(), new InMemoryMessageBus()); 117 | var testLock = await locker.AcquireAsync("test"); 118 | // ... 119 | await testLock.ReleaseAsync(); 120 | 121 | ILockProvider throttledLocker = new ThrottlingLockProvider(new InMemoryCacheClient(), 1, TimeSpan.FromMinutes(1)); 122 | var throttledLock = await throttledLocker.AcquireAsync("test"); 123 | // ... 124 | await throttledLock.ReleaseAsync(); 125 | ``` 126 | 127 | ### [Messaging](https://github.com/FoundatioFx/Foundatio/tree/master/src/Foundatio/Messaging) 128 | 129 | Allows you to publish and subscribe to messages flowing through your application. We provide four different message bus implementations that derive from the [`IMessageBus` interface](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Messaging/IMessageBus.cs): 130 | 131 | 1. [InMemoryMessageBus](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Messaging/InMemoryMessageBus.cs): An in memory message bus implementation. This message bus implementation is only valid for the lifetime of the process. 132 | 2. [RedisMessageBus](https://github.com/FoundatioFx/Foundatio.Redis/blob/master/src/Foundatio.Redis/Messaging/RedisMessageBus.cs): A Redis message bus implementation. 133 | 3. [RabbitMQMessageBus](https://github.com/FoundatioFx/Foundatio.RabbitMQ/blob/master/src/Foundatio.RabbitMQ/Messaging/RabbitMQMessageBus.cs): A RabbitMQ implementation. 134 | 4. [KafkaMessageBus](https://github.com/FoundatioFx/Foundatio.Kafka/blob/main/src/Foundatio.Kafka/Messaging/KafkaMessageBus.cs): A Kafka implementation. 135 | 5. [AzureServiceBusMessageBus](https://github.com/FoundatioFx/Foundatio.AzureServiceBus/blob/master/src/Foundatio.AzureServiceBus/Messaging/AzureServiceBusMessageBus.cs): An Azure Service Bus implementation. 136 | 137 | #### Sample 138 | 139 | ```csharp 140 | using Foundatio.Messaging; 141 | 142 | IMessageBus messageBus = new InMemoryMessageBus(); 143 | await messageBus.SubscribeAsync(msg => { 144 | // Got message 145 | }); 146 | 147 | await messageBus.PublishAsync(new SimpleMessageA { Data = "Hello" }); 148 | ``` 149 | 150 | ### [Jobs](https://github.com/FoundatioFx/Foundatio/tree/master/src/Foundatio/Jobs) 151 | 152 | Allows you to run a long running process (in process or out of process) without worrying about it being terminated prematurely. We provide three different ways of defining a job, based on your use case: 153 | 154 | 1. **Jobs**: All jobs must derive from the [`IJob` interface](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Jobs/IJob.cs). We also have a [`JobBase` base class](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Jobs/JobBase.cs) you can derive from which provides a JobContext and logging. You can then run jobs by calling `RunAsync()` on the job or by creating a instance of the [`JobRunner` class](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Jobs/JobRunner.cs) and calling one of the Run methods. The JobRunner can be used to easily run your jobs as Azure Web Jobs. 155 | 156 | #### Sample 157 | 158 | ```csharp 159 | using Foundatio.Jobs; 160 | 161 | public class HelloWorldJob : JobBase { 162 | public int RunCount { get; set; } 163 | 164 | protected override Task RunInternalAsync(JobContext context) { 165 | RunCount++; 166 | return Task.FromResult(JobResult.Success); 167 | } 168 | } 169 | ``` 170 | 171 | ```csharp 172 | var job = new HelloWorldJob(); 173 | await job.RunAsync(); // job.RunCount = 1; 174 | await job.RunContinuousAsync(iterationLimit: 2); // job.RunCount = 3; 175 | await job.RunContinuousAsync(cancellationToken: new CancellationTokenSource(10).Token); // job.RunCount > 10; 176 | ``` 177 | 178 | 2. **Queue Processor Jobs**: A queue processor job works great for working with jobs that will be driven from queued data. Queue Processor jobs must derive from [`QueueJobBase` class](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Jobs/QueueJobBase.cs). You can then run jobs by calling `RunAsync()` on the job or passing it to the [`JobRunner` class](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Jobs/JobRunner.cs). The JobRunner can be used to easily run your jobs as Azure Web Jobs. 179 | 180 | #### Sample 181 | 182 | ```csharp 183 | using Foundatio.Jobs; 184 | 185 | public class HelloWorldQueueJob : QueueJobBase { 186 | public int RunCount { get; set; } 187 | 188 | public HelloWorldQueueJob(IQueue queue) : base(queue) {} 189 | 190 | protected override Task ProcessQueueEntryAsync(QueueEntryContext context) { 191 | RunCount++; 192 | 193 | return Task.FromResult(JobResult.Success); 194 | } 195 | } 196 | 197 | public class HelloWorldQueueItem { 198 | public string Message { get; set; } 199 | } 200 | ``` 201 | 202 | ```csharp 203 | // Register the queue for HelloWorldQueueItem. 204 | container.AddSingleton>(s => new InMemoryQueue()); 205 | 206 | // To trigger the job we need to queue the HelloWorldWorkItem message. 207 | // This assumes that we injected an instance of IQueue queue 208 | 209 | IJob job = new HelloWorldQueueJob(); 210 | await job.RunAsync(); // job.RunCount = 0; The RunCount wasn't incremented because we didn't enqueue any data. 211 | 212 | await queue.EnqueueAsync(new HelloWorldWorkItem { Message = "Hello World" }); 213 | await job.RunAsync(); // job.RunCount = 1; 214 | 215 | await queue.EnqueueAsync(new HelloWorldWorkItem { Message = "Hello World" }); 216 | await queue.EnqueueAsync(new HelloWorldWorkItem { Message = "Hello World" }); 217 | await job.RunUntilEmptyAsync(); // job.RunCount = 3; 218 | ``` 219 | 220 | 3. **Work Item Jobs**: A work item job will run in a job pool among other work item jobs. This type of job works great for things that don't happen often but should be in a job (Example: Deleting an entity that has many children.). It will be triggered when you publish a message on the `message bus`. The job must derive from the [`WorkItemHandlerBase` class](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Jobs/WorkItemJob/WorkItemHandlerBase.cs). You can then run all shared jobs via [`JobRunner` class](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Jobs/JobRunner.cs). The JobRunner can be used to easily run your jobs as Azure Web Jobs. 221 | 222 | #### Sample 223 | 224 | ```csharp 225 | using System.Threading.Tasks; 226 | using Foundatio.Jobs; 227 | 228 | public class HelloWorldWorkItemHandler : WorkItemHandlerBase { 229 | public override async Task HandleItemAsync(WorkItemContext ctx) { 230 | var workItem = ctx.GetData(); 231 | 232 | // We can report the progress over the message bus easily. 233 | // To receive these messages just inject IMessageSubscriber 234 | // and Subscribe to messages of type WorkItemStatus 235 | await ctx.ReportProgressAsync(0, "Starting Hello World Job"); 236 | await Task.Delay(TimeSpan.FromSeconds(2.5)); 237 | await ctx.ReportProgressAsync(50, "Reading value"); 238 | await Task.Delay(TimeSpan.FromSeconds(.5)); 239 | await ctx.ReportProgressAsync(70, "Reading value"); 240 | await Task.Delay(TimeSpan.FromSeconds(.5)); 241 | await ctx.ReportProgressAsync(90, "Reading value."); 242 | await Task.Delay(TimeSpan.FromSeconds(.5)); 243 | 244 | await ctx.ReportProgressAsync(100, workItem.Message); 245 | } 246 | } 247 | 248 | public class HelloWorldWorkItem { 249 | public string Message { get; set; } 250 | } 251 | ``` 252 | 253 | ```csharp 254 | // Register the shared job. 255 | var handlers = new WorkItemHandlers(); 256 | handlers.Register(); 257 | 258 | // Register the handlers with dependency injection. 259 | container.AddSingleton(handlers); 260 | 261 | // Register the queue for WorkItemData. 262 | container.AddSingleton>(s => new InMemoryQueue()); 263 | 264 | // The job runner will automatically look for and run all registered WorkItemHandlers. 265 | new JobRunner(container.GetRequiredService(), instanceCount: 2).RunInBackground(); 266 | ``` 267 | 268 | ```csharp 269 | // To trigger the job we need to queue the HelloWorldWorkItem message. 270 | // This assumes that we injected an instance of IQueue queue 271 | 272 | // NOTE: You may have noticed that HelloWorldWorkItem doesn't derive from WorkItemData. 273 | // Foundatio has an extension method that takes the model you post and serializes it to the 274 | // WorkItemData.Data property. 275 | await queue.EnqueueAsync(new HelloWorldWorkItem { Message = "Hello World" }); 276 | ``` 277 | 278 | ### [File Storage](https://github.com/FoundatioFx/Foundatio/tree/master/src/Foundatio/Storage) 279 | 280 | We provide different file storage implementations that derive from the [`IFileStorage` interface](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Storage/IFileStorage.cs): 281 | 282 | 1. [InMemoryFileStorage](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Storage/InMemoryFileStorage.cs): An in memory file implementation. This file storage implementation is only valid for the lifetime of the process. 283 | 2. [FolderFileStorage](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Storage/FolderFileStorage.cs): An file storage implementation that uses the hard drive for storage. 284 | 3. [AzureFileStorage](https://github.com/FoundatioFx/Foundatio.AzureStorage/blob/master/src/Foundatio.AzureStorage/Storage/AzureFileStorage.cs): An Azure Blob storage implementation. 285 | 4. [S3FileStorage](https://github.com/FoundatioFx/Foundatio.AWS/blob/master/src/Foundatio.AWS/Storage/S3FileStorage.cs): An AWS S3 file storage implementation. 286 | 5. [RedisFileStorage](https://github.com/FoundatioFx/Foundatio.Redis/blob/master/src/Foundatio.Redis/Storage/RedisFileStorage.cs): An Redis file storage implementation. 287 | 6. [MinioFileStorage](https://github.com/FoundatioFx/Foundatio.Minio/blob/master/src/Foundatio.Minio/Storage/MinioFileStorage.cs) An Minio file storage implementation. 288 | 7. [AliyunFileStorage](https://github.com/FoundatioFx/Foundatio.Aliyun/blob/master/src/Foundatio.Aliyun/Storage/AliyunFileStorage.cs): An Aliyun file storage implementation. 289 | 8. [SshNetFileStorage](https://github.com/FoundatioFx/Foundatio.Storage.SshNet/blob/master/src/Foundatio.Storage.SshNet/Storage/SshNetFileStorage.cs): An SFTP file storage implementation. 290 | 291 | We recommend using all of the `IFileStorage` implementations as singletons. 292 | 293 | #### Sample 294 | 295 | ```csharp 296 | using Foundatio.Storage; 297 | 298 | IFileStorage storage = new InMemoryFileStorage(); 299 | await storage.SaveFileAsync("test.txt", "test"); 300 | string content = await storage.GetFileContentsAsync("test.txt") 301 | ``` 302 | 303 | ### [Metrics](https://github.com/FoundatioFx/Foundatio/tree/master/src/Foundatio/Metrics) 304 | 305 | We provide five implementations that derive from the [`IMetricsClient` interface](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Metrics/IMetricsClient.cs): 306 | 307 | 1. [InMemoryMetricsClient](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Metrics/InMemoryMetricsClient.cs): An in memory metrics implementation. 308 | 2. [RedisMetricsClient](https://github.com/FoundatioFx/Foundatio.Redis/blob/master/src/Foundatio.Redis/Metrics/RedisMetricsClient.cs): An Redis metrics implementation. 309 | 3. [StatsDMetricsClient](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Metrics/StatsDMetricsClient.cs): An statsd metrics implementation. 310 | 4. [MetricsNETClient](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio.MetricsNET/MetricsNETClient.cs): An [Metrics.NET](https://github.com/Recognos/Metrics.NET) implementation. 311 | 5. [AppMetricsClient](https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio.AppMetrics/AppMetricsClient.cs): An [AppMetrics](https://github.com/AppMetrics/AppMetrics) implementation. 312 | 6. [CloudWatchMetricsClient](https://github.com/FoundatioFx/Foundatio.AWS/blob/master/src/Foundatio.AWS/Metrics/CloudWatchMetricsClient.cs): An [AWS CloudWatch](https://aws.amazon.com/cloudwatch/) implementation. 313 | 314 | We recommend using all of the `IMetricsClient` implementations as singletons. 315 | 316 | #### Sample 317 | 318 | ```csharp 319 | IMetricsClient metrics = new InMemoryMetricsClient(); 320 | metrics.Counter("c1"); 321 | metrics.Gauge("g1", 2.534); 322 | metrics.Timer("t1", 50788); 323 | ``` 324 | 325 | ## Sample Application 326 | 327 | We have both [slides](https://docs.google.com/presentation/d/1ax4YmfCdao75aEakjdMvapHs4QxvTZOimd3cHTZ9JG0/edit?usp=sharing) and a [sample application](https://github.com/FoundatioFx/Foundatio.Samples) that shows off how to use Foundatio. 328 | 329 | ## Thanks to all the people who have contributed 330 | 331 | [![contributors](https://contributors-img.web.app/image?repo=foundatiofx/RabbitMQ)](https://github.com/foundatiofx/Foundatio.RabbitMQ/graphs/contributors) -------------------------------------------------------------------------------- /build/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rabbitmq:4.0.3-management 2 | 3 | COPY rabbitmq_delayed_message_exchange-4.0.2.ez /opt/rabbitmq/plugins 4 | RUN rabbitmq-plugins enable rabbitmq_delayed_message_exchange 5 | -------------------------------------------------------------------------------- /build/Foundatio.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundatioFx/Foundatio.RabbitMQ/6e5d04014a9c8cf5a443a2659bb84fd3cd819c80/build/Foundatio.snk -------------------------------------------------------------------------------- /build/common.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1;net8.0 5 | Foundatio 6 | Pluggable foundation blocks for building distributed apps. 7 | https://github.com/FoundatioFx/Foundatio.RabbitMQ 8 | https://github.com/FoundatioFx/Foundatio.RabbitMQ/releases 9 | Queue;Messaging;Message;Bus;ServiceBus;Locking;Lock;Distributed;File;Storage;Blob;Jobs;Metrics;Stats;Azure;Redis;StatsD;Amazon;AWS;S3;broker;Logging;Log 10 | true 11 | v 12 | true 13 | 14 | Copyright (c) 2025 Foundatio. All rights reserved. 15 | FoundatioFx 16 | $(NoWarn);CS1591;NU1701 17 | true 18 | latest 19 | true 20 | $(SolutionDir)artifacts 21 | foundatio-icon.png 22 | README.md 23 | Apache-2.0 24 | $(PackageProjectUrl) 25 | true 26 | true 27 | embedded 28 | 29 | 30 | 31 | true 32 | 33 | 34 | 35 | true 36 | $(MSBuildThisFileDirectory)Foundatio.snk 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /build/foundatio-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundatioFx/Foundatio.RabbitMQ/6e5d04014a9c8cf5a443a2659bb84fd3cd819c80/build/foundatio-icon.png -------------------------------------------------------------------------------- /build/rabbitmq_delayed_message_exchange-4.0.2.ez: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/FoundatioFx/Foundatio.RabbitMQ/6e5d04014a9c8cf5a443a2659bb84fd3cd819c80/build/rabbitmq_delayed_message_exchange-4.0.2.ez -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | rabbitmq: 3 | image: rabbitmq:4.1.0-management 4 | ports: 5 | - "5672:5672" 6 | - "8080:15672" # management ui - login with guest:guest 7 | 8 | rabbitmq-delayed: 9 | build: ./build # use RabbitMQ docker image with delay plugin installed 10 | ports: 11 | - "5673:5672" 12 | - "8081:15672" # management ui - login with guest:guest 13 | 14 | ready: 15 | image: andrewlock/wait-for-dependencies 16 | command: rabbitmq:15672 rabbitmq-delayed:15672 17 | depends_on: 18 | - rabbitmq 19 | - rabbitmq-delayed 20 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "8.0.100", 4 | "rollForward": "latestMinor", 5 | "allowPrerelease": false 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /samples/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | Exe 6 | False 7 | 8 | -------------------------------------------------------------------------------- /samples/Foundatio.RabbitMQ.Publish/Foundatio.RabbitMQ.Publish.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /samples/Foundatio.RabbitMQ.Publish/MyMessage.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.RabbitMQ; 2 | 3 | public class MyMessage 4 | { 5 | public string Hey { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /samples/Foundatio.RabbitMQ.Publish/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Foundatio.Messaging; 4 | 5 | namespace Foundatio.RabbitMQ.Publish; 6 | 7 | public class Program 8 | { 9 | public static async Task Main() 10 | { 11 | Console.WriteLine("Enter the message and press enter to send:"); 12 | 13 | using var messageBus = new RabbitMQMessageBus(new RabbitMQMessageBusOptions { ConnectionString = "amqp://localhost:5672" }); 14 | string message; 15 | do 16 | { 17 | message = Console.ReadLine(); 18 | var delay = TimeSpan.FromSeconds(1); 19 | var body = new MyMessage { Hey = message }; 20 | await messageBus.PublishAsync(body, delay); 21 | Console.WriteLine("Message sent. Enter new message or press enter to exit:"); 22 | } while (!String.IsNullOrEmpty(message)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /samples/Foundatio.RabbitMQ.Subscribe/Foundatio.RabbitMQ.Subscribe.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /samples/Foundatio.RabbitMQ.Subscribe/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Foundatio.Messaging; 5 | 6 | namespace Foundatio.RabbitMQ.Subscribe; 7 | 8 | public class Program 9 | { 10 | public static async Task Main(string[] args) 11 | { 12 | Console.WriteLine("Waiting to receive messages, press enter to quit..."); 13 | 14 | var tasks = new List(); 15 | var messageBuses = new List(); 16 | for (int i = 0; i < 3; i++) 17 | { 18 | var messageBus = new RabbitMQMessageBus(new RabbitMQMessageBusOptions { ConnectionString = "amqp://localhost:5672" }); 19 | messageBuses.Add(messageBus); 20 | tasks.Add(messageBus.SubscribeAsync(msg => { Console.WriteLine($"Got subscriber {messageBus.MessageBusId} message: {msg.Hey}"); })); 21 | } 22 | await Task.WhenAll(tasks); 23 | Console.ReadLine(); 24 | foreach (var messageBus in messageBuses) 25 | messageBus.Dispose(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/Foundatio.RabbitMQ/Extensions/TaskExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading.Tasks; 5 | using Foundatio.AsyncEx; 6 | 7 | namespace Foundatio.Extensions; 8 | 9 | internal static class TaskExtensions 10 | { 11 | [DebuggerStepThrough] 12 | public static ConfiguredTaskAwaitable AnyContext(this Task task) 13 | { 14 | return task.ConfigureAwait(continueOnCapturedContext: false); 15 | } 16 | 17 | [DebuggerStepThrough] 18 | public static ConfiguredTaskAwaitable AnyContext(this Task task) 19 | { 20 | return task.ConfigureAwait(continueOnCapturedContext: false); 21 | } 22 | 23 | [DebuggerStepThrough] 24 | public static ConfiguredTaskAwaitable AnyContext(this AwaitableDisposable task) where TResult : IDisposable 25 | { 26 | return task.ConfigureAwait(continueOnCapturedContext: false); 27 | } 28 | 29 | [DebuggerStepThrough] 30 | public static ConfiguredValueTaskAwaitable AnyContext(this ValueTask task) 31 | { 32 | return task.ConfigureAwait(continueOnCapturedContext: false); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Foundatio.RabbitMQ/Foundatio.RabbitMQ.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Foundatio.RabbitMQ/Messaging/AcknowledgementStrategy.cs: -------------------------------------------------------------------------------- 1 | namespace Foundatio.Messaging; 2 | 3 | public enum AcknowledgementStrategy 4 | { 5 | FireAndForget, 6 | Automatic 7 | } 8 | -------------------------------------------------------------------------------- /src/Foundatio.RabbitMQ/Messaging/RabbitMQMessageBus.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Foundatio.AsyncEx; 8 | using Foundatio.Extensions; 9 | using Microsoft.Extensions.Logging; 10 | using RabbitMQ.Client; 11 | using RabbitMQ.Client.Events; 12 | using RabbitMQ.Client.Exceptions; 13 | 14 | namespace Foundatio.Messaging; 15 | 16 | public class RabbitMQMessageBus : MessageBusBase, IAsyncDisposable 17 | { 18 | private readonly AsyncLock _lock = new(); 19 | private readonly ConnectionFactory _factory; 20 | private IConnection _publisherConnection; 21 | private IConnection _subscriberConnection; 22 | private IChannel _publisherChannel; 23 | private IChannel _subscriberChannel; 24 | private bool? _delayedExchangePluginEnabled; 25 | private bool _isDisposed; 26 | 27 | public RabbitMQMessageBus(RabbitMQMessageBusOptions options) : base(options) 28 | { 29 | if (String.IsNullOrEmpty(options.ConnectionString)) 30 | throw new ArgumentException("ConnectionString is required."); 31 | 32 | // Initialize the connection factory. automatic recovery will allow the connections to be restored 33 | // in case the server is restarted or there has been any network failures 34 | // Topology ( queues, exchanges, bindings and consumers) recovery "TopologyRecoveryEnabled" is already enabled 35 | // by default so no need to initialize it. NetworkRecoveryInterval is also by default set to 5 seconds. 36 | // it can always be fine-tuned if needed. 37 | _factory = new ConnectionFactory 38 | { 39 | Uri = new Uri(options.ConnectionString), 40 | AutomaticRecoveryEnabled = true 41 | }; 42 | } 43 | 44 | public RabbitMQMessageBus(Builder config) 45 | : this(config(new RabbitMQMessageBusOptionsBuilder()).Build()) { } 46 | 47 | protected override Task RemoveTopicSubscriptionAsync() 48 | { 49 | _logger.LogTrace("RemoveTopicSubscriptionAsync"); 50 | return CloseSubscriberConnectionAsync(); 51 | } 52 | 53 | protected override async Task EnsureTopicSubscriptionAsync(CancellationToken cancellationToken) 54 | { 55 | if (_subscriberChannel != null) 56 | return; 57 | 58 | await EnsureTopicCreatedAsync(cancellationToken).AnyContext(); 59 | 60 | using (await _lock.LockAsync().AnyContext()) 61 | { 62 | if (_subscriberChannel != null) 63 | return; 64 | 65 | _subscriberConnection = await CreateConnectionAsync().AnyContext(); 66 | _subscriberChannel = await _subscriberConnection.CreateChannelAsync(cancellationToken: cancellationToken).AnyContext(); 67 | 68 | // If InitPublisher is called first, then we will never come in this if-clause. 69 | if (!await CreateDelayedExchangeAsync(_subscriberChannel).AnyContext()) 70 | { 71 | await _subscriberChannel.DisposeAsync().AnyContext(); 72 | await _subscriberConnection.DisposeAsync().AnyContext(); 73 | 74 | _subscriberConnection = await CreateConnectionAsync().AnyContext(); 75 | _subscriberChannel = await _subscriberConnection.CreateChannelAsync(cancellationToken: cancellationToken).AnyContext(); 76 | await CreateRegularExchangeAsync(_subscriberChannel).AnyContext(); 77 | } 78 | 79 | _subscriberConnection.CallbackExceptionAsync += OnSubscriberConnectionOnCallbackExceptionAsync; 80 | _subscriberConnection.ConnectionBlockedAsync += OnSubscriberConnectionOnConnectionBlockedAsync; 81 | _subscriberConnection.ConnectionRecoveryErrorAsync += OnSubscriberConnectionOnConnectionRecoveryErrorAsync; 82 | _subscriberConnection.ConnectionShutdownAsync += OnSubscriberConnectionOnConnectionShutdownAsync; 83 | _subscriberConnection.ConnectionUnblockedAsync += OnSubscriberConnectionOnConnectionUnblockedAsync; 84 | _subscriberConnection.RecoveringConsumerAsync += OnSubscriberConnectionOnRecoveringConsumerAsync; 85 | _subscriberConnection.RecoverySucceededAsync += OnSubscriberConnectionOnRecoverySucceededAsync; 86 | 87 | string queueName = await CreateQueueAsync(_subscriberChannel).AnyContext(); 88 | var consumer = new AsyncEventingBasicConsumer(_subscriberChannel); 89 | consumer.ReceivedAsync += OnMessageAsync; 90 | consumer.ShutdownAsync += OnConsumerShutdownAsync; 91 | 92 | await _subscriberChannel.BasicConsumeAsync(queueName, _options.AcknowledgementStrategy == AcknowledgementStrategy.FireAndForget, consumer, cancellationToken: cancellationToken).AnyContext(); 93 | _logger.LogTrace("The unique channel number for the subscriber is : {ChannelNumber}", _subscriberChannel.ChannelNumber); 94 | } 95 | } 96 | 97 | private Task OnSubscriberConnectionOnCallbackExceptionAsync(object sender, CallbackExceptionEventArgs e) 98 | { 99 | _logger.LogError(e.Exception, "Subscriber callback exception: {Message}", e.Exception.Message); 100 | return Task.CompletedTask; 101 | } 102 | 103 | private Task OnSubscriberConnectionOnConnectionBlockedAsync(object sender, ConnectionBlockedEventArgs e) 104 | { 105 | _logger.LogError("Subscriber connection blocked: {Reason}", e.Reason); 106 | return Task.CompletedTask; 107 | } 108 | 109 | private Task OnSubscriberConnectionOnConnectionRecoveryErrorAsync(object sender, ConnectionRecoveryErrorEventArgs e) 110 | { 111 | _logger.LogError(e.Exception, "Subscriber connection recovery error: {Message}", e.Exception.Message); 112 | return Task.CompletedTask; 113 | } 114 | 115 | private Task OnSubscriberConnectionOnConnectionShutdownAsync(object sender, ShutdownEventArgs e) 116 | { 117 | _logger.LogInformation(e.Exception, "Subscriber shutdown. Reply Code: {ReplyCode} Reason: {ReplyText} Initiator: {Initiator}", e.ReplyCode, e.ReplyText, e.Initiator); 118 | return Task.CompletedTask; 119 | } 120 | 121 | private Task OnSubscriberConnectionOnConnectionUnblockedAsync(object sender, AsyncEventArgs e) 122 | { 123 | _logger.LogInformation("Subscriber connection unblocked"); 124 | return Task.CompletedTask; 125 | } 126 | 127 | private Task OnSubscriberConnectionOnRecoveringConsumerAsync(object sender, RecoveringConsumerEventArgs e) 128 | { 129 | _logger.LogInformation("Subscriber connection recovering: {ConsumerTag}", e.ConsumerTag); 130 | return Task.CompletedTask; 131 | } 132 | 133 | private Task OnSubscriberConnectionOnRecoverySucceededAsync(object sender, AsyncEventArgs e) 134 | { 135 | _logger.LogInformation("Subscriber connection recovery succeeded"); 136 | return Task.CompletedTask; 137 | } 138 | 139 | private Task OnConsumerShutdownAsync(object sender, ShutdownEventArgs e) 140 | { 141 | _logger.LogInformation(e.Exception, "Consumer shutdown. Reply Code: {ReplyCode} Reason: {ReplyText} Initiator: {Initiator}", e.ReplyCode, e.ReplyText, e.Initiator); 142 | return Task.CompletedTask; 143 | } 144 | 145 | private async Task OnMessageAsync(object sender, BasicDeliverEventArgs envelope) 146 | { 147 | _logger.LogTrace("OnMessageAsync({MessageId})", envelope.BasicProperties?.MessageId); 148 | 149 | if (_subscribers.IsEmpty) 150 | { 151 | _logger.LogTrace("No subscribers ({MessageId})", envelope.BasicProperties?.MessageId); 152 | if (_options.AcknowledgementStrategy == AcknowledgementStrategy.Automatic) 153 | await _subscriberChannel.BasicRejectAsync(envelope.DeliveryTag, true).AnyContext(); 154 | 155 | return; 156 | } 157 | 158 | try 159 | { 160 | var message = ConvertToMessage(envelope); 161 | await SendMessageToSubscribersAsync(message).AnyContext(); 162 | 163 | if (_options.AcknowledgementStrategy == AcknowledgementStrategy.Automatic) 164 | await _subscriberChannel.BasicAckAsync(envelope.DeliveryTag, false).AnyContext(); 165 | } 166 | catch (Exception ex) 167 | { 168 | _logger.LogError(ex, "Error handling message ({MessageId}): {Message}", envelope.BasicProperties?.MessageId, ex.Message); 169 | if (_options.AcknowledgementStrategy == AcknowledgementStrategy.Automatic) 170 | await _subscriberChannel.BasicRejectAsync(envelope.DeliveryTag, true).AnyContext(); 171 | } 172 | } 173 | 174 | /// 175 | /// Get MessageBusData from a RabbitMQ delivery 176 | /// 177 | /// The RabbitMQ delivery arguments 178 | /// The MessageBusData for the message 179 | protected virtual IMessage ConvertToMessage(BasicDeliverEventArgs envelope) 180 | { 181 | var message = new Message(envelope.Body.ToArray(), DeserializeMessageBody) 182 | { 183 | Type = envelope.BasicProperties.Type, 184 | ClrType = GetMappedMessageType(envelope.BasicProperties.Type), 185 | CorrelationId = envelope.BasicProperties.CorrelationId, 186 | UniqueId = envelope.BasicProperties.MessageId 187 | }; 188 | 189 | if (envelope.BasicProperties.Headers != null) 190 | foreach (var header in envelope.BasicProperties.Headers) 191 | { 192 | if (header.Value is byte[] byteData) 193 | message.Properties[header.Key] = Encoding.UTF8.GetString(byteData); 194 | else 195 | message.Properties[header.Key] = header.Value.ToString(); 196 | } 197 | 198 | return message; 199 | } 200 | 201 | protected override async Task EnsureTopicCreatedAsync(CancellationToken cancellationToken) 202 | { 203 | if (_publisherChannel != null) 204 | return; 205 | 206 | using (await _lock.LockAsync().AnyContext()) 207 | { 208 | if (_publisherChannel != null) 209 | return; 210 | 211 | // Create the client connection, channel, declares the exchange, queue and binds 212 | // the exchange with the publisher queue. It requires the name of our exchange, exchange type, durability and auto-delete. 213 | // For now, we are using same autoDelete for both exchange and queue (it will survive a server restart) 214 | _publisherConnection = await CreateConnectionAsync().AnyContext(); 215 | _publisherChannel = await _publisherConnection.CreateChannelAsync(cancellationToken: cancellationToken).AnyContext(); 216 | 217 | // We first attempt to create "x-delayed-type". For this plugin should be installed. 218 | // However, we plug in is not installed this will throw an exception. In that case 219 | // we attempt to create regular exchange. If regular exchange also throws and exception 220 | // then troubleshoot the problem. 221 | if (!await CreateDelayedExchangeAsync(_publisherChannel).AnyContext()) 222 | { 223 | // if the initial exchange creation was not successful, then we must close the previous connection 224 | // and establish the new client connection and model; otherwise you will keep receiving failure in creation 225 | // of the regular exchange too. 226 | await _publisherChannel.DisposeAsync().AnyContext(); 227 | await _publisherConnection.DisposeAsync().AnyContext(); 228 | 229 | _publisherConnection = await CreateConnectionAsync().AnyContext(); 230 | _publisherChannel = await _publisherConnection.CreateChannelAsync(cancellationToken: cancellationToken).AnyContext(); 231 | await CreateRegularExchangeAsync(_publisherChannel).AnyContext(); 232 | } 233 | 234 | _publisherConnection.CallbackExceptionAsync += OnPublisherConnectionOnCallbackExceptionAsync; 235 | _publisherConnection.ConnectionBlockedAsync += OnPublisherConnectionOnConnectionBlockedAsync; 236 | _publisherConnection.ConnectionRecoveryErrorAsync += OnPublisherConnectionOnConnectionRecoveryErrorAsync; 237 | _publisherConnection.ConnectionShutdownAsync += OnPublisherConnectionOnConnectionShutdownAsync; 238 | _publisherConnection.ConnectionUnblockedAsync += OnPublisherConnectionOnConnectionUnblockedAsync; 239 | _publisherConnection.RecoveringConsumerAsync += OnPublisherConnectionOnRecoveringConsumerAsync; 240 | _publisherConnection.RecoverySucceededAsync += OnPublisherConnectionOnRecoverySucceededAsync; 241 | 242 | _logger.LogTrace("The unique channel number for the publisher is : {ChannelNumber}", _publisherChannel.ChannelNumber); 243 | } 244 | } 245 | 246 | private Task OnPublisherConnectionOnCallbackExceptionAsync(object sender, CallbackExceptionEventArgs e) 247 | { 248 | _logger.LogError(e.Exception, "Publisher callback exception: {Message}", e.Exception.Message); 249 | return Task.CompletedTask; 250 | } 251 | 252 | private Task OnPublisherConnectionOnConnectionBlockedAsync(object sender, ConnectionBlockedEventArgs e) 253 | { 254 | _logger.LogError("Publisher connection blocked: {Reason}", e.Reason); 255 | return Task.CompletedTask; 256 | } 257 | 258 | private Task OnPublisherConnectionOnConnectionRecoveryErrorAsync(object sender, ConnectionRecoveryErrorEventArgs e) 259 | { 260 | _logger.LogError(e.Exception, "Publisher connection recovery error: {Message}", e.Exception.Message); 261 | return Task.CompletedTask; 262 | } 263 | 264 | private Task OnPublisherConnectionOnConnectionShutdownAsync(object sender, ShutdownEventArgs e) 265 | { 266 | _logger.LogInformation(e.Exception, "Publisher shutdown. Reply Code: {ReplyCode} Reason: {ReplyText} Initiator: {Initiator}", e.ReplyCode, e.ReplyText, e.Initiator); 267 | return Task.CompletedTask; 268 | } 269 | 270 | private Task OnPublisherConnectionOnConnectionUnblockedAsync(object sender, AsyncEventArgs e) 271 | { 272 | _logger.LogInformation("Publisher connection unblocked"); 273 | return Task.CompletedTask; 274 | } 275 | 276 | private Task OnPublisherConnectionOnRecoveringConsumerAsync(object sender, RecoveringConsumerEventArgs e) 277 | { 278 | _logger.LogInformation("Publisher connection recovering: {ConsumerTag}", e.ConsumerTag); 279 | return Task.CompletedTask; 280 | } 281 | 282 | private Task OnPublisherConnectionOnRecoverySucceededAsync(object sender, AsyncEventArgs e) 283 | { 284 | _logger.LogInformation("Publisher connection recovery succeeded"); 285 | return Task.CompletedTask; 286 | } 287 | 288 | /// 289 | /// Publish the message 290 | /// 291 | /// 292 | /// 293 | /// Message options 294 | /// 295 | /// RabbitMQ has an upper limit of 2GB for messages.BasicPublish blocking AMQP operations. 296 | /// The rule of thumb is: avoid sharing channels across threads. 297 | /// Publishers in your application that publish from separate threads should use their own channels. 298 | /// The same is a good idea for consumers. 299 | protected override async Task PublishImplAsync(string messageType, object message, MessageOptions options, CancellationToken cancellationToken) 300 | { 301 | byte[] data = SerializeMessageBody(messageType, message); 302 | 303 | // if the RabbitMQ plugin is not available then use the base class delay mechanism 304 | if (!_delayedExchangePluginEnabled.Value && options.DeliveryDelay.HasValue && options.DeliveryDelay.Value > TimeSpan.Zero) 305 | { 306 | var mappedType = GetMappedMessageType(messageType); 307 | _logger.LogTrace("Schedule delayed message: {MessageType} ({Delay}ms)", messageType, options.DeliveryDelay.Value.TotalMilliseconds); 308 | 309 | await AddDelayedMessageAsync(mappedType, message, options.DeliveryDelay.Value).AnyContext(); 310 | return; 311 | } 312 | 313 | var basicProperties = new BasicProperties 314 | { 315 | MessageId = options.UniqueId ?? Guid.NewGuid().ToString("N"), 316 | CorrelationId = options.CorrelationId, 317 | Type = messageType, 318 | }; 319 | 320 | if (_options.IsDurable) 321 | basicProperties.Persistent = true; 322 | if (_options.DefaultMessageTimeToLive.HasValue) 323 | basicProperties.Expiration = _options.DefaultMessageTimeToLive.Value.TotalMilliseconds.ToString(CultureInfo.InvariantCulture); 324 | 325 | if (options.Properties.Count > 0) 326 | { 327 | basicProperties.Headers ??= new Dictionary(); 328 | foreach (var property in options.Properties) 329 | basicProperties.Headers.Add(property.Key, property.Value); 330 | } 331 | 332 | // RabbitMQ only supports delayed messages with a third party plugin called "rabbitmq_delayed_message_exchange" 333 | if (_delayedExchangePluginEnabled.Value && options.DeliveryDelay.HasValue && options.DeliveryDelay.Value > TimeSpan.Zero) 334 | { 335 | // It's necessary to typecast long to int because RabbitMQ on the consumer side is reading the 336 | // data back as signed (using BinaryReader#ReadInt64). You will see the value to be negative 337 | // and the data will be delivered immediately. 338 | basicProperties.Headers = new Dictionary { { "x-delay", Convert.ToInt32(options.DeliveryDelay.Value.TotalMilliseconds) } }; 339 | 340 | _logger.LogTrace("Schedule delayed message: {MessageType} ({Delay}ms)", messageType, options.DeliveryDelay.Value.TotalMilliseconds); 341 | } 342 | else 343 | { 344 | _logger.LogTrace("Message publish type {MessageType} {MessageId}", messageType, basicProperties.MessageId); 345 | } 346 | 347 | using (await _lock.LockAsync().AnyContext()) 348 | await _publisherChannel.BasicPublishAsync(_options.Topic, String.Empty, mandatory: false, basicProperties, data, cancellationToken: cancellationToken).AnyContext(); 349 | 350 | _logger.LogTrace("Done publishing type {MessageType} {MessageId}", messageType, basicProperties.MessageId); 351 | } 352 | 353 | /// 354 | /// Connect to a broker - RabbitMQ 355 | /// 356 | /// 357 | private Task CreateConnectionAsync() 358 | { 359 | return _factory.CreateConnectionAsync(); 360 | } 361 | 362 | /// 363 | /// Attempts to create the delayed exchange. 364 | /// 365 | /// 366 | /// true if the delayed exchange was successfully declared. Which means plugin was installed. 367 | private async Task CreateDelayedExchangeAsync(IChannel channel) 368 | { 369 | bool success = true; 370 | if (_delayedExchangePluginEnabled.HasValue) 371 | return _delayedExchangePluginEnabled.Value; 372 | 373 | try 374 | { 375 | // This exchange is a delayed exchange (fanout). You need rabbitmq_delayed_message_exchange plugin to RabbitMQ 376 | // Disclaimer : https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/ 377 | // Please read the *Performance Impact* of the delayed exchange type. 378 | var args = new Dictionary { { "x-delayed-type", ExchangeType.Fanout } }; 379 | await channel.ExchangeDeclareAsync(_options.Topic, "x-delayed-message", _options.IsDurable, false, args).AnyContext(); 380 | } 381 | catch (OperationInterruptedException ex) 382 | { 383 | _logger.LogInformation(ex, "Unable to create x-delayed-type exchange: {Message}", ex.Message); 384 | success = false; 385 | } 386 | 387 | _delayedExchangePluginEnabled = success; 388 | return _delayedExchangePluginEnabled.Value; 389 | } 390 | 391 | private Task CreateRegularExchangeAsync(IChannel channel) 392 | { 393 | return channel.ExchangeDeclareAsync(_options.Topic, ExchangeType.Fanout, _options.IsDurable, false); 394 | } 395 | 396 | /// 397 | /// The client sends a message to an exchange and attaches a routing key to it. 398 | /// The message is sent to all queues with the matching routing key. Each queue has a 399 | /// receiver attached which will process the message. We’ll initiate a dedicated message 400 | /// exchange and not use the default one. Note that a queue can be dedicated to one or more routing keys. 401 | /// 402 | /// channel 403 | private async Task CreateQueueAsync(IChannel channel) 404 | { 405 | // Set up the queue where the messages will reside - it requires the queue name and durability. 406 | // Durable (the queue will survive a broker restart) 407 | // Arguments (some brokers use it to implement additional features like message TTL) 408 | var result = await channel.QueueDeclareAsync(_options.SubscriptionQueueName, _options.IsDurable, _options.IsSubscriptionQueueExclusive, _options.SubscriptionQueueAutoDelete, _options.Arguments).AnyContext(); 409 | string queueName = result.QueueName; 410 | 411 | // bind the queue with the exchange. 412 | await channel.QueueBindAsync(queueName, _options.Topic, "").AnyContext(); 413 | 414 | return queueName; 415 | } 416 | 417 | public override void Dispose() 418 | { 419 | base.Dispose(); 420 | 421 | if (_isDisposed) 422 | return; 423 | 424 | _isDisposed = true; 425 | 426 | if (_factory != null) 427 | _factory.AutomaticRecoveryEnabled = false; 428 | 429 | ClosePublisherConnection(); 430 | CloseSubscriberConnection(); 431 | GC.SuppressFinalize(this); 432 | } 433 | 434 | public async ValueTask DisposeAsync() 435 | { 436 | base.Dispose(); 437 | 438 | if (_isDisposed) 439 | return; 440 | 441 | _isDisposed = true; 442 | 443 | if (_factory != null) 444 | _factory.AutomaticRecoveryEnabled = false; 445 | 446 | await ClosePublisherConnectionAsync().AnyContext(); 447 | await CloseSubscriberConnectionAsync().AnyContext(); 448 | GC.SuppressFinalize(this); 449 | } 450 | 451 | private void ClosePublisherConnection() 452 | { 453 | if (_publisherConnection == null) 454 | return; 455 | 456 | using (_lock.Lock()) 457 | { 458 | _logger.LogTrace("ClosePublisherConnection"); 459 | 460 | if (_publisherChannel != null) 461 | { 462 | _publisherChannel.Dispose(); 463 | _publisherChannel = null; 464 | } 465 | 466 | if (_publisherConnection != null) 467 | { 468 | _publisherConnection.Dispose(); 469 | _publisherConnection = null; 470 | } 471 | } 472 | } 473 | 474 | private async Task ClosePublisherConnectionAsync() 475 | { 476 | if (_publisherConnection == null) 477 | return; 478 | 479 | using (await _lock.LockAsync().AnyContext()) 480 | { 481 | _logger.LogTrace("ClosePublisherConnectionAsync"); 482 | 483 | if (_publisherChannel != null) 484 | { 485 | await _publisherChannel.DisposeAsync().AnyContext(); 486 | _publisherChannel = null; 487 | } 488 | 489 | if (_publisherConnection != null) 490 | { 491 | _publisherConnection.CallbackExceptionAsync -= OnPublisherConnectionOnCallbackExceptionAsync; 492 | _publisherConnection.ConnectionBlockedAsync -= OnPublisherConnectionOnConnectionBlockedAsync; 493 | _publisherConnection.ConnectionRecoveryErrorAsync -= OnPublisherConnectionOnConnectionRecoveryErrorAsync; 494 | _publisherConnection.ConnectionShutdownAsync -= OnPublisherConnectionOnConnectionShutdownAsync; 495 | _publisherConnection.ConnectionUnblockedAsync -= OnPublisherConnectionOnConnectionUnblockedAsync; 496 | _publisherConnection.RecoveringConsumerAsync -= OnPublisherConnectionOnRecoveringConsumerAsync; 497 | _publisherConnection.RecoverySucceededAsync -= OnPublisherConnectionOnRecoverySucceededAsync; 498 | await _publisherConnection.DisposeAsync().AnyContext(); 499 | _publisherConnection = null; 500 | } 501 | } 502 | } 503 | 504 | private void CloseSubscriberConnection() 505 | { 506 | if (_subscriberConnection == null) 507 | return; 508 | 509 | using (_lock.Lock()) 510 | { 511 | _logger.LogTrace("CloseSubscriberConnection"); 512 | 513 | if (_subscriberChannel != null) 514 | { 515 | _subscriberChannel.Dispose(); 516 | _subscriberChannel = null; 517 | } 518 | 519 | if (_subscriberConnection != null) 520 | { 521 | _subscriberConnection.CallbackExceptionAsync -= OnSubscriberConnectionOnCallbackExceptionAsync; 522 | _subscriberConnection.ConnectionBlockedAsync -= OnSubscriberConnectionOnConnectionBlockedAsync; 523 | _subscriberConnection.ConnectionRecoveryErrorAsync -= OnSubscriberConnectionOnConnectionRecoveryErrorAsync; 524 | _subscriberConnection.ConnectionShutdownAsync -= OnSubscriberConnectionOnConnectionShutdownAsync; 525 | _subscriberConnection.ConnectionUnblockedAsync -= OnSubscriberConnectionOnConnectionUnblockedAsync; 526 | _subscriberConnection.RecoveringConsumerAsync -= OnSubscriberConnectionOnRecoveringConsumerAsync; 527 | _subscriberConnection.RecoverySucceededAsync -= OnSubscriberConnectionOnRecoverySucceededAsync; 528 | _subscriberConnection.Dispose(); 529 | _subscriberConnection = null; 530 | } 531 | } 532 | } 533 | 534 | private async Task CloseSubscriberConnectionAsync() 535 | { 536 | if (_subscriberConnection == null) 537 | return; 538 | 539 | using (await _lock.LockAsync().AnyContext()) 540 | { 541 | _logger.LogTrace("CloseSubscriberConnectionAsync"); 542 | 543 | if (_subscriberChannel != null) 544 | { 545 | await _subscriberChannel.DisposeAsync().AnyContext(); 546 | _subscriberChannel = null; 547 | } 548 | 549 | if (_subscriberConnection != null) 550 | { 551 | await _subscriberConnection.DisposeAsync().AnyContext(); 552 | _subscriberConnection = null; 553 | } 554 | } 555 | } 556 | } 557 | -------------------------------------------------------------------------------- /src/Foundatio.RabbitMQ/Messaging/RabbitMQMessageBusOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Foundatio.Messaging; 5 | 6 | public class RabbitMQMessageBusOptions : SharedMessageBusOptions 7 | { 8 | /// 9 | /// The connection string. See https://www.rabbitmq.com/uri-spec.html for more information. 10 | /// 11 | public string ConnectionString { get; set; } 12 | 13 | /// 14 | /// The default message time to live. The value of the expiration field describes the TTL period in milliseconds. 15 | /// 16 | public TimeSpan? DefaultMessageTimeToLive { get; set; } 17 | 18 | /// 19 | /// Arguments passed to QueueDeclare. Some brokers use it to implement additional features like message TTL. 20 | /// 21 | public IDictionary Arguments { get; set; } 22 | 23 | /// 24 | /// Durable (will survive a broker restart) 25 | /// 26 | public bool IsDurable { get; set; } = true; 27 | 28 | /// 29 | /// Whether or not the subscription queue is exclusive to this message bus instance. 30 | /// 31 | public bool IsSubscriptionQueueExclusive { get; set; } = true; 32 | 33 | /// 34 | /// Whether or not the subscription queue should be automatically deleted. 35 | /// 36 | public bool SubscriptionQueueAutoDelete { get; set; } = true; 37 | 38 | /// 39 | /// The name of the subscription queue this message bus instance will listen on. 40 | /// 41 | public string SubscriptionQueueName { get; set; } = String.Empty; 42 | 43 | /// 44 | /// How messages should be acknowledged. 45 | /// 46 | public AcknowledgementStrategy AcknowledgementStrategy { get; set; } = AcknowledgementStrategy.FireAndForget; 47 | } 48 | 49 | public class RabbitMQMessageBusOptionsBuilder : SharedMessageBusOptionsBuilder 50 | { 51 | public RabbitMQMessageBusOptionsBuilder ConnectionString(string connectionString) 52 | { 53 | Target.ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); 54 | return this; 55 | } 56 | 57 | public RabbitMQMessageBusOptionsBuilder DefaultMessageTimeToLive(TimeSpan defaultMessageTimeToLive) 58 | { 59 | Target.DefaultMessageTimeToLive = defaultMessageTimeToLive; 60 | return this; 61 | } 62 | 63 | public RabbitMQMessageBusOptionsBuilder Arguments(IDictionary arguments) 64 | { 65 | Target.Arguments = arguments ?? throw new ArgumentNullException(nameof(arguments)); 66 | return this; 67 | } 68 | 69 | public RabbitMQMessageBusOptionsBuilder IsDurable(bool isDurable) 70 | { 71 | Target.IsDurable = isDurable; 72 | return this; 73 | } 74 | 75 | public RabbitMQMessageBusOptionsBuilder IsSubscriptionQueueExclusive(bool isExclusive) 76 | { 77 | Target.IsSubscriptionQueueExclusive = isExclusive; 78 | return this; 79 | } 80 | 81 | public RabbitMQMessageBusOptionsBuilder SubscriptionQueueAutoDelete(bool autoDelete) 82 | { 83 | Target.SubscriptionQueueAutoDelete = autoDelete; 84 | return this; 85 | } 86 | 87 | public RabbitMQMessageBusOptionsBuilder SubscriptionQueueName(string subscriptionQueueName) 88 | { 89 | Target.SubscriptionQueueName = subscriptionQueueName; 90 | return this; 91 | } 92 | 93 | public RabbitMQMessageBusOptionsBuilder AcknowledgementStrategy(AcknowledgementStrategy acknowledgementStrategy) 94 | { 95 | Target.AcknowledgementStrategy = acknowledgementStrategy; 96 | return this; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | False 6 | $(NoWarn);CS1591;NU1701 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/Foundatio.RabbitMQ.Tests/Foundatio.RabbitMQ.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | docker-compose.yml 8 | 9 | 10 | -------------------------------------------------------------------------------- /tests/Foundatio.RabbitMQ.Tests/Messaging/RabbitMqMessageBusDelayedExchangeTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit.Abstractions; 2 | 3 | namespace Foundatio.RabbitMQ.Tests.Messaging; 4 | 5 | public class RabbitMqMessageBusDelayedExchangeTests : RabbitMqMessageBusTestBase 6 | { 7 | public RabbitMqMessageBusDelayedExchangeTests(ITestOutputHelper output) : base("amqp://localhost:5673", output) { } 8 | } 9 | -------------------------------------------------------------------------------- /tests/Foundatio.RabbitMQ.Tests/Messaging/RabbitMqMessageBusTestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using Foundatio.AsyncEx; 5 | using Foundatio.Messaging; 6 | using Foundatio.Tests.Extensions; 7 | using Foundatio.Tests.Messaging; 8 | using Microsoft.Extensions.Logging; 9 | using Xunit; 10 | using Xunit.Abstractions; 11 | 12 | namespace Foundatio.RabbitMQ.Tests.Messaging; 13 | 14 | public abstract class RabbitMqMessageBusTestBase(string connectionString, ITestOutputHelper output) : MessageBusTestBase(output) 15 | { 16 | private readonly string _topic = $"test_topic_{DateTime.UtcNow.Ticks}"; 17 | 18 | protected override IMessageBus GetMessageBus(Func config = null) 19 | { 20 | return new RabbitMQMessageBus(o => 21 | { 22 | o.ConnectionString(connectionString); 23 | o.LoggerFactory(Log); 24 | 25 | config?.Invoke(o.Target); 26 | 27 | return o; 28 | }); 29 | } 30 | 31 | [Fact] 32 | public override Task CanUseMessageOptionsAsync() 33 | { 34 | return base.CanUseMessageOptionsAsync(); 35 | } 36 | 37 | [Fact] 38 | public override Task CanSendMessageAsync() 39 | { 40 | return base.CanSendMessageAsync(); 41 | } 42 | 43 | [Fact] 44 | public override Task CanHandleNullMessageAsync() 45 | { 46 | return base.CanHandleNullMessageAsync(); 47 | } 48 | 49 | [Fact] 50 | public override Task CanSendDerivedMessageAsync() 51 | { 52 | return base.CanSendDerivedMessageAsync(); 53 | } 54 | 55 | [Fact] 56 | public override Task CanSendMappedMessageAsync() 57 | { 58 | return base.CanSendMappedMessageAsync(); 59 | } 60 | 61 | [Fact] 62 | public override Task CanSendDelayedMessageAsync() 63 | { 64 | return base.CanSendDelayedMessageAsync(); 65 | } 66 | 67 | [Fact] 68 | public override Task CanSubscribeConcurrentlyAsync() 69 | { 70 | return base.CanSubscribeConcurrentlyAsync(); 71 | } 72 | 73 | [Fact] 74 | public override Task CanReceiveMessagesConcurrentlyAsync() 75 | { 76 | return base.CanReceiveMessagesConcurrentlyAsync(); 77 | } 78 | 79 | [Fact] 80 | public override Task CanSendMessageToMultipleSubscribersAsync() 81 | { 82 | return base.CanSendMessageToMultipleSubscribersAsync(); 83 | } 84 | 85 | [Fact] 86 | public override Task CanTolerateSubscriberFailureAsync() 87 | { 88 | return base.CanTolerateSubscriberFailureAsync(); 89 | } 90 | 91 | [Fact] 92 | public override Task WillOnlyReceiveSubscribedMessageTypeAsync() 93 | { 94 | return base.WillOnlyReceiveSubscribedMessageTypeAsync(); 95 | } 96 | 97 | [Fact] 98 | public override Task WillReceiveDerivedMessageTypesAsync() 99 | { 100 | return base.WillReceiveDerivedMessageTypesAsync(); 101 | } 102 | 103 | [Fact] 104 | public override Task CanSubscribeToAllMessageTypesAsync() 105 | { 106 | return base.CanSubscribeToAllMessageTypesAsync(); 107 | } 108 | 109 | [Fact] 110 | public override Task CanSubscribeToRawMessagesAsync() 111 | { 112 | return base.CanSubscribeToRawMessagesAsync(); 113 | } 114 | 115 | [Fact] 116 | public override Task CanCancelSubscriptionAsync() 117 | { 118 | return base.CanCancelSubscriptionAsync(); 119 | } 120 | 121 | [Fact] 122 | public override Task WontKeepMessagesWithNoSubscribersAsync() 123 | { 124 | return base.WontKeepMessagesWithNoSubscribersAsync(); 125 | } 126 | 127 | [Fact] 128 | public override Task CanReceiveFromMultipleSubscribersAsync() 129 | { 130 | return base.CanReceiveFromMultipleSubscribersAsync(); 131 | } 132 | 133 | [Fact] 134 | public override void CanDisposeWithNoSubscribersOrPublishers() 135 | { 136 | base.CanDisposeWithNoSubscribersOrPublishers(); 137 | } 138 | 139 | [Fact] 140 | public async Task CanPersistAndNotLoseMessages() 141 | { 142 | var messageBus1 = new RabbitMQMessageBus(o => o 143 | .ConnectionString(connectionString) 144 | .LoggerFactory(Log) 145 | .SubscriptionQueueName($"{_topic}-offline") 146 | .IsSubscriptionQueueExclusive(false) 147 | .SubscriptionQueueAutoDelete(false) 148 | .AcknowledgementStrategy(AcknowledgementStrategy.Automatic)); 149 | 150 | var countdownEvent = new AsyncCountdownEvent(1); 151 | var cts = new CancellationTokenSource(); 152 | await messageBus1.SubscribeAsync(msg => 153 | { 154 | _logger.LogInformation("[Subscriber1] Got message: {Message}", msg.Data); 155 | countdownEvent.Signal(); 156 | }, cts.Token); 157 | 158 | await messageBus1.PublishAsync(new SimpleMessageA { Data = "Audit message 1" }); 159 | await countdownEvent.WaitAsync(TimeSpan.FromSeconds(5)); 160 | Assert.Equal(0, countdownEvent.CurrentCount); 161 | await cts.CancelAsync(); 162 | 163 | await messageBus1.PublishAsync(new SimpleMessageA { Data = "Audit message 2" }); 164 | 165 | cts = new CancellationTokenSource(); 166 | countdownEvent.AddCount(1); 167 | await messageBus1.SubscribeAsync(msg => 168 | { 169 | _logger.LogInformation("[Subscriber2] Got message: {Message}", msg.Data); 170 | countdownEvent.Signal(); 171 | }, cts.Token); 172 | await countdownEvent.WaitAsync(TimeSpan.FromSeconds(5)); 173 | Assert.Equal(0, countdownEvent.CurrentCount); 174 | await cts.CancelAsync(); 175 | 176 | await messageBus1.PublishAsync(new SimpleMessageA { Data = "Audit offline message 1" }); 177 | await messageBus1.PublishAsync(new SimpleMessageA { Data = "Audit offline message 2" }); 178 | await messageBus1.PublishAsync(new SimpleMessageA { Data = "Audit offline message 3" }); 179 | 180 | await messageBus1.DisposeAsync(); 181 | 182 | var messageBus2 = new RabbitMQMessageBus(o => o 183 | .ConnectionString(connectionString) 184 | .LoggerFactory(Log) 185 | .SubscriptionQueueName($"{_topic}-offline") 186 | .IsSubscriptionQueueExclusive(false) 187 | .SubscriptionQueueAutoDelete(false) 188 | .AcknowledgementStrategy(AcknowledgementStrategy.Automatic)); 189 | 190 | cts = new CancellationTokenSource(); 191 | countdownEvent.AddCount(4); 192 | await messageBus2.SubscribeAsync(msg => 193 | { 194 | _logger.LogInformation("[Subscriber3] Got message: {Message}", msg.Data); 195 | countdownEvent.Signal(); 196 | }, cts.Token); 197 | await messageBus2.PublishAsync(new SimpleMessageA { Data = "Another audit message 4" }); 198 | await countdownEvent.WaitAsync(TimeSpan.FromSeconds(5)); 199 | Assert.Equal(0, countdownEvent.CurrentCount); 200 | 201 | await messageBus2.DisposeAsync(); 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /tests/Foundatio.RabbitMQ.Tests/Messaging/RabbitMqMessageBusTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit.Abstractions; 2 | 3 | namespace Foundatio.RabbitMQ.Tests.Messaging; 4 | 5 | public class RabbitMqMessageBusTests : RabbitMqMessageBusTestBase 6 | { 7 | public RabbitMqMessageBusTests(ITestOutputHelper output) : base("amqp://localhost:5672", output) { } 8 | } 9 | -------------------------------------------------------------------------------- /tests/Foundatio.RabbitMQ.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | [assembly: Xunit.CollectionBehaviorAttribute(DisableTestParallelization = true, MaxParallelThreads = 1)] 2 | --------------------------------------------------------------------------------