diff --git a/src/Somnia.GuestLog/Components/Pages/DebugPages/AddressQuery.razor b/src/Somnia.GuestLog/Components/Pages/DebugPages/AddressQuery.razor new file mode 100644 index 0000000..e1db666 --- /dev/null +++ b/src/Somnia.GuestLog/Components/Pages/DebugPages/AddressQuery.razor @@ -0,0 +1,65 @@ +@page "/debug/addrSearch" + +@using Somnia.GuestLog.Models.DTOs; +@using Somnia.GuestLog.Services.AddressQuery; + +@inject ILogger logger; +@inject IAddressQueryFactory addressQueryFactory; +@inject ISnackbar Snackbar + +DEBUG - Address Query + +Address Search + + + +Search + + + @if (addressResults.Count == 0) + { + No results! + } + @foreach (AddressResultDTO result in addressResults) + { + + + Address: @result.AddressRoad , @result.HouseNumber + City: @result.AddressCity + Province: @result.AddressProvince + State: @result.AddressState + Country: @result.AddressCountry + ZipCode: @result.ZipCode + + + } + + +@code { + + public string SearchText { get; set; } = ""; + private List addressResults = []; + + private MudTextField txtSearch = null!; + + private async Task btnSearchAddress() + { + if (SearchText.Length == 0) + { + return; + } + + try + { + var queryService = addressQueryFactory.GetService(); + + addressResults = await queryService.SearchAddressAsync(SearchText); + + } + catch (Exception ex) + { + logger.LogError(ex, "SearchAddress returned an error!"); + Snackbar.Add($"Error searching address: {ex.Message}", Severity.Error); + } + } +} diff --git a/src/Somnia.GuestLog/Models/Configuration/AddressQueryConfig.cs b/src/Somnia.GuestLog/Models/Configuration/AddressQueryConfig.cs new file mode 100644 index 0000000..b2b9bb6 --- /dev/null +++ b/src/Somnia.GuestLog/Models/Configuration/AddressQueryConfig.cs @@ -0,0 +1,21 @@ +namespace Somnia.GuestLog.Models.Configuration; + +public class AddressQueryConfig +{ + public const string SectionName = "AddressQuery"; + + public bool EnableAddressQuery { get; set; } + public AddressQueryProviderEnum? Provider { get; set; } + public string? ServiceURL { get; set; } + public string? ApiKey { get; set; } + public string? UserAgent { get; set; } + + + public enum AddressQueryProviderEnum + { + Nominatim, + GoogleApi + } + +} + diff --git a/src/Somnia.GuestLog/Models/DTOs/AddressResultDTO.cs b/src/Somnia.GuestLog/Models/DTOs/AddressResultDTO.cs new file mode 100644 index 0000000..b3e9b2b --- /dev/null +++ b/src/Somnia.GuestLog/Models/DTOs/AddressResultDTO.cs @@ -0,0 +1,12 @@ +namespace Somnia.GuestLog.Models.DTOs; + +public class AddressResultDTO +{ + public string? HouseNumber { get; set; } + public string? AddressRoad { get; set; } + public string? AddressCity { get; set; } + public string? AddressProvince { get; set; } + public string? AddressState { get; set; } + public string? AddressCountry { get; set; } + public string? ZipCode { get; set; } +} diff --git a/src/Somnia.GuestLog/Program.cs b/src/Somnia.GuestLog/Program.cs index 2e54ad2..427e27e 100644 --- a/src/Somnia.GuestLog/Program.cs +++ b/src/Somnia.GuestLog/Program.cs @@ -4,8 +4,11 @@ using Microsoft.EntityFrameworkCore; using MudBlazor.Services; using Somnia.GuestLog.Components; using Somnia.GuestLog.Infrastructure; +using Somnia.GuestLog.Models.Configuration; using Somnia.GuestLog.Models.DTOs; using Somnia.GuestLog.Services; +using Somnia.GuestLog.Services.AddressQuery; +using Somnia.GuestLog.Utils; using Somnia.GuestLog.Validators; var builder = WebApplication.CreateBuilder(args); @@ -22,10 +25,13 @@ builder.Services.AddDatabaseDeveloperPageExceptionFilter(); // Add MudBlazor services builder.Services.AddMudServices(); -// Add services to the container. +// Add components services to the container. builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); +builder.Services.AddLocalization(); +builder.Services.AddHttpClient(); + // Add Validation Services builder.Services.AddSingleton, GuestDocumentDTOValidator>(); builder.Services.AddSingleton, GuestDocIdDTOValidator>(); @@ -34,7 +40,12 @@ builder.Services.AddSingleton, GuestDocIdDTOValidator> builder.Services.AddSingleton(); builder.Services.AddSingleton(); -builder.Services.AddLocalization(); +// Add Factories (and it's services) +builder.Services.AddFactoryDI(); + +// Add Options config +builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection(AddressQueryConfig.SectionName)); var app = builder.Build(); diff --git a/src/Somnia.GuestLog/Services/AddressQuery/ConfigFileAddressQueryFactory.cs b/src/Somnia.GuestLog/Services/AddressQuery/ConfigFileAddressQueryFactory.cs new file mode 100644 index 0000000..0d4dbc0 --- /dev/null +++ b/src/Somnia.GuestLog/Services/AddressQuery/ConfigFileAddressQueryFactory.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Options; +using Somnia.GuestLog.Models.Configuration; +using Somnia.GuestLog.Utils.DI; + +namespace Somnia.GuestLog.Services.AddressQuery; + +public class ConfigFileAddressQueryFactory( + IServiceProvider serviceProvider, + IOptions config +) : IAddressQueryFactory, IFactoryDI +{ + public static void RegisterFactoryItems(IServiceCollection services) + { + services.AddKeyedTransient(AddressQueryConfig.AddressQueryProviderEnum.Nominatim); + } + + public IAddressQueryService GetService() + { + if (config.Value.EnableAddressQuery == false) + { + throw new InvalidOperationException("AddressQuery is disabled. Enable and configure it first!"); + } + + return serviceProvider.GetKeyedService(config.Value.Provider) + ?? throw new InvalidOperationException($"AddressQueryService not found for provider: '{config.Value.Provider}'"); + } +} diff --git a/src/Somnia.GuestLog/Services/AddressQuery/IAddressQueryFactory.cs b/src/Somnia.GuestLog/Services/AddressQuery/IAddressQueryFactory.cs new file mode 100644 index 0000000..4b0be76 --- /dev/null +++ b/src/Somnia.GuestLog/Services/AddressQuery/IAddressQueryFactory.cs @@ -0,0 +1,9 @@ +namespace Somnia.GuestLog.Services.AddressQuery; + +public interface IAddressQueryFactory +{ + /// + /// Returns the AddressQuery service implementation given current configuration. + /// + public IAddressQueryService GetService(); +} diff --git a/src/Somnia.GuestLog/Services/AddressQuery/IAddressQueryService.cs b/src/Somnia.GuestLog/Services/AddressQuery/IAddressQueryService.cs new file mode 100644 index 0000000..0cd1d2f --- /dev/null +++ b/src/Somnia.GuestLog/Services/AddressQuery/IAddressQueryService.cs @@ -0,0 +1,19 @@ +using Somnia.GuestLog.Models.DTOs; + +namespace Somnia.GuestLog.Services.AddressQuery; + +public interface IAddressQueryService +{ + + /// + /// Search an address by plain text query. + /// Data is returned as a structured address. + /// This service allow to complete and validate addresses. + /// + /// Note: Not all fields are always available for a given query. + /// + /// Address to search + /// List of resulting structured addresses + public Task> SearchAddressAsync(string query); + +} diff --git a/src/Somnia.GuestLog/Services/AddressQuery/NominatimAddressQueryService.cs b/src/Somnia.GuestLog/Services/AddressQuery/NominatimAddressQueryService.cs new file mode 100644 index 0000000..54f6279 --- /dev/null +++ b/src/Somnia.GuestLog/Services/AddressQuery/NominatimAddressQueryService.cs @@ -0,0 +1,109 @@ +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Options; +using Somnia.GuestLog.Models.Configuration; +using Somnia.GuestLog.Models.DTOs; + +namespace Somnia.GuestLog.Services.AddressQuery; + +public class NominatimAddressQueryService( + IHttpClientFactory httpClientFactory, + ILogger logger, + IOptions configOptions +) : IAddressQueryService +{ + private const string defaultBaseAddr = "https://nominatim.openstreetmap.org"; + private const string defaultUserAgent = "Somnia-GuestLog"; + private const string apiUri = "/search"; + + private readonly AddressQueryConfig config = configOptions.Value; + + + public async Task> SearchAddressAsync(string query) + { + var http = GetHttpClient(); + List rsList = []; + + try + { + string uri = GetRequestUri(query); + List apiList = (await http.GetFromJsonAsync>(uri)) ?? []; + + foreach (ApiResult result in apiList) + { + if (result.Address is null) + { + logger.LogWarning("Skipping result without address details. '{result}'", result); + continue; + } + + rsList.Add(new() + { + HouseNumber = result.Address.House_number, + AddressRoad = result.Address.Road ?? result.Address.Square, + AddressCity = result.Address.City ?? result.Address.Municipality ?? result.Address.Village ?? result.Address.Village, + AddressProvince = result.Address.Province, + AddressState = result.Address.State, + AddressCountry = result.Address.Country, + ZipCode = result.Address.Postcode + }); + } + + return rsList; + } + catch (System.Exception ex) + { + logger.LogError(ex, "Error getting info from API!"); + return []; + } + } + + + 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.AcceptLanguage.ParseAdd("es"); + + return client; + } + + private static string GetRequestUri(string query) + { + return QueryHelpers.AddQueryString(apiUri, [ + new KeyValuePair("format", "jsonv2"), + new KeyValuePair("addressdetails", "1"), + new KeyValuePair("layer", "address"), + new KeyValuePair("q", query), + ]); + } + + private record class ApiResultAddress( + string? House_number = null, + string? Road = null, + string? Square = null, + string? City = null, + string? Municipality = null, + string? Village = null, + string? Town = null, + string? Province = null, + string? State = null, + string? Country = null, + string? Postcode = null + ); + + private record class ApiResult( + string? Osm_type = null, + long? Osm_id = null, + string? Category = null, + string? AddressType = null, + string? Name = null, + string? Display_name = null, + ApiResultAddress? Address = null + ); + + +} \ No newline at end of file diff --git a/src/Somnia.GuestLog/Services/IGuestDocumentService.cs b/src/Somnia.GuestLog/Services/IGuestDocumentService.cs index 6611a6a..1e17df8 100644 --- a/src/Somnia.GuestLog/Services/IGuestDocumentService.cs +++ b/src/Somnia.GuestLog/Services/IGuestDocumentService.cs @@ -13,6 +13,4 @@ public interface IGuestDocumentService public Task> GetDtoListByFilterAsync(GuestDocFilterDTO filter, int limit = 0); public Task> GetDtoListAsync(); - //TODO: Add Filter - } \ No newline at end of file diff --git a/src/Somnia.GuestLog/Utils/DI/IFactoryDI.cs b/src/Somnia.GuestLog/Utils/DI/IFactoryDI.cs new file mode 100644 index 0000000..65cc823 --- /dev/null +++ b/src/Somnia.GuestLog/Utils/DI/IFactoryDI.cs @@ -0,0 +1,15 @@ +namespace Somnia.GuestLog.Utils.DI; + +/// +/// Interface for Factories that need to register DI services. +///
+///
+public interface IFactoryDI +{ + /// + /// Register required services in the ServiceCollection. + /// This method is automaticaly call when the factory is register. + /// + /// + public abstract static void RegisterFactoryItems(IServiceCollection services); +} diff --git a/src/Somnia.GuestLog/Utils/ServiceCollectionExtensions.cs b/src/Somnia.GuestLog/Utils/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..779c857 --- /dev/null +++ b/src/Somnia.GuestLog/Utils/ServiceCollectionExtensions.cs @@ -0,0 +1,24 @@ +using Somnia.GuestLog.Utils.DI; + +namespace Somnia.GuestLog.Utils; + +public static class ServiceCollectionExtensions +{ + + /// + /// Helper for registering factories using IFactoryDI interface. + /// Those factories register the required services directly to the service collection. + /// This helper ensures all required methods are called. + /// + /// Factory Interface + /// Factory Implementation + public static IServiceCollection AddFactoryDI(this IServiceCollection services) + where TFactory : class + where TFactoryImpl : class, TFactory, IFactoryDI + { + services.AddSingleton(); + TFactoryImpl.RegisterFactoryItems(services); + return services; + } + +} diff --git a/src/Somnia.GuestLog/appsettings.json b/src/Somnia.GuestLog/appsettings.json index c5f6eaa..e48845a 100644 --- a/src/Somnia.GuestLog/appsettings.json +++ b/src/Somnia.GuestLog/appsettings.json @@ -8,5 +8,12 @@ "Microsoft.AspNetCore": "Warning" } }, - "AllowedHosts": "*" + "AllowedHosts": "*", + "AddressQuery": { + "EnableAddressQuery": true, + "Provider": "Nominatim", + "ServiceURL": "https://nominatim.openstreetmap.org/", + "ApiKey": "", + "UserAgent": "Somnia-GuestLog-Dev" + } } \ No newline at end of file