Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

So/cognitive search service adapter #14

Closed
wants to merge 35 commits into from
Closed
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1b933fd
Initial commit
spencerohegartyDfE Jul 3, 2024
d95793a
Added mapper scaffolding
spencerohegartyDfE Jul 3, 2024
f0ed393
Added scaffolding for core domain constructs
spencerohegartyDfE Jul 3, 2024
4eeec38
Latest merge from main
spencerohegartyDfE Jul 3, 2024
a3f324f
Missed from latest merge from main
spencerohegartyDfE Jul 3, 2024
2f14ffe
More scaffolding for domain level constructs
spencerohegartyDfE Jul 3, 2024
9d969bf
Added search service adapter at the infrastructure level and supporti…
spencerohegartyDfE Jul 4, 2024
3e701cc
Updated NewtonSoft NuGet dependency.
spencerohegartyDfE Jul 4, 2024
113077a
Added test class stubs for integration tier
spencerohegartyDfE Jul 4, 2024
f6f845f
Added domain test scaffolding
spencerohegartyDfE Jul 4, 2024
c583c91
Added comments to the CognitiveSearchServiceAdapter.
spencerohegartyDfE Jul 5, 2024
074cb65
WIP
spencerohegartyDfE Jul 5, 2024
91f27f1
WIP - initial test setup for search service adapter.
spencerohegartyDfE Jul 5, 2024
540731b
WIP - more test coverage for service adapter
spencerohegartyDfE Jul 5, 2024
0e84656
Added cognitive search service adapter tests.
spencerohegartyDfE Jul 8, 2024
9a9fc1e
WIP - SearchOptionsFactory tests.
spencerohegartyDfE Jul 8, 2024
f1599d2
Completed SearchOptionsFactory tests.
spencerohegartyDfE Jul 8, 2024
fb79522
Added comments to options constructs.
spencerohegartyDfE Jul 8, 2024
045d6af
Completed SearchOptionsToAzureOptionsMapper tests.
spencerohegartyDfE Jul 8, 2024
fb7e957
WIP - Commenting and testing
spencerohegartyDfE Jul 8, 2024
fd07025
WIP - AzureSearchResultExtensions tests.
spencerohegartyDfE Jul 8, 2024
c84af35
Completed AzureSearchResultExtensions tests.
spencerohegartyDfE Jul 9, 2024
6ebcb6f
WIP - CognitiveSearchServiceAdapter tests.
spencerohegartyDfE Jul 9, 2024
985975d
Completed AzureSearchResponseToSearchResultsMapper tests.
spencerohegartyDfE Jul 9, 2024
9ee6048
Added comments to sub-mappers.
spencerohegartyDfE Jul 9, 2024
61a38b6
Completed sub mapper tests.
spencerohegartyDfE Jul 9, 2024
7243aea
Added comments to aggregate.
spencerohegartyDfE Jul 9, 2024
73ef27a
Added comments to domain level constructs.
spencerohegartyDfE Jul 9, 2024
9f1e89b
Completed Establishments aggregate root tests.
spencerohegartyDfE Jul 9, 2024
74938ab
Removed unnecessary comment from search servic adapter implementation.
spencerohegartyDfE Jul 9, 2024
60a301c
Corrected spellings and typos in comments.
spencerohegartyDfE Jul 10, 2024
2334876
Correct issue introduced in test due to spelling correction.
spencerohegartyDfE Jul 10, 2024
6985a7d
added nuget
CathLass Jul 10, 2024
84075e2
Added additional comments to AzureSearchResultExtensions class.
spencerohegartyDfE Jul 10, 2024
6de50f4
Merge branch 'SO/cognitive-search-service-adapter' of https://github.…
spencerohegartyDfE Jul 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions Dfe.Data.SearchPrototype.sln
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dfe.Data.SearchPrototype.Web.Tests", "Dfe.Data.SearchPrototype\Web\Tests\Dfe.Data.SearchPrototype.Web.Tests.csproj", "{A6882653-52E1-472D-97F4-03C0FB8D0B2F}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "NuGet", "NuGet", "{0D4ACC06-9FA9-422A-8F92-92B565879E16}"
ProjectSection(SolutionItems) = preProject
NuGet\DfE.Data.ComponentLibrary.CleanArchitecture.2.0.37-beta-ci-20240610-104522.nupkg = NuGet\DfE.Data.ComponentLibrary.CleanArchitecture.2.0.37-beta-ci-20240610-104522.nupkg
NuGet\DfE.Data.ComponentLibrary.CrossCuttingConcerns.Mapping.2.0.37-beta-ci-20240610-104522.nupkg = NuGet\DfE.Data.ComponentLibrary.CrossCuttingConcerns.Mapping.2.0.37-beta-ci-20240610-104522.nupkg
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dfe.Data.SearchPrototype.Infrastructure", "Dfe.Data.SearchPrototype\Infrastructure\Dfe.Data.SearchPrototype.Infrastructure.csproj", "{09713CBF-BB89-4FBB-9398-F38A2F329F80}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dfe.Data.SearchPrototype.Infrastructure.Tests", "Dfe.Data.SearchPrototype\Infrastructure\Tests\Dfe.Data.SearchPrototype.Infrastructure.Tests.csproj", "{AD01EF06-F7D9-4848-A6DC-E4AEB4031251}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -35,6 +43,14 @@ Global
{A6882653-52E1-472D-97F4-03C0FB8D0B2F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A6882653-52E1-472D-97F4-03C0FB8D0B2F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A6882653-52E1-472D-97F4-03C0FB8D0B2F}.Release|Any CPU.Build.0 = Release|Any CPU
{09713CBF-BB89-4FBB-9398-F38A2F329F80}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{09713CBF-BB89-4FBB-9398-F38A2F329F80}.Debug|Any CPU.Build.0 = Debug|Any CPU
{09713CBF-BB89-4FBB-9398-F38A2F329F80}.Release|Any CPU.ActiveCfg = Release|Any CPU
{09713CBF-BB89-4FBB-9398-F38A2F329F80}.Release|Any CPU.Build.0 = Release|Any CPU
{AD01EF06-F7D9-4848-A6DC-E4AEB4031251}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AD01EF06-F7D9-4848-A6DC-E4AEB4031251}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AD01EF06-F7D9-4848-A6DC-E4AEB4031251}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AD01EF06-F7D9-4848-A6DC-E4AEB4031251}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using Azure;
using Azure.Search.Documents;
using Azure.Search.Documents.Models;
using Dfe.Data.SearchPrototype.Infrastructure.Options;
using Dfe.Data.SearchPrototype.Search.Application.Adapters;
using Dfe.Data.SearchPrototype.Search.Domain.AggregateRoot;
using DfE.Data.ComponentLibrary.CrossCuttingConcerns.Mapping;
using DfE.Data.ComponentLibrary.Infrastructure.CognitiveSearch.Search;

