diff --git a/CHANGELOG.md b/CHANGELOG.md index cc93198..3040bab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,23 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org/) for commit guidelines. -## 1.1.0 +## [1.2.0](https://github.com/JustinCanton/Geo.NET/compare/1.1.1...1.2.0) (2022-08-20) +### Features +- adding extra information and logging surrounding exceptions ([#44](https://github.com/JustinCanton/Geo.NET/pull/44)) ([7b5b154](https://github.com/JustinCanton/Geo.NET/commit/7b5b15441181bda16b0a644e2b3ef8e7b06cc074)) + +### Bug Fixes +- fixing an issue where the query is not properly encoded, and fixing an issue where the query string building is using an outdated method ([#46](https://github.com/JustinCanton/Geo.NET/pull/46)) ([5b55f4d](https://github.com/JustinCanton/Geo.NET/commit/5b55f4d249a617e4667e92b5cb0b2c9b6b02ec6f)) + +## [1.1.1](https://github.com/JustinCanton/Geo.NET/compare/1.1.0...1.1.1) (2022-07-21) +### Bug Fixes +- fixing a vulnerability in newtonsoft nuget ([#39](https://github.com/JustinCanton/Geo.NET/pull/39)) ([fa192ca](https://github.com/JustinCanton/Geo.NET/commit/fa192cab2a965503aa5a50885010836461cb822b)) + +## [1.1.0](https://github.com/JustinCanton/Geo.NET/compare/1.0.0...1.1.0) (2022-06-04) ### Features - Adding score into the HERE Geocoding response ([#33](https://github.com/JustinCanton/Geo.NET/pull/33)) ([c8f42e1](https://github.com/JustinCanton/Geo.NET/commit/c8f42e1f155da17dd3869f304c3b9e36a938da71)) - Adding net6.0 support ([#34](https://github.com/JustinCanton/Geo.NET/pull/34)) ([e8eebca](https://github.com/JustinCanton/Geo.NET/commit/e8eebca37d82e3659e7c5e6e2ea4f4777f45f4f7)) -## 1.0.0 +## 1.0.0 (2021-01-10) ### Features - Adding support for ArcGIS Suggest API - Adding support for ArcGIS Address Candidate API diff --git a/README.md b/README.md index 7b65bea..db27805 100644 --- a/README.md +++ b/README.md @@ -49,23 +49,7 @@ The configuration and sample usage for each supported interface can be found wit - [MapQuest](https://github.com/JustinCanton/Geo.NET/tree/master/src/Geo.MapQuest) -## Roadmap - -### 1.1.0 -|Status|Goal| -|:--:|--| -|✅|Adding score into the HERE Geocoding response| -|✅|Adding net6.0 support| -|✅|Adding ChangeLog| - - -### 1.2.0 -|Status|Goal| -|:--:|--| -|❌|Adding support for conversion from coordinates to flexible polylines rather than requiring polylines as input for the HERE API ([#36](https://github.com/JustinCanton/Geo.NET/issues/36))| - - -### Suggestions or Discussion Points +## Suggestions or Discussion Points If you would like to weigh in on any suggestion, please make a Github Issue and we can discuss further. ✅ = Accepted diff --git a/src/Geo.ArcGIS/Models/Exceptions/ArcGISException.cs b/src/Geo.ArcGIS/Models/Exceptions/ArcGISException.cs index 382b626..ad08971 100644 --- a/src/Geo.ArcGIS/Models/Exceptions/ArcGISException.cs +++ b/src/Geo.ArcGIS/Models/Exceptions/ArcGISException.cs @@ -9,6 +9,7 @@ namespace Geo.ArcGIS.Models.Exceptions using System.Globalization; using System.Net.Http; using System.Threading.Tasks; + using Geo.Core.Models.Exceptions; using Newtonsoft.Json; /// @@ -23,7 +24,7 @@ namespace Geo.ArcGIS.Models.Exceptions /// Thrown when the ArcGIS request is cancelled. /// Thrown when an error occurs while reading the return JSON text. /// Thrown when when an error occurs during JSON deserialization. - public class ArcGISException : Exception + public sealed class ArcGISException : GeoCoreException { private const string DefaultMessage = "{0} See the inner exception for more information."; diff --git a/src/Geo.ArcGIS/Services/ArcGISGeocoding.cs b/src/Geo.ArcGIS/Services/ArcGISGeocoding.cs index 71332a6..980862f 100644 --- a/src/Geo.ArcGIS/Services/ArcGISGeocoding.cs +++ b/src/Geo.ArcGIS/Services/ArcGISGeocoding.cs @@ -7,14 +7,12 @@ namespace Geo.ArcGIS.Services { using System; using System.Collections.Generic; - using System.Collections.Specialized; using System.Configuration; using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; - using System.Web; using Geo.ArcGIS.Abstractions; using Geo.ArcGIS.Enums; using Geo.ArcGIS.Models.Exceptions; @@ -22,6 +20,7 @@ namespace Geo.ArcGIS.Services using Geo.ArcGIS.Models.Responses; using Geo.Core; using Geo.Core.Extensions; + using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -47,18 +46,20 @@ public class ArcGISGeocoding : ClientExecutor, IArcGISGeocoding /// /// A used for making calls to the ArcGIS system. /// A used for retreiving the ArcGIS token. - /// A used to create a localizer for localizing log or exception messages. - /// A used for logging information. + /// An used to provide exceptions based on an exception type. + /// An used to create a localizer for localizing log or exception messages. + /// An used to create a logger used for logging information. public ArcGISGeocoding( HttpClient client, IArcGISTokenContainer tokenContainer, + IGeoNETExceptionProvider exceptionProvider, IStringLocalizerFactory localizerFactory, - ILogger logger = null) - : base(client, localizerFactory) + ILoggerFactory loggerFactory = null) + : base(client, exceptionProvider, localizerFactory, loggerFactory) { _tokenContainer = tokenContainer ?? throw new ArgumentNullException(nameof(tokenContainer)); _localizer = localizerFactory?.Create(typeof(ArcGISGeocoding)) ?? throw new ArgumentNullException(nameof(localizerFactory)); - _logger = logger ?? NullLogger.Instance; + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; } /// @@ -159,15 +160,15 @@ internal async Task BuildAddressCandidateRequest(AddressCandidateParameters } var uriBuilder = new UriBuilder(CandidatesUri); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); - query.Add("f", "json"); - query.Add("outFields", "Match_addr,Addr_type"); + var query = QueryString.Empty; + query = query.Add("f", "json"); + query = query.Add("outFields", "Match_addr,Addr_type"); - query.Add("singleLine", parameters.SingleLineAddress); + query = query.Add("singleLine", parameters.SingleLineAddress); - AddStorageParameter(parameters, query); + AddStorageParameter(parameters, ref query); - await AddArcGISToken(query, cancellationToken).ConfigureAwait(false); + query = await AddArcGISToken(query, cancellationToken).ConfigureAwait(false); uriBuilder.Query = query.ToString(); @@ -183,13 +184,13 @@ internal async Task BuildAddressCandidateRequest(AddressCandidateParameters internal async Task BuildPlaceCandidateRequest(PlaceCandidateParameters parameters, CancellationToken cancellationToken) { var uriBuilder = new UriBuilder(CandidatesUri); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); - query.Add("f", "json"); - query.Add("outFields", "Place_addr,PlaceName"); + var query = QueryString.Empty; + query = query.Add("f", "json"); + query = query.Add("outFields", "Place_addr,PlaceName"); if (!string.IsNullOrWhiteSpace(parameters.Category)) { - query.Add("category", parameters.Category); + query = query.Add("category", parameters.Category); } else { @@ -198,7 +199,7 @@ internal async Task BuildPlaceCandidateRequest(PlaceCandidateParameters par if (parameters.Location != null) { - query.Add("location", parameters.Location.ToString()); + query = query.Add("location", parameters.Location.ToString()); } else { @@ -207,16 +208,16 @@ internal async Task BuildPlaceCandidateRequest(PlaceCandidateParameters par if (parameters.MaximumLocations > 0 && parameters.MaximumLocations <= 50) { - query.Add("maxLocations", parameters.MaximumLocations.ToString(CultureInfo.InvariantCulture)); + query = query.Add("maxLocations", parameters.MaximumLocations.ToString(CultureInfo.InvariantCulture)); } else { _logger.ArcGISWarning(_localizer["Invalid Maximum Locations"]); } - AddStorageParameter(parameters, query); + AddStorageParameter(parameters, ref query); - await AddArcGISToken(query, cancellationToken).ConfigureAwait(false); + query = await AddArcGISToken(query, cancellationToken).ConfigureAwait(false); uriBuilder.Query = query.ToString(); @@ -232,8 +233,8 @@ internal async Task BuildPlaceCandidateRequest(PlaceCandidateParameters par internal Task BuildSuggestRequest(SuggestParameters parameters, CancellationToken cancellationToken) { var uriBuilder = new UriBuilder(SuggestUri); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); - query.Add("f", "json"); + var query = QueryString.Empty; + query = query.Add("f", "json"); if (string.IsNullOrWhiteSpace(parameters.Text)) { @@ -242,11 +243,11 @@ internal Task BuildSuggestRequest(SuggestParameters parameters, Cancellatio throw new ArgumentException(error, nameof(parameters.Text)); } - query.Add("text", parameters.Text); + query = query.Add("text", parameters.Text); if (parameters.Location != null) { - query.Add("location", parameters.Location.ToString()); + query = query.Add("location", parameters.Location.ToString()); } else { @@ -255,7 +256,7 @@ internal Task BuildSuggestRequest(SuggestParameters parameters, Cancellatio if (!string.IsNullOrWhiteSpace(parameters.Category)) { - query.Add("category", parameters.Category); + query = query.Add("category", parameters.Category); } else { @@ -264,7 +265,7 @@ internal Task BuildSuggestRequest(SuggestParameters parameters, Cancellatio if (parameters.SearchExtent != null) { - query.Add("searchExtent", parameters.SearchExtent.ToString()); + query = query.Add("searchExtent", parameters.SearchExtent.ToString()); } else { @@ -273,7 +274,7 @@ internal Task BuildSuggestRequest(SuggestParameters parameters, Cancellatio if (parameters.MaximumLocations > 0 && parameters.MaximumLocations < 16) { - query.Add("maxLocations", parameters.MaximumLocations.ToString(CultureInfo.InvariantCulture)); + query = query.Add("maxLocations", parameters.MaximumLocations.ToString(CultureInfo.InvariantCulture)); } else { @@ -294,8 +295,8 @@ internal Task BuildSuggestRequest(SuggestParameters parameters, Cancellatio internal async Task BuildReverseGeocodingRequest(ReverseGeocodingParameters parameters, CancellationToken cancellationToken) { var uriBuilder = new UriBuilder(ReverseGeocodingUri); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); - query.Add("f", "json"); + var query = QueryString.Empty; + query = query.Add("f", "json"); if (parameters.Location is null) { @@ -304,11 +305,11 @@ internal async Task BuildReverseGeocodingRequest(ReverseGeocodingParameters throw new ArgumentException(error, nameof(parameters.Location)); } - query.Add("location", parameters.Location.ToString()); + query = query.Add("location", parameters.Location.ToString()); if (parameters.OutSpatialReference > 0) { - query.Add("outSR", parameters.OutSpatialReference.ToString(CultureInfo.InvariantCulture)); + query = query.Add("outSR", parameters.OutSpatialReference.ToString(CultureInfo.InvariantCulture)); } else { @@ -317,7 +318,7 @@ internal async Task BuildReverseGeocodingRequest(ReverseGeocodingParameters if (parameters.LanguageCode != null) { - query.Add("langCode", parameters.LanguageCode.TwoLetterISOLanguageName); + query = query.Add("langCode", parameters.LanguageCode.TwoLetterISOLanguageName); } else { @@ -332,7 +333,7 @@ internal async Task BuildReverseGeocodingRequest(ReverseGeocodingParameters featureTypesBuilder.Add(featureType.ToEnumString()); } - query.Add("featureTypes", featureTypesBuilder.ToString()); + query = query.Add("featureTypes", featureTypesBuilder.ToString()); } else { @@ -341,7 +342,7 @@ internal async Task BuildReverseGeocodingRequest(ReverseGeocodingParameters if (parameters.LocationType >= 0) { - query.Add("locationType", parameters.LocationType.ToEnumString()); + query = query.Add("locationType", parameters.LocationType.ToEnumString()); } else { @@ -350,16 +351,16 @@ internal async Task BuildReverseGeocodingRequest(ReverseGeocodingParameters if (parameters.PreferredLabelValue >= 0) { - query.Add("preferredLabelValues", parameters.PreferredLabelValue.ToEnumString()); + query = query.Add("preferredLabelValues", parameters.PreferredLabelValue.ToEnumString()); } else { _logger.ArcGISWarning(_localizer["Invalid Preferred Label Value"]); } - AddStorageParameter(parameters, query); + AddStorageParameter(parameters, ref query); - await AddArcGISToken(query, cancellationToken).ConfigureAwait(false); + query = await AddArcGISToken(query, cancellationToken).ConfigureAwait(false); uriBuilder.Query = query.ToString(); @@ -375,8 +376,8 @@ internal async Task BuildReverseGeocodingRequest(ReverseGeocodingParameters internal async Task BuildGeocodingRequest(GeocodingParameters parameters, CancellationToken cancellationToken) { var uriBuilder = new UriBuilder(GeocodingUri); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); - query.Add("f", "json"); + var query = QueryString.Empty; + query = query.Add("f", "json"); if (parameters.AddressAttributes is null || parameters.AddressAttributes.Count == 0) { @@ -400,15 +401,15 @@ internal async Task BuildGeocodingRequest(GeocodingParameters parameters, C records = attributes, }; - query.Add("addresses", JsonConvert.SerializeObject(addresses)); + query = query.Add("addresses", JsonConvert.SerializeObject(addresses)); #pragma warning disable CA1308 // Normalize strings to uppercase - query.Add("matchOutOfRange", parameters.MatchOutOfRange.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + query = query.Add("matchOutOfRange", parameters.MatchOutOfRange.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); #pragma warning restore CA1308 // Normalize strings to uppercase if (!string.IsNullOrWhiteSpace(parameters.Category)) { - query.Add("category", parameters.Category); + query = query.Add("category", parameters.Category); } else { @@ -417,7 +418,7 @@ internal async Task BuildGeocodingRequest(GeocodingParameters parameters, C if (parameters.SourceCountry.Count != 0) { - query.Add("sourceCountry", string.Join(",", parameters.SourceCountry.Select(x => x.ThreeLetterISORegionName))); + query = query.Add("sourceCountry", string.Join(",", parameters.SourceCountry.Select(x => x.ThreeLetterISORegionName))); } else { @@ -426,7 +427,7 @@ internal async Task BuildGeocodingRequest(GeocodingParameters parameters, C if (parameters.OutSpatialReference > 0) { - query.Add("outSR", parameters.OutSpatialReference.ToString(CultureInfo.InvariantCulture)); + query = query.Add("outSR", parameters.OutSpatialReference.ToString(CultureInfo.InvariantCulture)); } else { @@ -435,7 +436,7 @@ internal async Task BuildGeocodingRequest(GeocodingParameters parameters, C if (parameters.SearchExtent != null) { - query.Add("searchExtent", parameters.SearchExtent.ToString()); + query = query.Add("searchExtent", parameters.SearchExtent.ToString()); } else { @@ -444,7 +445,7 @@ internal async Task BuildGeocodingRequest(GeocodingParameters parameters, C if (parameters.LanguageCode != null) { - query.Add("langCode", parameters.LanguageCode.TwoLetterISOLanguageName); + query = query.Add("langCode", parameters.LanguageCode.TwoLetterISOLanguageName); } else { @@ -453,7 +454,7 @@ internal async Task BuildGeocodingRequest(GeocodingParameters parameters, C if (parameters.LocationType >= 0) { - query.Add("locationType", parameters.LocationType.ToEnumString()); + query = query.Add("locationType", parameters.LocationType.ToEnumString()); } else { @@ -462,14 +463,14 @@ internal async Task BuildGeocodingRequest(GeocodingParameters parameters, C if (parameters.PreferredLabelValue >= 0) { - query.Add("preferredLabelValues", parameters.PreferredLabelValue.ToEnumString()); + query = query.Add("preferredLabelValues", parameters.PreferredLabelValue.ToEnumString()); } else { _logger.ArcGISWarning(_localizer["Invalid Preferred Label Value"]); } - await AddArcGISToken(query, cancellationToken).ConfigureAwait(false); + query = await AddArcGISToken(query, cancellationToken).ConfigureAwait(false); uriBuilder.Query = query.ToString(); @@ -480,11 +481,11 @@ internal async Task BuildGeocodingRequest(GeocodingParameters parameters, C /// Adds the ArcGIS storage flag to the query parameters. /// /// A with the storage information. - /// A with the query parameters. - internal void AddStorageParameter(StorageParameters parameters, NameValueCollection query) + /// A with the query parameters. + internal void AddStorageParameter(StorageParameters parameters, ref QueryString query) { #pragma warning disable CA1308 // Normalize strings to uppercase - query.Add("forStorage", parameters.ForStorage.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + query = query.Add("forStorage", parameters.ForStorage.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); #pragma warning restore CA1308 // Normalize strings to uppercase } @@ -492,16 +493,18 @@ internal void AddStorageParameter(StorageParameters parameters, NameValueCollect /// Adds the ArcGIS token to the query parameters. /// If the token cannot be generated, it will not be added to the request. /// - /// A with the query parameters. + /// A with the query parameters. /// A used to cancel the request. /// A . - internal async Task AddArcGISToken(NameValueCollection query, CancellationToken cancellationToken) + internal async Task AddArcGISToken(QueryString query, CancellationToken cancellationToken) { var token = await _tokenContainer.GetTokenAsync(cancellationToken).ConfigureAwait(false); if (!string.IsNullOrWhiteSpace(token)) { - query.Add("token", token); + return query.Add("token", token); } + + return query; } } } diff --git a/src/Geo.Bing/Models/Exceptions/BingException.cs b/src/Geo.Bing/Models/Exceptions/BingException.cs index 228d8f9..8d38566 100644 --- a/src/Geo.Bing/Models/Exceptions/BingException.cs +++ b/src/Geo.Bing/Models/Exceptions/BingException.cs @@ -9,6 +9,7 @@ namespace Geo.Bing.Models.Exceptions using System.Globalization; using System.Net.Http; using System.Threading.Tasks; + using Geo.Core.Models.Exceptions; using Newtonsoft.Json; /// @@ -23,7 +24,7 @@ namespace Geo.Bing.Models.Exceptions /// Thrown when the Bing request is cancelled. /// Thrown when an error occurs while reading the return JSON text. /// Thrown when when an error occurs during JSON deserialization. - public class BingException : Exception + public sealed class BingException : GeoCoreException { private const string DefaultMessage = "{0} See the inner exception for more information."; diff --git a/src/Geo.Bing/Services/BingGeocoding.cs b/src/Geo.Bing/Services/BingGeocoding.cs index 9cc746e..e3d3f93 100644 --- a/src/Geo.Bing/Services/BingGeocoding.cs +++ b/src/Geo.Bing/Services/BingGeocoding.cs @@ -6,18 +6,17 @@ namespace Geo.Bing.Services { using System; - using System.Collections.Specialized; using System.Configuration; using System.Globalization; using System.Net.Http; using System.Threading; using System.Threading.Tasks; - using System.Web; using Geo.Bing.Abstractions; using Geo.Bing.Models.Exceptions; using Geo.Bing.Models.Parameters; using Geo.Bing.Models.Responses; using Geo.Core; + using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -39,18 +38,20 @@ public class BingGeocoding : ClientExecutor, IBingGeocoding /// /// A used for placing calls to the Bing Geocoding API. /// A used for fetching the Bing key. - /// A used to create a localizer for localizing log or exception messages. - /// A used for logging information. + /// An used to provide exceptions based on an exception type. + /// An used to create a localizer for localizing log or exception messages. + /// An used to create a logger used for logging information. public BingGeocoding( HttpClient client, IBingKeyContainer keyContainer, + IGeoNETExceptionProvider exceptionProvider, IStringLocalizerFactory localizerFactory, - ILogger logger = null) - : base(client, localizerFactory) + ILoggerFactory loggerFactory = null) + : base(client, exceptionProvider, localizerFactory, loggerFactory) { _keyContainer = keyContainer ?? throw new ArgumentNullException(nameof(keyContainer)); _localizer = localizerFactory?.Create(typeof(BingGeocoding)) ?? throw new ArgumentNullException(nameof(localizerFactory)); - _logger = logger ?? NullLogger.Instance; + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; } /// @@ -118,7 +119,7 @@ internal Uri ValidateAndBuildUri(TParameters parameters, Func 0) { - query.Add("includeEntityTypes", includes.ToString()); + query = query.Add("includeEntityTypes", includes.ToString()); } else { @@ -203,7 +204,7 @@ internal Uri BuildReverseGeocodingRequest(ReverseGeocodingParameters parameters) BuildBaseQuery(parameters, ref query); - AddBingKey(query); + AddBingKey(ref query); uriBuilder.Query = query.ToString(); @@ -230,11 +231,11 @@ internal Uri BuildAddressGeocodingRequest(AddressGeocodingParameters parameters) } var uriBuilder = new UriBuilder(BaseUri); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); + var query = QueryString.Empty; if (!string.IsNullOrWhiteSpace(parameters.AdministrationDistrict)) { - query.Add("adminDistrict", parameters.AdministrationDistrict); + query = query.Add("adminDistrict", parameters.AdministrationDistrict); } else { @@ -243,7 +244,7 @@ internal Uri BuildAddressGeocodingRequest(AddressGeocodingParameters parameters) if (!string.IsNullOrWhiteSpace(parameters.Locality)) { - query.Add("locality", parameters.Locality); + query = query.Add("locality", parameters.Locality); } else { @@ -252,7 +253,7 @@ internal Uri BuildAddressGeocodingRequest(AddressGeocodingParameters parameters) if (!string.IsNullOrWhiteSpace(parameters.PostalCode)) { - query.Add("postalCode", parameters.PostalCode); + query = query.Add("postalCode", parameters.PostalCode); } else { @@ -261,7 +262,7 @@ internal Uri BuildAddressGeocodingRequest(AddressGeocodingParameters parameters) if (!string.IsNullOrWhiteSpace(parameters.AddressLine)) { - query.Add("addressLine", parameters.AddressLine); + query = query.Add("addressLine", parameters.AddressLine); } else { @@ -270,7 +271,7 @@ internal Uri BuildAddressGeocodingRequest(AddressGeocodingParameters parameters) if (parameters.CountryRegion != null) { - query.Add("countryRegion", parameters.CountryRegion.TwoLetterISORegionName.ToUpperInvariant()); + query = query.Add("countryRegion", parameters.CountryRegion.TwoLetterISORegionName.ToUpperInvariant()); } else { @@ -279,7 +280,7 @@ internal Uri BuildAddressGeocodingRequest(AddressGeocodingParameters parameters) BuildLimitedResultQuery(parameters, ref query); - AddBingKey(query); + AddBingKey(ref query); uriBuilder.Query = query.ToString(); @@ -290,12 +291,12 @@ internal Uri BuildAddressGeocodingRequest(AddressGeocodingParameters parameters) /// Builds up the limited result query parameters. /// /// A with the limited result parameters to build the uri with. - /// A with the built up query parameters. - internal void BuildLimitedResultQuery(ResultParameters parameters, ref NameValueCollection query) + /// A with the built up query parameters. + internal void BuildLimitedResultQuery(ResultParameters parameters, ref QueryString query) { if (parameters.MaximumResults > 0 && parameters.MaximumResults <= 20) { - query.Add("maxResults", parameters.MaximumResults.ToString(CultureInfo.InvariantCulture)); + query = query.Add("maxResults", parameters.MaximumResults.ToString(CultureInfo.InvariantCulture)); } else { @@ -309,12 +310,12 @@ internal void BuildLimitedResultQuery(ResultParameters parameters, ref NameValue /// Builds up the base query parameters. /// /// A with the base parameters to build the uri with. - /// A with the built up query parameters. - internal void BuildBaseQuery(BaseParameters parameters, ref NameValueCollection query) + /// A with the built up query parameters. + internal void BuildBaseQuery(BaseParameters parameters, ref QueryString query) { if (parameters.IncludeNeighbourhood == true) { - query.Add("includeNeighborhood", "1"); + query = query.Add("includeNeighborhood", "1"); } else { @@ -334,7 +335,7 @@ internal void BuildBaseQuery(BaseParameters parameters, ref NameValueCollection if (includes.Count > 0) { - query.Add("include", includes.ToString()); + query = query.Add("include", includes.ToString()); } else { @@ -345,10 +346,10 @@ internal void BuildBaseQuery(BaseParameters parameters, ref NameValueCollection /// /// Adds the Bing key to the query parameters. /// - /// A with the query parameters. - internal void AddBingKey(NameValueCollection query) + /// A with the query parameters. + internal void AddBingKey(ref QueryString query) { - query.Add("key", _keyContainer.GetKey()); + query = query.Add("key", _keyContainer.GetKey()); } } } diff --git a/src/Geo.Core/Abstractions/IGeoNETExceptionProvider.cs b/src/Geo.Core/Abstractions/IGeoNETExceptionProvider.cs new file mode 100644 index 0000000..72768dc --- /dev/null +++ b/src/Geo.Core/Abstractions/IGeoNETExceptionProvider.cs @@ -0,0 +1,26 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Core +{ + using System; + using Geo.Core.Models.Exceptions; + + /// + /// An interface used to provide an exception based on the exception type. + /// + public interface IGeoNETExceptionProvider + { + /// + /// Gets an exception of type . + /// + /// The type of the exception to get. + /// The message the exception should have. + /// Optional. The inner exception to include in the exception. + /// An exception of type . + TException GetException(string message, Exception innerException = null) + where TException : GeoCoreException; + } +} diff --git a/src/Geo.Core/ClientExecutor.cs b/src/Geo.Core/ClientExecutor.cs index b7d4b89..89aba6c 100644 --- a/src/Geo.Core/ClientExecutor.cs +++ b/src/Geo.Core/ClientExecutor.cs @@ -6,13 +6,14 @@ namespace Geo.Core { using System; - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.Linq.Expressions; using System.Net.Http; using System.Threading; using System.Threading.Tasks; + using Geo.Core.Models; + using Geo.Core.Models.Exceptions; using Microsoft.Extensions.Localization; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging.Abstractions; using Newtonsoft.Json; /// @@ -20,21 +21,28 @@ namespace Geo.Core /// public class ClientExecutor { - private static readonly ConcurrentDictionary> _cachedExceptionDelegates = new ConcurrentDictionary>(); private readonly HttpClient _client; + private readonly IGeoNETExceptionProvider _exceptionProvider; private readonly IStringLocalizer _localizer; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// A used for placing calls to the APIs. - /// A used to create a localizer for localizing log or exception messages. + /// An used to provide exceptions based on an exception type. + /// An used to create a localizer for localizing log or exception messages. + /// An used to create a logger used for logging information. public ClientExecutor( HttpClient client, - IStringLocalizerFactory localizerFactory) + IGeoNETExceptionProvider exceptionProvider, + IStringLocalizerFactory localizerFactory, + ILoggerFactory loggerFactory = null) { _client = client ?? throw new ArgumentNullException(nameof(client)); + _exceptionProvider = exceptionProvider ?? throw new ArgumentNullException(nameof(exceptionProvider)); _localizer = localizerFactory?.Create(typeof(ClientExecutor)) ?? throw new ArgumentNullException(nameof(localizerFactory)); + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; } /// @@ -49,9 +57,14 @@ public ClientExecutor( /// An exception of type thrown when any exception occurs and wraps the original exception. public async Task CallAsync(Uri uri, string apiName, CancellationToken cancellationToken = default) where TResult : class - where TException : Exception + where TException : GeoCoreException { - (TResult Result, string JSON) response; + if (uri is null) + { + throw new ArgumentNullException(nameof(uri)); + } + + CallResult response; try { @@ -59,45 +72,42 @@ public async Task CallAsync(Uri uri, string apiNam } catch (ArgumentNullException ex) { - var exceptionConstructor = GetExceptionConstructor(typeof(TException)); - throw exceptionConstructor(_localizer["Null Uri", apiName].ToString(), ex) as TException; + throw _exceptionProvider.GetException(_localizer["Null Uri", apiName].ToString(), ex); } catch (InvalidOperationException ex) { - var exceptionConstructor = GetExceptionConstructor(typeof(TException)); - throw exceptionConstructor(_localizer["Invalid Uri", apiName].ToString(), ex) as TException; + throw _exceptionProvider.GetException(_localizer["Invalid Uri", apiName].ToString(), ex); } catch (HttpRequestException ex) { - var exceptionConstructor = GetExceptionConstructor(typeof(TException)); - throw exceptionConstructor(_localizer["Request Failed", apiName].ToString(), ex) as TException; + throw _exceptionProvider.GetException(_localizer["Request Failed", apiName].ToString(), ex); } catch (TaskCanceledException ex) { - var exceptionConstructor = GetExceptionConstructor(typeof(TException)); - throw exceptionConstructor(_localizer["Request Cancelled", apiName].ToString(), ex) as TException; + throw _exceptionProvider.GetException(_localizer["Request Cancelled", apiName].ToString(), ex); } catch (JsonReaderException ex) { - var exceptionConstructor = GetExceptionConstructor(typeof(TException)); - throw exceptionConstructor(_localizer["Reader Failed To Parse", apiName].ToString(), ex) as TException; + throw _exceptionProvider.GetException(_localizer["Reader Failed To Parse", apiName].ToString(), ex); } catch (JsonSerializationException ex) { - var exceptionConstructor = GetExceptionConstructor(typeof(TException)); - throw exceptionConstructor(_localizer["Serializer Failed To Parse", apiName].ToString(), ex) as TException; + throw _exceptionProvider.GetException(_localizer["Serializer Failed To Parse", apiName].ToString(), ex); } catch (Exception ex) { - var exceptionConstructor = GetExceptionConstructor(typeof(TException)); - throw exceptionConstructor(_localizer["Request Failed Exception", apiName].ToString(), ex) as TException; + throw _exceptionProvider.GetException(_localizer["Request Failed Exception", apiName].ToString(), ex); } - if (response.Result is null || !string.IsNullOrEmpty(response.JSON)) + if (!response.IsSuccessful) { - var exceptionConstructor = GetExceptionConstructor(typeof(TException)); - var ex = exceptionConstructor(_localizer["Request Failure", apiName].ToString(), null) as TException; - ex.Data.Add("responseBody", response.JSON); + var ex = _exceptionProvider.GetException(_localizer["Request Failure", apiName].ToString(), null); + ex.Data.Add("uri", uri); + ex.Data.Add("responseStatusCode", response.StatusCode); + ex.Data.Add("responseBody", response.Body); + + _logger.ApiCallFailed(uri, ex); + throw ex; } @@ -120,7 +130,7 @@ public async Task CallAsync(Uri uri, string apiNam /// Thrown when the request is cancelled. /// Thrown when an error occurs while reading the return JSON text. /// Thrown when when an error occurs during JSON deserialization. - internal async Task<(TResult Result, string JSON)> CallAsync(Uri uri, CancellationToken cancellationToken = default) + internal async Task> CallAsync(Uri uri, CancellationToken cancellationToken = default) where TResult : class { var response = await _client.GetAsync(uri, cancellationToken).ConfigureAwait(false); @@ -133,37 +143,10 @@ public async Task CallAsync(Uri uri, string apiNam if (!response.IsSuccessStatusCode) { - return (null, json); + return new CallResult(response.StatusCode, json); } - return (JsonConvert.DeserializeObject(json), string.Empty); - } - - private static Func GetExceptionConstructor(Type type) - { - if (type == null) - { - throw new ArgumentNullException(nameof(type)); - } - - if (_cachedExceptionDelegates.TryGetValue(type, out var cachedConstructor)) - { - return cachedConstructor; - } - - var constructorParameters = new Type[] { typeof(string), typeof(Exception) }; - var constructor = type.GetConstructor(constructorParameters); - var parameters = new List() - { - Expression.Parameter(typeof(string)), - Expression.Parameter(typeof(Exception)), - }; - - var expression = Expression.New(constructor, parameters); - var lambdaExpression = Expression.Lambda>(expression, parameters); - var function = lambdaExpression.Compile(); - - return _cachedExceptionDelegates.AddOrUpdate(type, function, (key, value) => function); + return new CallResult(JsonConvert.DeserializeObject(json), response.StatusCode, json); } } } diff --git a/src/Geo.Core/DependencyInjection/ServiceCollectionExtensions.cs b/src/Geo.Core/DependencyInjection/ServiceCollectionExtensions.cs index fd33d04..146c8b3 100644 --- a/src/Geo.Core/DependencyInjection/ServiceCollectionExtensions.cs +++ b/src/Geo.Core/DependencyInjection/ServiceCollectionExtensions.cs @@ -19,7 +19,9 @@ public static class ServiceCollectionExtensions /// A with the added services. public static IServiceCollection AddCoreServices(this IServiceCollection services) { - services.AddLocalization(); + services + .AddLocalization() + .AddTransient(); return services; } diff --git a/src/Geo.Core/Extensions/LoggerExtensions.cs b/src/Geo.Core/Extensions/LoggerExtensions.cs new file mode 100644 index 0000000..bdd393f --- /dev/null +++ b/src/Geo.Core/Extensions/LoggerExtensions.cs @@ -0,0 +1,32 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Core +{ + using System; + using Microsoft.Extensions.Logging; + + /// + /// Extension methods for the class. + /// + internal static class LoggerExtensions + { + private static readonly Action _apiCallFailed = LoggerMessage.Define( + LogLevel.Error, + new EventId(1, nameof(ClientExecutor)), + "ClientExecutor: The call to the API {URI} failed."); + + /// + /// "ClientExecutor: The call to the API {API} failed.". + /// + /// An used to log the error message. + /// The uri that failed. + /// The exception that occured. + public static void ApiCallFailed(this ILogger logger, Uri uri, Exception ex) + { + _apiCallFailed(logger, uri.ToString(), ex); + } + } +} diff --git a/src/Geo.Core/GeoNETExceptionProvider.cs b/src/Geo.Core/GeoNETExceptionProvider.cs new file mode 100644 index 0000000..483386c --- /dev/null +++ b/src/Geo.Core/GeoNETExceptionProvider.cs @@ -0,0 +1,101 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Core +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Linq.Expressions; + using Geo.Core.Models.Exceptions; + + /// + /// Used to provide an exception based on the exception type. + /// + public sealed class GeoNETExceptionProvider : IGeoNETExceptionProvider + { + private static readonly ConcurrentDictionary> _cachedInnerExceptionDelegates = new ConcurrentDictionary>(); + private static readonly ConcurrentDictionary> _cachedExceptionDelegates = new ConcurrentDictionary>(); + + /// + public TException GetException(string message, Exception innerException = null) + where TException : GeoCoreException + { + if (innerException == null) + { + var exceptionConstructor = GetExceptionConstructor(typeof(TException)); + return exceptionConstructor(message) as TException; + } + else + { + var exceptionConstructor = GetInnerExceptionConstructor(typeof(TException)); + return exceptionConstructor(message, innerException) as TException; + } + } + + private static Func GetInnerExceptionConstructor(Type type) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (_cachedInnerExceptionDelegates.TryGetValue(type, out var cachedConstructor)) + { + return cachedConstructor; + } + + var function = GetInnerExceptionDelegate(type); + + return _cachedInnerExceptionDelegates.AddOrUpdate(type, function, (key, value) => function); + } + + private static Func GetExceptionConstructor(Type type) + { + if (type == null) + { + throw new ArgumentNullException(nameof(type)); + } + + if (_cachedExceptionDelegates.TryGetValue(type, out var cachedConstructor)) + { + return cachedConstructor; + } + + var function = GetExceptionDelegate(type); + + return _cachedExceptionDelegates.AddOrUpdate(type, function, (key, value) => function); + } + + private static Func GetInnerExceptionDelegate(Type type) + { + var constructorParameters = new Type[] { typeof(string), typeof(Exception) }; + var constructor = type.GetConstructor(constructorParameters); + var parameters = new List() + { + Expression.Parameter(typeof(string)), + Expression.Parameter(typeof(Exception)), + }; + + var expression = Expression.New(constructor, parameters); + var lambdaExpression = Expression.Lambda>(expression, parameters); + return lambdaExpression.Compile(); + } + + private static Func GetExceptionDelegate(Type type) + { + var constructorParameters = new Type[] { typeof(string) }; + var constructor = type.GetConstructor(constructorParameters); + var parameters = new List() + { + Expression.Parameter(typeof(string)), + }; + + var expression = Expression.New(constructor, parameters); + var lambdaExpression = Expression.Lambda>(expression, parameters); + return lambdaExpression.Compile(); + } + } +} diff --git a/src/Geo.Core/Models/CallResult.cs b/src/Geo.Core/Models/CallResult.cs new file mode 100644 index 0000000..38cbdf9 --- /dev/null +++ b/src/Geo.Core/Models/CallResult.cs @@ -0,0 +1,64 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Core.Models +{ + using System.Net; + + /// + /// A results class used to store information relating to a call to an API. + /// + /// The type of the expected results. + internal class CallResult + where TResult : class + { + /// + /// Initializes a new instance of the class. + /// + /// The result from the call. + /// The status code of the call. + /// The string body of the call. + public CallResult(TResult result, HttpStatusCode statusCode, string body) + { + IsSuccessful = true; + Result = result; + StatusCode = statusCode; + Body = body; + } + + /// + /// Initializes a new instance of the class. + /// + /// The status code of the call. + /// The string body of the call. + public CallResult(HttpStatusCode statusCode, string body) + { + IsSuccessful = false; + Result = null; + StatusCode = statusCode; + Body = body; + } + + /// + /// Gets a value indicating whether the call was successful or not. + /// + public bool IsSuccessful { get; } + + /// + /// Gets the result of the call. + /// + public TResult Result { get; } + + /// + /// Gets the status code resulting from the call. + /// + public HttpStatusCode StatusCode { get; } + + /// + /// Gets the body of the result. + /// + public string Body { get; } + } +} diff --git a/src/Geo.Core/Models/Exceptions/GeoCoreException.cs b/src/Geo.Core/Models/Exceptions/GeoCoreException.cs new file mode 100644 index 0000000..c95f0eb --- /dev/null +++ b/src/Geo.Core/Models/Exceptions/GeoCoreException.cs @@ -0,0 +1,69 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Core.Models.Exceptions +{ + using System; + using System.Collections; + using System.Globalization; + + /// + /// A base exception typ eused by all derived exceptions, which overrides the ToString method to display the Data in the exception as well. + /// + public abstract class GeoCoreException : Exception + { + /// + /// Initializes a new instance of the class. + /// + protected GeoCoreException() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + protected GeoCoreException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception. + protected GeoCoreException(string message, Exception inner) + : base(message, inner) + { + } + + /// + public override string ToString() + { + return base.ToString() + GetData(); + } + + /// + /// Formats the information in the Data field of the exception and displays it when printing. + /// + /// A string with the formatted data. + private string GetData() + { + if (Data.Count == 0) + { + return string.Empty; + } + + var data = Environment.NewLine + "Data:"; + foreach (DictionaryEntry item in Data) + { + data += Environment.NewLine + " " + string.Format(CultureInfo.InvariantCulture, "{0}: {1}", item.Key, item.Value); + } + + return data; + } + } +} diff --git a/src/Geo.Google/Models/Exceptions/GoogleException.cs b/src/Geo.Google/Models/Exceptions/GoogleException.cs index 5cc6e86..b02e6ca 100644 --- a/src/Geo.Google/Models/Exceptions/GoogleException.cs +++ b/src/Geo.Google/Models/Exceptions/GoogleException.cs @@ -9,6 +9,7 @@ namespace Geo.Google.Models.Exceptions using System.Globalization; using System.Net.Http; using System.Threading.Tasks; + using Geo.Core.Models.Exceptions; using Newtonsoft.Json; /// @@ -23,7 +24,7 @@ namespace Geo.Google.Models.Exceptions /// Thrown when the Google request is cancelled. /// Thrown when an error occurs while reading the return JSON text. /// Thrown when when an error occurs during JSON deserialization. - public class GoogleException : Exception + public sealed class GoogleException : GeoCoreException { private const string DefaultMessage = "{0} See the inner exception for more information."; diff --git a/src/Geo.Google/Services/GoogleGeocoding.cs b/src/Geo.Google/Services/GoogleGeocoding.cs index 99243a4..4afed56 100644 --- a/src/Geo.Google/Services/GoogleGeocoding.cs +++ b/src/Geo.Google/Services/GoogleGeocoding.cs @@ -6,14 +6,12 @@ namespace Geo.Google.Services { using System; - using System.Collections.Specialized; using System.Globalization; using System.Linq; using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; - using System.Web; using Geo.Core; using Geo.Core.Extensions; using Geo.Google.Abstractions; @@ -22,6 +20,7 @@ namespace Geo.Google.Services using Geo.Google.Models.Exceptions; using Geo.Google.Models.Parameters; using Geo.Google.Models.Responses; + using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -49,18 +48,20 @@ public class GoogleGeocoding : ClientExecutor, IGoogleGeocoding /// /// A used for placing calls to the Google Geocoding API. /// A used for fetching the Google key. - /// A used to create a localizer for localizing log or exception messages. - /// A used for logging information. + /// An used to provide exceptions based on an exception type. + /// An used to create a localizer for localizing log or exception messages. + /// An used to create a logger used for logging information. public GoogleGeocoding( HttpClient client, IGoogleKeyContainer keyContainer, + IGeoNETExceptionProvider exceptionProvider, IStringLocalizerFactory localizerFactory, - ILogger logger = null) - : base(client, localizerFactory) + ILoggerFactory loggerFactory = null) + : base(client, exceptionProvider, localizerFactory, loggerFactory) { _keyContainer = keyContainer ?? throw new ArgumentNullException(nameof(keyContainer)); _localizer = localizerFactory?.Create(typeof(GoogleGeocoding)) ?? throw new ArgumentNullException(nameof(localizerFactory)); - _logger = logger ?? NullLogger.Instance; + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; } /// @@ -180,7 +181,7 @@ internal Uri ValidateAndBuildUri(TParameters parameters, Func()); } - query.Add("result_type", resultTypesBuilder.ToString()); + query = query.Add("result_type", resultTypesBuilder.ToString()); } else { @@ -282,16 +283,16 @@ internal Uri BuildReverseGeocodingRequest(ReverseGeocodingParameters parameters) locationTypesBuilder.Append(locationType.ToEnumString()); } - query.Add("location_type", locationTypesBuilder.ToString()); + query = query.Add("location_type", locationTypesBuilder.ToString()); } else { _logger.GoogleDebug(_localizer["Invalid Location Types"]); } - AddBaseParameters(parameters, query); + AddBaseParameters(parameters, ref query); - AddGoogleKey(query); + AddGoogleKey(ref query); uriBuilder.Query = query.ToString(); @@ -307,7 +308,7 @@ internal Uri BuildReverseGeocodingRequest(ReverseGeocodingParameters parameters) internal Uri BuildFindPlaceRequest(FindPlacesParameters parameters) { var uriBuilder = new UriBuilder(FindPlaceUri); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); + var query = QueryString.Empty; if (string.IsNullOrWhiteSpace(parameters.Input)) { @@ -323,13 +324,13 @@ internal Uri BuildFindPlaceRequest(FindPlacesParameters parameters) throw new ArgumentException(error, nameof(parameters.InputType)); } - query.Add("input", parameters.Input); + query = query.Add("input", parameters.Input); - query.Add("inputtype", parameters.InputType.ToEnumString()); + query = query.Add("inputtype", parameters.InputType.ToEnumString()); if (parameters.Fields != null && parameters.Fields.Count > 0) { - query.Add("fields", string.Join(",", parameters.Fields)); + query = query.Add("fields", string.Join(",", parameters.Fields)); } else { @@ -341,13 +342,13 @@ internal Uri BuildFindPlaceRequest(FindPlacesParameters parameters) switch (parameters.LocationBias) { case Coordinate coordinate: - query.Add("locationbias", $"point:{coordinate}"); + query = query.Add("locationbias", $"point:{coordinate}"); break; case Circle circle: - query.Add("locationbias", $"circle:{circle}"); + query = query.Add("locationbias", $"circle:{circle}"); break; case Boundaries boundary: - query.Add("locationbias", $"rectangle:{boundary}"); + query = query.Add("locationbias", $"rectangle:{boundary}"); break; default: _logger.GoogleWarning(_localizer["Invalid Location Bias Type"]); @@ -359,9 +360,9 @@ internal Uri BuildFindPlaceRequest(FindPlacesParameters parameters) _logger.GoogleDebug(_localizer["Invalid Location Bias"]); } - AddBaseParameters(parameters, query); + AddBaseParameters(parameters, ref query); - AddGoogleKey(query); + AddGoogleKey(ref query); uriBuilder.Query = query.ToString(); @@ -381,7 +382,7 @@ internal Uri BuildFindPlaceRequest(FindPlacesParameters parameters) internal Uri BuildNearbySearchRequest(NearbySearchParameters parameters) { var uriBuilder = new UriBuilder(NearbySearchUri); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); + var query = QueryString.Empty; if (parameters.Location == null) { @@ -415,7 +416,7 @@ internal Uri BuildNearbySearchRequest(NearbySearchParameters parameters) if (!string.IsNullOrWhiteSpace(parameters.Keyword)) { - query.Add("keyword", parameters.Keyword); + query = query.Add("keyword", parameters.Keyword); } else { @@ -424,16 +425,16 @@ internal Uri BuildNearbySearchRequest(NearbySearchParameters parameters) if (parameters.RankBy >= RankType.Prominence && parameters.RankBy <= RankType.Distance) { - query.Add("rankby", parameters.RankBy.ToEnumString()); + query = query.Add("rankby", parameters.RankBy.ToEnumString()); } else { _logger.GoogleDebug(_localizer["Invalid RankBy"]); } - AddBaseSearchParameters(parameters, query); + AddBaseSearchParameters(parameters, ref query); - AddGoogleKey(query); + AddGoogleKey(ref query); uriBuilder.Query = query.ToString(); @@ -449,7 +450,7 @@ internal Uri BuildNearbySearchRequest(NearbySearchParameters parameters) internal Uri BuildTextSearchRequest(TextSearchParameters parameters) { var uriBuilder = new UriBuilder(TextSearchUri); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); + var query = QueryString.Empty; if (string.IsNullOrWhiteSpace(parameters.Query)) { @@ -458,20 +459,20 @@ internal Uri BuildTextSearchRequest(TextSearchParameters parameters) throw new ArgumentException(error, nameof(parameters.Query)); } - query.Add("query", parameters.Query); + query = query.Add("query", parameters.Query); if (parameters.Region != null) { - query.Add("region", RegionInfoToCCTLD(parameters.Region)); + query = query.Add("region", RegionInfoToCCTLD(parameters.Region)); } else { _logger.GoogleDebug(_localizer["Invalid Region"]); } - AddBaseSearchParameters(parameters, query); + AddBaseSearchParameters(parameters, ref query); - AddGoogleKey(query); + AddGoogleKey(ref query); uriBuilder.Query = query.ToString(); @@ -487,7 +488,7 @@ internal Uri BuildTextSearchRequest(TextSearchParameters parameters) internal Uri BuildDetailsRequest(DetailsParameters parameters) { var uriBuilder = new UriBuilder(DetailsUri); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); + var query = QueryString.Empty; if (string.IsNullOrWhiteSpace(parameters.PlaceId)) { @@ -496,11 +497,11 @@ internal Uri BuildDetailsRequest(DetailsParameters parameters) throw new ArgumentException(error, nameof(parameters.PlaceId)); } - query.Add("place_id", parameters.PlaceId); + query = query.Add("place_id", parameters.PlaceId); if (parameters.Region != null) { - query.Add("region", RegionInfoToCCTLD(parameters.Region)); + query = query.Add("region", RegionInfoToCCTLD(parameters.Region)); } else { @@ -509,7 +510,7 @@ internal Uri BuildDetailsRequest(DetailsParameters parameters) if (!string.IsNullOrWhiteSpace(parameters.SessionToken)) { - query.Add("sessiontoken", parameters.SessionToken); + query = query.Add("sessiontoken", parameters.SessionToken); } else { @@ -518,16 +519,16 @@ internal Uri BuildDetailsRequest(DetailsParameters parameters) if (parameters.Fields != null && parameters.Fields.Count > 0) { - query.Add("fields", string.Join(",", parameters.Fields)); + query = query.Add("fields", string.Join(",", parameters.Fields)); } else { _logger.GoogleDebug(_localizer["Invalid Fields"]); } - AddBaseParameters(parameters, query); + AddBaseParameters(parameters, ref query); - AddGoogleKey(query); + AddGoogleKey(ref query); uriBuilder.Query = query.ToString(); @@ -543,7 +544,7 @@ internal Uri BuildDetailsRequest(DetailsParameters parameters) internal Uri BuildPlaceAutocompleteRequest(PlacesAutocompleteParameters parameters) { var uriBuilder = new UriBuilder(PlaceAutocompleteUri); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); + var query = QueryString.Empty; if (string.IsNullOrWhiteSpace(parameters.Input)) { @@ -554,7 +555,7 @@ internal Uri BuildPlaceAutocompleteRequest(PlacesAutocompleteParameters paramete if (!string.IsNullOrWhiteSpace(parameters.SessionToken)) { - query.Add("sessiontoken", parameters.SessionToken); + query = query.Add("sessiontoken", parameters.SessionToken); } else { @@ -563,7 +564,7 @@ internal Uri BuildPlaceAutocompleteRequest(PlacesAutocompleteParameters paramete if (parameters.Origin != null) { - query.Add("origin", parameters.Origin.ToString()); + query = query.Add("origin", parameters.Origin.ToString()); } else { @@ -572,7 +573,7 @@ internal Uri BuildPlaceAutocompleteRequest(PlacesAutocompleteParameters paramete if (parameters.Types != null && parameters.Types.Count > 0) { - query.Add("types", string.Join(",", parameters.Types.Select(x => x.ToEnumString()))); + query = query.Add("types", string.Join(",", parameters.Types.Select(x => x.ToEnumString()))); } else { @@ -581,7 +582,7 @@ internal Uri BuildPlaceAutocompleteRequest(PlacesAutocompleteParameters paramete if (parameters.Components != null) { - query.Add("components", parameters.Components.ToString()); + query = query.Add("components", parameters.Components.ToString()); } else { @@ -589,12 +590,12 @@ internal Uri BuildPlaceAutocompleteRequest(PlacesAutocompleteParameters paramete } #pragma warning disable CA1308 // Normalize strings to uppercase - query.Add("strictbounds", parameters.StrictBounds.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + query = query.Add("strictbounds", parameters.StrictBounds.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); #pragma warning restore CA1308 // Normalize strings to uppercase - AddAutocompleteParameters(parameters, query); + AddAutocompleteParameters(parameters, ref query); - AddGoogleKey(query); + AddGoogleKey(ref query); uriBuilder.Query = query.ToString(); @@ -610,7 +611,7 @@ internal Uri BuildPlaceAutocompleteRequest(PlacesAutocompleteParameters paramete internal Uri BuildQueryAutocompleteRequest(QueryAutocompleteParameters parameters) { var uriBuilder = new UriBuilder(QueryAutocompleteUri); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); + var query = QueryString.Empty; if (string.IsNullOrWhiteSpace(parameters.Input)) { @@ -619,9 +620,9 @@ internal Uri BuildQueryAutocompleteRequest(QueryAutocompleteParameters parameter throw new ArgumentException(error, nameof(parameters.Input)); } - AddAutocompleteParameters(parameters, query); + AddAutocompleteParameters(parameters, ref query); - AddGoogleKey(query); + AddGoogleKey(ref query); uriBuilder.Query = query.ToString(); @@ -632,12 +633,12 @@ internal Uri BuildQueryAutocompleteRequest(QueryAutocompleteParameters parameter /// Adds the autocomplete parameters based on the allowed logic. /// /// A with the autocomplete parameters to build the uri with. - /// A with the query parameters. - internal void AddAutocompleteParameters(QueryAutocompleteParameters parameters, NameValueCollection query) + /// A with the query parameters. + internal void AddAutocompleteParameters(QueryAutocompleteParameters parameters, ref QueryString query) { if (parameters.Offset > 0) { - query.Add("offset", parameters.Offset.ToString(CultureInfo.InvariantCulture)); + query = query.Add("offset", parameters.Offset.ToString(CultureInfo.InvariantCulture)); } else { @@ -646,26 +647,26 @@ internal void AddAutocompleteParameters(QueryAutocompleteParameters parameters, if (!string.IsNullOrWhiteSpace(parameters.Input)) { - query.Add("input", parameters.Input); + query = query.Add("input", parameters.Input); } else { _logger.GoogleDebug(_localizer["Invalid Input Info"]); } - AddCoordinateParameters(parameters, query); + AddCoordinateParameters(parameters, ref query); } /// /// Adds the base search parameters based on the allowed logic. /// /// A with the base search parameters to build the uri with. - /// A with the query parameters. - internal void AddBaseSearchParameters(BaseSearchParameters parameters, NameValueCollection query) + /// A with the query parameters. + internal void AddBaseSearchParameters(BaseSearchParameters parameters, ref QueryString query) { if (parameters.MinimumPrice >= 0 && parameters.MinimumPrice <= 4 && parameters.MinimumPrice <= parameters.MaximumPrice) { - query.Add("minprice", parameters.MinimumPrice.ToString(CultureInfo.InvariantCulture)); + query = query.Add("minprice", parameters.MinimumPrice.ToString(CultureInfo.InvariantCulture)); } else { @@ -674,7 +675,7 @@ internal void AddBaseSearchParameters(BaseSearchParameters parameters, NameValue if (parameters.MaximumPrice >= 0 && parameters.MaximumPrice <= 4 && parameters.MinimumPrice <= parameters.MaximumPrice) { - query.Add("maxprice", parameters.MaximumPrice.ToString(CultureInfo.InvariantCulture)); + query = query.Add("maxprice", parameters.MaximumPrice.ToString(CultureInfo.InvariantCulture)); } else { @@ -682,12 +683,12 @@ internal void AddBaseSearchParameters(BaseSearchParameters parameters, NameValue } #pragma warning disable CA1308 // Normalize strings to uppercase - query.Add("opennow", parameters.OpenNow.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + query = query.Add("opennow", parameters.OpenNow.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); #pragma warning restore CA1308 // Normalize strings to uppercase if (parameters.PageToken > 0) { - query.Add("pagetoken", parameters.PageToken.ToString(CultureInfo.InvariantCulture)); + query = query.Add("pagetoken", parameters.PageToken.ToString(CultureInfo.InvariantCulture)); } else { @@ -696,26 +697,26 @@ internal void AddBaseSearchParameters(BaseSearchParameters parameters, NameValue if (!string.IsNullOrWhiteSpace(parameters.Type)) { - query.Add("type", parameters.Type); + query = query.Add("type", parameters.Type); } else { _logger.GoogleDebug(_localizer["Invalid Type"]); } - AddCoordinateParameters(parameters, query); + AddCoordinateParameters(parameters, ref query); } /// /// Adds the coordinate parameters based on the allowed logic. /// /// A with the coordinate parameters to build the uri with. - /// A with the query parameters. - internal void AddCoordinateParameters(CoordinateParameters parameters, NameValueCollection query) + /// A with the query parameters. + internal void AddCoordinateParameters(CoordinateParameters parameters, ref QueryString query) { if (parameters.Location != null) { - query.Add("location", parameters.Location.ToString()); + query = query.Add("location", parameters.Location.ToString()); } else { @@ -724,26 +725,26 @@ internal void AddCoordinateParameters(CoordinateParameters parameters, NameValue if (parameters.Radius > 0 && parameters.Radius <= 50000) { - query.Add("radius", parameters.Radius.ToString(CultureInfo.InvariantCulture)); + query = query.Add("radius", parameters.Radius.ToString(CultureInfo.InvariantCulture)); } else { _logger.GoogleWarning(_localizer["Invalid Radius Value"]); } - AddBaseParameters(parameters, query); + AddBaseParameters(parameters, ref query); } /// /// Adds the base query parameters based on the allowed logic. /// /// A with the base parameters to build the uri with. - /// A with the query parameters. - internal void AddBaseParameters(BaseParameters parameters, NameValueCollection query) + /// A with the query parameters. + internal void AddBaseParameters(BaseParameters parameters, ref QueryString query) { if (parameters.Language != null) { - query.Add("language", parameters.Language.Name); + query = query.Add("language", parameters.Language.Name); } else { @@ -754,10 +755,10 @@ internal void AddBaseParameters(BaseParameters parameters, NameValueCollection q /// /// Adds the Google key to the query parameters. /// - /// A with the query parameters. - internal void AddGoogleKey(NameValueCollection query) + /// A with the query parameters. + internal void AddGoogleKey(ref QueryString query) { - query.Add("key", _keyContainer.GetKey()); + query = query.Add("key", _keyContainer.GetKey()); } /// diff --git a/src/Geo.Here/Models/Exceptions/HereException.cs b/src/Geo.Here/Models/Exceptions/HereException.cs index b6d5100..96a59eb 100644 --- a/src/Geo.Here/Models/Exceptions/HereException.cs +++ b/src/Geo.Here/Models/Exceptions/HereException.cs @@ -9,6 +9,7 @@ namespace Geo.Here.Models.Exceptions using System.Globalization; using System.Net.Http; using System.Threading.Tasks; + using Geo.Core.Models.Exceptions; using Newtonsoft.Json; /// @@ -23,7 +24,7 @@ namespace Geo.Here.Models.Exceptions /// Thrown when the HERE request is cancelled. /// Thrown when an error occurs while reading the return JSON text. /// Thrown when when an error occurs during JSON deserialization. - public class HereException : Exception + public sealed class HereException : GeoCoreException { private const string DefaultMessage = "{0} See the inner exception for more information."; diff --git a/src/Geo.Here/Services/HereGeocoding.cs b/src/Geo.Here/Services/HereGeocoding.cs index 6cb44ef..2d6a3db 100644 --- a/src/Geo.Here/Services/HereGeocoding.cs +++ b/src/Geo.Here/Services/HereGeocoding.cs @@ -6,18 +6,17 @@ namespace Geo.Here.Services { using System; - using System.Collections.Specialized; using System.Globalization; using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; - using System.Web; using Geo.Core; using Geo.Here.Abstractions; using Geo.Here.Models.Exceptions; using Geo.Here.Models.Parameters; using Geo.Here.Models.Responses; + using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -44,18 +43,20 @@ public class HereGeocoding : ClientExecutor, IHereGeocoding /// /// A used for placing calls to the HERE Geocoding API. /// A used for fetching the HERE key. - /// A used to create a localizer for localizing log or exception messages. - /// A used for logging information. + /// An used to provide exceptions based on an exception type. + /// An used to create a localizer for localizing log or exception messages. + /// An used to create a logger used for logging information. public HereGeocoding( HttpClient client, IHereKeyContainer keyContainer, + IGeoNETExceptionProvider exceptionProvider, IStringLocalizerFactory localizerFactory, - ILogger logger = null) - : base(client, localizerFactory) + ILoggerFactory loggerFactory = null) + : base(client, exceptionProvider, localizerFactory, loggerFactory) { _keyContainer = keyContainer ?? throw new ArgumentNullException(nameof(keyContainer)); _localizer = localizerFactory?.Create(typeof(HereGeocoding)) ?? throw new ArgumentNullException(nameof(localizerFactory)); - _logger = logger ?? NullLogger.Instance; + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; } /// @@ -155,7 +156,7 @@ internal Uri ValidateAndBuildUri(TParameters parameters, Func 0) { - query.Add("in", string.Join(",", parameters.InCountry.Select(x => x.ThreeLetterISORegionName))); + query = query.Add("in", string.Join(",", parameters.InCountry.Select(x => x.ThreeLetterISORegionName))); } else { _logger.HereDebug(_localizer["Invalid In Country"]); } - AddLocatingParameters(parameters, query); + AddLocatingParameters(parameters, ref query); - AddHereKey(query); + AddHereKey(ref query); uriBuilder.Query = query.ToString(); @@ -209,7 +210,7 @@ internal Uri BuildGeocodingRequest(GeocodeParameters parameters) internal Uri BuildReverseGeocodingRequest(ReverseGeocodeParameters parameters) { var uriBuilder = new UriBuilder(ReverseGeocodeUri); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); + var query = QueryString.Empty; if (parameters.At is null) { @@ -218,9 +219,9 @@ internal Uri BuildReverseGeocodingRequest(ReverseGeocodeParameters parameters) throw new ArgumentException(error, nameof(parameters.At)); } - AddLocatingParameters(parameters, query); + AddLocatingParameters(parameters, ref query); - AddHereKey(query); + AddHereKey(ref query); uriBuilder.Query = query.ToString(); @@ -236,7 +237,7 @@ internal Uri BuildReverseGeocodingRequest(ReverseGeocodeParameters parameters) internal Uri BuildDiscoverRequest(DiscoverParameters parameters) { var uriBuilder = new UriBuilder(DiscoverUri); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); + var query = QueryString.Empty; if (string.IsNullOrWhiteSpace(parameters.Query)) { @@ -245,11 +246,11 @@ internal Uri BuildDiscoverRequest(DiscoverParameters parameters) throw new ArgumentException(error, nameof(parameters.Query)); } - query.Add("q", parameters.Query); + query = query.Add("q", parameters.Query); - AddBoundingParameters(parameters, query); + AddBoundingParameters(parameters, ref query); - AddHereKey(query); + AddHereKey(ref query); uriBuilder.Query = query.ToString(); @@ -265,7 +266,7 @@ internal Uri BuildDiscoverRequest(DiscoverParameters parameters) internal Uri BuildAutosuggestRequest(AutosuggestParameters parameters) { var uriBuilder = new UriBuilder(AutosuggestUri); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); + var query = QueryString.Empty; if (string.IsNullOrWhiteSpace(parameters.Query)) { @@ -274,20 +275,20 @@ internal Uri BuildAutosuggestRequest(AutosuggestParameters parameters) throw new ArgumentException(error, nameof(parameters.Query)); } - query.Add("q", parameters.Query); + query = query.Add("q", parameters.Query); if (parameters.TermsLimit <= 10) { - query.Add("termsLimit", parameters.TermsLimit.ToString(CultureInfo.InvariantCulture)); + query = query.Add("termsLimit", parameters.TermsLimit.ToString(CultureInfo.InvariantCulture)); } else { _logger.HereWarning(_localizer["Invalid Terms Limit"]); } - AddBoundingParameters(parameters, query); + AddBoundingParameters(parameters, ref query); - AddHereKey(query); + AddHereKey(ref query); uriBuilder.Query = query.ToString(); @@ -303,7 +304,7 @@ internal Uri BuildAutosuggestRequest(AutosuggestParameters parameters) internal Uri BuildBrowseRequest(BrowseParameters parameters) { var uriBuilder = new UriBuilder(BrowseUri); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); + var query = QueryString.Empty; if (parameters.At is null) { @@ -314,7 +315,7 @@ internal Uri BuildBrowseRequest(BrowseParameters parameters) if (!string.IsNullOrWhiteSpace(parameters.Categories)) { - query.Add("categories", parameters.Categories); + query = query.Add("categories", parameters.Categories); } else { @@ -323,16 +324,16 @@ internal Uri BuildBrowseRequest(BrowseParameters parameters) if (!string.IsNullOrWhiteSpace(parameters.Name)) { - query.Add("name", parameters.Name); + query = query.Add("name", parameters.Name); } else { _logger.HereDebug(_localizer["Invalid Name"]); } - AddBoundingParameters(parameters, query); + AddBoundingParameters(parameters, ref query); - AddHereKey(query); + AddHereKey(ref query); uriBuilder.Query = query.ToString(); @@ -348,7 +349,7 @@ internal Uri BuildBrowseRequest(BrowseParameters parameters) internal Uri BuildLookupRequest(LookupParameters parameters) { var uriBuilder = new UriBuilder(LookupUri); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); + var query = QueryString.Empty; if (string.IsNullOrWhiteSpace(parameters.Id)) { @@ -357,11 +358,11 @@ internal Uri BuildLookupRequest(LookupParameters parameters) throw new ArgumentException(error, nameof(parameters.Id)); } - query.Add("id", parameters.Id); + query = query.Add("id", parameters.Id); - AddBaseParameters(parameters, query); + AddBaseParameters(parameters, ref query); - AddHereKey(query); + AddHereKey(ref query); uriBuilder.Query = query.ToString(); @@ -372,8 +373,8 @@ internal Uri BuildLookupRequest(LookupParameters parameters) /// Adds the bounding query parameters based on the allowed logic. /// /// A with the bounding parameters to build the uri with. - /// A with the query parameters. - internal void AddBoundingParameters(AreaParameters parameters, NameValueCollection query) + /// A with the query parameters. + internal void AddBoundingParameters(AreaParameters parameters, ref QueryString query) { var hasAt = parameters.At != null && parameters.At.IsValid(); var hasCircle = parameters.InCircle != null && parameters.InCircle.IsValid(); @@ -399,7 +400,7 @@ internal void AddBoundingParameters(AreaParameters parameters, NameValueCollecti if (hasAt) { - query.Add("at", parameters.At.ToString()); + query = query.Add("at", parameters.At.ToString()); } else { @@ -408,7 +409,7 @@ internal void AddBoundingParameters(AreaParameters parameters, NameValueCollecti if (!string.IsNullOrWhiteSpace(parameters.InCountry)) { - query.Add("in", $"countryCode:{parameters.InCountry}"); + query = query.Add("in", $"countryCode:{parameters.InCountry}"); } else { @@ -417,7 +418,7 @@ internal void AddBoundingParameters(AreaParameters parameters, NameValueCollecti if (hasCircle) { - query.Add("in", $"circle:{parameters.InCircle}"); + query = query.Add("in", $"circle:{parameters.InCircle}"); } else { @@ -426,7 +427,7 @@ internal void AddBoundingParameters(AreaParameters parameters, NameValueCollecti if (hasBoundingBox) { - query.Add("in", $"bbox:{parameters.InBoundingBox}"); + query = query.Add("in", $"bbox:{parameters.InBoundingBox}"); } else { @@ -435,64 +436,64 @@ internal void AddBoundingParameters(AreaParameters parameters, NameValueCollecti if (!string.IsNullOrWhiteSpace(parameters.Route)) { - query.Add("route", parameters.Route); + query = query.Add("route", parameters.Route); } else { _logger.HereDebug(_localizer["Invalid Route"]); } - AddLimitingParameters(parameters, query); + AddLimitingParameters(parameters, ref query); } /// /// Adds the locating query parameters based on the allowed logic. /// /// A with the base limiting parameters to build the uri with. - /// A with the query parameters. - internal void AddLocatingParameters(BaseFilterParameters parameters, NameValueCollection query) + /// A with the query parameters. + internal void AddLocatingParameters(BaseFilterParameters parameters, ref QueryString query) { if (parameters.At != null) { - query.Add("at", parameters.At.ToString()); + query = query.Add("at", parameters.At.ToString()); } else { _logger.HereDebug(_localizer["Invalid At Debug"]); } - AddLimitingParameters(parameters, query); + AddLimitingParameters(parameters, ref query); } /// /// Adds the base limiting query parameters based on the allowed logic. /// /// A with the base limiting parameters to build the uri with. - /// A with the query parameters. - internal void AddLimitingParameters(BaseFilterParameters parameters, NameValueCollection query) + /// A with the query parameters. + internal void AddLimitingParameters(BaseFilterParameters parameters, ref QueryString query) { if (parameters.Limit > 0 && parameters.Limit <= 100) { - query.Add("limit", parameters.Limit.ToString(CultureInfo.InvariantCulture)); + query = query.Add("limit", parameters.Limit.ToString(CultureInfo.InvariantCulture)); } else { _logger.HereDebug(_localizer["Invalid Limit"]); } - AddBaseParameters(parameters, query); + AddBaseParameters(parameters, ref query); } /// /// Adds the base query parameters based on the allowed logic. /// /// A with the base parameters to build the uri with. - /// A with the query parameters. - internal void AddBaseParameters(BaseParameters parameters, NameValueCollection query) + /// A with the query parameters. + internal void AddBaseParameters(BaseParameters parameters, ref QueryString query) { if (parameters.Language != null) { - query.Add("lang", parameters.Language.Name); + query = query.Add("lang", parameters.Language.Name); } else { @@ -503,10 +504,10 @@ internal void AddBaseParameters(BaseParameters parameters, NameValueCollection q /// /// Adds the HERE key to the query parameters. /// - /// A with the query parameters. - internal void AddHereKey(NameValueCollection query) + /// A with the query parameters. + internal void AddHereKey(ref QueryString query) { - query.Add("apiKey", _keyContainer.GetKey()); + query = query.Add("apiKey", _keyContainer.GetKey()); } } } diff --git a/src/Geo.MapBox/Models/Exceptions/MapBoxException.cs b/src/Geo.MapBox/Models/Exceptions/MapBoxException.cs index c776f52..0a9a7fb 100644 --- a/src/Geo.MapBox/Models/Exceptions/MapBoxException.cs +++ b/src/Geo.MapBox/Models/Exceptions/MapBoxException.cs @@ -9,6 +9,7 @@ namespace Geo.MapBox.Models.Exceptions using System.Globalization; using System.Net.Http; using System.Threading.Tasks; + using Geo.Core.Models.Exceptions; using Newtonsoft.Json; /// @@ -23,7 +24,7 @@ namespace Geo.MapBox.Models.Exceptions /// Thrown when the MapBox request is cancelled. /// Thrown when an error occurs while reading the return JSON text. /// Thrown when when an error occurs during JSON deserialization. - public class MapBoxException : Exception + public sealed class MapBoxException : GeoCoreException { private const string DefaultMessage = "{0} See the inner exception for more information."; diff --git a/src/Geo.MapBox/README.md b/src/Geo.MapBox/README.md index 6c91244..781c0af 100644 --- a/src/Geo.MapBox/README.md +++ b/src/Geo.MapBox/README.md @@ -37,7 +37,7 @@ public MyService(IMapBoxGeocoding mapBoxGeocoding) Now simply call the geocoding methods in the interface. ## Endpoint Types -MapQuest has 2 endpoint types, permanent and non-permanent. +MapBox has 2 endpoint types, permanent and non-permanent. - Requests to the non-permanent endpoint must be triggered by user activity. Any results must be displayed on a Mapbox map and cannot be stored permanently, as described in Mapbox�s [terms of service](https://www.mapbox.com/tos/#geocoding). - The permanent endpoint gives you access to two services: permanent geocoding and batch geocoding. \ No newline at end of file diff --git a/src/Geo.MapBox/Services/MapBoxGeocoding.cs b/src/Geo.MapBox/Services/MapBoxGeocoding.cs index be965fa..7054b57 100644 --- a/src/Geo.MapBox/Services/MapBoxGeocoding.cs +++ b/src/Geo.MapBox/Services/MapBoxGeocoding.cs @@ -7,13 +7,12 @@ namespace Geo.MapBox.Services { using System; using System.Collections.Generic; - using System.Collections.Specialized; using System.Globalization; using System.Linq; using System.Net.Http; + using System.Text.Encodings.Web; using System.Threading; using System.Threading.Tasks; - using System.Web; using Geo.Core; using Geo.MapBox.Abstractions; using Geo.MapBox.Enums; @@ -21,6 +20,7 @@ namespace Geo.MapBox.Services using Geo.MapBox.Models.Exceptions; using Geo.MapBox.Models.Parameters; using Geo.MapBox.Models.Responses; + using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -45,18 +45,20 @@ public class MapBoxGeocoding : ClientExecutor, IMapBoxGeocoding /// /// A used for placing calls to the here Geocoding API. /// A used for fetching the here key. - /// A used to create a localizer for localizing log or exception messages. - /// A used for logging information. + /// An used to provide exceptions based on an exception type. + /// An used to create a localizer for localizing log or exception messages. + /// An used to create a logger used for logging information. public MapBoxGeocoding( HttpClient client, IMapBoxKeyContainer keyContainer, + IGeoNETExceptionProvider exceptionProvider, IStringLocalizerFactory localizerFactory, - ILogger logger = null) - : base(client, localizerFactory) + ILoggerFactory loggerFactory = null) + : base(client, exceptionProvider, localizerFactory, loggerFactory) { _keyContainer = keyContainer ?? throw new ArgumentNullException(nameof(keyContainer)); _localizer = localizerFactory?.Create(typeof(MapBoxGeocoding)) ?? throw new ArgumentNullException(nameof(localizerFactory)); - _logger = logger ?? NullLogger.Instance; + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; } /// @@ -122,16 +124,16 @@ internal Uri BuildGeocodingRequest(GeocodingParameters parameters) throw new ArgumentException(error, nameof(parameters.Query)); } - var uriBuilder = new UriBuilder(string.Format(CultureInfo.InvariantCulture, GeocodeUri, parameters.EndpointType == EndpointType.Places ? PlacesEndpoint : PermanentEndpoint, parameters.Query)); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); + var uriBuilder = new UriBuilder(string.Format(CultureInfo.InvariantCulture, GeocodeUri, parameters.EndpointType == EndpointType.Places ? PlacesEndpoint : PermanentEndpoint, UrlEncoder.Default.Encode(parameters.Query))); + var query = QueryString.Empty; #pragma warning disable CA1308 // Normalize strings to uppercase - query.Add("autocomplete", parameters.ReturnAutocomplete.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + query = query.Add("autocomplete", parameters.ReturnAutocomplete.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); #pragma warning restore CA1308 // Normalize strings to uppercase if (parameters.BoundingBox != null) { - query.Add("bbox", parameters.BoundingBox.ToString()); + query = query.Add("bbox", parameters.BoundingBox.ToString()); } else { @@ -139,21 +141,21 @@ internal Uri BuildGeocodingRequest(GeocodingParameters parameters) } #pragma warning disable CA1308 // Normalize strings to uppercase - query.Add("fuzzyMatch", parameters.FuzzyMatch.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + query = query.Add("fuzzyMatch", parameters.FuzzyMatch.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); #pragma warning restore CA1308 // Normalize strings to uppercase if (parameters.Proximity != null) { - query.Add("proximity", parameters.Proximity.ToString()); + query = query.Add("proximity", parameters.Proximity.ToString()); } else { _logger.MapBoxDebug(_localizer["Invalid Proximity"]); } - AddBaseParameters(parameters, query); + AddBaseParameters(parameters, ref query); - AddMapBoxKey(query); + AddMapBoxKey(ref query); uriBuilder.Query = query.ToString(); @@ -176,15 +178,15 @@ internal Uri BuildReverseGeocodingRequest(ReverseGeocodingParameters parameters) } var uriBuilder = new UriBuilder(string.Format(CultureInfo.InvariantCulture, ReverseGeocodeUri, parameters.EndpointType == EndpointType.Places ? PlacesEndpoint : PermanentEndpoint, parameters.Coordinate)); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); + var query = QueryString.Empty; #pragma warning disable CA1308 // Normalize strings to uppercase - query.Add("reverseMode", parameters.ReverseMode.ToString().ToLowerInvariant()); + query = query.Add("reverseMode", parameters.ReverseMode.ToString().ToLowerInvariant()); #pragma warning restore CA1308 // Normalize strings to uppercase - AddBaseParameters(parameters, query); + AddBaseParameters(parameters, ref query); - AddMapBoxKey(query); + AddMapBoxKey(ref query); uriBuilder.Query = query.ToString(); @@ -195,12 +197,12 @@ internal Uri BuildReverseGeocodingRequest(ReverseGeocodingParameters parameters) /// Adds the base query parameters based on the allowed logic. /// /// A with the base parameters to build the uri with. - /// A with the query parameters. - internal void AddBaseParameters(BaseParameters parameters, NameValueCollection query) + /// A with the query parameters. + internal void AddBaseParameters(BaseParameters parameters, ref QueryString query) { if (parameters.Countries.Count > 0) { - query.Add("country", string.Join(",", parameters.Countries.Select(x => x.TwoLetterISORegionName))); + query = query.Add("country", string.Join(",", parameters.Countries.Select(x => x.TwoLetterISORegionName))); } else { @@ -209,7 +211,7 @@ internal void AddBaseParameters(BaseParameters parameters, NameValueCollection q if (parameters.Languages.Count > 0) { - query.Add("language", string.Join(",", parameters.Languages.Select(x => x.Name))); + query = query.Add("language", string.Join(",", parameters.Languages.Select(x => x.Name))); } else { @@ -218,7 +220,7 @@ internal void AddBaseParameters(BaseParameters parameters, NameValueCollection q if (parameters.Limit > 0 && parameters.Limit < 6) { - query.Add("limit", parameters.Limit.ToString(CultureInfo.InvariantCulture)); + query = query.Add("limit", parameters.Limit.ToString(CultureInfo.InvariantCulture)); } else { @@ -226,13 +228,13 @@ internal void AddBaseParameters(BaseParameters parameters, NameValueCollection q } #pragma warning disable CA1308 // Normalize strings to uppercase - query.Add("routing", parameters.Routing.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + query = query.Add("routing", parameters.Routing.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); #pragma warning restore CA1308 // Normalize strings to uppercase if (parameters.Types != null && parameters.Types.Count > 0) { #pragma warning disable CA1308 // Normalize strings to uppercase - query.Add("types", string.Join(",", parameters.Types.Select(x => x.ToString().ToLowerInvariant()))); + query = query.Add("types", string.Join(",", parameters.Types.Select(x => x.ToString().ToLowerInvariant()))); #pragma warning restore CA1308 // Normalize strings to uppercase } else @@ -244,10 +246,10 @@ internal void AddBaseParameters(BaseParameters parameters, NameValueCollection q /// /// Adds the here key to the query parameters. /// - /// A with the query parameters. - internal void AddMapBoxKey(NameValueCollection query) + /// A with the query parameters. + internal void AddMapBoxKey(ref QueryString query) { - query.Add("access_token", _keyContainer.GetKey()); + query = query.Add("access_token", _keyContainer.GetKey()); } } } diff --git a/src/Geo.MapQuest/Models/Exceptions/MapQuestException.cs b/src/Geo.MapQuest/Models/Exceptions/MapQuestException.cs index 67cec43..a4c977c 100644 --- a/src/Geo.MapQuest/Models/Exceptions/MapQuestException.cs +++ b/src/Geo.MapQuest/Models/Exceptions/MapQuestException.cs @@ -9,6 +9,7 @@ namespace Geo.MapQuest.Models.Exceptions using System.Globalization; using System.Net.Http; using System.Threading.Tasks; + using Geo.Core.Models.Exceptions; using Newtonsoft.Json; /// @@ -23,7 +24,7 @@ namespace Geo.MapQuest.Models.Exceptions /// Thrown when the MapQuest request is cancelled. /// Thrown when an error occurs while reading the return JSON text. /// Thrown when when an error occurs during JSON deserialization. - public class MapQuestException : Exception + public sealed class MapQuestException : GeoCoreException { private const string DefaultMessage = "{0} See the inner exception for more information."; diff --git a/src/Geo.MapQuest/Services/MapQuestGeocoding.cs b/src/Geo.MapQuest/Services/MapQuestGeocoding.cs index b48f88f..c789bc4 100644 --- a/src/Geo.MapQuest/Services/MapQuestGeocoding.cs +++ b/src/Geo.MapQuest/Services/MapQuestGeocoding.cs @@ -6,18 +6,17 @@ namespace Geo.MapQuest.Services { using System; - using System.Collections.Specialized; using System.Globalization; using System.Net.Http; using System.Threading; using System.Threading.Tasks; - using System.Web; using Geo.Core; using Geo.MapQuest.Abstractions; using Geo.MapQuest.Enums; using Geo.MapQuest.Models.Exceptions; using Geo.MapQuest.Models.Parameters; using Geo.MapQuest.Models.Responses; + using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -44,20 +43,22 @@ public class MapQuestGeocoding : ClientExecutor, IMapQuestGeocoding /// A used for placing calls to the MapQuest Geocoding API. /// A used for fetching the MapQuest key. /// A used for fetching which MapQuest endpoint to use. - /// A used to create a localizer for localizing log or exception messages. - /// A used for logging information. + /// An used to provide exceptions based on an exception type. + /// An used to create a localizer for localizing log or exception messages. + /// An used to create a logger used for logging information. public MapQuestGeocoding( HttpClient client, IMapQuestKeyContainer keyContainer, IMapQuestEndpoint endpoint, + IGeoNETExceptionProvider exceptionProvider, IStringLocalizerFactory localizerFactory, - ILogger logger = null) - : base(client, localizerFactory) + ILoggerFactory loggerFactory = null) + : base(client, exceptionProvider, localizerFactory, loggerFactory) { _keyContainer = keyContainer ?? throw new ArgumentNullException(nameof(keyContainer)); _endpoint = endpoint ?? throw new ArgumentNullException(nameof(endpoint)); _localizer = localizerFactory?.Create(typeof(MapQuestGeocoding)) ?? throw new ArgumentNullException(nameof(localizerFactory)); - _logger = logger ?? NullLogger.Instance; + _logger = loggerFactory?.CreateLogger() ?? NullLogger.Instance; } /// @@ -117,7 +118,7 @@ internal Uri ValidateAndBuildUri(TParameters parameters, Func 0) { - query.Add("maxResults", parameters.MaxResults.ToString(CultureInfo.InvariantCulture)); + query = query.Add("maxResults", parameters.MaxResults.ToString(CultureInfo.InvariantCulture)); } else { @@ -154,24 +155,24 @@ internal Uri BuildGeocodingRequest(GeocodingParameters parameters) if (parameters.IntlMode == InternationalMode.FiveBox) { - query.Add("intlMode", "5BOX"); + query = query.Add("intlMode", "5BOX"); } else if (parameters.IntlMode == InternationalMode.OneBox) { - query.Add("intlMode", "1BOX"); + query = query.Add("intlMode", "1BOX"); } else if (parameters.IntlMode == InternationalMode.Auto) { - query.Add("intlMode", "AUTO"); + query = query.Add("intlMode", "AUTO"); } else { _logger.MapQuestWarning(_localizer["Invalid Intl Mode"]); } - AddBaseParameters(parameters, query); + AddBaseParameters(parameters, ref query); - AddMapQuestKey(query); + AddMapQuestKey(ref query); uriBuilder.Query = query.ToString(); @@ -187,7 +188,7 @@ internal Uri BuildGeocodingRequest(GeocodingParameters parameters) internal Uri BuildReverseGeocodingRequest(ReverseGeocodingParameters parameters) { var uriBuilder = new UriBuilder(_endpoint.UseLicensedEndpoint() ? ReverseGeocodeUri : OpenReverseGeocodeUri); - var query = HttpUtility.ParseQueryString(uriBuilder.Query); + var query = QueryString.Empty; if (parameters.Location is null) { @@ -196,19 +197,19 @@ internal Uri BuildReverseGeocodingRequest(ReverseGeocodingParameters parameters) throw new ArgumentException(error, nameof(parameters.Location)); } - query.Add("location", parameters.Location.ToString()); + query = query.Add("location", parameters.Location.ToString()); #pragma warning disable CA1308 // Normalize strings to uppercase - query.Add("includeNearestIntersection", parameters.IncludeNearestIntersection.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + query = query.Add("includeNearestIntersection", parameters.IncludeNearestIntersection.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); #pragma warning restore CA1308 // Normalize strings to uppercase #pragma warning disable CA1308 // Normalize strings to uppercase - query.Add("includeRoadMetadata", parameters.IncludeRoadMetadata.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + query = query.Add("includeRoadMetadata", parameters.IncludeRoadMetadata.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); #pragma warning restore CA1308 // Normalize strings to uppercase - AddBaseParameters(parameters, query); + AddBaseParameters(parameters, ref query); - AddMapQuestKey(query); + AddMapQuestKey(ref query); uriBuilder.Query = query.ToString(); @@ -219,21 +220,21 @@ internal Uri BuildReverseGeocodingRequest(ReverseGeocodingParameters parameters) /// Adds the base query parameters based on the allowed logic. /// /// A with the base parameters to build the uri with. - /// A with the query parameters. - internal void AddBaseParameters(BaseParameters parameters, NameValueCollection query) + /// A with the query parameters. + internal void AddBaseParameters(BaseParameters parameters, ref QueryString query) { #pragma warning disable CA1308 // Normalize strings to uppercase - query.Add("thumbMaps", parameters.IncludeThumbMaps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + query = query.Add("thumbMaps", parameters.IncludeThumbMaps.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); #pragma warning restore CA1308 // Normalize strings to uppercase } /// /// Adds the MapQuest key to the query parameters. /// - /// A with the query parameters. - internal void AddMapQuestKey(NameValueCollection query) + /// A with the query parameters. + internal void AddMapQuestKey(ref QueryString query) { - query.Add("key", _keyContainer.GetKey()); + query = query.Add("key", _keyContainer.GetKey()); } } } diff --git a/src/Source.props b/src/Source.props index 7e58f1c..a31909d 100644 --- a/src/Source.props +++ b/src/Source.props @@ -16,4 +16,13 @@ + + + + + + + + + diff --git a/test/Geo.ArcGIS.Tests/Services/ArcGISGeocodingShould.cs b/test/Geo.ArcGIS.Tests/Services/ArcGISGeocodingShould.cs index da5c8f2..66e2787 100644 --- a/test/Geo.ArcGIS.Tests/Services/ArcGISGeocodingShould.cs +++ b/test/Geo.ArcGIS.Tests/Services/ArcGISGeocodingShould.cs @@ -7,7 +7,6 @@ namespace Geo.ArcGIS.Tests.Services { using System; using System.Collections.Generic; - using System.Collections.Specialized; using System.Globalization; using System.Net; using System.Net.Http; @@ -22,6 +21,7 @@ namespace Geo.ArcGIS.Tests.Services using Geo.ArcGIS.Models.Responses; using Geo.ArcGIS.Services; using Geo.Core; + using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -36,6 +36,7 @@ public class ArcGISGeocodingShould : IDisposable { private readonly HttpClient _httpClient; private readonly Mock _mockTokenContainer; + private readonly IGeoNETExceptionProvider _exceptionProvider; private readonly IStringLocalizerFactory _localizerFactory; private readonly List _responseMessages = new List(); private bool _disposed; @@ -144,6 +145,7 @@ public ArcGISGeocodingShould() var options = Options.Create(new LocalizationOptions { ResourcesPath = "Resources" }); _localizerFactory = new ResourceManagerStringLocalizerFactory(options, NullLoggerFactory.Instance); _httpClient = new HttpClient(mockHandler.Object); + _exceptionProvider = new GeoNETExceptionProvider(); } /// @@ -162,11 +164,13 @@ public async Task AddArcGISTokenSuccessfully() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; - await sut.AddArcGISToken(query, CancellationToken.None).ConfigureAwait(false); - query.Count.Should().Be(1); - query["token"].Should().Be("token123"); + query = await sut.AddArcGISToken(query, CancellationToken.None).ConfigureAwait(false); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(1); + queryParameters["token"].Should().Be("token123"); } /// @@ -177,25 +181,29 @@ public void AddStorageParameterSuccessfully() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; var parameters = new StorageParameters() { ForStorage = false, }; - sut.AddStorageParameter(parameters, query); - query.Count.Should().Be(1); - query["forStorage"].Should().Be("false"); + sut.AddStorageParameter(parameters, ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(1); + queryParameters["forStorage"].Should().Be("false"); - query.Clear(); + query = QueryString.Empty; parameters = new StorageParameters() { ForStorage = true, }; - sut.AddStorageParameter(parameters, query); - query.Count.Should().Be(1); - query["forStorage"].Should().Be("true"); + sut.AddStorageParameter(parameters, ref query); + + queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(1); + queryParameters["forStorage"].Should().Be("true"); } /// @@ -568,7 +576,7 @@ protected virtual void Dispose(bool disposing) private ArcGISGeocoding BuildService() { - return new ArcGISGeocoding(_httpClient, _mockTokenContainer.Object, _localizerFactory); + return new ArcGISGeocoding(_httpClient, _mockTokenContainer.Object, _exceptionProvider, _localizerFactory); } } } \ No newline at end of file diff --git a/test/Geo.Bing.Tests/Services/BingGeocodingShould.cs b/test/Geo.Bing.Tests/Services/BingGeocodingShould.cs index 88ad5e9..8eb1afa 100644 --- a/test/Geo.Bing.Tests/Services/BingGeocodingShould.cs +++ b/test/Geo.Bing.Tests/Services/BingGeocodingShould.cs @@ -7,7 +7,6 @@ namespace Geo.Bing.Tests.Services { using System; using System.Collections.Generic; - using System.Collections.Specialized; using System.Globalization; using System.Net; using System.Net.Http; @@ -19,6 +18,7 @@ namespace Geo.Bing.Tests.Services using Geo.Bing.Models.Parameters; using Geo.Bing.Services; using Geo.Core; + using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -33,6 +33,7 @@ public class BingGeocodingShould : IDisposable { private readonly HttpClient _httpClient; private readonly BingKeyContainer _keyContainer; + private readonly IGeoNETExceptionProvider _exceptionProvider; private readonly IStringLocalizerFactory _localizerFactory; private readonly List _responseMessages = new List(); private bool _disposed; @@ -129,6 +130,7 @@ public BingGeocodingShould() var options = Options.Create(new LocalizationOptions { ResourcesPath = "Resources" }); _localizerFactory = new ResourceManagerStringLocalizerFactory(options, NullLoggerFactory.Instance); _httpClient = new HttpClient(mockHandler.Object); + _exceptionProvider = new GeoNETExceptionProvider(); } /// @@ -146,11 +148,13 @@ public void AddBingKeySuccessfully() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; - sut.AddBingKey(query); - query.Count.Should().Be(1); - query["key"].Should().Be("123abc"); + sut.AddBingKey(ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(1); + queryParameters["key"].Should().Be("123abc"); } /// @@ -161,7 +165,7 @@ public void BuildBaseQuerySuccessfully() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; var parameters = new BaseParameters() { IncludeNeighbourhood = true, @@ -170,9 +174,11 @@ public void BuildBaseQuerySuccessfully() }; sut.BuildBaseQuery(parameters, ref query); - query.Count.Should().Be(2); - query["includeNeighborhood"].Should().Be("1"); - query["include"].Should().Contain("queryParse").And.Contain("ciso2"); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(2); + queryParameters["includeNeighborhood"].Should().Be("1"); + queryParameters["include"].Should().Contain("queryParse").And.Contain("ciso2"); } /// @@ -183,7 +189,7 @@ public void BuildLimitedResultQuerySuccessfully() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; var parameters = new ResultParameters() { MaximumResults = 7, @@ -193,10 +199,12 @@ public void BuildLimitedResultQuerySuccessfully() }; sut.BuildLimitedResultQuery(parameters, ref query); - query.Count.Should().Be(3); - query["maxResults"].Should().Be("7"); - query["includeNeighborhood"].Should().Be("1"); - query["include"].Should().Contain("queryParse").And.Contain("ciso2"); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(3); + queryParameters["maxResults"].Should().Be("7"); + queryParameters["includeNeighborhood"].Should().Be("1"); + queryParameters["include"].Should().Contain("queryParse").And.Contain("ciso2"); } /// @@ -432,7 +440,7 @@ protected virtual void Dispose(bool disposing) private BingGeocoding BuildService() { - return new BingGeocoding(_httpClient, _keyContainer, _localizerFactory); + return new BingGeocoding(_httpClient, _keyContainer, _exceptionProvider, _localizerFactory); } } } \ No newline at end of file diff --git a/test/Geo.Core.Tests/ClientExecutorShould.cs b/test/Geo.Core.Tests/ClientExecutorShould.cs index 80dee25..e150c5a 100644 --- a/test/Geo.Core.Tests/ClientExecutorShould.cs +++ b/test/Geo.Core.Tests/ClientExecutorShould.cs @@ -29,6 +29,7 @@ public class ClientExecutorShould : IDisposable { private const string ApiName = "Test"; private readonly HttpClient _httpClient; + private readonly IGeoNETExceptionProvider _exceptionProvider; private readonly IStringLocalizerFactory _localizerFactory; private readonly List _responseMessages = new List(); private bool _disposed; @@ -125,6 +126,7 @@ public ClientExecutorShould() var options = Options.Create(new LocalizationOptions { ResourcesPath = "Resources" }); _localizerFactory = new ResourceManagerStringLocalizerFactory(options, NullLoggerFactory.Instance); _httpClient = new HttpClient(mockHandler.Object); + _exceptionProvider = new GeoNETExceptionProvider(); } /// @@ -140,7 +142,7 @@ public void Dispose() [Fact] public void ThrowExceptionOnNullUri() { - var sut = new TestClientExecutor(_httpClient, _localizerFactory); + var sut = new TestClientExecutor(_httpClient, _exceptionProvider, _localizerFactory); sut.Invoking(x => x.CallAsync(new Uri("http://test.com/ArgumentNullException"))) .Should() @@ -154,7 +156,7 @@ public void ThrowExceptionOnNullUri() [Fact] public void ThrowExceptionOnInvalidUri() { - var sut = new TestClientExecutor(_httpClient, _localizerFactory); + var sut = new TestClientExecutor(_httpClient, _exceptionProvider, _localizerFactory); sut.Invoking(x => x.CallAsync(new Uri("http://test.com/InvalidOperationException"))) .Should() @@ -168,7 +170,7 @@ public void ThrowExceptionOnInvalidUri() [Fact] public void ThrowExceptionOnHttpFailure() { - var sut = new TestClientExecutor(_httpClient, _localizerFactory); + var sut = new TestClientExecutor(_httpClient, _exceptionProvider, _localizerFactory); sut.Invoking(x => x.CallAsync(new Uri("http://test.com/HttpRequestException"))) .Should() @@ -182,7 +184,7 @@ public void ThrowExceptionOnHttpFailure() [Fact] public void ThrowExceptionOnCancelledRequest() { - var sut = new TestClientExecutor(_httpClient, _localizerFactory); + var sut = new TestClientExecutor(_httpClient, _exceptionProvider, _localizerFactory); sut.Invoking(x => x.CallAsync(new Uri("http://test.com/TaskCanceledException"))) .Should() @@ -195,7 +197,7 @@ public void ThrowExceptionOnCancelledRequest() [Fact] public void ThrowExceptionOnInvalidJson1() { - var sut = new TestClientExecutor(_httpClient, _localizerFactory); + var sut = new TestClientExecutor(_httpClient, _exceptionProvider, _localizerFactory); sut.Invoking(x => x.CallAsync(new Uri("http://test.com/JsonReaderException"))) .Should() @@ -208,7 +210,7 @@ public void ThrowExceptionOnInvalidJson1() [Fact] public void ThrowExceptionOnInvalidJson2() { - var sut = new TestClientExecutor(_httpClient, _localizerFactory); + var sut = new TestClientExecutor(_httpClient, _exceptionProvider, _localizerFactory); sut.Invoking(x => x.CallAsync(new Uri("http://test.com/JsonSerializationException"))) .Should() @@ -222,10 +224,11 @@ public void ThrowExceptionOnInvalidJson2() [Fact] public async Task ReturnsErrorJson() { - var sut = new TestClientExecutor(_httpClient, _localizerFactory); + var sut = new TestClientExecutor(_httpClient, _exceptionProvider, _localizerFactory); var result = await sut.CallAsync(new Uri("http://test.com/Failure")).ConfigureAwait(false); + result.IsSuccessful.Should().BeFalse(); result.Result.Should().BeNull(); - result.JSON.Should().Be("{'Message':'Access denied'}"); + result.Body.Should().Be("{'Message':'Access denied'}"); } /// @@ -235,9 +238,10 @@ public async Task ReturnsErrorJson() [Fact] public async Task SuccesfullyReturnObject() { - var sut = new TestClientExecutor(_httpClient, _localizerFactory); + var sut = new TestClientExecutor(_httpClient, _exceptionProvider, _localizerFactory); var result = await sut.CallAsync(new Uri("http://test.com/Success")).ConfigureAwait(false); + result.IsSuccessful.Should().BeTrue(); result.Result.TestField.Should().Be(1); } @@ -247,7 +251,7 @@ public async Task SuccesfullyReturnObject() [Fact] public void ThrowWrappedExceptionOnNullUri() { - var sut = new TestClientExecutor(_httpClient, _localizerFactory); + var sut = new TestClientExecutor(_httpClient, _exceptionProvider, _localizerFactory); sut.Invoking(x => x.CallAsync(new Uri("http://test.com/ArgumentNullException"), ApiName)) .Should() @@ -261,7 +265,7 @@ public void ThrowWrappedExceptionOnNullUri() [Fact] public void ThrowWrappedExceptionOnInvalidUri() { - var sut = new TestClientExecutor(_httpClient, _localizerFactory); + var sut = new TestClientExecutor(_httpClient, _exceptionProvider, _localizerFactory); sut.Invoking(x => x.CallAsync(new Uri("http://test.com/InvalidOperationException"), ApiName)) .Should() @@ -275,7 +279,7 @@ public void ThrowWrappedExceptionOnInvalidUri() [Fact] public void ThrowWrappedExceptionOnHttpFailure() { - var sut = new TestClientExecutor(_httpClient, _localizerFactory); + var sut = new TestClientExecutor(_httpClient, _exceptionProvider, _localizerFactory); sut.Invoking(x => x.CallAsync(new Uri("http://test.com/HttpRequestException"), ApiName)) .Should() @@ -289,7 +293,7 @@ public void ThrowWrappedExceptionOnHttpFailure() [Fact] public void ThrowWrappedExceptionOnCancelledRequest() { - var sut = new TestClientExecutor(_httpClient, _localizerFactory); + var sut = new TestClientExecutor(_httpClient, _exceptionProvider, _localizerFactory); sut.Invoking(x => x.CallAsync(new Uri("http://test.com/TaskCanceledException"), ApiName)) .Should() @@ -303,7 +307,7 @@ public void ThrowWrappedExceptionOnCancelledRequest() [Fact] public void ThrowWrappedExceptionOnInvalidJson1() { - var sut = new TestClientExecutor(_httpClient, _localizerFactory); + var sut = new TestClientExecutor(_httpClient, _exceptionProvider, _localizerFactory); sut.Invoking(x => x.CallAsync(new Uri("http://test.com/JsonReaderException"), ApiName)) .Should() @@ -317,7 +321,7 @@ public void ThrowWrappedExceptionOnInvalidJson1() [Fact] public void ThrowWrappedExceptionOnInvalidJson2() { - var sut = new TestClientExecutor(_httpClient, _localizerFactory); + var sut = new TestClientExecutor(_httpClient, _exceptionProvider, _localizerFactory); sut.Invoking(x => x.CallAsync(new Uri("http://test.com/JsonSerializationException"), ApiName)) .Should() @@ -331,12 +335,16 @@ public void ThrowWrappedExceptionOnInvalidJson2() [Fact] public void ThrowWrappedExceptionOnErrorJson() { - var sut = new TestClientExecutor(_httpClient, _localizerFactory); + var sut = new TestClientExecutor(_httpClient, _exceptionProvider, _localizerFactory); sut.Invoking(x => x.CallAsync(new Uri("http://test.com/Failure"), ApiName)) .Should() .Throw() - .Where(x => x.Data.Count == 1 && x.Data["responseBody"].ToString() == "{'Message':'Access denied'}"); + .Where(x => + x.Data.Count == 3 && + x.Data["responseBody"].ToString() == "{'Message':'Access denied'}" && + (HttpStatusCode)x.Data["responseStatusCode"] == HttpStatusCode.Forbidden && + x.Data["uri"].ToString() == "http://test.com/Failure"); } /// @@ -346,7 +354,7 @@ public void ThrowWrappedExceptionOnErrorJson() [Fact] public async Task SuccesfullyReturnOnlyObject() { - var sut = new TestClientExecutor(_httpClient, _localizerFactory); + var sut = new TestClientExecutor(_httpClient, _exceptionProvider, _localizerFactory); var result = await sut.CallAsync(new Uri("http://test.com/Success"), ApiName).ConfigureAwait(false); result.TestField.Should().Be(1); diff --git a/test/Geo.Core.Tests/GeoNETExceptionProviderTests.cs b/test/Geo.Core.Tests/GeoNETExceptionProviderTests.cs new file mode 100644 index 0000000..7f921fa --- /dev/null +++ b/test/Geo.Core.Tests/GeoNETExceptionProviderTests.cs @@ -0,0 +1,47 @@ +// +// Copyright (c) Geo.NET. +// Licensed under the MIT license. See the LICENSE file in the solution root for full license information. +// + +namespace Geo.Core.Tests +{ + using System; + using FluentAssertions; + using Geo.Core.Tests.Models; + using Xunit; + + /// + /// Unit tests for the class. + /// + public class GeoNETExceptionProviderTests + { + [Fact] + public void GetException_WithoutInnerException_ReturnsException() + { + // Arrange + var sut = new GeoNETExceptionProvider(); + + // Act + var ex = sut.GetException("Test exception"); + + // Assert + ex.Message.Should().Be("Test exception"); + ex.InnerException.Should().BeNull(); + } + + [Fact] + public void GetException_WithInnerException_ReturnsException() + { + // Arrange + var sut = new GeoNETExceptionProvider(); + + // Act + var ex = sut.GetException("Test exception", new ArgumentNullException()); + + // Assert + ex.Message.Should().Be("Test exception"); + ex.InnerException.Should().NotBeNull(); + ex.InnerException.Should().BeOfType(); + } + } +} diff --git a/test/Geo.Core.Tests/GlobalSuppressions.cs b/test/Geo.Core.Tests/GlobalSuppressions.cs new file mode 100644 index 0000000..bd98412 --- /dev/null +++ b/test/Geo.Core.Tests/GlobalSuppressions.cs @@ -0,0 +1,9 @@ +// 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("Naming", "CA1707:Identifiers should not contain underscores", Justification = "Using Microsoft recommended unit test naming", Scope = "namespaceanddescendants", Target = "~N:Geo.Core.Tests")] +[assembly: SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1600:Elements should be documented", Justification = "The name of the test should explain the test", Scope = "namespaceanddescendants", Target = "~N:Geo.Core.Tests")] diff --git a/test/Geo.Core.Tests/Models/TestClientExecutor.cs b/test/Geo.Core.Tests/Models/TestClientExecutor.cs index 2a0e644..bd1be26 100644 --- a/test/Geo.Core.Tests/Models/TestClientExecutor.cs +++ b/test/Geo.Core.Tests/Models/TestClientExecutor.cs @@ -19,8 +19,11 @@ public class TestClientExecutor : ClientExecutor /// /// A used for calls. /// A used for localizing log or exception messages. - public TestClientExecutor(HttpClient client, IStringLocalizerFactory localizer) - : base(client, localizer) + public TestClientExecutor( + HttpClient client, + IGeoNETExceptionProvider exceptionProvider, + IStringLocalizerFactory localizer) + : base(client, exceptionProvider, localizer) { } } diff --git a/test/Geo.Core.Tests/Models/TestException.cs b/test/Geo.Core.Tests/Models/TestException.cs index fa9e38f..827fb87 100644 --- a/test/Geo.Core.Tests/Models/TestException.cs +++ b/test/Geo.Core.Tests/Models/TestException.cs @@ -6,8 +6,9 @@ namespace Geo.Core.Tests.Models { using System; + using Geo.Core.Models.Exceptions; - public class TestException : Exception + public sealed class TestException : GeoCoreException { /// /// Initializes a new instance of the class. diff --git a/test/Geo.Google.Tests/Services/GoogleGeocodingShould.cs b/test/Geo.Google.Tests/Services/GoogleGeocodingShould.cs index 82cc7d1..d991ebb 100644 --- a/test/Geo.Google.Tests/Services/GoogleGeocodingShould.cs +++ b/test/Geo.Google.Tests/Services/GoogleGeocodingShould.cs @@ -7,7 +7,6 @@ namespace Geo.Google.Tests.Services { using System; using System.Collections.Generic; - using System.Collections.Specialized; using System.Globalization; using System.Linq; using System.Net; @@ -22,6 +21,7 @@ namespace Geo.Google.Tests.Services using Geo.Google.Models; using Geo.Google.Models.Parameters; using Geo.Google.Services; + using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -36,6 +36,7 @@ public class GoogleGeocodingShould : IDisposable { private readonly HttpClient _httpClient; private readonly GoogleKeyContainer _keyContainer; + private readonly IGeoNETExceptionProvider _exceptionProvider; private readonly IStringLocalizerFactory _localizerFactory; private readonly List _responseMessages = new List(); private bool _disposed; @@ -105,6 +106,7 @@ public GoogleGeocodingShould() var options = Options.Create(new LocalizationOptions { ResourcesPath = "Resources" }); _localizerFactory = new ResourceManagerStringLocalizerFactory(options, NullLoggerFactory.Instance); _httpClient = new HttpClient(mockHandler.Object); + _exceptionProvider = new GeoNETExceptionProvider(); } /// @@ -122,11 +124,13 @@ public void AddGoogleKeySuccessfully() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; - sut.AddGoogleKey(query); - query.Count.Should().Be(1); - query["key"].Should().Be("abc123"); + sut.AddGoogleKey(ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(1); + queryParameters["key"].Should().Be("abc123"); } /// @@ -137,15 +141,17 @@ public void AddBaseParametersSuccessfully() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; var parameters = new BaseParameters() { Language = new CultureInfo("da"), }; - sut.AddBaseParameters(parameters, query); - query.Count.Should().Be(1); - query["language"].Should().Be("da"); + sut.AddBaseParameters(parameters, ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(1); + queryParameters["language"].Should().Be("da"); } /// @@ -156,7 +162,7 @@ public void AddCoordinateParametersSuccessfully() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; var parameters = new CoordinateParameters() { Location = new Coordinate() @@ -168,11 +174,13 @@ public void AddCoordinateParametersSuccessfully() Language = new CultureInfo("da"), }; - sut.AddCoordinateParameters(parameters, query); - query.Count.Should().Be(3); - query["location"].Should().Be("76.54,34.56"); - query["radius"].Should().Be("10000"); - query["language"].Should().Be("da"); + sut.AddCoordinateParameters(parameters, ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(3); + queryParameters["location"].Should().Be("76.54,34.56"); + queryParameters["radius"].Should().Be("10000"); + queryParameters["language"].Should().Be("da"); } /// @@ -183,7 +191,7 @@ public void AddBaseSearchParametersSuccessfully() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; var parameters = new BaseSearchParameters() { MinimumPrice = 2, @@ -200,16 +208,18 @@ public void AddBaseSearchParametersSuccessfully() Language = new CultureInfo("pt-BR"), }; - sut.AddBaseSearchParameters(parameters, query); - query.Count.Should().Be(8); - query["minprice"].Should().Be("2"); - query["maxprice"].Should().Be("3"); - query["opennow"].Should().Be("false"); - query["pagetoken"].Should().Be("987654"); - query["type"].Should().Be("Restaurant"); - query["location"].Should().Be("76.14,34.54"); - query["radius"].Should().Be("10001"); - query["language"].Should().Be("pt-BR"); + sut.AddBaseSearchParameters(parameters, ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(8); + queryParameters["minprice"].Should().Be("2"); + queryParameters["maxprice"].Should().Be("3"); + queryParameters["opennow"].Should().Be("false"); + queryParameters["pagetoken"].Should().Be("987654"); + queryParameters["type"].Should().Be("Restaurant"); + queryParameters["location"].Should().Be("76.14,34.54"); + queryParameters["radius"].Should().Be("10001"); + queryParameters["language"].Should().Be("pt-BR"); } /// @@ -220,7 +230,7 @@ public void AddAutocompleteParametersSuccessfully() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; var parameters = new QueryAutocompleteParameters() { Offset = 64, @@ -234,13 +244,15 @@ public void AddAutocompleteParametersSuccessfully() Language = new CultureInfo("fr"), }; - sut.AddAutocompleteParameters(parameters, query); - query.Count.Should().Be(5); - query["offset"].Should().Be("64"); - query["input"].Should().Be("123 East"); - query["location"].Should().Be("6.14,3.54"); - query["radius"].Should().Be("25000"); - query["language"].Should().Be("fr"); + sut.AddAutocompleteParameters(parameters, ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(5); + queryParameters["offset"].Should().Be("64"); + queryParameters["input"].Should().Be("123 East"); + queryParameters["location"].Should().Be("6.14,3.54"); + queryParameters["radius"].Should().Be("25000"); + queryParameters["language"].Should().Be("fr"); } /// @@ -251,7 +263,7 @@ public void AddBaseSearchParametersWithRestrictions1() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; var parameters = new BaseSearchParameters() { MinimumPrice = 5, @@ -266,11 +278,13 @@ public void AddBaseSearchParametersWithRestrictions1() Language = new CultureInfo("es"), }; - sut.AddBaseSearchParameters(parameters, query); - query.Count.Should().Be(3); - query["opennow"].Should().Be("false"); - query["location"].Should().Be("76.14,34.54"); - query["language"].Should().Be("es"); + sut.AddBaseSearchParameters(parameters, ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(3); + queryParameters["opennow"].Should().Be("false"); + queryParameters["location"].Should().Be("76.14,34.54"); + queryParameters["language"].Should().Be("es"); } /// @@ -281,7 +295,7 @@ public void AddBaseSearchParametersWithRestrictions2() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; var parameters = new BaseSearchParameters() { MinimumPrice = 3, @@ -296,11 +310,13 @@ public void AddBaseSearchParametersWithRestrictions2() Language = new CultureInfo("es"), }; - sut.AddBaseSearchParameters(parameters, query); - query.Count.Should().Be(3); - query["opennow"].Should().Be("false"); - query["location"].Should().Be("76.14,34.54"); - query["language"].Should().Be("es"); + sut.AddBaseSearchParameters(parameters, ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(3); + queryParameters["opennow"].Should().Be("false"); + queryParameters["location"].Should().Be("76.14,34.54"); + queryParameters["language"].Should().Be("es"); } /// @@ -876,7 +892,7 @@ protected virtual void Dispose(bool disposing) private GoogleGeocoding BuildService() { - return new GoogleGeocoding(_httpClient, _keyContainer, _localizerFactory); + return new GoogleGeocoding(_httpClient, _keyContainer, _exceptionProvider, _localizerFactory); } } } \ No newline at end of file diff --git a/test/Geo.Here.Tests/Services/HereGeocodingShould.cs b/test/Geo.Here.Tests/Services/HereGeocodingShould.cs index d7100a2..c1c70b3 100644 --- a/test/Geo.Here.Tests/Services/HereGeocodingShould.cs +++ b/test/Geo.Here.Tests/Services/HereGeocodingShould.cs @@ -7,7 +7,6 @@ namespace Geo.Here.Tests.Services { using System; using System.Collections.Generic; - using System.Collections.Specialized; using System.Globalization; using System.Net; using System.Net.Http; @@ -20,6 +19,7 @@ namespace Geo.Here.Tests.Services using Geo.Here.Models.Exceptions; using Geo.Here.Models.Parameters; using Geo.Here.Services; + using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -34,6 +34,7 @@ public class HereGeocodingShould : IDisposable { private readonly HttpClient _httpClient; private readonly HereKeyContainer _keyContainer; + private readonly IGeoNETExceptionProvider _exceptionProvider; private readonly IStringLocalizerFactory _localizerFactory; private readonly List _responseMessages = new List(); private bool _disposed; @@ -173,6 +174,7 @@ public HereGeocodingShould() var options = Options.Create(new LocalizationOptions { ResourcesPath = "Resources" }); _localizerFactory = new ResourceManagerStringLocalizerFactory(options, NullLoggerFactory.Instance); _httpClient = new HttpClient(mockHandler.Object); + _exceptionProvider = new GeoNETExceptionProvider(); } /// @@ -190,11 +192,13 @@ public void AddHereKeySuccessfully() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; - sut.AddHereKey(query); - query.Count.Should().Be(1); - query["apiKey"].Should().Be("abc123"); + sut.AddHereKey(ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(1); + queryParameters["apiKey"].Should().Be("abc123"); } /// @@ -205,15 +209,17 @@ public void AddBaseParametersSuccessfully() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; var parameters = new BaseParameters() { Language = new CultureInfo("es"), }; - sut.AddBaseParameters(parameters, query); - query.Count.Should().Be(1); - query["lang"].Should().Be("es"); + sut.AddBaseParameters(parameters, ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(1); + queryParameters["lang"].Should().Be("es"); } /// @@ -224,17 +230,19 @@ public void AddLimitingParametersSuccessfully() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; var parameters = new BaseFilterParameters() { Limit = 17, Language = new CultureInfo("da"), }; - sut.AddLimitingParameters(parameters, query); - query.Count.Should().Be(2); - query["limit"].Should().Be("17"); - query["lang"].Should().Be("da"); + sut.AddLimitingParameters(parameters, ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(2); + queryParameters["limit"].Should().Be("17"); + queryParameters["lang"].Should().Be("da"); } /// @@ -245,7 +253,7 @@ public void AddLocatingParametersSuccessfully() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; var parameters = new BaseFilterParameters() { At = new Coordinate() @@ -257,11 +265,13 @@ public void AddLocatingParametersSuccessfully() Language = new CultureInfo("fr-FR"), }; - sut.AddLocatingParameters(parameters, query); - query.Count.Should().Be(3); - query["at"].Should().Be("56.789,123.456"); - query["limit"].Should().Be("91"); - query["lang"].Should().Be("fr-FR"); + sut.AddLocatingParameters(parameters, ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(3); + queryParameters["at"].Should().Be("56.789,123.456"); + queryParameters["limit"].Should().Be("91"); + queryParameters["lang"].Should().Be("fr-FR"); } /// @@ -272,7 +282,7 @@ public void AddBoundingParametersSuccessfully() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; var parameters = new AreaParameters() { At = new Coordinate() @@ -284,13 +294,15 @@ public void AddBoundingParametersSuccessfully() Language = new CultureInfo("fr"), }; - sut.AddBoundingParameters(parameters, query); - query.Count.Should().Be(3); - query["at"].Should().Be("56.789,123.456"); - query["limit"].Should().Be("91"); - query["lang"].Should().Be("fr"); + sut.AddBoundingParameters(parameters, ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(3); + queryParameters["at"].Should().Be("56.789,123.456"); + queryParameters["limit"].Should().Be("91"); + queryParameters["lang"].Should().Be("fr"); - query.Clear(); + query = QueryString.Empty; parameters = new AreaParameters() { InCountry = "BEL", @@ -303,14 +315,16 @@ public void AddBoundingParametersSuccessfully() Language = new CultureInfo("nl"), }; - sut.AddBoundingParameters(parameters, query); - query.Count.Should().Be(4); - query["in"].Should().Be("countryCode:BEL"); - query["at"].Should().Be("56.789,123.456"); - query["limit"].Should().Be("91"); - query["lang"].Should().Be("nl"); + sut.AddBoundingParameters(parameters, ref query); - query.Clear(); + queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(4); + queryParameters["in"].Should().Be("countryCode:BEL"); + queryParameters["at"].Should().Be("56.789,123.456"); + queryParameters["limit"].Should().Be("91"); + queryParameters["lang"].Should().Be("nl"); + + query = QueryString.Empty; parameters = new AreaParameters() { InCircle = new Circle() @@ -326,13 +340,15 @@ public void AddBoundingParametersSuccessfully() Language = new CultureInfo("gl"), }; - sut.AddBoundingParameters(parameters, query); - query.Count.Should().Be(3); - query["in"].Should().Be("circle:78.9,45.32;r=50000"); - query["limit"].Should().Be("83"); - query["lang"].Should().Be("gl"); + sut.AddBoundingParameters(parameters, ref query); + + queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(3); + queryParameters["in"].Should().Be("circle:78.9,45.32;r=50000"); + queryParameters["limit"].Should().Be("83"); + queryParameters["lang"].Should().Be("gl"); - query.Clear(); + query = QueryString.Empty; parameters = new AreaParameters() { InCountry = "DNK", @@ -349,13 +365,15 @@ public void AddBoundingParametersSuccessfully() Language = new CultureInfo("da"), }; - sut.AddBoundingParameters(parameters, query); - query.Count.Should().Be(3); - query["in"].Should().Be("countryCode:DNK,circle:48.9,15.32;r=6000"); - query["limit"].Should().Be("48"); - query["lang"].Should().Be("da"); + sut.AddBoundingParameters(parameters, ref query); + + queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(3); + queryParameters["in"].Should().Be("countryCode:DNK,circle:48.9,15.32;r=6000"); + queryParameters["limit"].Should().Be("48"); + queryParameters["lang"].Should().Be("da"); - query.Clear(); + query = QueryString.Empty; parameters = new AreaParameters() { InBoundingBox = new BoundingBox() @@ -369,13 +387,15 @@ public void AddBoundingParametersSuccessfully() Language = new CultureInfo("ca"), }; - sut.AddBoundingParameters(parameters, query); - query.Count.Should().Be(3); - query["in"].Should().Be("bbox:-1.5,87.99,1.5,89.99"); - query["limit"].Should().Be("65"); - query["lang"].Should().Be("ca"); + sut.AddBoundingParameters(parameters, ref query); - query.Clear(); + queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(3); + queryParameters["in"].Should().Be("bbox:-1.5,87.99,1.5,89.99"); + queryParameters["limit"].Should().Be("65"); + queryParameters["lang"].Should().Be("ca"); + + query = QueryString.Empty; parameters = new AreaParameters() { InCountry = "POL", @@ -390,11 +410,13 @@ public void AddBoundingParametersSuccessfully() Language = new CultureInfo("pl"), }; - sut.AddBoundingParameters(parameters, query); - query.Count.Should().Be(3); - query["in"].Should().Be("countryCode:POL,bbox:-43.5,45.99,-39.5,54.99"); - query["limit"].Should().Be("33"); - query["lang"].Should().Be("pl"); + sut.AddBoundingParameters(parameters, ref query); + + queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(3); + queryParameters["in"].Should().Be("countryCode:POL,bbox:-43.5,45.99,-39.5,54.99"); + queryParameters["limit"].Should().Be("33"); + queryParameters["lang"].Should().Be("pl"); } /// @@ -405,8 +427,8 @@ public void AddBoundingParametersWithException() { var sut = BuildService(); - var query = new NameValueCollection(); - Action act = () => sut.AddBoundingParameters(new AreaParameters(), query); + var query = QueryString.Empty; + Action act = () => sut.AddBoundingParameters(new AreaParameters(), ref query); act.Should() .Throw() @@ -440,7 +462,7 @@ public void AddBoundingParametersWithException() Language = new CultureInfo("pl"), }; - act = () => sut.AddBoundingParameters(new AreaParameters(), query); + act = () => sut.AddBoundingParameters(new AreaParameters(), ref query); act.Should() .Throw() @@ -958,7 +980,7 @@ protected virtual void Dispose(bool disposing) private HereGeocoding BuildService() { - return new HereGeocoding(_httpClient, _keyContainer, _localizerFactory); + return new HereGeocoding(_httpClient, _keyContainer, _exceptionProvider, _localizerFactory); } } } diff --git a/test/Geo.MapBox.Tests/Services/MapBoxGeocodingShould.cs b/test/Geo.MapBox.Tests/Services/MapBoxGeocodingShould.cs index 3e73e45..cbfd5f7 100644 --- a/test/Geo.MapBox.Tests/Services/MapBoxGeocodingShould.cs +++ b/test/Geo.MapBox.Tests/Services/MapBoxGeocodingShould.cs @@ -7,7 +7,6 @@ namespace Geo.MapBox.Tests.Services { using System; using System.Collections.Generic; - using System.Collections.Specialized; using System.Globalization; using System.Net; using System.Net.Http; @@ -21,6 +20,7 @@ namespace Geo.MapBox.Tests.Services using Geo.MapBox.Models.Exceptions; using Geo.MapBox.Models.Parameters; using Geo.MapBox.Services; + using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -35,6 +35,7 @@ public class MapBoxGeocodingShould : IDisposable { private readonly HttpClient _httpClient; private readonly MapBoxKeyContainer _keyContainer; + private readonly IGeoNETExceptionProvider _exceptionProvider; private readonly IStringLocalizerFactory _localizerFactory; private readonly List _responseMessages = new List(); private bool _disposed; @@ -94,6 +95,7 @@ public MapBoxGeocodingShould() var options = Options.Create(new LocalizationOptions { ResourcesPath = "Resources" }); _localizerFactory = new ResourceManagerStringLocalizerFactory(options, NullLoggerFactory.Instance); _httpClient = new HttpClient(mockHandler.Object); + _exceptionProvider = new GeoNETExceptionProvider(); } /// @@ -111,11 +113,13 @@ public void AddMapBoxKeySuccessfully() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; - sut.AddMapBoxKey(query); - query.Count.Should().Be(1); - query["access_token"].Should().Be("abc123"); + sut.AddMapBoxKey(ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(1); + queryParameters["access_token"].Should().Be("abc123"); } /// @@ -126,7 +130,7 @@ public void AddBaseParametersSuccessfully() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; var parameters = new BaseParameters() { Limit = 5, @@ -142,13 +146,15 @@ public void AddBaseParametersSuccessfully() parameters.Types.Add(FeatureType.Address); parameters.Types.Add(FeatureType.Place); - sut.AddBaseParameters(parameters, query); - query.Count.Should().Be(5); - query["country"].Should().Be("CA,FR"); - query["language"].Should().Be("en,fr-FR"); - query["limit"].Should().Be("5"); - query["routing"].Should().Be("true"); - query["types"].Should().Be("address,place"); + sut.AddBaseParameters(parameters, ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(5); + queryParameters["country"].Should().Be("CA,FR"); + queryParameters["language"].Should().Be("en,fr-FR"); + queryParameters["limit"].Should().Be("5"); + queryParameters["routing"].Should().Be("true"); + queryParameters["types"].Should().Be("address,place"); } /// @@ -207,6 +213,29 @@ public void BuildGeocodingRequestSuccessfully() path.Should().Contain("mapbox.places/123 East"); } + [Fact] + public void BuildGeocodingRequest_WithCharacterNeedingEncoding_SuccessfullyBuildsAnEncodedUrl() + { + var sut = BuildService(); + + var parameters = new GeocodingParameters() + { + Query = "123 East #425", + Limit = 5, + Routing = true, + }; + + var uri = sut.BuildGeocodingRequest(parameters); + var query = HttpUtility.UrlDecode(uri.PathAndQuery); + query.Should().Contain("limit=5"); + query.Should().Contain("routing=true"); + query.Should().Contain("access_token=abc123"); + + var path = HttpUtility.UrlDecode(uri.AbsolutePath); + path.Should().Contain("mapbox.places/123 East #425"); + uri.AbsolutePath.Should().Contain("mapbox.places/123%20East%20%23425"); + } + /// /// Tests the building of the geocoding parameters fails if no query is provided. /// @@ -479,7 +508,7 @@ protected virtual void Dispose(bool disposing) private MapBoxGeocoding BuildService() { - return new MapBoxGeocoding(_httpClient, _keyContainer, _localizerFactory); + return new MapBoxGeocoding(_httpClient, _keyContainer, _exceptionProvider, _localizerFactory); } } } \ No newline at end of file diff --git a/test/Geo.MapQuest.Tests/Services/MapQuestGeocodingShould.cs b/test/Geo.MapQuest.Tests/Services/MapQuestGeocodingShould.cs index 634ff9c..df79b88 100644 --- a/test/Geo.MapQuest.Tests/Services/MapQuestGeocodingShould.cs +++ b/test/Geo.MapQuest.Tests/Services/MapQuestGeocodingShould.cs @@ -7,7 +7,6 @@ namespace Geo.MapQuest.Tests.Services { using System; using System.Collections.Generic; - using System.Collections.Specialized; using System.Net; using System.Net.Http; using System.Threading; @@ -20,6 +19,7 @@ namespace Geo.MapQuest.Tests.Services using Geo.MapQuest.Models.Exceptions; using Geo.MapQuest.Models.Parameters; using Geo.MapQuest.Services; + using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -35,6 +35,7 @@ public class MapQuestGeocodingShould : IDisposable private readonly HttpClient _httpClient; private readonly MapQuestKeyContainer _keyContainer; private readonly MapQuestEndpoint _endpoint; + private readonly IGeoNETExceptionProvider _exceptionProvider; private readonly IStringLocalizerFactory _localizerFactory; private readonly List _responseMessages = new List(); private bool _disposed; @@ -106,6 +107,7 @@ public MapQuestGeocodingShould() var options = Options.Create(new LocalizationOptions { ResourcesPath = "Resources" }); _localizerFactory = new ResourceManagerStringLocalizerFactory(options, NullLoggerFactory.Instance); _httpClient = new HttpClient(mockHandler.Object); + _exceptionProvider = new GeoNETExceptionProvider(); } /// @@ -123,11 +125,13 @@ public void AddMapBoxKeySuccessfully() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; - sut.AddMapQuestKey(query); - query.Count.Should().Be(1); - query["key"].Should().Be("abc123"); + sut.AddMapQuestKey(ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(1); + queryParameters["key"].Should().Be("abc123"); } /// @@ -138,15 +142,17 @@ public void AddBaseParametersSuccessfully() { var sut = BuildService(); - var query = new NameValueCollection(); + var query = QueryString.Empty; var parameters = new BaseParameters() { IncludeThumbMaps = true, }; - sut.AddBaseParameters(parameters, query); - query.Count.Should().Be(1); - query["thumbMaps"].Should().Be("true"); + sut.AddBaseParameters(parameters, ref query); + + var queryParameters = HttpUtility.ParseQueryString(query.ToString()); + queryParameters.Count.Should().Be(1); + queryParameters["thumbMaps"].Should().Be("true"); } /// @@ -476,7 +482,7 @@ protected virtual void Dispose(bool disposing) private MapQuestGeocoding BuildService(MapQuestEndpoint endpoint = null) { - return new MapQuestGeocoding(_httpClient, _keyContainer, endpoint ?? _endpoint, _localizerFactory); + return new MapQuestGeocoding(_httpClient, _keyContainer, endpoint ?? _endpoint, _exceptionProvider, _localizerFactory); } } } \ No newline at end of file