SHA256
SomniaGuestLog - Address search/query using Nominatim - Initial implementation
Initial implementation for address search/query using external APIs (OSM Nominatim) & Added required classes for supporting more APIs in the future.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
@page "/debug/addrSearch"
|
||||
|
||||
@using Somnia.GuestLog.Models.DTOs;
|
||||
@using Somnia.GuestLog.Services.AddressQuery;
|
||||
|
||||
@inject ILogger<AddressQuery> logger;
|
||||
@inject IAddressQueryFactory addressQueryFactory;
|
||||
@inject ISnackbar Snackbar
|
||||
|
||||
<PageTitle>DEBUG - Address Query</PageTitle>
|
||||
|
||||
<MudText Typo="Typo.h3" GutterBottom="true">Address Search</MudText>
|
||||
|
||||
<MudTextField @ref="txtSearch" @bind-Value="SearchText" Label="Search address" Required />
|
||||
|
||||
<MudButton Color="Color.Primary" Variant="Variant.Filled" @onclick="btnSearchAddress">Search</MudButton>
|
||||
|
||||
<MudPaper>
|
||||
@if (addressResults.Count == 0)
|
||||
{
|
||||
<MudText Typo="Typo.body1">No results!</MudText>
|
||||
}
|
||||
@foreach (AddressResultDTO result in addressResults)
|
||||
{
|
||||
<MudItem>
|
||||
<MudStack>
|
||||
<MudText>Address: @result.AddressRoad , @result.HouseNumber</MudText>
|
||||
<MudText>City: @result.AddressCity</MudText>
|
||||
<MudText>Province: @result.AddressProvince</MudText>
|
||||
<MudText>State: @result.AddressState</MudText>
|
||||
<MudText>Country: @result.AddressCountry</MudText>
|
||||
<MudText>ZipCode: @result.ZipCode</MudText>
|
||||
</MudStack>
|
||||
</MudItem>
|
||||
}
|
||||
</MudPaper>
|
||||
|
||||
@code {
|
||||
|
||||
public string SearchText { get; set; } = "";
|
||||
private List<AddressResultDTO> addressResults = [];
|
||||
|
||||
private MudTextField<string> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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<IValidator<GuestDocumentDTO>, GuestDocumentDTOValidator>();
|
||||
builder.Services.AddSingleton<IValidator<GuestDocIdDTO>, GuestDocIdDTOValidator>();
|
||||
@@ -34,7 +40,12 @@ builder.Services.AddSingleton<IValidator<GuestDocIdDTO>, GuestDocIdDTOValidator>
|
||||
builder.Services.AddSingleton<IGuestDocumentService, GuestDocumentService>();
|
||||
builder.Services.AddSingleton<IDataListService, StaticDataListService>();
|
||||
|
||||
builder.Services.AddLocalization();
|
||||
// Add Factories (and it's services)
|
||||
builder.Services.AddFactoryDI<IAddressQueryFactory, ConfigFileAddressQueryFactory>();
|
||||
|
||||
// Add Options config
|
||||
builder.Services.AddOptions<AddressQueryConfig>()
|
||||
.Bind(builder.Configuration.GetSection(AddressQueryConfig.SectionName));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
@@ -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<AddressQueryConfig> config
|
||||
) : IAddressQueryFactory, IFactoryDI
|
||||
{
|
||||
public static void RegisterFactoryItems(IServiceCollection services)
|
||||
{
|
||||
services.AddKeyedTransient<IAddressQueryService, NominatimAddressQueryService>(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<IAddressQueryService>(config.Value.Provider)
|
||||
?? throw new InvalidOperationException($"AddressQueryService not found for provider: '{config.Value.Provider}'");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace Somnia.GuestLog.Services.AddressQuery;
|
||||
|
||||
public interface IAddressQueryFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the AddressQuery service implementation given current configuration.
|
||||
/// </summary>
|
||||
public IAddressQueryService GetService();
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
using Somnia.GuestLog.Models.DTOs;
|
||||
|
||||
namespace Somnia.GuestLog.Services.AddressQuery;
|
||||
|
||||
public interface IAddressQueryService
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="query">Address to search</param>
|
||||
/// <returns>List of resulting structured addresses</returns>
|
||||
public Task<List<AddressResultDTO>> SearchAddressAsync(string query);
|
||||
|
||||
}
|
||||
@@ -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<NominatimAddressQueryService> logger,
|
||||
IOptions<AddressQueryConfig> 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<List<AddressResultDTO>> SearchAddressAsync(string query)
|
||||
{
|
||||
var http = GetHttpClient();
|
||||
List<AddressResultDTO> rsList = [];
|
||||
|
||||
try
|
||||
{
|
||||
string uri = GetRequestUri(query);
|
||||
List<ApiResult> apiList = (await http.GetFromJsonAsync<List<ApiResult>>(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<string, string?>("format", "jsonv2"),
|
||||
new KeyValuePair<string, string?>("addressdetails", "1"),
|
||||
new KeyValuePair<string, string?>("layer", "address"),
|
||||
new KeyValuePair<string, string?>("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
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
@@ -13,6 +13,4 @@ public interface IGuestDocumentService
|
||||
public Task<IEnumerable<GuestDocumentDTO>> GetDtoListByFilterAsync(GuestDocFilterDTO filter, int limit = 0);
|
||||
public Task<IEnumerable<GuestDocumentDTO>> GetDtoListAsync();
|
||||
|
||||
//TODO: Add Filter
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace Somnia.GuestLog.Utils.DI;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for Factories that need to register DI services.
|
||||
/// <br/><see cref="ServiceCollectionExtensions.AddFactoryDI{TFactory, TFactoryImpl}(IServiceCollection)"/>
|
||||
/// </summary>
|
||||
public interface IFactoryDI
|
||||
{
|
||||
/// <summary>
|
||||
/// Register required services in the ServiceCollection.
|
||||
/// This method is automaticaly call when the factory is register.
|
||||
/// </summary>
|
||||
/// <param name="services"></param>
|
||||
public abstract static void RegisterFactoryItems(IServiceCollection services);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using Somnia.GuestLog.Utils.DI;
|
||||
|
||||
namespace Somnia.GuestLog.Utils;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <typeparam name="TFactory">Factory Interface</typeparam>
|
||||
/// <typeparam name="TFactoryImpl">Factory Implementation</typeparam>
|
||||
public static IServiceCollection AddFactoryDI<TFactory, TFactoryImpl>(this IServiceCollection services)
|
||||
where TFactory : class
|
||||
where TFactoryImpl : class, TFactory, IFactoryDI
|
||||
{
|
||||
services.AddSingleton<TFactory, TFactoryImpl>();
|
||||
TFactoryImpl.RegisterFactoryItems(services);
|
||||
return services;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -8,5 +8,12 @@
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
"AllowedHosts": "*",
|
||||
"AddressQuery": {
|
||||
"EnableAddressQuery": true,
|
||||
"Provider": "Nominatim",
|
||||
"ServiceURL": "https://nominatim.openstreetmap.org/",
|
||||
"ApiKey": "",
|
||||
"UserAgent": "Somnia-GuestLog-Dev"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user