namespace Dfe.Data.SearchPrototype.Infrastructure
{
/// <summary>
/// Provides an adaption of the core Azure cognitive search services to allow
/// compatibility with the Dfe.Data.SearchPrototype application search service definition.
/// </summary>
public sealed class CognitiveSearchServiceAdapter : ISearchServiceAdapter
{
private readonly ISearchService _cognitiveSearchService;
private readonly ISearchOptionsFactory _searchOptionsFactory;
private readonly IMapper<Response<SearchResults<object>>, Establishments> _searchResponseMapper;

/// <summary>
/// The following dependencies include the core cognitive search service definition,
/// the complete implementation of which is defined in the IOC container.
/// </summary>
/// <param name="cognitiveSearchService">
/// Cognitive search service definition injected via IOC container.
/// </param>
/// <param name="searchOptionsFactory">
/// Factory class definition for prescribing the requested search options (by collection context).
/// </param>
/// <param name="searchResponseMapper">
/// Maps the raw azure search response to the required "T:Dfe.Data.SearchPrototype.Search.Domain.AgregateRoot.Establishments"
/// </param>
public CognitiveSearchServiceAdapter(
ISearchService cognitiveSearchService,
ISearchOptionsFactory searchOptionsFactory,
IMapper<Response<SearchResults<object>>, Establishments> searchResponseMapper)
{
_searchOptionsFactory = searchOptionsFactory;
_cognitiveSearchService = cognitiveSearchService;
_searchResponseMapper = searchResponseMapper;
}

/// <summary>
/// Makes call to underlying azure cognitive search service and uses the prescribed mapper
/// to adapt the raw Azure search results to the "T:Dfe.Data.SearchPrototype.Search.Domain.AgregateRoot.Establishments" type.
/// </summary>
/// <param name="searchContext">
/// Prescribes the context of the search including the keyword and collection target.
/// </param>
/// <returns>
/// A configured "T:Dfe.Data.SearchPrototype.Search.Domain.AgregateRoot.Establishments"
/// object hydrated from the results of the azure search.
/// </returns>
/// <exception cref="ApplicationException">
/// An application exception is thrown if we either have no options configured, which
/// is unrecoverable, or no azure search results are returned which should never be the
/// case given no matches should return an empty wrapper result object.
/// </exception>
public async Task<Establishments> Search(SearchContext searchContext)
{
SearchOptions searchOptions =
_searchOptionsFactory.GetSearchOptions(searchContext.TargetCollection) ??
throw new ApplicationException(
$"Search options cannot be derived for {searchContext.TargetCollection}.");

Response<SearchResults<object>> searchResults =
await _cognitiveSearchService.SearchAsync<object>(
searchContext.SearchKeyword,
searchContext.TargetCollection,
searchOptions
)
.ConfigureAwait(false) ??
throw new ApplicationException(
$"Unable to derive search results based on input {searchContext.SearchKeyword}.");

return _searchResponseMapper.MapFrom(searchResults);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<Compile Remove="Tests\**" />
<EmbeddedResource Remove="Tests\**" />
<None Remove="Tests\**" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Dfe.Data.SearchPrototype.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="DfE.Data.ComponentLibrary.Infrastructure.CognitiveSearch" Version="2.0.37-beta-CI-20240610-104522" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.2" />
<PackageReference Include="Azure.Search.Documents" Version="11.4.0" />
<PackageReference Include="DfE.Data.ComponentLibrary.CrossCuttingConcerns.Mapping" Version="2.0.37-beta-CI-20240610-104522" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using Azure;
using Azure.Search.Documents.Models;
using Dfe.Data.SearchPrototype.Search.Domain.AggregateRoot;
using Dfe.Data.SearchPrototype.Search.Domain.AggregateRoot.Entities;
using Dfe.Data.SearchPrototype.Search.Domain.AggregateRoot.ValueObjects;
using DfE.Data.ComponentLibrary.CrossCuttingConcerns.Mapping;

namespace Dfe.Data.SearchPrototype.Infrastructure.Mapping
{
/// <summary>
/// Facilitates mapping from the received T:Azure.Search.Documents.Models.SearchResults
/// into the required T:Dfe.Data.SearchPrototype.Search.Domain.AgregateRoot.Establishments object.
/// </summary>
public sealed class AzureSearchResponseToSearchResultsMapper : IMapper<Response<SearchResults<object>>, Establishments>
{
private readonly IMapper<SearchResult<object>, EstablishmentIdentifier> _establishmentIdentityMapper;
private readonly IMapper<SearchResult<object>, EstablishmentDefinition> _establishmentNameMapper;

/// <summary>
/// The following dependencies provide the sub-mapping behaviour for creating a configured,
/// T:Dfe.Data.SearchPrototype.Search.Domain.AgregateRoot.Establishments instance from the
/// provided T:Azure.Search.Documents.Models.SearchResults, the complete implementation of
/// which is defined in the IOC container.
/// </summary>
/// <param name="establishmentIdentityMapper">
/// Mapper for handling hydration of the
/// T:Dfe.Data.SearchPrototype.Search.Domain.AgregateRoot.Entities.EstablishmentIdentifier
/// object injected via IOC container.
/// </param>
/// <param name="establishmentNameMapper">
/// Mapper for handling hydration of the
/// T:Dfe.Data.SearchPrototype.Search.Domain.AgregateRoot.Entities.EstablishmentName
/// object injected via IOC container.
/// </param>
public AzureSearchResponseToSearchResultsMapper(
IMapper<SearchResult<object>, EstablishmentIdentifier> establishmentIdentityMapper,
IMapper<SearchResult<object>, EstablishmentDefinition> establishmentNameMapper)
{
_establishmentIdentityMapper = establishmentIdentityMapper;
_establishmentNameMapper = establishmentNameMapper;
}

/// <summary>
/// The mapping input is the raw Azure search response T:Azure.Search.Documents.Models.SearchResults
/// and if any results are contained within the response a new T:Dfe.Data.SearchPrototype.Search.Domain.AgregateRoot.Establishments
/// instance is created, with the responsibility of hydrating this root object and children delegated to the sub-mappers.
/// </summary>
/// <param name="input">
/// A configured T:Azure.Search.Documents.Models.SearchResults instance.
/// </param>
/// <returns>
/// A configured T:Dfe.Data.SearchPrototype.Search.Domain.AgregateRoot.Establishments instance.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Exception thrown if an invalid document is derived from the Azure search result.
/// </exception>
public Establishments MapFrom(Response<SearchResults<object>> input)
{
ArgumentNullException.ThrowIfNull(input);

var establismentResults = Establishments.Create();
var results = input.Value.GetResults();

if (results.Any())
{
results.ToList().ForEach(searchResult =>
{
if (searchResult.Document == null)
{
throw new InvalidOperationException(
"Search result document object cannot be null.");
}

establismentResults.AddEstablishment(
new Establishment(
_establishmentIdentityMapper.MapFrom(searchResult),
_establishmentNameMapper.MapFrom(searchResult))
);
});
}

return establismentResults;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using Azure.Search.Documents.Models;
using Dfe.Data.SearchPrototype.Infrastructure.Mapping.Extensions;
using Dfe.Data.SearchPrototype.Search.Domain.AggregateRoot.ValueObjects;
using DfE.Data.ComponentLibrary.CrossCuttingConcerns.Mapping;
using System.Dynamic;

namespace Dfe.Data.SearchPrototype.Infrastructure.Mapping
{
/// <summary>
/// Facilitates mapping from the received T:Azure.Search.Documents.Models.SearchResults
/// into the required T:Dfe.Data.SearchPrototype.Search.Domain.AgregateRoot.ValueObjects.EstablishmentIdentifier object.
/// </summary>
public sealed class EstablishmentIdentityMapper : IMapper<SearchResult<object>, EstablishmentIdentifier>
{
private const string MapKey = "SearchResultToEstablishmentIdentityMap";

private readonly IObjectFactoryMapper _objectFactoryMapper;

/// <summary>
/// Mapper is injected with an T:DfE.Data.ComponentLibrary.CrossCuttingConcerns.Mapping.IObjectFactoryMapper
/// instance and uses the configuration map key 'SearchResultToEstablishmentIdentityMap' to target the
/// configuration options for this particular mapping definition, the complete implementation of which is defined in app-settings.
/// </summary>
/// <param name="objectFactoryMapper">
/// The T:DfE.Data.ComponentLibrary.CrossCuttingConcerns.Mapping.IObjectFactoryMapper definition injected via IOC container.
/// </param>
public EstablishmentIdentityMapper(IObjectFactoryMapper objectFactoryMapper)
{
_objectFactoryMapper = objectFactoryMapper;
}

/// <summary>
/// Object factory mapper definition for automatically wiring the mapping fields (described by app settings).
/// </summary>
/// <param name="input">
/// The T:Azure.Search.Documents.Models.SearchResult instance to map from.
/// </param>
/// <returns>
/// The target T:Dfe.Data.SearchPrototype.Search.Domain.AgregateRoot.ValueObjects.EstablishmentIdentifier to be mapped and returned.
/// </returns>
/// <exception cref="ArgumentException">
/// The exception thrown if a document cannot be derived from a given Azure search result.
/// </exception>
public EstablishmentIdentifier MapFrom(SearchResult<object> input)
{
ExpandoObject? searchResult = input.DeserialiseSearchResultDocument();

return searchResult == null ?
throw new ArgumentException(
$"Unable to derive search result for establishment identity map with input: {input} ") :
_objectFactoryMapper.Map<dynamic, EstablishmentIdentifier>(searchResult!, MapKey);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Azure.Search.Documents.Models;
using Dfe.Data.SearchPrototype.Infrastructure.Mapping.Extensions;
using Dfe.Data.SearchPrototype.Search.Domain.AggregateRoot.ValueObjects;
using DfE.Data.ComponentLibrary.CrossCuttingConcerns.Mapping;
using System.Dynamic;

namespace Dfe.Data.SearchPrototype.Infrastructure.Mapping
{
/// <summary>
/// Facilitates mapping from the received T:Azure.Search.Documents.Models.SearchResults
/// into the required T:Dfe.Data.SearchPrototype.Search.Domain.AgregateRoot.ValueObjects.EstablishmentName object.
/// </summary>
public sealed class EstablishmentNameMapper : IMapper<SearchResult<object>, EstablishmentDefinition>
{
private const string MapKey = "SearchResultToEstablishmentNameMap";

private readonly IObjectFactoryMapper _objectFactoryMapper;

/// <summary>
/// Mapper is injected with an T:DfE.Data.ComponentLibrary.CrossCuttingConcerns.Mapping.IObjectFactoryMapper
/// instance and uses the configuration map key 'SearchResultToEstablishmentNameMap' to target the
/// configuration options for this particular mapping definition, the complete implementation of which is defined in app settings.
/// </summary>
/// <param name="objectFactoryMapper">
/// The T:DfE.Data.ComponentLibrary.CrossCuttingConcerns.Mapping.IObjectFactoryMapper definition injected via IOC container.
/// </param>
public EstablishmentNameMapper(IObjectFactoryMapper objectFactoryMapper)
{
_objectFactoryMapper = objectFactoryMapper ??
throw new ArgumentNullException(nameof(objectFactoryMapper));
}

/// <summary>
/// Object factory mapper definition for automatically wiring the mapping fields (described by app settings).
/// </summary>
/// <param name="input">
/// The T:Azure.Search.Documents.Models.SearchResult instance to map from.
/// </param>
/// <returns>
/// The target T:Dfe.Data.SearchPrototype.Search.Domain.AgregateRoot.ValueObjects.EstablishmentName to be mapped and returned.
/// </returns>
/// <exception cref="ArgumentException">
/// The exception thrown if a document cannot be derived from a given Azure search result.
/// </exception>
public EstablishmentDefinition MapFrom(SearchResult<object> input)
{
ExpandoObject? searchResult = input.DeserialiseSearchResultDocument();

return searchResult == null ?
throw new ArgumentException(
$"Unable to derive search result for establishment name map with input: {input}.") :
_objectFactoryMapper.Map<dynamic, EstablishmentDefinition>(searchResult!, MapKey);
}
}
}
Loading
Loading