├── .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 |
5 |
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 | 
2 |
3 | [](https://github.com/FoundatioFx/Foundatio.RabbitMQ/actions)
4 | [](https://www.nuget.org/packages/Foundatio.RabbitMQ/)
5 | [](https://f.feedz.io/foundatio/foundatio/packages/Foundatio.RabbitMQ/latest/download)
6 | [](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 | [](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 |
--------------------------------------------------------------------------------