From b4e032b7be25c8ae8295da1326b1291f3d9a6ee0 Mon Sep 17 00:00:00 2001 From: skwas Date: Tue, 23 Jul 2019 11:03:58 +0200 Subject: [PATCH] Initial implementation --- .editorconfig | 389 ++++++++++++++++++ .gitattributes | 63 +++ .gitignore | 264 ++++++++++++ Hangfire.Correlate.sln | 48 +++ LICENSE.md | 190 +++++++++ .../CorrelateFilterAttribute.cs | 58 +++ .../Hangfire.Correlate.csproj | 21 + .../IGlobalConfigurationExtensions.cs | 67 +++ .../Properties/InternalsVisibleTo.cs | 3 + src/package.props | 26 ++ .../BackgroundTestExecutor.cs | 56 +++ .../Extensions/LoggingExtensions.cs | 42 ++ .../Extensions/TaskExtensions.cs | 19 + .../Hangfire.Correlate.Tests.csproj | 37 ++ .../HangfireBuiltInConfigurationTests.cs | 32 ++ .../HangfireIntegrationTests.cs | 198 +++++++++ .../HangfireServiceProviderTests.cs | 31 ++ test/Hangfire.Correlate.Tests/TestLogger.cs | 38 ++ test/Hangfire.Correlate.Tests/TestService.cs | 20 + 19 files changed, 1602 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 Hangfire.Correlate.sln create mode 100644 LICENSE.md create mode 100644 src/Hangfire.Correlate/CorrelateFilterAttribute.cs create mode 100644 src/Hangfire.Correlate/Hangfire.Correlate.csproj create mode 100644 src/Hangfire.Correlate/IGlobalConfigurationExtensions.cs create mode 100644 src/Hangfire.Correlate/Properties/InternalsVisibleTo.cs create mode 100644 src/package.props create mode 100644 test/Hangfire.Correlate.Tests/BackgroundTestExecutor.cs create mode 100644 test/Hangfire.Correlate.Tests/Extensions/LoggingExtensions.cs create mode 100644 test/Hangfire.Correlate.Tests/Extensions/TaskExtensions.cs create mode 100644 test/Hangfire.Correlate.Tests/Hangfire.Correlate.Tests.csproj create mode 100644 test/Hangfire.Correlate.Tests/HangfireBuiltInConfigurationTests.cs create mode 100644 test/Hangfire.Correlate.Tests/HangfireIntegrationTests.cs create mode 100644 test/Hangfire.Correlate.Tests/HangfireServiceProviderTests.cs create mode 100644 test/Hangfire.Correlate.Tests/TestLogger.cs create mode 100644 test/Hangfire.Correlate.Tests/TestService.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d278b0b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,389 @@ +root = true + +# Editor config : https://editorconfig.org/ +# VS reference: : https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference?view=vs-2017 +# Resharper properties : https://www.jetbrains.com/help/resharper/EditorConfig_Index.html +# VSCode : https://github.com/editorconfig/editorconfig-vscode + +############################### +# Base settings # +############################### + +[*] +indent_style = space +indent_size = 4 +insert_final_newline = false + +[*.{cs,tt}] +charset = utf-8-bom +end_of_line = crlf +indent_style = tab + +[*.{csproj,props}] +charset = utf-8-bom +end_of_line = crlf +indent_size = 2 + +# Misc +[*.{js,json,yml}] +charset = utf-8 +indent_size = 2 +end_of_line = lf + +[*.md] +charset = utf-8 +indent_size = 2 +end_of_line = lf +trim_trailing_whitespace = false + +############################### +# .NET Coding Conventions # +############################### + +[*.{cs,vb}] +# Organize usings +dotnet_sort_system_directives_first = true + +# this. preferences +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = always:warning +dotnet_style_readonly_field = true:suggestion + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent + +############################### +# Naming Conventions # +############################### + +# Style Definitions +dotnet_naming_style.pascal_case_style.capitalization = pascal_case + +# Use PascalCase for constant fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +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.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const + +############################### +# C# Code Style Rules # +############################### + +[*.cs] +# var preferences +csharp_style_var_for_built_in_types = false:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = false:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = false:none +csharp_style_expression_bodied_constructors = false:none +csharp_style_expression_bodied_operators = false:none +csharp_style_expression_bodied_properties = true:none +csharp_style_expression_bodied_indexers = true:none +csharp_style_expression_bodied_accessors = true:none + +# Pattern-matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion + +# Null-checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion + +# Expression-level preferences +csharp_prefer_braces = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +############################### +# C# Formatting Rules # +############################### + +# New line preferences +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 +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false + +# Wrapping preferences +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true + +############################### +# Resharper custom rules # +############################### + +# Preserve Existing Formatting +keep_blank_lines_in_declarations = 1 +remove_blank_lines_near_braces_in_declarations = true +keep_blank_lines_in_code = 1 +remove_blank_lines_near_braces_in_code = true + +# Blank Lines +blank_lines_around_namespace = 1 +blank_lines_inside_namespace = 0 +blank_lines_around_type = 1 +blank_lines_inside_type = 0 +blank_lines_around_field = 0 +blank_lines_around_single_line_field = 0 +blank_lines_around_property = 0 +blank_lines_around_single_line_property = 0 +blank_lines_around_auto_property = 0 +blank_lines_around_single_line_auto_property = 0 +blank_lines_around_invocable = 1 +blank_lines_around_single_line_invocable = 0 +blank_lines_around_local_method = 1 +blank_lines_around_single_line_local_method = 0 +blank_lines_around_region = 1 +blank_lines_inside_region = 1 +blank_lines_between_using_groups = 0 +blank_lines_after_using_list = 1 +blank_lines_after_start_comment = 1 +blank_lines_before_single_line_comment = 0 +blank_lines_after_control_transfer_statements = 1 #check for block or each + +# _Braces Layout_ +brace_style = next_line +anonymous_method_declaration_braces = next_line +empty_block_style = multiline + +# _Line Breaks_ +# Preserve Existing Formatting +simple_embedded_statement_style = do_not_change +simple_case_statement_style = do_not_change +simple_embedded_block_style = do_not_change + +# Place on New Line +new_line_before_else = true +new_line_before_while = false +new_line_before_catch = true +new_line_before_finally = true + +# Line Wrapping +wrap_parameters_style = chop_if_long +wrap_before_declaration_lpar = true +wrap_after_declaration_lpar = true +wrap_arguments_style = chop_if_long +wrap_before_invocation_lpar = false +wrap_after_invocation_lpar = false +wrap_before_comma = false +wrap_before_arrow_with_expressions = false +wrap_after_dot_in_method_calls = false +wrap_chained_method_calls = chop_if_long +wrap_before_extends_colon = true +wrap_extends_list_style = wrap_if_long +wrap_for_stmt_header_style = chop_if_long +wrap_before_ternary_opsigns = true +wrap_ternary_expr_style = chop_if_long +wrap_linq_expressions = chop_if_long +wrap_before_binary_opsign = true +wrap_chained_binary_expressions = true +force_chop_compound_if_expression = false +force_chop_compound_while_expression = false +force_chop_compound_do_expression = false +wrap_multiple_type_parameer_constraints_style = chop_if_long +wrap_object_and_collection_initializer_style = chop_if_long #check +wrap_array_initializer_style = chop_if_long #check +wrap_before_first_type_parameter_constraint = true +wrap_before_type_parameter_langle = false + +# Other +place_abstract_accessorholder_on_single_line = true +place_simple_accessorholder_on_single_line = true +place_accessor_with_attrs_holder_on_single_line = true #check +place_simple_accessor_on_single_line = true +place_simple_method_on_single_line = false +place_simple_anonymousmethod_on_single_line = true +place_simple_initializer_on_single_line = true +place_type_attribute_on_same_line = false +place_method_attribute_on_same_line = false +place_accessorholder_attribute_on_same_line = false +place_simple_accessor_attribute_on_same_line = false +place_complex_accessor_attribute_on_same_line = false +place_field_attribute_on_same_line = false +place_constructor_initializer_on_same_line = true +place_type_constraints_on_same_line = false +allow_comment_after_lbrace = false + +# _Indentation_ +indent_switch_labels = true +indent_nested_usings_stmt = false +indent_nested_fixed_stmt = false +indent_nested_lock_stmt = false +indent_nested_for_stmt = true +indent_nested_foreach_stmt = true +indent_nested_while_stmt = true +indent_type_constraints = true +stick_comment = false +indent_method_decl_pars = inside +indent_invocation_pars = inside +indent_statement_pars = outside +indent_pars = outside + + +# Alignments +align_multiline_parameter = true +align_first_arg_by_paren = false +align_multiline_argument = false +align_multiline_extends_list = true +align_multiline_expression = false +align_multiline_binary_expressions_chain = false #check +align_multiline_calls_chain = false +align_multiline_array_and_object_initializer = false +indent_anonymous_method_block = false +align_multiline_for_stmt = true +align_multiple_declaration = true +align_multline_type_parameter_list = true +align_multline_type_parameter_constrains = true +align_linq_query = true + +int_align_fields = false +int_align_properties = false +int_align_methods = false +int_align_parameters = false +int_align_variables = false +int_align_assignments = false +int_align_nested_ternary = false +int_align_invocations = false +int_align_binary_expressions = false +int_align_comments = false + +outdent_binary_ops = true +outdent_dots = false +special_else_if_treatment = true + +# _Spaces_ +space_before_method_call_parentheses = false +space_before_empty_method_call_parentheses = false +space_before_method_parentheses = false +space_before_empty_method_parentheses = false +space_before_array_access_brackets = false +space_before_if_parentheses = true +space_before_while_parentheses = true +space_before_catch_parentheses = true +space_before_switch_parentheses = true +space_before_for_parentheses = true +space_before_foreach_parentheses = true +space_before_using_parentheses = true +space_before_lock_parentheses = true +space_before_typeof_parentheses = false +space_before_default_parentheses = false +space_before_checked_parentheses = false +space_before_fixed_parentheses = true +space_before_sizeof_parentheses = false +space_before_nameof_parentheses = false +space_before_type_parameter_angle = false +space_before_type_argument_angle = false +space_around_binary_operator = true +space_around_member_access_operator = false +space_after_logical_not_op = false +space_after_unary_minus_op = false +space_after_unary_plus_op = false +space_after_ampersand_op = false +space_after_asterik_op = false +space_within_parentheses = false +space_between_method_declaration_parameter_list_parentheses = false +space_between_method_declaration_empty_parameter_list_parentheses = false +space_between_method_call_parameter_list_parentheses = false +space_between_method_call_empty_parameter_list_parentheses = false +space_within_array_access_brackets = false +space_between_typecast_parentheses = false +space_between_parentheses_of_control_flow_statements = false +space_within_typeof_parentheses = false +space_within_default_parentheses = false +space_within_checked_parentheses = false +space_within_sizeof_parentheses = false +space_within_nameof_parentheses = false +space_within_type_parameter_angles = false +space_within_type_argument_angles = false +space_before_ternary_quest = true +space_after_ternary_quest = true +space_before_ternary_colon = true +space_after_ternary_colon = true +space_after_cast = false +space_near_postfix_and_prefix_op = false +space_before_comma = false +space_after_comma = true +space_before_semicolon_in_for_statement = false +space_after_semicolon_in_for_statement = true +space_before_attribute_colon = false +space_after_attribute_colon = true +space_before_colon_in_inheritance_clause = true +space_after_colon_in_inheritance_clause = true +space_around_member_access_operator = false +space_around_lambda_arrow = true +space_before_singleline_accessorholder = true +space_in_singleline_accessorholder = true +space_between_accessors_in_singleline_property = true +space_between_attribute_sections = false #check +space_withing_empty_braces = true +space_in_singleline_method = true +space_in_singleline_anonymous_method = true +space_within_attribute_brackets = false +space_before_array_rank_brackets = false +space_within_array_rank_brackets = false +space_within_array_rank_empty_brackets = false +space_within_single_line_array_initializer_braces = true +space_before_pointer_asterik_declaration = false +space_before_semicolon = false +space_before_colon_in_case = false +space_before_nullable_mark = false +space_before_type_parameter_constraint_colon = true +space_after_type_parameter_constraint_colon = true +space_around_alias_eq = true +space_before_trailing_comment = true +space_after_operator_keyword = true \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# 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 + +############################################################################### +# 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 + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d11813 --- /dev/null +++ b/.gitignore @@ -0,0 +1,264 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +project.fragment.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.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 + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# 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 +# TODO: 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 +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# 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 + +# 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 +node_modules/ +orleans.codegen.cs + +# 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 + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml + +# CodeRush +.cr/ + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Launch settings in unit tests +/test/**/launchSettings.json diff --git a/Hangfire.Correlate.sln b/Hangfire.Correlate.sln new file mode 100644 index 0000000..16b1984 --- /dev/null +++ b/Hangfire.Correlate.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29025.244 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0DFB1AB7-052F-4CEA-8009-3A7EE69828EC}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + appveyor.yml = appveyor.yml + Changelog.md = Changelog.md + src\package.props = src\package.props + README.md = README.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{AF2A3408-B291-46D8-9CCC-DF6B2D8A2BCB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{AC76FA94-9C63-4885-9171-2FBA0BF4D2F5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hangfire.Correlate", "src\Hangfire.Correlate\Hangfire.Correlate.csproj", "{25C5F8B0-C030-40A8-A6CF-41D338FB4686}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Hangfire.Correlate.Tests", "test\Hangfire.Correlate.Tests\Hangfire.Correlate.Tests.csproj", "{FEF21A28-C5AB-46AE-929F-91FE25815B12}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {25C5F8B0-C030-40A8-A6CF-41D338FB4686}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25C5F8B0-C030-40A8-A6CF-41D338FB4686}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25C5F8B0-C030-40A8-A6CF-41D338FB4686}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25C5F8B0-C030-40A8-A6CF-41D338FB4686}.Release|Any CPU.Build.0 = Release|Any CPU + {FEF21A28-C5AB-46AE-929F-91FE25815B12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FEF21A28-C5AB-46AE-929F-91FE25815B12}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FEF21A28-C5AB-46AE-929F-91FE25815B12}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FEF21A28-C5AB-46AE-929F-91FE25815B12}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {25C5F8B0-C030-40A8-A6CF-41D338FB4686} = {AF2A3408-B291-46D8-9CCC-DF6B2D8A2BCB} + {FEF21A28-C5AB-46AE-929F-91FE25815B12} = {AC76FA94-9C63-4885-9171-2FBA0BF4D2F5} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6C8F5909-4144-4A5B-B314-6B6AA10D188C} + EndGlobalSection +EndGlobal diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..26cab69 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,190 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright 2017 Martijn Bodeman + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/src/Hangfire.Correlate/CorrelateFilterAttribute.cs b/src/Hangfire.Correlate/CorrelateFilterAttribute.cs new file mode 100644 index 0000000..2f8b482 --- /dev/null +++ b/src/Hangfire.Correlate/CorrelateFilterAttribute.cs @@ -0,0 +1,58 @@ +using System; +using Correlate; +using Hangfire.Client; +using Hangfire.Common; +using Hangfire.Server; + +namespace Hangfire.Correlate +{ + internal class CorrelateFilterAttribute : JobFilterAttribute, IClientFilter, IServerFilter + { + private const string CorrelationIdKey = "CorrelationId"; + private const string CorrelateActivityKey = "Correlate-Activity"; + + private readonly ICorrelationContextAccessor _correlationContextAccessor; + private readonly IActivityFactory _activityFactory; + + public CorrelateFilterAttribute(ICorrelationContextAccessor correlationContextAccessor, IActivityFactory activityFactory) + { + _correlationContextAccessor = correlationContextAccessor ?? throw new ArgumentNullException(nameof(correlationContextAccessor)); + _activityFactory = activityFactory ?? throw new ArgumentNullException(nameof(activityFactory)); + } + + public void OnCreating(CreatingContext filterContext) + { + // Assign correlation id to job if job is started in correlation context. + string correlationId = _correlationContextAccessor.CorrelationContext?.CorrelationId; + if (!string.IsNullOrWhiteSpace(correlationId)) + { + filterContext.SetJobParameter(CorrelationIdKey, correlationId); + } + } + + public void OnCreated(CreatedContext filterContext) + { + } + + public void OnPerforming(PerformingContext filterContext) + { + string correlationId = filterContext.GetJobParameter(CorrelationIdKey) ?? filterContext.BackgroundJob.Id; + IActivity activity = _activityFactory.CreateActivity(); + CorrelationContext correlationContext = activity.Start(correlationId); + if (correlationContext != null) + { + filterContext.Items[CorrelationIdKey] = correlationContext.CorrelationId; + filterContext.Items[CorrelateActivityKey] = activity; + } + } + + public void OnPerformed(PerformedContext filterContext) + { + if (filterContext.Items.TryGetValue(CorrelateActivityKey, out object objActivity) + && objActivity is IActivity activity) + { + activity.Stop(); + } + } + } +} diff --git a/src/Hangfire.Correlate/Hangfire.Correlate.csproj b/src/Hangfire.Correlate/Hangfire.Correlate.csproj new file mode 100644 index 0000000..142113e --- /dev/null +++ b/src/Hangfire.Correlate/Hangfire.Correlate.csproj @@ -0,0 +1,21 @@ + + + + netstandard2.0;netstandard1.3;net46 + false + + + + + + + https://github.com/skwasjer/Hangfire.Correlate + hangfire, correlationid, correlation, correlate, causation + + + + + + + + diff --git a/src/Hangfire.Correlate/IGlobalConfigurationExtensions.cs b/src/Hangfire.Correlate/IGlobalConfigurationExtensions.cs new file mode 100644 index 0000000..bb3c937 --- /dev/null +++ b/src/Hangfire.Correlate/IGlobalConfigurationExtensions.cs @@ -0,0 +1,67 @@ +using System; +using Correlate; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Hangfire.Correlate +{ + /// + /// Extensions for + /// + public static class IGlobalConfigurationExtensions + { + /// + /// Use Correlate with Hangfire to manage the correlation context. + /// + /// The global configuration. + /// The service provider with Correlate dependencies registered. + public static IGlobalConfiguration UseCorrelate(this IGlobalConfiguration configuration, IServiceProvider serviceProvider) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + if (serviceProvider == null) + { + throw new ArgumentNullException(nameof(serviceProvider)); + } + + try + { + return configuration.UseFilter(ActivatorUtilities.CreateInstance(serviceProvider)); + } + catch (SystemException ex) + { + throw new InvalidOperationException("Failed to register Correlate with Hangfire. Please ensure `.AddCorrelate()` is called on the service collection.", ex); + } + } + + /// + /// Use Correlate with Hangfire to manage the correlation context. + /// + /// The global configuration. + /// The logger factory. + public static IGlobalConfiguration UseCorrelate(this IGlobalConfiguration configuration, ILoggerFactory loggerFactory) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + var correlationContextAccessor = new CorrelationContextAccessor(); + var correlationManager = new CorrelationManager( + new CorrelationContextFactory(correlationContextAccessor), + new GuidCorrelationIdFactory(), correlationContextAccessor, + loggerFactory.CreateLogger() + ); + var correlateFilterAttribute = new CorrelateFilterAttribute(correlationContextAccessor, correlationManager); + return configuration.UseFilter(correlateFilterAttribute); + } + } +} diff --git a/src/Hangfire.Correlate/Properties/InternalsVisibleTo.cs b/src/Hangfire.Correlate/Properties/InternalsVisibleTo.cs new file mode 100644 index 0000000..453e7d3 --- /dev/null +++ b/src/Hangfire.Correlate/Properties/InternalsVisibleTo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Hangfire.Correlate.Tests")] \ No newline at end of file diff --git a/src/package.props b/src/package.props new file mode 100644 index 0000000..7b22112 --- /dev/null +++ b/src/package.props @@ -0,0 +1,26 @@ + + + + 1.0.0 + 1.0.0 + 1.0.0.0 + 1.0.0.0 + 1.0.0 + + + + Martijn Bodeman + + Copyright © 2019 + Apache-2.0 + https://github.com/skwasjer/Hangfire.Correlate + git + https://raw.githubusercontent.com/skwasjer/Hangfire.Correlate/master/assets/PackageIcon64.png + + + + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + NU1605;CS1591 + + + \ No newline at end of file diff --git a/test/Hangfire.Correlate.Tests/BackgroundTestExecutor.cs b/test/Hangfire.Correlate.Tests/BackgroundTestExecutor.cs new file mode 100644 index 0000000..de5e82d --- /dev/null +++ b/test/Hangfire.Correlate.Tests/BackgroundTestExecutor.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Correlate; +using Hangfire.Server; +using Xunit.Abstractions; + +namespace Hangfire.Correlate +{ + public class BackgroundTestExecutor + { + private readonly TestService _testService; + private readonly ICorrelationContextAccessor _correlationContextAccessor; + private readonly ITestOutputHelper _testOutputHelper; + + internal BackgroundTestExecutor() + { + } + + public BackgroundTestExecutor(TestService testService, ICorrelationContextAccessor correlationContextAccessor, ICollection jobState, ITestOutputHelper testOutputHelper) + { + _testService = testService ?? throw new ArgumentNullException(nameof(testService)); + _correlationContextAccessor = correlationContextAccessor ?? throw new ArgumentNullException(nameof(correlationContextAccessor)); + _testOutputHelper = testOutputHelper ?? throw new ArgumentNullException(nameof(testOutputHelper)); + + jobState.Add(this); + } + + /// + /// Gets the job id. + /// + public string JobId { get; set; } + + /// + /// Gets whether the job has completed. + /// + public bool JobHasCompleted { get; set; } + + /// + /// Gets the correlation id that was in the correlation context while the job was running. + /// + public string CorrelationId { get; set; } + + [AutomaticRetry(Attempts = 0)] + public async Task RunAsync(int timeoutInMillis, PerformContext performContext) + { + JobId = performContext.BackgroundJob.Id; + CorrelationId = _correlationContextAccessor.CorrelationContext.CorrelationId; + _testOutputHelper.WriteLine("Executing job {0} with correlation id {1}", JobId, CorrelationId); + await Task.Delay(timeoutInMillis / 2); + await _testService.CallApi(); + await Task.Delay(timeoutInMillis / 2); + JobHasCompleted = true; + } + } +} \ No newline at end of file diff --git a/test/Hangfire.Correlate.Tests/Extensions/LoggingExtensions.cs b/test/Hangfire.Correlate.Tests/Extensions/LoggingExtensions.cs new file mode 100644 index 0000000..f4643b3 --- /dev/null +++ b/test/Hangfire.Correlate.Tests/Extensions/LoggingExtensions.cs @@ -0,0 +1,42 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Hangfire.Correlate.Extensions +{ + public static class LoggingExtensions + { +#if NETCOREAPP1_1 || NETFRAMEWORK + public static IServiceCollection ForceEnableLogging(this IServiceCollection services) + { + return services + .AddSingleton(s => new LoggerFactory().ForceEnableLogging()) + .AddLogging(); + } +#else + public static IServiceCollection ForceEnableLogging(this IServiceCollection services) + { + return services.AddLogging(logging => logging.AddProvider(new TestLoggerProvider())); + } +#endif + + public static ILoggerFactory ForceEnableLogging(this ILoggerFactory loggerFactory) + { + loggerFactory.AddProvider(new TestLoggerProvider()); + return loggerFactory; + } + + private class TestLoggerProvider : ILoggerProvider + { + private TestLogger _testLogger; + + public void Dispose() + { + } + + public ILogger CreateLogger(string categoryName) + { + return _testLogger ?? (_testLogger = new TestLogger()); + } + } + } +} \ No newline at end of file diff --git a/test/Hangfire.Correlate.Tests/Extensions/TaskExtensions.cs b/test/Hangfire.Correlate.Tests/Extensions/TaskExtensions.cs new file mode 100644 index 0000000..6bda838 --- /dev/null +++ b/test/Hangfire.Correlate.Tests/Extensions/TaskExtensions.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; + +namespace Hangfire.Correlate.Extensions +{ + public static class TaskExtensions + { + public static async Task WithTimeout(this Task task, int millisecondsTimeout = 5000) + { + await Task.WhenAny(task, Task.Delay(millisecondsTimeout)); + if (task.IsCompleted) + { + return await task; + } + + throw new TimeoutException(); + } + } +} diff --git a/test/Hangfire.Correlate.Tests/Hangfire.Correlate.Tests.csproj b/test/Hangfire.Correlate.Tests/Hangfire.Correlate.Tests.csproj new file mode 100644 index 0000000..e15dc3a --- /dev/null +++ b/test/Hangfire.Correlate.Tests/Hangfire.Correlate.Tests.csproj @@ -0,0 +1,37 @@ + + + + netcoreapp2.2 + true + false + Hangfire.Correlate + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/Hangfire.Correlate.Tests/HangfireBuiltInConfigurationTests.cs b/test/Hangfire.Correlate.Tests/HangfireBuiltInConfigurationTests.cs new file mode 100644 index 0000000..2568e31 --- /dev/null +++ b/test/Hangfire.Correlate.Tests/HangfireBuiltInConfigurationTests.cs @@ -0,0 +1,32 @@ +using System; +using Hangfire.MemoryStorage; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Xunit; +using Xunit.Abstractions; + +namespace Hangfire.Correlate +{ + /// + /// Tests Hangfire integration with the overload. + /// + /// + /// Parallel test execution is not supported since we use memory storage with Hangfire that is being set into a static property Storage.Current. When tests are run in parallel, the test that last set the storage will win, while the others will break. This is also true for other Hangfire dependencies, but they do not directly affect our tests atm. + /// + [Collection(nameof(HangfireIntegrationTests))] + public class HangfireBuiltInConfigurationTests : HangfireIntegrationTests + { + public HangfireBuiltInConfigurationTests(ITestOutputHelper toh) : base(toh, services => + services + .AddHangfire((s, config) => + { + toh.WriteLine(nameof(HangfireServiceProviderTests) + DateTime.Now.Ticks); + config + .UseCorrelate(s.GetRequiredService()) + .UseMemoryStorage(); + }) + ) + { + } + } +} \ No newline at end of file diff --git a/test/Hangfire.Correlate.Tests/HangfireIntegrationTests.cs b/test/Hangfire.Correlate.Tests/HangfireIntegrationTests.cs new file mode 100644 index 0000000..1e7376f --- /dev/null +++ b/test/Hangfire.Correlate.Tests/HangfireIntegrationTests.cs @@ -0,0 +1,198 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Threading.Tasks; +using Correlate; +using Correlate.DependencyInjection; +using Correlate.Http; +using FluentAssertions; +using Hangfire.Correlate.Extensions; +using Hangfire.Storage; +using Hangfire.Storage.Monitoring; +using Microsoft.Extensions.DependencyInjection; +using MockHttp; +using Xunit; +using Xunit.Abstractions; +using CorrelationManager = Correlate.CorrelationManager; + +namespace Hangfire.Correlate +{ + public abstract class HangfireIntegrationTests : IDisposable + { + private readonly ITestOutputHelper _testOutputHelper; + private readonly ServiceProvider _services; + private readonly JobStorage _jobStorage; + private readonly IBackgroundJobClient _client; + private readonly CorrelationManager _correlationManager; + private readonly ICollection _executedJobs; + private readonly MockHttpHandler _mockHttp; + + protected HangfireIntegrationTests(ITestOutputHelper testOutputHelper, Action configureServices) + { + _testOutputHelper = testOutputHelper; + _executedJobs = new List(); + + _mockHttp = new MockHttpHandler(); + _mockHttp.Fallback.Respond(HttpStatusCode.OK); + + // Register Correlate. The provided action should register Hangfire and tell Hangfire to use Correlate. + IServiceCollection serviceCollection = new ServiceCollection() + .AddCorrelate(); + configureServices(serviceCollection); + + // Below, dependencies for test only. + + // Register a typed client which is used by the job to call an endpoint. + // We use it to assert the request header contains the correlation id. + serviceCollection + .AddHttpClient(client => + { + client.BaseAddress = new Uri("http://0.0.0.0"); + }) + .ConfigurePrimaryHttpMessageHandler(() => _mockHttp) + .CorrelateRequests(); + + serviceCollection + .AddSingleton() + .AddSingleton(_executedJobs) + .AddSingleton(testOutputHelper) + .ForceEnableLogging(); + + _services = serviceCollection.BuildServiceProvider(); + + _jobStorage = _services.GetRequiredService(); + _client = _services.GetRequiredService(); + _correlationManager = _services.GetRequiredService(); + } + + public void Dispose() + { + _services.Dispose(); + } + + [Fact] + public async Task Given_job_is_queued_outside_correlationContext_should_use_jobId_as_correlationId() + { + string jobId = _client.Enqueue(job => job.RunAsync(250, null)); + var expectedJob = new BackgroundTestExecutor + { + JobId = jobId, + CorrelationId = jobId, + JobHasCompleted = true + }; + + // Act + await WaitUntilJobCompletedAsync(jobId); + + // Assert + _executedJobs.Should().BeEquivalentTo( + new List { expectedJob }, + "no correlation context exists, so the job id should be used when performing the job" + ); + } + + [Fact] + public async Task Given_job_is_queued_inside_correlationContext_should_use_correlationId_of_correlation_context() + { + const string correlationId = "my-id"; + var expectedJob = new BackgroundTestExecutor + { + CorrelationId = correlationId, + JobHasCompleted = true + }; + + // Act + await _correlationManager.CorrelateAsync(correlationId, + async () => + { + await Task.Yield(); + expectedJob.JobId = _client.Enqueue(job => job.RunAsync(250, null)); + }); + + await WaitUntilJobCompletedAsync(expectedJob.JobId); + + // Assert + _executedJobs.Should().BeEquivalentTo( + new List { expectedJob }, + "a correlation context exists, so the correlation id should be used when performing the job" + ); + } + + [Fact] + public async Task Given_job_is_queued_outside_correlationContext_should_put_correlationId_in_http_header() + { + string jobId = _client.Enqueue(job => job.RunAsync(250, null)); + + _mockHttp + .When(matching => matching.Header(CorrelationHttpHeaders.CorrelationId, jobId)) + .Callback(r => _testOutputHelper.WriteLine("Request sent with correlation id: {0}", jobId)) + .Respond(HttpStatusCode.OK) + .Verifiable(); + + // Act + await WaitUntilJobCompletedAsync(jobId); + + // Assert + _mockHttp.Verify(); + } + + [Fact] + public async Task Given_job_is_queued_inside_correlationContext_should_put_correlationId_in_http_header() + { + const string correlationId = "my-id"; + string jobId = null; + + _mockHttp + .When(matching => matching.Header(CorrelationHttpHeaders.CorrelationId, correlationId)) + .Callback(r => _testOutputHelper.WriteLine("Request sent with correlation id: {0}", correlationId)) + .Respond(HttpStatusCode.OK) + .Verifiable(); + + // Act + await _correlationManager.CorrelateAsync(correlationId, + async () => + { + await Task.Yield(); + jobId = _client.Enqueue(job => job.RunAsync(250, null)); + }); + + await WaitUntilJobCompletedAsync(jobId); + + // Assert + _mockHttp.Verify(); + } + + private async Task WaitUntilJobCompletedAsync(string jobId, int maxWaitInMilliseconds = 5000) + { + // Request the server to initialize it and to start processing. + _services.GetRequiredService(); + + IMonitoringApi monitoringApi = _jobStorage.GetMonitoringApi(); + + Stopwatch sw = Stopwatch.StartNew(); + JobDetailsDto jobDetails = null; + while ((jobDetails == null || jobDetails.History.All(s => s.StateName != "Succeeded")) && (sw.Elapsed.TotalMilliseconds < maxWaitInMilliseconds || Debugger.IsAttached)) + { + await Task.Delay(25); + jobDetails = monitoringApi.JobDetails(jobId); + if (monitoringApi.FailedCount() > 0) + { + break; + } + } + + FailedJobDto failedJob = monitoringApi + .FailedJobs(0, int.MaxValue) + .Select(j => j.Value) + .FirstOrDefault(); + if (failedJob != null) + { + throw new InvalidOperationException($"Job failed: {failedJob.ExceptionDetails}."); + } + + _client.Delete(jobId); + } + } +} diff --git a/test/Hangfire.Correlate.Tests/HangfireServiceProviderTests.cs b/test/Hangfire.Correlate.Tests/HangfireServiceProviderTests.cs new file mode 100644 index 0000000..2304d14 --- /dev/null +++ b/test/Hangfire.Correlate.Tests/HangfireServiceProviderTests.cs @@ -0,0 +1,31 @@ +using System; +using Hangfire.MemoryStorage; +using Xunit; +using Xunit.Abstractions; + +namespace Hangfire.Correlate +{ + /// + /// Tests Hangfire integration with the overload. + /// + /// + /// Parallel test execution is not supported since we use memory storage with Hangfire that is being set into a static property Storage.Current. When tests are run in parallel, the test that last set the storage will win, while the others will break. This is also true for other Hangfire dependencies, but they do not directly affect our tests atm. + /// + + [Collection(nameof(HangfireIntegrationTests))] + public class HangfireServiceProviderTests : HangfireIntegrationTests + { + public HangfireServiceProviderTests(ITestOutputHelper toh) : base(toh, services => + services + .AddHangfire((s, config) => + { + toh.WriteLine(nameof(HangfireServiceProviderTests) + DateTime.Now.Ticks); + config + .UseCorrelate(s) + .UseMemoryStorage(); + }) + ) + { + } + } +} diff --git a/test/Hangfire.Correlate.Tests/TestLogger.cs b/test/Hangfire.Correlate.Tests/TestLogger.cs new file mode 100644 index 0000000..a48bc30 --- /dev/null +++ b/test/Hangfire.Correlate.Tests/TestLogger.cs @@ -0,0 +1,38 @@ +using System; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions.Internal; + +namespace Hangfire.Correlate +{ + public class TestLogger : TestLogger, ILogger + { + public TestLogger(bool isEnabled = true) + : base(isEnabled) + { + } + } + + public class TestLogger : ILogger + { + private readonly bool _isEnabled; + + public TestLogger(bool isEnabled = true) + { + _isEnabled = isEnabled; + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + } + + public bool IsEnabled(LogLevel logLevel) + { + return _isEnabled; + } + + public IDisposable BeginScope(TState state) + { + return NullScope.Instance; + } + } +} \ No newline at end of file diff --git a/test/Hangfire.Correlate.Tests/TestService.cs b/test/Hangfire.Correlate.Tests/TestService.cs new file mode 100644 index 0000000..5184fc0 --- /dev/null +++ b/test/Hangfire.Correlate.Tests/TestService.cs @@ -0,0 +1,20 @@ +using System.Net.Http; +using System.Threading.Tasks; + +namespace Hangfire.Correlate +{ + public class TestService + { + private readonly HttpClient _client; + + public TestService(HttpClient client) + { + _client = client; + } + + public Task CallApi() + { + return _client.GetStringAsync(""); + } + } +} \ No newline at end of file