diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0d1a9ae --- /dev/null +++ b/.editorconfig @@ -0,0 +1,562 @@ +; EditorConfig to support per-solution formatting. +; Use the EditorConfig VS add-in to make this work. +; http://editorconfig.org/ +; +; Here are some resources for what's supported for .NET/C# +; https://kent-boogaart.com/blog/editorconfig-reference-for-c-developers +; https://learn.microsoft.com/visualstudio/ide/editorconfig-code-style-settings-reference +; +; Be **careful** editing this because some of the rules don't support adding a severity level +; For instance if you change to `dotnet_sort_system_directives_first = true:warning` (adding `:warning`) +; then the rule will be silently ignored. + +; This is the default for the codeline. +root = true + +[*] +indent_style = space +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +spelling_exclusion_path = spelling.dic + +[*.cs] +# gago - rules - begin +dotnet_style_qualification_for_field = true:Error +dotnet_style_qualification_for_property = true:Error +dotnet_style_qualification_for_method = true:Error +dotnet_style_qualification_for_event = true:Error + +dotnet_code_quality.null_check_validation_methods = Dawn.Guard.Argument(ParamType).NotNull() | Argument | NotNull + +dotnet_public_api_analyzer.require_api_files = true +# gago - rules - end + +indent_size = 4 +dotnet_sort_system_directives_first = true + + + +# use int x = .. over Int32 +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion + +# use int.MaxValue over Int32.MaxValue +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Require var all the time. +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Disallow throw expressions. +csharp_style_throw_expression = false:suggestion + +# Newline settings +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true + +# Namespace settings +csharp_style_namespace_declarations = file_scoped:silent + +# Brace settings +csharp_prefer_braces = true:silent# Prefer curly braces even for one line of code + +# name all constant fields using PascalCase +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = warning +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# static fields should have s_ prefix +dotnet_naming_rule.static_fields_should_have_prefix.severity = warning +dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields +dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected +dotnet_naming_style.static_prefix_style.required_prefix = s_ +dotnet_naming_style.static_prefix_style.capitalization = camel_case + +# internal and private fields should be _camelCase +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = warning +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal +# dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +# dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:warning +csharp_prefer_simple_using_statement = true:silent +csharp_style_prefer_method_group_conversion = true:suggestion +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = when_on_single_line:suggestion +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:warning +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_deconstructed_variable_declaration = false:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:warning +csharp_style_unused_value_expression_statement_preference = discard_variable:warning +csharp_prefer_static_local_function = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_prefer_static_anonymous_function = true:suggestion +csharp_style_prefer_readonly_struct_member = true:suggestion +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent +csharp_style_conditional_delegate_call = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_space_around_binary_operators = before_and_after + +[*.{xml,config,*proj,nuspec,props,resx,targets,yml,tasks}] +indent_size = 2 + +# Xml config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +[*.json] +indent_size = 2 + +[*.{ps1,psm1}] +indent_size = 4 + +[*.sh] +indent_size = 4 +end_of_line = lf + +[*.{razor,cshtml}] +charset = utf-8-bom + +[*.{cs,vb}] + +# SYSLIB1054: Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time +dotnet_diagnostic.SYSLIB1054.severity = warning + +# CA1018: Mark attributes with AttributeUsageAttribute +dotnet_diagnostic.CA1018.severity = warning + +# CA1047: Do not declare protected member in sealed type +dotnet_diagnostic.CA1047.severity = warning + +# CA1305: Specify IFormatProvider +dotnet_diagnostic.CA1305.severity = warning + +# CA1507: Use nameof to express symbol names +dotnet_diagnostic.CA1507.severity = warning + +# CA1510: Use ArgumentNullException throw helper +dotnet_diagnostic.CA1510.severity = warning + +# CA1511: Use ArgumentException throw helper +dotnet_diagnostic.CA1511.severity = warning + +# CA1512: Use ArgumentOutOfRangeException throw helper +dotnet_diagnostic.CA1512.severity = warning + +# CA1513: Use ObjectDisposedException throw helper +dotnet_diagnostic.CA1513.severity = warning + +# CA1725: Parameter names should match base declaration +dotnet_diagnostic.CA1725.severity = suggestion + +# CA1802: Use literals where appropriate +dotnet_diagnostic.CA1802.severity = warning + +# CA1805: Do not initialize unnecessarily +dotnet_diagnostic.CA1805.severity = warning + +# CA1810: Do not initialize unnecessarily +dotnet_diagnostic.CA1810.severity = warning + +# CA1821: Remove empty Finalizers +dotnet_diagnostic.CA1821.severity = warning + +# CA1822: Make member static +dotnet_diagnostic.CA1822.severity = warning +dotnet_code_quality.CA1822.api_surface = private, internal + +# CA1823: Avoid unused private fields +dotnet_diagnostic.CA1823.severity = warning + +# CA1825: Avoid zero-length array allocations +dotnet_diagnostic.CA1825.severity = warning + +# CA1826: Do not use Enumerable methods on indexable collections. Instead use the collection directly +dotnet_diagnostic.CA1826.severity = warning + +# CA1827: Do not use Count() or LongCount() when Any() can be used +dotnet_diagnostic.CA1827.severity = warning + +# CA1828: Do not use CountAsync() or LongCountAsync() when AnyAsync() can be used +dotnet_diagnostic.CA1828.severity = warning + +# CA1829: Use Length/Count property instead of Count() when available +dotnet_diagnostic.CA1829.severity = warning + +# CA1830: Prefer strongly-typed Append and Insert method overloads on StringBuilder +dotnet_diagnostic.CA1830.severity = warning + +# CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +dotnet_diagnostic.CA1831.severity = warning + +# CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +dotnet_diagnostic.CA1832.severity = warning + +# CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +dotnet_diagnostic.CA1833.severity = warning + +# CA1834: Consider using 'StringBuilder.Append(char)' when applicable +dotnet_diagnostic.CA1834.severity = warning + +# CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' +dotnet_diagnostic.CA1835.severity = warning + +# CA1836: Prefer IsEmpty over Count +dotnet_diagnostic.CA1836.severity = warning + +# CA1837: Use 'Environment.ProcessId' +dotnet_diagnostic.CA1837.severity = warning + +# CA1838: Avoid 'StringBuilder' parameters for P/Invokes +dotnet_diagnostic.CA1838.severity = warning + +# CA1839: Use 'Environment.ProcessPath' +dotnet_diagnostic.CA1839.severity = warning + +# CA1840: Use 'Environment.CurrentManagedThreadId' +dotnet_diagnostic.CA1840.severity = warning + +# CA1841: Prefer Dictionary.Contains methods +dotnet_diagnostic.CA1841.severity = warning + +# CA1842: Do not use 'WhenAll' with a single task +dotnet_diagnostic.CA1842.severity = warning + +# CA1843: Do not use 'WaitAll' with a single task +dotnet_diagnostic.CA1843.severity = warning + +# CA1844: Provide memory-based overrides of async methods when subclassing 'Stream' +dotnet_diagnostic.CA1844.severity = warning + +# CA1845: Use span-based 'string.Concat' +dotnet_diagnostic.CA1845.severity = warning + +# CA1846: Prefer AsSpan over Substring +dotnet_diagnostic.CA1846.severity = warning + +# CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters +dotnet_diagnostic.CA1847.severity = warning + +# CA1852: Seal internal types +dotnet_diagnostic.CA1852.severity = warning + +# CA1854: Prefer the IDictionary.TryGetValue(TKey, out TValue) method +dotnet_diagnostic.CA1854.severity = warning + +# CA1855: Prefer 'Clear' over 'Fill' +dotnet_diagnostic.CA1855.severity = warning + +# CA1856: Incorrect usage of ConstantExpected attribute +dotnet_diagnostic.CA1856.severity = error + +# CA1857: A constant is expected for the parameter +dotnet_diagnostic.CA1857.severity = warning + +# CA1858: Use 'StartsWith' instead of 'IndexOf' +dotnet_diagnostic.CA1858.severity = warning + +# CA2007: Consider calling ConfigureAwait on the awaited task +dotnet_diagnostic.CA2007.severity = warning + +# CA2008: Do not create tasks without passing a TaskScheduler +dotnet_diagnostic.CA2008.severity = warning + +# CA2009: Do not call ToImmutableCollection on an ImmutableCollection value +dotnet_diagnostic.CA2009.severity = warning + +# CA2011: Avoid infinite recursion +dotnet_diagnostic.CA2011.severity = warning + +# CA2012: Use ValueTask correctly +dotnet_diagnostic.CA2012.severity = warning + +# CA2013: Do not use ReferenceEquals with value types +dotnet_diagnostic.CA2013.severity = warning + +# CA2014: Do not use stackalloc in loops. +dotnet_diagnostic.CA2014.severity = warning + +# CA2016: Forward the 'CancellationToken' parameter to methods that take one +dotnet_diagnostic.CA2016.severity = warning + +# CA2200: Rethrow to preserve stack details +dotnet_diagnostic.CA2200.severity = warning + +# CA2201: Do not raise reserved exception types +dotnet_diagnostic.CA2201.severity = warning + +# CA2208: Instantiate argument exceptions correctly +dotnet_diagnostic.CA2208.severity = warning + +# CA2245: Do not assign a property to itself +dotnet_diagnostic.CA2245.severity = warning + +# CA2246: Assigning symbol and its member in the same statement +dotnet_diagnostic.CA2246.severity = warning + +# CA2249: Use string.Contains instead of string.IndexOf to improve readability. +dotnet_diagnostic.CA2249.severity = warning + +# IDE0005: Remove unnecessary usings +dotnet_diagnostic.IDE0005.severity = warning + +# IDE0011: Curly braces to surround blocks of code +dotnet_diagnostic.IDE0011.severity = warning + +# IDE0020: Use pattern matching to avoid is check followed by a cast (with variable) +dotnet_diagnostic.IDE0020.severity = warning + +# IDE0029: Use coalesce expression (non-nullable types) +dotnet_diagnostic.IDE0029.severity = warning + +# IDE0030: Use coalesce expression (nullable types) +dotnet_diagnostic.IDE0030.severity = warning + +# IDE0031: Use null propagation +dotnet_diagnostic.IDE0031.severity = warning + +# IDE0035: Remove unreachable code +dotnet_diagnostic.IDE0035.severity = warning + +# IDE0036: Order modifiers +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion +dotnet_diagnostic.IDE0036.severity = warning + +# IDE0038: Use pattern matching to avoid is check followed by a cast (without variable) +dotnet_diagnostic.IDE0038.severity = warning + +# IDE0043: Format string contains invalid placeholder +dotnet_diagnostic.IDE0043.severity = warning + +# IDE0044: Make field readonly +dotnet_diagnostic.IDE0044.severity = warning + +# IDE0051: Remove unused private members +dotnet_diagnostic.IDE0051.severity = warning + +# IDE0055: All formatting rules +dotnet_diagnostic.IDE0055.severity = suggestion + +# IDE0059: Unnecessary assignment to a value +dotnet_diagnostic.IDE0059.severity = warning + +# IDE0060: Remove unused parameter +dotnet_code_quality_unused_parameters = non_public:suggestion +dotnet_diagnostic.IDE0060.severity = warning + +# IDE0062: Make local function static +dotnet_diagnostic.IDE0062.severity = warning + +# IDE0073: File header +dotnet_diagnostic.IDE0073.severity = warning +file_header_template = Licensed to the .NET Foundation under one or more agreements.\nThe .NET Foundation licenses this file to you under the MIT license. + +# IDE1006: Required naming style +dotnet_diagnostic.IDE1006.severity = suggestion + +# IDE0161: Convert to file-scoped namespace +dotnet_diagnostic.IDE0161.severity = warning + +# IDE0200: Lambda expression can be removed +dotnet_diagnostic.IDE0200.severity = warning + +# IDE2000: Disallow multiple blank lines +dotnet_style_allow_multiple_blank_lines_experimental = false:silent +dotnet_diagnostic.IDE2000.severity = warning + +[{eng/tools/**.cs,**/{test,testassets,samples,Samples,perf,benchmarkapps,scripts,stress}/**.cs,src/Hosting/Server.IntegrationTesting/**.cs,src/Servers/IIS/IntegrationTesting.IIS/**.cs,src/Shared/Http2cat/**.cs,src/Testing/**.cs}] +# CA1018: Mark attributes with AttributeUsageAttribute +dotnet_diagnostic.CA1018.severity = suggestion +# CA1507: Use nameof to express symbol names +dotnet_diagnostic.CA1507.severity = suggestion +# CA1510: Use ArgumentNullException throw helper +dotnet_diagnostic.CA1510.severity = suggestion +# CA1511: Use ArgumentException throw helper +dotnet_diagnostic.CA1511.severity = suggestion +# CA1512: Use ArgumentOutOfRangeException throw helper +dotnet_diagnostic.CA1512.severity = suggestion +# CA1513: Use ObjectDisposedException throw helper +dotnet_diagnostic.CA1513.severity = suggestion +# CA1802: Use literals where appropriate +dotnet_diagnostic.CA1802.severity = suggestion +# CA1805: Do not initialize unnecessarily +dotnet_diagnostic.CA1805.severity = suggestion +# CA1810: Do not initialize unnecessarily +dotnet_diagnostic.CA1810.severity = suggestion +# CA1822: Make member static +dotnet_diagnostic.CA1822.severity = suggestion +# CA1823: Avoid zero-length array allocations +dotnet_diagnostic.CA1825.severity = suggestion +# CA1826: Do not use Enumerable methods on indexable collections. Instead use the collection directly +dotnet_diagnostic.CA1826.severity = suggestion +# CA1827: Do not use Count() or LongCount() when Any() can be used +dotnet_diagnostic.CA1827.severity = suggestion +# CA1829: Use Length/Count property instead of Count() when available +dotnet_diagnostic.CA1829.severity = suggestion +# CA1831: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +dotnet_diagnostic.CA1831.severity = suggestion +# CA1832: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +dotnet_diagnostic.CA1832.severity = suggestion +# CA1833: Use AsSpan or AsMemory instead of Range-based indexers when appropriate +dotnet_diagnostic.CA1833.severity = suggestion +# CA1834: Consider using 'StringBuilder.Append(char)' when applicable +dotnet_diagnostic.CA1834.severity = suggestion +# CA1835: Prefer the 'Memory'-based overloads for 'ReadAsync' and 'WriteAsync' +dotnet_diagnostic.CA1835.severity = suggestion +# CA1837: Use 'Environment.ProcessId' +dotnet_diagnostic.CA1837.severity = suggestion +# CA1838: Avoid 'StringBuilder' parameters for P/Invokes +dotnet_diagnostic.CA1838.severity = suggestion +# CA1841: Prefer Dictionary.Contains methods +dotnet_diagnostic.CA1841.severity = suggestion +# CA1844: Provide memory-based overrides of async methods when subclassing 'Stream' +dotnet_diagnostic.CA1844.severity = suggestion +# CA1845: Use span-based 'string.Concat' +dotnet_diagnostic.CA1845.severity = suggestion +# CA1846: Prefer AsSpan over Substring +dotnet_diagnostic.CA1846.severity = suggestion +# CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters +dotnet_diagnostic.CA1847.severity = suggestion +# CA1852: Seal internal types +dotnet_diagnostic.CA1852.severity = suggestion +# CA1854: Prefer the IDictionary.TryGetValue(TKey, out TValue) method +dotnet_diagnostic.CA1854.severity = suggestion +# CA1855: Prefer 'Clear' over 'Fill' +dotnet_diagnostic.CA1855.severity = suggestion +# CA1856: Incorrect usage of ConstantExpected attribute +dotnet_diagnostic.CA1856.severity = suggestion +# CA1857: A constant is expected for the parameter +dotnet_diagnostic.CA1857.severity = suggestion +# CA1858: Use 'StartsWith' instead of 'IndexOf' +dotnet_diagnostic.CA1858.severity = suggestion +# CA2007: Consider calling ConfigureAwait on the awaited task +dotnet_diagnostic.CA2007.severity = suggestion +# CA2008: Do not create tasks without passing a TaskScheduler +dotnet_diagnostic.CA2008.severity = suggestion +# CA2012: Use ValueTask correctly +dotnet_diagnostic.CA2012.severity = suggestion +# CA2201: Do not raise reserved exception types +dotnet_diagnostic.CA2201.severity = suggestion +# CA2249: Use string.Contains instead of string.IndexOf to improve readability. +dotnet_diagnostic.CA2249.severity = suggestion +# IDE0005: Remove unnecessary usings +dotnet_diagnostic.IDE0005.severity = suggestion +# IDE0020: Use pattern matching to avoid is check followed by a cast (with variable) +dotnet_diagnostic.IDE0020.severity = suggestion +# IDE0029: Use coalesce expression (non-nullable types) +dotnet_diagnostic.IDE0029.severity = suggestion +# IDE0030: Use coalesce expression (nullable types) +dotnet_diagnostic.IDE0030.severity = suggestion +# IDE0031: Use null propagation +dotnet_diagnostic.IDE0031.severity = suggestion +# IDE0038: Use pattern matching to avoid is check followed by a cast (without variable) +dotnet_diagnostic.IDE0038.severity = suggestion +# IDE0044: Make field readonly +dotnet_diagnostic.IDE0044.severity = suggestion +# IDE0051: Remove unused private members +dotnet_diagnostic.IDE0051.severity = suggestion +# IDE0059: Unnecessary assignment to a value +dotnet_diagnostic.IDE0059.severity = suggestion +# IDE0060: Remove unused parameters +dotnet_diagnostic.IDE0060.severity = suggestion +# IDE0062: Make local function static +dotnet_diagnostic.IDE0062.severity = suggestion +# IDE0200: Lambda expression can be removed +dotnet_diagnostic.IDE0200.severity = suggestion + +# CA2016: Forward the 'CancellationToken' parameter to methods that take one +dotnet_diagnostic.CA2016.severity = suggestion + +# Defaults for content in the shared src/ and shared runtime dir + +[{**/Shared/runtime/**.{cs,vb},src/Shared/test/Shared.Tests/runtime/**.{cs,vb},**/microsoft.extensions.hostfactoryresolver.sources/**.{cs,vb}}] +# CA1822: Make member static +dotnet_diagnostic.CA1822.severity = silent +# IDE0011: Use braces +dotnet_diagnostic.IDE0011.severity = silent +# IDE0055: Fix formatting +dotnet_diagnostic.IDE0055.severity = silent +# IDE0060: Remove unused parameters +dotnet_diagnostic.IDE0060.severity = silent +# IDE0062: Make local function static +dotnet_diagnostic.IDE0062.severity = silent +# IDE0161: Convert to file-scoped namespace +dotnet_diagnostic.IDE0161.severity = silent + +[{**/Shared/**.cs,**/microsoft.extensions.hostfactoryresolver.sources/**.{cs,vb}}] +# IDE0005: Remove unused usings. Ignore for shared src files since imports for those depend on the projects in which they are included. +dotnet_diagnostic.IDE0005.severity = silent + +[{*.razor.cs,src/Aspire.Dashboard/Components/**.cs}] +# CA2007: Consider calling ConfigureAwait on the awaited task +dotnet_diagnostic.CA2007.severity = silent +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:warning +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion +dotnet_style_namespace_match_folder = true:suggestion + +dotnet_style_readonly_field = true:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_allow_statement_immediately_after_block_experimental = true:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent + + diff --git a/.gitattributes b/.gitattributes index 1ff0c42..671f86b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,63 +1,60 @@ -############################################################################### # Set default behavior to automatically normalize line endings. -############################################################################### * text=auto -############################################################################### -# Set default behavior for command prompt diff. -# -# This is need for earlier builds of msysgit that does not have it on by -# default for csharp files. -# Note: This is only used by command line -############################################################################### -#*.cs diff=csharp +# Collapse these files in PRs by default +*.xlf linguist-generated=true +*.lcl linguist-generated=true -############################################################################### -# Set the merge driver for project and solution files -# -# Merging from the command prompt will add diff markers to the files if there -# are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following -# file extensions to fail to load in VS. An alternative would be to treat -# these files as binary and thus will always conflict and require user -# intervention with every merge. To do so, just uncomment the entries below -############################################################################### -#*.sln merge=binary -#*.csproj merge=binary -#*.vbproj merge=binary -#*.vcxproj merge=binary -#*.vcproj merge=binary -#*.dbproj merge=binary -#*.fsproj merge=binary -#*.lsproj merge=binary -#*.wixproj merge=binary -#*.modelproj merge=binary -#*.sqlproj merge=binary -#*.wwaproj merge=binary +*.jpg binary +*.png binary +*.gif binary -############################################################################### -# behavior for image files -# -# image files are treated as binary by default. -############################################################################### -#*.jpg binary -#*.png binary -#*.gif binary +# Force bash scripts to always use lf line endings so that if a repo is accessed +# in Unix via a file share from Windows, the scripts will work. +*.in text eol=lf +*.sh text eol=lf -############################################################################### -# diff behavior for common document formats -# -# Convert binary document formats to text before diffing them. This feature -# is only available from the command line. Turn it on by uncommenting the -# entries below. -############################################################################### -#*.doc diff=astextplain -#*.DOC diff=astextplain -#*.docx diff=astextplain -#*.DOCX diff=astextplain -#*.dot diff=astextplain -#*.DOT diff=astextplain -#*.pdf diff=astextplain -#*.PDF diff=astextplain -#*.rtf diff=astextplain -#*.RTF diff=astextplain +# Likewise, force cmd and batch scripts to always use crlf +*.cmd text eol=crlf +*.bat text eol=crlf + +*.cs text=auto diff=csharp +*.vb text=auto +*.resx text=auto +*.c text=auto +*.cpp text=auto +*.cxx text=auto +*.h text=auto +*.hxx text=auto +*.py text=auto +*.rb text=auto +*.java text=auto +*.html text=auto +*.htm text=auto +*.css text=auto +*.scss text=auto +*.sass text=auto +*.less text=auto +*.js text=auto +*.lisp text=auto +*.clj text=auto +*.sql text=auto +*.php text=auto +*.lua text=auto +*.m text=auto +*.asm text=auto +*.erl text=auto +*.fs text=auto +*.fsx text=auto +*.hs text=auto + +*.csproj text=auto +*.vbproj text=auto +*.fsproj text=auto +*.dbproj text=auto +*.sln text=auto eol=crlf + +# Set linguist language for .h files explicitly based on +# https://github.com/github/linguist/issues/1626#issuecomment-401442069 +# this only affects the repo's language statistics +*.h linguist-language=C diff --git a/.gitignore b/.gitignore index 9491a2f..b53c09b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,363 +1,146 @@ -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +syntax: glob + +### VisualStudio ### + +# Tools directory +.dotnet/ +.packages/ +.tools/ # User-specific files -*.rsuser *.suo *.user *.userosscache *.sln.docstates -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - # Build results + +artifacts/ +artifacts_stage_1/ [Dd]ebug/ -[Dd]ebugPublic/ [Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ +x64/ !eng/common/cross/x64/ +x86/ !eng/common/cross/x86/ [Bb]in/ [Oo]bj/ -[Oo]ut/ -[Ll]og/ -[Ll]ogs/ +msbuild.log +msbuild.err +msbuild.wrn +*.binlog -# Visual Studio 2015/2017 cache/options directory +# Visual Studio 2015 .vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ -# Visual Studio 2017 auto generated files -Generated\ Files/ +# Visual Studio 2015 Pre-CTP6 +*.sln.ide +*.ide/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* -# NUnit +#NUNIT *.VisualState.xml TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user -# TeamCity is a build add-in -_TeamCity* - # DotCover is a Code Coverage Tool *.dotCover -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*.json -coverage*.xml -coverage*.info - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - # NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets +*.nupkg +**/packages/* -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat -node_modules/ +### Windows ### -# Visual Studio 6 build log -*.plg +# Windows image file caches +Thumbs.db +ehthumbs.db -# Visual Studio 6 workspace options file -*.opt +# Folder config file +Desktop.ini -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw +# Recycle Bin used on file shares +$RECYCLE.BIN/ -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions +# Windows Installer files +*.cab +*.msi +*.msm +*.msp -# Paket dependency manager -.paket/paket.exe -paket-files/ +# Windows shortcuts +*.lnk -# FAKE - F# Make -.fake/ +### Linux ### -# CodeRush personal settings -.cr/personal +*~ -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc +# KDE directory preferences +.directory -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config +### OSX ### -# Tabs Studio -*.tss +.DS_Store +.AppleDouble +.LSOverride -# Telerik's JustMock configuration file -*.jmconfig +# Icon must end with two \r +Icon -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs +# Thumbnails +._* -# OpenCover UI analysis results -OpenCover/ +# Files that might appear on external disk +.Spotlight-V100 +.Trashes -# Azure Stream Analytics local run output -ASALocalRun/ +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk -# MSBuild Binary and Structured Log -*.binlog +# vim temporary files +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +*.un~ +Session.vim +.netrwhist +*~ + +# Visual Studio Code +.vscode/ + +# Private test configuration and binaries. +config.ps1 +**/IISApplications -# NVidia Nsight GPU debugger configuration file -*.nvuser -# MFractors (Xamarin productivity tool) working folder -.mfractor/ +# Node.js modules +node_modules/ + +# Python Compile Outputs -# Local History for Visual Studio -.localhistory/ +*.pyc -# BeatPulse healthcheck temp database -healthchecksdb +# IntelliJ +.idea/ -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ +# vscode python env files +.env -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ +# Storage emulator storage files +**/.azurite/* -# Fody - auto-generated XML schema -FodyWeavers.xsd \ No newline at end of file +# Azure Developer CLI files +/playground/**/.gitignore +/playground/**/azure.yaml +/playground/**/next-steps.md diff --git a/AspireDemo.code-workspace b/AspireDemo.code-workspace deleted file mode 100644 index 876a149..0000000 --- a/AspireDemo.code-workspace +++ /dev/null @@ -1,8 +0,0 @@ -{ - "folders": [ - { - "path": "." - } - ], - "settings": {} -} \ No newline at end of file diff --git a/GagoAspireApp.AppInit/GagoAspireApp.AppInit.csproj b/GagoAspireApp.AppInit/GagoAspireApp.AppInit.csproj deleted file mode 100644 index 9d008d8..0000000 --- a/GagoAspireApp.AppInit/GagoAspireApp.AppInit.csproj +++ /dev/null @@ -1,17 +0,0 @@ - - - - Exe - net8.0 - enable - enable - - - - - - - - - - diff --git a/GagoAspireApp.Architecture/Messaging/AMQPRemoteException.cs b/GagoAspireApp.Architecture/Messaging/AMQPRemoteException.cs deleted file mode 100644 index 902a5c1..0000000 --- a/GagoAspireApp.Architecture/Messaging/AMQPRemoteException.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace GagoAspireApp.Architecture.Messaging; - - -[Serializable] -public class AMQPRemoteException : Exception -{ - private readonly string remoteStackTrace; - - public AMQPRemoteException() : this(message: null, remoteStackTrace: null, inner: null) { } - public AMQPRemoteException(string message) : base(message) { } - public AMQPRemoteException(string message, Exception inner) : base(message, inner) { } - public AMQPRemoteException(string message, string remoteStackTrace, Exception inner) : base(message, inner) { this.remoteStackTrace = remoteStackTrace; } - - - public override string StackTrace => this.remoteStackTrace; - - - protected AMQPRemoteException( - System.Runtime.Serialization.SerializationInfo info, - System.Runtime.Serialization.StreamingContext context) : base(info, context) { } -} diff --git a/GagoAspireApp.Architecture/Messaging/Consumer/Actions/AckResult.cs b/GagoAspireApp.Architecture/Messaging/Consumer/Actions/AckResult.cs deleted file mode 100644 index e73ac7b..0000000 --- a/GagoAspireApp.Architecture/Messaging/Consumer/Actions/AckResult.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Dawn; -using RabbitMQ.Client.Events; -using RabbitMQ.Client; - -namespace GagoAspireApp.Architecture.Messaging.Consumer.Actions; - -public class AckResult : IAMQPResult -{ - public void Execute(IModel model, BasicDeliverEventArgs delivery) - { - Guard.Argument(model).NotNull(); - Guard.Argument(delivery).NotNull(); - - model.BasicAck(delivery.DeliveryTag, false); - } -} diff --git a/GagoAspireApp.Architecture/Messaging/Consumer/Actions/IAMQPResult.cs b/GagoAspireApp.Architecture/Messaging/Consumer/Actions/IAMQPResult.cs deleted file mode 100644 index 86e266f..0000000 --- a/GagoAspireApp.Architecture/Messaging/Consumer/Actions/IAMQPResult.cs +++ /dev/null @@ -1,10 +0,0 @@ -using RabbitMQ.Client; -using RabbitMQ.Client.Events; - -namespace GagoAspireApp.Architecture.Messaging.Consumer.Actions; - - -public interface IAMQPResult -{ - void Execute(IModel model, BasicDeliverEventArgs delivery); -} diff --git a/GagoAspireApp.Architecture/Messaging/Consumer/Actions/NackResult.cs b/GagoAspireApp.Architecture/Messaging/Consumer/Actions/NackResult.cs deleted file mode 100644 index f407820..0000000 --- a/GagoAspireApp.Architecture/Messaging/Consumer/Actions/NackResult.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Dawn; -using RabbitMQ.Client.Events; -using RabbitMQ.Client; - -namespace GagoAspireApp.Architecture.Messaging.Consumer.Actions; - -public class NackResult : IAMQPResult -{ - public bool Requeue { get; } - - public NackResult(bool requeue) - { - this.Requeue = requeue; - } - - public void Execute(IModel model, BasicDeliverEventArgs delivery) - { - Guard.Argument(model).NotNull(); - Guard.Argument(delivery).NotNull(); - - model.BasicNack(delivery.DeliveryTag, false, this.Requeue); - } -} diff --git a/GagoAspireApp.Architecture/Messaging/Consumer/Actions/RejectResult.cs b/GagoAspireApp.Architecture/Messaging/Consumer/Actions/RejectResult.cs deleted file mode 100644 index 64336e4..0000000 --- a/GagoAspireApp.Architecture/Messaging/Consumer/Actions/RejectResult.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Dawn; -using RabbitMQ.Client.Events; -using RabbitMQ.Client; - -namespace GagoAspireApp.Architecture.Messaging.Consumer.Actions; - -public class RejectResult : IAMQPResult -{ - public bool Requeue { get; } - - public RejectResult(bool requeue) - { - this.Requeue = requeue; - } - - public void Execute(IModel model, BasicDeliverEventArgs delivery) - { - Guard.Argument(model).NotNull(); - Guard.Argument(delivery).NotNull(); - - model.BasicReject(delivery.DeliveryTag, this.Requeue); - } -} diff --git a/GagoAspireApp.Architecture/Messaging/Consumer/AsyncQueueConsumer.cs b/GagoAspireApp.Architecture/Messaging/Consumer/AsyncQueueConsumer.cs deleted file mode 100644 index 970b90c..0000000 --- a/GagoAspireApp.Architecture/Messaging/Consumer/AsyncQueueConsumer.cs +++ /dev/null @@ -1,161 +0,0 @@ -using GagoAspireApp.Architecture.Messaging.Consumer.Actions; -using Dawn; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using RabbitMQ.Client; -using RabbitMQ.Client.Events; -using System.Diagnostics; -using OpenTelemetry.Context.Propagation; -using OpenTelemetry; -using System.Text; - -namespace GagoAspireApp.Architecture.Messaging.Consumer; - - -public class AsyncQueueConsumer : ConsumerBase - where TResponse : Task - where TRequest : class -{ - private AsyncQueueConsumerParameters parameters; - - protected static readonly ActivitySource activitySource = new(MessagingTelemetryNames.GetName(nameof(AsyncQueueConsumer))); - private static readonly TextMapPropagator propagator = Propagators.DefaultTextMapPropagator; - - #region Constructors - - public AsyncQueueConsumer(ILogger logger, AsyncQueueConsumerParameters parameters, IServiceProvider serviceProvider) - : base(logger, parameters, serviceProvider) - { - this.parameters = Guard.Argument(parameters).NotNull().Value; - this.parameters.Validate(); - } - - #endregion - - - protected override IBasicConsumer BuildConsumer() - { - Guard.Argument(this.Model).NotNull(); - - var consumer = new AsyncEventingBasicConsumer(this.Model); - - consumer.Received += this.Receive; - - return consumer; - } - - public async Task Receive(object sender, BasicDeliverEventArgs delivery) - { - Guard.Argument(delivery).NotNull(); - Guard.Argument(delivery.BasicProperties).NotNull(); - - - var parentContext = propagator.Extract(default, delivery.BasicProperties, this.ExtractTraceContextFromBasicProperties); - Baggage.Current = parentContext.Baggage; - - using Activity receiveActivity = activitySource.StartActivity("AsyncQueueConsumer.Receive", ActivityKind.Consumer, parentContext.ActivityContext) ?? new Activity("?AsyncQueueConsumer.Receive"); - - receiveActivity.AddTag("Queue", this.parameters.QueueName); - receiveActivity.AddTag("MessageId", delivery.BasicProperties.MessageId); - receiveActivity.AddTag("CorrelationId", delivery.BasicProperties.CorrelationId); - - receiveActivity.SetTag("messaging.system", "rabbitmq"); - receiveActivity.SetTag("messaging.destination_kind", "queue"); - receiveActivity.SetTag("messaging.destination", delivery.Exchange); - receiveActivity.SetTag("messaging.rabbitmq.routing_key", delivery.RoutingKey); - - IAMQPResult result = this.TryDeserialize(receiveActivity, delivery, out TRequest request) - ? await this.Dispatch(receiveActivity, delivery, request) - : new RejectResult(false); - - result.Execute(this.Model, delivery); - - //receiveActivity?.SetEndTime(DateTime.UtcNow); - } - - private IEnumerable ExtractTraceContextFromBasicProperties(IBasicProperties props, string key) - { - try - { - if (props.Headers.TryGetValue(key, out var value)) - { - var bytes = value as byte[]; - return new[] { Encoding.UTF8.GetString(bytes) }; - } - } - catch (Exception ex) - { - this.logger.LogError(ex, "Failed to extract trace context."); - } - - return Enumerable.Empty(); - } - - private bool TryDeserialize(Activity receiveActivity, BasicDeliverEventArgs receivedItem, out TRequest request) - { - Guard.Argument(receivedItem).NotNull(); - - bool returnValue = true; - - request = default; - try - { - request = this.parameters.Serializer.Deserialize(eventArgs: receivedItem); - } - catch (Exception exception) - { - returnValue = false; - - receiveActivity.SetStatus(ActivityStatusCode.Error, exception.ToString()); - - this.logger.LogWarning("Message rejected during deserialization {exception}", exception); - } - - return returnValue; - } - - protected virtual async Task Dispatch(Activity receiveActivity, BasicDeliverEventArgs receivedItem, TRequest request) - { - Guard.Argument(receiveActivity).NotNull(); - Guard.Argument(receivedItem).NotNull(); - - if (request == null) return new RejectResult(false); - - IAMQPResult returnValue; - - using Activity? dispatchActivity = activitySource.StartActivity(this.parameters.AdapterExpressionText, ActivityKind.Internal, receiveActivity.Context); - - //using (var logContext = new EnterpriseApplicationLogContext()) - //{ - try - { - TService service = this.parameters.ServiceProvider.GetRequiredService(); - - if (this.parameters.DispatchScope == DispatchScope.RootScope) - { - await this.parameters.AdapterFunc(service, request); - } - else if (this.parameters.DispatchScope == DispatchScope.ChildScope) - { - using (var scope = this.parameters.ServiceProvider.CreateScope()) - { - await this.parameters.AdapterFunc(service, request); - } - } - returnValue = new AckResult(); - } - catch (Exception exception) - { - - this.logger.LogWarning("Exception on processing message {queueName} {exception}", this.parameters.QueueName, exception); - returnValue = new NackResult(this.parameters.RequeueOnCrash); - - dispatchActivity?.SetStatus(ActivityStatusCode.Error, exception.ToString()); - } - //} - - - - return returnValue; - } -} \ No newline at end of file diff --git a/GagoAspireApp.Architecture/Messaging/Consumer/AsyncQueueConsumerParameters.cs b/GagoAspireApp.Architecture/Messaging/Consumer/AsyncQueueConsumerParameters.cs deleted file mode 100644 index 7ff1977..0000000 --- a/GagoAspireApp.Architecture/Messaging/Consumer/AsyncQueueConsumerParameters.cs +++ /dev/null @@ -1,87 +0,0 @@ -using GagoAspireApp.Architecture.Messaging.Serialization; -using Dawn; -using System.Diagnostics; -using System.Linq.Expressions; - -namespace GagoAspireApp.Architecture.Messaging.Consumer; - -public class AsyncQueueConsumerParameters : ConsumerBaseParameters - where TResponse : Task - where TRequest : class -{ - public IServiceProvider ServiceProvider { get; private set; } - public AsyncQueueConsumerParameters WithServiceProvider(IServiceProvider serviceProvider) - { - this.ServiceProvider = serviceProvider; - return this; - } - - public IAMQPSerializer Serializer { get; private set; } - public AsyncQueueConsumerParameters WithSerializer(IAMQPSerializer serializer) - { - this.Serializer = serializer; - return this; - } - - public Expression>? AdapterExpression { get; private set; } - public string? AdapterExpressionText { get; private set; } - public Func AdapterFunc { get; private set; } - public AsyncQueueConsumerParameters WithAdapter(Expression> adapterExpression) - { - this.AdapterExpression = adapterExpression; - this.AdapterFunc = adapterExpression.Compile(); - this.AdapterExpressionText = adapterExpression.ToString(); - return this; - } - - /*public AsyncQueueConsumerParameters WithEnterpriseApplicationLog(Func adapterFunc) - { - Guard.Argument(this.AdapterFunc).NotNull(); - - Func oldAdapterFunc = this.AdapterFunc; - - this.AdapterFunc = async (svc, msg) => - { - using (var logContext = new EnterpriseApplicationLogContext()) - { - //logContext.SetIdentity(nameof(DiscordSyncService.SyncAsync)); - logContext.AddArgument("msg", msg); - return await logContext.ExecuteWithLogAsync(() => oldAdapterFunc(svc, msg)); - } - }; - return this; - }*/ - - public DispatchScope DispatchScope { get; private set; } - public AsyncQueueConsumerParameters WithDispatchScope(DispatchScope dispatchScope) - { - this.DispatchScope = dispatchScope; - return this; - } - - public bool RequeueOnCrash { get; private set; } - public AsyncQueueConsumerParameters WithRequeueOnCrash(bool requeueOnCrash = true) - { - this.RequeueOnCrash = requeueOnCrash; - return this; - } - - - public AsyncQueueConsumerParameters WithDispatchInRootScope() - => this.WithDispatchScope(DispatchScope.RootScope); - - public AsyncQueueConsumerParameters WithDispatchInChildScope() - => this.WithDispatchScope(DispatchScope.ChildScope); - - - public override void Validate() - { - base.Validate(); - - Guard.Argument(this.ServiceProvider).NotNull(); - Guard.Argument(this.Serializer).NotNull(); - Guard.Argument(this.AdapterFunc).NotNull(); - Guard.Argument(this.DispatchScope).NotIn(DispatchScope.None); - } - -} diff --git a/GagoAspireApp.Architecture/Messaging/Consumer/AsyncRpcConsumer.cs b/GagoAspireApp.Architecture/Messaging/Consumer/AsyncRpcConsumer.cs deleted file mode 100644 index 5f8fadc..0000000 --- a/GagoAspireApp.Architecture/Messaging/Consumer/AsyncRpcConsumer.cs +++ /dev/null @@ -1,102 +0,0 @@ -using GagoAspireApp.Architecture.Messaging.Consumer.Actions; -using Dawn; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using RabbitMQ.Client; -using RabbitMQ.Client.Events; -using System.Diagnostics; - -namespace GagoAspireApp.Architecture.Messaging.Consumer; - - -public class AsyncRpcConsumer : AsyncQueueConsumer> - where TResponse : class - where TRequest : class -{ - private AsyncQueueConsumerParameters> parameters; - - public AsyncRpcConsumer(ILogger logger, AsyncQueueConsumerParameters> parameters, IServiceProvider serviceProvider) - : base(logger, parameters, serviceProvider) - { - this.parameters = Guard.Argument(parameters).NotNull().Value; - this.parameters.Validate(); - } - - protected override async Task Dispatch(Activity receiveActivity, BasicDeliverEventArgs receivedItem, TRequest request) - { - Guard.Argument(receivedItem).NotNull(); - Guard.Argument(receiveActivity).NotNull(); - Guard.Argument(request).NotNull(); - - if (receivedItem.BasicProperties.ReplyTo == null) - { - this.logger.LogWarning("Message cannot be processed in RPC Flow because original message didn't have a ReplyTo."); - - return new RejectResult(false); - } - - TResponse responsePayload = default; - - using (Activity? dispatchActivity = activitySource.StartActivity(this.parameters.AdapterExpressionText, ActivityKind.Internal, receiveActivity.Context)) - { - try - { - TService service = this.parameters.ServiceProvider.GetRequiredService(); - - if (this.parameters.DispatchScope == DispatchScope.RootScope) - { - responsePayload = await this.parameters.AdapterFunc(service, request); - } - else if (this.parameters.DispatchScope == DispatchScope.ChildScope) - { - using (var scope = this.parameters.ServiceProvider.CreateScope()) - { - responsePayload = await this.parameters.AdapterFunc(service, request); - } - } - } - catch (Exception exception) - { - dispatchActivity?.SetStatus(ActivityStatusCode.Error, exception.ToString()); - - this.SendReply(dispatchActivity, receivedItem, null, exception); - - return new NackResult(this.parameters.RequeueOnCrash); - } - } - - using (Activity? replyActivity = activitySource.StartActivity(this.parameters.AdapterExpressionText, ActivityKind.Internal, receiveActivity.Context)) - { - this.SendReply(replyActivity, receivedItem, responsePayload); - } - return new AckResult(); - } - - private void SendReply(Activity activity, BasicDeliverEventArgs receivedItem, TResponse responsePayload = null, Exception exception = null) - { - Guard.Argument(receivedItem).NotNull(); - Guard.Argument(responsePayload).NotNull(); - - - IBasicProperties responseProperties = this.Model.CreateBasicProperties() - .SetMessageId() - .IfFunction(it => exception != null, it => it.SetException(exception)) - .SetTelemetry(activity) - .SetCorrelationId(receivedItem.BasicProperties); - - activity?.AddTag("Queue", receivedItem.BasicProperties.ReplyTo); - activity?.AddTag("MessageId", responseProperties.MessageId); - activity?.AddTag("CorrelationId", responseProperties.CorrelationId); - - this.Model.BasicPublish(string.Empty, - receivedItem.BasicProperties.ReplyTo, - responseProperties, - exception != null - ? Array.Empty() - : this.parameters.Serializer.Serialize(basicProperties: responseProperties, objectToSerialize: responsePayload) - ); - - //replyActivity?.SetEndTime(DateTime.UtcNow); - } - -} diff --git a/GagoAspireApp.Architecture/Messaging/Consumer/ConsumerBase.cs b/GagoAspireApp.Architecture/Messaging/Consumer/ConsumerBase.cs deleted file mode 100644 index 75169d2..0000000 --- a/GagoAspireApp.Architecture/Messaging/Consumer/ConsumerBase.cs +++ /dev/null @@ -1,114 +0,0 @@ -using Dawn; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Polly; -using RabbitMQ.Client; -using RabbitMQ.Client.Exceptions; - -namespace GagoAspireApp.Architecture.Messaging.Consumer; - -public abstract class ConsumerBase : BackgroundService -{ - - protected readonly ILogger logger; - private readonly IServiceProvider serviceProvider; - protected IConnection connection; - protected IBasicConsumer consumer; - private string consumerTag; - private ConsumerBaseParameters parameters; - - protected IModel Model { get; private set; } - - - #region Constructors - - protected ConsumerBase(ILogger logger, ConsumerBaseParameters parameters, IServiceProvider serviceProvider) - { - this.logger = Guard.Argument(logger).NotNull().Value; - this.parameters = Guard.Argument(parameters).NotNull().Value; - this.parameters.Validate(); - this.serviceProvider = serviceProvider; - } - - - #endregion - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - this.connection = this.parameters.ConnectionFactoryFunc(this.serviceProvider); - - if (this.parameters.Configurer != null) - { - using var tmpModel = this.connection.CreateModel(); - this.parameters.Configurer(this.serviceProvider, tmpModel); - } - - await this.WaitQueueCreationAsync(); - - this.Model = this.connection.CreateModel(); - - this.Model.BasicQos(0, this.parameters.PrefetchCount, false); - - this.consumer = this.BuildConsumer(); - - DateTimeOffset startTime = DateTimeOffset.UtcNow; - - this.logger.LogInformation($"Consuming Queue {this.parameters.QueueName} since: {startTime}"); - - this.consumerTag = this.Model.BasicConsume( - queue: this.parameters.QueueName, - autoAck: false, - consumer: this.consumer); - - int timeToDisplay = (int)this.parameters.DisplayLoopInConsoleEvery.TotalSeconds; - - - long loopCount = 0; - while (!stoppingToken.IsCancellationRequested) - { - loopCount++; - string logMessage = $"Consuming Queue {this.parameters.QueueName} since: {startTime} uptime: {DateTimeOffset.Now - startTime}"; - - if (loopCount % timeToDisplay == 0) - this.logger.LogInformation(logMessage); - else - this.logger.LogTrace(logMessage); - - await Task.Delay(1000, stoppingToken); - } - } - - protected virtual async Task WaitQueueCreationAsync() - { - await Policy - .Handle() - .WaitAndRetryAsync(this.parameters.TestQueueRetryCount, retryAttempt => - { - var timeToWait = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)); - this.logger.LogWarning("Queue {queueName} not found... We will try in {tempo}.", this.parameters.QueueName, timeToWait); - return timeToWait; - }) - .ExecuteAsync(() => - { - using IModel testModel = this.connection.CreateModel(); - testModel.QueueDeclarePassive(this.parameters.QueueName); - return Task.CompletedTask; - }); - } - - protected abstract IBasicConsumer BuildConsumer(); - - public override void Dispose() - { - if (this.Model != null && !string.IsNullOrWhiteSpace(this.consumerTag)) - this.Model.BasicCancelNoWait(this.consumerTag); - if (this.Model != null) - { - this.Model.Dispose(); - this.Model = null; - } - - base.Dispose(); - } - -} diff --git a/GagoAspireApp.Architecture/Messaging/Consumer/ConsumerBaseParameters.cs b/GagoAspireApp.Architecture/Messaging/Consumer/ConsumerBaseParameters.cs deleted file mode 100644 index 716beec..0000000 --- a/GagoAspireApp.Architecture/Messaging/Consumer/ConsumerBaseParameters.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Dawn; -using RabbitMQ.Client; - -namespace GagoAspireApp.Architecture.Messaging.Consumer; - -public class ConsumerBaseParameters -{ - - public string QueueName { get; private set; } - public ConsumerBaseParameters WithQueueName(string queueName) - { - this.QueueName = queueName; - return this; - } - - public Action Configurer { get; private set; } - public ConsumerBaseParameters WithTopology(Action configurer) - { - this.Configurer = configurer; - return this; - } - - public ushort PrefetchCount { get; private set; } - public ConsumerBaseParameters WithPrefetchCount(ushort prefetchCount) - { - this.PrefetchCount = prefetchCount; - return this; - } - - public Func ConnectionFactoryFunc { get; private set; } - public ConsumerBaseParameters WithConnectionFactoryFunc(Func connectionFactoryFunc) - { - this.ConnectionFactoryFunc = connectionFactoryFunc; - return this; - } - - public int TestQueueRetryCount { get; private set; } - public ConsumerBaseParameters WithTestQueueRetryCount(int testQueueRetryCount) - { - this.TestQueueRetryCount = testQueueRetryCount; - return this; - } - - public TimeSpan DisplayLoopInConsoleEvery { get; private set; } - public ConsumerBaseParameters WithDisplayLoopInConsoleEvery(TimeSpan timeToDisplay) - { - this.DisplayLoopInConsoleEvery = timeToDisplay; - return this; - } - - public virtual void Validate() - { - Guard.Argument(this.QueueName).NotNull().NotEmpty().NotWhiteSpace(); - Guard.Argument(this.PrefetchCount).NotZero().NotNegative(); - Guard.Argument(this.TestQueueRetryCount).NotNegative(); - Guard.Argument(this.ConnectionFactoryFunc).NotNull(); - } - - -} diff --git a/GagoAspireApp.Architecture/Messaging/Consumer/DispatchScope.cs b/GagoAspireApp.Architecture/Messaging/Consumer/DispatchScope.cs deleted file mode 100644 index 9eadd49..0000000 --- a/GagoAspireApp.Architecture/Messaging/Consumer/DispatchScope.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace GagoAspireApp.Architecture.Messaging.Consumer; - -public enum DispatchScope -{ - None, - RootScope, - ChildScope -} diff --git a/GagoAspireApp.Architecture/Messaging/Extensions.RabbitMQ.cs b/GagoAspireApp.Architecture/Messaging/Extensions.RabbitMQ.cs deleted file mode 100644 index bac9530..0000000 --- a/GagoAspireApp.Architecture/Messaging/Extensions.RabbitMQ.cs +++ /dev/null @@ -1,204 +0,0 @@ -using Dawn; -using RabbitMQ.Client; -using System.Text; - -namespace GagoAspireApp.Architecture.Messaging; - -public static partial class RabbitMQExtensions -{ - public static IBasicProperties SetMessageId(this IBasicProperties basicProperties, string messageId = null) - { - ArgumentNullException.ThrowIfNull(basicProperties); - basicProperties.MessageId = messageId ?? Guid.NewGuid().ToString("D"); - return basicProperties; - } - - public static IBasicProperties SetCorrelationId(this IBasicProperties basicProperties, IBasicProperties originalBasicProperties) - { - ArgumentNullException.ThrowIfNull(basicProperties); - ArgumentNullException.ThrowIfNull(originalBasicProperties); - - return basicProperties.SetCorrelationId(originalBasicProperties.MessageId); - } - - public static IBasicProperties SetCorrelationId(this IBasicProperties basicProperties, string correlationId) - { - ArgumentNullException.ThrowIfNull(basicProperties); - if (string.IsNullOrEmpty(correlationId)) throw new ArgumentException($"'{nameof(correlationId)}' cannot be null or empty.", nameof(correlationId)); - - basicProperties.CorrelationId = correlationId; - return basicProperties; - } - - public static IBasicProperties SetDurable(this IBasicProperties basicProperties, bool durable = true) - { - ArgumentNullException.ThrowIfNull(basicProperties); - basicProperties.Persistent = durable; - return basicProperties; - } - - public static IBasicProperties SetReplyTo(this IBasicProperties basicProperties, string replyTo = null) - { - ArgumentNullException.ThrowIfNull(basicProperties); - - if (!string.IsNullOrEmpty(replyTo)) - basicProperties.ReplyTo = replyTo; - - return basicProperties; - } - - public static IBasicProperties SetAppId(this IBasicProperties basicProperties, string appId = null) - { - ArgumentNullException.ThrowIfNull(basicProperties); - - if (!string.IsNullOrEmpty(appId)) - basicProperties.AppId = appId; - - return basicProperties; - } - - private static string AsString(this object objectToConvert) - { - return objectToConvert != null ? Encoding.UTF8.GetString((byte[])objectToConvert) : null; - } - - public static string AsString(this IDictionary dic, string key) - { - object content = dic?[key]; - return content != null ? Encoding.UTF8.GetString((byte[])content) : null; - } - - public static List AsStringList(this object objectToConvert) - { - ArgumentNullException.ThrowIfNull(objectToConvert); - var routingKeyList = (List)objectToConvert; - - List items = routingKeyList.ConvertAll(key => key.AsString()); - - return items; - } - - public static IBasicProperties SetException(this IBasicProperties basicProperties, Exception exception) - { - ArgumentNullException.ThrowIfNull(basicProperties); - ArgumentNullException.ThrowIfNull(exception); - - if (basicProperties.Headers == null) basicProperties.Headers = new Dictionary(); - - Type exceptionType = exception.GetType(); - - basicProperties.Headers.Add("exception.type", $"{exceptionType.Namespace}.{exceptionType.Name}, {exceptionType.Assembly.FullName}"); - basicProperties.Headers.Add("exception.message", exception.Message); - basicProperties.Headers.Add("exception.stacktrace", exception.StackTrace); - - return basicProperties; - } - - - public static bool TryReconstructException(this IBasicProperties basicProperties, out AMQPRemoteException remoteException) - { - remoteException = default; - if (basicProperties?.Headers?.ContainsKey("exception.type") ?? false) - { - string exceptionTypeString = basicProperties.Headers.AsString("exception.type"); - string exceptionMessage = basicProperties.Headers.AsString("exception.message"); - string exceptionStackTrace = basicProperties.Headers.AsString("exception.stacktrace"); - var exceptionInstance = (Exception)Activator.CreateInstance(Type.GetType(exceptionTypeString) ?? typeof(Exception), exceptionMessage); - remoteException = new AMQPRemoteException("Remote consumer report a exception during execution", exceptionStackTrace, exceptionInstance); - return true; - } - return false; - } - - public static ConnectionFactory DispatchConsumersAsync(this ConnectionFactory connectionFactory, bool useAsync = true) - { - connectionFactory.DispatchConsumersAsync = useAsync; - return connectionFactory; - } - - public static ConnectionFactory Unbox(this IConnectionFactory connectionFactory) => (ConnectionFactory)connectionFactory; - - - - public static List GetDeathHeader(this IBasicProperties basicProperties) - { - return (List)basicProperties.Headers["x-death"]; - } - - public static string GetQueueName(this Dictionary xdeath) - { - return xdeath.AsString("queue"); - } - - public static string GetExchangeName(this Dictionary xdeath) - { - return xdeath.AsString("exchange"); - } - - public static List GetRoutingKeys(this Dictionary xdeath) - { - return xdeath["routing-keys"].AsStringList(); - } - - public static long Count(this Dictionary xdeath) - { - return (long)xdeath["count"]; - } - - public static T IfFunction(this T target, Func condition, Func actionWhenTrue, Func actionWhenFalse = null) - { - Guard.Argument(condition, nameof(condition)).NotNull(); - Guard.Argument(actionWhenTrue, nameof(actionWhenTrue)).NotNull(); - - if (target == null) - return target; - - bool conditionResult = condition(target); - - if (conditionResult) - target = actionWhenTrue(target); - else if (actionWhenFalse != null) - target = actionWhenFalse(target); - - return target; - } - - public static T IfAction(this T target, Func condition, Action actionWhenTrue, Action actionWhenFalse = null) - { - Guard.Argument(condition, nameof(condition)).NotNull(); - Guard.Argument(actionWhenTrue, nameof(actionWhenTrue)).NotNull(); - - if (target == null) - return target; - - bool conditionResult = condition(target); - - if (conditionResult) - actionWhenTrue(target); - else actionWhenFalse?.Invoke(target); - - return target; - } - - - public static T Fluent(this T target, Action action) - where T : class - { - Guard.Argument(target, nameof(target)).NotNull(); - Guard.Argument(action, nameof(action)).NotNull(); - - action(); - - return target; - } - - public static T Fluent(this T target, Func func) - where T : class - { - Guard.Argument(target, nameof(target)).NotNull(); - Guard.Argument(func, nameof(func)).NotNull(); - - return func(); - - } -} diff --git a/GagoAspireApp.Architecture/Messaging/Extensions.Telemetry.cs b/GagoAspireApp.Architecture/Messaging/Extensions.Telemetry.cs deleted file mode 100644 index bc72a91..0000000 --- a/GagoAspireApp.Architecture/Messaging/Extensions.Telemetry.cs +++ /dev/null @@ -1,83 +0,0 @@ -using RabbitMQ.Client; -using System.Diagnostics; -using System.Runtime.CompilerServices; - -namespace GagoAspireApp.Architecture.Messaging; - -public static partial class TelemetryExtensions -{ - - public static Activity SafeStartActivity(this ActivitySource activitySource, [CallerMemberName] string name = "", ActivityKind kind = ActivityKind.Internal) - { - ArgumentNullException.ThrowIfNull(activitySource); - Activity activity = activitySource.StartActivity(name, kind) ?? new Activity("?" + name); - activity.SetStartTime(DateTime.UtcNow); - return activity; - } - - public static Activity SafeStartActivity(this ActivitySource activitySource, string name, ActivityKind kind, ActivityContext parentContext) - { - ArgumentNullException.ThrowIfNull(activitySource); - Activity activity = activitySource.StartActivity(name, kind, parentContext) ?? new Activity("?" + name); - activity.SetStartTime(DateTime.UtcNow); - return activity; - } - - public static ActivityTraceId GetTraceId(this IBasicProperties basicProperties) - { - ArgumentNullException.ThrowIfNull(basicProperties); - return basicProperties.Headers != null && basicProperties.Headers.ContainsKey("TraceId") - ? ActivityTraceId.CreateFromString(basicProperties.Headers.AsString("TraceId")) - : default; - } - - public static ActivitySpanId GetSpanId(this IBasicProperties basicProperties) - { - ArgumentNullException.ThrowIfNull(basicProperties); - if (basicProperties.Headers != null && basicProperties.Headers.ContainsKey("SpanId")) - return ActivitySpanId.CreateFromString(basicProperties.Headers.AsString("SpanId")); - return default; - } - - public static IBasicProperties EnsureHeaders(this IBasicProperties basicProperties) - { - ArgumentNullException.ThrowIfNull(basicProperties); - basicProperties.Headers ??= new Dictionary(); - return basicProperties; - } - - public static IBasicProperties SetTelemetry(this IBasicProperties basicProperties, Activity? activity) - { - ArgumentNullException.ThrowIfNull(basicProperties); - if (activity != null) - { - basicProperties - .SetSpanId(activity.SpanId) - .SetTraceId(activity.TraceId); - } - return basicProperties; - } - - private static IBasicProperties SetTraceId(this IBasicProperties basicProperties, ActivityTraceId? activityTraceId) - { - ArgumentNullException.ThrowIfNull(basicProperties); - if (activityTraceId != null) - { - basicProperties.EnsureHeaders().Headers["TraceId"] = activityTraceId.ToString(); - } - return basicProperties; - } - - - - private static IBasicProperties SetSpanId(this IBasicProperties basicProperties, ActivitySpanId? activitySpanId) - { - ArgumentNullException.ThrowIfNull(basicProperties); - if (activitySpanId != null) - { - basicProperties.EnsureHeaders().Headers["SpanId"] = activitySpanId.ToString(); - } - return basicProperties; - } - -} diff --git a/GagoAspireApp.Architecture/Messaging/MessagingTelemetryNames.cs b/GagoAspireApp.Architecture/Messaging/MessagingTelemetryNames.cs deleted file mode 100644 index d77ffc7..0000000 --- a/GagoAspireApp.Architecture/Messaging/MessagingTelemetryNames.cs +++ /dev/null @@ -1,28 +0,0 @@ -using OpenTelemetry.Trace; - -namespace GagoAspireApp.Architecture.Messaging; -public static class MessagingTelemetryNames -{ - private static List names = [ - "gaGO.io/RabbitMQ/AsyncQueueConsumer", - "gaGO.io/RabbitMQ/AsyncRpcConsumer", - "gaGO.io/RabbitMQ/MessagePublisher" - ]; - - public static string GetName(string name) - { - string fullName = $"gaGO.io/RabbitMQ/{name}"; - return !names.Contains(fullName) - ? throw new InvalidOperationException($"Name '{name}' is not registred ") - : fullName; - } - - public static TracerProviderBuilder AddRabbitMQInstrumentation(this TracerProviderBuilder tracerProviderBuilder) - { - foreach (string name in names) - { - tracerProviderBuilder.AddSource(name); - } - return tracerProviderBuilder; - } -} diff --git a/GagoAspireApp.Architecture/Messaging/Publisher/MessagePublisher.cs b/GagoAspireApp.Architecture/Messaging/Publisher/MessagePublisher.cs deleted file mode 100644 index b8e2fca..0000000 --- a/GagoAspireApp.Architecture/Messaging/Publisher/MessagePublisher.cs +++ /dev/null @@ -1,65 +0,0 @@ -using GagoAspireApp.Architecture.Messaging.Serialization; -using OpenTelemetry.Context.Propagation; -using OpenTelemetry; -using RabbitMQ.Client; -using System.Diagnostics; -using Microsoft.Extensions.Logging; - -namespace GagoAspireApp.Architecture.Messaging.Publisher; -public class MessagePublisher(IConnection connection, IAMQPSerializer serializer, ILogger logger) -{ - private static readonly ActivitySource activitySource = new(MessagingTelemetryNames.GetName(nameof(MessagePublisher))); - private static readonly TextMapPropagator propagator = Propagators.DefaultTextMapPropagator; - - private readonly IAMQPSerializer serializer = serializer; - private readonly ILogger logger = logger; - private readonly IConnection connection = connection; - - public void Send(string exchange, string routingKey, T message) - { - using Activity publisherActivity = activitySource.StartActivity("MessagePublisher.Send", ActivityKind.Producer) ?? throw new NullReferenceException(nameof(publisherActivity)); - - using IModel model = this.connection.CreateModel(); - - var properties = model.CreateBasicProperties().EnsureHeaders().SetDurable(true); - - ActivityContext contextToInject = GetActivityContext(publisherActivity); - - // Inject the ActivityContext into the message headers to propagate trace context to the receiving service. - propagator.Inject(new PropagationContext(contextToInject, Baggage.Current), properties, this.InjectTraceContextIntoBasicProperties); - - byte[] body = this.serializer.Serialize(basicProperties: properties, objectToSerialize: message); - - model.BasicPublish(exchange, routingKey, properties, body); - - //publisherActivity?.SetEndTime(DateTime.UtcNow); - } - - private static ActivityContext GetActivityContext(Activity? activity) - { - ActivityContext contextToInject = default; - if (activity != null) - { - contextToInject = activity.Context; - } - else if (Activity.Current != null) - { - contextToInject = Activity.Current.Context; - } - return contextToInject; - } - - private void InjectTraceContextIntoBasicProperties(IBasicProperties props, string key, string value) - { - try - { - props.Headers ??= new Dictionary(); - - props.Headers[key] = value; - } - catch (Exception ex) - { - this.logger.LogError(ex, "Failed to inject trace context."); - } - } -} diff --git a/GagoAspireApp.Architecture/Messaging/Serialization/AMQPBaseSerializer.cs b/GagoAspireApp.Architecture/Messaging/Serialization/AMQPBaseSerializer.cs deleted file mode 100644 index b08bb01..0000000 --- a/GagoAspireApp.Architecture/Messaging/Serialization/AMQPBaseSerializer.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Dawn; -using RabbitMQ.Client; -using RabbitMQ.Client.Events; -using System.Diagnostics; - -namespace GagoAspireApp.Architecture.Messaging.Serialization; - -public abstract class AMQPBaseSerializer : IAMQPSerializer -{ - private readonly ActivitySource activitySource; - private readonly string name; - - public AMQPBaseSerializer(ActivitySource activitySource, string name) - { - this.activitySource = activitySource; - this.name = name; - } - - protected abstract TResponse DeserializeInternal(IBasicProperties basicProperties, ReadOnlyMemory body); - - protected abstract byte[] SerializeInternal(IBasicProperties basicProperties, T objectToSerialize); - - - public TResponse Deserialize(BasicDeliverEventArgs eventArgs) - { - Guard.Argument(eventArgs).NotNull(); - Guard.Argument(eventArgs.BasicProperties).NotNull(); - - TResponse? returnValue = this.DeserializeInternal(eventArgs.BasicProperties, eventArgs.Body); - - return returnValue; - } - - public byte[] Serialize(IBasicProperties basicProperties, T objectToSerialize) - { - Guard.Argument(basicProperties).NotNull(); - - byte[] returnValue = this.SerializeInternal(basicProperties, objectToSerialize); ; - - return returnValue; - } -} diff --git a/GagoAspireApp.Architecture/Messaging/Serialization/IAMQPSerializer.cs b/GagoAspireApp.Architecture/Messaging/Serialization/IAMQPSerializer.cs deleted file mode 100644 index 00e6a04..0000000 --- a/GagoAspireApp.Architecture/Messaging/Serialization/IAMQPSerializer.cs +++ /dev/null @@ -1,11 +0,0 @@ -using RabbitMQ.Client; -using RabbitMQ.Client.Events; - -namespace GagoAspireApp.Architecture.Messaging.Serialization; - -public interface IAMQPSerializer -{ - TResponse Deserialize(BasicDeliverEventArgs eventArgs); - - byte[] Serialize(IBasicProperties basicProperties, T objectToSerialize); -} diff --git a/GagoAspireApp.Architecture/Messaging/Serialization/NewtonsoftAMQPSerializer.cs b/GagoAspireApp.Architecture/Messaging/Serialization/NewtonsoftAMQPSerializer.cs deleted file mode 100644 index 20c30bb..0000000 --- a/GagoAspireApp.Architecture/Messaging/Serialization/NewtonsoftAMQPSerializer.cs +++ /dev/null @@ -1,23 +0,0 @@ -using RabbitMQ.Client; -using System.Diagnostics; -using System.Text; - -namespace GagoAspireApp.Architecture.Messaging.Serialization; - -public class NewtonsoftAMQPSerializer : AMQPBaseSerializer -{ - - public NewtonsoftAMQPSerializer(ActivitySource activitySource) : base(activitySource, nameof(NewtonsoftAMQPSerializer)) { } - - - protected override TResponse DeserializeInternal(IBasicProperties basicProperties, ReadOnlyMemory body) - { - string message = Encoding.UTF8.GetString(body.ToArray()); - return Newtonsoft.Json.JsonConvert.DeserializeObject(message); - } - - protected override byte[] SerializeInternal(IBasicProperties basicProperties, T objectToSerialize) - { - return Encoding.UTF8.GetBytes(Newtonsoft.Json.JsonConvert.SerializeObject(objectToSerialize)); - } -} diff --git a/GagoAspireApp.Architecture/Messaging/Serialization/SystemTextJsonAMQPSerializer.cs b/GagoAspireApp.Architecture/Messaging/Serialization/SystemTextJsonAMQPSerializer.cs deleted file mode 100644 index 630d6c3..0000000 --- a/GagoAspireApp.Architecture/Messaging/Serialization/SystemTextJsonAMQPSerializer.cs +++ /dev/null @@ -1,25 +0,0 @@ -using RabbitMQ.Client; -using System.Diagnostics; -using System.Text; - -namespace GagoAspireApp.Architecture.Messaging.Serialization; - - -public class SystemTextJsonAMQPSerializer : AMQPBaseSerializer -{ - - public SystemTextJsonAMQPSerializer(ActivitySource activitySource) : base(activitySource, nameof(SystemTextJsonAMQPSerializer)) { } - - - protected override TResponse DeserializeInternal(IBasicProperties basicProperties, ReadOnlyMemory body) - { - string message = Encoding.UTF8.GetString(body.ToArray()); - return System.Text.Json.JsonSerializer.Deserialize(message); - } - - protected override byte[] SerializeInternal(IBasicProperties basicProperties, T objectToSerialize) - { - return Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize(objectToSerialize)); - } -} - diff --git a/GagoAspireApp.BackendHost/GagoAspireApp.BackendHost.csproj b/GagoAspireApp.BackendHost/GagoAspireApp.BackendHost.csproj deleted file mode 100644 index 81f8ca7..0000000 --- a/GagoAspireApp.BackendHost/GagoAspireApp.BackendHost.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - Exe - net8.0 - enable - enable - linux-x64 - True - Linux - - - - 1701;1702;IDE0058 - - - - 1701;1702;IDE0058 - - - - - - - - - - - - diff --git a/GagoAspireApp.sln b/GagoAspireApp.sln deleted file mode 100644 index 57d39dd..0000000 --- a/GagoAspireApp.sln +++ /dev/null @@ -1,57 +0,0 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.9.34310.174 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GagoAspireApp.AppHost", "GagoAspireApp.AppHost\GagoAspireApp.AppHost.csproj", "{7C8BC069-4C61-4E00-941B-EC8F5646171C}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GagoAspireApp.BackendHost", "GagoAspireApp.BackendHost\GagoAspireApp.BackendHost.csproj", "{EABBAA34-03A9-4F90-A6E7-79C1FE10BC8B}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GagoAspireApp.FrontEndHost", "GagoAspireApp.FrontEndHost\GagoAspireApp.FrontEndHost.csproj", "{506B189A-BC29-437C-8DE8-AE3C16A533EC}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GagoAspireApp.Architecture", "GagoAspireApp.Architecture\GagoAspireApp.Architecture.csproj", "{161ACC0A-6EEE-4010-B2D3-994AD929388A}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GagoAspireApp.AppInit", "GagoAspireApp.AppInit\GagoAspireApp.AppInit.csproj", "{665FFE8C-8E67-4657-B80A-FEFC47D53022}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "hosts", "hosts", "{B3664596-7035-497F-9BAB-F60AA0269F16}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{A9EE1312-4A3C-4238-B6A4-EF2803F381B2}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {7C8BC069-4C61-4E00-941B-EC8F5646171C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7C8BC069-4C61-4E00-941B-EC8F5646171C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7C8BC069-4C61-4E00-941B-EC8F5646171C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7C8BC069-4C61-4E00-941B-EC8F5646171C}.Release|Any CPU.Build.0 = Release|Any CPU - {EABBAA34-03A9-4F90-A6E7-79C1FE10BC8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EABBAA34-03A9-4F90-A6E7-79C1FE10BC8B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EABBAA34-03A9-4F90-A6E7-79C1FE10BC8B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EABBAA34-03A9-4F90-A6E7-79C1FE10BC8B}.Release|Any CPU.Build.0 = Release|Any CPU - {506B189A-BC29-437C-8DE8-AE3C16A533EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {506B189A-BC29-437C-8DE8-AE3C16A533EC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {506B189A-BC29-437C-8DE8-AE3C16A533EC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {506B189A-BC29-437C-8DE8-AE3C16A533EC}.Release|Any CPU.Build.0 = Release|Any CPU - {161ACC0A-6EEE-4010-B2D3-994AD929388A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {161ACC0A-6EEE-4010-B2D3-994AD929388A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {161ACC0A-6EEE-4010-B2D3-994AD929388A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {161ACC0A-6EEE-4010-B2D3-994AD929388A}.Release|Any CPU.Build.0 = Release|Any CPU - {665FFE8C-8E67-4657-B80A-FEFC47D53022}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {665FFE8C-8E67-4657-B80A-FEFC47D53022}.Debug|Any CPU.Build.0 = Debug|Any CPU - {665FFE8C-8E67-4657-B80A-FEFC47D53022}.Release|Any CPU.ActiveCfg = Release|Any CPU - {665FFE8C-8E67-4657-B80A-FEFC47D53022}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(NestedProjects) = preSolution - {7C8BC069-4C61-4E00-941B-EC8F5646171C} = {B3664596-7035-497F-9BAB-F60AA0269F16} - {EABBAA34-03A9-4F90-A6E7-79C1FE10BC8B} = {B3664596-7035-497F-9BAB-F60AA0269F16} - {506B189A-BC29-437C-8DE8-AE3C16A533EC} = {B3664596-7035-497F-9BAB-F60AA0269F16} - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {15BFB62B-2566-4748-95BA-300E2503F5B5} - EndGlobalSection -EndGlobal diff --git a/Global.Build.props b/Global.Build.props new file mode 100644 index 0000000..117e812 --- /dev/null +++ b/Global.Build.props @@ -0,0 +1,46 @@ + + + + + + net8.0 + preview + + enable + enable + 0.0.1-alpha + + + + + $(MSBuildThisFileDirectory)/src/Assets/ + $(MSBuildThisFileDirectory)/tests/Shared/ + $(AssetsDir)Oragon.RabbitMQ.png + false + true + true + true + true + Open + + $([MSBuild]::NormalizeDirectory('$(ArtifactsLogDir)', 'TestLogs')) + + + + + + linux + windows + 386 + arm64 + amd64 + + + + true + + + diff --git a/Oragon.RabbitMQ.sln b/Oragon.RabbitMQ.sln new file mode 100644 index 0000000..eea1dc8 --- /dev/null +++ b/Oragon.RabbitMQ.sln @@ -0,0 +1,122 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34310.174 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oragon.RabbitMQ.MinimalConsumer", "src\Oragon.RabbitMQ.MinimalConsumer\Oragon.RabbitMQ.MinimalConsumer.csproj", "{8E87D9EC-99DD-45E4-873D-EF38F1299760}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oragon.RabbitMQ.Abstractions", "src\Oragon.RabbitMQ.Abstractions\Oragon.RabbitMQ.Abstractions.csproj", "{FED927D0-CF78-4629-8135-2DC185D61D2E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oragon.RabbitMQ", "src\Oragon.RabbitMQ\Oragon.RabbitMQ.csproj", "{93E485CD-96A0-4389-BF32-D5CF267950C8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Core", "Core", "{1E36D008-C06F-4C9A-9712-6F2B3C34B2E6}" + ProjectSection(SolutionItems) = preProject + src\Oragon.RabbitMQ.Build.props = src\Oragon.RabbitMQ.Build.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{6AAFD3EC-83DD-4D06-AD71-535673D08885}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{BA980939-3879-4FB5-98E8-563D3DFE0F45}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oragon.RabbitMQ.UnitTests", "tests\Oragon.RabbitMQ.UnitTests\Oragon.RabbitMQ.UnitTests.csproj", "{26FF5258-CF22-415C-A578-FCE3F8B02EF0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oragon.RabbitMQ.IntegratedTests", "tests\Oragon.RabbitMQ.IntegratedTests\Oragon.RabbitMQ.IntegratedTests.csproj", "{593BE77F-37B8-43EB-99C8-C37FF20AA111}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Aspire", "Aspire", "{F36972F4-70F1-4315-8E0D-CA172765FF5B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetAspire.AppHost", "samples\Aspire\DotNetAspire.AppHost\DotNetAspire.AppHost.csproj", "{3A64150C-D705-4A0A-AD5D-DE8955018095}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetAspire.AppInit", "samples\Aspire\DotNetAspire.AppInit\DotNetAspire.AppInit.csproj", "{1A369EC8-2C0C-42F7-9E37-2A3397F7D549}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetAspire.BackendHost", "samples\Aspire\DotNetAspire.BackendHost\DotNetAspire.BackendHost.csproj", "{CE5CB468-2D3F-4B8F-86E6-E72F6FDE5770}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetAspire.FrontEndHost", "samples\Aspire\DotNetAspire.FrontEndHost\DotNetAspire.FrontEndHost.csproj", "{EE9412F2-E634-43B6-9DFB-4E6FFB4FDE2B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_", "_", "{7F63D8A9-AB50-4B01-914F-E2B1563249DA}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + .gitattributes = .gitattributes + .gitignore = .gitignore + Global.Build.props = Global.Build.props + nuget.config = nuget.config + README.md = README.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oragon.RabbitMQ.Serializer.NewtonsoftJson", "src\Oragon.RabbitMQ.Serializer.NewtonsoftJson\Oragon.RabbitMQ.Serializer.NewtonsoftJson.csproj", "{539B433C-3A59-4035-ACEC-81124E60FA69}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Oragon.RabbitMQ.Serializer.SystemTextJson", "src\Oragon.RabbitMQ.Serializer.SystemTextJson\Oragon.RabbitMQ.Serializer.SystemTextJson.csproj", "{BB40F5FE-54D7-4662-9DCE-FDC2EB7995BF}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Serializers", "Serializers", "{34C26882-DAE9-4D04-8EAD-506ED87BCFE7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8E87D9EC-99DD-45E4-873D-EF38F1299760}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8E87D9EC-99DD-45E4-873D-EF38F1299760}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8E87D9EC-99DD-45E4-873D-EF38F1299760}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8E87D9EC-99DD-45E4-873D-EF38F1299760}.Release|Any CPU.Build.0 = Release|Any CPU + {FED927D0-CF78-4629-8135-2DC185D61D2E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FED927D0-CF78-4629-8135-2DC185D61D2E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FED927D0-CF78-4629-8135-2DC185D61D2E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FED927D0-CF78-4629-8135-2DC185D61D2E}.Release|Any CPU.Build.0 = Release|Any CPU + {93E485CD-96A0-4389-BF32-D5CF267950C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {93E485CD-96A0-4389-BF32-D5CF267950C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {93E485CD-96A0-4389-BF32-D5CF267950C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {93E485CD-96A0-4389-BF32-D5CF267950C8}.Release|Any CPU.Build.0 = Release|Any CPU + {26FF5258-CF22-415C-A578-FCE3F8B02EF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {26FF5258-CF22-415C-A578-FCE3F8B02EF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {26FF5258-CF22-415C-A578-FCE3F8B02EF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {26FF5258-CF22-415C-A578-FCE3F8B02EF0}.Release|Any CPU.Build.0 = Release|Any CPU + {593BE77F-37B8-43EB-99C8-C37FF20AA111}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {593BE77F-37B8-43EB-99C8-C37FF20AA111}.Debug|Any CPU.Build.0 = Debug|Any CPU + {593BE77F-37B8-43EB-99C8-C37FF20AA111}.Release|Any CPU.ActiveCfg = Release|Any CPU + {593BE77F-37B8-43EB-99C8-C37FF20AA111}.Release|Any CPU.Build.0 = Release|Any CPU + {3A64150C-D705-4A0A-AD5D-DE8955018095}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3A64150C-D705-4A0A-AD5D-DE8955018095}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3A64150C-D705-4A0A-AD5D-DE8955018095}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3A64150C-D705-4A0A-AD5D-DE8955018095}.Release|Any CPU.Build.0 = Release|Any CPU + {1A369EC8-2C0C-42F7-9E37-2A3397F7D549}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A369EC8-2C0C-42F7-9E37-2A3397F7D549}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A369EC8-2C0C-42F7-9E37-2A3397F7D549}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A369EC8-2C0C-42F7-9E37-2A3397F7D549}.Release|Any CPU.Build.0 = Release|Any CPU + {CE5CB468-2D3F-4B8F-86E6-E72F6FDE5770}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CE5CB468-2D3F-4B8F-86E6-E72F6FDE5770}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CE5CB468-2D3F-4B8F-86E6-E72F6FDE5770}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CE5CB468-2D3F-4B8F-86E6-E72F6FDE5770}.Release|Any CPU.Build.0 = Release|Any CPU + {EE9412F2-E634-43B6-9DFB-4E6FFB4FDE2B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EE9412F2-E634-43B6-9DFB-4E6FFB4FDE2B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EE9412F2-E634-43B6-9DFB-4E6FFB4FDE2B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EE9412F2-E634-43B6-9DFB-4E6FFB4FDE2B}.Release|Any CPU.Build.0 = Release|Any CPU + {539B433C-3A59-4035-ACEC-81124E60FA69}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {539B433C-3A59-4035-ACEC-81124E60FA69}.Debug|Any CPU.Build.0 = Debug|Any CPU + {539B433C-3A59-4035-ACEC-81124E60FA69}.Release|Any CPU.ActiveCfg = Release|Any CPU + {539B433C-3A59-4035-ACEC-81124E60FA69}.Release|Any CPU.Build.0 = Release|Any CPU + {BB40F5FE-54D7-4662-9DCE-FDC2EB7995BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BB40F5FE-54D7-4662-9DCE-FDC2EB7995BF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BB40F5FE-54D7-4662-9DCE-FDC2EB7995BF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BB40F5FE-54D7-4662-9DCE-FDC2EB7995BF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {8E87D9EC-99DD-45E4-873D-EF38F1299760} = {1E36D008-C06F-4C9A-9712-6F2B3C34B2E6} + {FED927D0-CF78-4629-8135-2DC185D61D2E} = {1E36D008-C06F-4C9A-9712-6F2B3C34B2E6} + {93E485CD-96A0-4389-BF32-D5CF267950C8} = {1E36D008-C06F-4C9A-9712-6F2B3C34B2E6} + {26FF5258-CF22-415C-A578-FCE3F8B02EF0} = {BA980939-3879-4FB5-98E8-563D3DFE0F45} + {593BE77F-37B8-43EB-99C8-C37FF20AA111} = {BA980939-3879-4FB5-98E8-563D3DFE0F45} + {F36972F4-70F1-4315-8E0D-CA172765FF5B} = {6AAFD3EC-83DD-4D06-AD71-535673D08885} + {3A64150C-D705-4A0A-AD5D-DE8955018095} = {F36972F4-70F1-4315-8E0D-CA172765FF5B} + {1A369EC8-2C0C-42F7-9E37-2A3397F7D549} = {F36972F4-70F1-4315-8E0D-CA172765FF5B} + {CE5CB468-2D3F-4B8F-86E6-E72F6FDE5770} = {F36972F4-70F1-4315-8E0D-CA172765FF5B} + {EE9412F2-E634-43B6-9DFB-4E6FFB4FDE2B} = {F36972F4-70F1-4315-8E0D-CA172765FF5B} + {539B433C-3A59-4035-ACEC-81124E60FA69} = {34C26882-DAE9-4D04-8EAD-506ED87BCFE7} + {BB40F5FE-54D7-4662-9DCE-FDC2EB7995BF} = {34C26882-DAE9-4D04-8EAD-506ED87BCFE7} + {34C26882-DAE9-4D04-8EAD-506ED87BCFE7} = {1E36D008-C06F-4C9A-9712-6F2B3C34B2E6} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {15BFB62B-2566-4748-95BA-300E2503F5B5} + EndGlobalSection +EndGlobal diff --git a/README.md b/README.md index ddd9c02..c4c5819 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,22 @@ -# ASPIRE / RabbitMQ Minimal API's -## Demos +# Oragon.RabbitMQ -Demonstração do uso do ASPIRE em ambiente de desenvolvimento. +An opinionated and simplest minimal APIs for consuming messages from RabbitMQ, without hidden important configurations. +## What is Oragon.RabbitMQ? +Oragon.RabbitMQ delivery anything that you need to create resilient RabbitMQ Consumers without need to understand or read many books and posts, or add unknown risks to your environment. -# RabbitMQ Minimal API's - -Uma vez que temos uma classe de negócio assim: +### If you have a service like this ```cs public class BusinessService { public async Task DoSomethingAsync(BusinessCommandOrEvent commandOrEvent) { - Console.WriteLine($"Consumer Recebeu | {commandOrEvent.ItemId}"); - - await Task.Delay(5000); + ... business core ... } } ``` -podemos conectar um método à uma fila assim: - +### You will create a RabbitMQ Consumers with this ```cs builder.Services.AddSingleton(); @@ -35,16 +31,20 @@ builder.Services.MapQueue(config => con ``` -Essa é uma abordagem projetada para desacoplar o consumidor do RabbitMQ do código de negócio, forçando com que o código de negócio não saiba que está em um contexto de consumo de filas. +# Concepts +This is an approach designed to decouple the RabbitMQ consumer from the business code, forcing the business code to not know that it is in a queue consumption context. -Essa abordagem intensionalmente remove a capacidade de utilização de notification pattern para a rejeição de mensagens, fazendo com que necessariamente seja lançada uma exceção, de tal forma que permita ao administrador da infraestrutura de observabilidade, ser notificado claramente quando os processos falham, permitindo assim a criação de issues para correção no código, ao invés de omitir e suprimir erros. +The result is absurdly simple, decoupled, agnostic, more reusable and highly testable code. -O resultado é um código absurdamente simples, desacoplado, agnóstico, mais reaproveitável e altamente testável. # RabbitMQ Tracing com OpenTelemetry -Suporte completo para OpenTelemetry na **publicação** e no **consumo** de mensagens do RabbitMQ. +Full support for OpenTelemetry on **publishing** or **consuming** RabbitMQ messages. + +# Others + +Refactored to use RabbitMQ.Client 7x (with IChannel instead IModel) diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..baeba71 --- /dev/null +++ b/nuget.config @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/GagoAspireApp.AppHost/GagoAspireApp.AppHost.csproj b/samples/Aspire/DotNetAspire.AppHost/DotNetAspire.AppHost.csproj similarity index 51% rename from GagoAspireApp.AppHost/GagoAspireApp.AppHost.csproj rename to samples/Aspire/DotNetAspire.AppHost/DotNetAspire.AppHost.csproj index 57aa073..d26fb7f 100644 --- a/GagoAspireApp.AppHost/GagoAspireApp.AppHost.csproj +++ b/samples/Aspire/DotNetAspire.AppHost/DotNetAspire.AppHost.csproj @@ -1,19 +1,17 @@ + + Exe - net8.0 - enable - enable true - latest-all - - - - + + + + diff --git a/GagoAspireApp.AppHost/Program.cs b/samples/Aspire/DotNetAspire.AppHost/Program.cs similarity index 63% rename from GagoAspireApp.AppHost/Program.cs rename to samples/Aspire/DotNetAspire.AppHost/Program.cs index 6c70447..cc50daf 100644 --- a/GagoAspireApp.AppHost/Program.cs +++ b/samples/Aspire/DotNetAspire.AppHost/Program.cs @@ -4,14 +4,14 @@ var rabbitmq = builder.AddRabbitMQContainer("rabbitmq"); -var init = builder.AddProject("init") +var init = builder.AddProject("init") .WithReference(cache); -var backend = builder.AddProject("backend") +var backend = builder.AddProject("backend") .WithReference(rabbitmq) .WithReference(init); -builder.AddProject("webfrontend") +builder.AddProject("webfrontend") .WithReference(init) .WithReference(cache) .WithReference(backend); diff --git a/GagoAspireApp.AppHost/Properties/launchSettings.json b/samples/Aspire/DotNetAspire.AppHost/Properties/launchSettings.json similarity index 100% rename from GagoAspireApp.AppHost/Properties/launchSettings.json rename to samples/Aspire/DotNetAspire.AppHost/Properties/launchSettings.json diff --git a/GagoAspireApp.AppHost/appsettings.Development.json b/samples/Aspire/DotNetAspire.AppHost/appsettings.Development.json similarity index 100% rename from GagoAspireApp.AppHost/appsettings.Development.json rename to samples/Aspire/DotNetAspire.AppHost/appsettings.Development.json diff --git a/GagoAspireApp.AppHost/appsettings.json b/samples/Aspire/DotNetAspire.AppHost/appsettings.json similarity index 100% rename from GagoAspireApp.AppHost/appsettings.json rename to samples/Aspire/DotNetAspire.AppHost/appsettings.json diff --git a/samples/Aspire/DotNetAspire.AppInit/DotNetAspire.AppInit.csproj b/samples/Aspire/DotNetAspire.AppInit/DotNetAspire.AppInit.csproj new file mode 100644 index 0000000..70b82c4 --- /dev/null +++ b/samples/Aspire/DotNetAspire.AppInit/DotNetAspire.AppInit.csproj @@ -0,0 +1,16 @@ + + + + + + Exe + + + + + + + + + + diff --git a/GagoAspireApp.AppInit/InitializerService.cs b/samples/Aspire/DotNetAspire.AppInit/InitializerService.cs similarity index 90% rename from GagoAspireApp.AppInit/InitializerService.cs rename to samples/Aspire/DotNetAspire.AppInit/InitializerService.cs index b24d3c5..ea535c4 100644 --- a/GagoAspireApp.AppInit/InitializerService.cs +++ b/samples/Aspire/DotNetAspire.AppInit/InitializerService.cs @@ -1,4 +1,4 @@ -namespace GagoAspireApp.AppInit; +namespace DotNetAspire.AppInit; public class InitializerService: IHostedService { diff --git a/GagoAspireApp.AppInit/Program.cs b/samples/Aspire/DotNetAspire.AppInit/Program.cs similarity index 83% rename from GagoAspireApp.AppInit/Program.cs rename to samples/Aspire/DotNetAspire.AppInit/Program.cs index 7a8e3d9..5bb91e2 100644 --- a/GagoAspireApp.AppInit/Program.cs +++ b/samples/Aspire/DotNetAspire.AppInit/Program.cs @@ -1,7 +1,7 @@ // See https://aka.ms/new-console-template for more information -using GagoAspireApp.AppInit; -using GagoAspireApp.Architecture.Aspire; +using DotNetAspire.AppInit; +using DotNetAspire.Architecture.Aspire; using Microsoft.AspNetCore.Builder; var builder = WebApplication.CreateBuilder(args); diff --git a/samples/Aspire/DotNetAspire.BackendHost/DotNetAspire.BackendHost.csproj b/samples/Aspire/DotNetAspire.BackendHost/DotNetAspire.BackendHost.csproj new file mode 100644 index 0000000..0f3032b --- /dev/null +++ b/samples/Aspire/DotNetAspire.BackendHost/DotNetAspire.BackendHost.csproj @@ -0,0 +1,30 @@ + + + + + + + Exe + linux-x64 + True + Linux + + + + 1701;1702;IDE0058 + + + + 1701;1702;IDE0058 + + + + + + + + + + + + diff --git a/GagoAspireApp.BackendHost/Program.cs b/samples/Aspire/DotNetAspire.BackendHost/Program.cs similarity index 92% rename from GagoAspireApp.BackendHost/Program.cs rename to samples/Aspire/DotNetAspire.BackendHost/Program.cs index a054fbd..e7156ff 100644 --- a/GagoAspireApp.BackendHost/Program.cs +++ b/samples/Aspire/DotNetAspire.BackendHost/Program.cs @@ -1,7 +1,7 @@ -using GagoAspireApp.Architecture.Aspire; -using GagoAspireApp.Architecture.Messaging; -using GagoAspireApp.Architecture.Messaging.Publisher; -using GagoAspireApp.Architecture.Messaging.Serialization; +using DotNetAspire.Architecture.Aspire; +using DotNetAspire.Architecture.Messaging; +using DotNetAspire.Architecture.Messaging.Publisher; +using DotNetAspire.Architecture.Messaging.Serialization; using Microsoft.AspNetCore.Mvc; using RabbitMQ.Client; using System.Diagnostics; diff --git a/GagoAspireApp.BackendHost/Properties/launchSettings.json b/samples/Aspire/DotNetAspire.BackendHost/Properties/launchSettings.json similarity index 100% rename from GagoAspireApp.BackendHost/Properties/launchSettings.json rename to samples/Aspire/DotNetAspire.BackendHost/Properties/launchSettings.json diff --git a/GagoAspireApp.BackendHost/appsettings.Development.json b/samples/Aspire/DotNetAspire.BackendHost/appsettings.Development.json similarity index 100% rename from GagoAspireApp.BackendHost/appsettings.Development.json rename to samples/Aspire/DotNetAspire.BackendHost/appsettings.Development.json diff --git a/GagoAspireApp.BackendHost/appsettings.json b/samples/Aspire/DotNetAspire.BackendHost/appsettings.json similarity index 100% rename from GagoAspireApp.BackendHost/appsettings.json rename to samples/Aspire/DotNetAspire.BackendHost/appsettings.json diff --git a/GagoAspireApp.FrontEndHost/BackendApiClient.cs b/samples/Aspire/DotNetAspire.FrontEndHost/BackendApiClient.cs similarity index 93% rename from GagoAspireApp.FrontEndHost/BackendApiClient.cs rename to samples/Aspire/DotNetAspire.FrontEndHost/BackendApiClient.cs index e4d9d83..8645962 100644 --- a/GagoAspireApp.FrontEndHost/BackendApiClient.cs +++ b/samples/Aspire/DotNetAspire.FrontEndHost/BackendApiClient.cs @@ -1,4 +1,4 @@ -namespace GagoAspireApp.FrontEndHost; +namespace DotNetAspire.FrontEndHost; public class BackendApiClient(HttpClient httpClient) { diff --git a/GagoAspireApp.FrontEndHost/Components/App.razor b/samples/Aspire/DotNetAspire.FrontEndHost/Components/App.razor similarity index 86% rename from GagoAspireApp.FrontEndHost/Components/App.razor rename to samples/Aspire/DotNetAspire.FrontEndHost/Components/App.razor index 83acd69..5aeba9a 100644 --- a/GagoAspireApp.FrontEndHost/Components/App.razor +++ b/samples/Aspire/DotNetAspire.FrontEndHost/Components/App.razor @@ -7,7 +7,7 @@ - + diff --git a/GagoAspireApp.FrontEndHost/Components/Layout/MainLayout.razor b/samples/Aspire/DotNetAspire.FrontEndHost/Components/Layout/MainLayout.razor similarity index 100% rename from GagoAspireApp.FrontEndHost/Components/Layout/MainLayout.razor rename to samples/Aspire/DotNetAspire.FrontEndHost/Components/Layout/MainLayout.razor diff --git a/GagoAspireApp.FrontEndHost/Components/Layout/MainLayout.razor.css b/samples/Aspire/DotNetAspire.FrontEndHost/Components/Layout/MainLayout.razor.css similarity index 100% rename from GagoAspireApp.FrontEndHost/Components/Layout/MainLayout.razor.css rename to samples/Aspire/DotNetAspire.FrontEndHost/Components/Layout/MainLayout.razor.css diff --git a/GagoAspireApp.FrontEndHost/Components/Layout/NavMenu.razor b/samples/Aspire/DotNetAspire.FrontEndHost/Components/Layout/NavMenu.razor similarity index 95% rename from GagoAspireApp.FrontEndHost/Components/Layout/NavMenu.razor rename to samples/Aspire/DotNetAspire.FrontEndHost/Components/Layout/NavMenu.razor index b5c7dcc..34ac3ca 100644 --- a/GagoAspireApp.FrontEndHost/Components/Layout/NavMenu.razor +++ b/samples/Aspire/DotNetAspire.FrontEndHost/Components/Layout/NavMenu.razor @@ -1,6 +1,6 @@  diff --git a/GagoAspireApp.FrontEndHost/Components/Layout/NavMenu.razor.css b/samples/Aspire/DotNetAspire.FrontEndHost/Components/Layout/NavMenu.razor.css similarity index 100% rename from GagoAspireApp.FrontEndHost/Components/Layout/NavMenu.razor.css rename to samples/Aspire/DotNetAspire.FrontEndHost/Components/Layout/NavMenu.razor.css diff --git a/GagoAspireApp.FrontEndHost/Components/Pages/Counter.razor b/samples/Aspire/DotNetAspire.FrontEndHost/Components/Pages/Counter.razor similarity index 100% rename from GagoAspireApp.FrontEndHost/Components/Pages/Counter.razor rename to samples/Aspire/DotNetAspire.FrontEndHost/Components/Pages/Counter.razor diff --git a/GagoAspireApp.FrontEndHost/Components/Pages/Enqueue.razor b/samples/Aspire/DotNetAspire.FrontEndHost/Components/Pages/Enqueue.razor similarity index 100% rename from GagoAspireApp.FrontEndHost/Components/Pages/Enqueue.razor rename to samples/Aspire/DotNetAspire.FrontEndHost/Components/Pages/Enqueue.razor diff --git a/GagoAspireApp.FrontEndHost/Components/Pages/Error.razor b/samples/Aspire/DotNetAspire.FrontEndHost/Components/Pages/Error.razor similarity index 100% rename from GagoAspireApp.FrontEndHost/Components/Pages/Error.razor rename to samples/Aspire/DotNetAspire.FrontEndHost/Components/Pages/Error.razor diff --git a/GagoAspireApp.FrontEndHost/Components/Pages/Home.razor b/samples/Aspire/DotNetAspire.FrontEndHost/Components/Pages/Home.razor similarity index 100% rename from GagoAspireApp.FrontEndHost/Components/Pages/Home.razor rename to samples/Aspire/DotNetAspire.FrontEndHost/Components/Pages/Home.razor diff --git a/GagoAspireApp.FrontEndHost/Components/Pages/Weather.razor b/samples/Aspire/DotNetAspire.FrontEndHost/Components/Pages/Weather.razor similarity index 100% rename from GagoAspireApp.FrontEndHost/Components/Pages/Weather.razor rename to samples/Aspire/DotNetAspire.FrontEndHost/Components/Pages/Weather.razor diff --git a/GagoAspireApp.FrontEndHost/Components/Routes.razor b/samples/Aspire/DotNetAspire.FrontEndHost/Components/Routes.razor similarity index 100% rename from GagoAspireApp.FrontEndHost/Components/Routes.razor rename to samples/Aspire/DotNetAspire.FrontEndHost/Components/Routes.razor diff --git a/GagoAspireApp.FrontEndHost/Components/_Imports.razor b/samples/Aspire/DotNetAspire.FrontEndHost/Components/_Imports.razor similarity index 82% rename from GagoAspireApp.FrontEndHost/Components/_Imports.razor rename to samples/Aspire/DotNetAspire.FrontEndHost/Components/_Imports.razor index 253dd71..57b5f38 100644 --- a/GagoAspireApp.FrontEndHost/Components/_Imports.razor +++ b/samples/Aspire/DotNetAspire.FrontEndHost/Components/_Imports.razor @@ -7,5 +7,5 @@ @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.AspNetCore.OutputCaching @using Microsoft.JSInterop -@using GagoAspireApp.FrontEndHost -@using GagoAspireApp.FrontEndHost.Components +@using DotNetAspire.FrontEndHost +@using DotNetAspire.FrontEndHost.Components diff --git a/GagoAspireApp.FrontEndHost/GagoAspireApp.FrontEndHost.csproj b/samples/Aspire/DotNetAspire.FrontEndHost/DotNetAspire.FrontEndHost.csproj similarity index 72% rename from GagoAspireApp.FrontEndHost/GagoAspireApp.FrontEndHost.csproj rename to samples/Aspire/DotNetAspire.FrontEndHost/DotNetAspire.FrontEndHost.csproj index 549cc1e..d274dca 100644 --- a/GagoAspireApp.FrontEndHost/GagoAspireApp.FrontEndHost.csproj +++ b/samples/Aspire/DotNetAspire.FrontEndHost/DotNetAspire.FrontEndHost.csproj @@ -1,10 +1,10 @@ + + + Exe - net8.0 - enable - enable @@ -21,7 +21,7 @@ - + diff --git a/GagoAspireApp.FrontEndHost/Program.cs b/samples/Aspire/DotNetAspire.FrontEndHost/Program.cs similarity index 85% rename from GagoAspireApp.FrontEndHost/Program.cs rename to samples/Aspire/DotNetAspire.FrontEndHost/Program.cs index 31583bd..de491b9 100644 --- a/GagoAspireApp.FrontEndHost/Program.cs +++ b/samples/Aspire/DotNetAspire.FrontEndHost/Program.cs @@ -1,6 +1,6 @@ -using GagoAspireApp.Architecture.Aspire; -using GagoAspireApp.FrontEndHost.Components; -using GagoAspireApp.FrontEndHost; +using DotNetAspire.Architecture.Aspire; +using DotNetAspire.FrontEndHost.Components; +using DotNetAspire.FrontEndHost; var builder = WebApplication.CreateBuilder(args); diff --git a/GagoAspireApp.FrontEndHost/Properties/launchSettings.json b/samples/Aspire/DotNetAspire.FrontEndHost/Properties/launchSettings.json similarity index 100% rename from GagoAspireApp.FrontEndHost/Properties/launchSettings.json rename to samples/Aspire/DotNetAspire.FrontEndHost/Properties/launchSettings.json diff --git a/GagoAspireApp.FrontEndHost/appsettings.Development.json b/samples/Aspire/DotNetAspire.FrontEndHost/appsettings.Development.json similarity index 100% rename from GagoAspireApp.FrontEndHost/appsettings.Development.json rename to samples/Aspire/DotNetAspire.FrontEndHost/appsettings.Development.json diff --git a/GagoAspireApp.FrontEndHost/appsettings.json b/samples/Aspire/DotNetAspire.FrontEndHost/appsettings.json similarity index 100% rename from GagoAspireApp.FrontEndHost/appsettings.json rename to samples/Aspire/DotNetAspire.FrontEndHost/appsettings.json diff --git a/GagoAspireApp.FrontEndHost/wwwroot/app.css b/samples/Aspire/DotNetAspire.FrontEndHost/wwwroot/app.css similarity index 100% rename from GagoAspireApp.FrontEndHost/wwwroot/app.css rename to samples/Aspire/DotNetAspire.FrontEndHost/wwwroot/app.css diff --git a/GagoAspireApp.FrontEndHost/wwwroot/bootstrap/bootstrap.min.css b/samples/Aspire/DotNetAspire.FrontEndHost/wwwroot/bootstrap/bootstrap.min.css similarity index 100% rename from GagoAspireApp.FrontEndHost/wwwroot/bootstrap/bootstrap.min.css rename to samples/Aspire/DotNetAspire.FrontEndHost/wwwroot/bootstrap/bootstrap.min.css diff --git a/GagoAspireApp.FrontEndHost/wwwroot/bootstrap/bootstrap.min.css.map b/samples/Aspire/DotNetAspire.FrontEndHost/wwwroot/bootstrap/bootstrap.min.css.map similarity index 100% rename from GagoAspireApp.FrontEndHost/wwwroot/bootstrap/bootstrap.min.css.map rename to samples/Aspire/DotNetAspire.FrontEndHost/wwwroot/bootstrap/bootstrap.min.css.map diff --git a/GagoAspireApp.FrontEndHost/wwwroot/favicon.png b/samples/Aspire/DotNetAspire.FrontEndHost/wwwroot/favicon.png similarity index 100% rename from GagoAspireApp.FrontEndHost/wwwroot/favicon.png rename to samples/Aspire/DotNetAspire.FrontEndHost/wwwroot/favicon.png diff --git a/src/Assets/Oragon-Architecture-Penknife.export.png b/src/Assets/Oragon-Architecture-Penknife.export.png new file mode 100644 index 0000000..8f7f793 Binary files /dev/null and b/src/Assets/Oragon-Architecture-Penknife.export.png differ diff --git a/src/Assets/Oragon-Architecture-Penknife.webp b/src/Assets/Oragon-Architecture-Penknife.webp new file mode 100644 index 0000000..6192dc8 Binary files /dev/null and b/src/Assets/Oragon-Architecture-Penknife.webp differ diff --git a/src/Assets/Oragon.RabbitMQ.fw.png b/src/Assets/Oragon.RabbitMQ.fw.png new file mode 100644 index 0000000..422b2a6 Binary files /dev/null and b/src/Assets/Oragon.RabbitMQ.fw.png differ diff --git a/src/Assets/Oragon.RabbitMQ.png b/src/Assets/Oragon.RabbitMQ.png new file mode 100644 index 0000000..d55c30d Binary files /dev/null and b/src/Assets/Oragon.RabbitMQ.png differ diff --git a/src/Oragon.RabbitMQ.Abstractions/AMQPRemoteException.cs b/src/Oragon.RabbitMQ.Abstractions/AMQPRemoteException.cs new file mode 100644 index 0000000..e70216a --- /dev/null +++ b/src/Oragon.RabbitMQ.Abstractions/AMQPRemoteException.cs @@ -0,0 +1,46 @@ +namespace Oragon.RabbitMQ; + + + +/// +/// Represents errors that occur during remote operations. +/// +[Serializable] +public class AMQPRemoteException : Exception +{ + private readonly string remoteStackTrace; + + /// + /// Initializes a new instance of the class. + /// + public AMQPRemoteException() : this(message: null, remoteStackTrace: null, inner: null) { } + + /// + /// Initializes a new instance of the class with a specified error message. + /// + /// The error message that explains the reason for the exception. + public AMQPRemoteException(string message) : base(message) { } + + /// + /// Initializes a new instance of the class with a specified error message and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public AMQPRemoteException(string message, Exception inner) : base(message, inner) { } + + /// + /// Initializes a new instance of the class with a specified error message, a reference to the remote stack trace, and a reference to the inner exception that is the cause of this exception. + /// + /// The error message that explains the reason for the exception. + /// The remote stack trace. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public AMQPRemoteException(string message, string remoteStackTrace, Exception inner) : base(message, inner) { + this.remoteStackTrace = remoteStackTrace; + } + + /// + /// Gets the string representation of the frames on the call stack at the time the current exception was thrown. + /// + public override string StackTrace => remoteStackTrace; + +} diff --git a/src/Oragon.RabbitMQ.Abstractions/Consumer/Actions/AckResult.cs b/src/Oragon.RabbitMQ.Abstractions/Consumer/Actions/AckResult.cs new file mode 100644 index 0000000..80bee64 --- /dev/null +++ b/src/Oragon.RabbitMQ.Abstractions/Consumer/Actions/AckResult.cs @@ -0,0 +1,31 @@ +using Dawn; +using RabbitMQ.Client.Events; +using RabbitMQ.Client; + +namespace Oragon.RabbitMQ.Consumer.Actions; + +/// +/// Acknowledges the message +/// +public class AckResult : IAMQPResult +{ + /// + /// Create a instance of AckResult + /// + public AckResult() { } + + + /// + /// Perform ack on channel + /// + /// + /// + /// + public async Task ExecuteAsync(IChannel channel, BasicDeliverEventArgs delivery) + { + _ = Guard.Argument(channel).NotNull(); + _ = Guard.Argument(delivery).NotNull(); + + await channel.BasicAckAsync(delivery.DeliveryTag, false).ConfigureAwait(true); + } +} diff --git a/src/Oragon.RabbitMQ.Abstractions/Consumer/Actions/IAMQPResult.cs b/src/Oragon.RabbitMQ.Abstractions/Consumer/Actions/IAMQPResult.cs new file mode 100644 index 0000000..cea5acf --- /dev/null +++ b/src/Oragon.RabbitMQ.Abstractions/Consumer/Actions/IAMQPResult.cs @@ -0,0 +1,19 @@ +using RabbitMQ.Client.Events; +using RabbitMQ.Client; + +namespace Oragon.RabbitMQ.Consumer.Actions; + + +/// +/// Represents a AMQP result that can be executed by the consumer after a message is processed. +/// +public interface IAMQPResult +{ + + /// + /// Executes the result. + /// + /// + /// + Task ExecuteAsync(IChannel channel, BasicDeliverEventArgs delivery); +} diff --git a/src/Oragon.RabbitMQ.Abstractions/Consumer/Actions/NackResult.cs b/src/Oragon.RabbitMQ.Abstractions/Consumer/Actions/NackResult.cs new file mode 100644 index 0000000..9718476 --- /dev/null +++ b/src/Oragon.RabbitMQ.Abstractions/Consumer/Actions/NackResult.cs @@ -0,0 +1,40 @@ +using Dawn; +using RabbitMQ.Client.Events; +using RabbitMQ.Client; + +namespace Oragon.RabbitMQ.Consumer.Actions; + +/// +/// Nack the message +/// +public class NackResult : IAMQPResult +{ + + /// + /// Indicates if the message should be requeued + /// + public bool Requeue { get; } + + /// + /// Creates a new instance of + /// + /// + public NackResult(bool requeue) + { + this.Requeue = requeue; + } + + /// + /// Perform nack on channel + /// + /// + /// + /// + public async Task ExecuteAsync(IChannel channel, BasicDeliverEventArgs delivery) + { + _ = Guard.Argument(channel).NotNull(); + _ = Guard.Argument(delivery).NotNull(); + + await channel.BasicNackAsync(delivery.DeliveryTag, false, this.Requeue).ConfigureAwait(true); + } +} diff --git a/src/Oragon.RabbitMQ.Abstractions/Consumer/Actions/RejectResult.cs b/src/Oragon.RabbitMQ.Abstractions/Consumer/Actions/RejectResult.cs new file mode 100644 index 0000000..7d7b2b4 --- /dev/null +++ b/src/Oragon.RabbitMQ.Abstractions/Consumer/Actions/RejectResult.cs @@ -0,0 +1,38 @@ +using Dawn; +using RabbitMQ.Client.Events; +using RabbitMQ.Client; + +namespace Oragon.RabbitMQ.Consumer.Actions; + +/// +/// +/// +public class RejectResult : IAMQPResult +{ + /// + /// + /// + public bool Requeue { get; } + + /// + /// + /// + /// + public RejectResult(bool requeue) + { + this.Requeue = requeue; + } + + /// + /// Perform reject on channel + /// + /// + /// + public async Task ExecuteAsync(IChannel channel, BasicDeliverEventArgs delivery) + { + _ = Guard.Argument(channel).NotNull(); + _ = Guard.Argument(delivery).NotNull(); + + await channel.BasicRejectAsync(delivery.DeliveryTag, this.Requeue).ConfigureAwait(true); + } +} diff --git a/src/Oragon.RabbitMQ.Abstractions/Oragon.RabbitMQ.Abstractions.csproj b/src/Oragon.RabbitMQ.Abstractions/Oragon.RabbitMQ.Abstractions.csproj new file mode 100644 index 0000000..aad0483 --- /dev/null +++ b/src/Oragon.RabbitMQ.Abstractions/Oragon.RabbitMQ.Abstractions.csproj @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/Oragon.RabbitMQ.Abstractions/Serialization/IAMQPSerializer.cs b/src/Oragon.RabbitMQ.Abstractions/Serialization/IAMQPSerializer.cs new file mode 100644 index 0000000..c22f0ba --- /dev/null +++ b/src/Oragon.RabbitMQ.Abstractions/Serialization/IAMQPSerializer.cs @@ -0,0 +1,28 @@ +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace Oragon.RabbitMQ.Serialization; + +/// +/// Define a implementation of a serializer for AMQP +/// +public interface IAMQPSerializer +{ + /// + /// Desserialize a mesage from a BasicDeliverEventArgs + /// + /// + /// + /// + TMessage Deserialize(BasicDeliverEventArgs eventArgs); + + + /// + /// Serialize a message to a byte array + /// + /// + /// + /// + /// + byte[] Serialize(BasicProperties basicProperties, T message); +} diff --git a/src/Oragon.RabbitMQ.Build.props b/src/Oragon.RabbitMQ.Build.props new file mode 100644 index 0000000..1971d81 --- /dev/null +++ b/src/Oragon.RabbitMQ.Build.props @@ -0,0 +1,53 @@ + + + + + + + disable + embedded + true + preview + + + + + Oragon.RabbitMQ + true + latest-all + + + + + + True + README.md + Oragon.RabbitMQ.png + + © Oragon, gaGO.io, LuizCarlosFaria. All rights reserved. + MIT + https://github.com/luizcarlosfaria/Oragon.RabbitMQ + LuizCarlosFaria + oragon.io + Oragon + oragon architecture luizcarlosfaria rabbitmq + Library + + + + + True + \ + + + True + \ + + + + + + + + + diff --git a/GagoAspireApp.Architecture/Aspire/Extensions.cs b/src/Oragon.RabbitMQ.MinimalConsumer/Aspire/Extensions.cs similarity index 97% rename from GagoAspireApp.Architecture/Aspire/Extensions.cs rename to src/Oragon.RabbitMQ.MinimalConsumer/Aspire/Extensions.cs index ff22abc..64b650d 100644 --- a/GagoAspireApp.Architecture/Aspire/Extensions.cs +++ b/src/Oragon.RabbitMQ.MinimalConsumer/Aspire/Extensions.cs @@ -1,5 +1,4 @@ -using GagoAspireApp.Architecture.Messaging; -using Microsoft.AspNetCore.Builder; +/*using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -9,7 +8,8 @@ using OpenTelemetry.Metrics; using OpenTelemetry.Trace; -namespace GagoAspireApp.Architecture.Aspire; + +namespace Oragon.RabbitMQ.Aspire; public static class Extensions { @@ -129,3 +129,4 @@ private static MeterProviderBuilder AddBuiltInMeters(this MeterProviderBuilder m "Microsoft.AspNetCore.Server.Kestrel", "System.Net.Http"); } +*/ \ No newline at end of file diff --git a/GagoAspireApp.Architecture/GagoAspireApp.Architecture.csproj b/src/Oragon.RabbitMQ.MinimalConsumer/Oragon.RabbitMQ.MinimalConsumer.csproj similarity index 53% rename from GagoAspireApp.Architecture/GagoAspireApp.Architecture.csproj rename to src/Oragon.RabbitMQ.MinimalConsumer/Oragon.RabbitMQ.MinimalConsumer.csproj index 7b68c1e..bc66ff7 100644 --- a/GagoAspireApp.Architecture/GagoAspireApp.Architecture.csproj +++ b/src/Oragon.RabbitMQ.MinimalConsumer/Oragon.RabbitMQ.MinimalConsumer.csproj @@ -1,11 +1,6 @@ - + - - net8.0 - enable - disable - true - + 1701;1702;IDE0058 @@ -14,30 +9,33 @@ 1701;1702;IDE0058 + - - - - + + + + - - - - - + + + + + + + + + + + + + - - - - - - - - - - + + + + diff --git a/src/Oragon.RabbitMQ.MinimalConsumer/Publisher/MessagePublisher.cs b/src/Oragon.RabbitMQ.MinimalConsumer/Publisher/MessagePublisher.cs new file mode 100644 index 0000000..f6bb3ba --- /dev/null +++ b/src/Oragon.RabbitMQ.MinimalConsumer/Publisher/MessagePublisher.cs @@ -0,0 +1,87 @@ +using OpenTelemetry.Context.Propagation; +using OpenTelemetry; +using RabbitMQ.Client; +using System.Diagnostics; +using Microsoft.Extensions.Logging; +using Oragon.RabbitMQ.Serialization; +using System.Diagnostics.CodeAnalysis; + +namespace Oragon.RabbitMQ.Publisher; + + +/// +/// Basic publisher for RabbitMQ. +/// +/// +/// +/// +public class MessagePublisher(IConnection connection, IAMQPSerializer serializer, ILogger logger) +{ + private static readonly ActivitySource s_activitySource = new(MessagingTelemetryNames.GetName(nameof(MessagePublisher))); + private static readonly TextMapPropagator s_propagator = Propagators.DefaultTextMapPropagator; + + private readonly IAMQPSerializer serializer = serializer; + private readonly ILogger logger = logger; + private readonly IConnection connection = connection; + + /// + /// Send a message to the RabbitMQ. + /// + /// + /// + /// + /// + /// + /// + [SuppressMessage("Usage", "CA2201", Justification = "Do not raise reserved exception types")] + public async Task SendAsync(string exchange, string routingKey, T message) + { + using Activity publisherActivity = s_activitySource.StartActivity("MessagePublisher.SendAsync", ActivityKind.Producer) ?? throw new NullReferenceException(nameof(publisherActivity)); + + using IChannel model = await connection.CreateChannelAsync().ConfigureAwait(true); + + var properties = model.CreateBasicProperties().EnsureHeaders().SetDurable(true); + + var contextToInject = GetActivityContext(publisherActivity); + + // Inject the ActivityContext into the message headers to propagate trace context to the receiving service. + s_propagator.Inject(new PropagationContext(contextToInject, Baggage.Current), properties, InjectTraceContextIntoBasicProperties); + + var body = serializer.Serialize(basicProperties: properties, message: message); + + await model.BasicPublishAsync(exchange, routingKey, properties, body).ConfigureAwait(true); + + //publisherActivity?.SetEndTime(DateTime.UtcNow); + } + + private static ActivityContext GetActivityContext(Activity activity) + { + ActivityContext contextToInject = default; + if (activity != null) + { + contextToInject = activity.Context; + } + else if (Activity.Current != null) + { + contextToInject = Activity.Current.Context; + } + return contextToInject; + } + + [SuppressMessage("Performance", "CA1848", Justification = "Use the LoggerMessage delegates")] + [SuppressMessage("Performance", "CA2254", Justification = "Template should be a static expression")] + [SuppressMessage("Design", "CA1031", Justification = "Do not catch general exception types")] + private void InjectTraceContextIntoBasicProperties(BasicProperties props, string key, string value) + { + try + { + props.Headers ??= new Dictionary(); + + props.Headers[key] = value; + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to inject trace context."); + } + } +} diff --git a/src/Oragon.RabbitMQ.Serializer.NewtonsoftJson/Oragon.RabbitMQ.Serializer.NewtonsoftJson.csproj b/src/Oragon.RabbitMQ.Serializer.NewtonsoftJson/Oragon.RabbitMQ.Serializer.NewtonsoftJson.csproj new file mode 100644 index 0000000..fdd64c6 --- /dev/null +++ b/src/Oragon.RabbitMQ.Serializer.NewtonsoftJson/Oragon.RabbitMQ.Serializer.NewtonsoftJson.csproj @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/src/Oragon.RabbitMQ.Serializer.NewtonsoftJson/Serialization/NewtonsoftAMQPSerializer.cs b/src/Oragon.RabbitMQ.Serializer.NewtonsoftJson/Serialization/NewtonsoftAMQPSerializer.cs new file mode 100644 index 0000000..199e990 --- /dev/null +++ b/src/Oragon.RabbitMQ.Serializer.NewtonsoftJson/Serialization/NewtonsoftAMQPSerializer.cs @@ -0,0 +1,54 @@ +using Dawn; +using RabbitMQ.Client; +using System.Diagnostics; +using System.Text; + +namespace Oragon.RabbitMQ.Serialization; + +/// +/// Implements serialization using Newtonsoft.Json +/// +public class NewtonsoftAMQPSerializer : AMQPBaseSerializer +{ + + /// + /// Create a instance of NewtonsoftAMQPSerializer + /// + /// + public NewtonsoftAMQPSerializer(ActivitySource activitySource) : base(activitySource, nameof(NewtonsoftAMQPSerializer)) { } + + /// + /// Implement deserialization using Newtonsoft.Json + /// + /// + /// + /// + /// + protected override TMessage DeserializeInternal(IReadOnlyBasicProperties basicProperties, ReadOnlyMemory body) + { + _ = Guard.Argument(basicProperties).NotNull(); + + var bytes = body.ToArray(); + if (bytes.Length > 0) + { + var message = Encoding.UTF8.GetString(bytes); + if (!string.IsNullOrWhiteSpace(message)) + { + return Newtonsoft.Json.JsonConvert.DeserializeObject(message); + } + } + return default; + } + + /// + /// Implement Serialization with Newtonsoft.Json + /// + /// + /// + /// + /// + protected override byte[] SerializeInternal(BasicProperties basicProperties, T objectToSerialize) + { + return Encoding.UTF8.GetBytes(Newtonsoft.Json.JsonConvert.SerializeObject(objectToSerialize)); + } +} diff --git a/src/Oragon.RabbitMQ.Serializer.SystemTextJson/Oragon.RabbitMQ.Serializer.SystemTextJson.csproj b/src/Oragon.RabbitMQ.Serializer.SystemTextJson/Oragon.RabbitMQ.Serializer.SystemTextJson.csproj new file mode 100644 index 0000000..13e460b --- /dev/null +++ b/src/Oragon.RabbitMQ.Serializer.SystemTextJson/Oragon.RabbitMQ.Serializer.SystemTextJson.csproj @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/Oragon.RabbitMQ.Serializer.SystemTextJson/Serialization/SystemTextJsonAMQPSerializer.cs b/src/Oragon.RabbitMQ.Serializer.SystemTextJson/Serialization/SystemTextJsonAMQPSerializer.cs new file mode 100644 index 0000000..8cf26c0 --- /dev/null +++ b/src/Oragon.RabbitMQ.Serializer.SystemTextJson/Serialization/SystemTextJsonAMQPSerializer.cs @@ -0,0 +1,55 @@ +using RabbitMQ.Client; +using System.Diagnostics; +using System.Text; + +namespace Oragon.RabbitMQ.Serialization; + + +/// +/// Implements serialization using System.Text.Json +/// +public class SystemTextJsonAMQPSerializer : AMQPBaseSerializer +{ + + /// + /// Create a instance of SystemTextJsonAMQPSerializer + /// + /// + public SystemTextJsonAMQPSerializer(ActivitySource activitySource) : base(activitySource, nameof(SystemTextJsonAMQPSerializer)) { } + + + /// + /// Deserialize a message using System.Text.Json + /// + /// + /// + /// + /// + protected override TMessage DeserializeInternal(IReadOnlyBasicProperties basicProperties, ReadOnlyMemory body) + { + var bytes = body.ToArray(); + if (bytes.Length > 0) + { + var message = Encoding.UTF8.GetString(bytes); + if (!string.IsNullOrWhiteSpace(message)) + { + return System.Text.Json.JsonSerializer.Deserialize(message); + } + } + return default; + + } + + /// + /// Serialize a message using System.Text.Json + /// + /// + /// + /// + /// + protected override byte[] SerializeInternal(BasicProperties basicProperties, TMessage message) + { + return Encoding.UTF8.GetBytes(System.Text.Json.JsonSerializer.Serialize(message)); + } +} + diff --git a/src/Oragon.RabbitMQ/Consumer/AsyncQueueConsumer.cs b/src/Oragon.RabbitMQ/Consumer/AsyncQueueConsumer.cs new file mode 100644 index 0000000..f5a3f7a --- /dev/null +++ b/src/Oragon.RabbitMQ/Consumer/AsyncQueueConsumer.cs @@ -0,0 +1,222 @@ +using Dawn; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using System.Diagnostics; +using OpenTelemetry.Context.Propagation; +using OpenTelemetry; +using System.Text; +using Oragon.RabbitMQ.Consumer.Actions; +using System.Diagnostics.CodeAnalysis; + + +namespace Oragon.RabbitMQ.Consumer; + + + +/// +/// Represents an asynchronous queue Consumer. +/// +/// The type of the service. +/// The type of the request. +/// The type of the response. +public class AsyncQueueConsumer : ConsumerBase + where TResponse : Task + where TRequest : class +{ + /// + /// The parameters for the Consumer. + /// + private readonly AsyncQueueConsumerParameters parameters; + + /// + /// The activity source for telemetry. + /// + protected static readonly ActivitySource activitySource = new(MessagingTelemetryNames.GetName(nameof(AsyncQueueConsumer))); + + /// + /// The propagator for trace context. + /// + private static readonly TextMapPropagator s_propagator = Propagators.DefaultTextMapPropagator; + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The Logger. + /// The parameters. + /// The service provider. + public AsyncQueueConsumer(ILogger logger, AsyncQueueConsumerParameters parameters, IServiceProvider serviceProvider) + : base(logger, parameters, serviceProvider) + { + this.parameters = Guard.Argument(parameters).NotNull().Value; + this.parameters.Validate(); + } + + #endregion + + + /// + protected override IBasicConsumer BuildConsumer() + { + _ = Guard.Argument(Channel).NotNull(); + + var consumer = new AsyncEventingBasicConsumer(Channel); + + consumer.Received += ReceiveAsync; + + return consumer; + } + + + /// + /// Handles the asynchronous receive of a message. + /// + /// The sender. + /// The delivery arguments. + [SuppressMessage("Performance", "CA1859", Justification = "Utilização de IAMQPResult é necessária pois ou temos uma resposta do DispatchAsync que pode ser de vários tipos, ou temos uma resposta com RejectResult")] + public async Task ReceiveAsync(object sender, BasicDeliverEventArgs delivery) + { + _ = Guard.Argument(delivery).NotNull(); + + var parentContext = s_propagator.Extract(default, (IReadOnlyBasicProperties)delivery.BasicProperties, this.ExtractTraceContextFromBasicProperties); + Baggage.Current = parentContext.Baggage; + + using var receiveActivity = activitySource.StartActivity("AsyncQueueConsumer.ReceiveAsync", ActivityKind.Consumer, parentContext.ActivityContext) ?? new Activity("?AsyncQueueConsumer.ReceiveAsync"); + + _ = receiveActivity.AddTag("Queue", parameters.QueueName); + _ = receiveActivity.AddTag("MessageId", delivery.BasicProperties.MessageId); + _ = receiveActivity.AddTag("CorrelationId", delivery.BasicProperties.CorrelationId); + + _ = receiveActivity.SetTag("messaging.system", "rabbitmq"); + _ = receiveActivity.SetTag("messaging.destination_kind", "queue"); + _ = receiveActivity.SetTag("messaging.destination", delivery.Exchange); + _ = receiveActivity.SetTag("messaging.rabbitmq.routing_key", delivery.RoutingKey); + + IAMQPResult result = TryDeserialize(receiveActivity, delivery, out var request) + ? await DispatchAsync(receiveActivity, delivery, request).ConfigureAwait(true) + : (IAMQPResult)new RejectResult(false); + + await result.ExecuteAsync(Channel, delivery).ConfigureAwait(true); + + //receiveActivity?.SetEndTime(DateTime.UtcNow); + } + + private static readonly Action s_logErrorOnExtractTraceContext = LoggerMessage.Define(LogLevel.Error, new EventId(1, "Failed to extract trace context."), "Failed to extract trace context."); + + /// + /// Extracts the trace context from the basic properties. + /// + /// The basic properties. + /// The key. + /// The trace context. + [SuppressMessage("Design", "CA1031", Justification = "Tratamento de exceçào global, isolando uma micro-operação")] + private IEnumerable ExtractTraceContextFromBasicProperties(IReadOnlyBasicProperties props, string key) + { + try + { + if (props.Headers.TryGetValue(key, out var value)) + { + var bytes = value as byte[]; + return new[] { Encoding.UTF8.GetString(bytes) }; + } + } + catch (Exception ex) + { + s_logErrorOnExtractTraceContext(Logger, ex); + } + + return Enumerable.Empty(); + } + + + private static readonly Action s_logErrorOnDesserialize = LoggerMessage.Define(LogLevel.Error, new EventId(1, "Message rejected during deserialization"), "Message rejected during deserialization {ExceptionDetails}"); + + + /// + /// Tries to deserialize the received item. + /// + /// The receive activity. + /// The received item. + /// The deserialized request. + /// true if deserialization is successful; otherwise, false. + [SuppressMessage("Design", "CA1031", Justification = "Tratamento de exceçào global, isolando uma micro-operação")] + private bool TryDeserialize(Activity receiveActivity, BasicDeliverEventArgs receivedItem, out TRequest request) + { + _ = Guard.Argument(receivedItem).NotNull(); + + var returnValue = true; + + request = default; + try + { + request = parameters.Serializer.Deserialize(eventArgs: receivedItem); + } + catch (Exception exception) + { + returnValue = false; + + _ = receiveActivity.SetStatus(ActivityStatusCode.Error, exception.ToString()); + + s_logErrorOnDesserialize(Logger, exception, exception); + } + + return returnValue; + } + + private static readonly Action s_logErrorOnDispatch = LoggerMessage.Define(LogLevel.Error, new EventId(1, "Exception on processing message"), "Exception on processing message {QueueName} {Exception}"); + + + /// + /// Dispatches the request to the appropriate handler. + /// + /// The receive activity. + /// The received item. + /// The request. + /// The result of the dispatch. + [SuppressMessage("Design", "CA1031", Justification = "Tratamento de exceção global, isolando uma macro-operação")] + protected virtual async Task DispatchAsync(Activity receiveActivity, BasicDeliverEventArgs receivedItem, TRequest request) + { + _ = Guard.Argument(receiveActivity).NotNull(); + _ = Guard.Argument(receivedItem).NotNull(); + + if (request == null) return new RejectResult(false); + + IAMQPResult returnValue; + + using var dispatchActivity = activitySource.StartActivity(parameters.AdapterExpressionText, ActivityKind.Internal, receiveActivity.Context); + + //using (var logContext = new EnterpriseApplicationLogContext()) + //{ + try + { + var service = parameters.ServiceProvider.GetRequiredService(); + + if (parameters.DispatchScope == DispatchScope.RootScope) + { + await parameters.AdapterFunc(service, request); + } + else if (parameters.DispatchScope == DispatchScope.ChildScope) + { + using (var scope = parameters.ServiceProvider.CreateScope()) + { + await parameters.AdapterFunc(service, request); + } + } + returnValue = new AckResult(); + } + catch (Exception exception) + { + s_logErrorOnDispatch(Logger, parameters.QueueName, exception, exception); + + returnValue = new NackResult(parameters.RequeueOnCrash); + + _ = (dispatchActivity?.SetStatus(ActivityStatusCode.Error, exception.ToString())); + } + //} + + return returnValue; + } +} diff --git a/src/Oragon.RabbitMQ/Consumer/AsyncQueueConsumerParameters.cs b/src/Oragon.RabbitMQ/Consumer/AsyncQueueConsumerParameters.cs new file mode 100644 index 0000000..4c2e0bb --- /dev/null +++ b/src/Oragon.RabbitMQ/Consumer/AsyncQueueConsumerParameters.cs @@ -0,0 +1,158 @@ +using Dawn; +using System.Linq.Expressions; +using Oragon.RabbitMQ.Serialization; + +namespace Oragon.RabbitMQ.Consumer; + +/// +/// factory for . +/// +/// +/// +/// +public class AsyncQueueConsumerParameters : ConsumerBaseParameters + where TResponse : Task + where TRequest : class +{ + /// + /// Service Provider + /// + public IServiceProvider ServiceProvider { get; private set; } + + /// + /// Set a + /// + /// + /// + public AsyncQueueConsumerParameters WithServiceProvider(IServiceProvider serviceProvider) + { + ServiceProvider = serviceProvider; + return this; + } + + /// + /// Serializer used to serialize and deserialize messages + /// + public IAMQPSerializer Serializer { get; private set; } + + /// + /// Set an IAMQPSerializer + /// + /// + /// + public AsyncQueueConsumerParameters WithSerializer(IAMQPSerializer serializer) + { + Serializer = serializer; + return this; + } + + /// + /// Call Adapter, used to adapt message to service method + /// + public Expression> AdapterExpression { get; private set; } + + /// + /// Call Adapter, used to adapt message to service method + /// + public string AdapterExpressionText { get; private set; } + + /// + /// Adapter Func, used to adapt message to service method + /// + public Func AdapterFunc { get; private set; } + + /// + /// Set an Adapter Func + /// + /// + /// + public AsyncQueueConsumerParameters WithAdapter(Expression> adapterExpression) + { + _ = Guard.Argument(adapterExpression).NotNull(); + + AdapterExpression = adapterExpression; + AdapterFunc = adapterExpression.Compile(); + AdapterExpressionText = adapterExpression.ToString(); + return this; + } + + /*public AsyncQueueConsumerParameters WithEnterpriseApplicationLog(Func adapterFunc) + { + _ = Guard.Argument(this.AdapterFunc).NotNull(); + + Func oldAdapterFunc = this.AdapterFunc; + + this.AdapterFunc = async (svc, msg) => + { + using (var logContext = new EnterpriseApplicationLogContext()) + { + //logContext.SetIdentity(nameof(DiscordSyncService.SyncAsync)); + logContext.AddArgument("msg", msg); + return await logContext.ExecuteWithLogAsync(() => oldAdapterFunc(svc, msg)); + } + }; + return this; + }*/ + + /// + /// Dispatch Scope + /// + public DispatchScope DispatchScope { get; private set; } + + /// + /// Set a Dispatch Scope + /// + /// + /// + public AsyncQueueConsumerParameters WithDispatchScope(DispatchScope dispatchScope) + { + DispatchScope = dispatchScope; + return this; + } + + /// + /// Requeue On Crash + /// + public bool RequeueOnCrash { get; private set; } + + /// + /// Set Requeue On Crash + /// + /// + /// + public AsyncQueueConsumerParameters WithRequeueOnCrash(bool requeueOnCrash = true) + { + RequeueOnCrash = requeueOnCrash; + return this; + } + + + /// + /// Set dispatch in root scope + /// + /// + public AsyncQueueConsumerParameters WithDispatchInRootScope() + => WithDispatchScope(DispatchScope.RootScope); + + /// + /// Set dispatch in child scope + /// + /// + public AsyncQueueConsumerParameters WithDispatchInChildScope() + => WithDispatchScope(DispatchScope.ChildScope); + + + /// + /// Validate parameters + /// + public override void Validate() + { + base.Validate(); + + _ = Guard.Argument(ServiceProvider).NotNull(); + _ = Guard.Argument(Serializer).NotNull(); + _ = Guard.Argument(AdapterFunc).NotNull(); + _ = Guard.Argument(DispatchScope).NotIn(DispatchScope.None); + } + +} diff --git a/src/Oragon.RabbitMQ/Consumer/AsyncRpcConsumer.cs b/src/Oragon.RabbitMQ/Consumer/AsyncRpcConsumer.cs new file mode 100644 index 0000000..08aed22 --- /dev/null +++ b/src/Oragon.RabbitMQ/Consumer/AsyncRpcConsumer.cs @@ -0,0 +1,125 @@ +using Dawn; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using RabbitMQ.Client.Events; +using System.Diagnostics; +using Oragon.RabbitMQ.Consumer.Actions; +using System.Diagnostics.CodeAnalysis; + +namespace Oragon.RabbitMQ.Consumer; + + +/// +/// A consumer that processes messages in an RPC flow. +/// +/// +/// +/// +public class AsyncRpcConsumer : AsyncQueueConsumer> + where TResponse : class + where TRequest : class +{ + private readonly AsyncQueueConsumerParameters> parameters; + + /// + /// Creates a new instance of the class. + /// + /// + /// + /// + public AsyncRpcConsumer(ILogger logger, AsyncQueueConsumerParameters> parameters, IServiceProvider serviceProvider) + : base(logger, parameters, serviceProvider) + { + this.parameters = Guard.Argument(parameters).NotNull().Value; + this.parameters.Validate(); + } + + private static readonly Action s_logErrorOnDispatchWithoutReplyTo= LoggerMessage.Define(LogLevel.Error, new EventId(1, "Message cannot be processed in RPC Flow because original message didn't have a ReplyTo."), "Message cannot be processed in RPC Flow because original message didn't have a ReplyTo."); + + + /// + /// Dispatches the message to the service. + /// + /// + /// + /// + /// + [SuppressMessage("Design", "CA1031", Justification = "Tratamento de exceçào global, isolando uma MACRO-operação")] + protected override async Task DispatchAsync(Activity receiveActivity, BasicDeliverEventArgs receivedItem, TRequest request) + { + _ = Guard.Argument(receivedItem).NotNull(); + _ = Guard.Argument(receiveActivity).NotNull(); + _ = Guard.Argument(request).NotNull(); + + if (receivedItem.BasicProperties.ReplyTo == null) + { + s_logErrorOnDispatchWithoutReplyTo(Logger, null); + + return new RejectResult(false); + } + + TResponse responsePayload = default; + + using (var dispatchActivity = activitySource.StartActivity(parameters.AdapterExpressionText, ActivityKind.Internal, receiveActivity.Context)) + { + try + { + var service = parameters.ServiceProvider.GetRequiredService(); + + if (parameters.DispatchScope == DispatchScope.RootScope) + { + responsePayload = await parameters.AdapterFunc(service, request).ConfigureAwait(true); + } + else if (parameters.DispatchScope == DispatchScope.ChildScope) + { + using (var scope = parameters.ServiceProvider.CreateScope()) + { + responsePayload = await parameters.AdapterFunc(service, request).ConfigureAwait(true); + } + } + } + catch (Exception exception) + { + _ = (dispatchActivity?.SetStatus(ActivityStatusCode.Error, exception.ToString())); + + await SendReplyAsync(dispatchActivity, receivedItem, null, exception).ConfigureAwait(true); + + return new NackResult(parameters.RequeueOnCrash); + } + } + + using (var replyActivity = activitySource.StartActivity(parameters.AdapterExpressionText, ActivityKind.Internal, receiveActivity.Context)) + { + await this.SendReplyAsync(replyActivity, receivedItem, responsePayload).ConfigureAwait(true); + } + return new AckResult(); + } + + private async Task SendReplyAsync(Activity activity, BasicDeliverEventArgs receivedItem, TResponse responsePayload = null, Exception exception = null) + { + _ = Guard.Argument(receivedItem).NotNull(); + _ = Guard.Argument(responsePayload).NotNull(); + + + var responseProperties = Channel.CreateBasicProperties() + .SetMessageId() + .IfFunction(it => exception != null, it => it.SetException(exception)) + .SetTelemetry(activity) + .SetCorrelationId(receivedItem.BasicProperties); + + _ = (activity?.AddTag("Queue", receivedItem.BasicProperties.ReplyTo)); + _ = (activity?.AddTag("MessageId", responseProperties.MessageId)); + _ = (activity?.AddTag("CorrelationId", responseProperties.CorrelationId)); + + await Channel.BasicPublishAsync(string.Empty, + receivedItem.BasicProperties.ReplyTo, + responseProperties, + exception != null + ? Array.Empty() + : parameters.Serializer.Serialize(basicProperties: responseProperties, message: responsePayload) + ).ConfigureAwait(true); + + //replyActivity?.SetEndTime(DateTime.UtcNow); + } + +} diff --git a/src/Oragon.RabbitMQ/Consumer/ConsumerBase.cs b/src/Oragon.RabbitMQ/Consumer/ConsumerBase.cs new file mode 100644 index 0000000..69c8083 --- /dev/null +++ b/src/Oragon.RabbitMQ/Consumer/ConsumerBase.cs @@ -0,0 +1,174 @@ +using System.Diagnostics.CodeAnalysis; +using Dawn; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Polly; +using RabbitMQ.Client; +using RabbitMQ.Client.Exceptions; + +namespace Oragon.RabbitMQ.Consumer; + +/// +/// Base class for consumers. +/// +[SuppressMessage("Performance", "CA1848", Justification = "Use the LoggerMessage delegates")] +[SuppressMessage("Performance", "CA2254", Justification = "Template should be a static expression")] +public abstract class ConsumerBase : BackgroundService +{ + /// + /// The logger. + /// + protected ILogger Logger { get; private set; } + + private readonly IServiceProvider serviceProvider; + + /// + /// The connection to RabbitMQ. + /// + protected IConnection Connection { get; private set; } + + /// + /// The consumer instance. + /// + protected IBasicConsumer Consumer { get; private set; } + + private string consumerTag; + + private readonly ConsumerBaseParameters parameters; + + /// + /// The channel for communication with RabbitMQ. + /// + protected IChannel Channel { get; private set; } + + #region Constructors + + /// + /// Initializes a new instance of the class. + /// + /// The logger. + /// The consumer base parameters. + /// The service provider. + protected ConsumerBase(ILogger logger, ConsumerBaseParameters parameters, IServiceProvider serviceProvider) + { + this.Logger = Guard.Argument(logger).NotNull().Value; + this.parameters = Guard.Argument(parameters).NotNull().Value; + this.parameters.Validate(); + this.serviceProvider = serviceProvider; + this.consumerTag = $"{this.parameters.QueueName}-{Guid.NewGuid().ToString("D").Split("-").Last()}"; + } + + #endregion + + /// + /// Executes the consumer asynchronously. + /// + /// The stopping token. + /// A representing the asynchronous operation. + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + Connection = parameters.ConnectionFactoryFunc(serviceProvider); + + if (parameters.Configurer != null) + { + using var tmpModel = await this.Connection.CreateChannelAsync(stoppingToken).ConfigureAwait(true); + parameters.Configurer(serviceProvider, tmpModel); + } + + await this.WaitQueueCreationAsync().ConfigureAwait(true); + + this.Channel = await this.Connection.CreateChannelAsync(stoppingToken).ConfigureAwait(true); + + await this.Channel.BasicQosAsync(0, parameters.PrefetchCount, false, stoppingToken).ConfigureAwait(true); + + this.Consumer = BuildConsumer(); + + var startTime = DateTimeOffset.UtcNow; + + Logger.LogInformation($"Consuming Queue {parameters.QueueName} since: {startTime}"); + + this.consumerTag = await this.Channel.BasicConsumeAsync( + queue: parameters.QueueName, + autoAck: false, + consumer: this.Consumer, + consumerTag: this.consumerTag, + arguments: null, + exclusive: false, + noLocal: true, + cancellationToken: stoppingToken) + .ConfigureAwait(true); + + var timeToDisplay = (int)parameters.DisplayLoopInConsoleEvery.TotalSeconds; + + long loopCount = 0; + while (!stoppingToken.IsCancellationRequested) + { + loopCount++; + var logMessage = $"Consuming Queue {parameters.QueueName} since: {startTime} uptime: {DateTimeOffset.Now - startTime}"; + + if (loopCount % timeToDisplay == 0) + Logger.LogInformation(logMessage); + else + Logger.LogTrace(logMessage); + + await Task.Delay(1000, stoppingToken).ConfigureAwait(true); + } + } + + /// + /// Waits for the queue creation asynchronously. + /// + /// A representing the asynchronous operation. + protected virtual async Task WaitQueueCreationAsync() + { + _ = await Policy + .Handle() + .WaitAndRetryAsync(parameters.TestQueueRetryCount, retryAttempt => + { + var timeToWait = TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)); + Logger.LogWarning("Queue {QueueName} not found... We will try in {Tempo}.", parameters.QueueName, timeToWait); + return timeToWait; + }) + .ExecuteAsync(async () => + { + using IChannel testModel = await Connection.CreateChannelAsync().ConfigureAwait(true); + _ = await testModel.QueueDeclarePassiveAsync(parameters.QueueName).ConfigureAwait(true); + return Task.CompletedTask; + }).ConfigureAwait(true); + } + + /// + /// Builds the consumer. + /// + /// The built . + protected abstract IBasicConsumer BuildConsumer(); + + + /// + /// Disposes the consumer. + /// + public override void Dispose() + { + GC.SuppressFinalize(this); + + this.DisposeAsync() + .GetAwaiter() + .GetResult(); + + base.Dispose(); + } + + + private async Task DisposeAsync() + { + if (this.Channel != null && !string.IsNullOrWhiteSpace(consumerTag)) + { + await this.Channel.BasicCancelAsync(consumerTag, true).ConfigureAwait(false); + } + if (this.Channel != null) + { + this.Channel.Dispose(); + this.Channel = null; + } + } +} diff --git a/src/Oragon.RabbitMQ/Consumer/ConsumerBaseParameters.cs b/src/Oragon.RabbitMQ/Consumer/ConsumerBaseParameters.cs new file mode 100644 index 0000000..fc9c918 --- /dev/null +++ b/src/Oragon.RabbitMQ/Consumer/ConsumerBaseParameters.cs @@ -0,0 +1,117 @@ +using Dawn; +using RabbitMQ.Client; + +namespace Oragon.RabbitMQ.Consumer; + +/// +/// Represents the parameters for the consumer base class. +/// +public class ConsumerBaseParameters +{ + /// + /// Gets or sets the queue name. + /// + public string QueueName { get; private set; } + + /// + /// Sets the queue name. + /// + /// The queue name. + /// The updated instance of . + public ConsumerBaseParameters WithQueueName(string queueName) + { + QueueName = queueName; + return this; + } + + /// + /// Gets or sets the configurer action for topology. + /// + public Action Configurer { get; private set; } + + /// + /// Sets the configurer action for topology. + /// + /// The configurer action for topology. + /// The updated instance of . + public ConsumerBaseParameters WithTopology(Action configurer) + { + Configurer = configurer; + return this; + } + + /// + /// Gets or sets the prefetch count. + /// + public ushort PrefetchCount { get; private set; } + + /// + /// Sets the prefetch count. + /// + /// The prefetch count. + /// The updated instance of . + public ConsumerBaseParameters WithPrefetchCount(ushort prefetchCount) + { + PrefetchCount = prefetchCount; + return this; + } + + /// + /// Gets or sets the connection factory function. + /// + public Func ConnectionFactoryFunc { get; private set; } + + /// + /// Sets the connection factory function. + /// + /// The connection factory function. + /// The updated instance of . + public ConsumerBaseParameters WithConnectionFactoryFunc(Func connectionFactoryFunc) + { + ConnectionFactoryFunc = connectionFactoryFunc; + return this; + } + + /// + /// Gets or sets the test queue retry count. + /// + public int TestQueueRetryCount { get; private set; } + + /// + /// Sets the test queue retry count. + /// + /// The test queue retry count. + /// The updated instance of . + public ConsumerBaseParameters WithTestQueueRetryCount(int testQueueRetryCount) + { + TestQueueRetryCount = testQueueRetryCount; + return this; + } + + /// + /// Gets or sets the display loop in console every time span. + /// + public TimeSpan DisplayLoopInConsoleEvery { get; private set; } + + /// + /// Sets the display loop in console every time span. + /// + /// The time span to display. + /// The updated instance of . + public ConsumerBaseParameters WithDisplayLoopInConsoleEvery(TimeSpan timeToDisplay) + { + DisplayLoopInConsoleEvery = timeToDisplay; + return this; + } + + /// + /// Validates the consumer base parameters. + /// + public virtual void Validate() + { + _ = Guard.Argument(QueueName).NotNull().NotEmpty().NotWhiteSpace(); + _ = Guard.Argument(PrefetchCount).NotZero().NotNegative(); + _ = Guard.Argument(TestQueueRetryCount).NotNegative(); + _ = Guard.Argument(ConnectionFactoryFunc).NotNull(); + } +} diff --git a/src/Oragon.RabbitMQ/Consumer/DispatchScope.cs b/src/Oragon.RabbitMQ/Consumer/DispatchScope.cs new file mode 100644 index 0000000..4683cce --- /dev/null +++ b/src/Oragon.RabbitMQ/Consumer/DispatchScope.cs @@ -0,0 +1,22 @@ +namespace Oragon.RabbitMQ.Consumer; + +/// +/// Define scope used on dispatch +/// +public enum DispatchScope +{ + /// + /// Ignore scope (will cause error) + /// + None, + + /// + /// Use the same scope as the parent + /// + RootScope, + + /// + /// Use a new scope for each message + /// + ChildScope +} diff --git a/GagoAspireApp.Architecture/Messaging/Extensions.DependencyInjection.cs b/src/Oragon.RabbitMQ/Extensions.DependencyInjection.cs similarity index 53% rename from GagoAspireApp.Architecture/Messaging/Extensions.DependencyInjection.cs rename to src/Oragon.RabbitMQ/Extensions.DependencyInjection.cs index bbf756b..bcb5de4 100644 --- a/GagoAspireApp.Architecture/Messaging/Extensions.DependencyInjection.cs +++ b/src/Oragon.RabbitMQ/Extensions.DependencyInjection.cs @@ -1,14 +1,16 @@ -using GagoAspireApp.Architecture.Messaging.Consumer; -using GagoAspireApp.Architecture.Messaging.Serialization; using Dawn; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using RabbitMQ.Client; -using System.Diagnostics; +using Oragon.RabbitMQ.Serialization; +using Oragon.RabbitMQ.Consumer; -namespace GagoAspireApp.Architecture.Messaging; +namespace Oragon.RabbitMQ; +/// +/// Extensions for Dependency Injection +/// public static class DependencyInjectionExtensions { @@ -16,28 +18,27 @@ public static class DependencyInjectionExtensions /// Create a new QueueServiceWorker to bind a queue with an function /// /// Service Type will be used to determine which service will be used to connect on queue - /// Type of message sent by publisher to consumer. Must be exactly same Type that functionToExecute parameter requests. - /// Type of returned message sent by consumer to publisher. Must be exactly same Type that functionToExecute returns. - /// Dependency Injection Service Collection - /// Name of queue - /// Function to execute when any message are consumed from queue + /// Type of message sent by publisher to Consumer. Must be exactly same Type that functionToExecute parameter requests. + /// Type of returned message sent by Consumer to publisher. Must be exactly same Type that functionToExecute returns. + /// Services + /// Configuration handler public static void MapQueueRPC(this IServiceCollection services, Action>> config) where TResponse : class where TRequest : class { - Guard.Argument(services).NotNull(); - Guard.Argument(config).NotNull(); + _ = Guard.Argument(services).NotNull(); + _ = Guard.Argument(config).NotNull(); - services.AddSingleton(sp => + _ = services.AddSingleton(sp => { var parameters = new AsyncQueueConsumerParameters>(); - parameters.WithServiceProvider(sp); - parameters.WithDisplayLoopInConsoleEvery(TimeSpan.FromMinutes(1)); - parameters.WithTestQueueRetryCount(5); - parameters.WithConnectionFactoryFunc((sp) => sp.GetRequiredService()); - parameters.WithDispatchInRootScope(); - parameters.WithSerializer(sp.GetRequiredService()); + _ = parameters.WithServiceProvider(sp); + _ = parameters.WithDisplayLoopInConsoleEvery(TimeSpan.FromMinutes(1)); + _ = parameters.WithTestQueueRetryCount(5); + _ = parameters.WithConnectionFactoryFunc((sp) => sp.GetRequiredService()); + _ = parameters.WithDispatchInRootScope(); + _ = parameters.WithSerializer(sp.GetRequiredService()); config(parameters); @@ -53,28 +54,26 @@ public static void MapQueueRPC(this IServiceColle /// Create a new QueueServiceWorker to bind a queue with an function /// /// Service Type will be used to determine which service will be used to connect on queue - /// Type of message sent by publisher to consumer. Must be exactly same Type that functionToExecute parameter requests. - /// Type of returned message sent by consumer to publisher. Must be exactly same Type that functionToExecute returns. + /// Type of message sent by publisher to Consumer. Must be exactly same Type that functionToExecute parameter requests. /// Dependency Injection Service Collection - /// Name of queue - /// Function to execute when any message are consumed from queue + /// Configuration handler public static void MapQueue(this IServiceCollection services, Action> config) where TRequest : class { - Guard.Argument(services).NotNull(); - Guard.Argument(config).NotNull(); + _ = Guard.Argument(services).NotNull(); + _ = Guard.Argument(config).NotNull(); - services.AddSingleton(sp => + _ = services.AddSingleton(sp => { var parameters = new AsyncQueueConsumerParameters(); - parameters.WithServiceProvider(sp); - parameters.WithDisplayLoopInConsoleEvery(TimeSpan.FromMinutes(1)); - parameters.WithTestQueueRetryCount(5); - parameters.WithConnectionFactoryFunc((sp) => sp.GetRequiredService()); - parameters.WithDispatchInRootScope(); - parameters.WithSerializer(sp.GetRequiredService()); + _ = parameters.WithServiceProvider(sp); + _ = parameters.WithDisplayLoopInConsoleEvery(TimeSpan.FromMinutes(1)); + _ = parameters.WithTestQueueRetryCount(5); + _ = parameters.WithConnectionFactoryFunc((sp) => sp.GetRequiredService()); + _ = parameters.WithDispatchInRootScope(); + _ = parameters.WithSerializer(sp.GetRequiredService()); config(parameters); diff --git a/src/Oragon.RabbitMQ/Extensions.RabbitMQ.cs b/src/Oragon.RabbitMQ/Extensions.RabbitMQ.cs new file mode 100644 index 0000000..817b95d --- /dev/null +++ b/src/Oragon.RabbitMQ/Extensions.RabbitMQ.cs @@ -0,0 +1,314 @@ +using Dawn; +using RabbitMQ.Client; +using System.Text; + +namespace Oragon.RabbitMQ; + +/// +/// Extensiosn for RabbitMQ +/// +public static partial class RabbitMQExtensions +{ + + /// + /// Reimplementing creation of BasicProperties from IChannel + /// + /// + /// + public static BasicProperties CreateBasicProperties(this IChannel channel) + { + return new BasicProperties(); + } + + /// + /// Set MessageId + /// + /// + /// + /// + public static BasicProperties SetMessageId(this BasicProperties basicProperties, string messageId = null) + { + ArgumentNullException.ThrowIfNull(basicProperties); + basicProperties.MessageId = messageId ?? Guid.NewGuid().ToString("D"); + return basicProperties; + } + + + /// + /// Set CorrelationId + /// + /// + /// + /// + public static BasicProperties SetCorrelationId(this BasicProperties basicProperties, IReadOnlyBasicProperties originalBasicProperties) + { + _ = Guard.Argument(originalBasicProperties).NotNull().NotSame(basicProperties); + return basicProperties.SetCorrelationId(originalBasicProperties.MessageId); + } + + /// + /// Set CorrelationId + /// + /// + /// + /// + /// + public static BasicProperties SetCorrelationId(this BasicProperties basicProperties, string correlationId) + { + if (string.IsNullOrEmpty(correlationId)) throw new ArgumentException($"'{nameof(correlationId)}' cannot be null or empty.", nameof(correlationId)); + + basicProperties.CorrelationId = correlationId; + return basicProperties; + } + + /// + /// Set if message is Persistent (durable) or not + /// + /// + /// + /// + public static BasicProperties SetDurable(this BasicProperties basicProperties, bool durable = true) + { + basicProperties.Persistent = durable; + return basicProperties; + } + + /// + /// Set if message is Transient (memory only) or not + /// + /// + /// + /// + public static BasicProperties SetTransient(this BasicProperties basicProperties, bool transient = true) => basicProperties.SetDurable(!transient); + + + /// + /// Set ReplyTo + /// + /// + /// + /// + public static BasicProperties SetReplyTo(this BasicProperties basicProperties, string replyTo = null) + { + if (!string.IsNullOrEmpty(replyTo)) + basicProperties.ReplyTo = replyTo; + + return basicProperties; + } + + /// + /// Set AppId + /// + /// + /// + /// + public static BasicProperties SetAppId(this BasicProperties basicProperties, string appId = null) + { + if (!string.IsNullOrEmpty(appId)) + basicProperties.AppId = appId; + + return basicProperties; + } + + //private static string AsString(this object objectToConvert) + //{ + // return objectToConvert != null ? Encoding.UTF8.GetString((byte[])objectToConvert) : null; + //} + + /// + /// Get a string from a dictionary + /// + /// + /// + /// + public static string AsString(this IDictionary dic, string key) + { + var content = dic?[key]; + return content != null ? Encoding.UTF8.GetString((byte[])content) : null; + } + + //public static List AsStringList(this object objectToConvert) + //{ + // ArgumentNullException.ThrowIfNull(objectToConvert); + // var routingKeyList = (List)objectToConvert; + + // var items = routingKeyList.ConvertAll(key => key.AsString()); + + // return items; + //} + + /// + /// Set Exception on BasicProperties + /// + /// + /// + /// + public static BasicProperties SetException(this BasicProperties basicProperties, Exception exception) + { + _ = Guard.Argument(exception).NotNull(); + + basicProperties.Headers ??= new Dictionary(); + + var exceptionType = exception.GetType(); + + basicProperties.Headers.Add("exception.type", $"{exceptionType.Namespace}.{exceptionType.Name}, {exceptionType.Assembly.FullName}"); + basicProperties.Headers.Add("exception.message", exception.Message); + basicProperties.Headers.Add("exception.stacktrace", exception.StackTrace); + + return basicProperties; + } + + + //public static bool TryReconstructException(this BasicProperties basicProperties, out AMQPRemoteException remoteException) + //{ + // remoteException = default; + // if (basicProperties?.Headers?.ContainsKey("exception.type") ?? false) + // { + // var exceptionTypeString = basicProperties.Headers.AsString("exception.type"); + // var exceptionMessage = basicProperties.Headers.AsString("exception.message"); + // var exceptionStackTrace = basicProperties.Headers.AsString("exception.stacktrace"); + // var exceptionInstance = (Exception)Activator.CreateInstance(Type.GetType(exceptionTypeString) ?? typeof(Exception), exceptionMessage); + // remoteException = new AMQPRemoteException("Remote Consumer report a exception during execution", exceptionStackTrace, exceptionInstance); + // return true; + // } + // return false; + //} + + /// + /// + /// + /// + /// + /// + public static ConnectionFactory DispatchConsumersAsync(this ConnectionFactory connectionFactory, bool useAsync = true) + { + _ = Guard.Argument(connectionFactory).NotNull(); + + connectionFactory.DispatchConsumersAsync = useAsync; + + return connectionFactory; + } + + /// + /// Convert IConnectionFactory in ConnectionFactory + /// + /// + /// + public static ConnectionFactory Unbox(this global::RabbitMQ.Client.IConnectionFactory connectionFactory) + => (ConnectionFactory)connectionFactory; + + + + //public static List GetDeathHeader(this BasicProperties basicProperties) + //{ + // return (List)basicProperties.Headers["x-death"]; + //} + + //public static string GetQueueName(this Dictionary xdeath) + //{ + // return xdeath.AsString("queue"); + //} + + //public static string GetExchangeName(this Dictionary xdeath) + //{ + // return xdeath.AsString("exchange"); + //} + + //public static List GetRoutingKeys(this Dictionary xdeath) + //{ + // return xdeath["routing-keys"].AsStringList(); + //} + + //public static long Count(this Dictionary xdeath) + //{ + // return (long)xdeath["count"]; + //} + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static T IfFunction(this T target, Func condition, Func actionWhenTrue, Func actionWhenFalse = null) + { + _ = Guard.Argument(condition, nameof(condition)).NotNull(); + _ = Guard.Argument(actionWhenTrue, nameof(actionWhenTrue)).NotNull(); + + if (target == null) + return target; + + var conditionResult = condition(target); + + if (conditionResult) + target = actionWhenTrue(target); + else if (actionWhenFalse != null) + target = actionWhenFalse(target); + + return target; + } + + /// + /// + /// + /// + /// + /// + /// + /// + /// + public static T IfAction(this T target, Func condition, Action actionWhenTrue, Action actionWhenFalse = null) + { + _ = Guard.Argument(condition, nameof(condition)).NotNull(); + _ = Guard.Argument(actionWhenTrue, nameof(actionWhenTrue)).NotNull(); + + if (target == null) + return target; + + var conditionResult = condition(target); + + if (conditionResult) + actionWhenTrue(target); + else actionWhenFalse?.Invoke(target); + + return target; + } + + /// + /// + /// + /// + /// + /// + /// + public static T Fluent(this T target, Action action) + where T : class + { + _ = Guard.Argument(target, nameof(target)).NotNull(); + _ = Guard.Argument(action, nameof(action)).NotNull(); + + action(); + + return target; + } + + /// + /// + /// + /// + /// + /// + /// + public static T Fluent(this T target, Func func) + where T : class + { + _ = Guard.Argument(target, nameof(target)).NotNull(); + _ = Guard.Argument(func, nameof(func)).NotNull(); + + return func(); + + } +} diff --git a/src/Oragon.RabbitMQ/Extensions.Telemetry.cs b/src/Oragon.RabbitMQ/Extensions.Telemetry.cs new file mode 100644 index 0000000..3bd25a0 --- /dev/null +++ b/src/Oragon.RabbitMQ/Extensions.Telemetry.cs @@ -0,0 +1,132 @@ +using RabbitMQ.Client; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace Oragon.RabbitMQ; + +/// +/// Extensions for Telemetry +/// +public static partial class TelemetryExtensions +{ + /// + /// StartActivity with the given name and kind, or return a new Activity with the name prefixed with "?". + /// + /// + /// + /// + /// + public static Activity SafeStartActivity(this ActivitySource activitySource, [CallerMemberName] string name = "", ActivityKind kind = ActivityKind.Internal) + { + ArgumentNullException.ThrowIfNull(activitySource); + var activity = activitySource.StartActivity(name, kind) ?? new Activity("?" + name); + _ = activity.SetStartTime(DateTime.UtcNow); + return activity; + } + + /// + /// StartActivity with the given name and kind, or return a new Activity with the name prefixed with "?". + /// + /// + /// + /// + /// + /// + public static Activity SafeStartActivity(this ActivitySource activitySource, string name, ActivityKind kind, ActivityContext parentContext) + { + ArgumentNullException.ThrowIfNull(activitySource); + var activity = activitySource.StartActivity(name, kind, parentContext) ?? new Activity("?" + name); + _ = activity.SetStartTime(DateTime.UtcNow); + return activity; + } + + /// + /// Get the TraceId from the BasicProperties + /// + /// + /// + public static ActivityTraceId GetTraceId(this BasicProperties basicProperties) + { + ArgumentNullException.ThrowIfNull(basicProperties); + return basicProperties.Headers != null && basicProperties.Headers.ContainsKey("TraceId") + ? ActivityTraceId.CreateFromString(basicProperties.Headers.AsString("TraceId")) + : default; + } + + /// + /// Get the SpanId from the BasicProperties + /// + /// + /// + public static ActivitySpanId GetSpanId(this BasicProperties basicProperties) + { + ArgumentNullException.ThrowIfNull(basicProperties); + if (basicProperties.Headers != null && basicProperties.Headers.ContainsKey("SpanId")) + return ActivitySpanId.CreateFromString(basicProperties.Headers.AsString("SpanId")); + return default; + } + + /// + /// Validate if the BasicProperties has Headers, if not, create a new Dictionary + /// + /// + /// + public static BasicProperties EnsureHeaders(this BasicProperties basicProperties) + { + ArgumentNullException.ThrowIfNull(basicProperties); + basicProperties.Headers ??= new Dictionary(); + return basicProperties; + } + + /// + /// Set the TraceId and SpanId in the BasicProperties + /// + /// + /// + /// + public static BasicProperties SetTelemetry(this BasicProperties basicProperties, Activity activity) + { + ArgumentNullException.ThrowIfNull(basicProperties); + if (activity != null) + { + _ = basicProperties + .SetSpanId(activity.SpanId) + .SetTraceId(activity.TraceId); + } + return basicProperties; + } + + /// + /// Set the TraceId in the BasicProperties + /// + /// + /// + /// + private static BasicProperties SetTraceId(this BasicProperties basicProperties, ActivityTraceId? activityTraceId) + { + ArgumentNullException.ThrowIfNull(basicProperties); + if (activityTraceId != null) + { + basicProperties.EnsureHeaders().Headers["TraceId"] = activityTraceId.ToString(); + } + return basicProperties; + } + + + /// + /// set the SpanId in the BasicProperties + /// + /// + /// + /// + private static BasicProperties SetSpanId(this BasicProperties basicProperties, ActivitySpanId? activitySpanId) + { + ArgumentNullException.ThrowIfNull(basicProperties); + if (activitySpanId != null) + { + basicProperties.EnsureHeaders().Headers["SpanId"] = activitySpanId.ToString(); + } + return basicProperties; + } + +} diff --git a/src/Oragon.RabbitMQ/MessagingTelemetryNames.cs b/src/Oragon.RabbitMQ/MessagingTelemetryNames.cs new file mode 100644 index 0000000..b2abe2b --- /dev/null +++ b/src/Oragon.RabbitMQ/MessagingTelemetryNames.cs @@ -0,0 +1,49 @@ +using System.Diagnostics.CodeAnalysis; +using Dawn; +using OpenTelemetry.Trace; + +namespace Oragon.RabbitMQ; + +/// +/// +/// +[SuppressMessage("IDE", "IDE1006", Justification = "Template should be a static expression")] + +public static class MessagingTelemetryNames +{ + private static readonly List names = [ + "gaGO.io/RabbitMQ/AsyncQueueConsumer", + "gaGO.io/RabbitMQ/AsyncRpcConsumer", + "gaGO.io/RabbitMQ/MessagePublisher" + ]; + + /// + /// + /// + /// + /// + /// + public static string GetName(string name) + { + var fullName = $"gaGO.io/RabbitMQ/{name}"; + return !names.Contains(fullName) + ? throw new InvalidOperationException($"Name '{name}' is not registred ") + : fullName; + } + + /// + /// + /// + /// + /// + public static TracerProviderBuilder AddRabbitMQInstrumentation(this TracerProviderBuilder tracerProviderBuilder) + { + _ = Guard.Argument(tracerProviderBuilder).NotNull(); + + foreach (var name in names) + { + _ = tracerProviderBuilder.AddSource(name); + } + return tracerProviderBuilder; + } +} diff --git a/src/Oragon.RabbitMQ/Oragon.RabbitMQ.csproj b/src/Oragon.RabbitMQ/Oragon.RabbitMQ.csproj new file mode 100644 index 0000000..a13e7f8 --- /dev/null +++ b/src/Oragon.RabbitMQ/Oragon.RabbitMQ.csproj @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Oragon.RabbitMQ/Serialization/AMQPBaseSerializer.cs b/src/Oragon.RabbitMQ/Serialization/AMQPBaseSerializer.cs new file mode 100644 index 0000000..ed1d55b --- /dev/null +++ b/src/Oragon.RabbitMQ/Serialization/AMQPBaseSerializer.cs @@ -0,0 +1,77 @@ +using Dawn; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; +using System.Diagnostics; + +namespace Oragon.RabbitMQ.Serialization; + +/// +/// Base serializer for AMQP implementation with OpenTelemetry support +/// +public abstract class AMQPBaseSerializer : IAMQPSerializer +{ + private readonly ActivitySource activitySource; + private readonly string name; + + + /// + /// Create a new instance of AMQPBaseSerializer + /// + /// + /// + protected AMQPBaseSerializer(ActivitySource activitySource, string name) + { + this.activitySource = activitySource; + this.name = name; + } + + /// + /// Enable extension to deserialize the message + /// + /// + /// + /// + /// + protected abstract TMessage DeserializeInternal(IReadOnlyBasicProperties basicProperties, ReadOnlyMemory body); + + + /// + /// Enable extension to serialize the message + /// + /// + /// + /// + /// + protected abstract byte[] SerializeInternal(BasicProperties basicProperties, T objectToSerialize); + + + /// + /// Desserialize a mesage from a BasicDeliverEventArgs with OpenTelemetry support + /// + /// + /// + /// + public TMessage Deserialize(BasicDeliverEventArgs eventArgs) + { + _ = Guard.Argument(eventArgs).NotNull(); + _ = Guard.Argument((IReadOnlyBasicProperties)eventArgs.BasicProperties).NotNull(); + + var returnValue = this.DeserializeInternal(eventArgs.BasicProperties, eventArgs.Body); + + return returnValue; + } + + /// + /// Serialize a message to a byte array with OpenTelemetry support + /// + /// + /// + /// + /// + public byte[] Serialize(BasicProperties basicProperties, TMessage message) + { + var returnValue = SerializeInternal(basicProperties, message); ; + + return returnValue; + } +} diff --git a/src/PublicAPI.Shipped.txt b/src/PublicAPI.Shipped.txt new file mode 100644 index 0000000..e69de29 diff --git a/src/PublicAPI.Unshipped.txt b/src/PublicAPI.Unshipped.txt new file mode 100644 index 0000000..e69de29 diff --git a/tests/Oragon.RabbitMQ.IntegratedTests/Oragon.RabbitMQ.IntegratedTests.csproj b/tests/Oragon.RabbitMQ.IntegratedTests/Oragon.RabbitMQ.IntegratedTests.csproj new file mode 100644 index 0000000..a83e0ca --- /dev/null +++ b/tests/Oragon.RabbitMQ.IntegratedTests/Oragon.RabbitMQ.IntegratedTests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/tests/Oragon.RabbitMQ.IntegratedTests/UnitTest1.cs b/tests/Oragon.RabbitMQ.IntegratedTests/UnitTest1.cs new file mode 100644 index 0000000..6b71ac4 --- /dev/null +++ b/tests/Oragon.RabbitMQ.IntegratedTests/UnitTest1.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Oragon.RabbitMQ.IntegratedTests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} \ No newline at end of file diff --git a/tests/Oragon.RabbitMQ.UnitTests/Oragon.RabbitMQ.UnitTests.csproj b/tests/Oragon.RabbitMQ.UnitTests/Oragon.RabbitMQ.UnitTests.csproj new file mode 100644 index 0000000..a83e0ca --- /dev/null +++ b/tests/Oragon.RabbitMQ.UnitTests/Oragon.RabbitMQ.UnitTests.csproj @@ -0,0 +1,29 @@ + + + + net8.0 + enable + enable + + false + true + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/tests/Oragon.RabbitMQ.UnitTests/UnitTest1.cs b/tests/Oragon.RabbitMQ.UnitTests/UnitTest1.cs new file mode 100644 index 0000000..adade9c --- /dev/null +++ b/tests/Oragon.RabbitMQ.UnitTests/UnitTest1.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Oragon.RabbitMQ.UnitTests; + +public class UnitTest1 +{ + [Fact] + public void Test1() + { + + } +} \ No newline at end of file