From ec7efd867b78edc1c734dce43cdb3f0e96735f8778743507b0714c12c9f21eb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= <93217193+Daniel-Garmig@users.noreply.github.com> Date: Wed, 12 Aug 2026 14:10:18 +0200 Subject: [PATCH] GuestLog - Added AddressQuery service using Google Maps Geocode API. --- .../Configuration/AddressQueryConfig.cs | 2 +- .../ConfigFileAddressQueryFactory.cs | 1 + .../GoogleApiAddressQueryService.cs | 125 ++++++++++++++++++ src/Somnia.GuestLog/appsettings.json | 4 + 4 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 src/Somnia.GuestLog/Services/AddressQuery/GoogleApiAddressQueryService.cs diff --git a/src/Somnia.GuestLog/Models/Configuration/AddressQueryConfig.cs b/src/Somnia.GuestLog/Models/Configuration/AddressQueryConfig.cs index ee346ed..b5d63f1 100644 --- a/src/Somnia.GuestLog/Models/Configuration/AddressQueryConfig.cs +++ b/src/Somnia.GuestLog/Models/Configuration/AddressQueryConfig.cs @@ -15,7 +15,7 @@ public class AddressQueryConfig public enum AddressQueryProviderEnum { Nominatim, - GoogleApi + GoogleApiV4 } } diff --git a/src/Somnia.GuestLog/Services/AddressQuery/ConfigFileAddressQueryFactory.cs b/src/Somnia.GuestLog/Services/AddressQuery/ConfigFileAddressQueryFactory.cs index 0d4dbc0..0852f80 100644 --- a/src/Somnia.GuestLog/Services/AddressQuery/ConfigFileAddressQueryFactory.cs +++ b/src/Somnia.GuestLog/Services/AddressQuery/ConfigFileAddressQueryFactory.cs @@ -12,6 +12,7 @@ public class ConfigFileAddressQueryFactory( public static void RegisterFactoryItems(IServiceCollection services) { services.AddKeyedTransient(AddressQueryConfig.AddressQueryProviderEnum.Nominatim); + services.AddKeyedTransient(AddressQueryConfig.AddressQueryProviderEnum.GoogleApiV4); } public IAddressQueryService GetService() diff --git a/src/Somnia.GuestLog/Services/AddressQuery/GoogleApiAddressQueryService.cs b/src/Somnia.GuestLog/Services/AddressQuery/GoogleApiAddressQueryService.cs new file mode 100644 index 0000000..3e870e3 --- /dev/null +++ b/src/Somnia.GuestLog/Services/AddressQuery/GoogleApiAddressQueryService.cs @@ -0,0 +1,125 @@ +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Options; +using Somnia.GuestLog.Models.Configuration; +using Somnia.GuestLog.Models.DTOs; +using Somnia.GuestLog.Utils; + +namespace Somnia.GuestLog.Services.AddressQuery; + +public class GoogleApiAddressQueryService( + IHttpClientFactory httpClientFactory, + ILogger logger, + IOptions configOptions +) : IAddressQueryService +{ + private const string defaultBaseAddr = "https://geocode.googleapis.com/v4/"; + private const string defaultUserAgent = "Somnia-GuestLog"; + private const string apiUri = "geocode/address/"; + + private readonly AddressQueryConfig config = configOptions.Value; + + + public async Task> SearchAddressAsync(string query) + { + var http = GetHttpClient(); + List rsList = []; + + try + { + string uri = GetRequestUri(query); + GeocodeAddressResponse? apiList = await http.GetFromJsonAsync(uri) + ?? throw new InvalidOperationException("Response without results??"); + + foreach (GeocodeResult result in apiList.Results) + { + if (result.PostalAddress is null) + { + logger.LogWarning("Skipping result without PostalAddress. '{result}'", result); + continue; + } + + string? alpha3CountryCode = CountryCodesUtils.Alpha2ToAlpha3(result.PostalAddress.RegionCode); + string? addressLine = result.PostalAddress.AddressLines is not null ? string.Join(", ", result.PostalAddress.AddressLines) : null; + + string[]? formatedParts = result.FormattedAddress?.Split(","); + if (formatedParts is not null && formatedParts.Length < 2) + { + formatedParts = null; + } + + rsList.Add(new() + { + HouseNumber = null, + AddressRoad = addressLine, + AddressCity = result.PostalAddress.Locality, + AddressProvince = result.PostalAddress.AdministrativeArea, + AddressState = null, + AddressCountry = formatedParts?[^1], + ZipCode = result.PostalAddress.PostalCode, + AddressCountryCode = alpha3CountryCode + }); + } + + return rsList; + } + catch (Exception ex) + { + logger.LogError(ex, "Error getting info from API!"); + return []; + } + } + + #region Internal Methods + + private HttpClient GetHttpClient() + { + HttpClient client = httpClientFactory.CreateClient(); + + Uri baseAddr = new(config.ServiceURL ?? defaultBaseAddr); + + client.BaseAddress = baseAddr; + client.DefaultRequestHeaders.UserAgent.ParseAdd(config.UserAgent ?? defaultUserAgent); + client.DefaultRequestHeaders.Add("X-Goog-Api-Key", config.ApiKey); + + return client; + } + + private string GetRequestUri(string query) + { + string escapedQuery = Uri.EscapeDataString(query).TrimStart('/'); + string finalUri = $"{apiUri.TrimEnd('/')}/{escapedQuery}"; + + List> queryParams = []; + if (config.ResultLang is not null) + { + queryParams.Add(new KeyValuePair("languageCode", config.ResultLang)); + } + + return QueryHelpers.AddQueryString(finalUri, queryParams); + } + + #endregion + + #region Internal Models + + private record class PostalAddress( + string[]? AddressLines = null, + string? PostalCode = null, + + string? AdministrativeArea = null, //Province + string? Locality = null, //City + string? RegionCode = null //Country Code (CLDR region code) + ); + + private record class GeocodeResult( + string? FormattedAddress = null, + PostalAddress? PostalAddress = null + ); + + private record class GeocodeAddressResponse( + GeocodeResult[] Results = null! + ); + + #endregion + +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/appsettings.json b/src/Somnia.GuestLog/appsettings.json index d9e7306..6c41ac1 100644 --- a/src/Somnia.GuestLog/appsettings.json +++ b/src/Somnia.GuestLog/appsettings.json @@ -11,8 +11,12 @@ "AllowedHosts": "*", "AddressQuery": { "EnableAddressQuery": true, + // Nominatim "Provider": "Nominatim", "ServiceURL": "https://nominatim.openstreetmap.org/", + // Google Map API + // "Provider": "GoogleApiV4", + // "ServiceURL": "https://geocode.googleapis.com/v4/", "ApiKey": "", "UserAgent": "Somnia-GuestLog-Dev", "ResultLang": "es"