From a9672a343cbea55fb0c4fc058a20f92765e4d586 Mon Sep 17 00:00:00 2001 From: kdcllc Date: Mon, 8 Apr 2019 23:14:35 -0400 Subject: [PATCH 1/9] fix async issue with invalidating the token --- .../Resilience/ResilientForceClient.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/CometD.NetCore.Salesforce/Resilience/ResilientForceClient.cs b/src/CometD.NetCore.Salesforce/Resilience/ResilientForceClient.cs index 5e79acd..38d2e84 100644 --- a/src/CometD.NetCore.Salesforce/Resilience/ResilientForceClient.cs +++ b/src/CometD.NetCore.Salesforce/Resilience/ResilientForceClient.cs @@ -428,7 +428,7 @@ private IAsyncPolicy GetAuthenticationRetryPolicy() .Handle(x => x.Message.Contains("ErrorCode INVALID_SESSION_ID")) .RetryAsync( retryCount: _options.Retry, - onRetry: (ex, count, context) => + onRetryAsync: async (ex, count, context) => { var methodName = context[PolicyContextMethod] ?? "MethodNotSpecified"; @@ -439,7 +439,8 @@ private IAsyncPolicy GetAuthenticationRetryPolicy() _options.Retry, context.PolicyKey, ex.Message); - _forceClient.Invalidate(); + + await _forceClient.Invalidate(); }) .WithPolicyKey($"{nameof(ResilientForceClient)}RetryAsync"); } From 3bf6c67ecad7257f993fdd4d96ebaddd92e04121 Mon Sep 17 00:00:00 2001 From: kdcllc Date: Mon, 8 Apr 2019 23:35:21 -0400 Subject: [PATCH 2/9] add polly policy to the extension method. --- .../StreamingClientExtensions.cs | 47 +++++++++++++++---- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/CometD.NetCore.Salesforce/StreamingClientExtensions.cs b/src/CometD.NetCore.Salesforce/StreamingClientExtensions.cs index 3429d9d..2ff3dc4 100644 --- a/src/CometD.NetCore.Salesforce/StreamingClientExtensions.cs +++ b/src/CometD.NetCore.Salesforce/StreamingClientExtensions.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Options; using NetCoreForce.Client; using NetCoreForce.Client.Models; +using Polly; using System; using System.Threading; @@ -29,16 +30,30 @@ public static IServiceCollection AddResilientStreamingClient( { var options = sp.GetRequiredService>().Value; - TimeSpan.TryParse(options.TokenExpiration, out var tokenExperation); + if (!TimeSpan.TryParse(options.TokenExpiration, out var tokenExperation)) + { + tokenExperation = TimeSpan.FromHours(1); + } return new AsyncExpiringLazy(async data => { if (data.Result == null || DateTime.UtcNow > data.ValidUntil.Subtract(TimeSpan.FromSeconds(30))) { - var authClient = new AuthenticationClient(); + var policy = Policy + .Handle() + .WaitAndRetryAsync( + retryCount: options.Retry, + sleepDurationProvider: (retryAttempt) => TimeSpan.FromSeconds(Math.Pow(options.BackoffPower, retryAttempt))); + + var authClient = await policy.ExecuteAsync(async () => + { + var auth = new AuthenticationClient(); + + await auth.TokenRefreshAsync(options.RefreshToken, options.ClientId); - await authClient.TokenRefreshAsync(options.RefreshToken, options.ClientId); + return auth; + }); return new AsyncExpirationValue { @@ -55,21 +70,33 @@ public static IServiceCollection AddResilientStreamingClient( { var options = sp.GetRequiredService>().Value; - TimeSpan.TryParse(options.TokenExpiration, out var tokenExperation); + if (!TimeSpan.TryParse(options.TokenExpiration, out var tokenExperation)) + { + tokenExperation = TimeSpan.FromHours(1); + } return new AsyncExpiringLazy(async data => { if (data.Result == null || DateTime.UtcNow > data.ValidUntil.Subtract(TimeSpan.FromSeconds(30))) { - var authClient = new AuthenticationClient(); + var policy = Policy + .Handle() + .WaitAndRetryAsync( + retryCount: options.Retry, + sleepDurationProvider: (retryAttempt) => TimeSpan.FromSeconds(Math.Pow(options.BackoffPower, retryAttempt))); + + var client = await policy.ExecuteAsync(async () => + { + var authClient = new AuthenticationClient(); - await authClient.TokenRefreshAsync(options.RefreshToken, options.ClientId); + await authClient.TokenRefreshAsync(options.RefreshToken, options.ClientId); - var client = new ForceClient( - authClient.AccessInfo.InstanceUrl, - authClient.ApiVersion, - authClient.AccessInfo.AccessToken); + return new ForceClient( + authClient.AccessInfo.InstanceUrl, + authClient.ApiVersion, + authClient.AccessInfo.AccessToken); + }); return new AsyncExpirationValue { From f0fe20df9c2a72e06eaeccdc2fc5897908926cf4 Mon Sep 17 00:00:00 2001 From: kdcllc Date: Fri, 12 Apr 2019 15:54:23 -0400 Subject: [PATCH 3/9] add support for url login and text copy of the refresh token --- .editorconfig | 100 ++++++++++++++++-- build/settings.props | 2 +- src/AuthApp/AuthApp.csproj | 1 + src/AuthApp/Host/HttpServer.cs | 17 ++- src/AuthApp/Host/SfConfig.cs | 3 + src/AuthApp/HostBuilderExtensions.cs | 12 ++- src/AuthApp/HostBuilderOptions.cs | 1 + src/AuthApp/Program.cs | 6 +- src/AuthApp/Properties/launchSettings.json | 5 +- src/AuthApp/TokenGeneratorCommand.cs | 18 ++-- .../ForceClient/AuthenticationClientProxy.cs | 2 + .../ForceClient/ForceClientProxy.cs | 3 + .../ForceClient/IAuthenticationClientProxy.cs | 2 + .../ForceClient/IForceClientProxy.cs | 2 + .../IStreamingClient.cs | 1 + .../Resilience/IResilientForceClient.cs | 5 +- .../Resilience/ResilientForceClient.cs | 3 + .../ResilientStreamingClient.cs | 5 +- .../StreamingClient.cs | 5 +- .../StreamingClientExtensions.cs | 43 ++++++-- src/TestApp/Program.cs | 2 + test/CometD.UnitTest/UnitTest1.cs | 6 +- 22 files changed, 199 insertions(+), 45 deletions(-) diff --git a/.editorconfig b/.editorconfig index f7fd2f9..c7cab7f 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,46 +1,56 @@ # EditorConfig is awesome: https://EditorConfig.org # top-most EditorConfig file root = true + # Unix-style newlines with a newline ending every file [*] end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true + # Matches multiple files with brace expansion notation # Set default charset [*.js] charset = utf-8 + # Solution Files [*.sln] indent_style = tab + # XML Project Files [*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj}] indent_size = 2 + # YAML Files [*.{yml,yaml}] indent_size = 2 indent_style = space + # Markdown Files [*.md] trim_trailing_whitespace = false + # .NET Code Style Settings [*.{cs,csx,cake,vb}] + # "this." and "Me." qualifiers # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#this_and_me dotnet_style_qualification_for_field = false:warning dotnet_style_qualification_for_property = false:warning dotnet_style_qualification_for_method = false:warning dotnet_style_qualification_for_event = false:warning + # Language keywords instead of framework type names for type references # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#language_keywords dotnet_style_predefined_type_for_locals_parameters_members = true:warning dotnet_style_predefined_type_for_member_access = true:warning + # Modifier preferences # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#normalize_modifiers dotnet_style_require_accessibility_modifiers = always:warning -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async -visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning dotnet_style_readonly_field = true:warning + # Expression-level preferences # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#expression_level dotnet_style_object_initializer = true:warning @@ -50,13 +60,16 @@ dotnet_style_prefer_inferred_tuple_names = true:warning dotnet_style_prefer_inferred_anonymous_type_member_names = true:warning dotnet_style_prefer_auto_properties = true:warning dotnet_style_prefer_is_null_check_over_reference_equality_method = true:warning + # Null-checking preferences # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#null_checking dotnet_style_coalesce_expression = true:warning dotnet_style_null_propagation = true:warning + # Other (Undocumented) dotnet_style_prefer_conditional_expression_over_return = false dotnet_style_prefer_conditional_expression_over_assignment = false + # C# Code Style Settings [*.{cs,csx,cake}] # Implicit and explicit types @@ -64,6 +77,7 @@ dotnet_style_prefer_conditional_expression_over_assignment = false csharp_style_var_for_built_in_types = true:warning csharp_style_var_when_type_is_apparent = true:warning csharp_style_var_elsewhere = true:warning + # Expression-bodied members # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#expression_bodied_members csharp_style_expression_bodied_methods = false:warning @@ -72,32 +86,41 @@ csharp_style_expression_bodied_operators = true:warning csharp_style_expression_bodied_properties = true:warning csharp_style_expression_bodied_indexers = true:warning csharp_style_expression_bodied_accessors = true:warning + # Pattern matching # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#pattern_matching csharp_style_pattern_matching_over_is_with_cast_check = true:warning csharp_style_pattern_matching_over_as_with_null_check = true:warning + # Inlined variable declarations # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#inlined_variable_declarations csharp_style_inlined_variable_declaration = true:warning + # Expression-level preferences # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#expression_level_csharp csharp_prefer_simple_default_expression = true:warning csharp_style_deconstructed_variable_declaration = true:warning csharp_style_pattern_local_over_anonymous_function = true:warning + # "Null" checking preferences # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#null_checking_csharp csharp_style_throw_expression = true:warning csharp_style_conditional_delegate_call = true:warning + # Code block preferences # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#code_block csharp_prefer_braces = true:warning + ############################# # .NET Formatting Conventions # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#formatting-conventions ############################# + # Organize usings # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#usings -dotnet_sort_system_directives_first = true:warning +dotnet_sort_system_directives_first = true +dotnet_separate_import_directive_groups = true + # C# formatting settings # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#c-formatting-settings csharp_new_line_before_open_brace = all @@ -107,11 +130,13 @@ csharp_new_line_before_finally = true:warning csharp_new_line_before_members_in_object_initializers = true:warning csharp_new_line_before_members_in_anonymous_types = true:warning csharp_new_line_between_query_expression_clauses = true:warning + # Indentation options # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#indent csharp_indent_case_contents = true:warning csharp_indent_switch_labels = true:warning csharp_indent_labels = no_change:warning + # Spacing options # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#spacing csharp_space_after_cast = false:warning @@ -125,13 +150,16 @@ csharp_space_around_binary_operators = before_and_after:warning csharp_space_between_method_declaration_empty_parameter_list_parentheses = false:warning csharp_space_between_method_call_name_and_opening_parenthesis = false:warning csharp_space_between_method_call_empty_parameter_list_parentheses = false:warning + # Wrapping options # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference#wrapping csharp_preserve_single_line_statements = false:warning csharp_preserve_single_line_blocks = true:warning + # More Indentation options (Undocumented) csharp_indent_block_contents = true:warning csharp_indent_braces = false:warning + # Spacing Options (Undocumented) csharp_space_after_comma = true:warning csharp_space_after_dot = false:warning @@ -144,89 +172,145 @@ csharp_space_before_open_square_brackets = false:warning csharp_space_between_empty_square_brackets = false:warning csharp_space_between_method_declaration_name_and_open_parenthesis = false:warning csharp_space_between_square_brackets = false:warning + ######################### # .NET Naming conventions # https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-naming-conventions ######################### + [*.{cs,csx,cake,vb}] # Naming Symbols # constant_fields - Define constant fields dotnet_naming_symbols.constant_fields.applicable_kinds = field dotnet_naming_symbols.constant_fields.required_modifiers = const + # non_private_readonly_fields - Define public, internal and protected readonly fields dotnet_naming_symbols.non_private_readonly_fields.applicable_accessibilities = public, internal, protected dotnet_naming_symbols.non_private_readonly_fields.applicable_kinds = field dotnet_naming_symbols.non_private_readonly_fields.required_modifiers = readonly + # static_readonly_fields - Define static and readonly fields dotnet_naming_symbols.static_readonly_fields.applicable_kinds = field dotnet_naming_symbols.static_readonly_fields.required_modifiers = static, readonly + # private_readonly_fields - Define private readonly fields dotnet_naming_symbols.private_readonly_fields.applicable_accessibilities = private dotnet_naming_symbols.private_readonly_fields.applicable_kinds = field dotnet_naming_symbols.private_readonly_fields.required_modifiers = readonly + # public_internal_fields - Define public and internal fields dotnet_naming_symbols.public_internal_fields.applicable_accessibilities = public, internal dotnet_naming_symbols.public_internal_fields.applicable_kinds = field + # private_protected_fields - Define private and protected fields dotnet_naming_symbols.private_protected_fields.applicable_accessibilities = private, protected dotnet_naming_symbols.private_protected_fields.applicable_kinds = field + +# private_fields - Define private and protected fields +dotnet_naming_symbols.private_fields.applicable_accessibilities = private +dotnet_naming_symbols.private_fields.applicable_kinds = field + # public_symbols - Define any public symbol dotnet_naming_symbols.public_symbols.applicable_accessibilities = public, internal, protected, protected_internal dotnet_naming_symbols.public_symbols.applicable_kinds = method, property, event, delegate + # parameters - Defines any parameter dotnet_naming_symbols.parameters.applicable_kinds = parameter + # non_interface_types - Defines class, struct, enum and delegate types dotnet_naming_symbols.non_interface_types.applicable_kinds = class, struct, enum, delegate + # interface_types - Defines interfaces dotnet_naming_symbols.interface_types.applicable_kinds = interface + +######################### # Naming Styles +######################### + # camel_case - Define the camelCase style dotnet_naming_style.camel_case.capitalization = camel_case + +# underscore_camel_case _camelCase style +dotnet_naming_style.underscore_camel_case.required_prefix = _ +dotnet_naming_style.underscore_camel_case.capitalization = camel_case + # pascal_case - Define the Pascal_case style dotnet_naming_style.pascal_case.capitalization = pascal_case + # first_upper - The first character must start with an upper-case character dotnet_naming_style.first_upper.capitalization = first_word_upper + # prefix_interface_interface_with_i - Interfaces must be PascalCase and the first character of an interface must be an 'I' dotnet_naming_style.prefix_interface_interface_with_i.capitalization = pascal_case dotnet_naming_style.prefix_interface_interface_with_i.required_prefix = I + +######################### # Naming Rules +######################### + # Constant fields must be PascalCase dotnet_naming_rule.constant_fields_must_be_pascal_case.severity = warning dotnet_naming_rule.constant_fields_must_be_pascal_case.symbols = constant_fields dotnet_naming_rule.constant_fields_must_be_pascal_case.style = pascal_case + # Public, internal and protected readonly fields must be PascalCase dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.severity = warning dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.symbols = non_private_readonly_fields dotnet_naming_rule.non_private_readonly_fields_must_be_pascal_case.style = pascal_case + # Static readonly fields must be PascalCase dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.severity = warning dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.symbols = static_readonly_fields dotnet_naming_rule.static_readonly_fields_must_be_pascal_case.style = pascal_case -# Private readonly fields must be camelCase + +# Private readonly fields must be _camelCase dotnet_naming_rule.private_readonly_fields_must_be_camel_case.severity = warning dotnet_naming_rule.private_readonly_fields_must_be_camel_case.symbols = private_readonly_fields -dotnet_naming_rule.private_readonly_fields_must_be_camel_case.style = camel_case +dotnet_naming_rule.private_readonly_fields_must_be_camel_case.style = underscore_camel_case + # Public and internal fields must be PascalCase dotnet_naming_rule.public_internal_fields_must_be_pascal_case.severity = warning dotnet_naming_rule.public_internal_fields_must_be_pascal_case.symbols = public_internal_fields dotnet_naming_rule.public_internal_fields_must_be_pascal_case.style = pascal_case -# Private and protected fields must be camelCase + +# Private and protected fields must be _camelCase dotnet_naming_rule.private_protected_fields_must_be_camel_case.severity = warning dotnet_naming_rule.private_protected_fields_must_be_camel_case.symbols = private_protected_fields -dotnet_naming_rule.private_protected_fields_must_be_camel_case.style = camel_case +dotnet_naming_rule.private_protected_fields_must_be_camel_case.style = underscore_camel_case + +# Private fields must be _camelCase +dotnet_naming_rule.private_protected_must_be_camel_case.severity = warning +dotnet_naming_rule.private_protected_must_be_camel_case.symbols = private_fields +dotnet_naming_rule.private_protected_must_be_camel_case.style = underscore_camel_case + # Public members must be capitalized dotnet_naming_rule.public_members_must_be_capitalized.severity = warning dotnet_naming_rule.public_members_must_be_capitalized.symbols = public_symbols dotnet_naming_rule.public_members_must_be_capitalized.style = first_upper + # Parameters must be camelCase dotnet_naming_rule.parameters_must_be_camel_case.severity = warning dotnet_naming_rule.parameters_must_be_camel_case.symbols = parameters dotnet_naming_rule.parameters_must_be_camel_case.style = camel_case + # Class, struct, enum and delegates must be PascalCase dotnet_naming_rule.non_interface_types_must_be_pascal_case.severity = warning dotnet_naming_rule.non_interface_types_must_be_pascal_case.symbols = non_interface_types dotnet_naming_rule.non_interface_types_must_be_pascal_case.style = pascal_case + # Interfaces must be PascalCase and start with an 'I' dotnet_naming_rule.interface_types_must_be_prefixed_with_i.severity = warning dotnet_naming_rule.interface_types_must_be_prefixed_with_i.symbols = interface_types -dotnet_naming_rule.interface_types_must_be_prefixed_with_i.style = prefix_interface_interface_with_i \ No newline at end of file +dotnet_naming_rule.interface_types_must_be_prefixed_with_i.style = prefix_interface_interface_with_i + + + +# internal and private fields should be _camelCase +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.style = underscore_camel_case + +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal + + diff --git a/build/settings.props b/build/settings.props index cfbc187..cd53d45 100644 --- a/build/settings.props +++ b/build/settings.props @@ -6,7 +6,7 @@ latest false true - $(NoWarn);CS1591;IDE1006 + $(NoWarn);CS1591 diff --git a/src/AuthApp/AuthApp.csproj b/src/AuthApp/AuthApp.csproj index 79e9285..58faef5 100644 --- a/src/AuthApp/AuthApp.csproj +++ b/src/AuthApp/AuthApp.csproj @@ -23,6 +23,7 @@ + diff --git a/src/AuthApp/Host/HttpServer.cs b/src/AuthApp/Host/HttpServer.cs index bb6322c..41a6747 100644 --- a/src/AuthApp/Host/HttpServer.cs +++ b/src/AuthApp/Host/HttpServer.cs @@ -6,8 +6,13 @@ using System.Text; using System.Threading; using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using NetCoreForce.Client; + +using TextCopy; + using Console = Colorful.Console; namespace AuthApp.Host @@ -21,7 +26,7 @@ internal class HttpServer : BackgroundService private readonly HostBuilderOptions _hostOptions; private readonly SfConfig _config; private readonly IApplicationLifetime _applicationLifetime; - private bool isCompleted = false; + private bool _isCompleted = false; public HttpServer( HostBuilderOptions hostOptions, @@ -53,11 +58,12 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } var process = ConsoleHandler.OpenBrowser(authUrl); + var context = await http.GetContextAsync(); while (!stoppingToken.IsCancellationRequested) { - if (isCompleted) + if (_isCompleted) { _applicationLifetime.StopApplication(); return; @@ -97,9 +103,14 @@ await auth.WebServerAsync( $"{_config.LoginUrl}{_config.OAuthUri}"); Console.WriteLineFormatted("Access_token = {0}",Color.Green, Color.Yellow, auth.AccessInfo.AccessToken); + Console.WriteLineFormatted("Refresh_token = {0}", Color.Green, Color.Yellow, auth.AccessInfo.RefreshToken); - isCompleted = true; + Clipboard.SetText(auth.AccessInfo.RefreshToken); + + Console.WriteLine($"Refresh_token copied to the Clipboard", color: Color.WhiteSmoke); + + _isCompleted = true; http.Stop(); if (_hostOptions.Verbose) diff --git a/src/AuthApp/Host/SfConfig.cs b/src/AuthApp/Host/SfConfig.cs index 9ab6876..9384a4d 100644 --- a/src/AuthApp/Host/SfConfig.cs +++ b/src/AuthApp/Host/SfConfig.cs @@ -28,6 +28,9 @@ public class SfConfig /// public string OAuthUri { get; set; } = "/services/oauth2/token"; + /// + /// Authorize Uri for Salesforce end point. + /// public string OAuthorizeUri { get; set; } = "/services/oauth2/authorize"; } } diff --git a/src/AuthApp/HostBuilderExtensions.cs b/src/AuthApp/HostBuilderExtensions.cs index 2137a5b..6f9bb49 100644 --- a/src/AuthApp/HostBuilderExtensions.cs +++ b/src/AuthApp/HostBuilderExtensions.cs @@ -1,12 +1,16 @@ -using Bet.AspNetCore.Options; +using System.Collections.Generic; +using System.Drawing; +using System.IO; + +using Bet.AspNetCore.Options; + using McMaster.Extensions.CommandLineUtils; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using System.Collections.Generic; -using System.Drawing; -using System.IO; + using Console = Colorful.Console; namespace AuthApp diff --git a/src/AuthApp/HostBuilderOptions.cs b/src/AuthApp/HostBuilderOptions.cs index bb36159..aa4ef1b 100644 --- a/src/AuthApp/HostBuilderOptions.cs +++ b/src/AuthApp/HostBuilderOptions.cs @@ -1,4 +1,5 @@ using AuthApp.Host; + using Microsoft.Extensions.Logging; namespace AuthApp diff --git a/src/AuthApp/Program.cs b/src/AuthApp/Program.cs index 59c6198..e00f2bd 100644 --- a/src/AuthApp/Program.cs +++ b/src/AuthApp/Program.cs @@ -1,8 +1,8 @@ -using System; -using System.Drawing; -using System.Reflection; +using System.Reflection; using System.Threading.Tasks; + using McMaster.Extensions.CommandLineUtils; + using Console = Colorful.Console; namespace AuthApp diff --git a/src/AuthApp/Properties/launchSettings.json b/src/AuthApp/Properties/launchSettings.json index c46414a..02e8c99 100644 --- a/src/AuthApp/Properties/launchSettings.json +++ b/src/AuthApp/Properties/launchSettings.json @@ -5,7 +5,10 @@ //"commandLineArgs": "get-tokens --azure https://{name}.vault.azure.net/" //"commandLineArgs": "get-tokens --verbose:information --section:sfconfig" - "commandLineArgs": "get-tokens --key: --secret: --verbose:information --section:Salesforce" + //"commandLineArgs": "get-tokens --key: --secret: --verbose:information --section:Salesforce" + + "commandLineArgs": "get-tokens --key:3MVG9oNqAtcJCF.HPb0R40_6MW8Q4zs13WF6N3vgzLKj.Ku7CiHgtzRfCOrrrukW7.zRl8IUdd0MKECzaRjE8 --secret:7919576542672552253 --verbose:information --section:Salesforce" + } } } diff --git a/src/AuthApp/TokenGeneratorCommand.cs b/src/AuthApp/TokenGeneratorCommand.cs index f9df544..cf546bc 100644 --- a/src/AuthApp/TokenGeneratorCommand.cs +++ b/src/AuthApp/TokenGeneratorCommand.cs @@ -1,11 +1,15 @@ -using AuthApp.Host; +using System; +using System.Drawing; +using System.Threading.Tasks; + +using AuthApp.Host; + using McMaster.Extensions.CommandLineUtils; + using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using System; -using System.Drawing; -using System.Threading.Tasks; + using Console = Colorful.Console; namespace AuthApp @@ -38,14 +42,14 @@ internal class TokenGeneratorCommand /// Property types of ValueTuple{bool,T} translate to CommandOptionType.SingleOrNoValue. /// Input | Value /// ------------------------|-------------------------------- - /// (none) | (false, default(TraceLevel)) + /// (none) | (false, default(LogLevel)) /// --verbose | (true, LogLevel.Information) /// --verbose:information | (true, LogLevel.Information) /// --verbose:debug | (true, LogLevel.Debug) /// --verbose:trace | (true, LogLevel.Trace) /// - [Option(Description = "Allows Verbose logging for the tool. Enable this to get tracing information. Default is false.")] - public (bool HasValue, LogLevel level) Verbose { get; } + [Option(Description = "Allows Verbose logging for the tool. Enable this to get tracing information. Default is false and LogLevel.None.")] + public (bool HasValue, LogLevel level) Verbose { get; } = (false, LogLevel.None); [Option("--usesecrets", Description = "Enable UserSecrets.")] public bool UserSecrets { get; set; } diff --git a/src/CometD.NetCore.Salesforce/ForceClient/AuthenticationClientProxy.cs b/src/CometD.NetCore.Salesforce/ForceClient/AuthenticationClientProxy.cs index 43cd2ab..d7e8cfd 100644 --- a/src/CometD.NetCore.Salesforce/ForceClient/AuthenticationClientProxy.cs +++ b/src/CometD.NetCore.Salesforce/ForceClient/AuthenticationClientProxy.cs @@ -1,6 +1,8 @@ using System; using System.Threading.Tasks; + using NetCoreForce.Client; + using Polly; namespace CometD.NetCore.Salesforce.ForceClient diff --git a/src/CometD.NetCore.Salesforce/ForceClient/ForceClientProxy.cs b/src/CometD.NetCore.Salesforce/ForceClient/ForceClientProxy.cs index e6a4575..9b1f698 100644 --- a/src/CometD.NetCore.Salesforce/ForceClient/ForceClientProxy.cs +++ b/src/CometD.NetCore.Salesforce/ForceClient/ForceClientProxy.cs @@ -2,9 +2,12 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; + using Microsoft.Extensions.Logging; + using NetCoreForce.Client; using NetCoreForce.Client.Models; + using Polly; namespace CometD.NetCore.Salesforce.ForceClient diff --git a/src/CometD.NetCore.Salesforce/ForceClient/IAuthenticationClientProxy.cs b/src/CometD.NetCore.Salesforce/ForceClient/IAuthenticationClientProxy.cs index 1993df2..ca650f2 100644 --- a/src/CometD.NetCore.Salesforce/ForceClient/IAuthenticationClientProxy.cs +++ b/src/CometD.NetCore.Salesforce/ForceClient/IAuthenticationClientProxy.cs @@ -1,6 +1,8 @@ using System; using System.Threading.Tasks; + using CometD.NetCore.Salesforce.Resilience; + using NetCoreForce.Client; namespace CometD.NetCore.Salesforce.ForceClient diff --git a/src/CometD.NetCore.Salesforce/ForceClient/IForceClientProxy.cs b/src/CometD.NetCore.Salesforce/ForceClient/IForceClientProxy.cs index f29d95d..1bd529c 100644 --- a/src/CometD.NetCore.Salesforce/ForceClient/IForceClientProxy.cs +++ b/src/CometD.NetCore.Salesforce/ForceClient/IForceClientProxy.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; + using CometD.NetCore.Salesforce.Resilience; + using NetCoreForce.Client.Models; namespace CometD.NetCore.Salesforce.ForceClient diff --git a/src/CometD.NetCore.Salesforce/IStreamingClient.cs b/src/CometD.NetCore.Salesforce/IStreamingClient.cs index 5da683f..e529587 100644 --- a/src/CometD.NetCore.Salesforce/IStreamingClient.cs +++ b/src/CometD.NetCore.Salesforce/IStreamingClient.cs @@ -1,4 +1,5 @@ using System; + using CometD.NetCore.Bayeux.Client; namespace CometD.NetCore.Salesforce diff --git a/src/CometD.NetCore.Salesforce/Resilience/IResilientForceClient.cs b/src/CometD.NetCore.Salesforce/Resilience/IResilientForceClient.cs index ae5d2bb..529830b 100644 --- a/src/CometD.NetCore.Salesforce/Resilience/IResilientForceClient.cs +++ b/src/CometD.NetCore.Salesforce/Resilience/IResilientForceClient.cs @@ -1,8 +1,9 @@ -using NetCoreForce.Client.Models; -using System.Collections.Generic; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using NetCoreForce.Client.Models; + namespace CometD.NetCore.Salesforce.Resilience { /// diff --git a/src/CometD.NetCore.Salesforce/Resilience/ResilientForceClient.cs b/src/CometD.NetCore.Salesforce/Resilience/ResilientForceClient.cs index 38d2e84..6d58e72 100644 --- a/src/CometD.NetCore.Salesforce/Resilience/ResilientForceClient.cs +++ b/src/CometD.NetCore.Salesforce/Resilience/ResilientForceClient.cs @@ -2,10 +2,13 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; + using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; + using NetCoreForce.Client; using NetCoreForce.Client.Models; + using Polly; using Polly.Wrap; diff --git a/src/CometD.NetCore.Salesforce/ResilientStreamingClient.cs b/src/CometD.NetCore.Salesforce/ResilientStreamingClient.cs index 377c644..0f958a8 100644 --- a/src/CometD.NetCore.Salesforce/ResilientStreamingClient.cs +++ b/src/CometD.NetCore.Salesforce/ResilientStreamingClient.cs @@ -3,12 +3,15 @@ using System.Collections.Specialized; using System.Net; using System.Threading; + using CometD.NetCore.Bayeux.Client; using CometD.NetCore.Client; using CometD.NetCore.Client.Extension; using CometD.NetCore.Client.Transport; + using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; + using NetCoreForce.Client.Models; namespace CometD.NetCore.Salesforce @@ -182,7 +185,7 @@ private void CreateBayeuxClient() var serverUri = new Uri(accessToken.InstanceUrl); var endpoint = $"{serverUri.Scheme}://{serverUri.Host}{_options.CometDUri}"; - var headers = new NameValueCollection { { HttpRequestHeader.Authorization.ToString(), $"OAuth {accessToken.AccessToken}" } }; + var headers = new NameValueCollection { { nameof(HttpRequestHeader.Authorization), $"OAuth {accessToken.AccessToken}" } }; // Salesforce socket timeout during connection(CometD session) = 110 seconds var options = new Dictionary(StringComparer.OrdinalIgnoreCase) diff --git a/src/CometD.NetCore.Salesforce/StreamingClient.cs b/src/CometD.NetCore.Salesforce/StreamingClient.cs index 36c0aae..6a75f44 100644 --- a/src/CometD.NetCore.Salesforce/StreamingClient.cs +++ b/src/CometD.NetCore.Salesforce/StreamingClient.cs @@ -2,12 +2,15 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Net; + using CometD.NetCore.Bayeux.Client; using CometD.NetCore.Client; using CometD.NetCore.Client.Extension; using CometD.NetCore.Client.Transport; using CometD.NetCore.Salesforce.ForceClient; + using Microsoft.Extensions.Logging; + using NetCoreForce.Client.Models; namespace CometD.NetCore.Salesforce @@ -180,7 +183,7 @@ private void InitBayeuxClient() {ClientTransport.MAX_NETWORK_DELAY_OPTION, _options.ReadTimeOut ?? ReadTimeOut } }; - var headers = new NameValueCollection { { HttpRequestHeader.Authorization.ToString(), $"OAuth {_tokenInfo.AccessToken}" } }; + var headers = new NameValueCollection { { nameof(HttpRequestHeader.Authorization), $"OAuth {_tokenInfo.AccessToken}" } }; _clientTransport = new LongPollingTransport(options, headers); diff --git a/src/CometD.NetCore.Salesforce/StreamingClientExtensions.cs b/src/CometD.NetCore.Salesforce/StreamingClientExtensions.cs index 2ff3dc4..f0dbdbf 100644 --- a/src/CometD.NetCore.Salesforce/StreamingClientExtensions.cs +++ b/src/CometD.NetCore.Salesforce/StreamingClientExtensions.cs @@ -1,14 +1,18 @@ -using CometD.NetCore.Salesforce; +using System; +using System.Threading; + +using CometD.NetCore.Salesforce; using CometD.NetCore.Salesforce.ForceClient; using CometD.NetCore.Salesforce.Resilience; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; + using NetCoreForce.Client; using NetCoreForce.Client.Models; + using Polly; -using System; -using System.Threading; namespace Microsoft.Extensions.DependencyInjection { @@ -17,6 +21,15 @@ namespace Microsoft.Extensions.DependencyInjection /// public static class StreamingClientExtensions { + /// + /// Adds ForecClient Resilient version of it with Refresh Token Authentication. + /// https://help.salesforce.com/articleView?id=remoteaccess_oauth_refresh_token_flow.htm&type=5 + /// Can be used in the code with . + /// + /// + /// + /// + /// public static IServiceCollection AddResilientStreamingClient( this IServiceCollection services, IConfiguration configuration, @@ -30,9 +43,9 @@ public static IServiceCollection AddResilientStreamingClient( { var options = sp.GetRequiredService>().Value; - if (!TimeSpan.TryParse(options.TokenExpiration, out var tokenExperation)) + if (!TimeSpan.TryParse(options.TokenExpiration, out var tokenExpiration)) { - tokenExperation = TimeSpan.FromHours(1); + tokenExpiration = TimeSpan.FromHours(1); } return new AsyncExpiringLazy(async data => @@ -50,7 +63,11 @@ public static IServiceCollection AddResilientStreamingClient( { var auth = new AuthenticationClient(); - await auth.TokenRefreshAsync(options.RefreshToken, options.ClientId); + await auth.TokenRefreshAsync( + options.RefreshToken, + options.ClientId, + options.ClientSecret, + $"{options.LoginUrl}{options.OAuthUri}"); return auth; }); @@ -58,7 +75,7 @@ public static IServiceCollection AddResilientStreamingClient( return new AsyncExpirationValue { Result = authClient.AccessInfo, - ValidUntil = DateTimeOffset.UtcNow.Add(tokenExperation) + ValidUntil = DateTimeOffset.UtcNow.Add(tokenExpiration) }; } @@ -70,9 +87,9 @@ public static IServiceCollection AddResilientStreamingClient( { var options = sp.GetRequiredService>().Value; - if (!TimeSpan.TryParse(options.TokenExpiration, out var tokenExperation)) + if (!TimeSpan.TryParse(options.TokenExpiration, out var tokenExpiration)) { - tokenExperation = TimeSpan.FromHours(1); + tokenExpiration = TimeSpan.FromHours(1); } return new AsyncExpiringLazy(async data => @@ -90,7 +107,11 @@ public static IServiceCollection AddResilientStreamingClient( { var authClient = new AuthenticationClient(); - await authClient.TokenRefreshAsync(options.RefreshToken, options.ClientId); + await authClient.TokenRefreshAsync( + options.RefreshToken, + options.ClientId, + options.ClientSecret, + $"{options.LoginUrl}{options.OAuthUri}"); return new ForceClient( authClient.AccessInfo.InstanceUrl, @@ -101,7 +122,7 @@ public static IServiceCollection AddResilientStreamingClient( return new AsyncExpirationValue { Result = client, - ValidUntil = DateTimeOffset.UtcNow.Add(tokenExperation) + ValidUntil = DateTimeOffset.UtcNow.Add(tokenExpiration) }; } diff --git a/src/TestApp/Program.cs b/src/TestApp/Program.cs index c45e0b5..6b7505c 100644 --- a/src/TestApp/Program.cs +++ b/src/TestApp/Program.cs @@ -1,7 +1,9 @@ using System; using System.IO; using System.Threading.Tasks; + using CometD.NetCore.Salesforce.Resilience; + using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; diff --git a/test/CometD.UnitTest/UnitTest1.cs b/test/CometD.UnitTest/UnitTest1.cs index 16653ef..4b6213b 100644 --- a/test/CometD.UnitTest/UnitTest1.cs +++ b/test/CometD.UnitTest/UnitTest1.cs @@ -1,8 +1,8 @@ -using Microsoft.AspNetCore; +using System.Collections.Generic; + using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; -using System; -using System.Collections.Generic; + using Xunit; namespace CometD.UnitTest From 021ef5439f723c9b6a45a65085ce83db97aeb064 Mon Sep 17 00:00:00 2001 From: kdcllc Date: Fri, 12 Apr 2019 15:55:30 -0400 Subject: [PATCH 4/9] update configurations --- .editorconfig | 12 ------------ src/AuthApp/Properties/launchSettings.json | 2 +- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/.editorconfig b/.editorconfig index c7cab7f..a2b7262 100644 --- a/.editorconfig +++ b/.editorconfig @@ -302,15 +302,3 @@ dotnet_naming_rule.non_interface_types_must_be_pascal_case.style = pascal_case dotnet_naming_rule.interface_types_must_be_prefixed_with_i.severity = warning dotnet_naming_rule.interface_types_must_be_prefixed_with_i.symbols = interface_types dotnet_naming_rule.interface_types_must_be_prefixed_with_i.style = prefix_interface_interface_with_i - - - -# internal and private fields should be _camelCase -dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion -dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields -dotnet_naming_rule.camel_case_for_private_internal_fields.style = underscore_camel_case - -dotnet_naming_symbols.private_internal_fields.applicable_kinds = field -dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal - - diff --git a/src/AuthApp/Properties/launchSettings.json b/src/AuthApp/Properties/launchSettings.json index 02e8c99..125e087 100644 --- a/src/AuthApp/Properties/launchSettings.json +++ b/src/AuthApp/Properties/launchSettings.json @@ -7,7 +7,7 @@ //"commandLineArgs": "get-tokens --key: --secret: --verbose:information --section:Salesforce" - "commandLineArgs": "get-tokens --key:3MVG9oNqAtcJCF.HPb0R40_6MW8Q4zs13WF6N3vgzLKj.Ku7CiHgtzRfCOrrrukW7.zRl8IUdd0MKECzaRjE8 --secret:7919576542672552253 --verbose:information --section:Salesforce" + "commandLineArgs": "get-tokens --key: --secret: --verbose:information --section:Salesforce" } } From a8ba566da0d5e3b00b23ea8af109991227bc05e2 Mon Sep 17 00:00:00 2001 From: kdcllc Date: Fri, 12 Apr 2019 15:56:08 -0400 Subject: [PATCH 5/9] rename --- .../Properties/{launchSettings.json => _launchSettings.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/AuthApp/Properties/{launchSettings.json => _launchSettings.json} (100%) diff --git a/src/AuthApp/Properties/launchSettings.json b/src/AuthApp/Properties/_launchSettings.json similarity index 100% rename from src/AuthApp/Properties/launchSettings.json rename to src/AuthApp/Properties/_launchSettings.json From 149af4ad72cfb1f04754fbfcd6e61e20a5b0e8da Mon Sep 17 00:00:00 2001 From: kdcllc Date: Fri, 12 Apr 2019 15:56:23 -0400 Subject: [PATCH 6/9] rename back --- .../Properties/{_launchSettings.json => launchSettings.json} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/AuthApp/Properties/{_launchSettings.json => launchSettings.json} (100%) diff --git a/src/AuthApp/Properties/_launchSettings.json b/src/AuthApp/Properties/launchSettings.json similarity index 100% rename from src/AuthApp/Properties/_launchSettings.json rename to src/AuthApp/Properties/launchSettings.json From 40279631d871b9fcaa23cc1c3414830e10486137 Mon Sep 17 00:00:00 2001 From: KDCLLC Date: Thu, 14 Nov 2019 06:53:53 -0500 Subject: [PATCH 7/9] updating dotnetcore and also solving issue #10 --- Directory.Build.props | 6 ------ README.md | 1 + appveyor.yml | 16 +++++++------- build/dependecies.props | 21 ++++++++++++------- build/settings.props | 9 +++++++- src/AuthApp/AuthApp.csproj | 2 +- src/AuthApp/Host/HttpServer.cs | 2 +- src/AuthApp/HostBuilderExtensions.cs | 2 +- src/AuthApp/TokenGeneratorCommand.cs | 7 +++++-- .../CometD.NetCore.Salesforce.csproj | 2 +- src/TestApp/TestApp.csproj | 18 ++++++++-------- test/CometD.UnitTest/CometD.UnitTest.csproj | 14 +++++++------ 12 files changed, 57 insertions(+), 43 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 5eee227..a270ee9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,10 +3,4 @@ - - King David Consulting LLC - kdcllc - King David Consulting LLC - - diff --git a/README.md b/README.md index f4c854b..0c40f5c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # CometD .NET Core implementation of Salesforce Platform events + [![Build status](https://ci.appveyor.com/api/projects/status/baalfhs6vvc38icc?svg=true)](https://ci.appveyor.com/project/kdcllc/cometd-netcore-salesforce) This repo contains the CometD .NET Core implementation for Salesforce Platform events. diff --git a/appveyor.yml b/appveyor.yml index 23edca7..7253df0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,13 +1,13 @@ -version: 2.1.{build} +version: 2.2.{build} branches: only: - master pull_requests: do_not_increment_build_number: true -image: Visual Studio 2017 -## temporary until 3.0.100-preview-010184 sdk is installed +image: Visual Studio 2019 +## temporary until 3.0.100 sdk is installed install: - - ps: $urlCurrent = "https://dotnetcli.blob.core.windows.net/dotnet/Sdk/3.0.100-preview-010184/dotnet-sdk-3.0.100-preview-010184-win-x64.zip" + - ps: $urlCurrent = "https://dotnetcli.blob.core.windows.net/dotnet/Sdk/3.0.100/dotnet-sdk-3.0.100-win-x64.zip" - ps: $env:DOTNET_INSTALL_DIR = "$pwd\.dotnetsdk" - ps: mkdir $env:DOTNET_INSTALL_DIR -Force | Out-Null - ps: $tempFileCurrent = [System.IO.Path]::GetTempFileName() @@ -21,9 +21,9 @@ build_script: - ps: dotnet restore CometD.NetCore.Salesforce.sln -v quiet - ps: dotnet build CometD.NetCore.Salesforce.sln /p:configuration=Release /p:Version=$($env:appveyor_build_version) -test: off -# test_script: -# - dotnet test tests/XUnitTests.csproj +#test: off +test_script: + - dotnet test test/CometD.UnitTest/CometD.UnitTest.csproj artifacts: - path: .\src\**\*.nupkg @@ -33,6 +33,6 @@ deploy: - provider: NuGet artifact: /NuGet/ api_key: - secure: btoBG0IlGqSMBtVLUjeXmVtFT7B9Lw6CNf85ryr8Urf3982Vlz1LDkmIewZ/1tF7 + secure: jrexooMHJwdbj5MGacSL9fCj3g2haDXMarPEkiLpdIZmasb/zeMYb0NVofvi5HvJ on: branch: master diff --git a/build/dependecies.props b/build/dependecies.props index 2302e2e..3b9447e 100644 --- a/build/dependecies.props +++ b/build/dependecies.props @@ -2,7 +2,7 @@ 2.2.* - 1.1.19 + 2.0 @@ -13,8 +13,8 @@ 2.2.0 - - 3.0.0-preview* + + 3.0.0 @@ -37,6 +37,7 @@ + @@ -44,13 +45,19 @@ - - + - + - + + + + + + + + diff --git a/build/settings.props b/build/settings.props index cd53d45..c0d210f 100644 --- a/build/settings.props +++ b/build/settings.props @@ -2,13 +2,20 @@ true - 2.2.0-pre + 2.2.0-preview latest false true $(NoWarn);CS1591 + + + King David Consulting LLC + kdcllc + King David Consulting LLC + + Event Bus for Salesforce Platform events. https://github.com/kdcllc/CometD.NetCore.Salesforce diff --git a/src/AuthApp/AuthApp.csproj b/src/AuthApp/AuthApp.csproj index 58faef5..a4a79cc 100644 --- a/src/AuthApp/AuthApp.csproj +++ b/src/AuthApp/AuthApp.csproj @@ -23,7 +23,7 @@ - + diff --git a/src/AuthApp/Host/HttpServer.cs b/src/AuthApp/Host/HttpServer.cs index 41a6747..b4083d5 100644 --- a/src/AuthApp/Host/HttpServer.cs +++ b/src/AuthApp/Host/HttpServer.cs @@ -108,7 +108,7 @@ await auth.WebServerAsync( Clipboard.SetText(auth.AccessInfo.RefreshToken); - Console.WriteLine($"Refresh_token copied to the Clipboard", color: Color.WhiteSmoke); + Console.WriteLine($"Refresh_token copied to the Clipboard", color: Color.Yellow); _isCompleted = true; diff --git a/src/AuthApp/HostBuilderExtensions.cs b/src/AuthApp/HostBuilderExtensions.cs index 6f9bb49..d09a72b 100644 --- a/src/AuthApp/HostBuilderExtensions.cs +++ b/src/AuthApp/HostBuilderExtensions.cs @@ -39,7 +39,7 @@ internal static IHostBuilder CreateDefaultBuilder(HostBuilderOptions options) } builder - .UseStartupFilter() + .UseStartupFilters() .ConfigureAppConfiguration((context, config) => { // appsettings file or others diff --git a/src/AuthApp/TokenGeneratorCommand.cs b/src/AuthApp/TokenGeneratorCommand.cs index cf546bc..267634b 100644 --- a/src/AuthApp/TokenGeneratorCommand.cs +++ b/src/AuthApp/TokenGeneratorCommand.cs @@ -17,6 +17,7 @@ namespace AuthApp [Command("get-tokens", Description = "Generates Salesforce Access and Refresh Tokens", ThrowOnUnexpectedArgument = false)] + [HelpOption("-?")] internal class TokenGeneratorCommand { [Option("--key", Description = "The Salesforce Consumer Key.")] @@ -60,7 +61,7 @@ internal class TokenGeneratorCommand [Option("--section", Description ="Configuration Section Name to retrieve the options. The Default value is Salesforce.")] public string SectionName { get; set; } - private async Task OnExecuteAsync() + private async Task OnExecuteAsync(CommandLineApplication app) { var builderConfig = new HostBuilderOptions { @@ -95,7 +96,9 @@ private async Task OnExecuteAsync() } catch(Microsoft.Extensions.Options.OptionsValidationException exv) { - Console.WriteLine("Not all of the required configurations has been provided.", Color.Red); + Console.WriteLine($"Not all of the required configurations has been provided. {exv.Message}", Color.Red); + + app.ShowHelp(); } catch (Exception ex) { diff --git a/src/CometD.NetCore.Salesforce/CometD.NetCore.Salesforce.csproj b/src/CometD.NetCore.Salesforce/CometD.NetCore.Salesforce.csproj index d88e4f0..55a6270 100644 --- a/src/CometD.NetCore.Salesforce/CometD.NetCore.Salesforce.csproj +++ b/src/CometD.NetCore.Salesforce/CometD.NetCore.Salesforce.csproj @@ -1,7 +1,7 @@  - netstandard2.0;netcoreapp2.2;netcoreapp3.0 + netstandard2.0;netstandard2.1; CometD.NetCore2.Salesforce diff --git a/src/TestApp/TestApp.csproj b/src/TestApp/TestApp.csproj index b1d1939..bca01b9 100644 --- a/src/TestApp/TestApp.csproj +++ b/src/TestApp/TestApp.csproj @@ -7,15 +7,15 @@ - - - - - - - - - + + + + + + + + + diff --git a/test/CometD.UnitTest/CometD.UnitTest.csproj b/test/CometD.UnitTest/CometD.UnitTest.csproj index 541a432..d94c0ca 100644 --- a/test/CometD.UnitTest/CometD.UnitTest.csproj +++ b/test/CometD.UnitTest/CometD.UnitTest.csproj @@ -1,16 +1,18 @@  - netcoreapp3.0 + netcoreapp2.2;netcoreapp3.0 false - - - - - + + + + + + + From 5f2491bfe86920ca2849d18830056454f80e80a5 Mon Sep 17 00:00:00 2001 From: KDCLLC Date: Thu, 14 Nov 2019 08:11:58 -0500 Subject: [PATCH 8/9] update style cop and enable nullable issue #10 --- README.md | 2 +- build/dependecies.props | 4 + build/settings.props | 1 + src/AuthApp/AuthApp.csproj | 4 +- src/AuthApp/Host/HttpServer.cs | 36 ++--- src/AuthApp/Host/SfConfig.cs | 6 +- src/AuthApp/HostBuilderExtensions.cs | 28 ++-- src/AuthApp/HostBuilderOptions.cs | 4 +- src/AuthApp/Program.cs | 12 +- src/AuthApp/Properties/launchSettings.json | 2 +- src/AuthApp/TokenGeneratorCommand.cs | 15 +- .../CometD.NetCore.Salesforce.csproj | 3 +- .../ForceClient/AuthenticationClientProxy.cs | 54 +++++-- .../ForceClient/ForceClientProxy.cs | 119 +++++++-------- .../ForceClient/IAuthenticationClientProxy.cs | 4 +- .../ForceClient/IForceClientProxy.cs | 6 +- .../IStreamingClient.cs | 4 +- .../Messaging/MessageData.cs | 6 +- .../Messaging/MessageEnvelope.cs | 6 +- .../Messaging/MessagePayload.cs | 2 +- .../Resilience/IResilientForceClient.cs | 82 +++++----- .../Resilience/ResilientForceClient.cs | 87 ++++++----- .../ResilientStreamingClient.cs | 95 +++++++----- .../SalesforceConfiguration.cs | 40 ++--- .../StreamingClient.cs | 140 ++++++++++-------- .../StreamingClientExtensions.cs | 72 ++++----- src/TestApp/Program.cs | 6 +- test/CometD.UnitTest/UnitTest1.cs | 10 +- 28 files changed, 464 insertions(+), 386 deletions(-) diff --git a/README.md b/README.md index 0c40f5c..5630963 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The solution contains the following: Or ```cmd - dotnet add package CometD.NetCore.Salesforce + dotnet add package CometD.NetCore.Salesforce ``` - To Install Salesforce Cli tool globally run the following command: diff --git a/build/dependecies.props b/build/dependecies.props index 3b9447e..cd23326 100644 --- a/build/dependecies.props +++ b/build/dependecies.props @@ -60,6 +60,10 @@ + + + + diff --git a/build/settings.props b/build/settings.props index c0d210f..b7b7d20 100644 --- a/build/settings.props +++ b/build/settings.props @@ -4,6 +4,7 @@ true 2.2.0-preview latest + disable false true $(NoWarn);CS1591 diff --git a/src/AuthApp/AuthApp.csproj b/src/AuthApp/AuthApp.csproj index a4a79cc..e460356 100644 --- a/src/AuthApp/AuthApp.csproj +++ b/src/AuthApp/AuthApp.csproj @@ -16,14 +16,12 @@ - - - + diff --git a/src/AuthApp/Host/HttpServer.cs b/src/AuthApp/Host/HttpServer.cs index b4083d5..ce366df 100644 --- a/src/AuthApp/Host/HttpServer.cs +++ b/src/AuthApp/Host/HttpServer.cs @@ -19,7 +19,7 @@ namespace AuthApp.Host { /// /// Web Server OAuth Authentication Flow - /// https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_understanding_web_server_oauth_flow.htm + /// https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_understanding_web_server_oauth_flow.htm. /// internal class HttpServer : BackgroundService { @@ -84,6 +84,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { Console.WriteLine($"OAuth authorization error: {context.Request.QueryString.Get("error")}.", Color.Red); } + if (context.Request.QueryString.Get("code") == null) { Console.WriteLine($"Malformed authorization response {context.Request.QueryString}", Color.Red); @@ -102,7 +103,7 @@ await auth.WebServerAsync( code, $"{_config.LoginUrl}{_config.OAuthUri}"); - Console.WriteLineFormatted("Access_token = {0}",Color.Green, Color.Yellow, auth.AccessInfo.AccessToken); + Console.WriteLineFormatted("Access_token = {0}", Color.Green, Color.Yellow, auth.AccessInfo.AccessToken); Console.WriteLineFormatted("Refresh_token = {0}", Color.Green, Color.Yellow, auth.AccessInfo.RefreshToken); @@ -118,10 +119,26 @@ await auth.WebServerAsync( Console.WriteLine($"{nameof(HttpServer)} is stopping."); } } + await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); } } + private static async Task ShowBrowserMessage(HttpListenerContext context) + { + var response = context.Response; + var responseString = string.Format(@" + + Please return to the console to retrieve access and refresh tokens. + "); + + var buffer = Encoding.UTF8.GetBytes(responseString); + response.ContentLength64 = buffer.Length; + var responseOutput = response.OutputStream; + await responseOutput.WriteAsync(buffer, 0, buffer.Length); + return responseOutput; + } + private int GetRandomUnusedPort() { var listener = new TcpListener(IPAddress.Loopback, 5050); @@ -137,20 +154,5 @@ private string GetAuthorizationUrl(string redirectURI) var url = $"{authEndpoint}?response_type=code&access_type=offline&scope=openid%20profile%20api%20refresh_token%20offline_access&redirect_uri={Uri.EscapeDataString(redirectURI)}&client_id={_config.ClientId}"; return url; } - - private static async Task ShowBrowserMessage(HttpListenerContext context) - { - var response = context.Response; - var responseString = string.Format(@" - - Please return to the console to retrieve access and refresh tokens. - "); - - var buffer = Encoding.UTF8.GetBytes(responseString); - response.ContentLength64 = buffer.Length; - var responseOutput = response.OutputStream; - await responseOutput.WriteAsync(buffer, 0, buffer.Length); - return responseOutput; - } } } diff --git a/src/AuthApp/Host/SfConfig.cs b/src/AuthApp/Host/SfConfig.cs index 9384a4d..5930d70 100644 --- a/src/AuthApp/Host/SfConfig.cs +++ b/src/AuthApp/Host/SfConfig.cs @@ -5,19 +5,19 @@ namespace AuthApp.Host public class SfConfig { /// - /// Salesforce Client Id + /// Salesforce Client Id. /// [Required] public string ClientId { get; set; } /// - /// Salesforece Secret Id + /// Salesforece Secret Id. /// [Required] public string ClientSecret { get; set; } /// - /// i.e. https://login.salesforce.com + /// i.e. https://login.salesforce.com. /// [Required] [Url] diff --git a/src/AuthApp/HostBuilderExtensions.cs b/src/AuthApp/HostBuilderExtensions.cs index d09a72b..d4c3656 100644 --- a/src/AuthApp/HostBuilderExtensions.cs +++ b/src/AuthApp/HostBuilderExtensions.cs @@ -39,12 +39,12 @@ internal static IHostBuilder CreateDefaultBuilder(HostBuilderOptions options) } builder - .UseStartupFilters() + .UseOptionValidation() .ConfigureAppConfiguration((context, config) => { // appsettings file or others - config.AddJsonFile(Path.Combine(fullPath, $"{(defaultConfigName).Split(".")[0]}.json"), optional: true) - .AddJsonFile(Path.Combine(fullPath, $"{(defaultConfigName).Split(".")[0]}.{options.HostingEnviroment}.json"), optional: true); + config.AddJsonFile(Path.Combine(fullPath, $"{defaultConfigName.Split(".")[0]}.json"), optional: true) + .AddJsonFile(Path.Combine(fullPath, $"{defaultConfigName.Split(".")[0]}.{options.HostingEnviroment}.json"), optional: true); // add secrets if specified if (options.UserSecrets) @@ -53,14 +53,14 @@ internal static IHostBuilder CreateDefaultBuilder(HostBuilderOptions options) } // configure Azure Vault from the other settings. - var appAzureVaultUrl = config.Build().Bind("AzureVault",enableValidation: false); + var appAzureVaultUrl = config.Build().Bind("AzureVault", enableValidation: false); // build azure key vault from passed in parameter if (!string.IsNullOrWhiteSpace(options.AzureVault)) { var dic = new Dictionary { - {"AzureVault:BaseUrl", options.AzureVault } + { "AzureVault:BaseUrl", options.AzureVault } }; config.AddInMemoryCollection(dic); @@ -70,26 +70,26 @@ internal static IHostBuilder CreateDefaultBuilder(HostBuilderOptions options) if (!string.IsNullOrWhiteSpace(appAzureVaultUrl.BaseUrl) || !string.IsNullOrWhiteSpace(options.AzureVault)) { - config.AddAzureKeyVault(hostingEnviromentName:options.HostingEnviroment, options.UseAzureKeyPrefix); + config.AddAzureKeyVault(hostingEnviromentName: options.HostingEnviroment, options.UseAzureKeyPrefix); } - if(!string.IsNullOrWhiteSpace(options.Settings.ClientId) + if (!string.IsNullOrWhiteSpace(options.Settings.ClientId) && !string.IsNullOrWhiteSpace(options.Settings.ClientSecret)) { var inputValues = new Dictionary { - {$"{options.SectionName}:ClientId", options.Settings.ClientId }, - {$"{options.SectionName}:ClientSecret", options.Settings.ClientSecret }, - {$"{options.SectionName}:LoginUrl", options.Settings.LoginUrl }, - {$"{options.SectionName}:OAuthUri", options.Settings.OAuthUri }, - {$"{options.SectionName}:OAuthorizeUri", options.Settings.OAuthorizeUri }, + { $"{options.SectionName}:ClientId", options.Settings.ClientId }, + { $"{options.SectionName}:ClientSecret", options.Settings.ClientSecret }, + { $"{options.SectionName}:LoginUrl", options.Settings.LoginUrl }, + { $"{options.SectionName}:OAuthUri", options.Settings.OAuthUri }, + { $"{options.SectionName}:OAuthorizeUri", options.Settings.OAuthorizeUri }, }; config.AddInMemoryCollection(inputValues); } - if (options.Verbose && options.Level == LogLevel.Debug - || options.Level == LogLevel.Trace) + if ((options.Verbose && options.Level == LogLevel.Debug) + || options.Level == LogLevel.Trace) { config.Build().DebugConfigurations(); } diff --git a/src/AuthApp/HostBuilderOptions.cs b/src/AuthApp/HostBuilderOptions.cs index aa4ef1b..e87c0e0 100644 --- a/src/AuthApp/HostBuilderOptions.cs +++ b/src/AuthApp/HostBuilderOptions.cs @@ -19,7 +19,7 @@ internal class HostBuilderOptions /// /// TraceLevel if verbose is present. /// - public LogLevel Level { get; set; } + public LogLevel Level { get; set; } /// /// Ability to use Web project secrets. @@ -27,7 +27,7 @@ internal class HostBuilderOptions public bool UserSecrets { get; set; } /// - /// Url for the azure key vault i.e. https://{vaultname}.vault.azure.net/ + /// Url for the azure key vault i.e. https://{vaultname}.vault.azure.net/. /// public string AzureVault { get; set; } diff --git a/src/AuthApp/Program.cs b/src/AuthApp/Program.cs index e00f2bd..e79a2d0 100644 --- a/src/AuthApp/Program.cs +++ b/src/AuthApp/Program.cs @@ -15,7 +15,12 @@ public class Program { private static Task Main(string[] args) { - return CommandLineApplication.ExecuteAsync(args); + return CommandLineApplication.ExecuteAsync(args); + } + + private static string GetVersion() + { + return typeof(Program).Assembly.GetCustomAttribute().InformationalVersion; } private int OnExecute(CommandLineApplication app, IConsole console) @@ -26,10 +31,5 @@ private int OnExecute(CommandLineApplication app, IConsole console) app.ShowHelp(); return 1; } - - private static string GetVersion() - { - return typeof(Program).Assembly.GetCustomAttribute().InformationalVersion; - } } } diff --git a/src/AuthApp/Properties/launchSettings.json b/src/AuthApp/Properties/launchSettings.json index 125e087..52ce130 100644 --- a/src/AuthApp/Properties/launchSettings.json +++ b/src/AuthApp/Properties/launchSettings.json @@ -7,7 +7,7 @@ //"commandLineArgs": "get-tokens --key: --secret: --verbose:information --section:Salesforce" - "commandLineArgs": "get-tokens --key: --secret: --verbose:information --section:Salesforce" + "commandLineArgs": "get-tokens --key: --secret: --verbose:debug --section:Salesforce" } } diff --git a/src/AuthApp/TokenGeneratorCommand.cs b/src/AuthApp/TokenGeneratorCommand.cs index 267634b..3b4b937 100644 --- a/src/AuthApp/TokenGeneratorCommand.cs +++ b/src/AuthApp/TokenGeneratorCommand.cs @@ -9,12 +9,14 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Console = Colorful.Console; namespace AuthApp { - [Command("get-tokens", + [Command( + "get-tokens", Description = "Generates Salesforce Access and Refresh Tokens", ThrowOnUnexpectedArgument = false)] [HelpOption("-?")] @@ -29,7 +31,8 @@ internal class TokenGeneratorCommand [Option("--login", Description = "The Salesforce login url. The default value is https://login.salesforce.com.")] public string LoginUrl { get; set; } - [Option("--azure", + [Option( + "--azure", Description = "Allows to specify Azure Vault Url. It overrides url specified in the appsetting.json file or any other configuration provider.")] public string AzureVault { get; set; } @@ -47,7 +50,7 @@ internal class TokenGeneratorCommand /// --verbose | (true, LogLevel.Information) /// --verbose:information | (true, LogLevel.Information) /// --verbose:debug | (true, LogLevel.Debug) - /// --verbose:trace | (true, LogLevel.Trace) + /// --verbose:trace | (true, LogLevel.Trace). /// [Option(Description = "Allows Verbose logging for the tool. Enable this to get tracing information. Default is false and LogLevel.None.")] public (bool HasValue, LogLevel level) Verbose { get; } = (false, LogLevel.None); @@ -58,7 +61,7 @@ internal class TokenGeneratorCommand [Option("--environment", Description = "Specify Hosting Environment Name for the cli tool execution.")] public string HostingEnviroment { get; set; } - [Option("--section", Description ="Configuration Section Name to retrieve the options. The Default value is Salesforce.")] + [Option("--section", Description = "Configuration Section Name to retrieve the options. The Default value is Salesforce.")] public string SectionName { get; set; } private async Task OnExecuteAsync(CommandLineApplication app) @@ -76,7 +79,7 @@ private async Task OnExecuteAsync(CommandLineApplication app) { ClientId = ClientId, ClientSecret = ClientSecret, - LoginUrl = !string.IsNullOrWhiteSpace(LoginUrl) ? LoginUrl : "https://login.salesforce.com" + LoginUrl = !string.IsNullOrWhiteSpace(LoginUrl) ? LoginUrl : "https://login.salesforce.com" }, SectionName = string.IsNullOrWhiteSpace(SectionName) ? "Salesforce" : SectionName, }; @@ -94,7 +97,7 @@ private async Task OnExecuteAsync(CommandLineApplication app) return 0; } - catch(Microsoft.Extensions.Options.OptionsValidationException exv) + catch (OptionsValidationException exv) { Console.WriteLine($"Not all of the required configurations has been provided. {exv.Message}", Color.Red); diff --git a/src/CometD.NetCore.Salesforce/CometD.NetCore.Salesforce.csproj b/src/CometD.NetCore.Salesforce/CometD.NetCore.Salesforce.csproj index 55a6270..f666a0f 100644 --- a/src/CometD.NetCore.Salesforce/CometD.NetCore.Salesforce.csproj +++ b/src/CometD.NetCore.Salesforce/CometD.NetCore.Salesforce.csproj @@ -2,7 +2,8 @@ netstandard2.0;netstandard2.1; - CometD.NetCore2.Salesforce + enable + CometD.NetCore2.Salesforce diff --git a/src/CometD.NetCore.Salesforce/ForceClient/AuthenticationClientProxy.cs b/src/CometD.NetCore.Salesforce/ForceClient/AuthenticationClientProxy.cs index d7e8cfd..0c8f705 100644 --- a/src/CometD.NetCore.Salesforce/ForceClient/AuthenticationClientProxy.cs +++ b/src/CometD.NetCore.Salesforce/ForceClient/AuthenticationClientProxy.cs @@ -10,7 +10,7 @@ namespace CometD.NetCore.Salesforce.ForceClient /// /// The proxy /// the functionality of - /// library class + /// library class. /// [Obsolete("Use " + nameof(ResilientStreamingClient) + "class instead.")] @@ -21,17 +21,17 @@ public class AuthenticationClientProxy : IAuthenticationClientProxy private readonly AuthenticationClient _auth; /// - /// Constructor create instance of the class and authenticates the session. + /// Initializes a new instance of the class and authenticates the session. /// - /// + /// . public AuthenticationClientProxy(SalesforceConfiguration options) { - _options = options ?? throw new ArgumentNullException(nameof(options)); + _options = options ?? throw new ArgumentNullException(nameof(options)); _policy = Policy - .Handle() - .WaitAndRetryAsync(5, retryAttempt => - TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); + .Handle() + .WaitAndRetryAsync(5, retryAttempt => + TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))); _auth = new AuthenticationClient(); @@ -40,15 +40,24 @@ public AuthenticationClientProxy(SalesforceConfiguration options) } /// - /// Returns instance of + /// Finalizes an instance of the class. /// + ~AuthenticationClientProxy() + { + Dispose(false); + } + + /// + /// Returns instance of . + /// + /// If client is not authenticated. public AuthenticationClient AuthenticationClient { get { if (!IsAuthenticated) { - throw new ApplicationException($"{nameof(Authenticate)} must be called before fetching the {nameof(AuthenticationClient)}."); + throw new InvalidOperationException($"{nameof(Authenticate)} must be called before fetching the {nameof(AuthenticationClient)}."); } return _auth; @@ -67,11 +76,28 @@ public AuthenticationClient AuthenticationClient public Task Authenticate() { return _policy.ExecuteAsync(() => - _auth.TokenRefreshAsync(_options.RefreshToken, - _options.ClientId, - _options.ClientSecret, - $"{_options.LoginUrl}{_options.OAuthUri}") - ); + _auth.TokenRefreshAsync( + _options.RefreshToken, + _options.ClientId, + _options.ClientSecret, + $"{_options.LoginUrl}{_options.OAuthUri}")); + } + + /// + /// Disposing of the resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + _auth?.Dispose(); + } } } } diff --git a/src/CometD.NetCore.Salesforce/ForceClient/ForceClientProxy.cs b/src/CometD.NetCore.Salesforce/ForceClient/ForceClientProxy.cs index 9b1f698..dcddc26 100644 --- a/src/CometD.NetCore.Salesforce/ForceClient/ForceClientProxy.cs +++ b/src/CometD.NetCore.Salesforce/ForceClient/ForceClientProxy.cs @@ -14,23 +14,24 @@ namespace CometD.NetCore.Salesforce.ForceClient { /// /// The proxy functionality around . - /// + /// . /// public class ForceClientProxy : IForceClientProxy { private readonly SalesforceConfiguration _options; private readonly IAuthenticationClientProxy _authenticationClient; private readonly ILogger _logger; - private NetCoreForce.Client.ForceClient _forceClient; private readonly IAsyncPolicy _policy; + private NetCoreForce.Client.ForceClient _forceClient; + /// - /// Constructor for + /// Initializes a new instance of the class. /// that create an instance of . /// /// Instance of that creates instance of . /// Instance of the . - /// Options based on + /// Options based on . public ForceClientProxy( IAuthenticationClientProxy authenticationClient, ILogger logger, @@ -46,63 +47,29 @@ public ForceClientProxy( _forceClient = new NetCoreForce.Client.ForceClient( _authenticationClient.AuthenticationClient.AccessInfo.InstanceUrl, _authenticationClient.AuthenticationClient.ApiVersion, - _authenticationClient.AuthenticationClient.AccessInfo.AccessToken - ); + _authenticationClient.AuthenticationClient.AccessInfo.AccessToken); // create retry authentication policy - _policy = CreateAuthenticationWaitAndRetry(_options.Retry, - nameof(ForceClientProxy), OnWaitAndRetry); - } - - private async Task RefreshAuthorization() - { - await _authenticationClient.Authenticate(); - - _forceClient = new NetCoreForce.Client.ForceClient( - _authenticationClient.AuthenticationClient.AccessInfo.InstanceUrl, - _authenticationClient.AuthenticationClient.ApiVersion, - _authenticationClient.AuthenticationClient.AccessInfo.AccessToken - ); - - _logger.LogDebug($"Salesforce Authentication Successful!"); - } - - private async Task OnWaitAndRetry( - Exception ex, - int count, - Context context) - { - _logger.LogWarning($"Trying to {nameof(RefreshAuthorization)}"); - _logger.LogWarning($"Retry {context.Count}:{count} of {context.PolicyKey}, due to {ex.Message}."); - await RefreshAuthorization(); - } - - private IAsyncPolicy CreateAuthenticationWaitAndRetry( - int retryAuthorization, - string name, - Func retryHook) - { - return Policy - .Handle(x => x.Message.Contains("ErrorCode INVALID_SESSION_ID")) - .RetryAsync( - retryCount: retryAuthorization, - onRetryAsync: retryHook).WithPolicyKey($"{name}Retry"); + _policy = CreateAuthenticationWaitAndRetry( + _options.Retry, + nameof(ForceClientProxy), + OnWaitAndRetry); } /// - /// Retrieves a Salesforce object by its Id + /// Retrieves a Salesforce object by its Id. /// - /// Type of the object - /// Salesforce object type name - /// Id of the object to retrieve - /// Token to cancel operation + /// Type of the object. + /// Salesforce object type name. + /// Id of the object to retrieve. + /// Token to cancel operation. /// List of fields to return. /// The retrieved object from Salesforce. public async Task GetObjectById( string sObjectTypeName, string objectId, CancellationToken token, - List fields = null) + List? fields = null) { return await _policy.ExecuteAsync(ctx => _forceClient.GetObjectById(sObjectTypeName, objectId, fields), token); } @@ -110,35 +77,69 @@ public async Task GetObjectById( /// /// Creates a record in the Salesforce instance. /// - /// Type of the object - /// Salesforce object type name - /// Object to create + /// Type of the object. + /// Salesforce object type name. + /// Object to create. /// Optional request headers. /// A representing the operation result. public async Task CreateRecord( string sObjectTypeName, T instance, - Dictionary headers = null) + Dictionary? headers = null) { return await _policy.ExecuteAsync(() => _forceClient.CreateRecord(sObjectTypeName, instance, headers)); } /// - /// Creates a record in the Salesforce organization + /// Creates a record in the Salesforce organization. /// - /// Type of the object - /// Salesforce object type name - /// Object to create - /// Token to cancel operation + /// Type of the object. + /// Salesforce object type name. + /// Object to create. + /// Token to cancel operation. /// Optional request headers. /// A representing the operation result. public async Task CreateRecord( string sObjectTypeName, T instance, CancellationToken token, - Dictionary headers = null) + Dictionary? headers = null) { return await _policy.ExecuteAsync(ctx => _forceClient.CreateRecord(sObjectTypeName, instance, headers), token); } + + private async Task RefreshAuthorization() + { + await _authenticationClient.Authenticate(); + + _forceClient = new NetCoreForce.Client.ForceClient( + _authenticationClient.AuthenticationClient.AccessInfo.InstanceUrl, + _authenticationClient.AuthenticationClient.ApiVersion, + _authenticationClient.AuthenticationClient.AccessInfo.AccessToken); + + _logger.LogDebug($"Salesforce Authentication Successful!"); + } + + private async Task OnWaitAndRetry( + Exception ex, + int count, + Context context) + { + _logger.LogWarning($"Trying to {nameof(RefreshAuthorization)}"); + _logger.LogWarning($"Retry {context.Count}:{count} of {context.PolicyKey}, due to {ex.Message}."); + await RefreshAuthorization(); + } + + private IAsyncPolicy CreateAuthenticationWaitAndRetry( + int retryAuthorization, + string name, + Func retryHook) + { + return Policy + .Handle(x => x.Message.Contains("ErrorCode INVALID_SESSION_ID")) + .RetryAsync( + retryCount: retryAuthorization, + onRetryAsync: retryHook).WithPolicyKey($"{name}Retry"); + } } } diff --git a/src/CometD.NetCore.Salesforce/ForceClient/IAuthenticationClientProxy.cs b/src/CometD.NetCore.Salesforce/ForceClient/IAuthenticationClientProxy.cs index ca650f2..1af1449 100644 --- a/src/CometD.NetCore.Salesforce/ForceClient/IAuthenticationClientProxy.cs +++ b/src/CometD.NetCore.Salesforce/ForceClient/IAuthenticationClientProxy.cs @@ -8,10 +8,10 @@ namespace CometD.NetCore.Salesforce.ForceClient { /// - /// A wrapper interface around + /// A wrapper interface around . /// [Obsolete("Use " + nameof(IResilientForceClient) + "extension method instead.")] - public interface IAuthenticationClientProxy + public interface IAuthenticationClientProxy : IDisposable { /// /// Returns diff --git a/src/CometD.NetCore.Salesforce/ForceClient/IForceClientProxy.cs b/src/CometD.NetCore.Salesforce/ForceClient/IForceClientProxy.cs index 1bd529c..d0423a6 100644 --- a/src/CometD.NetCore.Salesforce/ForceClient/IForceClientProxy.cs +++ b/src/CometD.NetCore.Salesforce/ForceClient/IForceClientProxy.cs @@ -23,7 +23,7 @@ public interface IForceClientProxy /// /// /// - Task CreateRecord(string sObjectTypeName, T instance, Dictionary headers = null); + Task CreateRecord(string sObjectTypeName, T instance, Dictionary? headers = null); /// /// Return on record creation. @@ -34,7 +34,7 @@ public interface IForceClientProxy /// /// /// - Task CreateRecord(string sObjectTypeName, T instance, CancellationToken token, Dictionary headers = null); + Task CreateRecord(string sObjectTypeName, T instance, CancellationToken token, Dictionary? headers = null); /// /// Returns from the Salesforce call. @@ -45,6 +45,6 @@ public interface IForceClientProxy /// /// /// - Task GetObjectById(string sObjectTypeName, string objectId, CancellationToken token, List fields = null); + Task GetObjectById(string sObjectTypeName, string objectId, CancellationToken token, List? fields = null); } } diff --git a/src/CometD.NetCore.Salesforce/IStreamingClient.cs b/src/CometD.NetCore.Salesforce/IStreamingClient.cs index e529587..bfcb780 100644 --- a/src/CometD.NetCore.Salesforce/IStreamingClient.cs +++ b/src/CometD.NetCore.Salesforce/IStreamingClient.cs @@ -47,7 +47,7 @@ public interface IStreamingClient : IDisposable /// /// /// - void SubscribeTopic(string topicName, IMessageListener listener, long replayId=-1); + void SubscribeTopic(string topicName, IMessageListener listener, long replayId = -1); /// /// Unsubscribe from Salesforce Platform event. @@ -56,6 +56,6 @@ public interface IStreamingClient : IDisposable /// /// /// - bool UnsubscribeTopic(string topicName, IMessageListener listener = null, long replayId=-1); + bool UnsubscribeTopic(string topicName, IMessageListener? listener = null, long replayId = -1); } } diff --git a/src/CometD.NetCore.Salesforce/Messaging/MessageData.cs b/src/CometD.NetCore.Salesforce/Messaging/MessageData.cs index 003e03e..c51cce8 100644 --- a/src/CometD.NetCore.Salesforce/Messaging/MessageData.cs +++ b/src/CometD.NetCore.Salesforce/Messaging/MessageData.cs @@ -10,13 +10,13 @@ public class MessageData where TPayload : MessagePayload /// Unique id of the data. /// "schema": "1qUPELmVz7qUv3ntwyN1eA" /// - public string Schema { get; set; } + public string Schema { get; set; } = string.Empty; /// /// Generic type of the payload. /// "payload": {} /// - public TPayload Payload { get; set; } + public TPayload? Payload { get; set; } /// /// Contains Message event. @@ -26,6 +26,6 @@ public class MessageData where TPayload : MessagePayload /// } /// /// - public MessageEvent Event { get; set; } + public MessageEvent? Event { get; set; } } } diff --git a/src/CometD.NetCore.Salesforce/Messaging/MessageEnvelope.cs b/src/CometD.NetCore.Salesforce/Messaging/MessageEnvelope.cs index 6209418..66c17ef 100644 --- a/src/CometD.NetCore.Salesforce/Messaging/MessageEnvelope.cs +++ b/src/CometD.NetCore.Salesforce/Messaging/MessageEnvelope.cs @@ -8,12 +8,12 @@ /// /// /// - public class MessageEnvelope where TPayload : MessagePayload + public class MessageEnvelope where TPayload : MessagePayload { /// /// where TPayload is a generic message. /// - public MessageData Data { get; set; } + public MessageData? Data { get; set; } /// /// Channel or event information. @@ -21,6 +21,6 @@ public class MessageEnvelope where TPayload : MessagePayload /// "channel": "/event/Custom_Event__e" /// /// - public string Channel { get; set; } + public string Channel { get; set; } = string.Empty; } } diff --git a/src/CometD.NetCore.Salesforce/Messaging/MessagePayload.cs b/src/CometD.NetCore.Salesforce/Messaging/MessagePayload.cs index 0194f87..91a74ef 100644 --- a/src/CometD.NetCore.Salesforce/Messaging/MessagePayload.cs +++ b/src/CometD.NetCore.Salesforce/Messaging/MessagePayload.cs @@ -15,6 +15,6 @@ public class MessagePayload /// /// The user id. /// - public string CreatedById { get; set; } + public string CreatedById { get; set; } = string.Empty; } } diff --git a/src/CometD.NetCore.Salesforce/Resilience/IResilientForceClient.cs b/src/CometD.NetCore.Salesforce/Resilience/IResilientForceClient.cs index 529830b..de7b2c4 100644 --- a/src/CometD.NetCore.Salesforce/Resilience/IResilientForceClient.cs +++ b/src/CometD.NetCore.Salesforce/Resilience/IResilientForceClient.cs @@ -16,10 +16,10 @@ public interface IResilientForceClient /// The query must start with SELECT COUNT() FROM, with no named field in the count /// clause. COUNT() must be the only element in the SELECT list. /// - /// SOQL query string starting with SELECT COUNT() FROM + /// SOQL query string starting with SELECT COUNT() FROM. /// True if deleted records are to be included. The default is false. /// - /// The System.Threading.Tasks.Task`1 returning the count + /// The System.Threading.Tasks.Task`1 returning the count. Task CountQueryAsync( string queryString, bool queryAll = false, @@ -28,27 +28,27 @@ Task CountQueryAsync( /// /// Creates a new record. /// - /// The SObject name, e.g. "Account" + /// The type for the SObject name, e.g. "Account". /// - /// The SObject name, e.g. "Account" + /// The SObject name, e.g. "Account". /// Custom headers to include in request (Optional). await The HeaderFormatter helper /// class can be used to generate the custom header as needed. /// - /// The Cancellation Token - /// CreateResponse object, includes new object's ID + /// The Cancellation Token. + /// CreateResponse object, includes new object's ID. Task CreateRecordAsync( string sObjectTypeName, T sObject, - Dictionary customHeaders = null, + Dictionary? customHeaders = null, CancellationToken cancellationToken = default); /// - /// Delete record + /// Delete record. /// - /// SObject name, e.g. "Account" - /// Id of Object to update + /// SObject name, e.g. "Account". + /// Id of Object to update. /// - /// void, API returns 204/NoContent + /// void, API returns 204/NoContent. Task DeleteRecordAsync( string sObjectTypeName, string objectId, @@ -58,9 +58,9 @@ Task DeleteRecordAsync( /// Get a List of Objects /// Use the Describe Global resource to list the objects available in your org and /// available to the logged-in user. This resource also returns the org encoding, - /// as well as maximum batch size permitted in queries. /// + /// as well as maximum batch size permitted in queries. ///. /// - /// Returns DescribeGlobal object with a SObjectDescribe collection + /// Returns DescribeGlobal object with a SObjectDescribe collection. Task DescribeGlobalAsync(CancellationToken cancellationToken = default); /// @@ -75,14 +75,14 @@ Task DeleteRecordAsync( /// /// Task> GetAvailableRestApiVersionsAsync( - string currentInstanceUrl = null, + string? currentInstanceUrl = null, CancellationToken cancellationToken = default); /// /// Retrieve(basic) metadata for an object. /// Use the SObject Basic Information resource to retrieve metadata for an object. /// - /// SObject name, e.g. Account + /// SObject name, e.g. Account. /// /// Task GetObjectBasicInfoAsync( @@ -90,18 +90,18 @@ Task GetObjectBasicInfoAsync( CancellationToken cancellationToken = default); /// - /// Get SObject by ID + /// Get SObject by ID. /// /// - /// SObject name, e.g. "Account" - /// SObject ID + /// SObject name, e.g. "Account". + /// SObject ID. /// (optional) List of fields to retrieve, if not supplied, all fields are retrieved. /// /// Task GetObjectByIdAsync( string sObjectTypeName, string objectId, - List fields = null, + List? fields = null, CancellationToken cancellationToken = default); /// @@ -111,7 +111,7 @@ Task GetObjectByIdAsync( /// /// /// - /// Returns SObjectMetadataAll with full object meta including field metadata + /// Returns SObjectMetadataAll with full object meta including field metadata. Task GetObjectDescribeAsync( string objectTypeName, CancellationToken cancellationToken = default); @@ -127,23 +127,23 @@ Task GetObjectDescribeAsync( /// /// Get current user's info via Identity URL - /// https://developer.salesforce.com/docs/atlas.en-us.mobile_sdk.meta/mobile_sdk/oauth_using_identity_urls.htm + /// https://developer.salesforce.com/docs/atlas.en-us.mobile_sdk.meta/mobile_sdk/oauth_using_identity_urls.htm. /// /// /// - /// UserInfo + /// UserInfo. Task GetUserInfoAsync( string identityUrl, CancellationToken cancellationToken = default); /// - /// Inserts or Updates a records based on external id + /// Inserts or Updates a records based on external id. /// /// - /// SObject name, e.g. "Account" - /// External ID field name - /// External ID field value - /// Object to update + /// SObject name, e.g. "Account". + /// External ID field name. + /// External ID field value. + /// Object to update. /// Custom headers to include in request (Optional). await The HeaderFormatter helper class /// can be used to generate the custom header as needed. /// . @@ -153,7 +153,7 @@ Task InsertOrUpdateRecordAsync( string fieldName, string fieldValue, T sObject, - Dictionary customHeaders = null, + Dictionary? customHeaders = null, CancellationToken cancellationToken = default); /// @@ -162,8 +162,8 @@ Task InsertOrUpdateRecordAsync( /// you want to limit results, use the LIMIT operator in your query. /// /// - /// SOQL query string, without any URL escaping/encoding - /// True if deleted records are to be included + /// SOQL query string, without any URL escaping/encoding. + /// True if deleted records are to be included. /// /// Task> QueryAsync( @@ -177,10 +177,10 @@ Task> QueryAsync( /// are note sure of a single result, use Query{T} instead. /// /// - /// SOQL query string, without any URL escaping/encoding - /// True if deleted records are to be included + /// SOQL query string, without any URL escaping/encoding. + /// True if deleted records are to be included. /// - /// result object + /// result object. Task QuerySingleAsync( string queryString, bool queryAll = false, @@ -190,12 +190,12 @@ Task QuerySingleAsync( /// Executes a SOSL search, returning a type T, e.g. when using "RETURNING Account" /// in the SOSL query. /// Not properly matching the return type T and the RETURNING clause of the SOSL - /// query may return unexpected results + /// query may return unexpected results. /// /// /// /// - /// SearchResult{T} + /// SearchResult{T}. Task> SearchAsync( string searchString, CancellationToken cancellationToken = default); @@ -211,16 +211,16 @@ Task> SearchAsync( /// /// True or false. Does not throw exceptions, only false in case of any errors. Task TestConnectionAsync( - string currentInstanceUrl = null, + string? currentInstanceUrl = null, CancellationToken cancellationToken = default); /// - /// Updates + /// Updates. /// /// - /// SObject name, e.g. "Account" - /// Id of Object to update - /// Object to update + /// SObject name, e.g. "Account". + /// Id of Object to update. + /// Object to update. /// /// Custom headers to include in request (Optional). await The HeaderFormatter helper /// class can be used to generate the custom header as needed. @@ -231,7 +231,7 @@ Task UpdateRecordAsync( string sObjectTypeName, string objectId, T sObject, - Dictionary customHeaders = null, + Dictionary? customHeaders = null, CancellationToken cancellationToken = default); } } diff --git a/src/CometD.NetCore.Salesforce/Resilience/ResilientForceClient.cs b/src/CometD.NetCore.Salesforce/Resilience/ResilientForceClient.cs index 6d58e72..33c4de1 100644 --- a/src/CometD.NetCore.Salesforce/Resilience/ResilientForceClient.cs +++ b/src/CometD.NetCore.Salesforce/Resilience/ResilientForceClient.cs @@ -21,10 +21,10 @@ public class ResilientForceClient : IResilientForceClient private readonly ILogger _logger; private readonly AsyncPolicyWrap _policy; - private const string PolicyContextMethod = nameof(PolicyContextMethod); + private readonly string _policyContextMethod = nameof(_policyContextMethod); /// - /// Constructor for + /// Initializes a new instance of the class. /// /// /// @@ -47,7 +47,7 @@ public ResilientForceClient( _policy = Policy.WrapAsync(GetAuthenticationRetryPolicy(), GetWaitAndRetryPolicy()); } - /// + /// public async Task CountQueryAsync( string queryString, bool queryAll = false, @@ -55,7 +55,7 @@ public async Task CountQueryAsync( { var mContext = new Context { - [PolicyContextMethod] = nameof(CountQueryAsync) + [_policyContextMethod] = nameof(CountQueryAsync) }; return await _policy.ExecuteAsync( @@ -68,16 +68,16 @@ public async Task CountQueryAsync( cancellationToken); } - /// + /// public async Task CreateRecordAsync( string sObjectTypeName, T sObject, - Dictionary customHeaders = null, + Dictionary? customHeaders = null, CancellationToken cancellationToken = default) { var mContext = new Context { - [PolicyContextMethod] = nameof(CreateRecordAsync) + [_policyContextMethod] = nameof(CreateRecordAsync) }; return await _policy.ExecuteAsync( @@ -93,7 +93,7 @@ public async Task CreateRecordAsync( cancellationToken); } - /// + /// public Task DeleteRecordAsync( string sObjectTypeName, string objectId, @@ -101,7 +101,7 @@ public Task DeleteRecordAsync( { var mContext = new Context { - [PolicyContextMethod] = nameof(DeleteRecordAsync) + [_policyContextMethod] = nameof(DeleteRecordAsync) }; return _policy.ExecuteAsync( @@ -114,12 +114,12 @@ public Task DeleteRecordAsync( cancellationToken); } - /// + /// public async Task DescribeGlobalAsync(CancellationToken cancellationToken = default) { var mContext = new Context { - [PolicyContextMethod] = nameof(DescribeGlobalAsync) + [_policyContextMethod] = nameof(DescribeGlobalAsync) }; return await _policy.ExecuteAsync( @@ -132,14 +132,14 @@ public async Task DescribeGlobalAsync(CancellationToken cancella cancellationToken); } - /// + /// public async Task> GetAvailableRestApiVersionsAsync( - string currentInstanceUrl = null, + string? currentInstanceUrl = null, CancellationToken cancellationToken = default) { var mContext = new Context { - [PolicyContextMethod] = nameof(GetAvailableRestApiVersionsAsync) + [_policyContextMethod] = nameof(GetAvailableRestApiVersionsAsync) }; return await _policy.ExecuteAsync( @@ -152,14 +152,14 @@ public async Task> GetAvailableRestApiVersionsAsync( cancellationToken); } - /// + /// public async Task GetObjectBasicInfoAsync( string objectTypeName, CancellationToken cancellationToken = default) { var mContext = new Context { - [PolicyContextMethod] = nameof(GetObjectBasicInfoAsync) + [_policyContextMethod] = nameof(GetObjectBasicInfoAsync) }; return await _policy.ExecuteAsync( @@ -172,16 +172,16 @@ public async Task GetObjectBasicInfoAsync( cancellationToken); } - /// + /// public async Task GetObjectByIdAsync( string sObjectTypeName, string objectId, - List fields = null, + List? fields = null, CancellationToken cancellationToken = default) { var mContext = new Context { - [PolicyContextMethod] = nameof(GetObjectByIdAsync) + [_policyContextMethod] = nameof(GetObjectByIdAsync) }; return await _policy.ExecuteAsync( @@ -197,14 +197,14 @@ public async Task GetObjectByIdAsync( cancellationToken); } - /// + /// public async Task GetObjectDescribeAsync( string objectTypeName, CancellationToken cancellationToken = default) { var mContext = new Context { - [PolicyContextMethod] = nameof(GetObjectDescribeAsync) + [_policyContextMethod] = nameof(GetObjectDescribeAsync) }; return await _policy.ExecuteAsync( @@ -217,12 +217,12 @@ public async Task GetObjectDescribeAsync( cancellationToken); } - /// + /// public async Task GetOrganizationLimitsAsync(CancellationToken cancellationToken = default) { var mContext = new Context { - [PolicyContextMethod] = nameof(GetOrganizationLimitsAsync) + [_policyContextMethod] = nameof(GetOrganizationLimitsAsync) }; return await _policy.ExecuteAsync( @@ -235,14 +235,14 @@ public async Task GetOrganizationLimitsAsync(CancellationTok cancellationToken); } - /// + /// public async Task GetUserInfoAsync( string identityUrl, CancellationToken cancellationToken = default) { var mContext = new Context { - [PolicyContextMethod] = nameof(GetUserInfoAsync) + [_policyContextMethod] = nameof(GetUserInfoAsync) }; return await _policy.ExecuteAsync( @@ -255,18 +255,18 @@ public async Task GetUserInfoAsync( cancellationToken); } - /// + /// public async Task InsertOrUpdateRecordAsync( string sObjectTypeName, string fieldName, string fieldValue, T sObject, - Dictionary customHeaders = null, + Dictionary? customHeaders = null, CancellationToken cancellationToken = default) { var mContext = new Context { - [PolicyContextMethod] = nameof(InsertOrUpdateRecordAsync) + [_policyContextMethod] = nameof(InsertOrUpdateRecordAsync) }; return await _policy.ExecuteAsync( @@ -284,7 +284,7 @@ public async Task InsertOrUpdateRecordAsync( cancellationToken); } - /// + /// public async Task> QueryAsync( string queryString, bool queryAll = false, @@ -292,7 +292,7 @@ public async Task> QueryAsync( { var mContext = new Context { - [PolicyContextMethod] = nameof(QueryAsync) + [_policyContextMethod] = nameof(QueryAsync) }; return await _policy.ExecuteAsync( @@ -307,7 +307,7 @@ public async Task> QueryAsync( cancellationToken); } - /// + /// public async Task QuerySingleAsync( string queryString, bool queryAll = false, @@ -315,7 +315,7 @@ public async Task QuerySingleAsync( { var mContext = new Context { - [PolicyContextMethod] = nameof(QuerySingleAsync) + [_policyContextMethod] = nameof(QuerySingleAsync) }; return await _policy.ExecuteAsync( @@ -330,14 +330,14 @@ public async Task QuerySingleAsync( cancellationToken); } - /// + /// public async Task> SearchAsync( string searchString, CancellationToken cancellationToken = default) { var mContext = new Context { - [PolicyContextMethod] = nameof(SearchAsync) + [_policyContextMethod] = nameof(SearchAsync) }; return await _policy.ExecuteAsync( @@ -350,14 +350,14 @@ public async Task> SearchAsync( cancellationToken); } - /// + /// public async Task TestConnectionAsync( - string currentInstanceUrl = null, + string? currentInstanceUrl = null, CancellationToken cancellationToken = default) { var mContext = new Context { - [PolicyContextMethod] = nameof(TestConnectionAsync) + [_policyContextMethod] = nameof(TestConnectionAsync) }; return await _policy.ExecuteAsync( @@ -370,17 +370,17 @@ public async Task TestConnectionAsync( cancellationToken); } - /// + /// public Task UpdateRecordAsync( string sObjectTypeName, string objectId, T sObject, - Dictionary customHeaders = null, + Dictionary? customHeaders = null, CancellationToken cancellationToken = default) { var mContext = new Context { - [PolicyContextMethod] = nameof(UpdateRecordAsync) + [_policyContextMethod] = nameof(UpdateRecordAsync) }; return _policy.ExecuteAsync( @@ -412,7 +412,7 @@ private IAsyncPolicy GetWaitAndRetryPolicy() }, onRetry: (ex, span, context) => { - var methodName = context[PolicyContextMethod] ?? "MethodNotSpecified"; + var methodName = context[_policyContextMethod] ?? "MethodNotSpecified"; _logger.LogWarning( "{Method} wait {Seconds} to execute with exception: {Message} for named policy: {Policy}", @@ -420,8 +420,7 @@ private IAsyncPolicy GetWaitAndRetryPolicy() span.TotalSeconds, ex.Message, context.PolicyKey); - } - ) + }) .WithPolicyKey($"{nameof(ResilientForceClient)}WaitAndRetryAsync"); } @@ -433,7 +432,7 @@ private IAsyncPolicy GetAuthenticationRetryPolicy() retryCount: _options.Retry, onRetryAsync: async (ex, count, context) => { - var methodName = context[PolicyContextMethod] ?? "MethodNotSpecified"; + var methodName = context[_policyContextMethod] ?? "MethodNotSpecified"; _logger.LogWarning( "{Method} attempting to re-authenticate Retry {Count} of {Total} for named policy {PolicyKey}, due to {Message}.", diff --git a/src/CometD.NetCore.Salesforce/ResilientStreamingClient.cs b/src/CometD.NetCore.Salesforce/ResilientStreamingClient.cs index 0f958a8..f72dcbe 100644 --- a/src/CometD.NetCore.Salesforce/ResilientStreamingClient.cs +++ b/src/CometD.NetCore.Salesforce/ResilientStreamingClient.cs @@ -18,23 +18,25 @@ namespace CometD.NetCore.Salesforce { public class ResilientStreamingClient : IStreamingClient { - private BayeuxClient _bayeuxClient = null; - private bool _isDisposed = false; - private ErrorExtension _errorExtension; - private LongPollingTransport _clientTransport; - private ReplayExtension _replayIdExtension; - private readonly ILogger _logger; private readonly SalesforceConfiguration _options; private readonly AsyncExpiringLazy _tokenResponse; // long polling duration - private const int ReadTimeOut = 120 * 1000; - - public bool IsConnected => _bayeuxClient.Connected; + private readonly int _readTimeOut = 120 * 1000; - public event EventHandler Reconnect; + private BayeuxClient? _bayeuxClient = null; + private bool _isDisposed = false; + private ErrorExtension? _errorExtension; + private LongPollingTransport? _clientTransport; + private ReplayExtension? _replayIdExtension; + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// public ResilientStreamingClient( Func> tokenResponse, IOptions options, @@ -47,13 +49,36 @@ public ResilientStreamingClient( CreateBayeuxClient(); } - /// + /// + /// Finalizes an instance of the class. + /// + ~ResilientStreamingClient() + { + Dispose(false); + } + + public event EventHandler? Reconnect; + + public bool IsConnected + { + get + { + if (_bayeuxClient == null) + { + return false; + } + + return _bayeuxClient.Connected; + } + } + + /// public void Disconnect() { Disconnect(1000); } - /// + /// public void Disconnect(int timeout) { if (_isDisposed) @@ -67,9 +92,12 @@ public void Disconnect(int timeout) _bayeuxClient?.Disconnect(); _bayeuxClient?.WaitFor(timeout, new[] { BayeuxClient.State.DISCONNECTED }); - _errorExtension.ConnectionError -= ErrorExtension_ConnectionError; - _errorExtension.ConnectionException -= ErrorExtension_ConnectionException; - _errorExtension.ConnectionMessage -= ErrorExtension_ConnectionMessage; + if (_errorExtension != null) + { + _errorExtension.ConnectionError -= ErrorExtension_ConnectionError; + _errorExtension.ConnectionException -= ErrorExtension_ConnectionException; + _errorExtension.ConnectionMessage -= ErrorExtension_ConnectionMessage; + } _logger.LogDebug("Disconnected..."); } @@ -82,9 +110,14 @@ public void Handshake() Handshake(1000); } - /// + /// public void Handshake(int timeout) { + if (_bayeuxClient == null) + { + return; + } + if (_isDisposed) { throw new ObjectDisposedException("Cannot connect when disposed"); @@ -96,7 +129,7 @@ public void Handshake(int timeout) _logger.LogDebug("Connected"); } - /// + /// public void SubscribeTopic( string topicName, IMessageListener listener, @@ -112,14 +145,14 @@ public void SubscribeTopic( throw new ArgumentNullException(nameof(listener)); } - var channel = _bayeuxClient.GetChannel(topicName, replayId); + var channel = _bayeuxClient?.GetChannel(topicName, replayId); channel?.Subscribe(listener); } - /// + /// public bool UnsubscribeTopic( string topicName, - IMessageListener listener = null, + IMessageListener? listener = null, long replayId = -1) { if (topicName == null || (topicName = topicName.Trim()).Length == 0) @@ -127,7 +160,7 @@ public bool UnsubscribeTopic( throw new ArgumentNullException(nameof(topicName)); } - var channel = _bayeuxClient.GetChannel(topicName, replayId); + var channel = _bayeuxClient?.GetChannel(topicName, replayId); if (channel != null) { if (listener != null) @@ -138,13 +171,15 @@ public bool UnsubscribeTopic( { channel.Unsubscribe(); } + return true; } + return false; } /// - /// Disposing of the resources + /// Disposing of the resources. /// public void Dispose() { @@ -153,7 +188,7 @@ public void Dispose() } /// - /// Disposing of the resources + /// Disposing of the resources. /// /// protected virtual void Dispose(bool disposing) @@ -165,11 +200,6 @@ protected virtual void Dispose(bool disposing) } } - ~ResilientStreamingClient() - { - Dispose(false); - } - private void CreateBayeuxClient() { if (_isDisposed) @@ -190,8 +220,8 @@ private void CreateBayeuxClient() // Salesforce socket timeout during connection(CometD session) = 110 seconds var options = new Dictionary(StringComparer.OrdinalIgnoreCase) { - {ClientTransport.TIMEOUT_OPTION, _options.ReadTimeOut ?? ReadTimeOut }, - {ClientTransport.MAX_NETWORK_DELAY_OPTION, _options.ReadTimeOut ?? ReadTimeOut } + { ClientTransport.TIMEOUT_OPTION, _options.ReadTimeOut ?? _readTimeOut }, + { ClientTransport.MAX_NETWORK_DELAY_OPTION, _options.ReadTimeOut ?? _readTimeOut } }; _clientTransport = new LongPollingTransport(options, headers); @@ -217,8 +247,7 @@ private void ErrorExtension_ConnectionError( // authentication failure if (string.Equals(e, "403::Handshake denied", StringComparison.OrdinalIgnoreCase) || string.Equals(e, "403:denied_by_security_policy:create_denied", StringComparison.OrdinalIgnoreCase) - || string.Equals(e, "403::unknown client", StringComparison.OrdinalIgnoreCase) - ) + || string.Equals(e, "403::unknown client", StringComparison.OrdinalIgnoreCase)) { _logger.LogWarning("Handled CometD Exception: {message}", e); @@ -251,7 +280,7 @@ private void ErrorExtension_ConnectionException( { _logger.LogDebug(ex.Message); } - else + else if (ex != null) { _logger.LogError(ex.ToString()); } diff --git a/src/CometD.NetCore.Salesforce/SalesforceConfiguration.cs b/src/CometD.NetCore.Salesforce/SalesforceConfiguration.cs index b3c3eb2..cd23a79 100644 --- a/src/CometD.NetCore.Salesforce/SalesforceConfiguration.cs +++ b/src/CometD.NetCore.Salesforce/SalesforceConfiguration.cs @@ -1,56 +1,56 @@ namespace CometD.NetCore.Salesforce { /// - /// Represents the config settings in appsettings.json + /// Represents the config settings in appsettings.json. /// public sealed class SalesforceConfiguration { /// - /// Consumer Id of the Salesforce Connected App + /// Consumer Id of the Salesforce Connected App. /// - public string ClientId { get; set; } + public string ClientId { get; set; } = string.Empty; /// - /// Consumer secret of the Salesforce Connected App + /// Consumer secret of the Salesforce Connected App. /// - public string ClientSecret { get; set; } + public string ClientSecret { get; set; } = string.Empty; /// /// Url to login to Salesforce /// https://test.salesforce.com/services/oauth2/authorize - /// or https://login.salesforce.com/services/oauth2/authorize + /// or https://login.salesforce.com/services/oauth2/authorize. /// - public string LoginUrl { get; set; } + public string LoginUrl { get; set; } = string.Empty; /// - /// Url of the Salesforce organization + /// Url of the Salesforce organization. /// - public string OrganizationUrl { get; set; } + public string OrganizationUrl { get; set; } = string.Empty; /// - /// Path of the endpoint to publish platform events + /// Path of the endpoint to publish platform events. /// - public string PublishEndpoint { get; set; } + public string PublishEndpoint { get; set; } = string.Empty; /// - /// Oauth refresh token of the Salesforce Connected App + /// Oauth refresh token of the Salesforce Connected App. /// - public string RefreshToken { get; set; } + public string RefreshToken { get; set; } = string.Empty; /// - /// Uri to connect to CometD + /// Uri to connect to CometD. /// - public string CometDUri { get; set; } + public string CometDUri { get; set; } = string.Empty; /// - /// Topic or Event Uri + /// Topic or Event Uri. /// - public string EventOrTopicUri { get; set; } + public string EventOrTopicUri { get; set; } = string.Empty; /// /// Salesforce uri for oauth authentication. /// - public string OAuthUri { get; set; } + public string OAuthUri { get; set; } = string.Empty; /// /// Retry for the connections and authentications. @@ -68,9 +68,9 @@ public sealed class SalesforceConfiguration public string TokenExpiration { get; set; } = "01:00:00"; /// - /// The event that gets raised when a request for proposal is approved and the Deal is in working status + /// The event that gets raised when a request for proposal is approved and the Deal is in working status. /// - public string CustomEvent { get; set; } + public string CustomEvent { get; set; } = string.Empty; /// /// Salesforce ReplayId for specific message. diff --git a/src/CometD.NetCore.Salesforce/StreamingClient.cs b/src/CometD.NetCore.Salesforce/StreamingClient.cs index 6a75f44..3df08f8 100644 --- a/src/CometD.NetCore.Salesforce/StreamingClient.cs +++ b/src/CometD.NetCore.Salesforce/StreamingClient.cs @@ -22,26 +22,21 @@ namespace CometD.NetCore.Salesforce public class StreamingClient : IStreamingClient { - private AccessTokenResponse _tokenInfo; - - private BayeuxClient _bayeuxClient = null; - private ErrorExtension _errorExtension; - private LongPollingTransport _clientTransport; - private bool _isDisposed = false; - private ReplayExtension _replayIdExtension; - private readonly ILogger _logger; private readonly IAuthenticationClientProxy _authenticationClient; private readonly SalesforceConfiguration _options; // long polling duration - private const int ReadTimeOut = 120 * 1000; - - /// - public event EventHandler Reconnect; + private readonly int _readTimeOut = 120 * 1000; + private AccessTokenResponse? _tokenInfo; + private BayeuxClient? _bayeuxClient; + private ErrorExtension? _errorExtension; + private LongPollingTransport? _clientTransport; + private ReplayExtension? _replayIdExtension; + private bool _isDisposed; /// - /// Constructor creates instance of the class. + /// Initializes a new instance of the class. /// /// /// @@ -60,16 +55,38 @@ public StreamingClient( InitBayeuxClient(); } - /// - public bool IsConnected => _bayeuxClient.Connected; + /// + /// Finalizes an instance of the class. + /// + ~StreamingClient() + { + Dispose(false); + } + + /// + public event EventHandler? Reconnect; + + /// + public bool IsConnected + { + get + { + if (_bayeuxClient == null) + { + return false; + } + + return _bayeuxClient.Connected; + } + } - /// + /// public void Handshake() { Handshake(1000); } - /// + /// public void Handshake(int timeout) { if (_isDisposed) @@ -77,14 +94,19 @@ public void Handshake(int timeout) throw new ObjectDisposedException("Cannot connect when disposed"); } + if (_bayeuxClient == null) + { + return; + } + _logger.LogDebug("Handshaking..."); _bayeuxClient.Handshake(); _bayeuxClient.WaitFor(timeout, new[] { BayeuxClient.State.CONNECTED }); _logger.LogDebug("Connected"); } - /// - public void SubscribeTopic(string topicName, IMessageListener listener, long replayId=-1) + /// + public void SubscribeTopic(string topicName, IMessageListener listener, long replayId = -1) { if (topicName == null || (topicName = topicName.Trim()).Length == 0) { @@ -96,19 +118,19 @@ public void SubscribeTopic(string topicName, IMessageListener listener, long rep throw new ArgumentNullException(nameof(listener)); } - var channel = _bayeuxClient.GetChannel(topicName,replayId); + var channel = _bayeuxClient?.GetChannel(topicName, replayId); channel?.Subscribe(listener); } - /// - public bool UnsubscribeTopic(string topicName, IMessageListener listener = null, long replayId=-1) + /// + public bool UnsubscribeTopic(string topicName, IMessageListener? listener = null, long replayId = -1) { if (topicName == null || (topicName = topicName.Trim()).Length == 0) { throw new ArgumentNullException(nameof(topicName)); } - var channel = _bayeuxClient.GetChannel(topicName, replayId); + var channel = _bayeuxClient?.GetChannel(topicName, replayId); if (channel != null) { if (listener != null) @@ -119,18 +141,20 @@ public bool UnsubscribeTopic(string topicName, IMessageListener listener = null, { channel.Unsubscribe(); } + return true; } + return false; } - /// + /// public void Disconnect() { Disconnect(1000); } - /// + /// public void Disconnect(int timeout) { if (_isDisposed) @@ -144,13 +168,34 @@ public void Disconnect(int timeout) _bayeuxClient?.Disconnect(); _bayeuxClient?.WaitFor(timeout, new[] { BayeuxClient.State.DISCONNECTED }); - _errorExtension.ConnectionError -= ErrorExtension_ConnectionError; - _errorExtension.ConnectionException -= ErrorExtension_ConnectionException; - _errorExtension.ConnectionMessage -= ErrorExtension_ConnectionMessage; + if (_errorExtension != null) + { + _errorExtension.ConnectionError -= ErrorExtension_ConnectionError; + _errorExtension.ConnectionException -= ErrorExtension_ConnectionException; + _errorExtension.ConnectionMessage -= ErrorExtension_ConnectionMessage; + } _logger.LogDebug("Disconnected..."); } + /// + /// Disposing of the resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing && !_isDisposed) + { + Disconnect(); + _isDisposed = true; + } + } + /// /// This refreshes the session for the user. /// @@ -179,8 +224,8 @@ private void InitBayeuxClient() // Salesforce socket timeout during connection(CometD session) = 110 seconds var options = new Dictionary(StringComparer.OrdinalIgnoreCase) { - {ClientTransport.TIMEOUT_OPTION, _options.ReadTimeOut ?? ReadTimeOut }, - {ClientTransport.MAX_NETWORK_DELAY_OPTION, _options.ReadTimeOut ?? ReadTimeOut } + { ClientTransport.TIMEOUT_OPTION, _options.ReadTimeOut ?? _readTimeOut }, + { ClientTransport.MAX_NETWORK_DELAY_OPTION, _options.ReadTimeOut ?? _readTimeOut } }; var headers = new NameValueCollection { { nameof(HttpRequestHeader.Authorization), $"OAuth {_tokenInfo.AccessToken}" } }; @@ -218,7 +263,7 @@ private void ErrorExtension_ConnectionException(object sender, Exception ex) { _logger.LogDebug(ex.Message); } - else + else if (ex != null) { _logger.LogError(ex.ToString()); } @@ -234,8 +279,7 @@ private void ErrorExtension_ConnectionError(object sender, string message) // authentication failure if (string.Equals(message, "403::Handshake denied", StringComparison.OrdinalIgnoreCase) || string.Equals(message, "403:denied_by_security_policy:create_denied", StringComparison.OrdinalIgnoreCase) - || string.Equals(message, "403::unknown client", StringComparison.OrdinalIgnoreCase) - ) + || string.Equals(message, "403::unknown client", StringComparison.OrdinalIgnoreCase)) { _logger.LogWarning("Handled CometD Exception: {message}", message); @@ -257,35 +301,5 @@ private void ErrorExtension_ConnectionError(object sender, string message) _logger.LogError($"{nameof(StreamingClient)} failed with the following message: {message}"); } } - - /// - /// Disposing of the resources - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Disposing of the resources - /// - /// - protected virtual void Dispose(bool disposing) - { - if (disposing && !_isDisposed) - { - Disconnect(); - _isDisposed = true; - } - } - - /// - /// Destructor - /// - ~StreamingClient() - { - Dispose(false); - } } } diff --git a/src/CometD.NetCore.Salesforce/StreamingClientExtensions.cs b/src/CometD.NetCore.Salesforce/StreamingClientExtensions.cs index f0dbdbf..5ca3d13 100644 --- a/src/CometD.NetCore.Salesforce/StreamingClientExtensions.cs +++ b/src/CometD.NetCore.Salesforce/StreamingClientExtensions.cs @@ -23,7 +23,7 @@ public static class StreamingClientExtensions { /// /// Adds ForecClient Resilient version of it with Refresh Token Authentication. - /// https://help.salesforce.com/articleView?id=remoteaccess_oauth_refresh_token_flow.htm&type=5 + /// /// Can be used in the code with . /// /// @@ -41,7 +41,7 @@ public static IServiceCollection AddResilientStreamingClient( services.AddSingleton>>(sp => () => { - var options = sp.GetRequiredService>().Value; + var options = sp.GetRequiredService>().Value; if (!TimeSpan.TryParse(options.TokenExpiration, out var tokenExpiration)) { @@ -49,41 +49,41 @@ public static IServiceCollection AddResilientStreamingClient( } return new AsyncExpiringLazy(async data => - { - if (data.Result == null - || DateTime.UtcNow > data.ValidUntil.Subtract(TimeSpan.FromSeconds(30))) - { - var policy = Policy - .Handle() - .WaitAndRetryAsync( - retryCount: options.Retry, - sleepDurationProvider: (retryAttempt) => TimeSpan.FromSeconds(Math.Pow(options.BackoffPower, retryAttempt))); - - var authClient = await policy.ExecuteAsync(async () => - { - var auth = new AuthenticationClient(); - - await auth.TokenRefreshAsync( - options.RefreshToken, - options.ClientId, - options.ClientSecret, - $"{options.LoginUrl}{options.OAuthUri}"); - - return auth; - }); - - return new AsyncExpirationValue - { - Result = authClient.AccessInfo, - ValidUntil = DateTimeOffset.UtcNow.Add(tokenExpiration) - }; - } - - return data; - }); + { + if (data.Result == null + || DateTime.UtcNow > data.ValidUntil.Subtract(TimeSpan.FromSeconds(30))) + { + var policy = Policy + .Handle() + .WaitAndRetryAsync( + retryCount: options.Retry, + sleepDurationProvider: (retryAttempt) => TimeSpan.FromSeconds(Math.Pow(options.BackoffPower, retryAttempt))); + + var authClient = await policy.ExecuteAsync(async () => + { + var auth = new AuthenticationClient(); + + await auth.TokenRefreshAsync( + options.RefreshToken, + options.ClientId, + options.ClientSecret, + $"{options.LoginUrl}{options.OAuthUri}"); + + return auth; + }); + + return new AsyncExpirationValue + { + Result = authClient.AccessInfo, + ValidUntil = DateTimeOffset.UtcNow.Add(tokenExpiration) + }; + } + + return data; + }); }); - services.AddSingleton>>(sp => () => + services.AddSingleton>>(sp => () => { var options = sp.GetRequiredService>().Value; @@ -140,7 +140,7 @@ await authClient.TokenRefreshAsync( /// /// /// - [Obsolete("Use "+ nameof(AddResilientStreamingClient) + "extension method instead.")] + [Obsolete("Use " + nameof(AddResilientStreamingClient) + "extension method instead.")] public static IServiceCollection AddStreamingClient(this IServiceCollection services) { services.AddSingleton(sp => diff --git a/src/TestApp/Program.cs b/src/TestApp/Program.cs index 6b7505c..b749f6c 100644 --- a/src/TestApp/Program.cs +++ b/src/TestApp/Program.cs @@ -12,7 +12,7 @@ namespace TestApp { #pragma warning disable RCS1102 // Make class static. - public class Program + internal sealed class Program #pragma warning restore RCS1102 // Make class static. { public static async Task Main(string[] args) @@ -31,14 +31,14 @@ public static async Task Main(string[] args) $"appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json", optional: true); - configBuilder.AddAzureKeyVault(hostingEnviromentName:hostContext.HostingEnvironment.EnvironmentName); + configBuilder.AddAzureKeyVault(hostingEnviromentName: hostContext.HostingEnvironment.EnvironmentName); configBuilder.AddCommandLine(args); // print out the environment var config = configBuilder.Build(); config.DebugConfigurations(); }) - .ConfigureServices((context,services) => + .ConfigureServices((context, services) => { services.AddTransient, DefaultServiceProviderFactory>(); diff --git a/test/CometD.UnitTest/UnitTest1.cs b/test/CometD.UnitTest/UnitTest1.cs index 4b6213b..0f634af 100644 --- a/test/CometD.UnitTest/UnitTest1.cs +++ b/test/CometD.UnitTest/UnitTest1.cs @@ -14,18 +14,18 @@ public void Test1() { var dic = new Dictionary { - {"Salesforce:Id", "-2" }, - {"Salesforce:Name", "" } + { "Salesforce:Id", "-2" }, + { "Salesforce:Name", string.Empty } }; - IConfiguration Configuration = null; + IConfiguration configuration = null; var host = new WebHostBuilder() .ConfigureAppConfiguration((context, builder) => { - Configuration = builder.AddInMemoryCollection(dic).Build(); + configuration = builder.AddInMemoryCollection(dic).Build(); }) - .ConfigureServices((services)=> + .ConfigureServices((services) => { }); } From 3c67ae7f6b2a59eff1aa92e87ccee985af8a4ad1 Mon Sep 17 00:00:00 2001 From: KDCLLC Date: Thu, 14 Nov 2019 12:13:43 -0500 Subject: [PATCH 9/9] update documentation issue #10 --- README.md | 98 ++++---------------- src/AuthApp/AuthApp.csproj | 7 +- src/AuthApp/HostBuilderExtensions.cs | 30 +++--- src/AuthApp/HostBuilderOptions.cs | 12 +-- src/AuthApp/Internal/Constants.cs | 7 ++ src/AuthApp/{Host => Internal}/HttpServer.cs | 9 +- src/AuthApp/{Host => Internal}/SfConfig.cs | 10 +- src/AuthApp/Program.cs | 7 +- src/AuthApp/README.md | 85 +++++++++++++++++ src/AuthApp/TokenGeneratorCommand.cs | 41 ++++---- src/CometD.NetCore.Salesforce/README.md | 18 ++++ 11 files changed, 190 insertions(+), 134 deletions(-) create mode 100644 src/AuthApp/Internal/Constants.cs rename src/AuthApp/{Host => Internal}/HttpServer.cs (95%) rename src/AuthApp/{Host => Internal}/SfConfig.cs (78%) create mode 100644 src/AuthApp/README.md create mode 100644 src/CometD.NetCore.Salesforce/README.md diff --git a/README.md b/README.md index 5630963..9df57bd 100644 --- a/README.md +++ b/README.md @@ -1,102 +1,33 @@ -# CometD .NET Core implementation of Salesforce Platform events +# CometD.NetCore.Salesforce [![Build status](https://ci.appveyor.com/api/projects/status/baalfhs6vvc38icc?svg=true)](https://ci.appveyor.com/project/kdcllc/cometd-netcore-salesforce) +[![NuGet](https://img.shields.io/nuget/v/CometD.NetCore.Salesforce.svg)](https://www.nuget.org/packages?q=Bet.AspNetCore) +[![MyGet](https://img.shields.io/myget/kdcllc/v/CometD.NetCore.Salesforce.svg?label=myget)](https://www.myget.org/F/kdcllc/api/v2) This repo contains the CometD .NET Core implementation for Salesforce Platform events. - + These events can be subscribed to and listened to by your custom `Event Listener`. The sample application of this library can be found [here](https://github.com/kdcllc/Bet.BuildingBlocks.SalesforceEventBus). The solution contains the following: -1. `CometD.NetCore2.Salesforce` Project +1. [`CometD.NetCore2.Salesforce`](./src/CometD.NetCore.Salesforce/README.md) - A Salesforce Platform Events implementation based [Even Bus idea of eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers). - [Reusable Building Blocks and sample application that listens to Salesforce push events](https://github.com/kdcllc/Bet.BuildingBlocks.SalesforceEventBus). -2. DotNet Cli tool `salesforce` Project - - This dotnet cli tool allows for retrieval of `Access Token` and `Refresh Token` to be used by any other application. Please refer to [How Are Apps Authenticated with the Web Server OAuth Authentication Flow](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_understanding_web_server_oauth_flow.htm) - -## Installation - -- To include this library inside of your project run nuget package installation - -```cmd - PM> Install-Package CometD.NetCore2.Salesforce -``` - -Or - -```cmd - dotnet add package CometD.NetCore.Salesforce -``` - -- To Install Salesforce Cli tool globally run the following command: - -```cmd - dotnet tool install salesforce -g - -``` - -To verify the installation run: - -```cmd - dotnet tool list -g -``` - -## Usage of Salesforce dotnet cli tool - -There are several ways to run this cli tool. - -1. From any location with Consumer Key and Secret provided +2. [DotNet Cli tool `salesforce`](./src/AuthApp/README.md) + - This dotnet cli tool allows for retrieval of `Access` or `Refresh Tokens` to be used by any other application. + Please refer to [How Are Apps Authenticated with the Web Server OAuth Authentication Flow](https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/intro_understanding_web_server_oauth_flow.htm) -```bash - salesforce get-tokens --key:{key} --secret:{secret} --login:https://login.salesforce.com --verbose:information -``` - -2. Running the tool in the directory that contains `appsettings.json` file - -```bash - salesforce get-tokens --section Salesforce -``` -Note: required configurations are as follows: - -```json - "Salesforce": { - "ClientId": "", - "ClientSecret": "", - "LoginUrl": "" - } -```` - -3. Running with Azure Vault - -`appsettings.json -```json - "AzureVault": { - "BaseUrl": "https://{name}.vault.azure.net/" - }, -``` - -Then run: - -```cmd - salesforce get-tokens --verbose:debug -``` - -Or specify url within the dotnet cli tool like so: - -```cmd - salesforce get-tokens --azure https://{name}.vault.azure.net/" -``` +## Saleforce Setup -This tool will open web browser and will require you to log in with your credentials to Salesforce portal in order to retrieve the tokens. +[Watch Video](https://www.youtube.com/watch?v=L6OWyCfQD6U) -## Saleforce Setup -[Video](https://www.youtube.com/watch?v=L6OWyCfQD6U) 1. Sing up for development sandbox with Saleforce: [https://developer.salesforce.com/signup](https://developer.salesforce.com/signup). 2. Create Connected App in Salesforce. 3. Create a Platform Event. ### Create Connected App in Salesforce + 1. Setup -> Quick Find -> manage -> App Manager -> New Connected App. 2. Basic Info: @@ -120,13 +51,16 @@ This tool will open web browser and will require you to log in with your credent Use workbench to test the Event [workbench](https://workbench.developerforce.com/login.php?startUrl=%2Finsert.php) ## AuthApp + [Use login instead of test](https://github.com/developerforce/Force.com-Toolkit-for-NET/wiki/Web-Server-OAuth-Flow-Sample#am-i-using-the-test-environment) Simple application that provides with Web Server OAuth Authentication Flow to retrieve `Access Token` and `Refresh Token` to be used within the application. -## Special thanks to the following projects and contributors: +## Special thanks to + - [Oyatel/CometD.NET](https://github.com/Oyatel/CometD.NET) - [nthachus/CometD.NET](https://github.com/nthachus/CometD.NET) - [tdawgy/CometD.NetCore](https://github.com/tdawgy/CometD.NetCore) - [eShopOnContainers](https://github.com/dotnet-architecture/eShopOnContainers) -- [Chris Woolum](https://github.com/cwoolum) +- [cwoolum](https://github.com/cwoolum) +- [ts46235](https://github.com/ts46235) diff --git a/src/AuthApp/AuthApp.csproj b/src/AuthApp/AuthApp.csproj index e460356..704fb95 100644 --- a/src/AuthApp/AuthApp.csproj +++ b/src/AuthApp/AuthApp.csproj @@ -3,12 +3,17 @@ Exe netcoreapp2.2; + enable salesforce salesforce - A command-line tool that enables Salesforce Access and Refresh Tokens Generation. True + + A command-line tool that enables Salesforce Access and Refresh Tokens Generation. + Salesforce refresh tokens, Salesforce access tokens, DotNetCore, AspNetCore, salesforce, OAuth Authentication Flow + + diff --git a/src/AuthApp/HostBuilderExtensions.cs b/src/AuthApp/HostBuilderExtensions.cs index d4c3656..d3389df 100644 --- a/src/AuthApp/HostBuilderExtensions.cs +++ b/src/AuthApp/HostBuilderExtensions.cs @@ -55,8 +55,19 @@ internal static IHostBuilder CreateDefaultBuilder(HostBuilderOptions options) // configure Azure Vault from the other settings. var appAzureVaultUrl = config.Build().Bind("AzureVault", enableValidation: false); + var inputValues = new Dictionary + { + { $"{options.SectionName}:ClientId", options?.Settings?.ClientId ?? string.Empty }, + { $"{options.SectionName}:ClientSecret", options?.Settings?.ClientSecret ?? string.Empty }, + { $"{options.SectionName}:LoginUrl", options?.Settings?.LoginUrl ?? string.Empty }, + { $"{options.SectionName}:OAuthUri", options?.Settings?.OAuthUri ?? string.Empty }, + { $"{options.SectionName}:OAuthorizeUri", options?.Settings?.OAuthorizeUri ?? string.Empty }, + }; + + config.AddInMemoryCollection(inputValues); + // build azure key vault from passed in parameter - if (!string.IsNullOrWhiteSpace(options.AzureVault)) + if (!string.IsNullOrWhiteSpace(options?.AzureVault)) { var dic = new Dictionary { @@ -68,26 +79,11 @@ internal static IHostBuilder CreateDefaultBuilder(HostBuilderOptions options) // use appsettings vault information if (!string.IsNullOrWhiteSpace(appAzureVaultUrl.BaseUrl) - || !string.IsNullOrWhiteSpace(options.AzureVault)) + || !string.IsNullOrWhiteSpace(options?.AzureVault)) { config.AddAzureKeyVault(hostingEnviromentName: options.HostingEnviroment, options.UseAzureKeyPrefix); } - if (!string.IsNullOrWhiteSpace(options.Settings.ClientId) - && !string.IsNullOrWhiteSpace(options.Settings.ClientSecret)) - { - var inputValues = new Dictionary - { - { $"{options.SectionName}:ClientId", options.Settings.ClientId }, - { $"{options.SectionName}:ClientSecret", options.Settings.ClientSecret }, - { $"{options.SectionName}:LoginUrl", options.Settings.LoginUrl }, - { $"{options.SectionName}:OAuthUri", options.Settings.OAuthUri }, - { $"{options.SectionName}:OAuthorizeUri", options.Settings.OAuthorizeUri }, - }; - - config.AddInMemoryCollection(inputValues); - } - if ((options.Verbose && options.Level == LogLevel.Debug) || options.Level == LogLevel.Trace) { diff --git a/src/AuthApp/HostBuilderOptions.cs b/src/AuthApp/HostBuilderOptions.cs index e87c0e0..b1dd369 100644 --- a/src/AuthApp/HostBuilderOptions.cs +++ b/src/AuthApp/HostBuilderOptions.cs @@ -1,4 +1,4 @@ -using AuthApp.Host; +using AuthApp.Internal; using Microsoft.Extensions.Logging; @@ -9,7 +9,7 @@ internal class HostBuilderOptions /// /// Sets Configuration file besides appsettings.json. /// - public string ConfigFile { get; set; } + public string? ConfigFile { get; set; } /// /// Provides ability to get troubleshooting information. @@ -29,7 +29,7 @@ internal class HostBuilderOptions /// /// Url for the azure key vault i.e. https://{vaultname}.vault.azure.net/. /// - public string AzureVault { get; set; } + public string? AzureVault { get; set; } /// /// Prefix is based on Environment i.e. Development = dev, Production = prod. @@ -39,16 +39,16 @@ internal class HostBuilderOptions /// /// Pass Hosting environment for the context of the application. /// - public string HostingEnviroment { get; set; } + public string HostingEnviroment { get; set; } = "Production"; /// /// Salesforce options. /// - public SfConfig Settings { get; set; } + public SfConfig? Settings { get; set; } /// /// The name of the configuration section for the options. /// - public string SectionName { get; set; } + public string SectionName { get; set; } = "Salesforce"; } } diff --git a/src/AuthApp/Internal/Constants.cs b/src/AuthApp/Internal/Constants.cs new file mode 100644 index 0000000..7135b81 --- /dev/null +++ b/src/AuthApp/Internal/Constants.cs @@ -0,0 +1,7 @@ +namespace AuthApp.Internal +{ + internal static class Constants + { + public const string CLIToolName = "salesforce"; + } +} diff --git a/src/AuthApp/Host/HttpServer.cs b/src/AuthApp/Internal/HttpServer.cs similarity index 95% rename from src/AuthApp/Host/HttpServer.cs rename to src/AuthApp/Internal/HttpServer.cs index ce366df..38df8e2 100644 --- a/src/AuthApp/Host/HttpServer.cs +++ b/src/AuthApp/Internal/HttpServer.cs @@ -15,7 +15,7 @@ using Console = Colorful.Console; -namespace AuthApp.Host +namespace AuthApp.Internal { /// /// Web Server OAuth Authentication Flow @@ -40,6 +40,10 @@ public HttpServer( protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + Console.WriteLine(); + Console.WriteAscii("Token Generation Started...", Colorful.FigletFont.Default); + Console.WriteLine(); + if (_hostOptions.Verbose) { Console.WriteLine($"{nameof(HttpServer)} is starting."); @@ -121,6 +125,9 @@ await auth.WebServerAsync( } await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); + + Console.WriteLine(); + Console.Write("Thanks for using this cli tool"); } } diff --git a/src/AuthApp/Host/SfConfig.cs b/src/AuthApp/Internal/SfConfig.cs similarity index 78% rename from src/AuthApp/Host/SfConfig.cs rename to src/AuthApp/Internal/SfConfig.cs index 5930d70..d1dd040 100644 --- a/src/AuthApp/Host/SfConfig.cs +++ b/src/AuthApp/Internal/SfConfig.cs @@ -1,27 +1,27 @@ using System.ComponentModel.DataAnnotations; -namespace AuthApp.Host +namespace AuthApp.Internal { - public class SfConfig + internal class SfConfig { /// /// Salesforce Client Id. /// [Required] - public string ClientId { get; set; } + public string? ClientId { get; set; } /// /// Salesforece Secret Id. /// [Required] - public string ClientSecret { get; set; } + public string? ClientSecret { get; set; } /// /// i.e. https://login.salesforce.com. /// [Required] [Url] - public string LoginUrl { get; set; } + public string? LoginUrl { get; set; } /// /// Default set to /services/oauth2/token. diff --git a/src/AuthApp/Program.cs b/src/AuthApp/Program.cs index e79a2d0..29f889f 100644 --- a/src/AuthApp/Program.cs +++ b/src/AuthApp/Program.cs @@ -1,13 +1,15 @@ using System.Reflection; using System.Threading.Tasks; +using AuthApp.Internal; + using McMaster.Extensions.CommandLineUtils; using Console = Colorful.Console; namespace AuthApp { - [Command(Name = "salesforce", Description = "cli tool to help with salesforce development.")] + [Command(Name = Constants.CLIToolName, Description = "cli tool to help with Salesforce development by providing with Refresh token generation.")] [Subcommand(typeof(TokenGeneratorCommand))] [HelpOption("-?")] [VersionOptionFromMember("--version", MemberName = nameof(GetVersion))] @@ -25,8 +27,9 @@ private static string GetVersion() private int OnExecute(CommandLineApplication app, IConsole console) { - Console.WriteAscii("Salesforce", Colorful.FigletFont.Default); + Console.WriteAscii(Constants.CLIToolName, Colorful.FigletFont.Default); + console.WriteLine(); console.WriteLine("You must specify at a subcommand."); app.ShowHelp(); return 1; diff --git a/src/AuthApp/README.md b/src/AuthApp/README.md new file mode 100644 index 0000000..782a1b9 --- /dev/null +++ b/src/AuthApp/README.md @@ -0,0 +1,85 @@ +# `salesforce` CLI + +[![Build status](https://ci.appveyor.com/api/projects/status/baalfhs6vvc38icc?svg=true)](https://ci.appveyor.com/project/kdcllc/cometd-netcore-salesforce) +[![NuGet](https://img.shields.io/nuget/v/salesforce.svg)](https://www.nuget.org/packages?q=Bet.AspNetCore) +[![MyGet](https://img.shields.io/myget/kdcllc/v/salesforce.svg?label=myget)](https://www.myget.org/F/kdcllc/api/v2) + +This is a dotnet cli Saleforce Refresh and Access Tokens Generation tool. + +## Install DotNetCore Cli `salesforce` tool + +```bash + dotnet tool install salesforce -g +``` + +To verify the installation run: + +```bash + dotnet tool list -g +``` + +## Usage of Salesforce dotnet cli tool + +There are several ways to run this cli tool. + +This tool will open web browser and will require you to log in with your credentials to Salesforce portal in order to retrieve the tokens. + +1. From any location with Consumer Key and Secret provided + +```bash + # specify the custom login url + salesforce get-tokens --key:{key} --secret:{secret} --login:https://login.salesforce.com --verbose:information + + # use default login url + salesforce get-tokens --key:{key} --secret:{secret} --verbose +``` + +2. Running the tool in the directory that contains `appsettings.json` file with configurations + +```bash + salesforce get-tokens --section:Salesforce +``` + +Note: required configurations are as follows: + +```json + "Salesforce": { + "ClientId": "", + "ClientSecret": "", + "LoginUrl": "" + } +```` + +3. Running with Azure Vault + +a.) Location with `appsettings.json` file + +```json + "AzureVault": { + "BaseUrl": "https://{name}.vault.azure.net/" + }, +``` + +```bash + salesforce get-tokens --verbose:debug +``` +b.) From any location + +Or specify url within the dotnet cli tool like so: + +```cmd + salesforce get-tokens --azure https://{name}.vault.azure.net/" +``` + +## Tools possible switches + +- `--key` or `-k` (Salesforce `Consumer Key`) +- `--secret` or `-s` (Salesforce `Consumer Secret`) +- `--login` or `-l` (Salesforce login url) +- `--azure` or `-a` (Azure Vault Url) +- `--azureprefix` or `ax` ([Use Environment prefix for Azure vault](https://github.com/kdcllc/Bet.AspNetCore/blob/d8ff3b7bfb13817bc2b6b768d91ea19a2bc865a5/src/Bet.Extensions.AzureVault/AzureVaultKeyBuilder.cs#L24)) +- `--configfile` or `-c` (Specify configuration file) +- `--verbose:debug` or `--verbose:information` or `--verbose:trave` +- `--usesecrets` or `us` (Usually a Guid Id of the project that contains the secret) +- `--environment` or `-e` (Production, Development, Stage) +- `--section` or `-sn` (The root for the tools configuration the default is `Salesforce`) diff --git a/src/AuthApp/TokenGeneratorCommand.cs b/src/AuthApp/TokenGeneratorCommand.cs index 3b4b937..0ed602e 100644 --- a/src/AuthApp/TokenGeneratorCommand.cs +++ b/src/AuthApp/TokenGeneratorCommand.cs @@ -2,7 +2,7 @@ using System.Drawing; using System.Threading.Tasks; -using AuthApp.Host; +using AuthApp.Internal; using McMaster.Extensions.CommandLineUtils; @@ -18,29 +18,30 @@ namespace AuthApp [Command( "get-tokens", Description = "Generates Salesforce Access and Refresh Tokens", - ThrowOnUnexpectedArgument = false)] + ThrowOnUnexpectedArgument = false, + AllowArgumentSeparator = true)] [HelpOption("-?")] internal class TokenGeneratorCommand { - [Option("--key", Description = "The Salesforce Consumer Key.")] - public string ClientId { get; set; } + [Option("-k|--key", Description = "The Salesforce Consumer Key.")] + public string? ClientId { get; set; } - [Option("--secret", Description = "The Salesforce Consumer Secret.")] - public string ClientSecret { get; set; } + [Option("-s|--secret", Description = "The Salesforce Consumer Secret.")] + public string? ClientSecret { get; set; } - [Option("--login", Description = "The Salesforce login url. The default value is https://login.salesforce.com.")] - public string LoginUrl { get; set; } + [Option("-l|--login", Description = "The Salesforce login url. The default value is https://login.salesforce.com.")] + public string? LoginUrl { get; set; } [Option( - "--azure", + "-a|--azure", Description = "Allows to specify Azure Vault Url. It overrides url specified in the appsetting.json file or any other configuration provider.")] - public string AzureVault { get; set; } + public string? AzureVault { get; set; } - [Option("--azureprefix", Description = "Enables or disables Hosting Environment prefix to be used for Azure Key Vault. Default is true.")] + [Option("-ax|--azureprefix", Description = "Enables or disables Hosting Environment prefix to be used for Azure Key Vault. Default is true.")] public bool UseAzureKeyPrefix { get; set; } - [Option("--configfile", Description = "Allows to specify a configuration file besides appsettings.json to be specified.")] - public string ConfigFile { get; set; } + [Option("-c|--configfile", Description = "Allows to specify a configuration file besides appsettings.json to be specified.")] + public string? ConfigFile { get; set; } /// /// Property types of ValueTuple{bool,T} translate to CommandOptionType.SingleOrNoValue. @@ -55,14 +56,14 @@ internal class TokenGeneratorCommand [Option(Description = "Allows Verbose logging for the tool. Enable this to get tracing information. Default is false and LogLevel.None.")] public (bool HasValue, LogLevel level) Verbose { get; } = (false, LogLevel.None); - [Option("--usesecrets", Description = "Enable UserSecrets.")] + [Option("-us|--usesecrets", Description = "Enable UserSecrets.")] public bool UserSecrets { get; set; } - [Option("--environment", Description = "Specify Hosting Environment Name for the cli tool execution.")] - public string HostingEnviroment { get; set; } + [Option("-e|--environment", Description = "Specify Hosting Environment Name for the cli tool execution.")] + public string? HostingEnviroment { get; set; } - [Option("--section", Description = "Configuration Section Name to retrieve the options. The Default value is Salesforce.")] - public string SectionName { get; set; } + [Option("-sn|--section", Description = "Configuration Section Name to retrieve the options. The Default value is Salesforce.")] + public string? SectionName { get; set; } private async Task OnExecuteAsync(CommandLineApplication app) { @@ -74,14 +75,14 @@ private async Task OnExecuteAsync(CommandLineApplication app) Verbose = Verbose.HasValue, Level = Verbose.level, UserSecrets = UserSecrets, - HostingEnviroment = !string.IsNullOrWhiteSpace(HostingEnviroment) ? HostingEnviroment : "Development", + HostingEnviroment = !string.IsNullOrWhiteSpace(HostingEnviroment) ? HostingEnviroment ?? "Development" : "Development", Settings = new SfConfig { ClientId = ClientId, ClientSecret = ClientSecret, LoginUrl = !string.IsNullOrWhiteSpace(LoginUrl) ? LoginUrl : "https://login.salesforce.com" }, - SectionName = string.IsNullOrWhiteSpace(SectionName) ? "Salesforce" : SectionName, + SectionName = string.IsNullOrWhiteSpace(SectionName) ? "Salesforce" : SectionName ?? "Salesforce", }; try diff --git a/src/CometD.NetCore.Salesforce/README.md b/src/CometD.NetCore.Salesforce/README.md new file mode 100644 index 0000000..4462597 --- /dev/null +++ b/src/CometD.NetCore.Salesforce/README.md @@ -0,0 +1,18 @@ +# CometD.NetCore.Salesforce + +[![Build status](https://ci.appveyor.com/api/projects/status/baalfhs6vvc38icc?svg=true)](https://ci.appveyor.com/project/kdcllc/cometd-netcore-salesforce) +[![NuGet](https://img.shields.io/nuget/v/CometD.NetCore.Salesforce.svg)](https://www.nuget.org/packages?q=Bet.AspNetCore) +[![MyGet](https://img.shields.io/myget/kdcllc/v/CometD.NetCore.Salesforce.svg?label=myget)](https://www.myget.org/F/kdcllc/api/v2) + +Add the following to the project + +```csharp + dotnet add package CometD.NetCore.Salesforce +``` + +This library includes: + +- `ResilientStreamingClient` class to create an event bus for Salesforce +- `IResilientForceClient` wrapper class with Resilience for `NetCoreForce.Client` + +Complete Sample App using this library can be found at [Bet.BuildingBlocks.SalesforceEventBus](https://github.com/kdcllc/Bet.BuildingBlocks.SalesforceEventBus)