diff --git a/.gitignore b/.gitignore index 934db37..499e2e1 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 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..c480cbc 100644 --- a/2Legged_OAuth2Service.ConsoleApp/Program.cs +++ b/2Legged_OAuth2Service.ConsoleApp/Program.cs @@ -1,35 +1,26 @@ -using System.Diagnostics; -using System.Net; -using System.Web; -using ApiClient.Extensions; +//----------------------------------------------------------------------- +// +// 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 ApiClient.OAuth2.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..."); - 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()); var oAuth2Service = new ApiClient.OAuth2.OAuth2Service(_clientSettings); @@ -37,7 +28,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); @@ -54,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/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/3Legged_OAuth2Service.ConsoleApp.csproj b/3Legged_OAuth2Service.ConsoleApp/3Legged_OAuth2Service.ConsoleApp.csproj index e4c8b96..381faab 100644 --- a/3Legged_OAuth2Service.ConsoleApp/3Legged_OAuth2Service.ConsoleApp.csproj +++ b/3Legged_OAuth2Service.ConsoleApp/3Legged_OAuth2Service.ConsoleApp.csproj @@ -2,21 +2,16 @@ Exe - net6.0 + net8.0 enable enable - - + + - - - - - - + diff --git a/3Legged_OAuth2Service.ConsoleApp/Program.cs b/3Legged_OAuth2Service.ConsoleApp/Program.cs index 848f7e2..f108994 100644 --- a/3Legged_OAuth2Service.ConsoleApp/Program.cs +++ b/3Legged_OAuth2Service.ConsoleApp/Program.cs @@ -11,39 +11,20 @@ // //----------------------------------------------------------------------- -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 { 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 @@ -59,13 +40,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 +58,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); @@ -105,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/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/ApiClient.ConsoleApp.csproj b/ApiClient.ConsoleApp/ApiClient.ConsoleApp.csproj index 1cb7a51..d683a9e 100644 --- a/ApiClient.ConsoleApp/ApiClient.ConsoleApp.csproj +++ b/ApiClient.ConsoleApp/ApiClient.ConsoleApp.csproj @@ -1,9 +1,10 @@  - net6.0 + net8.0 enable enable + Exe diff --git a/ApiClient.ConsoleApp/Program.cs b/ApiClient.ConsoleApp/Program.cs index 84ce89b..69f9702 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; @@ -22,53 +21,38 @@ namespace ApiClient.ConsoleApp public class Program { static async Task Main() - { - var prog = new Program(); - - await prog.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() { 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.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} "); + + // 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.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 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/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/ApiClient.csproj b/ApiClient/ApiClient.csproj index 9236d1d..21dec4c 100644 --- a/ApiClient/ApiClient.csproj +++ b/ApiClient/ApiClient.csproj @@ -1,21 +1,31 @@  - net6.0 + net8.0 enable enable + False - - - - - - + + False + + + + False + + + + + + - + + + + \ No newline at end of file diff --git a/ApiClient/ApiClientService.cs b/ApiClient/ApiClientService.cs index 7870301..3097418 100644 --- a/ApiClient/ApiClientService.cs +++ b/ApiClient/ApiClientService.cs @@ -11,25 +11,27 @@ // //----------------------------------------------------------------------- -using System.Net; -using System.Net.Http.Headers; -using System.Net.Http.Json; +using ApiClient.API; 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 { - public class ApiClientService + public class ApiClientServiceParent { - private const string CustomHeader = "Api-StaleTokenRetry"; + internal const string CustomHeader = "Api-StaleTokenRetry"; - private ApiClientSettings? _clientSettings; + internal ApiClientSettings _clientSettings; + internal readonly ILogger _logger; + internal DateTime AfterDate = DateTime.MinValue; - public ApiClientSettings? ClientSettings + public ApiClientSettings ClientSettings { get => _clientSettings; set => _clientSettings = value; @@ -40,26 +42,18 @@ public ApiClientSettings? ClientSettings /// public HttpClient HttpClient { get; private set; } - public ApiClientService(ApiClientSettings? clientSettings) + public ApiClientServiceParent(ApiClientSettings clientSettings, ILogger? logger = null, DateTime? afterDate = null) { - ClientSettings = clientSettings ?? throw new ArgumentNullException(nameof(clientSettings)); - Initialize(); - } - - private void Initialize() - { - HttpClient = new HttpClient(); + if (afterDate != null) AfterDate = (DateTime)afterDate; + _logger = logger ?? ConsoleLogger.Create(); + _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")); } - public async Task ResetExpiredAccessTokenIfNeeded() { if (_clientSettings.ExpirationDateTime < DateTime.Now) @@ -70,14 +64,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}", _clientSettings.ToString()); // Reset the Authorization header value with the new access token. var authenticationHeaderValue = new AuthenticationHeaderValue("Bearer", _clientSettings.AccessToken); @@ -85,74 +79,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("Bearer", _clientSettings.AccessToken); + + return await PostAsJsonAsync(resourcePath, postRequest); + } } + + return response; } - protected 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) @@ -165,20 +158,39 @@ protected async Task GetServiceResponse(HttpResponseMessage response) 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); - var resp = new HttpResponseMessage(response.StatusCode) - { - Content = response.Content, - ReasonPhrase = 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(" : 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 new file mode 100644 index 0000000..1492cf0 --- /dev/null +++ b/ApiClient/ConsoleLogger.cs @@ -0,0 +1,35 @@ +//----------------------------------------------------------------------- +// +// 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 +{ + 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(); + Console.WriteLine("ApiClient Logger Created!"); + return logger; + } + } +} diff --git a/ApiClient/Constants/DigiKeyUriConstants.cs b/ApiClient/Constants/DigiKeyUriConstants.cs index 6f161ef..a147d64 100644 --- a/ApiClient/Constants/DigiKeyUriConstants.cs +++ b/ApiClient/Constants/DigiKeyUriConstants.cs @@ -11,8 +11,6 @@ // //----------------------------------------------------------------------- -using System; - namespace ApiClient.Constants { /// @@ -20,19 +18,41 @@ namespace ApiClient.Constants /// public static class DigiKeyUriConstants { + 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; } + } + + public static Uri AuthorizationEndpoint + { + get { return GetProductionVariable() ? ProductionAuthorizationEndpoint : SandboxAuthorizationEndpoint; } + } + // 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"); - - // 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 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 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 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"); } } diff --git a/ApiClient/Core/Configuration/ApiClientConfigHelper.cs b/ApiClient/Core/Configuration/ApiClientConfigHelper.cs index ec41961..b9e65fe 100644 --- a/ApiClient/Core/Configuration/ApiClientConfigHelper.cs +++ b/ApiClient/Core/Configuration/ApiClientConfigHelper.cs @@ -11,14 +11,11 @@ // //----------------------------------------------------------------------- -using System; -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; +using System.Globalization; +using System.Text.RegularExpressions; +using System.Xml.Linq; namespace ApiClient.Core.Configuration { @@ -27,7 +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 ApiClientConfigHelper _thisInstance = new ApiClientConfigHelper(); + private static readonly string _configFile = GetConfigFilePath(); + private static readonly ApiClientConfigHelper _thisInstance = new(); + + public static string ConfigFile { get => _configFile; } private const string _ClientId = "ApiClient.ClientId"; private const string _ClientSecret = "ApiClient.ClientSecret"; @@ -36,42 +36,50 @@ public class ApiClientConfigHelper : ConfigurationHelper, IApiClientConfigHelper private const string _RefreshToken = "ApiClient.RefreshToken"; private const string _ExpirationDateTime = "ApiClient.ExpirationDateTime"; - private ApiClientConfigHelper() + private ApiClientConfigHelper() { try { - string regexPattern = @"^(.*)(\\bin\\)(.*)$"; + Console.WriteLine($"XML file: {_configFile}"); + _xconfig = XElement.Load(_configFile); + } + catch (System.Exception ex) + { + throw new ApiException($"Error in ApiClientConfigHelper on opening up apiclient.config {ex.Message}"); + } + } - // 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('\\'); + private static string GetConfigFilePath() + { + var environmentPath = Environment.GetEnvironmentVariable("APICLIENT_CONFIG_PATH"); + var filePath = environmentPath ?? FindSolutionDir(); + return filePath; + } - // 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 + private static string FindSolutionDir() + { + string regexPattern = @"^(.*)(\\bin\\)(.*)$"; - if (!File.Exists(Path.Combine(solutionDir.FullName, "apiclient.config"))) - { - throw new ApiException($"Unable to locate apiclient.config in solution folder {solutionDir.FullName}"); - } + // 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('\\'); - var map = new ExeConfigurationFileMap - { - ExeConfigFilename = Path.Combine(solutionDir.FullName, "apiclient.config"), - }; - Console.WriteLine($"map.ExeConfigFilename {map.ExeConfigFilename}"); - _config = ConfigurationManager.OpenMappedExeConfiguration(map, ConfigurationUserLevel.None); - } - catch (System.Exception ex) + // 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($"Error in ApiClientConfigHelper on opening up apiclient.config {ex.Message}"); + throw new ApiException($"Unable to locate apiclient.config in solution folder {solutionDir.FullName}"); } + + return Path.Combine(solutionDir.FullName, "apiclient.config"); } - public static ApiClientConfigHelper Instance() + public static ApiClientConfigHelper Instance { - return _thisInstance; + get => _thisInstance; } /// @@ -79,8 +87,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 +96,8 @@ public string ClientId /// public string ClientSecret { - get { return GetAttribute(_ClientSecret); } - set { Update(_ClientSecret, value); } + get => GetAttribute(_ClientSecret); + set => Update(_ClientSecret, value); } /// @@ -97,8 +105,8 @@ public string ClientSecret /// public string RedirectUri { - get { return GetAttribute(_RedirectUri); } - set { Update(_RedirectUri, value); } + get => GetAttribute(_RedirectUri); + set => Update(_RedirectUri, value); } /// @@ -106,8 +114,8 @@ public string RedirectUri /// public string AccessToken { - get { return GetAttribute(_AccessToken); } - set { Update(_AccessToken, value); } + get => GetAttribute(_AccessToken); + set => Update(_AccessToken, value); } /// @@ -115,8 +123,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..9c6c05d 100644 --- a/ApiClient/Core/Configuration/ConfigurationHelper.cs +++ b/ApiClient/Core/Configuration/ConfigurationHelper.cs @@ -11,15 +11,9 @@ // //----------------------------------------------------------------------- -using System; -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; +using System.Diagnostics.CodeAnalysis; +using System.Xml.Linq; namespace ApiClient.Core.Configuration { @@ -32,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. @@ -41,14 +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(); } /// @@ -60,13 +69,11 @@ public string GetAttribute(string attrName) { try { - return _config.AppSettings.Settings[attrName] == null - ? null - : _config.AppSettings.Settings[attrName].Value; + return Value(Setting(attrName))?.Value!; } catch (System.Exception) { - return null; + return null!; } } @@ -93,27 +100,7 @@ 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; - } - } - - /// - /// 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(); } } 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/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; diff --git a/ApiClient/GlobalSuppressions.cs b/ApiClient/GlobalSuppressions.cs new file mode 100644 index 0000000..420d211 --- /dev/null +++ b/ApiClient/GlobalSuppressions.cs @@ -0,0 +1,17 @@ +//----------------------------------------------------------------------- +// +// 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/ApiClientSettings.cs b/ApiClient/Models/ApiClientSettings.cs index 74af47b..9a9672e 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,38 +24,38 @@ 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().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, }; } 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/Models/RequestSnapshot.cs b/ApiClient/Models/RequestSnapshot.cs new file mode 100644 index 0000000..df87a28 --- /dev/null +++ b/ApiClient/Models/RequestSnapshot.cs @@ -0,0 +1,41 @@ +//----------------------------------------------------------------------- +// +// 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 interface IRequestQuerySave + { + public void Save(RequestSnapshot requestSnapshot, T database); + + 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 + { + 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/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..850116d 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; @@ -114,7 +114,6 @@ public static string Base64Encode(string plainText) } 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 string Base64Encode(string plainText) } 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 19de821..17e3502 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,21 @@ 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); var tokenResponse = await httpClient.SendAsync(requestMessage).ConfigureAwait(false); var text = await tokenResponse.Content.ReadAsStringAsync(); @@ -120,7 +119,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,11 +137,12 @@ 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 var requestMessage = new HttpRequestMessage(HttpMethod.Post, DigiKeyUriConstants.TokenEndpoint); @@ -150,7 +150,6 @@ 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); var tokenResponse = await httpClient.SendAsync(requestMessage).ConfigureAwait(false); var text = await tokenResponse.Content.ReadAsStringAsync(); diff --git a/Readme.md b/Readme.md index e3735ae..0bed123 100644 --- a/Readme.md +++ b/Readme.md @@ -1,31 +1,39 @@ -# 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 ```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. * **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. 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 @@ -38,5 +46,99 @@ 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 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 RequestQuerySave : IRequestQuerySave +{ + public void Save(RequestSnapshot requestSnapshot, DbContext database) + { + database.DigikeyAPIRequests.Add(Convert(requestSnapshot)); + database.SaveChanges(); + } + + public DigikeyAPIRequest Convert(RequestSnapshot requestSnapshot) + { + return new() + { + 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() + { + 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. + +#### Global Time Cutoff + +```csharp +DateTime cutoffDateTime = DateTime.Today.AddDays(-30); +_clientService = new(_clientSettings, _requestQuerySave, 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. diff --git a/apiclient.config b/sample-apiclient.config similarity index 100% rename from apiclient.config rename to sample-apiclient.config