From 46baec1e58eab37a86acfeddda3fd247d67cbaa7 Mon Sep 17 00:00:00 2001 From: JackMBurch Date: Tue, 12 Dec 2023 17:40:35 -0500 Subject: [PATCH 01/18] gitignore update --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 934db37..dd9e5b4 100644 --- a/.gitignore +++ b/.gitignore @@ -261,3 +261,8 @@ _ReSharper*/ /src/B2BApi.Bom.Api/B2BApi.Bom.Api.V3.xml +apiclient.config +launchsettings.json + +*.nuspec +*.nupkg From a69131e55b6e26ca926e10aa6e79c5d03c4274de Mon Sep 17 00:00:00 2001 From: JackMBurch Date: Thu, 14 Dec 2023 19:43:39 -0500 Subject: [PATCH 02/18] .NET 8.0 --- .gitignore | 2 +- .../2Legged_OAuth2Service.ConsoleApp.csproj | 2 +- 2Legged_OAuth2Service.ConsoleApp/Program.cs | 11 ++--- .../3Legged_OAuth2Service.ConsoleApp.csproj | 12 +++--- 3Legged_OAuth2Service.ConsoleApp/Program.cs | 28 ++++++------- .../ApiClient.ConsoleApp.csproj | 2 +- ApiClient.ConsoleApp/Program.cs | 7 ++-- ApiClient.sln | 2 +- ApiClient/ApiClient.csproj | 12 +++--- ApiClient/ApiClientService.cs | 40 ++++++------------- ApiClient/Constants/DigiKeyUriConstants.cs | 12 +++--- .../Configuration/ApiClientConfigHelper.cs | 35 ++++++++-------- .../Core/Configuration/ConfigurationHelper.cs | 38 ++++-------------- ApiClient/Exception/ApiException.cs | 6 +-- ApiClient/Models/ApiClientSettings.cs | 14 +++---- ApiClient/Models/KeywordSearchRequest.cs | 2 +- ApiClient/Models/OAuth2Error.cs | 24 +++++------ ApiClient/OAuth2/Models/OAuth2AccessToken.cs | 16 ++++---- ApiClient/OAuth2/OAuth2Helpers.cs | 14 +++---- ApiClient/OAuth2/OAuth2Service.cs | 34 ++++++++-------- apiclient.config => sample-apiclient.config | 0 21 files changed, 131 insertions(+), 182 deletions(-) rename apiclient.config => sample-apiclient.config (100%) diff --git a/.gitignore b/.gitignore index dd9e5b4..499e2e1 100644 --- a/.gitignore +++ b/.gitignore @@ -261,7 +261,7 @@ _ReSharper*/ /src/B2BApi.Bom.Api/B2BApi.Bom.Api.V3.xml -apiclient.config +**/apiclient.config launchsettings.json *.nuspec diff --git a/2Legged_OAuth2Service.ConsoleApp/2Legged_OAuth2Service.ConsoleApp.csproj b/2Legged_OAuth2Service.ConsoleApp/2Legged_OAuth2Service.ConsoleApp.csproj index 5e1e575..54830af 100644 --- a/2Legged_OAuth2Service.ConsoleApp/2Legged_OAuth2Service.ConsoleApp.csproj +++ b/2Legged_OAuth2Service.ConsoleApp/2Legged_OAuth2Service.ConsoleApp.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 _2Legged_OAuth2Service.ConsoleApp enable enable diff --git a/2Legged_OAuth2Service.ConsoleApp/Program.cs b/2Legged_OAuth2Service.ConsoleApp/Program.cs index b0c806a..e3d51bb 100644 --- a/2Legged_OAuth2Service.ConsoleApp/Program.cs +++ b/2Legged_OAuth2Service.ConsoleApp/Program.cs @@ -1,9 +1,4 @@ -using System.Diagnostics; -using System.Net; -using System.Web; -using ApiClient.Extensions; -using ApiClient.Models; -using ApiClient.OAuth2.Models; +using ApiClient.Models; namespace _2Legged_OAuth2Service.ConsoleApp { @@ -37,7 +32,9 @@ private async void Authorize() var result = await oAuth2Service.Get2LeggedAccessToken(); // Check if you got an error during finishing the OAuth2 authorization - if (result.IsError) + if (result == null) + throw new Exception("Authorize result null"); + else if (result.IsError) { Console.WriteLine("\n\nError : {0}", result.Error); Console.WriteLine("\n\nError.Description: {0}", result.ErrorDescription); diff --git a/3Legged_OAuth2Service.ConsoleApp/3Legged_OAuth2Service.ConsoleApp.csproj b/3Legged_OAuth2Service.ConsoleApp/3Legged_OAuth2Service.ConsoleApp.csproj index e4c8b96..a630f9d 100644 --- a/3Legged_OAuth2Service.ConsoleApp/3Legged_OAuth2Service.ConsoleApp.csproj +++ b/3Legged_OAuth2Service.ConsoleApp/3Legged_OAuth2Service.ConsoleApp.csproj @@ -2,11 +2,17 @@ Exe - net6.0 + net8.0 enable enable + + + + + + @@ -15,10 +21,6 @@ - - - - diff --git a/3Legged_OAuth2Service.ConsoleApp/Program.cs b/3Legged_OAuth2Service.ConsoleApp/Program.cs index 848f7e2..1bab68e 100644 --- a/3Legged_OAuth2Service.ConsoleApp/Program.cs +++ b/3Legged_OAuth2Service.ConsoleApp/Program.cs @@ -11,13 +11,11 @@ // //----------------------------------------------------------------------- -using System; +using ApiClient.Extensions; +using ApiClient.Models; using System.Diagnostics; using System.Net; -using System.Runtime.InteropServices; using System.Web; -using ApiClient.Extensions; -using ApiClient.Models; namespace OAuth2Service.ConsoleApp { @@ -59,13 +57,13 @@ private async void Authorize() var authUrl = oAuth2Service.GenerateAuthUrl(scopes); authUrl = authUrl.Replace("&", "^&"); var psi = new ProcessStartInfo - { - FileName = "cmd", - WindowStyle = ProcessWindowStyle.Hidden, - UseShellExecute = false, - CreateNoWindow = true, - Arguments = $"/c start {authUrl}" - }; + { + FileName = "cmd", + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = false, + CreateNoWindow = true, + Arguments = $"/c start {authUrl}" + }; // create Authorize url and send call it thru Process.Start Process.Start(psi); @@ -77,17 +75,15 @@ private async void Authorize() httpListener.Stop(); // exact the query parameters from the returned URL - var queryString = context.Request.Url.Query; - var queryColl = HttpUtility.ParseQueryString(queryString); + var queryString = context.Request.Url?.Query; + var queryColl = HttpUtility.ParseQueryString(queryString!); // Grab the needed query parameter code from the query collection var code = queryColl["code"]; Console.WriteLine($"Using code {code}"); // Pass the returned code value to finish the OAuth2 authorization - var result = await oAuth2Service.FinishAuthorization(code); - - // Check if you got an error during finishing the OAuth2 authorization + var result = await oAuth2Service.FinishAuthorization(code!) ?? throw new Exception("Authorize result null"); if (result.IsError) { Console.WriteLine("\n\nError : {0}", result.Error); diff --git a/ApiClient.ConsoleApp/ApiClient.ConsoleApp.csproj b/ApiClient.ConsoleApp/ApiClient.ConsoleApp.csproj index 1cb7a51..a28c5da 100644 --- a/ApiClient.ConsoleApp/ApiClient.ConsoleApp.csproj +++ b/ApiClient.ConsoleApp/ApiClient.ConsoleApp.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 enable enable diff --git a/ApiClient.ConsoleApp/Program.cs b/ApiClient.ConsoleApp/Program.cs index 84ce89b..00bed9f 100644 --- a/ApiClient.ConsoleApp/Program.cs +++ b/ApiClient.ConsoleApp/Program.cs @@ -13,7 +13,6 @@ using ApiClient.Models; using ApiClient.OAuth2; -using ApiClient.OAuth2.Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -23,16 +22,16 @@ public class Program { static async Task Main() { - var prog = new Program(); + _ = new Program(); - await prog.CallKeywordSearch(); + await CallKeywordSearch(); // This will keep the console window up until a key is pressed in the console window. Console.WriteLine("\n\nPress any key to exit..."); Console.ReadKey(); } - private async Task CallKeywordSearch() + private static async Task CallKeywordSearch() { var settings = ApiClientSettings.CreateFromConfigFile(); Console.WriteLine(settings.ToString()); diff --git a/ApiClient.sln b/ApiClient.sln index 72cae0c..3464a28 100644 --- a/ApiClient.sln +++ b/ApiClient.sln @@ -13,8 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ApiClient", "ApiClient\ApiC EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{560625EF-E3D3-4CC0-85D6-94E70A9131A5}" ProjectSection(SolutionItems) = preProject - apiclient.config = apiclient.config Readme.md = Readme.md + sample-apiclient.config = sample-apiclient.config EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "2Legged_OAuth2Service.ConsoleApp", "2Legged_OAuth2Service.ConsoleApp\2Legged_OAuth2Service.ConsoleApp.csproj", "{B43EB828-423A-4A23-8768-5C35AD5F047B}" diff --git a/ApiClient/ApiClient.csproj b/ApiClient/ApiClient.csproj index 9236d1d..3287a5e 100644 --- a/ApiClient/ApiClient.csproj +++ b/ApiClient/ApiClient.csproj @@ -1,11 +1,17 @@  - net6.0 + net8.0 enable enable + + + + + + @@ -14,8 +20,4 @@ - - - - \ No newline at end of file diff --git a/ApiClient/ApiClientService.cs b/ApiClient/ApiClientService.cs index 7870301..4698f24 100644 --- a/ApiClient/ApiClientService.cs +++ b/ApiClient/ApiClientService.cs @@ -11,15 +11,13 @@ // //----------------------------------------------------------------------- -using System.Net; -using System.Net.Http.Headers; -using System.Net.Http.Json; using ApiClient.Constants; using ApiClient.Exception; using ApiClient.Models; using ApiClient.OAuth2; -using ApiClient.OAuth2.Models; -using Microsoft.Extensions.Logging; +using System.Net; +using System.Net.Http.Headers; +using System.Net.Http.Json; namespace ApiClient { @@ -27,9 +25,9 @@ public class ApiClientService { private const string CustomHeader = "Api-StaleTokenRetry"; - private ApiClientSettings? _clientSettings; + private ApiClientSettings _clientSettings; - public ApiClientSettings? ClientSettings + public ApiClientSettings ClientSettings { get => _clientSettings; set => _clientSettings = value; @@ -40,23 +38,14 @@ public ApiClientSettings? ClientSettings /// public HttpClient HttpClient { get; private set; } - public ApiClientService(ApiClientSettings? clientSettings) + public ApiClientService(ApiClientSettings clientSettings) { - ClientSettings = clientSettings ?? throw new ArgumentNullException(nameof(clientSettings)); - Initialize(); - } - - private void Initialize() - { - HttpClient = new HttpClient(); + _clientSettings = clientSettings ?? throw new ArgumentNullException(nameof(clientSettings)); ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; - - var authenticationHeaderValue = new AuthenticationHeaderValue("Bearer", ClientSettings.AccessToken); - HttpClient.DefaultRequestHeaders.Authorization = authenticationHeaderValue; - + HttpClient = new() { BaseAddress = DigiKeyUriConstants.BaseAddress }; + HttpClient.DefaultRequestHeaders.Authorization = new("Bearer", ClientSettings.AccessToken); HttpClient.DefaultRequestHeaders.Add("X-Digikey-Client-Id", ClientSettings.ClientId); - HttpClient.BaseAddress = DigiKeyUriConstants.BaseAddress; HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); } @@ -106,7 +95,7 @@ public async Task PostAsJsonAsync(string resourcePath, T Console.WriteLine(">ApiClientService::PostAsJsonAsync()"); try { - var response = await HttpClient.PostAsJsonAsync(resourcePath, postRequest); + HttpResponseMessage response = await HttpClient.PostAsJsonAsync(resourcePath, postRequest); Console.WriteLine(" PostAsJsonAsync(string resourcePath, T Console.WriteLine($"New Access token is {_clientSettings.AccessToken}"); //Only retry the first time. - if (!response.RequestMessage.Headers.Contains(CustomHeader)) + if (!response.RequestMessage!.Headers.Contains(CustomHeader)) { HttpClient.DefaultRequestHeaders.Add(CustomHeader, CustomHeader); HttpClient.DefaultRequestHeaders.Authorization = @@ -150,7 +139,7 @@ public async Task PostAsJsonAsync(string resourcePath, T } } - protected async Task GetServiceResponse(HttpResponseMessage response) + protected static async Task GetServiceResponse(HttpResponseMessage response) { Console.WriteLine(">ApiClientService::GetServiceResponse()"); var postResponse = string.Empty; @@ -169,11 +158,6 @@ protected async Task GetServiceResponse(HttpResponseMessage response) Console.WriteLine(" Status Code : {0}", response.StatusCode); Console.WriteLine(" Content : {0}", errorMessage); Console.WriteLine(" Reason : {0}", response.ReasonPhrase); - var resp = new HttpResponseMessage(response.StatusCode) - { - Content = response.Content, - ReasonPhrase = response.ReasonPhrase - }; throw new System.Exception(response.ReasonPhrase); } diff --git a/ApiClient/Constants/DigiKeyUriConstants.cs b/ApiClient/Constants/DigiKeyUriConstants.cs index 6f161ef..55ca246 100644 --- a/ApiClient/Constants/DigiKeyUriConstants.cs +++ b/ApiClient/Constants/DigiKeyUriConstants.cs @@ -11,8 +11,6 @@ // //----------------------------------------------------------------------- -using System; - namespace ApiClient.Constants { /// @@ -24,15 +22,15 @@ public static class DigiKeyUriConstants //public static readonly Uri BaseAddress = new Uri("https://sandbox-api.digikey.com"); //public static readonly Uri TokenEndpoint = new Uri("https://sandbox-api.digikey.com/v1/oauth2/token"); //public static readonly Uri AuthorizationEndpoint = new Uri("https://sandbox-api.digikey.com/v1/oauth2/authorize"); - + // Integration instance // public static readonly Uri BaseAddress = new Uri("https://apiint.digikey.com"); // public static readonly Uri TokenEndpoint = new Uri("https://apiint.digikey.com/v1/oauth2/token"); // public static readonly Uri AuthorizationEndpoint = new Uri("https://apiint.digikey.com/v1/oauth2/authorize"); - + // Production instance - public static readonly Uri BaseAddress = new Uri("https://api.digikey.com"); - public static readonly Uri TokenEndpoint = new Uri("https://api.digikey.com/v1/oauth2/token"); - public static readonly Uri AuthorizationEndpoint = new Uri("https://api.digikey.com/v1/oauth2/authorize"); + public static readonly Uri BaseAddress = new("https://api.digikey.com"); + public static readonly Uri TokenEndpoint = new("https://api.digikey.com/v1/oauth2/token"); + public static readonly Uri AuthorizationEndpoint = new("https://api.digikey.com/v1/oauth2/authorize"); } } diff --git a/ApiClient/Core/Configuration/ApiClientConfigHelper.cs b/ApiClient/Core/Configuration/ApiClientConfigHelper.cs index ec41961..7300918 100644 --- a/ApiClient/Core/Configuration/ApiClientConfigHelper.cs +++ b/ApiClient/Core/Configuration/ApiClientConfigHelper.cs @@ -11,14 +11,11 @@ // //----------------------------------------------------------------------- -using System; +using ApiClient.Core.Configuration.Interfaces; +using ApiClient.Exception; using System.Configuration; using System.Globalization; -using System.IO; using System.Text.RegularExpressions; -using ApiClient.Core.Configuration.Interfaces; -using ApiClient.Exception; -using Microsoft.Extensions.Logging; namespace ApiClient.Core.Configuration { @@ -27,7 +24,7 @@ public class ApiClientConfigHelper : ConfigurationHelper, IApiClientConfigHelper // Static members are 'eagerly initialized', that is, // immediately when class is loaded for the first time. // .NET guarantees thread safety for static initialization - private static readonly ApiClientConfigHelper _thisInstance = new ApiClientConfigHelper(); + private static readonly ApiClientConfigHelper _thisInstance = new(); private const string _ClientId = "ApiClient.ClientId"; private const string _ClientSecret = "ApiClient.ClientSecret"; @@ -36,7 +33,7 @@ public class ApiClientConfigHelper : ConfigurationHelper, IApiClientConfigHelper private const string _RefreshToken = "ApiClient.RefreshToken"; private const string _ExpirationDateTime = "ApiClient.ExpirationDateTime"; - private ApiClientConfigHelper() + private ApiClientConfigHelper() { try { @@ -48,10 +45,10 @@ private ApiClientConfigHelper() // This little hack is ugly but needed to work with Console apps and Asp.Net apps. var solutionDir = Regex.IsMatch(baseDir, regexPattern) - ? Directory.GetParent(baseDir).Parent.Parent.Parent // Console Apps + ? Directory.GetParent(baseDir)?.Parent?.Parent?.Parent // Console Apps : Directory.GetParent(baseDir); // Asp.Net apps - if (!File.Exists(Path.Combine(solutionDir.FullName, "apiclient.config"))) + if (!File.Exists(Path.Combine(solutionDir!.FullName, "apiclient.config"))) { throw new ApiException($"Unable to locate apiclient.config in solution folder {solutionDir.FullName}"); } @@ -79,8 +76,8 @@ public static ApiClientConfigHelper Instance() /// public string ClientId { - get { return GetAttribute(_ClientId); } - set { Update(_ClientId, value); } + get => GetAttribute(_ClientId); + set => Update(_ClientId, value); } /// @@ -88,8 +85,8 @@ public string ClientId /// public string ClientSecret { - get { return GetAttribute(_ClientSecret); } - set { Update(_ClientSecret, value); } + get => GetAttribute(_ClientSecret); + set => Update(_ClientSecret, value); } /// @@ -97,8 +94,8 @@ public string ClientSecret /// public string RedirectUri { - get { return GetAttribute(_RedirectUri); } - set { Update(_RedirectUri, value); } + get => GetAttribute(_RedirectUri); + set => Update(_RedirectUri, value); } /// @@ -106,8 +103,8 @@ public string RedirectUri /// public string AccessToken { - get { return GetAttribute(_AccessToken); } - set { Update(_AccessToken, value); } + get => GetAttribute(_AccessToken); + set => Update(_AccessToken, value); } /// @@ -115,8 +112,8 @@ public string AccessToken /// public string RefreshToken { - get { return GetAttribute(_RefreshToken); } - set { Update(_RefreshToken, value); } + get => GetAttribute(_RefreshToken); + set => Update(_RefreshToken, value); } /// diff --git a/ApiClient/Core/Configuration/ConfigurationHelper.cs b/ApiClient/Core/Configuration/ConfigurationHelper.cs index 09273f3..5dfc2d9 100644 --- a/ApiClient/Core/Configuration/ConfigurationHelper.cs +++ b/ApiClient/Core/Configuration/ConfigurationHelper.cs @@ -11,14 +11,9 @@ // //----------------------------------------------------------------------- -using System; +using ApiClient.Core.Configuration.Interfaces; using System.Configuration; using System.Diagnostics.CodeAnalysis; -using ApiClient.Core.Configuration.Interfaces; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using NLog; -using NLog.Internal; using ConfigurationManager = System.Configuration.ConfigurationManager; namespace ApiClient.Core.Configuration @@ -32,7 +27,7 @@ public class ConfigurationHelper : IConfigurationHelper /// /// This object represents the config file /// - protected System.Configuration.Configuration _config; + protected System.Configuration.Configuration? _config; /// /// Updates the value for the specified key in the AppSettings of the Config file. @@ -41,14 +36,10 @@ public class ConfigurationHelper : IConfigurationHelper /// The value. public void Update(string key, string value) { - if (_config.AppSettings.Settings[key] == null) - { - _config.AppSettings.Settings.Add(key, value); - } + if (_config?.AppSettings.Settings[key] == null) + _config?.AppSettings.Settings.Add(key, value); else - { _config.AppSettings.Settings[key].Value = value; - } } /// @@ -60,13 +51,11 @@ public string GetAttribute(string attrName) { try { - return _config.AppSettings.Settings[attrName] == null - ? null - : _config.AppSettings.Settings[attrName].Value; + return _config?.AppSettings.Settings[attrName]?.Value!; } catch (System.Exception) { - return null; + return null!; } } @@ -93,19 +82,8 @@ public bool GetBooleanAttribute(string attrName) /// public void Save() { - try - { - _config.Save(ConfigurationSaveMode.Full); - ConfigurationManager.RefreshSection("appSettings"); - } - catch (ConfigurationErrorsException cee) - { - throw; - } - catch (System.Exception ex) - { - throw; - } + _config?.Save(ConfigurationSaveMode.Full); + ConfigurationManager.RefreshSection("appSettings"); } /// diff --git a/ApiClient/Exception/ApiException.cs b/ApiClient/Exception/ApiException.cs index 18560c3..9d89eab 100644 --- a/ApiClient/Exception/ApiException.cs +++ b/ApiClient/Exception/ApiException.cs @@ -17,11 +17,7 @@ namespace ApiClient.Exception /// Base Api Exception /// /// - public class ApiException : System.Exception + public class ApiException(string message, System.Exception? innerEx = null) : System.Exception(message, innerEx) { - public ApiException(string message, System.Exception innerEx = null) : - base(message, innerEx) - { - } } } diff --git a/ApiClient/Models/ApiClientSettings.cs b/ApiClient/Models/ApiClientSettings.cs index 74af47b..0e5e42e 100644 --- a/ApiClient/Models/ApiClientSettings.cs +++ b/ApiClient/Models/ApiClientSettings.cs @@ -11,10 +11,10 @@ // //----------------------------------------------------------------------- -using System.Diagnostics.CodeAnalysis; -using System.Text; using ApiClient.Core.Configuration; using ApiClient.OAuth2.Models; +using System.Diagnostics.CodeAnalysis; +using System.Text; namespace ApiClient.Models { @@ -24,12 +24,12 @@ public class ApiClientSettings public string ClientSecret { get; set; } = string.Empty; public string RedirectUri { get; set; } = string.Empty; public string AccessToken { get; set; } = string.Empty; - public String RefreshToken { get; set; } = string.Empty; + public string RefreshToken { get; set; } = string.Empty; public DateTime ExpirationDateTime { get; set; } public void Save() { - ApiClientConfigHelper.Instance().ClientId = ClientId; + ApiClientConfigHelper.Instance().ClientId = ClientId ?? string.Empty; ApiClientConfigHelper.Instance().ClientSecret = ClientSecret; ApiClientConfigHelper.Instance().RedirectUri = RedirectUri; ApiClientConfigHelper.Instance().AccessToken = AccessToken; @@ -53,9 +53,9 @@ public static ApiClientSettings CreateFromConfigFile() public void UpdateAndSave(OAuth2AccessToken? oAuth2AccessToken) { - AccessToken = oAuth2AccessToken.AccessToken; - RefreshToken = oAuth2AccessToken.RefreshToken; - ExpirationDateTime = DateTime.Now.AddSeconds(oAuth2AccessToken.ExpiresIn); + AccessToken = oAuth2AccessToken?.AccessToken ?? string.Empty; + RefreshToken = oAuth2AccessToken?.RefreshToken ?? string.Empty; + ExpirationDateTime = DateTime.Now.AddSeconds(oAuth2AccessToken?.ExpiresIn ?? 0d); Save(); } [ExcludeFromCodeCoverage] diff --git a/ApiClient/Models/KeywordSearchRequest.cs b/ApiClient/Models/KeywordSearchRequest.cs index c706d29..d42706b 100644 --- a/ApiClient/Models/KeywordSearchRequest.cs +++ b/ApiClient/Models/KeywordSearchRequest.cs @@ -24,7 +24,7 @@ public class KeywordSearchRequest /// /// The keywords. /// - public string Keywords { get; set; } + public string Keywords { get; set; } = string.Empty; /// /// Gets or sets the record count. diff --git a/ApiClient/Models/OAuth2Error.cs b/ApiClient/Models/OAuth2Error.cs index 5e5e245..886836f 100644 --- a/ApiClient/Models/OAuth2Error.cs +++ b/ApiClient/Models/OAuth2Error.cs @@ -17,22 +17,22 @@ namespace ApiClient.Models { public class OAuth2Error { - [JsonProperty("ErrorResponseVersion")] - public string ErrorResponseVersion { get; set; } + [JsonProperty(nameof(ErrorResponseVersion))] + public string ErrorResponseVersion { get; set; } = string.Empty; - [JsonProperty("StatusCode")] - public int StatusCode { get; set; } + [JsonProperty(nameof(StatusCode))] + public int StatusCode { get; set; } - [JsonProperty("ErrorMessage")] - public string ErrorMessage { get; set; } + [JsonProperty(nameof(ErrorMessage))] + public string ErrorMessage { get; set; } = string.Empty; - [JsonProperty("ErrorDetails")] - public string ErrorDetails { get; set; } + [JsonProperty(nameof(ErrorDetails))] + public string ErrorDetails { get; set; } = string.Empty; - [JsonProperty("RequestId")] - public string RequestId { get; set; } + [JsonProperty(nameof(RequestId))] + public string RequestId { get; set; } = string.Empty; - [JsonProperty("ValidationErrors")] - public List ValidationErrors { get; set; } + [JsonProperty(nameof(ValidationErrors))] + public List ValidationErrors { get; set; } = []; } } diff --git a/ApiClient/OAuth2/Models/OAuth2AccessToken.cs b/ApiClient/OAuth2/Models/OAuth2AccessToken.cs index 0e1692a..9ba18cb 100644 --- a/ApiClient/OAuth2/Models/OAuth2AccessToken.cs +++ b/ApiClient/OAuth2/Models/OAuth2AccessToken.cs @@ -11,10 +11,10 @@ // //----------------------------------------------------------------------- -using System.Diagnostics.CodeAnalysis; -using System.Text; using ApiClient.Extensions; using Newtonsoft.Json; +using System.Diagnostics.CodeAnalysis; +using System.Text; namespace ApiClient.OAuth2.Models { @@ -22,29 +22,29 @@ public class OAuth2AccessToken { /// Gets or sets the access token. [JsonProperty(PropertyName = "access_token")] - public string AccessToken { get; set; } + public string AccessToken { get; set; } = string.Empty; /// Gets or sets the error. [JsonProperty(PropertyName = "error")] - public string Error { get; set; } + public string Error { get; set; } = string.Empty; public bool IsError => Error.IsPresent(); /// Gets or sets the error description. [JsonProperty(PropertyName = "error_description")] - public string ErrorDescription { get; set; } + public string ErrorDescription { get; set; } = string.Empty; /// Gets or sets the id token. [JsonProperty(PropertyName = "id_token")] - public string IdToken { get; set; } + public string IdToken { get; set; } = string.Empty; /// Gets or sets the refresh token. [JsonProperty(PropertyName = "refresh_token")] - public string RefreshToken { get; set; } + public string RefreshToken { get; set; } = string.Empty; /// Gets or sets the token type. [JsonProperty(PropertyName = "token_type")] - public string TokenType { get; set; } + public string TokenType { get; set; } = string.Empty; /// Gets or sets the expiration in seconds from now. [JsonProperty(PropertyName = "expires_in")] diff --git a/ApiClient/OAuth2/OAuth2Helpers.cs b/ApiClient/OAuth2/OAuth2Helpers.cs index f32a0dc..f9e705c 100644 --- a/ApiClient/OAuth2/OAuth2Helpers.cs +++ b/ApiClient/OAuth2/OAuth2Helpers.cs @@ -11,13 +11,13 @@ // //----------------------------------------------------------------------- -using System.Net; -using System.Text; using ApiClient.Constants; using ApiClient.Exception; using ApiClient.Models; using ApiClient.OAuth2.Models; using Newtonsoft.Json; +using System.Net; +using System.Text; namespace ApiClient.OAuth2 { @@ -55,7 +55,7 @@ public static string Base64Encode(string plainText) /// /// ApiClientSettings needed for creating a proper refresh token HTTP post call. /// Returns OAuth2AccessToken - public static async Task RefreshTokenAsync(ApiClientSettings? clientSettings) + public static async Task RefreshTokenAsync(ApiClientSettings? clientSettings) { ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; @@ -67,11 +67,11 @@ public static string Base64Encode(string plainText) new KeyValuePair( OAuth2Constants.GrantType, OAuth2Constants.GrantTypes.RefreshToken), - new KeyValuePair(OAuth2Constants.ClientId, clientSettings!.ClientId), - new KeyValuePair(OAuth2Constants.ClientSecret, clientSettings.ClientSecret), + new KeyValuePair(OAuth2Constants.ClientId, clientSettings?.ClientId!), + new KeyValuePair(OAuth2Constants.ClientSecret, clientSettings?.ClientSecret!), new KeyValuePair( OAuth2Constants.GrantTypes.RefreshToken, - clientSettings.RefreshToken), + clientSettings?.RefreshToken!), }); var httpClient = new HttpClient(); @@ -93,7 +93,7 @@ public static string Base64Encode(string plainText) } else { - clientSettings.UpdateAndSave(oAuth2AccessTokenResponse); + clientSettings?.UpdateAndSave(oAuth2AccessTokenResponse); } return oAuth2AccessTokenResponse; diff --git a/ApiClient/OAuth2/OAuth2Service.cs b/ApiClient/OAuth2/OAuth2Service.cs index 19de821..9ab037f 100644 --- a/ApiClient/OAuth2/OAuth2Service.cs +++ b/ApiClient/OAuth2/OAuth2Service.cs @@ -11,12 +11,12 @@ // //----------------------------------------------------------------------- -using System.Net; -using System.Net.Http.Headers; using ApiClient.Constants; using ApiClient.Models; using ApiClient.OAuth2.Models; using Newtonsoft.Json; +using System.Net; +using System.Net.Http.Headers; namespace ApiClient.OAuth2 { @@ -45,13 +45,13 @@ public OAuth2Service(ApiClientSettings? clientSettings) /// This is current not used and should be "". /// This is not currently used. /// String which is the oauth2 authorization url. - public string GenerateAuthUrl(string scopes = "", string state = null) + public string GenerateAuthUrl(string scopes = "", string? state = null) { var url = string.Format("{0}?client_id={1}&scope={2}&redirect_uri={3}&response_type={4}", DigiKeyUriConstants.AuthorizationEndpoint, - ClientSettings.ClientId, + ClientSettings?.ClientId, scopes, - ClientSettings.RedirectUri, + ClientSettings?.RedirectUri, OAuth2Constants.ResponseTypes.CodeResponse); if (!string.IsNullOrWhiteSpace(state)) @@ -76,22 +76,22 @@ public string GenerateAuthUrl(string scopes = "", string state = null) // Build up the body for the token request var body = new List> { - new KeyValuePair(OAuth2Constants.Code, code), - new KeyValuePair(OAuth2Constants.RedirectUri, ClientSettings.RedirectUri), - new KeyValuePair(OAuth2Constants.ClientId, ClientSettings.ClientId), - new KeyValuePair(OAuth2Constants.ClientSecret, ClientSettings.ClientSecret), - new KeyValuePair(OAuth2Constants.GrantType, + new(OAuth2Constants.Code, code), + new(OAuth2Constants.RedirectUri, ClientSettings?.RedirectUri!), + new(OAuth2Constants.ClientId, ClientSettings?.ClientId!), + new(OAuth2Constants.ClientSecret, ClientSettings?.ClientSecret!), + new(OAuth2Constants.GrantType, OAuth2Constants.GrantTypes.AuthorizationCode) }; // Request the token var requestMessage = new HttpRequestMessage(HttpMethod.Post, DigiKeyUriConstants.TokenEndpoint); - var httpClient = new HttpClient {BaseAddress = DigiKeyUriConstants.BaseAddress}; + var httpClient = new HttpClient { BaseAddress = DigiKeyUriConstants.BaseAddress }; requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); requestMessage.Content = new FormUrlEncodedContent(body); - Console.WriteLine("HttpRequestMessage {0}", requestMessage.RequestUri.AbsoluteUri); + Console.WriteLine("HttpRequestMessage {0}", requestMessage.RequestUri?.AbsoluteUri); var tokenResponse = await httpClient.SendAsync(requestMessage).ConfigureAwait(false); var text = await tokenResponse.Content.ReadAsStringAsync(); @@ -120,7 +120,7 @@ public string GenerateAuthUrl(string scopes = "", string state = null) /// Refreshes the token asynchronous. /// /// Returns OAuth2AccessToken - public async Task RefreshTokenAsync() + public async Task RefreshTokenAsync() { return await OAuth2Helpers.RefreshTokenAsync(ClientSettings); } @@ -138,9 +138,9 @@ public string GenerateAuthUrl(string scopes = "", string state = null) // Build up the body for the token request var body = new List> { - new KeyValuePair(OAuth2Constants.ClientId, ClientSettings.ClientId), - new KeyValuePair(OAuth2Constants.ClientSecret, ClientSettings.ClientSecret), - new KeyValuePair(OAuth2Constants.GrantType, OAuth2Constants.GrantTypes.ClientCredentials) + new(OAuth2Constants.ClientId, ClientSettings?.ClientId!), + new(OAuth2Constants.ClientSecret, ClientSettings?.ClientSecret!), + new(OAuth2Constants.GrantType, OAuth2Constants.GrantTypes.ClientCredentials) }; // Request the token @@ -150,7 +150,7 @@ public string GenerateAuthUrl(string scopes = "", string state = null) requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); requestMessage.Content = new FormUrlEncodedContent(body); - Console.WriteLine("HttpRequestMessage {0}", requestMessage.RequestUri.AbsoluteUri); + Console.WriteLine("HttpRequestMessage {0}", requestMessage.RequestUri?.AbsoluteUri); var tokenResponse = await httpClient.SendAsync(requestMessage).ConfigureAwait(false); var text = await tokenResponse.Content.ReadAsStringAsync(); diff --git a/apiclient.config b/sample-apiclient.config similarity index 100% rename from apiclient.config rename to sample-apiclient.config From 96fb5a32c048a1a9c17a37ea880387d8a6b30e2e Mon Sep 17 00:00:00 2001 From: JackMBurch Date: Thu, 14 Dec 2023 19:48:24 -0500 Subject: [PATCH 03/18] Update Readme.md --- Readme.md | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Readme.md b/Readme.md index e3735ae..9483b62 100644 --- a/Readme.md +++ b/Readme.md @@ -1,9 +1,16 @@ -# C# Api Client Library with OAuth2 +
+

C# DigiKey API Client Library

+ +![image](https://i.imgur.com/gLgV1tB.png) + +
+ + ### Features * Makes structured calls to the DigiKey API from .NET projects -* Logs in users using the OAuth2 code flow +* Handles the OAuth2 control flow, logs users in, refreshes tokens when needed, etc. ### Basic Usage @@ -22,10 +29,10 @@ Console.WriteLine("response is {0}", postResponse); ### Getting Started -1. Download the zip file containing the solution ApiClient -2. You will need to Register an application in order to create your unique Client ID, Client Secret, and OAuth Redirection URL. Follow the steps available on the API Portal here https://developer.digikey.com/ -3. In the solution folder update apiclient.config with the ClientId, ClientSecret, and RedirectUri values from step 2. -``` +1. Clone the repository or download and extract the zip file containing the ApiClient solution. +2. You will need to register an application on the [DigiKey Developer Portal](https://developer.digikey.com/) in order to create your unique Client ID, Client Secret as well as to set your redirection URI. +3. In the solution folder copy sample-apiclient.config as apiclient.config, and update it with the ClientId, ClientSecret, and RedirectUri values from step 2. +```xml From d4925c914ac06ad987d10f6457b77becae14c47e Mon Sep 17 00:00:00 2001 From: JackMBurch Date: Thu, 14 Dec 2023 23:10:21 -0500 Subject: [PATCH 04/18] Initial .NET 8.0 commit, support for logging, support for storing requests in DB --- 2Legged_OAuth2Service.ConsoleApp/Program.cs | 1 - .../3Legged_OAuth2Service.ConsoleApp.csproj | 7 +- ApiClient.ConsoleApp/Program.cs | 2 +- ApiClient/API/ProductInformation.cs | 422 ++++++++++++++++++ ApiClient/ApiClient.csproj | 9 +- ApiClient/ApiClientService.cs | 139 +++--- ApiClient/ConsoleLogger.cs | 24 + .../Configuration/ApiClientConfigHelper.cs | 40 +- ApiClient/GlobalSuppressions.cs | 8 + ApiClient/Models/RequestSnapshot.cs | 27 ++ ApiClient/OAuth2/OAuth2Helpers.cs | 2 - ApiClient/OAuth2/OAuth2Service.cs | 9 +- 12 files changed, 599 insertions(+), 91 deletions(-) create mode 100644 ApiClient/API/ProductInformation.cs create mode 100644 ApiClient/ConsoleLogger.cs create mode 100644 ApiClient/GlobalSuppressions.cs create mode 100644 ApiClient/Models/RequestSnapshot.cs diff --git a/2Legged_OAuth2Service.ConsoleApp/Program.cs b/2Legged_OAuth2Service.ConsoleApp/Program.cs index e3d51bb..c308fce 100644 --- a/2Legged_OAuth2Service.ConsoleApp/Program.cs +++ b/2Legged_OAuth2Service.ConsoleApp/Program.cs @@ -15,7 +15,6 @@ static void Main() // This will keep the console window up until a key is press in the console window. Console.WriteLine("\n\nPress any key to exit..."); - Console.ReadKey(); } /// diff --git a/3Legged_OAuth2Service.ConsoleApp/3Legged_OAuth2Service.ConsoleApp.csproj b/3Legged_OAuth2Service.ConsoleApp/3Legged_OAuth2Service.ConsoleApp.csproj index a630f9d..b219396 100644 --- a/3Legged_OAuth2Service.ConsoleApp/3Legged_OAuth2Service.ConsoleApp.csproj +++ b/3Legged_OAuth2Service.ConsoleApp/3Legged_OAuth2Service.ConsoleApp.csproj @@ -14,11 +14,10 @@ - - + + - - + diff --git a/ApiClient.ConsoleApp/Program.cs b/ApiClient.ConsoleApp/Program.cs index 00bed9f..ce1bac0 100644 --- a/ApiClient.ConsoleApp/Program.cs +++ b/ApiClient.ConsoleApp/Program.cs @@ -56,7 +56,7 @@ private static async Task CallKeywordSearch() } var client = new ApiClientService(settings); - var response = await client.KeywordSearch("P5555-ND"); + var response = await client.ProductInformation.KeywordSearch("P5555-ND"); // In order to pretty print the json object we need to do the following var jsonFormatted = JToken.Parse(response).ToString(Formatting.Indented); diff --git a/ApiClient/API/ProductInformation.cs b/ApiClient/API/ProductInformation.cs new file mode 100644 index 0000000..7364582 --- /dev/null +++ b/ApiClient/API/ProductInformation.cs @@ -0,0 +1,422 @@ +using ApiClient.Models; +using System.Web; + +namespace ApiClient.API +{ + public partial class ProductInformation(ApiClientService clientService) + { + private readonly ApiClientService _clientService = clientService; + + #region PartSearch + + public async Task KeywordSearch(string keyword, string[]? includes = null, DateTime? afterDate = null) + { + afterDate ??= _clientService.AfterDate; + string? previous = _clientService.PrevRequest(nameof(KeywordSearch), keyword, (DateTime)afterDate); + if (previous != null) return previous; + + + var resourcePath = "Search/v3/Products/Keyword"; + + var parameters = string.Empty; + + if (includes != null) + { + var includesString = HttpUtility.UrlEncode(string.Join(",", includes)); + parameters += $"?includes={includesString}"; + } + + var fullPath = $"/{resourcePath}{parameters}"; + + var request = new KeywordSearchRequest + { + Keywords = keyword ?? "P5555-ND", + RecordCount = 25 + }; + + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var postResponse = await _clientService.PostAsJsonAsync(fullPath, request); + var result = _clientService.GetServiceResponse(postResponse).Result; + RequestSnapshot digikeyAPIRequest = new() + { + Route = nameof(KeywordSearch), + RouteParameter = keyword!, + Parameters = parameters, + Response = result + }; + _clientService.SaveRequest.Save(digikeyAPIRequest); + return result; + } + + public async Task ProductDetails(string digikeyPartNumber, string[]? includes = null, DateTime? afterDate = null) + { + afterDate ??= _clientService.AfterDate; + string? previous = _clientService.PrevRequest(nameof(ProductDetails), digikeyPartNumber, (DateTime)afterDate); + if (previous != null) return previous; + + + var resourcePath = "Search/v3/Products"; + + var parameters = string.Empty; + + if (includes != null) + { + var includesString = HttpUtility.UrlEncode(string.Join(",", includes)); + parameters += $"?includes={includesString}"; + } + + + var encodedPN = HttpUtility.UrlEncode(digikeyPartNumber); + + var fullPath = $"/{resourcePath}/{encodedPN}{parameters}"; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var getResponse = await _clientService.GetAsync(fullPath); + + var result = _clientService.GetServiceResponse(getResponse).Result; + + RequestSnapshot digikeyAPIRequest = new() + { + Route = nameof(ProductDetails), + RouteParameter = digikeyPartNumber!, + Parameters = parameters, + Response = result + }; + + _clientService.SaveRequest.Save(digikeyAPIRequest); + + return result; + } + + public async Task DigiReelPricing(string digikeyPartNumber, int requestedQuantity, string[]? includes = null, DateTime? afterDate = null) + { + afterDate ??= _clientService.AfterDate; + string? previous = _clientService.PrevRequest(nameof(DigiReelPricing), digikeyPartNumber, (DateTime)afterDate); + if (previous != null) return previous; + + + var resourcePathPrefix = "Search/v3/Products"; + var resourcePathSuffix = "DigiReelPricing"; + + var encodedPN = HttpUtility.UrlEncode(digikeyPartNumber); + + var parameters = $"requestedQuantity={requestedQuantity}"; + + if (includes != null) + { + var includesString = HttpUtility.UrlEncode(string.Join(",", includes)); + parameters += $"&includes={includesString}"; + } + + var fullPath = $"/{resourcePathPrefix}/{encodedPN}/{resourcePathSuffix}?{parameters}"; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var getResponse = await _clientService.GetAsync(fullPath); + + var result = _clientService.GetServiceResponse(getResponse).Result; + + RequestSnapshot digikeyAPIRequest = new() + { + Route = nameof(DigiReelPricing), + RouteParameter = digikeyPartNumber!, + Parameters = parameters, + Response = result + }; + + _clientService.SaveRequest.Save(digikeyAPIRequest); + + return result; + } + + public async Task SuggestedParts(string partNumber, DateTime? afterDate = null) + { + afterDate ??= _clientService.AfterDate; + string? previous = _clientService.PrevRequest(nameof(DigiReelPricing), partNumber, (DateTime)afterDate); + if (previous != null) return previous; + + + var resourcePathPrefix = "Search/v3/Products"; + var resourcePathSuffix = "WithSuggestedProducts"; + + var encodedPN = HttpUtility.UrlEncode(partNumber); + + var fullPath = $"/{resourcePathPrefix}/{encodedPN}/{resourcePathSuffix}"; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var getResponse = await _clientService.GetAsync(fullPath); + + var result = _clientService.GetServiceResponse(getResponse).Result; + + RequestSnapshot digikeyAPIRequest = new() + { + Route = nameof(SuggestedParts), + RouteParameter = partNumber!, + Response = result + }; + + _clientService.SaveRequest.Save(digikeyAPIRequest); + + return result; + } + + public async Task Manufacturers(DateTime? afterDate = null) + { + afterDate ??= _clientService.AfterDate; + string? previous = _clientService.PrevRequest(nameof(Manufacturers), null!, (DateTime)afterDate); + if (previous != null) return previous; + + + var resourcePath = "Search/v3/Manufacturers"; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var getResponse = await _clientService.GetAsync($"{resourcePath}"); + + var result = _clientService.GetServiceResponse(getResponse).Result; + + RequestSnapshot digikeyAPIRequest = new() + { + Route = nameof(Manufacturers), + Response = result + }; + + _clientService.SaveRequest.Save(digikeyAPIRequest); + + return result; + } + + public async Task Categories(DateTime? afterDate = null) + { + afterDate ??= _clientService.AfterDate; + string? previous = _clientService.PrevRequest(nameof(Categories), null!, (DateTime)afterDate); + if (previous != null) return previous; + + + var resourcePath = "Search/v3/Categories"; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var getResponse = await _clientService.GetAsync($"{resourcePath}"); + + var result = _clientService.GetServiceResponse(getResponse).Result; + + RequestSnapshot digikeyAPIRequest = new() + { + Route = nameof(Categories), + Response = result + }; + + _clientService.SaveRequest.Save(digikeyAPIRequest); + + return result; + } + + public async Task CategoriesByID(int categoryID, DateTime? afterDate = null) + { + afterDate ??= _clientService.AfterDate; + string? previous = _clientService.PrevRequest(nameof(CategoriesByID), categoryID.ToString(), (DateTime)afterDate); + if (previous != null) return previous; + + var resourcePathPrefix = "Search/v3/Categories"; + + var fullPath = $"/{resourcePathPrefix}/{categoryID}"; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var getResponse = await _clientService.GetAsync($"{fullPath}"); + + var result = _clientService.GetServiceResponse(getResponse).Result; + + RequestSnapshot digikeyAPIRequest = new() + { + Route = nameof(CategoriesByID), + RouteParameter = categoryID.ToString(), + Response = result + }; + + _clientService.SaveRequest.Save(digikeyAPIRequest); + + return result; + } + + #endregion + + #region RecommendedParts + + public async Task RecommendedProducts(string digikeyPartNumber, int recordCount = 1, string[]? searchOptionList = null, bool excludeMarketPlaceProducts = false, string[]? includes = null, DateTime? afterDate = null) + { + afterDate ??= _clientService.AfterDate; + string? previous = _clientService.PrevRequest(nameof(DigiReelPricing), digikeyPartNumber, (DateTime)afterDate); + if (previous != null) return previous; + + + var resourcePath = "Recommendations/v3/Products"; + + var encodedPN = HttpUtility.UrlEncode(digikeyPartNumber); + + var parameters = $"recordCount={recordCount}"; + + if (searchOptionList != null) + { + var optionListString = HttpUtility.UrlEncode(string.Join(",", searchOptionList)); + parameters += $"&searchOptionList={optionListString}"; + } + + if (excludeMarketPlaceProducts == true) + { + parameters += $"&excludeMarketPlaceProducts=true"; + } + + if (includes != null) + { + var includesString = HttpUtility.UrlEncode(string.Join(",", includes)); + parameters += $"&includes={includesString}"; + } + + var fullPath = $"/{resourcePath}/{encodedPN}?{parameters}"; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var getResponse = await _clientService.GetAsync(fullPath); + + var result = _clientService.GetServiceResponse(getResponse).Result; + + RequestSnapshot digikeyAPIRequest = new() + { + Route = nameof(RecommendedProducts), + RouteParameter = digikeyPartNumber!, + Parameters = parameters, + Response = result + }; + + _clientService.SaveRequest.Save(digikeyAPIRequest); + + return result; + } + + #endregion + + #region PackageTypeByQuantity + + public async Task PackageByQuantity(string digikeyPartNumber, int requestedQuantity, string? packagingPreference = null, string[]? includes = null, DateTime? afterDate = null) + { + afterDate ??= _clientService.AfterDate; + string? previous = _clientService.PrevRequest(nameof(DigiReelPricing), digikeyPartNumber, (DateTime)afterDate); + if (previous != null) return previous; + + + var resourcePath = "PackageTypeByQuantity/v3/Products"; + + var encodedPN = HttpUtility.UrlEncode(digikeyPartNumber); + + var parameters = HttpUtility.UrlEncode($"requestedQuantity={requestedQuantity}"); + + if (packagingPreference != null) + parameters += $"&packagingPreference={HttpUtility.UrlEncode(packagingPreference)}"; + + if (includes != null) + { + var includesString = HttpUtility.UrlEncode(string.Join(",", includes)); + parameters += $"&includes={includesString}"; + } + + var fullPath = $"/{resourcePath}/{encodedPN}?{parameters}"; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var getResponse = await _clientService.GetAsync(fullPath); + + var result = _clientService.GetServiceResponse(getResponse).Result; + + RequestSnapshot digikeyAPIRequest = new() + { + Route = nameof(PackageByQuantity), + RouteParameter = digikeyPartNumber!, + Parameters = parameters, + Response = result + }; + + _clientService.SaveRequest.Save(digikeyAPIRequest); + + return result; + } + + #endregion + + #region ProductChangeNotifications + + public async Task ProductChangeNotifications(string digikeyPartNumber, string[]? includes = null, DateTime? afterDate = null) + { + afterDate ??= _clientService.AfterDate; + string? previous = _clientService.PrevRequest(nameof(DigiReelPricing), digikeyPartNumber, (DateTime)afterDate); + if (previous != null) return previous; + + + string? parameters = null; + var resourcePath = "ChangeNotifications/v3/Products"; + + var encodedPN = HttpUtility.UrlEncode(digikeyPartNumber); + + string fullPath; + + if (includes == null) + fullPath = $"/{resourcePath}/{encodedPN}"; + + else + { + var includesString = HttpUtility.UrlEncode(string.Join(",", includes)); + parameters = $"inlcudes={includesString}"; + fullPath = $"/{resourcePath}/{encodedPN}?{parameters}"; + } + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var getResponse = await _clientService.GetAsync(fullPath); + + var result = _clientService.GetServiceResponse(getResponse).Result; + + RequestSnapshot digikeyAPIRequest = new() + { + Route = nameof(ProductChangeNotifications), + RouteParameter = digikeyPartNumber!, + Parameters = parameters!, + Response = result + }; + + _clientService.SaveRequest.Save(digikeyAPIRequest); + + return result; + } + + #endregion + + #region ProductTracing + + public async Task ProductTracingDetails(string tracingID, DateTime? afterDate = null) + { + afterDate ??= _clientService.AfterDate; + string? previous = _clientService.PrevRequest(nameof(DigiReelPricing), tracingID, (DateTime)afterDate); + if (previous != null) return previous; + + + var resourcePath = "ProductTracing/v1/Details"; + + var encodedID = HttpUtility.UrlEncode(tracingID); + + var fullPath = $"/{resourcePath}/{encodedID}"; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var getResponse = await _clientService.GetAsync(fullPath); + + var result = _clientService.GetServiceResponse(getResponse).Result; + + RequestSnapshot digikeyAPIRequest = new() + { + Route = nameof(ProductTracingDetails), + RouteParameter = tracingID!, + Response = result + }; + + _clientService.SaveRequest.Save(digikeyAPIRequest); + + return result; + } + + #endregion + } +} diff --git a/ApiClient/ApiClient.csproj b/ApiClient/ApiClient.csproj index 3287a5e..334a825 100644 --- a/ApiClient/ApiClient.csproj +++ b/ApiClient/ApiClient.csproj @@ -13,11 +13,12 @@ - - + + + + - - + \ No newline at end of file diff --git a/ApiClient/ApiClientService.cs b/ApiClient/ApiClientService.cs index 4698f24..22c434a 100644 --- a/ApiClient/ApiClientService.cs +++ b/ApiClient/ApiClientService.cs @@ -11,10 +11,12 @@ // //----------------------------------------------------------------------- +using ApiClient.API; using ApiClient.Constants; using ApiClient.Exception; using ApiClient.Models; using ApiClient.OAuth2; +using Microsoft.Extensions.Logging; using System.Net; using System.Net.Http.Headers; using System.Net.Http.Json; @@ -26,6 +28,13 @@ public class ApiClientService private const string CustomHeader = "Api-StaleTokenRetry"; private ApiClientSettings _clientSettings; + private readonly ILogger _logger; + + public readonly ISaveRequest SaveRequest; + public ProductInformation ProductInformation { get; private set; } + public DateTime AfterDate = DateTime.MinValue; + + public readonly IQueryable ExistingRequests; public ApiClientSettings ClientSettings { @@ -38,10 +47,16 @@ public ApiClientSettings ClientSettings /// public HttpClient HttpClient { get; private set; } - public ApiClientService(ApiClientSettings clientSettings) + public ApiClientService(ApiClientSettings clientSettings, ILogger? logger = null, ISaveRequest? saveRequest = null, IQueryable? existingRequests = null, DateTime? afterDate = null) { + ExistingRequests = existingRequests ?? Enumerable.Empty().AsQueryable(); + if (afterDate != null) AfterDate = (DateTime)afterDate; + SaveRequest = saveRequest ?? new DefaultSaveRequest(); + _logger = logger ?? ConsoleLogger.Create(); _clientSettings = clientSettings ?? throw new ArgumentNullException(nameof(clientSettings)); + ProductInformation = new(this); + ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; HttpClient = new() { BaseAddress = DigiKeyUriConstants.BaseAddress }; HttpClient.DefaultRequestHeaders.Authorization = new("Bearer", ClientSettings.AccessToken); @@ -59,14 +74,14 @@ public async Task ResetExpiredAccessTokenIfNeeded() if (oAuth2AccessToken.IsError) { // Current Refresh token is invalid or expired - Console.WriteLine("Current Refresh token is invalid or expired "); + _logger?.LogInformation("Current Refresh token is invalid or expired "); return; } // Update the clientSettings _clientSettings.UpdateAndSave(oAuth2AccessToken); - Console.WriteLine("ApiClientService::CheckifAccessTokenIsExpired() call to refresh"); - Console.WriteLine(_clientSettings.ToString()); + _logger?.LogInformation("ApiClientService::CheckifAccessTokenIsExpired() call to refresh"); + _logger?.LogInformation(_clientSettings.ToString()); // Reset the Authorization header value with the new access token. var authenticationHeaderValue = new AuthenticationHeaderValue("Bearer", _clientSettings.AccessToken); @@ -74,74 +89,73 @@ public async Task ResetExpiredAccessTokenIfNeeded() } } - public async Task KeywordSearch(string keyword) + public async Task GetAsync(string resourcePath) { - var resourcePath = "/Search/v3/Products/Keyword"; + _logger?.LogInformation(">ApiClientService::GetAsync()"); + var response = await HttpClient.GetAsync(resourcePath); + _logger?.LogInformation(" PostAsJsonAsync(string resourcePath, T postRequest) { - Console.WriteLine(">ApiClientService::PostAsJsonAsync()"); - try + _logger?.LogInformation(">ApiClientService::PostAsJsonAsync()"); + HttpResponseMessage response = await HttpClient.PostAsJsonAsync(resourcePath, postRequest); + _logger?.LogInformation(": HttpRequestException is {hre.Message}"); - throw; - } - catch (ApiException dae) - { - Console.WriteLine($"PostAsJsonAsync: ApiException is {dae.Message}"); - throw; + _logger?.LogInformation("New Access token is {accessToken}", _clientSettings.AccessToken); + + //Only retry the first time. + if (response.RequestMessage!.Headers.Contains(CustomHeader)) + throw new ApiException($"Inside method {nameof(PostAsJsonAsync)} we received an unexpected stale token response - during the retry for a call whose token we just refreshed {response.StatusCode}"); + + HttpClient.DefaultRequestHeaders.Add(CustomHeader, CustomHeader); + HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Authorization", _clientSettings.AccessToken); + + return await PostAsJsonAsync(resourcePath, postRequest); + } } + + return response; } - protected static async Task GetServiceResponse(HttpResponseMessage response) + public async Task GetServiceResponse(HttpResponseMessage response) { - Console.WriteLine(">ApiClientService::GetServiceResponse()"); + _logger?.LogInformation(">ApiClientService::GetServiceResponse()"); var postResponse = string.Empty; if (response.IsSuccessStatusCode) @@ -154,15 +168,20 @@ protected static async Task GetServiceResponse(HttpResponseMessage respo else { var errorMessage = await response.Content.ReadAsStringAsync(); - Console.WriteLine("Response"); - Console.WriteLine(" Status Code : {0}", response.StatusCode); - Console.WriteLine(" Content : {0}", errorMessage); - Console.WriteLine(" Reason : {0}", response.ReasonPhrase); + _logger?.LogInformation("Response"); + _logger?.LogInformation(" Status Code : {statusCode}", response.StatusCode); + _logger?.LogInformation(" Content : {errorMessage}", errorMessage); + _logger?.LogInformation(" Reason : {reasonPhrase}", response.ReasonPhrase); throw new System.Exception(response.ReasonPhrase); } - Console.WriteLine(" r.Route == route && r.RouteParameter == routeParameter && r.DateTime > afterDate).OrderByDescending(r => r.DateTime).FirstOrDefault(); + return snapshot?.Response; + } } } diff --git a/ApiClient/ConsoleLogger.cs b/ApiClient/ConsoleLogger.cs new file mode 100644 index 0000000..e706791 --- /dev/null +++ b/ApiClient/ConsoleLogger.cs @@ -0,0 +1,24 @@ +using Microsoft.Extensions.Logging; + +namespace ApiClient +{ + public static class ConsoleLogger + { + public static ILogger Create() + { + using var loggerFactory = LoggerFactory.Create(builder => + { + builder + .AddFilter("Microsoft", LogLevel.Warning) + .AddFilter("System", LogLevel.Warning) + .AddFilter("ApiClient", LogLevel.Debug) + .AddConsole(); + }); + ILogger logger = loggerFactory.CreateLogger(); + + logger.LogInformation("Logger Initialized"); + + return logger; + } + } +} diff --git a/ApiClient/Core/Configuration/ApiClientConfigHelper.cs b/ApiClient/Core/Configuration/ApiClientConfigHelper.cs index 7300918..8efd527 100644 --- a/ApiClient/Core/Configuration/ApiClientConfigHelper.cs +++ b/ApiClient/Core/Configuration/ApiClientConfigHelper.cs @@ -37,25 +37,12 @@ private ApiClientConfigHelper() { try { - string regexPattern = @"^(.*)(\\bin\\)(.*)$"; - - // We are attempting to find the apiclient.config file in the solution folder for this project - // Using this method we can use the same apiclient.config for all the projects in this solution. - var baseDir = AppDomain.CurrentDomain.BaseDirectory.TrimEnd('\\'); - - // This little hack is ugly but needed to work with Console apps and Asp.Net apps. - var solutionDir = Regex.IsMatch(baseDir, regexPattern) - ? Directory.GetParent(baseDir)?.Parent?.Parent?.Parent // Console Apps - : Directory.GetParent(baseDir); // Asp.Net apps - - if (!File.Exists(Path.Combine(solutionDir!.FullName, "apiclient.config"))) - { - throw new ApiException($"Unable to locate apiclient.config in solution folder {solutionDir.FullName}"); - } + var environmentPath = Environment.GetEnvironmentVariable("APICLIENT_CONFIG_PATH"); + var filePath = environmentPath ?? FindSolutionDir(); var map = new ExeConfigurationFileMap { - ExeConfigFilename = Path.Combine(solutionDir.FullName, "apiclient.config"), + ExeConfigFilename = filePath }; Console.WriteLine($"map.ExeConfigFilename {map.ExeConfigFilename}"); _config = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None); @@ -66,6 +53,27 @@ private ApiClientConfigHelper() } } + private static string FindSolutionDir() + { + string regexPattern = @"^(.*)(\\bin\\)(.*)$"; + + // We are attempting to find the apiclient.config file in the solution folder for this project + // Using this method we can use the same apiclient.config for all the projects in this solution. + var baseDir = AppDomain.CurrentDomain.BaseDirectory.TrimEnd('\\'); + + // This little hack is ugly but needed to work with Console apps and Asp.Net apps. + var solutionDir = Regex.IsMatch(baseDir, regexPattern) + ? Directory.GetParent(baseDir)?.Parent?.Parent?.Parent // Console Apps + : Directory.GetParent(baseDir); // Asp.Net apps + + if (!File.Exists(Path.Combine(solutionDir!.FullName, "apiclient.config"))) + { + throw new ApiException($"Unable to locate apiclient.config in solution folder {solutionDir.FullName}"); + } + + return Path.Combine(solutionDir.FullName, "apiclient.config"); + } + public static ApiClientConfigHelper Instance() { return _thisInstance; diff --git a/ApiClient/GlobalSuppressions.cs b/ApiClient/GlobalSuppressions.cs new file mode 100644 index 0000000..e336b1b --- /dev/null +++ b/ApiClient/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:ApiClient.ApiClientService.ResetExpiredAccessTokenIfNeeded~System.Threading.Tasks.Task")] diff --git a/ApiClient/Models/RequestSnapshot.cs b/ApiClient/Models/RequestSnapshot.cs new file mode 100644 index 0000000..7495187 --- /dev/null +++ b/ApiClient/Models/RequestSnapshot.cs @@ -0,0 +1,27 @@ +namespace ApiClient.Models +{ + public class DefaultSaveRequest : ISaveRequest + { + public void Save(RequestSnapshot requestSnapshot) { } + } + + public interface ISaveRequest + { + public void Save(RequestSnapshot requestSnapshot); + } + + public class RequestSnapshot + { + public long RequestID { get; set; } + + public string Route { get; set; } = null!; + + public string RouteParameter { get; set; } = null!; + + public string Parameters { get; set; } = null!; + + public string Response { get; set; } = null!; + + public DateTime DateTime { get; set; } + } +} diff --git a/ApiClient/OAuth2/OAuth2Helpers.cs b/ApiClient/OAuth2/OAuth2Helpers.cs index f9e705c..850116d 100644 --- a/ApiClient/OAuth2/OAuth2Helpers.cs +++ b/ApiClient/OAuth2/OAuth2Helpers.cs @@ -114,7 +114,6 @@ public static async Task RefreshTokenAsync(ApiClientSettings? } catch (System.Exception e) { - Console.WriteLine(e.Message); throw new ApiException($"Unable to parse OAuth2 access token response {e.Message}"); } } @@ -135,7 +134,6 @@ public static async Task RefreshTokenAsync(ApiClientSettings? } catch (System.Exception e) { - Console.WriteLine(e.Message); //_log.DebugFormat($"Unable to parse OAuth2 access token response {e.Message}"); throw new ApiException($"Unable to parse OAuth2 error response {e.Message}"); } diff --git a/ApiClient/OAuth2/OAuth2Service.cs b/ApiClient/OAuth2/OAuth2Service.cs index 9ab037f..16b41e8 100644 --- a/ApiClient/OAuth2/OAuth2Service.cs +++ b/ApiClient/OAuth2/OAuth2Service.cs @@ -14,6 +14,7 @@ using ApiClient.Constants; using ApiClient.Models; using ApiClient.OAuth2.Models; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System.Net; using System.Net.Http.Headers; @@ -27,6 +28,7 @@ namespace ApiClient.OAuth2 public class OAuth2Service { private ApiClientSettings? _clientSettings; + private readonly ILogger? _logger; public ApiClientSettings? ClientSettings { @@ -34,8 +36,9 @@ public ApiClientSettings? ClientSettings set { _clientSettings = value; } } - public OAuth2Service(ApiClientSettings? clientSettings) + public OAuth2Service(ApiClientSettings? clientSettings, ILogger? logger = null) { + _logger = logger; ClientSettings = clientSettings; } @@ -91,7 +94,7 @@ public string GenerateAuthUrl(string scopes = "", string? state = null) requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); requestMessage.Content = new FormUrlEncodedContent(body); - Console.WriteLine("HttpRequestMessage {0}", requestMessage.RequestUri?.AbsoluteUri); + _logger?.LogInformation("HttpRequestMessage {uri}", requestMessage.RequestUri?.AbsoluteUri); var tokenResponse = await httpClient.SendAsync(requestMessage).ConfigureAwait(false); var text = await tokenResponse.Content.ReadAsStringAsync(); @@ -150,7 +153,7 @@ public async Task RefreshTokenAsync() requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); requestMessage.Content = new FormUrlEncodedContent(body); - Console.WriteLine("HttpRequestMessage {0}", requestMessage.RequestUri?.AbsoluteUri); + _logger?.LogInformation("HttpRequestMessage {uri}", requestMessage.RequestUri?.AbsoluteUri); var tokenResponse = await httpClient.SendAsync(requestMessage).ConfigureAwait(false); var text = await tokenResponse.Content.ReadAsStringAsync(); From 706d9e714368e585acc05a63ee66c7ace3f8ab32 Mon Sep 17 00:00:00 2001 From: JackMBurch Date: Thu, 14 Dec 2023 23:22:07 -0500 Subject: [PATCH 05/18] char strings --- ApiClient/Extensions/StringExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ApiClient/Extensions/StringExtensions.cs b/ApiClient/Extensions/StringExtensions.cs index bc96616..730f18c 100644 --- a/ApiClient/Extensions/StringExtensions.cs +++ b/ApiClient/Extensions/StringExtensions.cs @@ -37,9 +37,9 @@ public static string EnsureTrailingSlash(this string input) return input; } - if (!input.EndsWith("/")) + if (!input.EndsWith('/')) { - return input + "/"; + return input + '/'; } return input; From ae26e4083931da5e448b3efb1cf75b3539f7ded2 Mon Sep 17 00:00:00 2001 From: JackMBurch Date: Fri, 15 Dec 2023 15:57:56 -0500 Subject: [PATCH 06/18] Updating constants to use environment variables for production vs sandbox --- ApiClient/Constants/DigiKeyUriConstants.cs | 44 ++++++++++++++++------ 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/ApiClient/Constants/DigiKeyUriConstants.cs b/ApiClient/Constants/DigiKeyUriConstants.cs index 55ca246..a147d64 100644 --- a/ApiClient/Constants/DigiKeyUriConstants.cs +++ b/ApiClient/Constants/DigiKeyUriConstants.cs @@ -18,19 +18,41 @@ namespace ApiClient.Constants ///
public static class DigiKeyUriConstants { - // Production Sandbox instance - //public static readonly Uri BaseAddress = new Uri("https://sandbox-api.digikey.com"); - //public static readonly Uri TokenEndpoint = new Uri("https://sandbox-api.digikey.com/v1/oauth2/token"); - //public static readonly Uri AuthorizationEndpoint = new Uri("https://sandbox-api.digikey.com/v1/oauth2/authorize"); + public static bool GetProductionVariable() + { + try + { + return bool.Parse(Environment.GetEnvironmentVariable("DIGIKEY_PRODUCTION")!); + } + catch + { + throw new System.Exception("Issue getting the DIGIKEY_PRODUCTION environment variable. Make sure the variable is set to either true or false."); + } + } + + public static Uri BaseAddress + { + get { return GetProductionVariable() ? ProductionBaseAddress : SandboxBaseAddress; } + } + + public static Uri TokenEndpoint + { + get { return GetProductionVariable() ? ProductionTokenEndpoint : SandboxTokenEndpoint; } + } - // Integration instance - // public static readonly Uri BaseAddress = new Uri("https://apiint.digikey.com"); - // public static readonly Uri TokenEndpoint = new Uri("https://apiint.digikey.com/v1/oauth2/token"); - // public static readonly Uri AuthorizationEndpoint = new Uri("https://apiint.digikey.com/v1/oauth2/authorize"); + public static Uri AuthorizationEndpoint + { + get { return GetProductionVariable() ? ProductionAuthorizationEndpoint : SandboxAuthorizationEndpoint; } + } + + // Production Sandbox instance + public static readonly Uri SandboxBaseAddress = new("https://sandbox-api.digikey.com"); + public static readonly Uri SandboxTokenEndpoint = new("https://sandbox-api.digikey.com/v1/oauth2/token"); + public static readonly Uri SandboxAuthorizationEndpoint = new("https://sandbox-api.digikey.com/v1/oauth2/authorize"); // Production instance - public static readonly Uri BaseAddress = new("https://api.digikey.com"); - public static readonly Uri TokenEndpoint = new("https://api.digikey.com/v1/oauth2/token"); - public static readonly Uri AuthorizationEndpoint = new("https://api.digikey.com/v1/oauth2/authorize"); + public static readonly Uri ProductionBaseAddress = new("https://api.digikey.com"); + public static readonly Uri ProductionTokenEndpoint = new("https://api.digikey.com/v1/oauth2/token"); + public static readonly Uri ProductionAuthorizationEndpoint = new("https://api.digikey.com/v1/oauth2/authorize"); } } From d7efc225d813e0872b40d0770135ad2bdd372f5f Mon Sep 17 00:00:00 2001 From: JackMBurch Date: Fri, 15 Dec 2023 16:06:57 -0500 Subject: [PATCH 07/18] Changing console app project type to console app --- .../ApiClient.ConsoleApp.csproj | 1 + ApiClient.ConsoleApp/Program.cs | 44 ++++++++----------- 2 files changed, 19 insertions(+), 26 deletions(-) diff --git a/ApiClient.ConsoleApp/ApiClient.ConsoleApp.csproj b/ApiClient.ConsoleApp/ApiClient.ConsoleApp.csproj index a28c5da..d683a9e 100644 --- a/ApiClient.ConsoleApp/ApiClient.ConsoleApp.csproj +++ b/ApiClient.ConsoleApp/ApiClient.ConsoleApp.csproj @@ -4,6 +4,7 @@ net8.0 enable enable + Exe
diff --git a/ApiClient.ConsoleApp/Program.cs b/ApiClient.ConsoleApp/Program.cs index ce1bac0..04b2d0f 100644 --- a/ApiClient.ConsoleApp/Program.cs +++ b/ApiClient.ConsoleApp/Program.cs @@ -35,39 +35,31 @@ private static async Task CallKeywordSearch() { var settings = ApiClientSettings.CreateFromConfigFile(); Console.WriteLine(settings.ToString()); - try + if (settings.ExpirationDateTime < DateTime.Now) { - if (settings.ExpirationDateTime < DateTime.Now) + // Let's refresh the token + var oAuth2Service = new OAuth2Service(settings); + var oAuth2AccessToken = await oAuth2Service.RefreshTokenAsync(); + if (oAuth2AccessToken.IsError) { - // Let's refresh the token - var oAuth2Service = new OAuth2Service(settings); - var oAuth2AccessToken = await oAuth2Service.RefreshTokenAsync(); - if (oAuth2AccessToken.IsError) - { - // Current Refresh token is invalid or expired - Console.WriteLine("Current Refresh token is invalid or expired "); - return; - } + // Current Refresh token is invalid or expired + Console.WriteLine("Current Refresh token is invalid or expired "); + return; + } - settings.UpdateAndSave(oAuth2AccessToken); + settings.UpdateAndSave(oAuth2AccessToken); - Console.WriteLine("After call to refresh"); - Console.WriteLine(settings.ToString()); - } + Console.WriteLine("After call to refresh"); + Console.WriteLine(settings.ToString()); + } - var client = new ApiClientService(settings); - var response = await client.ProductInformation.KeywordSearch("P5555-ND"); + var client = new ApiClientService(settings); + var response = await client.ProductInformation.KeywordSearch("P5555-ND"); - // In order to pretty print the json object we need to do the following - var jsonFormatted = JToken.Parse(response).ToString(Formatting.Indented); + // In order to pretty print the json object we need to do the following + var jsonFormatted = JToken.Parse(response).ToString(Formatting.Indented); - Console.WriteLine($"Reponse is {jsonFormatted} "); - } - catch (System.Exception e) - { - Console.WriteLine(e); - throw; - } + Console.WriteLine($"Reponse is {jsonFormatted} "); } } } From 8d219883fa1d17a8969119470bf410ec9b370cc5 Mon Sep 17 00:00:00 2001 From: JackMBurch Date: Fri, 15 Dec 2023 16:46:24 -0500 Subject: [PATCH 08/18] Console apps tested --- 2Legged_OAuth2Service.ConsoleApp/Program.cs | 38 +++++++++---------- .../3Legged_OAuth2Service.ConsoleApp.csproj | 6 --- 3Legged_OAuth2Service.ConsoleApp/Program.cs | 25 +++--------- ApiClient.ConsoleApp/Program.cs | 15 ++------ ApiClient/API/ProductInformation.cs | 15 +++++++- ApiClient/ConsoleLogger.cs | 15 +++++++- ApiClient/GlobalSuppressions.cs | 15 +++++++- ApiClient/Models/RequestSnapshot.cs | 15 +++++++- ApiClient/OAuth2/OAuth2Service.cs | 8 +--- 9 files changed, 87 insertions(+), 65 deletions(-) diff --git a/2Legged_OAuth2Service.ConsoleApp/Program.cs b/2Legged_OAuth2Service.ConsoleApp/Program.cs index c308fce..c480cbc 100644 --- a/2Legged_OAuth2Service.ConsoleApp/Program.cs +++ b/2Legged_OAuth2Service.ConsoleApp/Program.cs @@ -1,29 +1,26 @@ -using ApiClient.Models; +//----------------------------------------------------------------------- +// +// THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTIES OF ANY KIND, EXPRESS, IMPLIED, STATUTORY, +// OR OTHERWISE. EXPECT TO THE EXTENT PROHIBITED BY APPLICABLE LAW, DIGI-KEY DISCLAIMS ALL WARRANTIES, +// INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, +// SATISFACTORY QUALITY, TITLE, NON-INFRINGEMENT, QUIET ENJOYMENT, +// AND WARRANTIES ARISING OUT OF ANY COURSE OF DEALING OR USAGE OF TRADE. +// +// DIGI-KEY DOES NOT WARRANT THAT THE SOFTWARE WILL FUNCTION AS DESCRIBED, +// WILL BE UNINTERRUPTED OR ERROR-FREE, OR FREE OF HARMFUL COMPONENTS. +// +//----------------------------------------------------------------------- + +using ApiClient.Models; namespace _2Legged_OAuth2Service.ConsoleApp { public class Program { - private ApiClientSettings? _clientSettings; - - static void Main() - { - var program = new Program(); - - // Read configuration values from apiclient.config file and run OAuth2 code flow with OAuth2 Server - program.Authorize(); - - // This will keep the console window up until a key is press in the console window. - Console.WriteLine("\n\nPress any key to exit..."); - } - - /// - /// OAuth2 code flow authorization with apiclient.config values - /// - private async void Authorize() + static async Task Main() { // read clientSettings values from apiclient.config - _clientSettings = ApiClientSettings.CreateFromConfigFile(); + ApiClientSettings? _clientSettings = ApiClientSettings.CreateFromConfigFile(); Console.WriteLine(_clientSettings.ToString()); var oAuth2Service = new ApiClient.OAuth2.OAuth2Service(_clientSettings); @@ -50,6 +47,9 @@ private async void Authorize() Console.WriteLine("After a good refresh"); Console.WriteLine(_clientSettings.ToString()); } + // This will keep the console window up until a key is press in the console window. + Console.WriteLine("\n\nPress any key to exit..."); + Console.ReadKey(); } } } \ No newline at end of file diff --git a/3Legged_OAuth2Service.ConsoleApp/3Legged_OAuth2Service.ConsoleApp.csproj b/3Legged_OAuth2Service.ConsoleApp/3Legged_OAuth2Service.ConsoleApp.csproj index b219396..381faab 100644 --- a/3Legged_OAuth2Service.ConsoleApp/3Legged_OAuth2Service.ConsoleApp.csproj +++ b/3Legged_OAuth2Service.ConsoleApp/3Legged_OAuth2Service.ConsoleApp.csproj @@ -7,12 +7,6 @@ enable - - - - - - diff --git a/3Legged_OAuth2Service.ConsoleApp/Program.cs b/3Legged_OAuth2Service.ConsoleApp/Program.cs index 1bab68e..f108994 100644 --- a/3Legged_OAuth2Service.ConsoleApp/Program.cs +++ b/3Legged_OAuth2Service.ConsoleApp/Program.cs @@ -21,27 +21,10 @@ namespace OAuth2Service.ConsoleApp { public class Program { - private ApiClientSettings? _clientSettings; - - static void Main() - { - var prog = new Program(); - - // Read configuration values from apiclient.config file and run OAuth2 code flow with OAuth2 Server - prog.Authorize(); - - // This will keep the console window up until a key is press in the console window. - Console.WriteLine("\n\nPress any key to exit..."); - Console.ReadKey(); - } - - /// - /// OAuth2 code flow authorization with apiclient.config values - /// - private async void Authorize() + static async Task Main() { // read clientSettings values from apiclient.config - _clientSettings = ApiClientSettings.CreateFromConfigFile(); + ApiClientSettings? _clientSettings = ApiClientSettings.CreateFromConfigFile(); Console.WriteLine(_clientSettings.ToString()); // start up a HttpListener for the callback(RedirectUri) from the OAuth2 server @@ -101,6 +84,10 @@ private async void Authorize() Console.WriteLine("After a good refresh"); Console.WriteLine(_clientSettings.ToString()); } + + // This will keep the console window up until a key is press in the console window. + Console.WriteLine("\n\nPress any key to exit..."); + Console.ReadKey(); } } } diff --git a/ApiClient.ConsoleApp/Program.cs b/ApiClient.ConsoleApp/Program.cs index 04b2d0f..69f9702 100644 --- a/ApiClient.ConsoleApp/Program.cs +++ b/ApiClient.ConsoleApp/Program.cs @@ -21,17 +21,6 @@ namespace ApiClient.ConsoleApp public class Program { static async Task Main() - { - _ = new Program(); - - await CallKeywordSearch(); - - // This will keep the console window up until a key is pressed in the console window. - Console.WriteLine("\n\nPress any key to exit..."); - Console.ReadKey(); - } - - private static async Task CallKeywordSearch() { var settings = ApiClientSettings.CreateFromConfigFile(); Console.WriteLine(settings.ToString()); @@ -60,6 +49,10 @@ private static async Task CallKeywordSearch() var jsonFormatted = JToken.Parse(response).ToString(Formatting.Indented); Console.WriteLine($"Reponse is {jsonFormatted} "); + + // This will keep the console window up until a key is pressed in the console window. + Console.WriteLine("\n\nPress any key to exit..."); + Console.ReadKey(); } } } diff --git a/ApiClient/API/ProductInformation.cs b/ApiClient/API/ProductInformation.cs index 7364582..10125f5 100644 --- a/ApiClient/API/ProductInformation.cs +++ b/ApiClient/API/ProductInformation.cs @@ -1,4 +1,17 @@ -using ApiClient.Models; +//----------------------------------------------------------------------- +// +// THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTIES OF ANY KIND, EXPRESS, IMPLIED, STATUTORY, +// OR OTHERWISE. EXPECT TO THE EXTENT PROHIBITED BY APPLICABLE LAW, DIGI-KEY DISCLAIMS ALL WARRANTIES, +// INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, +// SATISFACTORY QUALITY, TITLE, NON-INFRINGEMENT, QUIET ENJOYMENT, +// AND WARRANTIES ARISING OUT OF ANY COURSE OF DEALING OR USAGE OF TRADE. +// +// DIGI-KEY DOES NOT WARRANT THAT THE SOFTWARE WILL FUNCTION AS DESCRIBED, +// WILL BE UNINTERRUPTED OR ERROR-FREE, OR FREE OF HARMFUL COMPONENTS. +// +//----------------------------------------------------------------------- + +using ApiClient.Models; using System.Web; namespace ApiClient.API diff --git a/ApiClient/ConsoleLogger.cs b/ApiClient/ConsoleLogger.cs index e706791..c48bfd2 100644 --- a/ApiClient/ConsoleLogger.cs +++ b/ApiClient/ConsoleLogger.cs @@ -1,4 +1,17 @@ -using Microsoft.Extensions.Logging; +//----------------------------------------------------------------------- +// +// THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTIES OF ANY KIND, EXPRESS, IMPLIED, STATUTORY, +// OR OTHERWISE. EXPECT TO THE EXTENT PROHIBITED BY APPLICABLE LAW, DIGI-KEY DISCLAIMS ALL WARRANTIES, +// INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, +// SATISFACTORY QUALITY, TITLE, NON-INFRINGEMENT, QUIET ENJOYMENT, +// AND WARRANTIES ARISING OUT OF ANY COURSE OF DEALING OR USAGE OF TRADE. +// +// DIGI-KEY DOES NOT WARRANT THAT THE SOFTWARE WILL FUNCTION AS DESCRIBED, +// WILL BE UNINTERRUPTED OR ERROR-FREE, OR FREE OF HARMFUL COMPONENTS. +// +//----------------------------------------------------------------------- + +using Microsoft.Extensions.Logging; namespace ApiClient { diff --git a/ApiClient/GlobalSuppressions.cs b/ApiClient/GlobalSuppressions.cs index e336b1b..e4cdc19 100644 --- a/ApiClient/GlobalSuppressions.cs +++ b/ApiClient/GlobalSuppressions.cs @@ -1,4 +1,17 @@ -// This file is used by Code Analysis to maintain SuppressMessage +//----------------------------------------------------------------------- +// +// THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTIES OF ANY KIND, EXPRESS, IMPLIED, STATUTORY, +// OR OTHERWISE. EXPECT TO THE EXTENT PROHIBITED BY APPLICABLE LAW, DIGI-KEY DISCLAIMS ALL WARRANTIES, +// INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, +// SATISFACTORY QUALITY, TITLE, NON-INFRINGEMENT, QUIET ENJOYMENT, +// AND WARRANTIES ARISING OUT OF ANY COURSE OF DEALING OR USAGE OF TRADE. +// +// DIGI-KEY DOES NOT WARRANT THAT THE SOFTWARE WILL FUNCTION AS DESCRIBED, +// WILL BE UNINTERRUPTED OR ERROR-FREE, OR FREE OF HARMFUL COMPONENTS. +// +//----------------------------------------------------------------------- + +// This file is used by Code Analysis to maintain SuppressMessage // attributes that are applied to this project. // Project-level suppressions either have no target or are given // a specific target and scoped to a namespace, type, member, etc. diff --git a/ApiClient/Models/RequestSnapshot.cs b/ApiClient/Models/RequestSnapshot.cs index 7495187..593b30f 100644 --- a/ApiClient/Models/RequestSnapshot.cs +++ b/ApiClient/Models/RequestSnapshot.cs @@ -1,4 +1,17 @@ -namespace ApiClient.Models +//----------------------------------------------------------------------- +// +// THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTIES OF ANY KIND, EXPRESS, IMPLIED, STATUTORY, +// OR OTHERWISE. EXPECT TO THE EXTENT PROHIBITED BY APPLICABLE LAW, DIGI-KEY DISCLAIMS ALL WARRANTIES, +// INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, +// SATISFACTORY QUALITY, TITLE, NON-INFRINGEMENT, QUIET ENJOYMENT, +// AND WARRANTIES ARISING OUT OF ANY COURSE OF DEALING OR USAGE OF TRADE. +// +// DIGI-KEY DOES NOT WARRANT THAT THE SOFTWARE WILL FUNCTION AS DESCRIBED, +// WILL BE UNINTERRUPTED OR ERROR-FREE, OR FREE OF HARMFUL COMPONENTS. +// +//----------------------------------------------------------------------- + +namespace ApiClient.Models { public class DefaultSaveRequest : ISaveRequest { diff --git a/ApiClient/OAuth2/OAuth2Service.cs b/ApiClient/OAuth2/OAuth2Service.cs index 16b41e8..17e3502 100644 --- a/ApiClient/OAuth2/OAuth2Service.cs +++ b/ApiClient/OAuth2/OAuth2Service.cs @@ -14,7 +14,6 @@ using ApiClient.Constants; using ApiClient.Models; using ApiClient.OAuth2.Models; -using Microsoft.Extensions.Logging; using Newtonsoft.Json; using System.Net; using System.Net.Http.Headers; @@ -28,7 +27,6 @@ namespace ApiClient.OAuth2 public class OAuth2Service { private ApiClientSettings? _clientSettings; - private readonly ILogger? _logger; public ApiClientSettings? ClientSettings { @@ -36,9 +34,8 @@ public ApiClientSettings? ClientSettings set { _clientSettings = value; } } - public OAuth2Service(ApiClientSettings? clientSettings, ILogger? logger = null) + public OAuth2Service(ApiClientSettings? clientSettings) { - _logger = logger; ClientSettings = clientSettings; } @@ -94,7 +91,6 @@ public string GenerateAuthUrl(string scopes = "", string? state = null) requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); requestMessage.Content = new FormUrlEncodedContent(body); - _logger?.LogInformation("HttpRequestMessage {uri}", requestMessage.RequestUri?.AbsoluteUri); var tokenResponse = await httpClient.SendAsync(requestMessage).ConfigureAwait(false); var text = await tokenResponse.Content.ReadAsStringAsync(); @@ -146,6 +142,7 @@ public async Task RefreshTokenAsync() new(OAuth2Constants.GrantType, OAuth2Constants.GrantTypes.ClientCredentials) }; + // Request the token var requestMessage = new HttpRequestMessage(HttpMethod.Post, DigiKeyUriConstants.TokenEndpoint); @@ -153,7 +150,6 @@ public async Task RefreshTokenAsync() requestMessage.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); requestMessage.Content = new FormUrlEncodedContent(body); - _logger?.LogInformation("HttpRequestMessage {uri}", requestMessage.RequestUri?.AbsoluteUri); var tokenResponse = await httpClient.SendAsync(requestMessage).ConfigureAwait(false); var text = await tokenResponse.Content.ReadAsStringAsync(); From e4cbd54e688048f6011adeb77ed05bc7153f529d Mon Sep 17 00:00:00 2001 From: JackMBurch Date: Fri, 15 Dec 2023 16:47:53 -0500 Subject: [PATCH 09/18] launchSettings files added with DIGIKEY_PRODUCTION envirnoment variables --- .../Properties/launchSettings.json | 10 ++++++++++ .../Properties/launchSettings.json | 10 ++++++++++ ApiClient.ConsoleApp/Properties/launchSettings.json | 10 ++++++++++ 3 files changed, 30 insertions(+) create mode 100644 2Legged_OAuth2Service.ConsoleApp/Properties/launchSettings.json create mode 100644 3Legged_OAuth2Service.ConsoleApp/Properties/launchSettings.json create mode 100644 ApiClient.ConsoleApp/Properties/launchSettings.json diff --git a/2Legged_OAuth2Service.ConsoleApp/Properties/launchSettings.json b/2Legged_OAuth2Service.ConsoleApp/Properties/launchSettings.json new file mode 100644 index 0000000..c27d044 --- /dev/null +++ b/2Legged_OAuth2Service.ConsoleApp/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "2Legged_OAuth2Service.ConsoleApp": { + "commandName": "Project", + "environmentVariables": { + "DIGIKEY_PRODUCTION": "true" + } + } + } +} \ No newline at end of file diff --git a/3Legged_OAuth2Service.ConsoleApp/Properties/launchSettings.json b/3Legged_OAuth2Service.ConsoleApp/Properties/launchSettings.json new file mode 100644 index 0000000..c6a4c88 --- /dev/null +++ b/3Legged_OAuth2Service.ConsoleApp/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "3Legged_OAuth2Service.ConsoleApp": { + "commandName": "Project", + "environmentVariables": { + "DIGIKEY_PRODUCTION": "true" + } + } + } +} \ No newline at end of file diff --git a/ApiClient.ConsoleApp/Properties/launchSettings.json b/ApiClient.ConsoleApp/Properties/launchSettings.json new file mode 100644 index 0000000..60e6413 --- /dev/null +++ b/ApiClient.ConsoleApp/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "ApiClient.ConsoleApp": { + "commandName": "Project", + "environmentVariables": { + "DIGIKEY_PRODUCTION": "true" + } + } + } +} \ No newline at end of file From af98193f236a6bf931f740b8cbe2b88dcf7b1a0a Mon Sep 17 00:00:00 2001 From: JackMBurch Date: Fri, 15 Dec 2023 17:53:17 -0500 Subject: [PATCH 10/18] Json menthods --- ApiClient/API/ProductInformationJson.cs | 94 +++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 ApiClient/API/ProductInformationJson.cs diff --git a/ApiClient/API/ProductInformationJson.cs b/ApiClient/API/ProductInformationJson.cs new file mode 100644 index 0000000..0d5f018 --- /dev/null +++ b/ApiClient/API/ProductInformationJson.cs @@ -0,0 +1,94 @@ +//----------------------------------------------------------------------- +// +// THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTIES OF ANY KIND, EXPRESS, IMPLIED, STATUTORY, +// OR OTHERWISE. EXPECT TO THE EXTENT PROHIBITED BY APPLICABLE LAW, DIGI-KEY DISCLAIMS ALL WARRANTIES, +// INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, +// SATISFACTORY QUALITY, TITLE, NON-INFRINGEMENT, QUIET ENJOYMENT, +// AND WARRANTIES ARISING OUT OF ANY COURSE OF DEALING OR USAGE OF TRADE. +// +// DIGI-KEY DOES NOT WARRANT THAT THE SOFTWARE WILL FUNCTION AS DESCRIBED, +// WILL BE UNINTERRUPTED OR ERROR-FREE, OR FREE OF HARMFUL COMPONENTS. +// +//----------------------------------------------------------------------- + +using System.Text.Json; + +namespace ApiClient.API +{ + public partial class ProductInformation + { + #region PartSearch + + public async Task KeywordSearchJson(string keyword, string[]? includes = null, DateTime? afterDate = null) + { + return JsonSerializer.Deserialize(await KeywordSearch(keyword, includes, afterDate)); + } + + public async Task ProductDetailsJson(string digikeyPartNumber, string[]? includes = null, DateTime? afterDate = null) + { + return JsonSerializer.Deserialize(await ProductDetails(digikeyPartNumber, includes, afterDate)); + } + + public async Task DigiReelPricingJson(string digikeyPartNumber, int requestedQuantity, string[]? includes = null, DateTime? afterDate = null) + { + return JsonSerializer.Deserialize(await DigiReelPricing(digikeyPartNumber, requestedQuantity, includes, afterDate)); + } + + public async Task SuggestedPartsJson(string partNumber, DateTime? afterDate = null) + { + return JsonSerializer.Deserialize(await SuggestedParts(partNumber, afterDate)); + } + public async Task ManufacturersJson(DateTime? afterDate = null) + { + return JsonSerializer.Deserialize(await Manufacturers(afterDate)); + } + + public async Task CategoriesJson(DateTime? afterDate = null) + { + return JsonSerializer.Deserialize(await Categories(afterDate)); + } + + public async Task CategoriesByIDJson(int categoryID, DateTime? afterDate = null) + { + return JsonSerializer.Deserialize(await CategoriesByID(categoryID, afterDate)); + } + + #endregion + + #region RecommendedParts + + public async Task RecommendedProductsJson(string digikeyPartNumber, int recordCount = 1, string[]? searchOptionList = null, bool excludeMarketPlaceProducts = false, string[]? includes = null, DateTime? afterDate = null) + { + return JsonSerializer.Deserialize(await RecommendedProducts(digikeyPartNumber, recordCount, searchOptionList, excludeMarketPlaceProducts, includes, afterDate)); + } + + #endregion + + #region PackageTypeByQuantity + + public async Task PackageByQuantityJson(string digikeyPartNumber, int requestedQuantity, string? packagingPreference = null, string[]? includes = null, DateTime? afterDate = null) + { + return JsonSerializer.Deserialize(await PackageByQuantity(digikeyPartNumber, requestedQuantity, packagingPreference, includes, afterDate)); + } + + #endregion + + #region ProductChangeNotifications + + public async Task ProductChangeNotificationsJson(string digikeyPartNumber, string[]? includes = null, DateTime? afterDate = null) + { + return JsonSerializer.Deserialize(await ProductChangeNotifications(digikeyPartNumber, includes, afterDate)); + } + + #endregion + + #region ProductTracing + + public async Task ProductTracingDetailsJson(string tracingID, DateTime? afterDate = null) + { + return JsonSerializer.Deserialize(await ProductTracingDetails(tracingID, afterDate)); + } + + #endregion + } +} \ No newline at end of file From 8954f99609780dcba18fd74328d0c4e5a16f391f Mon Sep 17 00:00:00 2001 From: Jack Burch Date: Fri, 15 Dec 2023 18:31:35 -0500 Subject: [PATCH 11/18] Update Readme.md --- Readme.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/Readme.md b/Readme.md index 9483b62..2a90254 100644 --- a/Readme.md +++ b/Readme.md @@ -17,10 +17,11 @@ ```csharp var settings = ApiClientSettings.CreateFromConfigFile(); var client = new ApiClientService(settings); -var postResponse = await client.KeywordSearch("P5555-ND"); +var postResponse = await client.ProductInformation.KeywordSearch("P5555-ND"); Console.WriteLine("response is {0}", postResponse); ``` + ### Project Contents * **ApiClient** - Client Library that contains the code to manage a config file with OAuth2 settings and classes to do the OAuth2 call and an example call to DigiKey's KeywordSearch Api. @@ -45,5 +46,80 @@ Console.WriteLine("response is {0}", postResponse); ``` -4. Run OAuth2Service.ConsoleApp to set the access token, refresh token and expiration date in apiclient.config. +4. Run 3Legged_OAuth2Service.ConsoleApp to set the access token, refresh token and expiration date in apiclient.config. 5. Run ApiClient.ConsoleApp to get results from keyword search. + + + +### Advanced Usage +The library supports using external data sources to store previous requests and prevent sending duplicate requests by checking the external data source. Checking for existing requests can be done by mapping the data source object to a new RequestSnapshot object. Saving requests can be done by implementing the ISaveRequest interface. + + +```csharp +public class DigiKeyHelperService + { + private readonly ApiClientSettings _clientSettings; + private readonly ApiClientService _clientService; + private readonly EFCoreContext _efCoreContext; + private readonly SaveRequest _saveRequest; + + + public DigiKeyHelperService(EFCoreContext efCoreContext) + { + _efCoreContext = efCoreContext; + _saveRequest = new(_efCoreContext); + _clientSettings = ApiClientSettings.CreateFromConfigFile(); + _clientService = new(_clientSettings, + saveRequest: _saveRequest, + existingRequests: _efCoreContext.DigikeyAPIRequests.Select(x => new RequestSnapshot() + { + RequestID = x.RequestID, + Route = x.Route, + RouteParameter = x.RouteParameter, + Response = x.Response, + DateTime = x.DateTime + })); + } + + public class SaveRequest(EFCoreContext efCoreContext) : ISaveRequest + { + private readonly EFCoreContext _efCoreContext = efCoreContext; + + public void Save(RequestSnapshot requestSnapshot) + { + _efCoreContext.DigikeyAPIRequests.Add(new() + { + Route = requestSnapshot.Route, + RouteParameter = requestSnapshot.RouteParameter, + Response = requestSnapshot.Response + }); + _efCoreContext.SaveChanges(); + } + } +} +``` + +### Previous Request Cutoffs + +The age of previous requests to consider as recent enough can be globably set, or set on a request to request basis. + +#### Global Time Cutoff + +```csharp +DateTime cutoffDateTime = DateTime.Today.AddDays(-30); +_clientService = new(_clientSettings, afterDate: cutoffDateTime); +``` + +#### Single Request Time Cutoff + +```csharp +DateTime cutoffDateTime = DateTime.Today.AddDays(-5); +var postResponse = await _clientService.ProductInformation.KeywordSearch("P5555-ND", afterDate: cutoffDateTime); +Console.WriteLine("response is {0}", postResponse); +``` + +### Environment Variables + +The library also supports custom apiclient.config file locations. Simply set the APICLIENT_CONFIG_PATH environment variable to the path of your file. + +If you need to change between Production and Sandbox environments, use the DIGIKEY_PRODUCTION environement variable. From 8b62a1c93cfd006f4f27736a727dd1d4fc237101 Mon Sep 17 00:00:00 2001 From: JackMBurch Date: Fri, 15 Dec 2023 19:43:29 -0500 Subject: [PATCH 12/18] Removed logger initialization logging --- ApiClient/ConsoleLogger.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/ApiClient/ConsoleLogger.cs b/ApiClient/ConsoleLogger.cs index c48bfd2..c12416a 100644 --- a/ApiClient/ConsoleLogger.cs +++ b/ApiClient/ConsoleLogger.cs @@ -29,8 +29,6 @@ public static ILogger Create() }); ILogger logger = loggerFactory.CreateLogger(); - logger.LogInformation("Logger Initialized"); - return logger; } } From aaa3a67d0ce6fe46a39a1f6c9c6c1cea120d9e0c Mon Sep 17 00:00:00 2001 From: JackMBurch Date: Mon, 18 Dec 2023 21:04:21 -0500 Subject: [PATCH 13/18] DigiKey API Client restructured to allow scoped DB instances. --- ApiClient/API/ProductInformation.cs | 435 ---------------- .../ProductInformation/ProductInformation.cs | 490 ++++++++++++++++++ .../ProductInformationJson.cs | 171 ++++++ .../ProductInformationParent.cs | 166 ++++++ ApiClient/API/ProductInformationJson.cs | 94 ---- ApiClient/ApiClientService.cs | 49 +- ApiClient/ConsoleLogger.cs | 2 +- ApiClient/GlobalSuppressions.cs | 4 - ApiClient/Models/RequestSnapshot.cs | 13 +- 9 files changed, 864 insertions(+), 560 deletions(-) delete mode 100644 ApiClient/API/ProductInformation.cs create mode 100644 ApiClient/API/ProductInformation/ProductInformation.cs create mode 100644 ApiClient/API/ProductInformation/ProductInformationJson.cs create mode 100644 ApiClient/API/ProductInformation/ProductInformationParent.cs delete mode 100644 ApiClient/API/ProductInformationJson.cs diff --git a/ApiClient/API/ProductInformation.cs b/ApiClient/API/ProductInformation.cs deleted file mode 100644 index 10125f5..0000000 --- a/ApiClient/API/ProductInformation.cs +++ /dev/null @@ -1,435 +0,0 @@ -//----------------------------------------------------------------------- -// -// THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTIES OF ANY KIND, EXPRESS, IMPLIED, STATUTORY, -// OR OTHERWISE. EXPECT TO THE EXTENT PROHIBITED BY APPLICABLE LAW, DIGI-KEY DISCLAIMS ALL WARRANTIES, -// INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, -// SATISFACTORY QUALITY, TITLE, NON-INFRINGEMENT, QUIET ENJOYMENT, -// AND WARRANTIES ARISING OUT OF ANY COURSE OF DEALING OR USAGE OF TRADE. -// -// DIGI-KEY DOES NOT WARRANT THAT THE SOFTWARE WILL FUNCTION AS DESCRIBED, -// WILL BE UNINTERRUPTED OR ERROR-FREE, OR FREE OF HARMFUL COMPONENTS. -// -//----------------------------------------------------------------------- - -using ApiClient.Models; -using System.Web; - -namespace ApiClient.API -{ - public partial class ProductInformation(ApiClientService clientService) - { - private readonly ApiClientService _clientService = clientService; - - #region PartSearch - - public async Task KeywordSearch(string keyword, string[]? includes = null, DateTime? afterDate = null) - { - afterDate ??= _clientService.AfterDate; - string? previous = _clientService.PrevRequest(nameof(KeywordSearch), keyword, (DateTime)afterDate); - if (previous != null) return previous; - - - var resourcePath = "Search/v3/Products/Keyword"; - - var parameters = string.Empty; - - if (includes != null) - { - var includesString = HttpUtility.UrlEncode(string.Join(",", includes)); - parameters += $"?includes={includesString}"; - } - - var fullPath = $"/{resourcePath}{parameters}"; - - var request = new KeywordSearchRequest - { - Keywords = keyword ?? "P5555-ND", - RecordCount = 25 - }; - - - await _clientService.ResetExpiredAccessTokenIfNeeded(); - var postResponse = await _clientService.PostAsJsonAsync(fullPath, request); - var result = _clientService.GetServiceResponse(postResponse).Result; - RequestSnapshot digikeyAPIRequest = new() - { - Route = nameof(KeywordSearch), - RouteParameter = keyword!, - Parameters = parameters, - Response = result - }; - _clientService.SaveRequest.Save(digikeyAPIRequest); - return result; - } - - public async Task ProductDetails(string digikeyPartNumber, string[]? includes = null, DateTime? afterDate = null) - { - afterDate ??= _clientService.AfterDate; - string? previous = _clientService.PrevRequest(nameof(ProductDetails), digikeyPartNumber, (DateTime)afterDate); - if (previous != null) return previous; - - - var resourcePath = "Search/v3/Products"; - - var parameters = string.Empty; - - if (includes != null) - { - var includesString = HttpUtility.UrlEncode(string.Join(",", includes)); - parameters += $"?includes={includesString}"; - } - - - var encodedPN = HttpUtility.UrlEncode(digikeyPartNumber); - - var fullPath = $"/{resourcePath}/{encodedPN}{parameters}"; - - await _clientService.ResetExpiredAccessTokenIfNeeded(); - var getResponse = await _clientService.GetAsync(fullPath); - - var result = _clientService.GetServiceResponse(getResponse).Result; - - RequestSnapshot digikeyAPIRequest = new() - { - Route = nameof(ProductDetails), - RouteParameter = digikeyPartNumber!, - Parameters = parameters, - Response = result - }; - - _clientService.SaveRequest.Save(digikeyAPIRequest); - - return result; - } - - public async Task DigiReelPricing(string digikeyPartNumber, int requestedQuantity, string[]? includes = null, DateTime? afterDate = null) - { - afterDate ??= _clientService.AfterDate; - string? previous = _clientService.PrevRequest(nameof(DigiReelPricing), digikeyPartNumber, (DateTime)afterDate); - if (previous != null) return previous; - - - var resourcePathPrefix = "Search/v3/Products"; - var resourcePathSuffix = "DigiReelPricing"; - - var encodedPN = HttpUtility.UrlEncode(digikeyPartNumber); - - var parameters = $"requestedQuantity={requestedQuantity}"; - - if (includes != null) - { - var includesString = HttpUtility.UrlEncode(string.Join(",", includes)); - parameters += $"&includes={includesString}"; - } - - var fullPath = $"/{resourcePathPrefix}/{encodedPN}/{resourcePathSuffix}?{parameters}"; - - await _clientService.ResetExpiredAccessTokenIfNeeded(); - var getResponse = await _clientService.GetAsync(fullPath); - - var result = _clientService.GetServiceResponse(getResponse).Result; - - RequestSnapshot digikeyAPIRequest = new() - { - Route = nameof(DigiReelPricing), - RouteParameter = digikeyPartNumber!, - Parameters = parameters, - Response = result - }; - - _clientService.SaveRequest.Save(digikeyAPIRequest); - - return result; - } - - public async Task SuggestedParts(string partNumber, DateTime? afterDate = null) - { - afterDate ??= _clientService.AfterDate; - string? previous = _clientService.PrevRequest(nameof(DigiReelPricing), partNumber, (DateTime)afterDate); - if (previous != null) return previous; - - - var resourcePathPrefix = "Search/v3/Products"; - var resourcePathSuffix = "WithSuggestedProducts"; - - var encodedPN = HttpUtility.UrlEncode(partNumber); - - var fullPath = $"/{resourcePathPrefix}/{encodedPN}/{resourcePathSuffix}"; - - await _clientService.ResetExpiredAccessTokenIfNeeded(); - var getResponse = await _clientService.GetAsync(fullPath); - - var result = _clientService.GetServiceResponse(getResponse).Result; - - RequestSnapshot digikeyAPIRequest = new() - { - Route = nameof(SuggestedParts), - RouteParameter = partNumber!, - Response = result - }; - - _clientService.SaveRequest.Save(digikeyAPIRequest); - - return result; - } - - public async Task Manufacturers(DateTime? afterDate = null) - { - afterDate ??= _clientService.AfterDate; - string? previous = _clientService.PrevRequest(nameof(Manufacturers), null!, (DateTime)afterDate); - if (previous != null) return previous; - - - var resourcePath = "Search/v3/Manufacturers"; - - await _clientService.ResetExpiredAccessTokenIfNeeded(); - var getResponse = await _clientService.GetAsync($"{resourcePath}"); - - var result = _clientService.GetServiceResponse(getResponse).Result; - - RequestSnapshot digikeyAPIRequest = new() - { - Route = nameof(Manufacturers), - Response = result - }; - - _clientService.SaveRequest.Save(digikeyAPIRequest); - - return result; - } - - public async Task Categories(DateTime? afterDate = null) - { - afterDate ??= _clientService.AfterDate; - string? previous = _clientService.PrevRequest(nameof(Categories), null!, (DateTime)afterDate); - if (previous != null) return previous; - - - var resourcePath = "Search/v3/Categories"; - - await _clientService.ResetExpiredAccessTokenIfNeeded(); - var getResponse = await _clientService.GetAsync($"{resourcePath}"); - - var result = _clientService.GetServiceResponse(getResponse).Result; - - RequestSnapshot digikeyAPIRequest = new() - { - Route = nameof(Categories), - Response = result - }; - - _clientService.SaveRequest.Save(digikeyAPIRequest); - - return result; - } - - public async Task CategoriesByID(int categoryID, DateTime? afterDate = null) - { - afterDate ??= _clientService.AfterDate; - string? previous = _clientService.PrevRequest(nameof(CategoriesByID), categoryID.ToString(), (DateTime)afterDate); - if (previous != null) return previous; - - var resourcePathPrefix = "Search/v3/Categories"; - - var fullPath = $"/{resourcePathPrefix}/{categoryID}"; - - await _clientService.ResetExpiredAccessTokenIfNeeded(); - var getResponse = await _clientService.GetAsync($"{fullPath}"); - - var result = _clientService.GetServiceResponse(getResponse).Result; - - RequestSnapshot digikeyAPIRequest = new() - { - Route = nameof(CategoriesByID), - RouteParameter = categoryID.ToString(), - Response = result - }; - - _clientService.SaveRequest.Save(digikeyAPIRequest); - - return result; - } - - #endregion - - #region RecommendedParts - - public async Task RecommendedProducts(string digikeyPartNumber, int recordCount = 1, string[]? searchOptionList = null, bool excludeMarketPlaceProducts = false, string[]? includes = null, DateTime? afterDate = null) - { - afterDate ??= _clientService.AfterDate; - string? previous = _clientService.PrevRequest(nameof(DigiReelPricing), digikeyPartNumber, (DateTime)afterDate); - if (previous != null) return previous; - - - var resourcePath = "Recommendations/v3/Products"; - - var encodedPN = HttpUtility.UrlEncode(digikeyPartNumber); - - var parameters = $"recordCount={recordCount}"; - - if (searchOptionList != null) - { - var optionListString = HttpUtility.UrlEncode(string.Join(",", searchOptionList)); - parameters += $"&searchOptionList={optionListString}"; - } - - if (excludeMarketPlaceProducts == true) - { - parameters += $"&excludeMarketPlaceProducts=true"; - } - - if (includes != null) - { - var includesString = HttpUtility.UrlEncode(string.Join(",", includes)); - parameters += $"&includes={includesString}"; - } - - var fullPath = $"/{resourcePath}/{encodedPN}?{parameters}"; - - await _clientService.ResetExpiredAccessTokenIfNeeded(); - var getResponse = await _clientService.GetAsync(fullPath); - - var result = _clientService.GetServiceResponse(getResponse).Result; - - RequestSnapshot digikeyAPIRequest = new() - { - Route = nameof(RecommendedProducts), - RouteParameter = digikeyPartNumber!, - Parameters = parameters, - Response = result - }; - - _clientService.SaveRequest.Save(digikeyAPIRequest); - - return result; - } - - #endregion - - #region PackageTypeByQuantity - - public async Task PackageByQuantity(string digikeyPartNumber, int requestedQuantity, string? packagingPreference = null, string[]? includes = null, DateTime? afterDate = null) - { - afterDate ??= _clientService.AfterDate; - string? previous = _clientService.PrevRequest(nameof(DigiReelPricing), digikeyPartNumber, (DateTime)afterDate); - if (previous != null) return previous; - - - var resourcePath = "PackageTypeByQuantity/v3/Products"; - - var encodedPN = HttpUtility.UrlEncode(digikeyPartNumber); - - var parameters = HttpUtility.UrlEncode($"requestedQuantity={requestedQuantity}"); - - if (packagingPreference != null) - parameters += $"&packagingPreference={HttpUtility.UrlEncode(packagingPreference)}"; - - if (includes != null) - { - var includesString = HttpUtility.UrlEncode(string.Join(",", includes)); - parameters += $"&includes={includesString}"; - } - - var fullPath = $"/{resourcePath}/{encodedPN}?{parameters}"; - - await _clientService.ResetExpiredAccessTokenIfNeeded(); - var getResponse = await _clientService.GetAsync(fullPath); - - var result = _clientService.GetServiceResponse(getResponse).Result; - - RequestSnapshot digikeyAPIRequest = new() - { - Route = nameof(PackageByQuantity), - RouteParameter = digikeyPartNumber!, - Parameters = parameters, - Response = result - }; - - _clientService.SaveRequest.Save(digikeyAPIRequest); - - return result; - } - - #endregion - - #region ProductChangeNotifications - - public async Task ProductChangeNotifications(string digikeyPartNumber, string[]? includes = null, DateTime? afterDate = null) - { - afterDate ??= _clientService.AfterDate; - string? previous = _clientService.PrevRequest(nameof(DigiReelPricing), digikeyPartNumber, (DateTime)afterDate); - if (previous != null) return previous; - - - string? parameters = null; - var resourcePath = "ChangeNotifications/v3/Products"; - - var encodedPN = HttpUtility.UrlEncode(digikeyPartNumber); - - string fullPath; - - if (includes == null) - fullPath = $"/{resourcePath}/{encodedPN}"; - - else - { - var includesString = HttpUtility.UrlEncode(string.Join(",", includes)); - parameters = $"inlcudes={includesString}"; - fullPath = $"/{resourcePath}/{encodedPN}?{parameters}"; - } - - await _clientService.ResetExpiredAccessTokenIfNeeded(); - var getResponse = await _clientService.GetAsync(fullPath); - - var result = _clientService.GetServiceResponse(getResponse).Result; - - RequestSnapshot digikeyAPIRequest = new() - { - Route = nameof(ProductChangeNotifications), - RouteParameter = digikeyPartNumber!, - Parameters = parameters!, - Response = result - }; - - _clientService.SaveRequest.Save(digikeyAPIRequest); - - return result; - } - - #endregion - - #region ProductTracing - - public async Task ProductTracingDetails(string tracingID, DateTime? afterDate = null) - { - afterDate ??= _clientService.AfterDate; - string? previous = _clientService.PrevRequest(nameof(DigiReelPricing), tracingID, (DateTime)afterDate); - if (previous != null) return previous; - - - var resourcePath = "ProductTracing/v1/Details"; - - var encodedID = HttpUtility.UrlEncode(tracingID); - - var fullPath = $"/{resourcePath}/{encodedID}"; - - await _clientService.ResetExpiredAccessTokenIfNeeded(); - var getResponse = await _clientService.GetAsync(fullPath); - - var result = _clientService.GetServiceResponse(getResponse).Result; - - RequestSnapshot digikeyAPIRequest = new() - { - Route = nameof(ProductTracingDetails), - RouteParameter = tracingID!, - Response = result - }; - - _clientService.SaveRequest.Save(digikeyAPIRequest); - - return result; - } - - #endregion - } -} diff --git a/ApiClient/API/ProductInformation/ProductInformation.cs b/ApiClient/API/ProductInformation/ProductInformation.cs new file mode 100644 index 0000000..8222dc7 --- /dev/null +++ b/ApiClient/API/ProductInformation/ProductInformation.cs @@ -0,0 +1,490 @@ +//----------------------------------------------------------------------- +// +// THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTIES OF ANY KIND, EXPRESS, IMPLIED, STATUTORY, +// OR OTHERWISE. EXPECT TO THE EXTENT PROHIBITED BY APPLICABLE LAW, DIGI-KEY DISCLAIMS ALL WARRANTIES, +// INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, +// SATISFACTORY QUALITY, TITLE, NON-INFRINGEMENT, QUIET ENJOYMENT, +// AND WARRANTIES ARISING OUT OF ANY COURSE OF DEALING OR USAGE OF TRADE. +// +// DIGI-KEY DOES NOT WARRANT THAT THE SOFTWARE WILL FUNCTION AS DESCRIBED, +// WILL BE UNINTERRUPTED OR ERROR-FREE, OR FREE OF HARMFUL COMPONENTS. +// +//----------------------------------------------------------------------- + +using System.Web; +using static ApiClient.API.ProductInformationParent; + +namespace ApiClient.API +{ + public partial class ProductInformation(ApiClientService clientService) + { + private readonly ApiClientService _clientService = clientService; + + #region PartSearch + + public async Task KeywordSearch(string keyword, string[]? includes = null) + { + var parent = new KeywordSearchParent(keyword, includes); + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.PostAsJsonAsync(parent.Path, parent.KeywordSearchRequest); + var result = _clientService.GetServiceResponse(response).Result; + + return result; + } + + public async Task ProductDetails(string digikeyPartNumber, string[]? includes = null) + { + var parent = new ProductDetailsParent(digikeyPartNumber, includes); + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + var result = _clientService.GetServiceResponse(response).Result; + + return result; + } + + public async Task DigiReelPricing(string digikeyPartNumber, int requestedQuantity, string[]? includes = null) + { + var otherParameters = new Dictionary + { + { "requestedQuantity", requestedQuantity.ToString() } + }; + + var parent = new DigiReelPricingParent(digikeyPartNumber, otherParameters, includes); + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + var result = _clientService.GetServiceResponse(response).Result; + + return result; + } + + public async Task SuggestedParts(string partNumber) + { + var parent = new SuggestedPartsParent(partNumber); + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + var result = _clientService.GetServiceResponse(response).Result; + + return result; + } + + public async Task Manufacturers() + { + var parent = new ManufacturersParent(); + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + var result = _clientService.GetServiceResponse(response).Result; + + return result; + } + + public async Task Categories() + { + var parent = new CategoriesParent(); + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + var result = _clientService.GetServiceResponse(response).Result; + + return result; + } + + public async Task CategoriesByID(int categoryID) + { + var parent = new CategoriesByIDParent(categoryID); + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + var result = _clientService.GetServiceResponse(response).Result; + + return result; + } + + #endregion + + #region RecommendedParts + + public async Task RecommendedProducts(string digikeyPartNumber, int recordCount = 1, string[]? searchOptionList = null, bool excludeMarketPlaceProducts = false, string[]? includes = null) + { + var otherParameters = new Dictionary + { + { "recordCount", recordCount.ToString() } + }; + + if (searchOptionList != null) + otherParameters.Add("searchOptionList", HttpUtility.UrlEncode(string.Join(",", searchOptionList))); + + if (excludeMarketPlaceProducts == true) + otherParameters.Add("excludeMarketPlaceProducts", "true"); + + var parent = new RecommendedProductsParent(digikeyPartNumber, otherParameters, includes); + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + var result = _clientService.GetServiceResponse(response).Result; + + return result; + } + + #endregion + + #region PackageTypeByQuantity + + public async Task PackageByQuantity(string digikeyPartNumber, int requestedQuantity, string? packagingPreference = null, string[]? includes = null) + { + var otherParameters = new Dictionary + { + { "requestedQuantity", requestedQuantity.ToString() } + }; + + if (packagingPreference != null) + otherParameters.Add("packagingPreference", packagingPreference); + + var parent = new PackageByQuantityParent(digikeyPartNumber, otherParameters, includes); + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + var result = _clientService.GetServiceResponse(response).Result; + + return result; + } + + #endregion + + #region ProductChangeNotifications + + public async Task ProductChangeNotifications(string digikeyPartNumber, string[]? includes = null) + { + var parent = new ProductChangeNotificationsParent(digikeyPartNumber, includes); + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + var result = _clientService.GetServiceResponse(response).Result; + + return result; + } + + #endregion + + #region ProductTracing + + public async Task ProductTracingDetails(string tracingID) + { + var parent = new ProductTracingDetailsParent(tracingID); + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + var result = _clientService.GetServiceResponse(response).Result; + + return result; + } + + #endregion + } + + public partial class ProductInformation(ApiClientService clientService) + { + private readonly ApiClientService _clientService = clientService; + + #region PartSearch + + public async Task KeywordSearch(string keyword, T3 database, string[]? includes = null, DateTime? afterDate = null) + { + var parent = new KeywordSearchParent(keyword, includes); + + var existing = _clientService.RequestQuerySave.Query(parent.Route, parent.RouteParameter, database, afterDate); + if (existing != null) + return existing; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.PostAsJsonAsync(parent.Path, parent.KeywordSearchRequest); + string result = _clientService.GetServiceResponse(response).Result; + + _clientService.RequestQuerySave.Save(new() + { + Route = parent.Route, + RouteParameter = parent.RouteParameter, + Parameters = parent.Parameters, + Response = result + }, database); + + return result; + } + + public async Task ProductDetails(string digikeyPartNumber, T3 database, string[]? includes = null, DateTime? afterDate = null) + { + var parent = new ProductDetailsParent(digikeyPartNumber, includes); + + var existing = _clientService.RequestQuerySave.Query(parent.Route, parent.RouteParameter, database, afterDate); + if (existing != null) + return existing; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + string result = _clientService.GetServiceResponse(response).Result; + + _clientService.RequestQuerySave.Save(new() + { + Route = parent.Route, + RouteParameter = parent.RouteParameter, + Parameters = parent.Parameters, + Response = result + }, database); + + return result; + } + + public async Task DigiReelPricing(string digikeyPartNumber, int requestedQuantity, T3 database, string[]? includes = null, DateTime? afterDate = null) + { + var otherParameters = new Dictionary + { + { "requestedQuantity", requestedQuantity.ToString() } + }; + + var parent = new DigiReelPricingParent(digikeyPartNumber, otherParameters, includes); + + var existing = _clientService.RequestQuerySave.Query(parent.Route, parent.RouteParameter, database, afterDate); + if (existing != null) + return existing; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + string result = _clientService.GetServiceResponse(response).Result; + + _clientService.RequestQuerySave.Save(new() + { + Route = parent.Route, + RouteParameter = parent.RouteParameter, + Parameters = parent.Parameters, + Response = result + }, database); + + return result; + } + + public async Task SuggestedParts(string partNumber, T3 database, DateTime? afterDate = null) + { + var parent = new SuggestedPartsParent(partNumber); + + var existing = _clientService.RequestQuerySave.Query(parent.Route, parent.RouteParameter, database, afterDate); + if (existing != null) + return existing; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + string result = _clientService.GetServiceResponse(response).Result; + + _clientService.RequestQuerySave.Save(new() + { + Route = parent.Route, + RouteParameter = parent.RouteParameter, + Parameters = parent.Parameters, + Response = result + }, database); + + return result; + } + + public async Task Manufacturers(T3 database, DateTime? afterDate = null) + { + var parent = new ManufacturersParent(); + + var existing = _clientService.RequestQuerySave.Query(parent.Route, parent.RouteParameter, database, afterDate); + if (existing != null) + return existing; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + string result = _clientService.GetServiceResponse(response).Result; + + _clientService.RequestQuerySave.Save(new() + { + Route = parent.Route, + RouteParameter = parent.RouteParameter, + Parameters = parent.Parameters, + Response = result + }, database); + + return result; + } + + public async Task Categories(T3 database, DateTime? afterDate = null) + { + var parent = new CategoriesParent(); + + var existing = _clientService.RequestQuerySave.Query(parent.Route, parent.RouteParameter, database, afterDate); + if (existing != null) + return existing; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + string result = _clientService.GetServiceResponse(response).Result; + + _clientService.RequestQuerySave.Save(new() + { + Route = parent.Route, + RouteParameter = parent.RouteParameter, + Parameters = parent.Parameters, + Response = result + }, database); + + return result; + } + + public async Task CategoriesByID(int categoryID, T3 database, DateTime? afterDate = null) + { + var parent = new CategoriesByIDParent(categoryID); + + var existing = _clientService.RequestQuerySave.Query(parent.Route, parent.RouteParameter, database, afterDate); + if (existing != null) + return existing; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + string result = _clientService.GetServiceResponse(response).Result; + + _clientService.RequestQuerySave.Save(new() + { + Route = parent.Route, + RouteParameter = parent.RouteParameter, + Parameters = parent.Parameters, + Response = result + }, database); + + return result; + } + + #endregion + + #region RecommendedParts + + public async Task RecommendedProducts(string digikeyPartNumber, T3 database, int recordCount = 1, string[]? searchOptionList = null, bool excludeMarketPlaceProducts = false, string[]? includes = null, DateTime? afterDate = null) + { + var otherParameters = new Dictionary + { + { "recordCount", recordCount.ToString() } + }; + + if (searchOptionList != null) + otherParameters.Add("searchOptionList", HttpUtility.UrlEncode(string.Join(",", searchOptionList))); + + if (excludeMarketPlaceProducts == true) + otherParameters.Add("excludeMarketPlaceProducts", "true"); + + var parent = new RecommendedProductsParent(digikeyPartNumber, otherParameters, includes); + + var existing = _clientService.RequestQuerySave.Query(parent.Route, parent.RouteParameter, database, afterDate); + if (existing != null) + return existing; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + string result = _clientService.GetServiceResponse(response).Result; + + _clientService.RequestQuerySave.Save(new() + { + Route = parent.Route, + RouteParameter = parent.RouteParameter, + Parameters = parent.Parameters, + Response = result + }, database); + + return result; + } + + #endregion + + #region PackageTypeByQuantity + + public async Task PackageByQuantity(string digikeyPartNumber, int requestedQuantity, T3 database, string? packagingPreference = null, string[]? includes = null, DateTime? afterDate = null) + { + var otherParameters = new Dictionary + { + { "requestedQuantity", requestedQuantity.ToString() } + }; + + if (packagingPreference != null) + otherParameters.Add("packagingPreference", packagingPreference); + + var parent = new PackageByQuantityParent(digikeyPartNumber, otherParameters, includes); + + var existing = _clientService.RequestQuerySave.Query(parent.Route, parent.RouteParameter, database, afterDate); + if (existing != null) + return existing; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + string result = _clientService.GetServiceResponse(response).Result; + + _clientService.RequestQuerySave.Save(new() + { + Route = parent.Route, + RouteParameter = parent.RouteParameter, + Parameters = parent.Parameters, + Response = result + }, database); + + return result; + } + + #endregion + + #region ProductChangeNotifications + + public async Task ProductChangeNotifications(string digikeyPartNumber, T3 database, string[]? includes = null, DateTime? afterDate = null) + { + var parent = new ProductChangeNotificationsParent(digikeyPartNumber, includes); + + var existing = _clientService.RequestQuerySave.Query(parent.Route, parent.RouteParameter, database, afterDate); + if (existing != null) + return existing; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + string result = _clientService.GetServiceResponse(response).Result; + + _clientService.RequestQuerySave.Save(new() + { + Route = parent.Route, + RouteParameter = parent.RouteParameter, + Parameters = parent.Parameters, + Response = result + }, database); + + return result; + } + + #endregion + + #region ProductTracing + + public async Task ProductTracingDetails(string tracingID, T3 database, DateTime? afterDate = null) + { + var parent = new ProductTracingDetailsParent(tracingID); + + var existing = _clientService.RequestQuerySave.Query(parent.Route, parent.RouteParameter, database, afterDate); + if (existing != null) + return existing; + + await _clientService.ResetExpiredAccessTokenIfNeeded(); + var response = await _clientService.GetAsync(parent.Path); + string result = _clientService.GetServiceResponse(response).Result; + + _clientService.RequestQuerySave.Save(new() + { + Route = parent.Route, + RouteParameter = parent.RouteParameter, + Parameters = parent.Parameters, + Response = result + }, database); + + return result; + } + + #endregion + } +} diff --git a/ApiClient/API/ProductInformation/ProductInformationJson.cs b/ApiClient/API/ProductInformation/ProductInformationJson.cs new file mode 100644 index 0000000..a69f80b --- /dev/null +++ b/ApiClient/API/ProductInformation/ProductInformationJson.cs @@ -0,0 +1,171 @@ +//----------------------------------------------------------------------- +// +// THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTIES OF ANY KIND, EXPRESS, IMPLIED, STATUTORY, +// OR OTHERWISE. EXPECT TO THE EXTENT PROHIBITED BY APPLICABLE LAW, DIGI-KEY DISCLAIMS ALL WARRANTIES, +// INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, +// SATISFACTORY QUALITY, TITLE, NON-INFRINGEMENT, QUIET ENJOYMENT, +// AND WARRANTIES ARISING OUT OF ANY COURSE OF DEALING OR USAGE OF TRADE. +// +// DIGI-KEY DOES NOT WARRANT THAT THE SOFTWARE WILL FUNCTION AS DESCRIBED, +// WILL BE UNINTERRUPTED OR ERROR-FREE, OR FREE OF HARMFUL COMPONENTS. +// +//----------------------------------------------------------------------- + +using System.Text.Json; + +namespace ApiClient.API +{ + public partial class ProductInformation + { + #region PartSearch + + public async Task KeywordSearchJson(string keyword, string[]? includes = null) + { + return JsonSerializer.Deserialize(await KeywordSearch(keyword, includes)); + } + + public async Task ProductDetailsJson(string digikeyPartNumber, string[]? includes = null) + { + return JsonSerializer.Deserialize(await ProductDetails(digikeyPartNumber, includes)); + } + + public async Task DigiReelPricingJson(string digikeyPartNumber, int requestedQuantity, string[]? includes = null) + { + return JsonSerializer.Deserialize(await DigiReelPricing(digikeyPartNumber, requestedQuantity, includes)); + } + + public async Task SuggestedPartsJson(string partNumber) + { + return JsonSerializer.Deserialize(await SuggestedParts(partNumber)); + } + public async Task ManufacturersJson() + { + return JsonSerializer.Deserialize(await Manufacturers()); + } + + public async Task CategoriesJson() + { + return JsonSerializer.Deserialize(await Categories()); + } + + public async Task CategoriesByIDJson(int categoryID) + { + return JsonSerializer.Deserialize(await CategoriesByID(categoryID)); + } + + #endregion + + #region RecommendedParts + + public async Task RecommendedProductsJson(string digikeyPartNumber, int recordCount = 1, string[]? searchOptionList = null, bool excludeMarketPlaceProducts = false, string[]? includes = null) + { + return JsonSerializer.Deserialize(await RecommendedProducts(digikeyPartNumber, recordCount, searchOptionList, excludeMarketPlaceProducts, includes)); + } + + #endregion + + #region PackageTypeByQuantity + + public async Task PackageByQuantityJson(string digikeyPartNumber, int requestedQuantity, string? packagingPreference = null, string[]? includes = null) + { + return JsonSerializer.Deserialize(await PackageByQuantity(digikeyPartNumber, requestedQuantity, packagingPreference, includes)); + } + + #endregion + + #region ProductChangeNotifications + + public async Task ProductChangeNotificationsJson(string digikeyPartNumber, string[]? includes = null) + { + return JsonSerializer.Deserialize(await ProductChangeNotifications(digikeyPartNumber, includes)); + } + + #endregion + + #region ProductTracing + + public async Task ProductTracingDetailsJson(string tracingID) + { + return JsonSerializer.Deserialize(await ProductTracingDetails(tracingID)); + } + + #endregion + } + + public partial class ProductInformation + { + #region PartSearch + + public async Task KeywordSearchJson(string keyword, T3 database, string[]? includes = null) + { + return JsonSerializer.Deserialize(await KeywordSearch(keyword, database, includes)); + } + + public async Task ProductDetailsJson(string digikeyPartNumber, T3 database, string[]? includes = null) + { + return JsonSerializer.Deserialize(await ProductDetails(digikeyPartNumber, database, includes)); + } + + public async Task DigiReelPricingJson(string digikeyPartNumber, int requestedQuantity, T3 database, string[]? includes = null) + { + return JsonSerializer.Deserialize(await DigiReelPricing(digikeyPartNumber, requestedQuantity, database, includes)); + } + + public async Task SuggestedPartsJson(string partNumber, T3 database) + { + return JsonSerializer.Deserialize(await SuggestedParts(partNumber, database)); + } + public async Task ManufacturersJson(T3 database) + { + return JsonSerializer.Deserialize(await Manufacturers(database)); + } + + public async Task CategoriesJson(T3 database) + { + return JsonSerializer.Deserialize(await Categories(database)); + } + + public async Task CategoriesByIDJson(int categoryID, T3 database) + { + return JsonSerializer.Deserialize(await CategoriesByID(categoryID, database)); + } + + #endregion + + #region RecommendedParts + + public async Task RecommendedProductsJson(string digikeyPartNumber, T3 database, int recordCount = 1, string[]? searchOptionList = null, bool excludeMarketPlaceProducts = false, string[]? includes = null) + { + return JsonSerializer.Deserialize(await RecommendedProducts(digikeyPartNumber, database, recordCount, searchOptionList, excludeMarketPlaceProducts, includes)); + } + + #endregion + + #region PackageTypeByQuantity + + public async Task PackageByQuantityJson(string digikeyPartNumber, int requestedQuantity, T3 database, string? packagingPreference = null, string[]? includes = null) + { + return JsonSerializer.Deserialize(await PackageByQuantity(digikeyPartNumber, requestedQuantity, database, packagingPreference, includes)); + } + + #endregion + + #region ProductChangeNotifications + + public async Task ProductChangeNotificationsJson(string digikeyPartNumber, T3 database, string[]? includes = null) + { + return JsonSerializer.Deserialize(await ProductChangeNotifications(digikeyPartNumber, database, includes)); + } + + #endregion + + #region ProductTracing + + public async Task ProductTracingDetailsJson(string tracingID, T3 database) + { + return JsonSerializer.Deserialize(await ProductTracingDetails(tracingID, database)); + } + + #endregion + } +} \ No newline at end of file diff --git a/ApiClient/API/ProductInformation/ProductInformationParent.cs b/ApiClient/API/ProductInformation/ProductInformationParent.cs new file mode 100644 index 0000000..7d9ed99 --- /dev/null +++ b/ApiClient/API/ProductInformation/ProductInformationParent.cs @@ -0,0 +1,166 @@ +//----------------------------------------------------------------------- +// +// THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTIES OF ANY KIND, EXPRESS, IMPLIED, STATUTORY, +// OR OTHERWISE. EXPECT TO THE EXTENT PROHIBITED BY APPLICABLE LAW, DIGI-KEY DISCLAIMS ALL WARRANTIES, +// INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, +// SATISFACTORY QUALITY, TITLE, NON-INFRINGEMENT, QUIET ENJOYMENT, +// AND WARRANTIES ARISING OUT OF ANY COURSE OF DEALING OR USAGE OF TRADE. +// +// DIGI-KEY DOES NOT WARRANT THAT THE SOFTWARE WILL FUNCTION AS DESCRIBED, +// WILL BE UNINTERRUPTED OR ERROR-FREE, OR FREE OF HARMFUL COMPONENTS. +// +//----------------------------------------------------------------------- + +using ApiClient.Models; +using System.Web; + +namespace ApiClient.API +{ + public class ProductInformationParent + { + public class ResourcePathParent(string? arg = null, string? route = null, string? resourcePath = null, string? resourcePathSuffix = null, string[]? includes = null, Dictionary? otherParameters = null) + { + public string Route = route ?? string.Empty; + public string RouteParameter = arg ?? string.Empty; + public string ResourcePath = resourcePath ?? string.Empty; + public string ResourcePathSuffix = resourcePathSuffix ?? string.Empty; + public string Endpoint = arg ?? string.Empty; + internal string[]? _includes = includes; + internal Dictionary _otherParameters = otherParameters ?? []; + + public string Encoded = arg == null ? string.Empty : HttpUtility.UrlEncode(arg); + + public string Path + { + get + { + var prefix = ResourcePath == string.Empty ? string.Empty : '/' + ResourcePath; + var endpoint = Endpoint == string.Empty ? string.Empty : '/' + Endpoint; + var suffix = ResourcePathSuffix == string.Empty ? string.Empty : '/' + ResourcePathSuffix; + var parameters = Parameters == string.Empty ? string.Empty : '?' + Parameters; + return $"{prefix}{endpoint}{suffix}{parameters}"; + } + } + + public string Parameters + { + get + { + var parameters = string.Empty; + + foreach (KeyValuePair entry in _otherParameters) + { + parameters += (parameters == null ? string.Empty : '&') + $"{HttpUtility.UrlEncode(entry.Key)}={HttpUtility.UrlEncode(entry.Value)}"; + } + + if (_includes != null) + { + var includesString = HttpUtility.UrlEncode(string.Join(",", _includes)); + parameters += (parameters == null ? string.Empty : '&') + $"includes={includesString}"; + } + return parameters; + } + } + + public RequestSnapshot Snapshot(string result) + { + return new() + { + Route = Route, + RouteParameter = RouteParameter, + Parameters = Parameters, + Response = result + }; + } + } + + public class KeywordSearchParent(string keyword, string[]? includes = null) : ResourcePathParent( + keyword, + route: "KeywordSearch", + resourcePath: "Search/v3/Products/Keyword", + includes: includes) + { + new public string Path = "Search/v3/Products/Keyword"; + + public KeywordSearchRequest KeywordSearchRequest + { + get + { + return new() + { + Keywords = RouteParameter ?? "P5555-ND", + RecordCount = 25 + }; + } + } + } + + public class ProductDetailsParent(string digikeyPartNumber, string[]? includes = null) : ResourcePathParent( + digikeyPartNumber, + route: "ProductDetails", + resourcePath: "Search/v3/Products", + includes: includes) + { } + + public class DigiReelPricingParent(string digikeyPartNumber, Dictionary? otherParameters = null, string[]? includes = null) : ResourcePathParent( + digikeyPartNumber, + route: "DigiReelPricing", + resourcePath: "Search/v3/Products", + resourcePathSuffix: "DigiReelPricing", + includes: includes, + otherParameters: otherParameters) + { } + + public class SuggestedPartsParent(string partNumber) : ResourcePathParent( + partNumber, + route: "SuggestedParts", + resourcePath: "Search/v3/Products", + resourcePathSuffix: "WithSuggestedProducts") + { } + + public class ManufacturersParent() : ResourcePathParent( + route: "Manufacturers", + resourcePath: "Search/v3/Manufacturers") + { } + + public class CategoriesParent() : ResourcePathParent( + route: "Categories", + resourcePath: "Search/v3/Categories") + { } + + public class CategoriesByIDParent(int categoryID) : ResourcePathParent( + categoryID.ToString(), + route: "CategoriesByID", + resourcePath: "Search/v3/Categories") + { } + + public class RecommendedProductsParent(string digikeyPartNumber, Dictionary? otherParameters = null, string[]? includes = null) : ResourcePathParent( + digikeyPartNumber, + route: "RecommendedProducts", + resourcePath: "Recommendations/v3/Products", + includes: includes, + otherParameters: otherParameters) + { } + + public class PackageByQuantityParent(string digikeyPartNumber, Dictionary? otherParameters = null, string[]? includes = null) : ResourcePathParent( + digikeyPartNumber, + route: "PackageByQuantity", + resourcePath: "PackageTypeByQuantity/v3/Products", + includes: includes, + otherParameters: otherParameters) + { } + + public class ProductChangeNotificationsParent(string digikeyPartNumber, string[]? includes = null) : ResourcePathParent( + digikeyPartNumber, + route: "ProductChangeNotifications", + resourcePath: "ChangeNotifications/v3/Products", + includes: includes) + { } + + public class ProductTracingDetailsParent(string tracingID) : ResourcePathParent( + tracingID, + route: "ProductTracingDetails", + resourcePath: "ProductTracing/v1/Details") + { } + } +} diff --git a/ApiClient/API/ProductInformationJson.cs b/ApiClient/API/ProductInformationJson.cs deleted file mode 100644 index 0d5f018..0000000 --- a/ApiClient/API/ProductInformationJson.cs +++ /dev/null @@ -1,94 +0,0 @@ -//----------------------------------------------------------------------- -// -// THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTIES OF ANY KIND, EXPRESS, IMPLIED, STATUTORY, -// OR OTHERWISE. EXPECT TO THE EXTENT PROHIBITED BY APPLICABLE LAW, DIGI-KEY DISCLAIMS ALL WARRANTIES, -// INCLUDING, WITHOUT LIMITATION, ANY IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, -// SATISFACTORY QUALITY, TITLE, NON-INFRINGEMENT, QUIET ENJOYMENT, -// AND WARRANTIES ARISING OUT OF ANY COURSE OF DEALING OR USAGE OF TRADE. -// -// DIGI-KEY DOES NOT WARRANT THAT THE SOFTWARE WILL FUNCTION AS DESCRIBED, -// WILL BE UNINTERRUPTED OR ERROR-FREE, OR FREE OF HARMFUL COMPONENTS. -// -//----------------------------------------------------------------------- - -using System.Text.Json; - -namespace ApiClient.API -{ - public partial class ProductInformation - { - #region PartSearch - - public async Task KeywordSearchJson(string keyword, string[]? includes = null, DateTime? afterDate = null) - { - return JsonSerializer.Deserialize(await KeywordSearch(keyword, includes, afterDate)); - } - - public async Task ProductDetailsJson(string digikeyPartNumber, string[]? includes = null, DateTime? afterDate = null) - { - return JsonSerializer.Deserialize(await ProductDetails(digikeyPartNumber, includes, afterDate)); - } - - public async Task DigiReelPricingJson(string digikeyPartNumber, int requestedQuantity, string[]? includes = null, DateTime? afterDate = null) - { - return JsonSerializer.Deserialize(await DigiReelPricing(digikeyPartNumber, requestedQuantity, includes, afterDate)); - } - - public async Task SuggestedPartsJson(string partNumber, DateTime? afterDate = null) - { - return JsonSerializer.Deserialize(await SuggestedParts(partNumber, afterDate)); - } - public async Task ManufacturersJson(DateTime? afterDate = null) - { - return JsonSerializer.Deserialize(await Manufacturers(afterDate)); - } - - public async Task CategoriesJson(DateTime? afterDate = null) - { - return JsonSerializer.Deserialize(await Categories(afterDate)); - } - - public async Task CategoriesByIDJson(int categoryID, DateTime? afterDate = null) - { - return JsonSerializer.Deserialize(await CategoriesByID(categoryID, afterDate)); - } - - #endregion - - #region RecommendedParts - - public async Task RecommendedProductsJson(string digikeyPartNumber, int recordCount = 1, string[]? searchOptionList = null, bool excludeMarketPlaceProducts = false, string[]? includes = null, DateTime? afterDate = null) - { - return JsonSerializer.Deserialize(await RecommendedProducts(digikeyPartNumber, recordCount, searchOptionList, excludeMarketPlaceProducts, includes, afterDate)); - } - - #endregion - - #region PackageTypeByQuantity - - public async Task PackageByQuantityJson(string digikeyPartNumber, int requestedQuantity, string? packagingPreference = null, string[]? includes = null, DateTime? afterDate = null) - { - return JsonSerializer.Deserialize(await PackageByQuantity(digikeyPartNumber, requestedQuantity, packagingPreference, includes, afterDate)); - } - - #endregion - - #region ProductChangeNotifications - - public async Task ProductChangeNotificationsJson(string digikeyPartNumber, string[]? includes = null, DateTime? afterDate = null) - { - return JsonSerializer.Deserialize(await ProductChangeNotifications(digikeyPartNumber, includes, afterDate)); - } - - #endregion - - #region ProductTracing - - public async Task ProductTracingDetailsJson(string tracingID, DateTime? afterDate = null) - { - return JsonSerializer.Deserialize(await ProductTracingDetails(tracingID, afterDate)); - } - - #endregion - } -} \ No newline at end of file diff --git a/ApiClient/ApiClientService.cs b/ApiClient/ApiClientService.cs index 22c434a..f5bd9d7 100644 --- a/ApiClient/ApiClientService.cs +++ b/ApiClient/ApiClientService.cs @@ -23,18 +23,13 @@ namespace ApiClient { - public class ApiClientService + public class ApiClientServiceParent { - private const string CustomHeader = "Api-StaleTokenRetry"; + internal const string CustomHeader = "Api-StaleTokenRetry"; - private ApiClientSettings _clientSettings; - private readonly ILogger _logger; - - public readonly ISaveRequest SaveRequest; - public ProductInformation ProductInformation { get; private set; } - public DateTime AfterDate = DateTime.MinValue; - - public readonly IQueryable ExistingRequests; + internal ApiClientSettings _clientSettings; + internal readonly ILogger _logger; + internal DateTime AfterDate = DateTime.MinValue; public ApiClientSettings ClientSettings { @@ -47,23 +42,18 @@ public ApiClientSettings ClientSettings /// public HttpClient HttpClient { get; private set; } - public ApiClientService(ApiClientSettings clientSettings, ILogger? logger = null, ISaveRequest? saveRequest = null, IQueryable? existingRequests = null, DateTime? afterDate = null) + public ApiClientServiceParent(ApiClientSettings clientSettings, ILogger? logger = null, DateTime? afterDate = null) { - ExistingRequests = existingRequests ?? Enumerable.Empty().AsQueryable(); if (afterDate != null) AfterDate = (DateTime)afterDate; - SaveRequest = saveRequest ?? new DefaultSaveRequest(); _logger = logger ?? ConsoleLogger.Create(); _clientSettings = clientSettings ?? throw new ArgumentNullException(nameof(clientSettings)); - ProductInformation = new(this); - ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12; HttpClient = new() { BaseAddress = DigiKeyUriConstants.BaseAddress }; HttpClient.DefaultRequestHeaders.Authorization = new("Bearer", ClientSettings.AccessToken); HttpClient.DefaultRequestHeaders.Add("X-Digikey-Client-Id", ClientSettings.ClientId); HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); } - public async Task ResetExpiredAccessTokenIfNeeded() { if (_clientSettings.ExpirationDateTime < DateTime.Now) @@ -81,7 +71,7 @@ public async Task ResetExpiredAccessTokenIfNeeded() // Update the clientSettings _clientSettings.UpdateAndSave(oAuth2AccessToken); _logger?.LogInformation("ApiClientService::CheckifAccessTokenIsExpired() call to refresh"); - _logger?.LogInformation(_clientSettings.ToString()); + _logger?.LogInformation("{clientsettings}", _clientSettings.ToString()); // Reset the Authorization header value with the new access token. var authenticationHeaderValue = new AuthenticationHeaderValue("Bearer", _clientSettings.AccessToken); @@ -178,10 +168,29 @@ public async Task GetServiceResponse(HttpResponseMessage response) _logger?.LogInformation(" r.Route == route && r.RouteParameter == routeParameter && r.DateTime > afterDate).OrderByDescending(r => r.DateTime).FirstOrDefault(); - return snapshot?.Response; + ProductInformation = new(this); + } + } + + public class ApiClientService : ApiClientServiceParent + { + + public readonly IRequestQuerySave RequestQuerySave; + public ProductInformation ProductInformation { get; private set; } + + public ApiClientService(ApiClientSettings clientSettings, IRequestQuerySave requestQuerySave, ILogger? logger = null, DateTime? afterDate = null) : base(clientSettings, logger, afterDate) + { + RequestQuerySave = requestQuerySave; + + ProductInformation = new(this); } } } diff --git a/ApiClient/ConsoleLogger.cs b/ApiClient/ConsoleLogger.cs index c12416a..1492cf0 100644 --- a/ApiClient/ConsoleLogger.cs +++ b/ApiClient/ConsoleLogger.cs @@ -28,7 +28,7 @@ public static ILogger Create() .AddConsole(); }); ILogger logger = loggerFactory.CreateLogger(); - + Console.WriteLine("ApiClient Logger Created!"); return logger; } } diff --git a/ApiClient/GlobalSuppressions.cs b/ApiClient/GlobalSuppressions.cs index e4cdc19..420d211 100644 --- a/ApiClient/GlobalSuppressions.cs +++ b/ApiClient/GlobalSuppressions.cs @@ -15,7 +15,3 @@ // attributes that are applied to this project. // Project-level suppressions either have no target or are given // a specific target and scoped to a namespace, type, member, etc. - -using System.Diagnostics.CodeAnalysis; - -[assembly: SuppressMessage("Usage", "CA2254:Template should be a static expression", Justification = "", Scope = "member", Target = "~M:ApiClient.ApiClientService.ResetExpiredAccessTokenIfNeeded~System.Threading.Tasks.Task")] diff --git a/ApiClient/Models/RequestSnapshot.cs b/ApiClient/Models/RequestSnapshot.cs index 593b30f..df87a28 100644 --- a/ApiClient/Models/RequestSnapshot.cs +++ b/ApiClient/Models/RequestSnapshot.cs @@ -13,14 +13,15 @@ namespace ApiClient.Models { - public class DefaultSaveRequest : ISaveRequest + public interface IRequestQuerySave { - public void Save(RequestSnapshot requestSnapshot) { } - } + public void Save(RequestSnapshot requestSnapshot, T database); - public interface ISaveRequest - { - public void Save(RequestSnapshot requestSnapshot); + public T1? Convert(RequestSnapshot requestSnapshot); + + public string? Query(string route, string routeParameter, T database, DateTime? afterDate); + + public IQueryable RequestSnapshots(IQueryable? table = null); } public class RequestSnapshot From d5d8ab88805d90d7c36af04aedc83c0ac12d8430 Mon Sep 17 00:00:00 2001 From: Jack Burch Date: Mon, 18 Dec 2023 21:35:18 -0500 Subject: [PATCH 14/18] Update Readme.md --- Readme.md | 97 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 58 insertions(+), 39 deletions(-) diff --git a/Readme.md b/Readme.md index 2a90254..0bed123 100644 --- a/Readme.md +++ b/Readme.md @@ -28,7 +28,7 @@ Console.WriteLine("response is {0}", postResponse); * **ApiClient.ConsoleApp** - Console app to test out programmatic refresh of access token when needed and also check if access token failed to work and then refresh and try again. * **OAuth2Service.ConsoleApp** - Console app to create the initial access token and refresh token. -### Getting Started +### Getting Started 1. Clone the repository or download and extract the zip file containing the ApiClient solution. 2. You will need to register an application on the [DigiKey Developer Portal](https://developer.digikey.com/) in order to create your unique Client ID, Client Secret as well as to set your redirection URI. @@ -52,53 +52,72 @@ Console.WriteLine("response is {0}", postResponse); ### Advanced Usage -The library supports using external data sources to store previous requests and prevent sending duplicate requests by checking the external data source. Checking for existing requests can be done by mapping the data source object to a new RequestSnapshot object. Saving requests can be done by implementing the ISaveRequest interface. +The library supports using external data sources to store previous requests and prevent sending duplicate requests by checking the external data source. +Checking for existing requests and saving requests can be done by implementing the generic IRequestQuerySave interface within your application. When implementing the `IRequestQuerySave` interface, T3 is meant to be your DbContext, and T4 is meant to be the EF Core model for the specific table you are storing the requests in. The intended functions of the generic interface are as shown: + +* **Convert**: is meant to convert a `RequestSnapshot` object to the native object your table uses. +* **RequestSnapshots**: does the opposite, and is intended to convert from your EF Core table to a `IQueryable` using a LINQ select statement. +* **Query**: should use `RequestSnapshots` and find any matching results for the route and routeParameter. +* **Save** should use the Convert method to create a new EF Core object and insert it into your table & save. + +Here is a sample implementation of the IRequestQuerySave inteface: ```csharp -public class DigiKeyHelperService +public class RequestQuerySave : IRequestQuerySave +{ + public void Save(RequestSnapshot requestSnapshot, DbContext database) { - private readonly ApiClientSettings _clientSettings; - private readonly ApiClientService _clientService; - private readonly EFCoreContext _efCoreContext; - private readonly SaveRequest _saveRequest; - + database.DigikeyAPIRequests.Add(Convert(requestSnapshot)); + database.SaveChanges(); + } - public DigiKeyHelperService(EFCoreContext efCoreContext) + public DigikeyAPIRequest Convert(RequestSnapshot requestSnapshot) + { + return new() { - _efCoreContext = efCoreContext; - _saveRequest = new(_efCoreContext); - _clientSettings = ApiClientSettings.CreateFromConfigFile(); - _clientService = new(_clientSettings, - saveRequest: _saveRequest, - existingRequests: _efCoreContext.DigikeyAPIRequests.Select(x => new RequestSnapshot() - { - RequestID = x.RequestID, - Route = x.Route, - RouteParameter = x.RouteParameter, - Response = x.Response, - DateTime = x.DateTime - })); - } - - public class SaveRequest(EFCoreContext efCoreContext) : ISaveRequest + Route = requestSnapshot.Route, + RouteParameter = requestSnapshot.RouteParameter, + Response = requestSnapshot.Response + }; + } + + public string? Query(string route, string routeParameter, DbContext database, DateTime? afterDate = null) + { + afterDate ??= DateTime.MinValue; + var snapshot = RequestSnapshots(database.DigikeyAPIRequests) + .Where(r => + r.Route == route + && r.RouteParameter == routeParameter + && r.DateTime > afterDate) + .OrderByDescending( + r => r.DateTime) + .FirstOrDefault(); + return snapshot?.Response; + } + + public IQueryable RequestSnapshots(IQueryable? table) + { + return table == null ? Enumerable.Empty().AsQueryable() : table.Select(x => new RequestSnapshot() { - private readonly EFCoreContext _efCoreContext = efCoreContext; - - public void Save(RequestSnapshot requestSnapshot) - { - _efCoreContext.DigikeyAPIRequests.Add(new() - { - Route = requestSnapshot.Route, - RouteParameter = requestSnapshot.RouteParameter, - Response = requestSnapshot.Response - }); - _efCoreContext.SaveChanges(); - } - } + RequestID = x.RequestID, + Route = x.Route, + RouteParameter = x.RouteParameter, + Response = x.Response, + DateTime = x.DateTime + }); + } } ``` +An instance of this is then passed to the constructor of the ApiClientService: + +```csharp +var requestQuerySave = new RequestQuerySave(); +var clientSettings = ApiClientSettings.CreateFromConfigFile(); +ApiClientService clientService = new(clientSettings, requestQuerySave); +``` + ### Previous Request Cutoffs The age of previous requests to consider as recent enough can be globably set, or set on a request to request basis. @@ -107,7 +126,7 @@ The age of previous requests to consider as recent enough can be globably set, o ```csharp DateTime cutoffDateTime = DateTime.Today.AddDays(-30); -_clientService = new(_clientSettings, afterDate: cutoffDateTime); +_clientService = new(_clientSettings, _requestQuerySave, afterDate: cutoffDateTime); ``` #### Single Request Time Cutoff From 26b37a0b4b4c00d7bf0254b85286566d1a3c9604 Mon Sep 17 00:00:00 2001 From: JackMBurch Date: Tue, 26 Dec 2023 19:22:56 -0500 Subject: [PATCH 15/18] Dealing with "The configuration file has been changed by another program." ConfigurationErrorsException when the file hasn't been changed --- .../Configuration/ApiClientConfigHelper.cs | 30 ++++++++++++------- .../Core/Configuration/ConfigurationHelper.cs | 12 +++++++- ApiClient/Models/ApiClientSettings.cs | 26 ++++++++-------- 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/ApiClient/Core/Configuration/ApiClientConfigHelper.cs b/ApiClient/Core/Configuration/ApiClientConfigHelper.cs index 8efd527..52c5957 100644 --- a/ApiClient/Core/Configuration/ApiClientConfigHelper.cs +++ b/ApiClient/Core/Configuration/ApiClientConfigHelper.cs @@ -25,6 +25,9 @@ public class ApiClientConfigHelper : ConfigurationHelper, IApiClientConfigHelper // immediately when class is loaded for the first time. // .NET guarantees thread safety for static initialization private static readonly ApiClientConfigHelper _thisInstance = new(); + private static readonly ExeConfigurationFileMap _map = GetMap(); + + public static ExeConfigurationFileMap Map { get => _map; } private const string _ClientId = "ApiClient.ClientId"; private const string _ClientSecret = "ApiClient.ClientSecret"; @@ -37,15 +40,8 @@ private ApiClientConfigHelper() { try { - var environmentPath = Environment.GetEnvironmentVariable("APICLIENT_CONFIG_PATH"); - var filePath = environmentPath ?? FindSolutionDir(); - - var map = new ExeConfigurationFileMap - { - ExeConfigFilename = filePath - }; - Console.WriteLine($"map.ExeConfigFilename {map.ExeConfigFilename}"); - _config = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None); + Console.WriteLine($"map.ExeConfigFilename {_map.ExeConfigFilename}"); + _config = ConfigurationManager.OpenMappedExeConfiguration(_map, ConfigurationUserLevel.None); } catch (System.Exception ex) { @@ -53,6 +49,18 @@ private ApiClientConfigHelper() } } + private static ExeConfigurationFileMap GetMap() + { + var environmentPath = Environment.GetEnvironmentVariable("APICLIENT_CONFIG_PATH"); + var filePath = environmentPath ?? FindSolutionDir(); + + var map = new ExeConfigurationFileMap + { + ExeConfigFilename = filePath + }; + return map; + } + private static string FindSolutionDir() { string regexPattern = @"^(.*)(\\bin\\)(.*)$"; @@ -74,9 +82,9 @@ private static string FindSolutionDir() return Path.Combine(solutionDir.FullName, "apiclient.config"); } - public static ApiClientConfigHelper Instance() + public static ApiClientConfigHelper Instance { - return _thisInstance; + get => _thisInstance; } /// diff --git a/ApiClient/Core/Configuration/ConfigurationHelper.cs b/ApiClient/Core/Configuration/ConfigurationHelper.cs index 5dfc2d9..9bd210a 100644 --- a/ApiClient/Core/Configuration/ConfigurationHelper.cs +++ b/ApiClient/Core/Configuration/ConfigurationHelper.cs @@ -82,7 +82,17 @@ public bool GetBooleanAttribute(string attrName) /// public void Save() { - _config?.Save(ConfigurationSaveMode.Full); + try + { + _config?.Save(ConfigurationSaveMode.Modified); + } + catch (ConfigurationException cee) + { + if (cee.Message != "The configuration file has been changed by another program.") + throw; + + _config = ConfigurationManager.OpenMappedExeConfiguration(ApiClientConfigHelper.Map, ConfigurationUserLevel.None); + } ConfigurationManager.RefreshSection("appSettings"); } diff --git a/ApiClient/Models/ApiClientSettings.cs b/ApiClient/Models/ApiClientSettings.cs index 0e5e42e..9a9672e 100644 --- a/ApiClient/Models/ApiClientSettings.cs +++ b/ApiClient/Models/ApiClientSettings.cs @@ -29,25 +29,25 @@ public class ApiClientSettings public void Save() { - ApiClientConfigHelper.Instance().ClientId = ClientId ?? string.Empty; - ApiClientConfigHelper.Instance().ClientSecret = ClientSecret; - ApiClientConfigHelper.Instance().RedirectUri = RedirectUri; - ApiClientConfigHelper.Instance().AccessToken = AccessToken; - ApiClientConfigHelper.Instance().RefreshToken = RefreshToken; - ApiClientConfigHelper.Instance().ExpirationDateTime = ExpirationDateTime; - ApiClientConfigHelper.Instance().Save(); + ApiClientConfigHelper.Instance.ClientId = ClientId; + ApiClientConfigHelper.Instance.ClientSecret = ClientSecret; + ApiClientConfigHelper.Instance.RedirectUri = RedirectUri; + ApiClientConfigHelper.Instance.AccessToken = AccessToken; + ApiClientConfigHelper.Instance.RefreshToken = RefreshToken; + ApiClientConfigHelper.Instance.ExpirationDateTime = ExpirationDateTime; + ApiClientConfigHelper.Instance.Save(); } public static ApiClientSettings CreateFromConfigFile() { return new ApiClientSettings() { - ClientId = ApiClientConfigHelper.Instance().ClientId, - ClientSecret = ApiClientConfigHelper.Instance().ClientSecret, - RedirectUri = ApiClientConfigHelper.Instance().RedirectUri, - AccessToken = ApiClientConfigHelper.Instance().AccessToken, - RefreshToken = ApiClientConfigHelper.Instance().RefreshToken, - ExpirationDateTime = ApiClientConfigHelper.Instance().ExpirationDateTime, + ClientId = ApiClientConfigHelper.Instance.ClientId, + ClientSecret = ApiClientConfigHelper.Instance.ClientSecret, + RedirectUri = ApiClientConfigHelper.Instance.RedirectUri, + AccessToken = ApiClientConfigHelper.Instance.AccessToken, + RefreshToken = ApiClientConfigHelper.Instance.RefreshToken, + ExpirationDateTime = ApiClientConfigHelper.Instance.ExpirationDateTime, }; } From b32d4c801a02cceeeff0b36a00123fde55b192d4 Mon Sep 17 00:00:00 2001 From: JackMBurch Date: Tue, 26 Dec 2023 21:54:45 -0500 Subject: [PATCH 16/18] Fixing catch --- ApiClient/Core/Configuration/ApiClientConfigHelper.cs | 2 +- ApiClient/Core/Configuration/ConfigurationHelper.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ApiClient/Core/Configuration/ApiClientConfigHelper.cs b/ApiClient/Core/Configuration/ApiClientConfigHelper.cs index 52c5957..22487da 100644 --- a/ApiClient/Core/Configuration/ApiClientConfigHelper.cs +++ b/ApiClient/Core/Configuration/ApiClientConfigHelper.cs @@ -24,8 +24,8 @@ public class ApiClientConfigHelper : ConfigurationHelper, IApiClientConfigHelper // Static members are 'eagerly initialized', that is, // immediately when class is loaded for the first time. // .NET guarantees thread safety for static initialization - private static readonly ApiClientConfigHelper _thisInstance = new(); private static readonly ExeConfigurationFileMap _map = GetMap(); + private static readonly ApiClientConfigHelper _thisInstance = new(); public static ExeConfigurationFileMap Map { get => _map; } diff --git a/ApiClient/Core/Configuration/ConfigurationHelper.cs b/ApiClient/Core/Configuration/ConfigurationHelper.cs index 9bd210a..a6e267d 100644 --- a/ApiClient/Core/Configuration/ConfigurationHelper.cs +++ b/ApiClient/Core/Configuration/ConfigurationHelper.cs @@ -86,9 +86,9 @@ public void Save() { _config?.Save(ConfigurationSaveMode.Modified); } - catch (ConfigurationException cee) + catch (ConfigurationErrorsException cee) { - if (cee.Message != "The configuration file has been changed by another program.") + if (!cee.Message.StartsWith("The configuration file has been changed by another program.")) throw; _config = ConfigurationManager.OpenMappedExeConfiguration(ApiClientConfigHelper.Map, ConfigurationUserLevel.None); From 1632cc4ff54dc3aa200172995bb86868e4a3ade1 Mon Sep 17 00:00:00 2001 From: JackMBurch Date: Wed, 3 Jan 2024 18:49:21 -0500 Subject: [PATCH 17/18] Authorization header corrected to "Bearer" instead of "Authorization" --- ApiClient/ApiClientService.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ApiClient/ApiClientService.cs b/ApiClient/ApiClientService.cs index f5bd9d7..3097418 100644 --- a/ApiClient/ApiClientService.cs +++ b/ApiClient/ApiClientService.cs @@ -102,7 +102,7 @@ public async Task GetAsync(string resourcePath) throw new ApiException($"Inside method {nameof(GetAsync)} we received an unexpected stale token response - during the retry for a call whose token we just refreshed {response.StatusCode}"); HttpClient.DefaultRequestHeaders.Add(CustomHeader, CustomHeader); - HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Authorization", _clientSettings.AccessToken); + HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _clientSettings.AccessToken); return await GetAsync(resourcePath); } @@ -134,7 +134,7 @@ public async Task PostAsJsonAsync(string resourcePath, T throw new ApiException($"Inside method {nameof(PostAsJsonAsync)} we received an unexpected stale token response - during the retry for a call whose token we just refreshed {response.StatusCode}"); HttpClient.DefaultRequestHeaders.Add(CustomHeader, CustomHeader); - HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Authorization", _clientSettings.AccessToken); + HttpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _clientSettings.AccessToken); return await PostAsJsonAsync(resourcePath, postRequest); } From 6300579d2e6469e9bf323132a8f1c6bc59584405 Mon Sep 17 00:00:00 2001 From: JackMBurch Date: Wed, 3 Jan 2024 19:00:28 -0500 Subject: [PATCH 18/18] Swapping from ConfigurationManager to XElement to avoid "The configuration file has been changed by another program." errors. --- ApiClient/ApiClient.csproj | 13 +++-- .../Configuration/ApiClientConfigHelper.cs | 19 +++---- .../Core/Configuration/ConfigurationHelper.cs | 53 +++++++++---------- .../Interfaces/IConfigurationHelper.cs | 5 -- 4 files changed, 43 insertions(+), 47 deletions(-) diff --git a/ApiClient/ApiClient.csproj b/ApiClient/ApiClient.csproj index 334a825..21dec4c 100644 --- a/ApiClient/ApiClient.csproj +++ b/ApiClient/ApiClient.csproj @@ -4,21 +4,28 @@ net8.0 enable enable + False - + + False + + + + False + + + - - \ No newline at end of file diff --git a/ApiClient/Core/Configuration/ApiClientConfigHelper.cs b/ApiClient/Core/Configuration/ApiClientConfigHelper.cs index 22487da..b9e65fe 100644 --- a/ApiClient/Core/Configuration/ApiClientConfigHelper.cs +++ b/ApiClient/Core/Configuration/ApiClientConfigHelper.cs @@ -13,9 +13,9 @@ using ApiClient.Core.Configuration.Interfaces; using ApiClient.Exception; -using System.Configuration; using System.Globalization; using System.Text.RegularExpressions; +using System.Xml.Linq; namespace ApiClient.Core.Configuration { @@ -24,10 +24,10 @@ public class ApiClientConfigHelper : ConfigurationHelper, IApiClientConfigHelper // Static members are 'eagerly initialized', that is, // immediately when class is loaded for the first time. // .NET guarantees thread safety for static initialization - private static readonly ExeConfigurationFileMap _map = GetMap(); + private static readonly string _configFile = GetConfigFilePath(); private static readonly ApiClientConfigHelper _thisInstance = new(); - public static ExeConfigurationFileMap Map { get => _map; } + public static string ConfigFile { get => _configFile; } private const string _ClientId = "ApiClient.ClientId"; private const string _ClientSecret = "ApiClient.ClientSecret"; @@ -40,8 +40,8 @@ private ApiClientConfigHelper() { try { - Console.WriteLine($"map.ExeConfigFilename {_map.ExeConfigFilename}"); - _config = ConfigurationManager.OpenMappedExeConfiguration(_map, ConfigurationUserLevel.None); + Console.WriteLine($"XML file: {_configFile}"); + _xconfig = XElement.Load(_configFile); } catch (System.Exception ex) { @@ -49,16 +49,11 @@ private ApiClientConfigHelper() } } - private static ExeConfigurationFileMap GetMap() + private static string GetConfigFilePath() { var environmentPath = Environment.GetEnvironmentVariable("APICLIENT_CONFIG_PATH"); var filePath = environmentPath ?? FindSolutionDir(); - - var map = new ExeConfigurationFileMap - { - ExeConfigFilename = filePath - }; - return map; + return filePath; } private static string FindSolutionDir() diff --git a/ApiClient/Core/Configuration/ConfigurationHelper.cs b/ApiClient/Core/Configuration/ConfigurationHelper.cs index a6e267d..9c6c05d 100644 --- a/ApiClient/Core/Configuration/ConfigurationHelper.cs +++ b/ApiClient/Core/Configuration/ConfigurationHelper.cs @@ -12,9 +12,8 @@ //----------------------------------------------------------------------- using ApiClient.Core.Configuration.Interfaces; -using System.Configuration; using System.Diagnostics.CodeAnalysis; -using ConfigurationManager = System.Configuration.ConfigurationManager; +using System.Xml.Linq; namespace ApiClient.Core.Configuration { @@ -27,7 +26,7 @@ public class ConfigurationHelper : IConfigurationHelper /// /// This object represents the config file /// - protected System.Configuration.Configuration? _config; + protected XElement? _xconfig; /// /// Updates the value for the specified key in the AppSettings of the Config file. @@ -36,10 +35,29 @@ public class ConfigurationHelper : IConfigurationHelper /// The value. public void Update(string key, string value) { - if (_config?.AppSettings.Settings[key] == null) - _config?.AppSettings.Settings.Add(key, value); + if (Setting(key) == null) + AppSettings?.Add(CreateElement(key, value)); else - _config.AppSettings.Settings[key].Value = value; + Value(Setting(key))!.SetValue(value); + } + + public XElement? AppSettings { get => _xconfig?.Descendants("appSettings")?.FirstOrDefault(); } + + public IEnumerable? Settings { get => AppSettings?.Descendants("add"); } + + public static XElement CreateElement(string key, string value) + { + return new XElement("add", new XAttribute("key", key), new XAttribute("value", value)); + } + + public XElement? Setting(string name) + { + return Settings?.Where(e => e.Attributes().Where(r => r.Name == "key" && r.Value == name).Any()).FirstOrDefault(); + } + + public static XAttribute? Value(XElement? setting) + { + return setting?.Attributes().Where(l => l.Name == "value").FirstOrDefault(); } /// @@ -51,7 +69,7 @@ public string GetAttribute(string attrName) { try { - return _config?.AppSettings.Settings[attrName]?.Value!; + return Value(Setting(attrName))?.Value!; } catch (System.Exception) { @@ -82,26 +100,7 @@ public bool GetBooleanAttribute(string attrName) /// public void Save() { - try - { - _config?.Save(ConfigurationSaveMode.Modified); - } - catch (ConfigurationErrorsException cee) - { - if (!cee.Message.StartsWith("The configuration file has been changed by another program.")) - throw; - - _config = ConfigurationManager.OpenMappedExeConfiguration(ApiClientConfigHelper.Map, ConfigurationUserLevel.None); - } - ConfigurationManager.RefreshSection("appSettings"); - } - - /// - /// Refreshes the application settingses. - /// - public void RefreshAppSettings() - { - ConfigurationManager.RefreshSection("appSettings"); + _xconfig!.Save(ApiClientConfigHelper.ConfigFile); } } } diff --git a/ApiClient/Core/Configuration/Interfaces/IConfigurationHelper.cs b/ApiClient/Core/Configuration/Interfaces/IConfigurationHelper.cs index 9a5dbfb..5be4138 100644 --- a/ApiClient/Core/Configuration/Interfaces/IConfigurationHelper.cs +++ b/ApiClient/Core/Configuration/Interfaces/IConfigurationHelper.cs @@ -43,10 +43,5 @@ public interface IConfigurationHelper /// Saves changes to the Config file /// void Save(); - - /// - /// Refreshes the application settingses. - /// - void RefreshAppSettings(); } }