diff --git a/Dfe.Data.SearchPrototype/Infrastructure/Builders/ISearchOptionsBuilder.cs b/Dfe.Data.SearchPrototype/Infrastructure/Builders/ISearchOptionsBuilder.cs
index e409e45..85dbc00 100644
--- a/Dfe.Data.SearchPrototype/Infrastructure/Builders/ISearchOptionsBuilder.cs
+++ b/Dfe.Data.SearchPrototype/Infrastructure/Builders/ISearchOptionsBuilder.cs
@@ -21,6 +21,19 @@ public interface ISearchOptionsBuilder
///
ISearchOptionsBuilder WithSize(int? size);
+ ///
+ /// Sets the value used to define how many
+ /// records are skipped in the search response (if any),
+ /// by default we have an offset of zero and so choose not to skip any records.
+ ///
+ ///
+ /// The number of initial search results to skip, none (zero) by default .
+ ///
+ ///
+ /// The updated builder instance.
+ ///
+ ISearchOptionsBuilder WithOffset(int offset = 0);
+
///
/// Sets the mode of search to invoke, i.e. All or Any.
///
diff --git a/Dfe.Data.SearchPrototype/Infrastructure/Builders/SearchOptionsBuilder.cs b/Dfe.Data.SearchPrototype/Infrastructure/Builders/SearchOptionsBuilder.cs
index 05b64ad..bfb43f1 100644
--- a/Dfe.Data.SearchPrototype/Infrastructure/Builders/SearchOptionsBuilder.cs
+++ b/Dfe.Data.SearchPrototype/Infrastructure/Builders/SearchOptionsBuilder.cs
@@ -16,6 +16,7 @@ public sealed class SearchOptionsBuilder : ISearchOptionsBuilder
private SearchMode? _searchMode;
private int? _size;
+ private int _offset;
private bool? _includeTotalCount;
private IList? _searchFields;
private IList? _facets;
@@ -53,6 +54,23 @@ public ISearchOptionsBuilder WithSize(int? size)
return this;
}
+ ///
+ /// Sets the value used to define how many
+ /// records are skipped in the search response (if any),
+ /// by default we have an offset of zero and so choose not to skip any records.
+ ///
+ ///
+ /// The number of initial search results to skip, none (zero) by default .
+ ///
+ ///
+ /// The updated builder instance.
+ ///
+ public ISearchOptionsBuilder WithOffset(int offset = 0)
+ {
+ _offset = offset;
+ return this;
+ }
+
///
/// Sets the mode of search to invoke, i.e. All or Any.
///
@@ -139,6 +157,7 @@ public SearchOptions Build()
{
_searchOptions.SearchMode = _searchMode;
_searchOptions.Size = _size;
+ _searchOptions.Skip = _offset;
_searchOptions.IncludeTotalCount = _includeTotalCount;
_searchFields?.ToList().ForEach(_searchOptions.SearchFields.Add);
_facets?.ToList().ForEach(_searchOptions.Facets.Add);
diff --git a/Dfe.Data.SearchPrototype/Infrastructure/CognitiveSearchServiceAdapter.cs b/Dfe.Data.SearchPrototype/Infrastructure/CognitiveSearchServiceAdapter.cs
index 249be71..8cfe377 100644
--- a/Dfe.Data.SearchPrototype/Infrastructure/CognitiveSearchServiceAdapter.cs
+++ b/Dfe.Data.SearchPrototype/Infrastructure/CognitiveSearchServiceAdapter.cs
@@ -82,6 +82,7 @@ public async Task SearchAsync(SearchServiceAdapterRequest searchS
_searchOptionsBuilder
.WithSearchMode((SearchMode)_azureSearchOptions.SearchMode)
.WithSize(_azureSearchOptions.Size)
+ .WithOffset(searchServiceAdapterRequest.Offset)
.WithIncludeTotalCount(_azureSearchOptions.IncludeTotalCount)
.WithSearchFields(searchServiceAdapterRequest.SearchFields)
.WithFacets(searchServiceAdapterRequest.Facets)
@@ -100,10 +101,12 @@ await _searchByKeywordService.SearchAsync(
var results = new SearchResults()
{
- Establishments = _searchResultMapper.MapFrom(searchResults.Value.GetResults()),
+ Establishments =
+ _searchResultMapper.MapFrom(searchResults.Value.GetResults()),
Facets = searchResults.Value.Facets != null
? _facetsMapper.MapFrom(searchResults.Value.Facets.ToDictionary())
- : null
+ : null,
+ TotalNumberOfEstablishments = searchResults.Value.TotalCount
};
return results;
diff --git a/Dfe.Data.SearchPrototype/Infrastructure/Mappers/PageableSearchResultsToEstablishmentResultsMapper.cs b/Dfe.Data.SearchPrototype/Infrastructure/Mappers/PageableSearchResultsToEstablishmentResultsMapper.cs
index 6fff1cd..3ba464e 100644
--- a/Dfe.Data.SearchPrototype/Infrastructure/Mappers/PageableSearchResultsToEstablishmentResultsMapper.cs
+++ b/Dfe.Data.SearchPrototype/Infrastructure/Mappers/PageableSearchResultsToEstablishmentResultsMapper.cs
@@ -7,12 +7,12 @@
namespace Dfe.Data.SearchPrototype.Infrastructure.Mappers;
///
-/// Facilitates mapping from the received
-/// into the required object.
+/// Facilitates mapping from the received
+/// into the required object.
///
-public sealed class PageableSearchResultsToEstablishmentResultsMapper : IMapper>, Models.EstablishmentResults>
+public sealed class PageableSearchResultsToEstablishmentResultsMapper : IMapper>, Models.EstablishmentResults>
{
- private readonly IMapper _azureSearchResultToEstablishmentMapper;
+ private readonly IMapper _azureSearchResultToEstablishmentMapper;
///
/// The following mapping dependency provides the functionality to map from a raw Azure
@@ -22,7 +22,7 @@ public sealed class PageableSearchResultsToEstablishmentResultsMapper : IMapper<
///
/// Mapper used to map from the raw Azure search result to a instance.
///
- public PageableSearchResultsToEstablishmentResultsMapper(IMapper azureSearchResultToEstablishmentMapper)
+ public PageableSearchResultsToEstablishmentResultsMapper(IMapper azureSearchResultToEstablishmentMapper)
{
_azureSearchResultToEstablishmentMapper = azureSearchResultToEstablishmentMapper;
}
@@ -44,9 +44,10 @@ public PageableSearchResultsToEstablishmentResultsMapper(IMapper
/// Exception thrown if the data cannot be mapped
///
- public Models.EstablishmentResults MapFrom(Pageable> input)
+ public Models.EstablishmentResults MapFrom(Pageable> input)
{
ArgumentNullException.ThrowIfNull(input);
+ Models.EstablishmentResults establishmentResults = new();
if (input.Any())
{
@@ -54,13 +55,12 @@ public Models.EstablishmentResults MapFrom(Pageable> searchResultDocuments =
SearchResultFake.SearchResults();
+
var pageableSearchResults = PageableTestDouble.FromResults(searchResultDocuments);
// act
@@ -36,7 +36,8 @@ public void MapFrom_WithValidSearchResults_ReturnsConfiguredEstablishments()
// assert
mappedResult.Should().NotBeNull();
- mappedResult.Establishments.Should().HaveCount(searchResultDocuments.Count());
+ mappedResult.Establishments.Should().HaveCount(searchResultDocuments.Count);
+
foreach (var searchResult in searchResultDocuments)
{
searchResult.ShouldHaveMatchingMappedEstablishment(mappedResult);
@@ -47,7 +48,9 @@ public void MapFrom_WithValidSearchResults_ReturnsConfiguredEstablishments()
public void MapFrom_WithEmptySearchResults_ReturnsEmptyList()
{
// act
- EstablishmentResults? result = _searchResultsMapper.MapFrom(PageableTestDouble.FromResults(SearchResultFake.EmptySearchResult()));
+ EstablishmentResults? result =
+ _searchResultsMapper.MapFrom(
+ PageableTestDouble.FromResults(SearchResultFake.EmptySearchResult()));
// assert
result.Should().NotBeNull();
diff --git a/Dfe.Data.SearchPrototype/Infrastructure/Tests/TestDoubles/SearchOptionsBuilderTestDouble.cs b/Dfe.Data.SearchPrototype/Infrastructure/Tests/TestDoubles/SearchOptionsBuilderTestDouble.cs
index 999da1c..04d6a44 100644
--- a/Dfe.Data.SearchPrototype/Infrastructure/Tests/TestDoubles/SearchOptionsBuilderTestDouble.cs
+++ b/Dfe.Data.SearchPrototype/Infrastructure/Tests/TestDoubles/SearchOptionsBuilderTestDouble.cs
@@ -13,7 +13,9 @@ public static ISearchOptionsBuilder MockFor(SearchOptions searchOptions)
var mockSearchOptionsBuilder = new Mock();
mockSearchOptionsBuilder.Setup(searchOptionsBuilder =>
- searchOptionsBuilder.WithSize(It.IsAny())).Returns(mockSearchOptionsBuilder.Object);
+ searchOptionsBuilder.WithSize(It.IsAny())).Returns(mockSearchOptionsBuilder.Object);
+ mockSearchOptionsBuilder.Setup(searchOptionsBuilder =>
+ searchOptionsBuilder.WithOffset(It.IsAny())).Returns(mockSearchOptionsBuilder.Object);
mockSearchOptionsBuilder.Setup(searchOptionsBuilder =>
searchOptionsBuilder.WithSearchMode(It.IsAny())).Returns(mockSearchOptionsBuilder.Object);
mockSearchOptionsBuilder.Setup(searchOptionsBuilder =>
diff --git a/Dfe.Data.SearchPrototype/SearchForEstablishments/ByKeyword/ServiceAdapters/SearchServiceAdapterRequest.cs b/Dfe.Data.SearchPrototype/SearchForEstablishments/ByKeyword/ServiceAdapters/SearchServiceAdapterRequest.cs
index 1b31fd2..5fdfcfd 100644
--- a/Dfe.Data.SearchPrototype/SearchForEstablishments/ByKeyword/ServiceAdapters/SearchServiceAdapterRequest.cs
+++ b/Dfe.Data.SearchPrototype/SearchForEstablishments/ByKeyword/ServiceAdapters/SearchServiceAdapterRequest.cs
@@ -13,6 +13,12 @@ public sealed class SearchServiceAdapterRequest
///
public string SearchKeyword { get; }
+ ///
+ /// The value used to define how many records are skipped in the search response (if any),
+ /// by default we have an offset of zero and so choose not to skip any records.
+ ///
+ public int Offset { get; } = 0;
+
///
/// The collection of fields in the underlying collection to search over.
///
@@ -44,6 +50,10 @@ public sealed class SearchServiceAdapterRequest
///
/// Dictionary of search filter requests where key is the name of the filter and the value is the list of filter values.
///
+ ///
+ /// The value used to define how many records are skipped in the search response
+ /// (if any), by default we choose not to skip any records.
+ ///
///
/// The exception thrown if an invalid search keyword (null or whitespace) is prescribed.
///
@@ -51,37 +61,25 @@ public sealed class SearchServiceAdapterRequest
/// The exception type thrown if either a null or empty collection of search fields,
/// or search facets are prescribed.
///
- public SearchServiceAdapterRequest(string searchKeyword, IList searchFields, IList facets, IList? searchFilterRequests = null)
+ public SearchServiceAdapterRequest(
+ string searchKeyword,
+ IList searchFields,
+ IList facets,
+ IList? searchFilterRequests = null,
+ int offset = 0)
{
SearchKeyword =
string.IsNullOrWhiteSpace(searchKeyword) ?
throw new ArgumentNullException(nameof(searchKeyword)) : searchKeyword;
SearchFields = searchFields == null || searchFields.Count <= 0 ?
- throw new ArgumentException("", nameof(searchFields)) : searchFields;
+ throw new ArgumentException($"A valid {nameof(searchFields)} argument must be provided.") : searchFields;
Facets = facets == null || facets.Count <= 0 ?
- throw new ArgumentException("", nameof(facets)) : facets;
+ throw new ArgumentException($"A valid {nameof(facets)} argument must be provided.") : facets;
SearchFilterRequests = searchFilterRequests;
- }
- ///
- /// Factory method to allow implicit creation of a T:Dfe.Data.SearchPrototype.Search.SearchContext instance.
- ///
- ///
- /// The keyword string which defines the search.
- ///
- ///
- /// The collection of fields in the underlying collection to search over.
- ///
- ///
- /// The collection of facets to apply in the search request.
- ///
- ///
- /// A configured instance.
- ///
- public static SearchServiceAdapterRequest Create(
- string searchKeyword, IList searchFields, IList facets) =>
- new(searchKeyword, searchFields, facets);
+ Offset = offset;
+ }
}
diff --git a/Dfe.Data.SearchPrototype/SearchForEstablishments/ByKeyword/Usecase/SearchByKeywordRequest.cs b/Dfe.Data.SearchPrototype/SearchForEstablishments/ByKeyword/Usecase/SearchByKeywordRequest.cs
index 0dc7d39..44bd97b 100644
--- a/Dfe.Data.SearchPrototype/SearchForEstablishments/ByKeyword/Usecase/SearchByKeywordRequest.cs
+++ b/Dfe.Data.SearchPrototype/SearchForEstablishments/ByKeyword/Usecase/SearchByKeywordRequest.cs
@@ -14,11 +14,16 @@ public sealed class SearchByKeywordRequest
///
/// The string keyword used to search the collection specified.
///
- public SearchByKeywordRequest(string searchKeyword)
+ ///
+ /// The value used to define how many records are skipped in the search
+ /// response (if any), by default we choose not to skip any records.
+ ///
+ public SearchByKeywordRequest(string searchKeyword, int offset = 0)
{
ArgumentException.ThrowIfNullOrEmpty(nameof(searchKeyword));
SearchKeyword = searchKeyword;
+ Offset = offset;
}
///
@@ -32,7 +37,11 @@ public SearchByKeywordRequest(string searchKeyword)
///
/// The used to refine the search criteria.
///
- public SearchByKeywordRequest(string searchKeyword, IList filterRequests) : this(searchKeyword)
+ ///
+ /// The value used to define how many records are skipped in the search response (if any).
+ ///
+ public SearchByKeywordRequest(
+ string searchKeyword,IList filterRequests, int offset = 0) : this(searchKeyword, offset)
{
FilterRequests = filterRequests;
}
@@ -42,6 +51,11 @@ public SearchByKeywordRequest(string searchKeyword, IList filterR
///
public string SearchKeyword { get; }
+ ///
+ /// The value used to define how many records are skipped in the search response (if any).
+ ///
+ public int Offset { get; }
+
///
/// The filter (key/values) used to refine the search criteria.
///
diff --git a/Dfe.Data.SearchPrototype/SearchForEstablishments/ByKeyword/Usecase/SearchByKeywordResponse.cs b/Dfe.Data.SearchPrototype/SearchForEstablishments/ByKeyword/Usecase/SearchByKeywordResponse.cs
index e5608e5..a595788 100644
--- a/Dfe.Data.SearchPrototype/SearchForEstablishments/ByKeyword/Usecase/SearchByKeywordResponse.cs
+++ b/Dfe.Data.SearchPrototype/SearchForEstablishments/ByKeyword/Usecase/SearchByKeywordResponse.cs
@@ -47,7 +47,11 @@ public SearchByKeywordResponse(SearchResponseStatus status)
///
/// The of the result of the search.
///
- public SearchByKeywordResponse(EstablishmentResults establishments, EstablishmentFacets facetResults, SearchResponseStatus status)
+ public SearchByKeywordResponse(
+ EstablishmentResults establishments,
+ EstablishmentFacets facetResults,
+ SearchResponseStatus status
+ )
{
EstablishmentResults = establishments;
EstablishmentFacetResults = facetResults;
diff --git a/Dfe.Data.SearchPrototype/SearchForEstablishments/ByKeyword/Usecase/SearchByKeywordUseCase.cs b/Dfe.Data.SearchPrototype/SearchForEstablishments/ByKeyword/Usecase/SearchByKeywordUseCase.cs
index 98a5ec2..f48540a 100644
--- a/Dfe.Data.SearchPrototype/SearchForEstablishments/ByKeyword/Usecase/SearchByKeywordUseCase.cs
+++ b/Dfe.Data.SearchPrototype/SearchForEstablishments/ByKeyword/Usecase/SearchByKeywordUseCase.cs
@@ -78,7 +78,8 @@ await _searchServiceAdapter.SearchAsync(
request.SearchKeyword,
_searchByKeywordCriteria.SearchFields,
_searchByKeywordCriteria.Facets,
- request.FilterRequests));
+ request.FilterRequests,
+ request.Offset));
return results switch
{
diff --git a/Dfe.Data.SearchPrototype/SearchForEstablishments/Models/EstablishmentResults.cs b/Dfe.Data.SearchPrototype/SearchForEstablishments/Models/EstablishmentResults.cs
index 3e950e4..7d7bda1 100644
--- a/Dfe.Data.SearchPrototype/SearchForEstablishments/Models/EstablishmentResults.cs
+++ b/Dfe.Data.SearchPrototype/SearchForEstablishments/Models/EstablishmentResults.cs
@@ -6,14 +6,14 @@
///
public sealed class EstablishmentResults
{
+ private readonly List _establishments;
+
///
/// The readonly collection of
/// types derived from the underlying search mechanism.
///
public IReadOnlyCollection Establishments => _establishments.AsReadOnly();
- private readonly List _establishments;
-
///
/// Default constructor initialises a new readonly
/// collection of instances.
diff --git a/Dfe.Data.SearchPrototype/SearchForEstablishments/Models/SearchResults.cs b/Dfe.Data.SearchPrototype/SearchForEstablishments/Models/SearchResults.cs
index 86a77dc..8653abf 100644
--- a/Dfe.Data.SearchPrototype/SearchForEstablishments/Models/SearchResults.cs
+++ b/Dfe.Data.SearchPrototype/SearchForEstablishments/Models/SearchResults.cs
@@ -19,4 +19,10 @@ public class SearchResults
/// that is built from the underlying search response.
///
public EstablishmentFacets? Facets { get; init; }
+
+ ///
+ /// The Total Count returned from Establishment search gives us a total
+ /// of all available records which correlates with the given search criteria.
+ ///
+ public long? TotalNumberOfEstablishments { get; init; }
}
diff --git a/Dfe.Data.SearchPrototype/Tests/SearchForEstablishments/ByKeyword/SearchByKeywordRequestTests.cs b/Dfe.Data.SearchPrototype/Tests/SearchForEstablishments/ByKeyword/SearchByKeywordRequestTests.cs
index 09b41dc..340dcb5 100644
--- a/Dfe.Data.SearchPrototype/Tests/SearchForEstablishments/ByKeyword/SearchByKeywordRequestTests.cs
+++ b/Dfe.Data.SearchPrototype/Tests/SearchForEstablishments/ByKeyword/SearchByKeywordRequestTests.cs
@@ -37,4 +37,28 @@ public void Constructor_WithNoFilterParam_HasFilterRequestsNull()
// assert
request.FilterRequests.Should().BeNull();
}
+
+ [Fact]
+ public void Constructor_WithSetOffsetValue_AssignsCorrectPropertyValue()
+ {
+ //arrange
+ const int Offset = 10;
+ // act
+ var request = new SearchByKeywordRequest("searchKeyword", Offset);
+
+ // assert
+ request.Offset.
+ Should().Be(Offset);
+ }
+
+ [Fact]
+ public void Constructor_WithDefaultOffsetValue_AssignsDefaultPropertyValue()
+ {
+ // act
+ var request = new SearchByKeywordRequest("searchKeyword");
+
+ // assert
+ request.Offset.
+ Should().Be(0);//value of zero ensures no records are skipped
+ }
}
diff --git a/Dfe.Data.SearchPrototype/Tests/SearchForEstablishments/ByKeyword/ServiceAdapters/SearchServiceAdapterRequestTests.cs b/Dfe.Data.SearchPrototype/Tests/SearchForEstablishments/ByKeyword/ServiceAdapters/SearchServiceAdapterRequestTests.cs
new file mode 100644
index 0000000..6e90717
--- /dev/null
+++ b/Dfe.Data.SearchPrototype/Tests/SearchForEstablishments/ByKeyword/ServiceAdapters/SearchServiceAdapterRequestTests.cs
@@ -0,0 +1,108 @@
+using Dfe.Data.SearchPrototype.SearchForEstablishments.ByKeyword.ServiceAdapters;
+using Dfe.Data.SearchPrototype.SearchForEstablishments.ByKeyword.Usecase;
+using FluentAssertions;
+using Xunit;
+
+namespace Dfe.Data.SearchPrototype.Tests.SearchForEstablishments.ByKeyword.ServiceAdapters;
+
+public class SearchServiceAdapterRequestTests
+{
+ [Fact]
+ public void Constructor_WithNoFilterParam_AssignsCorrectPropertyValues()
+ {
+ // act
+ var request = new SearchServiceAdapterRequest(
+ searchKeyword:"searchKeyword",
+ searchFields:["ESTABLISHMENTNAME", "TOWN"],
+ facets:["facet1", "facet2"],
+ offset: 10
+ );
+
+ // assert
+ request.SearchKeyword.Should().Be("searchKeyword");
+ request.SearchFields[0].Should().Be("ESTABLISHMENTNAME");
+ request.SearchFields.Count().Should().Be(2);
+ request.Facets.Contains("facet2");
+ request.Facets.Should().HaveCount(2);
+ request.Offset.Should().Be(10);
+ request.SearchFilterRequests.Should().BeNull();
+ }
+
+ [Fact]
+ public void Constructor_WithFilterParam_PopulatesFilterRequests()
+ {
+ // arrange
+ var filterList = new List()
+ {
+ new FilterRequest("EducationPhase", new List