diff --git a/AzureSearchToolkit.IntegrationTest/AzureSearchToolkit.IntegrationTest.csproj b/AzureSearchToolkit.IntegrationTest/AzureSearchToolkit.IntegrationTest.csproj index e2edb50..081cc4e 100644 --- a/AzureSearchToolkit.IntegrationTest/AzureSearchToolkit.IntegrationTest.csproj +++ b/AzureSearchToolkit.IntegrationTest/AzureSearchToolkit.IntegrationTest.csproj @@ -1,21 +1,24 @@ - netcoreapp2.1 + netcoreapp3.1 false - - - - - - - - - + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/AzureSearchToolkit.IntegrationTest/Data.cs b/AzureSearchToolkit.IntegrationTest/Data.cs index 5311e05..e0cb307 100644 --- a/AzureSearchToolkit.IntegrationTest/Data.cs +++ b/AzureSearchToolkit.IntegrationTest/Data.cs @@ -20,7 +20,7 @@ class Data public const string Index = "azure-search-toolkit-integration-test"; const int ExpectedDataCount = 100; - + static readonly AzureSearchConnection azureSearchConnection; static readonly AzureSearchContext azureSearchContext; @@ -58,7 +58,7 @@ public void LoadToMemoryFromAzureSearch() { throw new InvalidOperationException( $"Tests expect {ExpectedDataCount} entries but {memory.Count} loaded from AzureSearch index '{Index}'"); - } + } } public void LoadFromJsonToAzureSearch() @@ -72,14 +72,14 @@ public void LoadFromJsonToAzureSearch() throw createIndexServiceResult.PotentialException.GetException(); } - var countServiceResult = AsyncHelper.RunSync(() => azureSearchHelper.CountDocuments(new SearchParameters(), indexName: Index)); + var countServiceResult = AsyncHelper.RunSync(() => azureSearchHelper.CountDocuments(new SearchOptions(), indexName: Index)); if (!countServiceResult.IsStatusOk() || countServiceResult.Data != ExpectedDataCount) { var baseDirectory = AppContext.BaseDirectory; var mockedDataPath = Path.Combine(baseDirectory, "App_Data\\listings-mocked.json"); - var searchParameters = new SearchParameters() + var searchOptions = new SearchOptions() { Select = new List() { "id" }, Top = 1000 @@ -87,15 +87,15 @@ public void LoadFromJsonToAzureSearch() if (countServiceResult.Data > 0) { - var allDocuments = AsyncHelper.RunSync(() => azureSearchHelper.SearchDocuments(searchParameters, indexName: Index)); + var allDocuments = AsyncHelper.RunSync(() => azureSearchHelper.SearchDocuments(searchOptions, indexName: Index)); AsyncHelper.RunSync(() => azureSearchHelper .DeleteDocumentsInIndex(allDocuments.Data.Results.Select(q => new Listing { Id = q.Document.Id }), Index)); azureSearchHelper.WaitForSearchOperationCompletion(0, Index); } - - var listings = JsonConvert.DeserializeObject>(File.ReadAllText(mockedDataPath), new JsonSerializerSettings + + var listings = JsonConvert.DeserializeObject>(File.ReadAllText(mockedDataPath), new JsonSerializerOptions { Converters = new List { new GeographyPointJsonConverter() } }); @@ -114,7 +114,7 @@ public void LoadFromJsonToAzureSearch() public void WaitForSearchOperationCompletion(int numberOfRequiredItemsInSearch) { - using (var azureSearchHelper = new AzureSearchHelper(LamaConfiguration.Current(), NullLogger.Instance)) + using (var azureSearchHelper = new AzureSearchHelper(LamaConfiguration.Current())) { azureSearchHelper.WaitForSearchOperationCompletion(numberOfRequiredItemsInSearch, Index); } diff --git a/AzureSearchToolkit.IntegrationTest/DataAssert.cs b/AzureSearchToolkit.IntegrationTest/DataAssert.cs index 1a72c8c..4910f78 100644 --- a/AzureSearchToolkit.IntegrationTest/DataAssert.cs +++ b/AzureSearchToolkit.IntegrationTest/DataAssert.cs @@ -34,13 +34,13 @@ public static void SetDefaultOrderForType(Expression(Func, IQueryable> query, + public static void Same(Func, IQueryable> query, bool useDefaultOrder = true, bool ignoreOrder = false) where TSource : class { Same(query, useDefaultOrder, ignoreOrder); } - public static void Same(Func, IQueryable> query, + public static void Same(Func, IQueryable> query, bool useDefaultOrder = true, bool ignoreOrder = false) where TSource : class { var expectQuery = query(Data.Memory()); diff --git a/AzureSearchToolkit.IntegrationTest/Models/Listing.cs b/AzureSearchToolkit.IntegrationTest/Models/Listing.cs index ef14df7..34bf1ea 100644 --- a/AzureSearchToolkit.IntegrationTest/Models/Listing.cs +++ b/AzureSearchToolkit.IntegrationTest/Models/Listing.cs @@ -1,6 +1,4 @@ using AzureSearchToolkit.Attributes; -using Microsoft.Azure.Search; -using Microsoft.Azure.Search.Models; using Microsoft.Spatial; using System; using System.Collections.Generic; diff --git a/AzureSearchToolkit.IntegrationTest/Utilities/AzureSearchHelper.cs b/AzureSearchToolkit.IntegrationTest/Utilities/AzureSearchHelper.cs index dc04e1a..48829f0 100644 --- a/AzureSearchToolkit.IntegrationTest/Utilities/AzureSearchHelper.cs +++ b/AzureSearchToolkit.IntegrationTest/Utilities/AzureSearchHelper.cs @@ -1,8 +1,10 @@ -using AzureSearchToolkit.IntegrationTest.Configuration; -using AzureSearchToolkit.Logging; +using Azure.Search.Documents; +using Azure.Search.Documents.Indexes; +using Azure.Search.Documents.Models; + +using AzureSearchToolkit.IntegrationTest.Configuration; using AzureSearchToolkit.Request; -using Microsoft.Azure.Search; -using Microsoft.Azure.Search.Models; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Linq; @@ -13,25 +15,25 @@ namespace AzureSearchToolkit.IntegrationTest.Utilities { class AzureSearchHelper: IDisposable { - private string _searchName; + private string endpoint; - private LamaConfiguration _configuration; - private SearchServiceClient _serviceClient; + private LamaConfiguration configuration; + private SearchClient serviceClient; private ILogger _logger { get; set; } public AzureSearchHelper(LamaConfiguration configuration, ILogger logger) { - _configuration = configuration; + this.configuration = configuration; _logger = logger; - var searchKey = _configuration.GetModel().SearchKey; + var searchKey = this.configuration.GetModel().SearchKey; - _searchName = _configuration.GetModel().SearchName; + endpoint = this.configuration.GetModel().SearchName; - if (!string.IsNullOrWhiteSpace(_searchName) && !string.IsNullOrWhiteSpace(searchKey)) + if (!string.IsNullOrWhiteSpace(endpoint) && !string.IsNullOrWhiteSpace(searchKey)) { - _serviceClient = new SearchServiceClient(_searchName, new SearchCredentials(searchKey)); + serviceClient = new SearchClient(endpoint, new SearchCredentials(searchKey)); } } @@ -44,7 +46,7 @@ public async Task> CreateSearchIndex(string indexName = n try { - indexExists = await _serviceClient.Indexes.ExistsAsync(indexName); + indexExists = await serviceClient.Indexes.ExistsAsync(indexName); } catch (Exception e) { @@ -70,7 +72,7 @@ public async Task> CreateSearchIndex(string indexName = n try { - var result = await _serviceClient.Indexes.CreateAsync(definition); + var result = await serviceClient.Indexes.CreateAsync(definition); if (result != null) { @@ -79,7 +81,7 @@ public async Task> CreateSearchIndex(string indexName = n } catch (Exception e) { - _logger.Log(TraceEventType.Error, e, null, "Index {0} was not created!", indexName); + _logger.LogError(e, "Index {indexName} was not created!", indexName); } } @@ -137,14 +139,14 @@ public async Task> ChangeDocumentsInIndex(IEnumerable return serviceResult.CopyStatus(searchIndexCreateServiceResult); } - var indexActions = new List>(); + var indexActions = new List>(); var documentCounter = 0; foreach (var document in documents) { var crudType = AzureSearchIndexType.Upload; - IndexAction indexAction = null; + IndexDocumentsAction indexAction = null; if (crudTypes.Count() > documentCounter) { @@ -158,16 +160,16 @@ public async Task> ChangeDocumentsInIndex(IEnumerable switch (crudType) { case AzureSearchIndexType.Upload: - indexAction = IndexAction.Upload(document); + indexAction = IndexDocumentsAction.Upload(document); break; case AzureSearchIndexType.Delete: - indexAction = IndexAction.Delete(document); + indexAction = IndexDocumentsAction.Delete(document); break; case AzureSearchIndexType.Merge: - indexAction = IndexAction.Merge(document); + indexAction = IndexDocumentsAction.Merge(document); break; default: - indexAction = IndexAction.MergeOrUpload(document); + indexAction = IndexDocumentsAction.MergeOrUpload(document); break; } @@ -177,7 +179,7 @@ public async Task> ChangeDocumentsInIndex(IEnumerable } var batch = IndexBatch.New(indexActions); - var indexClient = _serviceClient.Indexes.GetClient(indexName); + var indexClient = serviceClient.Indexes.GetClient(indexName); try { @@ -191,19 +193,18 @@ public async Task> ChangeDocumentsInIndex(IEnumerable // the batch. Depending on your application, you can take compensating actions like delaying and // retrying. For this simple demo, we just log the failed document keys and continue. serviceResult.SetException(e); - _logger.Log(TraceEventType.Error, e, null, "Failed to index some of the documents: {0}", - string.Join(", ", e.IndexingResults.Where(r => !r.Succeeded).Select(r => r.Key))); + _logger.LogError(e, $"Failed to index some of the documents: {Environment.NewLine}{{documents}}", e.IndexingResults.Where(r => !r.Succeeded).Select(r => r.Key)); } catch (Exception e) { serviceResult.SetException(e); - _logger.Log(TraceEventType.Error, e, null, "Search index failed"); + _logger.LogError(e, "Search index failed"); } return serviceResult; } - public async Task>> SearchDocuments(SearchParameters searchParameters, string searchText = null, + public async Task>> SearchDocuments(SearchOptions searchOptions, string searchText = null, string indexName = null) where T : class { var serviceResult = new ServiceResult>(); @@ -215,7 +216,7 @@ public async Task>> SearchDocuments(Sea indexName = GetIndexName(indexName); - var indexClient = _serviceClient.Indexes.GetClient(indexName); + var indexClient = serviceClient.Indexes.GetClient(indexName); if (indexClient != null) { @@ -223,7 +224,7 @@ public async Task>> SearchDocuments(Sea try { - var response = await indexClient.Documents.SearchWithHttpMessagesAsync(searchText, searchParameters, customHeaders: headers); + var response = await indexClient.Documents.SearchWithHttpMessagesAsync(searchText, searchOptions, customHeaders: headers); if (response.Response.IsSuccessStatusCode) { @@ -233,9 +234,9 @@ public async Task>> SearchDocuments(Sea { var searchId = headerValues.FirstOrDefault(); - _logger.Log(TraceEventType.Information, null, new Dictionary + _logger.LogInformation(, new Dictionary { - {"SearchServiceName", _searchName }, + {"SearchServiceName", endpoint }, {"SearchId", searchId}, {"IndexName", indexName}, {"QueryTerms", searchText} @@ -248,7 +249,7 @@ public async Task>> SearchDocuments(Sea { serviceResult.SetStatusWithMessage(response.Response.StatusCode, $"Search failed for indexName {indexName}."); - _logger.Log(TraceEventType.Warning, null, null, $"Search failed for indexName {indexName}. Reason: {response.Response.ReasonPhrase}"); + _logger.LogWarning("Search failed for indexName {indexName}. Reason: {reason}", indexName, response.Response.ReasonPhrase); } } catch (Exception e) @@ -256,23 +257,22 @@ public async Task>> SearchDocuments(Sea serviceResult .SetException(e) .SetMessage($"Search failed for indexName {indexName}."); - _logger.Log(TraceEventType.Error, e, null, - $"Search failed for indexName {indexName}. Query text: {searchText}, Query: {searchParameters.ToString()}"); + _logger.LogError(e, "Search failed for indexName {indexName}. Query text: {searchText}, Query: {searchOptions}", indexName, searchText, searchOptions); } } return serviceResult; } - public async Task> CountDocuments(SearchParameters searchParameters, string searchText = null, + public async Task> CountDocuments(SearchOptions searchOptions, string searchText = null, string indexName = null) where T : class { var serviceResult = new ServiceResult(); - searchParameters.Top = 0; - searchParameters.IncludeTotalResultCount = true; + searchOptions.Top = 0; + searchOptions.IncludeTotalResultCount = true; - var documentSearchServiceResult = await SearchDocuments(searchParameters, searchText, indexName); + var documentSearchServiceResult = await SearchDocuments(searchOptions, searchText, indexName); if (documentSearchServiceResult.IsStatusOk()) { @@ -286,20 +286,21 @@ public async Task> CountDocuments(SearchParameters search return serviceResult; } - public SearchParameters GetSearchParameters(ApiParameters apiParameters) + public SearchOptions GetSearchParameters(ApiParameters apiParameters) { - var searchParameters = new SearchParameters + var searchOptions = new SearchOptions { - IncludeTotalResultCount = true, + IncludeTotalCount = true, SearchMode = SearchMode.Any, - Top = apiParameters.Limit, + Size = apiParameters.Limit, Skip = (apiParameters.Page - 1) * apiParameters.Limit, - QueryType = QueryType.Full + QueryType = SearchQueryType.Full }; if (apiParameters.IsSearchQuery()) { - searchParameters.SearchFields = apiParameters.GetSplittedQueryBy(); + var searchFields = searchOptions.SearchFields; + apiParameters.GetSplittedQueryBy().ForEach(f => searchFields.Add(f)); } var orderBy = ""; @@ -341,9 +342,9 @@ public SearchParameters GetSearchParameters(ApiParameters apiParameters) orderBy = "createdAt desc"; } - searchParameters.OrderBy = new List { orderBy }; + searchOptions.OrderBy.Add(orderBy); - return searchParameters; + return searchOptions; } public string JoinFilters(string firstFilter, string secondFilter, bool isAnd = true) @@ -549,10 +550,10 @@ public async Task WaitForSearchOperationCompletionAsync(int numberOfRequiredI var numberOfRetries = 0; var numberOfItemsInSearch = -1; - var searchParemeters = new SearchParameters + var searchParemeters = new SearchOptions { - Top = 1, - IncludeTotalResultCount = true + Size = 1, + IncludeTotalCount = true }; do @@ -595,7 +596,7 @@ private string GetIndexName(string index) public void Dispose() { - _serviceClient.Dispose(); + serviceClient.Dispose(); } } } diff --git a/AzureSearchToolkit.Test/AzureSearchToolkit.Test.csproj b/AzureSearchToolkit.Test/AzureSearchToolkit.Test.csproj index 5e87261..82992e0 100644 --- a/AzureSearchToolkit.Test/AzureSearchToolkit.Test.csproj +++ b/AzureSearchToolkit.Test/AzureSearchToolkit.Test.csproj @@ -1,15 +1,18 @@ - netcoreapp2.1 + netcoreapp3.1 false - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/AzureSearchToolkit/Async/AsyncQueryable.Entity.cs b/AzureSearchToolkit/Async/AsyncQueryable.Entity.cs deleted file mode 100644 index 65dd84f..0000000 --- a/AzureSearchToolkit/Async/AsyncQueryable.Entity.cs +++ /dev/null @@ -1,162 +0,0 @@ -using System; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; - -namespace AzureSearchToolkit.Async -{ - public static partial class AsyncQueryable - { - static readonly Lazy firstMethodInfo = QueryableMethodByArgs("First", 1); - static readonly Lazy firstPredicateMethodInfo = QueryableMethodByArgs("First", 2); - static readonly Lazy firstOrDefaultMethodInfo = QueryableMethodByArgs("FirstOrDefault", 1); - static readonly Lazy firstOrDefaultPredicateMethodInfo = QueryableMethodByArgs("FirstOrDefault", 2); - - static readonly Lazy singleMethodInfo = QueryableMethodByArgs("Single", 1); - static readonly Lazy singlePredicateMethodInfo = QueryableMethodByArgs("Single", 2); - static readonly Lazy singleOrDefaultMethodInfo = QueryableMethodByArgs("SingleOrDefault", 1); - static readonly Lazy singleOrDefaultPredicateMethodInfo = QueryableMethodByArgs("SingleOrDefault", 2); - - /// - /// Asynchronously returns the first element of a sequence. - /// - /// - /// A task that returns the first element in . - /// - /// The to return the first element of. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - /// - /// is null. - /// The source sequence is empty. - public static async Task FirstAsync(this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) - { - return (TSource)await ExecuteAsync(source.Provider, FinalExpression(source, firstMethodInfo.Value), cancellationToken); - } - - /// - /// Asynchronously returns the first element of a sequence that satisfies a specified condition. - /// - /// - /// A task that returns the first element in that passes the test in . - /// - /// An to return an element from. - /// A function to test each element for a condition. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - /// - /// or is null. - /// No element satisfies the condition in .-or-The source sequence is empty. - public static async Task FirstAsync(this IQueryable source, Expression> predicate, CancellationToken cancellationToken = default(CancellationToken)) - { - return (TSource)await ExecuteAsync(source.Provider, FinalExpression(source, firstPredicateMethodInfo.Value, predicate), cancellationToken); - } - - /// - /// Asynchronously returns the first element of a sequence, or a default value if the sequence contains no elements. - /// - /// - /// A task that returns default() if is empty; otherwise, the first element in . - /// - /// The to return the first element of. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - /// - /// is null. - public static async Task FirstOrDefaultAsync(this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) - { - return (TSource)await ExecuteAsync(source.Provider, FinalExpression(source, firstOrDefaultMethodInfo.Value), cancellationToken); - } - - /// - /// Asynchronously returns the first element of a sequence that satisfies a specified condition or a default value if no such element is found. - /// - /// - /// A task that returns default() if is empty or if no element passes the test specified by ; otherwise, the first element in that passes the test specified by . - /// - /// An to return an element from. - /// A function to test each element for a condition. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - /// - /// or is null. - public static async Task FirstOrDefaultAsync(this IQueryable source, Expression> predicate, CancellationToken cancellationToken = default(CancellationToken)) - { - return (TSource)await ExecuteAsync(source.Provider, FinalExpression(source, firstOrDefaultPredicateMethodInfo.Value, predicate), cancellationToken); - } - - /// - /// Asynchronously returns the only element of a sequence, and throws an exception if there is not exactly one element in the sequence. - /// - /// - /// A task that returns the single element of the input sequence. - /// - /// An to return the single element of. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - /// - /// is null. - /// - /// has more than one element. - public static async Task SingleAsync(this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) - { - return (TSource)await ExecuteAsync(source.Provider, FinalExpression(source, singleMethodInfo.Value), cancellationToken); - } - - /// - /// Asynchronously returns the only element of a sequence that satisfies a specified condition, and throws an exception if more than one such element exists. - /// - /// - /// A task that returns the single element of the input sequence that satisfies the condition in . - /// - /// An to return a single element from. - /// A function to test an element for a condition. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - /// - /// or is null. - /// No element satisfies the condition in .-or-More than one element satisfies the condition in .-or-The source sequence is empty. - public static async Task SingleAsync(this IQueryable source, Expression> predicate, CancellationToken cancellationToken = default(CancellationToken)) - { - return (TSource)await ExecuteAsync(source.Provider, FinalExpression(source, singlePredicateMethodInfo.Value, predicate), cancellationToken); - } - - /// - /// Asynchronously returns the only element of a sequence, or a default value if the sequence is empty; this method throws an exception if there is more than one element in the sequence. - /// - /// - /// A task that returns the single element of the input sequence, or default() if the sequence contains no elements. - /// - /// An to return the single element of. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - /// - /// is null. - /// - /// has more than one element. - public static async Task SingleOrDefaultAsync(this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) - { - return (TSource)await ExecuteAsync(source.Provider, FinalExpression(source, singleOrDefaultMethodInfo.Value), cancellationToken); - } - - /// - /// Asynchronously returns the only element of a sequence that satisfies a specified condition or a default value if no such element exists; this method throws an exception if more than one element satisfies the condition. - /// - /// - /// A task that returns the single element of the input sequence that satisfies the condition in , or default() if no such element is found. - /// - /// An to return a single element from. - /// A function to test an element for a condition. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - /// - /// or is null. - /// More than one element satisfies the condition in . - public static async Task SingleOrDefaultAsync(this IQueryable source, Expression> predicate, CancellationToken cancellationToken = default(CancellationToken)) - { - return (TSource)await ExecuteAsync(source.Provider, FinalExpression(source, singleOrDefaultPredicateMethodInfo.Value, predicate), cancellationToken); - } - } -} diff --git a/AzureSearchToolkit/Async/AsyncQueryable.cs b/AzureSearchToolkit/Async/AsyncQueryable.cs deleted file mode 100644 index 324de0c..0000000 --- a/AzureSearchToolkit/Async/AsyncQueryable.cs +++ /dev/null @@ -1,287 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Threading; -using System.Threading.Tasks; - -namespace AzureSearchToolkit.Async -{ - /// - /// Provides a set of static methods for querying data structures that implement in an asynchronous manner. - /// - public static partial class AsyncQueryable - { - static readonly Lazy countMethodInfo = QueryableMethodByArgs("Count", 1); - static readonly Lazy countPredicateMethodInfo = QueryableMethodByArgs("Count", 2); - static readonly Lazy longCountMethodInfo = QueryableMethodByArgs("LongCount", 1); - static readonly Lazy longCountPredicateMethodInfo = QueryableMethodByArgs("LongCount", 2); - static readonly Lazy minMethodInfo = QueryableMethodByArgs("Min", 1); - static readonly Lazy minSelectorMethodInfo = QueryableMethodByArgs("Min", 2); - static readonly Lazy maxMethodInfo = QueryableMethodByArgs("Max", 1); - static readonly Lazy maxSelectorMethodInfo = QueryableMethodByArgs("Max", 2); - - /// - /// Asynchronously returns the number of elements in a sequence. - /// - /// - /// A task that returns the number of elements in the input sequence. - /// - /// The that contains the elements to be counted. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - /// - /// is null. - /// The number of elements in is larger than . - public static async Task CountAsync(this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) - { - return (int)await ExecuteAsync(source.Provider, FinalExpression(source, countMethodInfo.Value), cancellationToken); - } - - /// - /// Asynchronously returns the number of elements in the specified sequence that satisfies a condition. - /// - /// - /// A task that returns the number of elements in the sequence that satisfies the condition in the predicate function. - /// - /// An that contains the elements to be counted. - /// A function to test each element for a condition. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - /// - /// or is null. - /// The number of elements in is larger than . - public static async Task CountAsync(this IQueryable source, Expression> predicate, CancellationToken cancellationToken = default(CancellationToken)) - { - return (int)await ExecuteAsync(source.Provider, FinalExpression(source, countPredicateMethodInfo.Value, predicate), cancellationToken); - } - - /// - /// Asynchronously returns an that represents the total number of elements in a sequence. - /// - /// - /// A task that returns the number of elements in . - /// - /// An that contains the elements to be counted. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - /// - /// is null. - /// The number of elements exceeds . - public static async Task LongCountAsync(this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) - { - return (long)await ExecuteAsync(source.Provider, FinalExpression(source, longCountMethodInfo.Value), cancellationToken); - } - - /// - /// Asynchronously returns an that represents the number of elements in a sequence that satisfy a condition. - /// - /// - /// A task that returns the number of elements in that satisfy the condition in the predicate function. - /// - /// An that contains the elements to be counted. - /// A function to test each element for a condition. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - /// - /// or is null. - /// The number of matching elements exceeds . - public static async Task LongCountAsync(this IQueryable source, Expression> predicate, CancellationToken cancellationToken = default(CancellationToken)) - { - return (long)await ExecuteAsync(source.Provider, FinalExpression(source, longCountPredicateMethodInfo.Value, predicate), cancellationToken); - } - - /// - /// Asynchronously returns the minimum value of a generic . - /// - /// - /// A task that returns the minimum value in the sequence. - /// - /// A sequence of values to determine the minimum of. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - /// - /// is null. - public static async Task MinAsync(this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) - { - return (TSource)await ExecuteAsync(source.Provider, FinalExpression(source, minMethodInfo.Value), cancellationToken); - } - - /// - /// Invokes a projection function on each element of a generic and returns the minimum resulting value. - /// - /// - /// A task that returns the minimum value in the sequence. - /// - /// A sequence of values to determine the minimum of. - /// A projection function to apply to each element. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - /// The type of the value returned by the function represented by . - /// - /// or is null. - public static async Task MinAsync(this IQueryable source, Expression> selector, CancellationToken cancellationToken = default(CancellationToken)) - { - return (TResult)await ExecuteAsync(source.Provider, FinalExpression(source, minSelectorMethodInfo.Value, selector), cancellationToken); - } - - /// - /// Asynchronously returns the maximum value in a generic . - /// - /// - /// A task that returns the maximum value in the sequence. - /// - /// A sequence of values to determine the maximum of. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - /// - /// is null. - public static async Task MaxAsync(this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) - { - return (TSource)await ExecuteAsync(source.Provider, FinalExpression(source, maxMethodInfo.Value), cancellationToken); - } - - /// - /// Invokes a projection function on each element of a generic and returns the maximum resulting value. - /// - /// - /// A task that returns the maximum value in the sequence. - /// - /// A sequence of values to determine the maximum of. - /// A projection function to apply to each element. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - /// The type of the value returned by the function represented by . - /// - /// or is null. - public static async Task MaxAsync(this IQueryable source, Expression> selector, CancellationToken cancellationToken = default(CancellationToken)) - { - return (TResult)await ExecuteAsync(source.Provider, FinalExpression(source, maxSelectorMethodInfo.Value, selector), cancellationToken); - } - - /// - /// Creates a from an that is executed asyncronously. - /// - /// - /// A task that returns the newly created list. - /// - /// A sequence of values to create a list from. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - public static async Task> ToListAsync(this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) - { - return ((IEnumerable)await ExecuteAsync(source.Provider, source.Expression, cancellationToken)).ToList(); - } - - /// - /// Creates an array from an that is executed asyncronously. - /// - /// - /// A task that returns the newly created array. - /// - /// A sequence of values to create an array from. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - public static async Task ToArrayAsync(this IQueryable source, CancellationToken cancellationToken = default(CancellationToken)) - { - return ((IEnumerable)await ExecuteAsync(source.Provider, source.Expression, cancellationToken)).ToArray(); - } - /* - /// - /// Creates a from an according to a specified key selector function. - /// - /// - /// A that contains keys and values. - /// - /// An to create a from. - /// A function to extract a key from each element. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - /// The type of the key returned by . - /// - /// or is null.-or- produces a key that is null. - /// - /// produces duplicate keys for two elements. - public static async Task> ToDictionaryAsync(this IQueryable source, Func keySelector, CancellationToken cancellationToken = default(CancellationToken)) - { - return ((IEnumerable)await ExecuteAsync(source.Provider, source.Expression, cancellationToken)).ToDictionary(keySelector); - } - - /// - /// Creates a from an according to specified key selector and element selector functions. - /// - /// - /// A that contains values of type selected from the input sequence. - /// - /// An to create a from. - /// A function to extract a key from each element. - /// A transform function to produce a result element value from each element. - /// The optional which can be used to cancel this task. - /// The type of the elements of . - /// The type of the key returned by . - /// The type of the value returned by . - /// - /// or or is null.-or- produces a key that is null. - /// - /// produces duplicate keys for two elements. - public static async Task> ToDictionaryAsync(this IQueryable source, Func keySelector, Func elementSelector, CancellationToken cancellationToken = default(CancellationToken)) - { - return ((IEnumerable)await ExecuteAsync(source.Provider, source.Expression, cancellationToken)).ToDictionary(keySelector, elementSelector); - } - */ - - static Lazy QueryableMethodByArgs(string name, int parameterCount, Type secondParameterType = null) - { - return new Lazy(() => typeof(Queryable).GetTypeInfo().DeclaredMethods - .Single(m => m.Name == name && m.GetParameters().Length == parameterCount && - (secondParameterType == null || m.GetParameters()[1].ParameterType == secondParameterType))); - } - - static Lazy QueryableMethodByReturnType(string name, int parameterCount, Type returnType) - { - return new Lazy(() => typeof(Queryable).GetTypeInfo().DeclaredMethods - .Single(m => m.Name == name && m.ReturnType == returnType && m.GetParameters().Length == parameterCount)); - } - - static Lazy QueryableMethodBySelectorParameterType(string name, int parameterCount, Type selectorParameterType) - { - return new Lazy(() => - { - return typeof(Queryable).GetTypeInfo().DeclaredMethods - .Where(m => m.Name == name && m.IsGenericMethod) - .Select(m => Tuple.Create(m, m.GetParameters())) - .Where(m => m.Item2.Length == parameterCount) - .Single(m => m.Item2[1].ParameterType.GenericTypeArguments[0].GenericTypeArguments[1] == selectorParameterType).Item1; - }); - } - - static Lazy QueryableMethodByQueryableParameterType(string name, int parameterCount, Type sourceParameterType) - { - return new Lazy(() => - { - var parameterType = typeof(IQueryable<>).MakeGenericType(sourceParameterType); - - return typeof(Queryable).GetTypeInfo().DeclaredMethods - .Single(m => m.Name == name && m.GetParameters().Length == parameterCount && - m.GetParameters()[0].ParameterType == parameterType); - }); - } - - static Expression FinalExpression(IQueryable source, MethodInfo method, params Expression[] arguments) - { - var finalMethod = method.IsGenericMethod ? method.MakeGenericMethod(typeof(TSource)) : method; - return Expression.Call(null, finalMethod, new[] { source.Expression }.Concat(arguments)); - } - - static Expression FinalExpression(IQueryable source, MethodInfo method, params Expression[] arguments) - { - return Expression.Call(null, method.MakeGenericMethod(typeof(TSource), typeof(TResult)), new[] { source.Expression }.Concat(arguments)); - } - - static Task ExecuteAsync(IQueryProvider provider, Expression expression, CancellationToken cancellationToken) - { - return ((IAsyncQueryExecutor)provider).ExecuteAsync(expression, cancellationToken); - } - } -} diff --git a/AzureSearchToolkit/Async/IAsyncQueryExecutor.cs b/AzureSearchToolkit/Async/IAsyncQueryExecutor.cs deleted file mode 100644 index 5ebd469..0000000 --- a/AzureSearchToolkit/Async/IAsyncQueryExecutor.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Linq.Expressions; -using System.Threading; -using System.Threading.Tasks; - -namespace AzureSearchToolkit.Async -{ - /// - /// Defines methods to asyncronously execute queries that are described by an object. - /// - public interface IAsyncQueryExecutor - { - /// - /// Executes the query represented by a specified expression tree asyncronously. - /// - /// An expression tree that represents a LINQ query. - /// The optional token to monitor for cancellation requests. - /// The task that returns the value that results from executing the specified query. - Task ExecuteAsync(Expression expression, CancellationToken cancellationToken = default(CancellationToken)); - - /// - /// Executes the strongly-typed query represented by a specified expression tree asyncronously. - /// - /// The type of the value that results from executing the query. - /// An expression tree that represents a LINQ query. - /// The optional token to monitor for cancellation requests. - /// The task that returns the value that results from executing the specified query. - Task ExecuteAsync(Expression expression, CancellationToken cancellationToken = default(CancellationToken)) where TResult: class; - } -} diff --git a/AzureSearchToolkit/Attributes/AzureSearchIndexAttribute.cs b/AzureSearchToolkit/Attributes/AzureSearchIndexAttribute.cs deleted file mode 100644 index 2c32442..0000000 --- a/AzureSearchToolkit/Attributes/AzureSearchIndexAttribute.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace AzureSearchToolkit.Attributes -{ - [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] - public class AzureSearchIndexAttribute: Attribute - { - public readonly string Index; - - public AzureSearchIndexAttribute(string index) - { - Index = index; - } - } -} diff --git a/AzureSearchToolkit/AzureSearchConnection.cs b/AzureSearchToolkit/AzureSearchConnection.cs deleted file mode 100644 index 96c7276..0000000 --- a/AzureSearchToolkit/AzureSearchConnection.cs +++ /dev/null @@ -1,324 +0,0 @@ -using AzureSearchToolkit.Attributes; -using AzureSearchToolkit.Logging; -using AzureSearchToolkit.Request; -using AzureSearchToolkit.Utilities; -using Microsoft.Azure.Search; -using Microsoft.Azure.Search.Models; -using Microsoft.Rest.Azure; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace AzureSearchToolkit -{ - public class AzureSearchConnection : IAzureSearchConnection, IDisposable - { - static readonly TimeSpan defaultTimeout = TimeSpan.FromSeconds(10); - - /// - /// Placeholder for index when no Type is used in constructor - /// - string IndexPlaceholder { get; set; } - - /// - /// Azure Search index mapping for CLR Type - /// - readonly Dictionary indexes; - - internal Lazy SearchClient { get; private set; } - - /// - /// How long to wait for a response to a network request before - /// giving up. - /// - public TimeSpan Timeout { get; } - - public AzureSearchConnection(string searchName, string searchKey, TimeSpan? timeout = null) - : this(searchName, searchKey, string.Empty, timeout) { } - - public AzureSearchConnection(string searchName, string searchKey, string index, TimeSpan? timeout = null) - { - if (timeout.HasValue) - { - Argument.EnsurePositive(nameof(timeout), timeout.Value); - } - - Argument.EnsureNotBlank(nameof(searchName), searchName); - Argument.EnsureNotBlank(nameof(searchKey), searchKey); - - IndexPlaceholder = index; - indexes = new Dictionary(); - - SearchClient = new Lazy(() => new SearchServiceClient(searchName, new SearchCredentials(searchKey))); - Timeout = timeout ?? defaultTimeout; - } - - public AzureSearchConnection(string searchName, string searchKey, string index, Type indexType, TimeSpan? timeout = null) - : this(searchName, searchKey, new Dictionary() { { indexType, index } }, timeout) { } - - public AzureSearchConnection(string searchName, string searchKey, Dictionary indexesWithType, TimeSpan? timeout = null) - { - if (timeout.HasValue) - { - Argument.EnsurePositive(nameof(timeout), timeout.Value); - } - - Argument.EnsureNotEmpty(nameof(indexesWithType), indexesWithType); - Argument.EnsureNotBlank(nameof(searchName), searchName); - Argument.EnsureNotBlank(nameof(searchKey), searchKey); - - foreach (var indexWithType in indexesWithType) - { - Argument.EnsureNotNull(nameof(indexWithType.Key), indexWithType.Key); - Argument.EnsureNotBlank(nameof(indexWithType.Value), indexWithType.Value); - } - - if (indexes.GroupBy(q => q.Key).Count() != indexes.Count) - { - throw new ArgumentException("Duplicate types found in indexes!"); - } - - if (indexes.GroupBy(q => q.Value).Count() != indexes.Count) - { - throw new ArgumentException("Duplicate index names found in indexes!"); - } - - indexes = indexesWithType; - - SearchClient = new Lazy(() => new SearchServiceClient(searchName, new SearchCredentials(searchKey))); - Timeout = timeout ?? defaultTimeout; - } - - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - if (SearchClient.IsValueCreated) - { - SearchClient.Value.Dispose(); - } - } - } - - /// - public async Task ChangeDocumentsInIndexAsync(SortedDictionary changedDocuments, ILogger logger = null) - where T : class - { - Argument.EnsureNotEmpty(nameof(changedDocuments), changedDocuments); - - if (logger == null) - { - logger = NullLogger.Instance; - } - - await EnsureSearchIndexAsync(logger); - - var index = GetIndex(); - var indexActions = new List>(); - - foreach (var keyValuePair in changedDocuments) - { - IndexAction indexAction = null; - - switch (keyValuePair.Value) - { - case IndexActionType.Upload: - indexAction = IndexAction.Upload(keyValuePair.Key); - break; - case IndexActionType.Delete: - indexAction = IndexAction.Delete(keyValuePair.Key); - break; - case IndexActionType.Merge: - indexAction = IndexAction.Merge(keyValuePair.Key); - break; - default: - indexAction = IndexAction.MergeOrUpload(keyValuePair.Key); - break; - } - - indexActions.Add(indexAction); - } - - var batch = IndexBatch.New(indexActions); - var indexClient = SearchClient.Value.Indexes.GetClient(index); - - try - { - var documentIndexResult = await indexClient.Documents.IndexAsync(batch); - - return documentIndexResult.Results != null && documentIndexResult.Results.Count == changedDocuments.Count(); - } - catch (IndexBatchException e) - { - // Sometimes when your Search service is under load, indexing will fail for some of the documents in - // the batch. Depending on your application, you can take compensating actions like delaying and - // retrying. For this simple demo, we just log the failed document keys and continue. - logger.Log(TraceEventType.Error, e, null, "Failed to index some of the documents: {0}", - string.Join(", ", e.IndexingResults.Where(r => !r.Succeeded).Select(r => r.Key))); - } - catch (Exception e) - { - logger.Log(TraceEventType.Error, e, null, "Search index failed"); - } - - return false; - } - - /// - public async Task EnsureSearchIndexAsync(ILogger logger = null) where T : class - { - var index = GetIndex(); - - if (logger == null) - { - logger = NullLogger.Instance; - } - - var indexExists = false; - - try - { - indexExists = await SearchClient.Value.Indexes.ExistsAsync(index); - } - catch (Exception e) - { - var message = $"Error on checking if {index} exists!"; - - logger.Log(TraceEventType.Error, e, null, message); - - return false; - } - - if (indexExists) - { - return false; - } - - var definition = new Index() - { - Name = index, - Fields = FieldBuilder.BuildForType() - }; - - try - { - var result = await SearchClient.Value.Indexes.CreateAsync(definition); - - if (result != null) - { - return true; - } - } - catch (Exception e) - { - logger.Log(TraceEventType.Error, e, null, $"Index {index} was not created!", null); - } - - return false; - } - - /// - public async Task>> SearchAsync(SearchParameters searchParameters, Type searchType, - string searchText = null, ILogger logger = null) - { - if (logger == null) - { - logger = NullLogger.Instance; - } - - var searchIndex = GetIndex(searchType); - var indexClient = SearchClient.Value.Indexes.GetClient(searchIndex); - - if (indexClient != null) - { - var headers = new Dictionary>() { { "x-ms-azs-return-searchid", new List() { "true" } } }; - - try - { - var response = await indexClient.Documents.SearchWithHttpMessagesAsync(searchText, searchParameters, customHeaders: headers); - - if (response.Response.IsSuccessStatusCode) - { - IEnumerable headerValues = null; - - if (response.Response.Headers.TryGetValues("x-ms-azs-searchid", out headerValues)) - { - var searchId = headerValues.FirstOrDefault(); - - logger.Log(TraceEventType.Information, null, new Dictionary - { - {"SearchServiceName", SearchClient.Value.SearchServiceName }, - {"SearchId", searchId}, - {"IndexName", searchIndex}, - {"QueryTerms", searchText} - }, "Search"); - } - } - else - { - logger.Log(TraceEventType.Warning, null, null, - $"Search failed for indexName {searchIndex}. Reason: {response.Response.ReasonPhrase}"); - } - - return response; - } - catch (Exception e) - { - logger.Log(TraceEventType.Error, e, null, - $"Search failed for indexName {searchIndex}. Query text: {searchText}, Query: {searchParameters.ToString()}, Reason: {e.Message}"); - - throw e; - } - } - else - { - logger.Log(TraceEventType.Warning, null, null, $"Problem with creating search client for {searchIndex} index!"); - } - - return null; - } - - private string GetIndex() where T : class - { - return GetIndex(typeof(T)); - } - - private string GetIndex(Type type) - { - if (!indexes.ContainsKey(type)) - { - if (!string.IsNullOrWhiteSpace(IndexPlaceholder) && indexes.Count == 0) - { - indexes.Add(type, IndexPlaceholder); - - IndexPlaceholder = null; - } - else - { - var searchIndex = TypeHelper.GetAttributeValue(type, (searchAttribute) => searchAttribute.Index); - - if (!string.IsNullOrWhiteSpace(searchIndex)) - { - indexes.Add(type, searchIndex); - } - else - { - throw new KeyNotFoundException($"AzureSearch index for type {type} was not found!"); - } - } - } - - return indexes[type]; - } - } -} diff --git a/AzureSearchToolkit/AzureSearchContext.cs b/AzureSearchToolkit/AzureSearchContext.cs deleted file mode 100644 index 192007b..0000000 --- a/AzureSearchToolkit/AzureSearchContext.cs +++ /dev/null @@ -1,123 +0,0 @@ -using AzureSearchToolkit.Logging; -using AzureSearchToolkit.Mapping; -using AzureSearchToolkit.Utilities; -using Microsoft.Azure.Search.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace AzureSearchToolkit -{ - public class AzureSearchContext : IAzureSearchContext - { - /// - /// Specifies the connection to the AzureSearch instance. - /// - public IAzureSearchConnection Connection { get; } - - /// - /// The logging mechanism for diagnostic information. - /// - public ILogger Logger { get; } - - /// - /// The mapping to describe how objects and their properties are mapped to AzureSearch. - /// - public IAzureSearchMapping Mapping { get; } - - public AzureSearchContext(IAzureSearchConnection connection, IAzureSearchMapping mapping = null, ILogger logger = null) - { - Argument.EnsureNotNull(nameof(connection), connection); - - Connection = connection; - Mapping = mapping ?? new AzureSearchMapping(); - Logger = logger ?? NullLogger.Instance; - } - - /// - public async Task ChangeDocumentAsync(T document, IndexActionType indexActionType) where T : class - { - Argument.EnsureNotNull(nameof(document), document); - - return await Connection.ChangeDocumentsInIndexAsync(new SortedDictionary() - { - { document, indexActionType } - }); - } - - /// - public async Task ChangeDocumentAsync(IEnumerable documents, IndexActionType indexActionType) where T : class - { - Argument.EnsureNotEmpty(nameof(documents), documents); - - return await Connection.ChangeDocumentsInIndexAsync(documents.ToSortedDictionary( - q => q, - q => indexActionType - )); - } - - /// - public async Task ChangeDocumentAsync(SortedDictionary documents) where T : class - { - Argument.EnsureNotEmpty(nameof(documents), documents); - - return await Connection.ChangeDocumentsInIndexAsync(documents); - } - - /// - public async Task AddAsync(T document) where T: class - { - return await ChangeDocumentAsync(document, IndexActionType.Upload); - } - - /// - public async Task AddAsync(IEnumerable documents) where T : class - { - return await ChangeDocumentAsync(documents, IndexActionType.Upload); - } - - /// - public async Task AddOrUpdateAsync(T document) where T : class - { - return await ChangeDocumentAsync(document, IndexActionType.MergeOrUpload); - } - - /// - public async Task AddOrUpdateAsync(IEnumerable documents) where T : class - { - return await ChangeDocumentAsync(documents, IndexActionType.MergeOrUpload); - } - - /// - public async Task RemoveAsync(T document) where T : class - { - return await ChangeDocumentAsync(document, IndexActionType.Delete); - } - - /// - public async Task RemoveAsync(IEnumerable documents) where T : class - { - return await ChangeDocumentAsync(documents, IndexActionType.Delete); - } - - /// - public async Task UpdateAsync(T document) where T : class - { - return await ChangeDocumentAsync(document, IndexActionType.Upload); - } - - /// - public async Task UpdateAsync(IEnumerable documents) where T : class - { - return await ChangeDocumentAsync(documents, IndexActionType.Upload); - } - - /// - public IQueryable Query() where T : class - { - return new AzureSearchQuery(new AzureSearchQueryProvider(Connection, Mapping, typeof(T), Logger)); - } - } -} diff --git a/AzureSearchToolkit/AzureSearchQuery.cs b/AzureSearchToolkit/AzureSearchQuery.cs index 1e8e27f..addc34e 100644 --- a/AzureSearchToolkit/AzureSearchQuery.cs +++ b/AzureSearchToolkit/AzureSearchQuery.cs @@ -1,4 +1,5 @@ -using AzureSearchToolkit.Utilities; +using Azure.Search.Documents; +using AzureSearchToolkit.Utilities; using System; using System.Collections; using System.Collections.Generic; @@ -8,20 +9,20 @@ namespace AzureSearchToolkit { - public class AzureSearchQuery : IAzureSearchQuery + public class AzureSearchQuery : IAzureSearchQuery { - readonly AzureSearchQueryProvider provider; + readonly AzureSearchQueryProvider provider; /// /// Initializes a new instance of the class. /// /// The used to execute the queries. - public AzureSearchQuery(AzureSearchQueryProvider provider) + public AzureSearchQuery(AzureSearchQueryProvider provider) { Argument.EnsureNotNull(nameof(provider), provider); this.provider = provider; - Expression = Expression.Constant(this); + Expression = Expression.Constant(this) as Expression; } /// @@ -29,24 +30,24 @@ public AzureSearchQuery(AzureSearchQueryProvider provider) /// /// The used to execute the queries. /// The that represents the LINQ query so far. - public AzureSearchQuery(AzureSearchQueryProvider provider, Expression expression) + public AzureSearchQuery(AzureSearchQueryProvider provider, Expression expression) { Argument.EnsureNotNull(nameof(provider), provider); Argument.EnsureNotNull(nameof(expression), expression); - if (!typeof(IQueryable).IsAssignableFrom(expression.Type)) + if (!typeof(IQueryable).IsAssignableFrom(expression.Type)) { throw new ArgumentOutOfRangeException(nameof(expression)); } - + this.provider = provider; Expression = expression; } /// - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { - return ((IEnumerable)provider.Execute(Expression)).GetEnumerator(); + return ((IEnumerable)provider.Execute(Expression)).GetEnumerator(); } /// @@ -56,7 +57,7 @@ IEnumerator IEnumerable.GetEnumerator() } /// - public Type ElementType => typeof(T); + public Type ElementType => typeof(TTarget); /// public Expression Expression { get; } diff --git a/AzureSearchToolkit/AzureSearchQueryExtensions.cs b/AzureSearchToolkit/AzureSearchQueryExtensions.cs index f9a7559..74a9d7e 100644 --- a/AzureSearchToolkit/AzureSearchQueryExtensions.cs +++ b/AzureSearchToolkit/AzureSearchQueryExtensions.cs @@ -1,6 +1,6 @@ -using AzureSearchToolkit.Request; +using Azure.Search.Documents.Models; +using AzureSearchToolkit.Request; using AzureSearchToolkit.Utilities; -using Microsoft.Azure.Search.Models; using System; using System.Collections.Generic; using System.Linq; diff --git a/AzureSearchToolkit/AzureSearchQueryProvider.cs b/AzureSearchToolkit/AzureSearchQueryProvider.cs index b8f4759..5858edb 100644 --- a/AzureSearchToolkit/AzureSearchQueryProvider.cs +++ b/AzureSearchToolkit/AzureSearchQueryProvider.cs @@ -1,6 +1,5 @@ -using AzureSearchToolkit.Async; -using AzureSearchToolkit.Logging; -using AzureSearchToolkit.Mapping; +using Azure.Core.Serialization; +using Azure.Search.Documents; using AzureSearchToolkit.Request.Formatters; using AzureSearchToolkit.Request.Visitors; using AzureSearchToolkit.Utilities; @@ -11,51 +10,43 @@ using System.Reflection; using System.Runtime.ExceptionServices; using System.Text; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; namespace AzureSearchToolkit { - public sealed class AzureSearchQueryProvider : IQueryProvider, IAsyncQueryExecutor + public sealed class AzureSearchQueryProvider : IQueryProvider { - internal IAzureSearchConnection Connection { get; private set; } - - internal ILogger Logger { get; } - - internal IAzureSearchMapping Mapping { get;} - - internal Type SearchType { get; } + internal SearchClient SearchClient { get; private set; } + private readonly JsonSerializerOptions jsonOptions; /// /// Create a new AzureSearchQueryProvider for a given connection, logger and field prefix. /// - /// Connection to use to connect to Elasticsearch. - /// A mapping to specify how queries and results are translated. - /// A log to receive any information or debugging messages. - /// A policy to describe how to handle network issues. - public AzureSearchQueryProvider(IAzureSearchConnection connection, IAzureSearchMapping mapping, Type searchType, ILogger logger) + /// Connection to use to connect to Elasticsearch. + public AzureSearchQueryProvider(SearchClient searchClient, JsonSerializerOptions jsonOptions) { - Argument.EnsureNotNull(nameof(connection), connection); - Argument.EnsureNotNull(nameof(mapping), mapping); - Argument.EnsureNotNull(nameof(logger), logger); + Argument.EnsureNotNull(nameof(searchClient), searchClient); - Connection = connection; - Mapping = mapping; - SearchType = searchType; - Logger = logger; + SearchClient = searchClient; + this.jsonOptions = jsonOptions; } /// - public IQueryable CreateQuery(Expression expression) + IQueryable IQueryProvider.CreateQuery(Expression expression) { + if (!typeof(TResult).IsAssignableFrom(typeof(T))) + throw new NotSupportedException(); + Argument.EnsureNotNull(nameof(expression), expression); - if (!typeof(IQueryable).IsAssignableFrom(expression.Type)) + if (!typeof(IQueryable).IsAssignableFrom(expression.Type)) { throw new ArgumentOutOfRangeException(nameof(expression)); - } + } - return new AzureSearchQuery(this, expression); + return new AzureSearchQuery(this, expression); } /// @@ -64,7 +55,7 @@ public IQueryable CreateQuery(Expression expression) Argument.EnsureNotNull(nameof(expression), expression); var elementType = TypeHelper.GetSequenceElementType(expression.Type); - var queryType = typeof(AzureSearchQuery<>).MakeGenericType(elementType); + var queryType = typeof(AzureSearchQuery<,>).MakeGenericType(typeof(T), elementType); try { @@ -86,38 +77,13 @@ public TResult Execute(Expression expression) /// public object Execute(Expression expression) - { - return AsyncHelper.RunSync(() => ExecuteAsync(expression)); - } - - /// - public async Task ExecuteAsync(Expression expression, CancellationToken cancellationToken = default(CancellationToken)) where TResult: class - { - return (TResult)await ExecuteAsync(expression, cancellationToken); - } - - /// - public async Task ExecuteAsync(Expression expression, CancellationToken cancellationToken = default(CancellationToken)) { Argument.EnsureNotNull(nameof(expression), expression); - - var translation = AzureSearchQueryTranslator.Translate(Mapping, expression); - - try - { - var formatter = new SearchRequestFormatter(Mapping, translation.SearchRequest); - - var response = await Connection.SearchAsync(formatter.SearchRequest.SearchParameters, - SearchType, translation.SearchRequest.SearchText, Logger); - - return translation.Materializer.Materialize(response); - } - catch (Exception e) - { - ExceptionDispatchInfo.Capture(e).Throw(); - - return null; - } + var translation = AzureSearchQueryTranslator.Translate(expression, jsonOptions); + var formatter = new SearchRequestFormatter(translation.SearchRequest); + var searchRequest = formatter.SearchRequest; + var response = SearchClient.Search(searchRequest.SearchText, searchRequest.SearchOptions); + return translation.Materializer.Materialize(response); } } } diff --git a/AzureSearchToolkit/AzureSearchToolkit.csproj b/AzureSearchToolkit/AzureSearchToolkit.csproj index e5899b9..bc74f86 100644 --- a/AzureSearchToolkit/AzureSearchToolkit.csproj +++ b/AzureSearchToolkit/AzureSearchToolkit.csproj @@ -1,7 +1,7 @@  - netstandard2.0 + netstandard2.1 AzureSearchToolkit 0.5.0 Niko Kovačič @@ -17,7 +17,10 @@ It features a rich LINQ provider which allows to write strongly typed queries ba - + + + + diff --git a/AzureSearchToolkit/IAzureSearchConnection.cs b/AzureSearchToolkit/IAzureSearchConnection.cs deleted file mode 100644 index 1ffde22..0000000 --- a/AzureSearchToolkit/IAzureSearchConnection.cs +++ /dev/null @@ -1,45 +0,0 @@ -using AzureSearchToolkit.Logging; -using AzureSearchToolkit.Request; -using Microsoft.Azure.Search.Models; -using Microsoft.Rest.Azure; -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; - -namespace AzureSearchToolkit -{ - public interface IAzureSearchConnection - { - /// - /// How long to wait for a response to a network request before - /// giving up. - /// - TimeSpan Timeout { get; } - - /// - /// Create index if it does not exists - /// - /// Optionally override default index - /// If the index was created, true is returned, otherwise false - Task EnsureSearchIndexAsync(ILogger logger = null) where T : class; - - /// - /// Change documents in index - /// - /// - /// Dictionary with documents to change. Included are AzureSearchIndexType operation for each document. - /// If documents were succesfully changed, true is returned, otherwise false - Task ChangeDocumentsInIndexAsync(SortedDictionary changedDocuments, ILogger logger = null) where T : class; - - /// - /// Issues search requests to AzureSearch. - /// - /// Search parameters for the request. - /// The search text applied to the request. - /// The logging mechanism for diagnostic information. - /// An AzureOperationResponse object containing the desired search results. - Task>> SearchAsync(SearchParameters searchParameters, Type searchType, - string searchText = null, ILogger logger = null); - } -} diff --git a/AzureSearchToolkit/IAzureSearchContext.cs b/AzureSearchToolkit/IAzureSearchContext.cs deleted file mode 100644 index 1984588..0000000 --- a/AzureSearchToolkit/IAzureSearchContext.cs +++ /dev/null @@ -1,86 +0,0 @@ -using Microsoft.Azure.Search.Models; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace AzureSearchToolkit -{ - /// - /// Represents a unit of work in AzureSearchToolkit. - /// - public interface IAzureSearchContext - { - /// - /// Add document to index - /// - /// - /// Document to add to index - /// If document is successfully added, true is returned, otherwise false - Task AddAsync(T document) where T: class; - - /// - /// Add documents to index - /// - /// - /// Documents to add to index - /// If documents is successfully added, true is returned, otherwise false - Task AddAsync(IEnumerable documents) where T : class; - - Task AddOrUpdateAsync(T document) where T : class; - Task AddOrUpdateAsync(IEnumerable documents) where T : class; - - /// - /// Changes document state in index with regards to - /// - /// - /// Document to change - /// Type of Azure Search action - /// If operation is successfull, true is returned, otherwise false - Task ChangeDocumentAsync(T document, IndexActionType indexActionType) where T : class; - - /// - /// Changes documents state in index with regards to - /// - /// - /// Documents to change - /// Type of Azure Search action - /// If operation is successfull, true is returned, otherwise false - Task ChangeDocumentAsync(IEnumerable documents, IndexActionType indexActionType) where T : class; - - /// - /// Changes documents state in index with regards to their - /// - /// - /// Documents to change their state. - /// If operation is successfull, true is returned, otherwise false - Task ChangeDocumentAsync(SortedDictionary documents) where T : class; - - /// - /// Remove document to index - /// - /// - /// Document to remove from index - /// If document is successfully removed, true is returned, otherwise false - Task RemoveAsync(T document) where T : class; - - /// - /// Remove documents from index - /// - /// - /// Documents to remove from index - /// If documents were successfully removed, true is returned, otherwise false - Task RemoveAsync(IEnumerable documents) where T : class; - - Task UpdateAsync(T document) where T : class; - Task UpdateAsync(IEnumerable documents) where T : class; - - /// - /// Gets a query that can search for documents of type . - /// - /// The document type. - /// The query that can search for documents of the given type. - IQueryable Query() where T : class; - } -} diff --git a/AzureSearchToolkit/IAzureSearchQuery.cs b/AzureSearchToolkit/IAzureSearchQuery.cs index f7c6845..6f7d8e3 100644 --- a/AzureSearchToolkit/IAzureSearchQuery.cs +++ b/AzureSearchToolkit/IAzureSearchQuery.cs @@ -5,7 +5,7 @@ namespace AzureSearchToolkit { - public interface IAzureSearchQuery : IOrderedQueryable + public interface IAzureSearchQuery : IOrderedQueryable { } } diff --git a/AzureSearchToolkit/Json/GeographyPointJsonConverter.cs b/AzureSearchToolkit/Json/GeographyPointJsonConverter.cs index 508f5da..eb7dfae 100644 --- a/AzureSearchToolkit/Json/GeographyPointJsonConverter.cs +++ b/AzureSearchToolkit/Json/GeographyPointJsonConverter.cs @@ -1,49 +1,47 @@ using Microsoft.Spatial; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; + using System; using System.Collections.Generic; -using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; namespace AzureSearchToolkit.Json { - public class GeographyPointJsonConverter : JsonConverter + public class GeographyPointJsonConverter : JsonConverter { public override bool CanConvert(Type objectType) { return typeof(GeographyPoint).IsAssignableFrom(objectType); } - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + public override GeographyPoint Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - if (reader.TokenType != JsonToken.Null) + if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException(); + var properties = new Dictionary(2); + reader.Read(); + while (reader.TokenType != JsonTokenType.EndObject) { - var jo = JObject.Load(reader); - - var latitudeProperty = jo.GetValue("latitude"); - var longitudeProperty = jo.GetValue("longitude"); - - if (latitudeProperty != null && longitudeProperty != null) + var propertyName = reader.GetString(); + reader.Read(); + switch (propertyName) { - return GeographyPoint.Create(latitudeProperty.ToObject(), longitudeProperty.ToObject()); + case "latitude": + properties.Add("latitude", reader.GetDouble()); + break; + case "longitude": + properties.Add("longitude", reader.GetDouble()); + break; } } - - return null; + return GeographyPoint.Create(properties["latitude"], properties["longitude"]); } - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + public override void Write(Utf8JsonWriter writer, GeographyPoint value, JsonSerializerOptions options) { - if (value is GeographyPoint geographyPoint) - { - var geographyPointJson = new JObject - { - { "latitude", geographyPoint.Latitude }, - { "longitude", geographyPoint.Longitude } - }; - - geographyPointJson.WriteTo(writer); - } + writer.WriteStartObject(); + writer.WriteNumber("latitude", value.Latitude); + writer.WriteNumber("longitude", value.Longitude); + writer.WriteEndObject(); } } } diff --git a/AzureSearchToolkit/Logging/ILogger.cs b/AzureSearchToolkit/Logging/ILogger.cs deleted file mode 100644 index 057e9e5..0000000 --- a/AzureSearchToolkit/Logging/ILogger.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace AzureSearchToolkit.Logging -{ - /// - /// An interface which is used for logging various events in ElasticLINQ. - /// - public interface ILogger - { - /// - /// Logs a message to the logging system. - /// - /// The event type of the message. - /// The exception (optional). - /// Additional information to be logged (optional). - /// The message (will be formatted, if is not null/empty; otherwise, - /// should be sent directly to the logging system). - /// The arguments for (optional). - void Log(TraceEventType type, Exception ex, IDictionary additionalInfo, string messageFormat, params object[] args); - } -} diff --git a/AzureSearchToolkit/Logging/NullLogger.cs b/AzureSearchToolkit/Logging/NullLogger.cs deleted file mode 100644 index 08df239..0000000 --- a/AzureSearchToolkit/Logging/NullLogger.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace AzureSearchToolkit.Logging -{ - public sealed class NullLogger : ILogger - { - /// - /// Gets the singleton instance. - /// - public static readonly NullLogger Instance = new NullLogger(); - - public void Log(TraceEventType type, Exception ex, IDictionary additionalInfo, string messageFormat, params object[] args) - { - return; - } - } -} diff --git a/AzureSearchToolkit/Logging/TraceEventType.cs b/AzureSearchToolkit/Logging/TraceEventType.cs deleted file mode 100644 index 606be41..0000000 --- a/AzureSearchToolkit/Logging/TraceEventType.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace AzureSearchToolkit.Logging -{ - public enum TraceEventType - { - /// - /// Fatal error or application crash. - /// - Critical = 1, - - /// - /// Recoverable error. - /// - Error = 2, - - /// - /// Noncritical problem. - /// - Warning = 4, - - /// - /// Informational message. - /// - Information = 8, - - /// - /// Debugging trace. - /// - Verbose = 16, - } -} diff --git a/AzureSearchToolkit/Mapping/AzureSearchMapping.cs b/AzureSearchToolkit/Mapping/AzureSearchMapping.cs deleted file mode 100644 index 2b6d7fd..0000000 --- a/AzureSearchToolkit/Mapping/AzureSearchMapping.cs +++ /dev/null @@ -1,119 +0,0 @@ -using AzureSearchToolkit.Json; -using AzureSearchToolkit.Request.Criteria; -using AzureSearchToolkit.Utilities; -using Microsoft.Azure.Search.Models; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; - -namespace AzureSearchToolkit.Mapping -{ - /// - /// A base class for mapping Elasticsearch values that can camel-case field names - /// (and respects to opt-in the camel-casing), - /// - public class AzureSearchMapping : IAzureSearchMapping - { - private static Dictionary> mappedPropertiesCache = new Dictionary>(); - private static JsonSerializerSettings jsonSettings = new JsonSerializerSettings - { - Converters = new List { new GeographyPointJsonConverter() }, - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - Formatting = Formatting.None - }; - private static JsonSerializer jsonSerializer = JsonSerializer.Create(jsonSettings); - - public JToken FormatValue(MemberInfo member, object value) - { - throw new NotImplementedException(); - } - - /// - public string GetFieldName(Type type, MemberExpression memberExpression) - { - Argument.EnsureNotNull(nameof(memberExpression), memberExpression); - - switch (memberExpression.Expression.NodeType) - { - case ExpressionType.MemberAccess: - case ExpressionType.Parameter: - return GetFieldName(type, memberExpression.Member); - - default: - throw new NotSupportedException($"Unknown expression type {memberExpression.Expression.NodeType} for left hand side of expression {memberExpression}"); - } - } - - /// - /// Get the AzureSearch field name for a given member. - /// - /// The prefix to put in front of this field name, if the field is - /// an ongoing part of the document search. - /// The member whose field name is required. - /// The AzureSearch field name that matches the member. - public virtual string GetFieldName(Type type, MemberInfo memberInfo) - { - Argument.EnsureNotNull(nameof(type), type); - Argument.EnsureNotNull(nameof(memberInfo), memberInfo); - - var propertyName = memberInfo.Name; - - var mappedProperties = GetMappedPropertiesForType(type); - - var mappedProperty = mappedProperties.FirstOrDefault(q => q.Value == propertyName); - - if (!string.IsNullOrWhiteSpace(mappedProperty.Value)) - { - return mappedProperty.Key; - } - else - { - throw new KeyNotFoundException($"Property {propertyName} was not found on {type}."); - } - } - - /// - public ICriteria GetTypeSelectionCriteria(Type type) - { - Argument.EnsureNotNull(nameof(type), type); - - return null; - } - - public object Materialize(Document sourceDocument, Type sourceType) - { - return JObject.FromObject(sourceDocument, jsonSerializer).ToObject(sourceType, jsonSerializer); - } - - private Dictionary GetMappedPropertiesForType(Type sourceType) - { - if (!mappedPropertiesCache.ContainsKey(sourceType)) - { - mappedPropertiesCache.Add(sourceType, new Dictionary()); - - var camelCasePropertyAttribute = sourceType.GetCustomAttribute(inherit: true); - - foreach (var property in sourceType.GetProperties()) - { - if (camelCasePropertyAttribute != null) - { - var camelCasePropertyName = MappingHelper.ToCamelCase(property.Name); - - mappedPropertiesCache[sourceType].Add(camelCasePropertyName, property.Name); - } - else - { - mappedPropertiesCache[sourceType].Add(property.Name, property.Name); - } - } - } - - return mappedPropertiesCache[sourceType]; - } - } -} diff --git a/AzureSearchToolkit/Mapping/IAzureSearchMapping.cs b/AzureSearchToolkit/Mapping/IAzureSearchMapping.cs deleted file mode 100644 index 8dc5104..0000000 --- a/AzureSearchToolkit/Mapping/IAzureSearchMapping.cs +++ /dev/null @@ -1,53 +0,0 @@ -using AzureSearchToolkit.Request.Criteria; -using Microsoft.Azure.Search.Models; -using Newtonsoft.Json.Linq; -using System; -using System.Linq.Expressions; -using System.Reflection; - -namespace AzureSearchToolkit.Mapping -{ - /// - /// Interface to describe how types and properties are mapped into AzureSearch. - /// - public interface IAzureSearchMapping - { - /// - /// Used to format values used in AzureSearch criteria. Extending this allows you to - /// specify rules like lower-casing values for certain types of criteria so that searched - /// values match the rules AzureSearch is using to store/search values. - /// - /// The member that this value is searching. - /// The value to be formatted. - /// Returns the formatted value. - JToken FormatValue(MemberInfo member, object value); - - /// - /// Gets the field name for the given member. Extending this allows you to change the - /// mapping field names in the CLR to field names in AzureSearch. Typically, these rules - /// will need to match the serialization rules you use when storing your documents. - /// - /// The CLR type used in the source query. - /// The member expression whose name is required. - /// Returns the AzureSearch field name that matches the member. - string GetFieldName(Type type, MemberExpression memberExpression); - - /// - /// Gets criteria that can be used to find documents of a particular type. Will be used by - /// AzureSearchLINQ when a query does not have any suitable Where or Query criteria, so that it - /// can unambiguously select documents of the given type. Typically this should return an - /// ExistsCriteria for a field that's known to always have a value. - /// - /// The CLR type that's being searched. - /// The criteria for selecting documents of this type. - ICriteria GetTypeSelectionCriteria(Type type); - - /// - /// Materialize the JObject document object from AzureSearch to a CLR object. - /// - /// Source document. - /// Type of CLR object to materialize to. - /// Freshly materialized CLR object version of the source document. - object Materialize(Document sourceDocument, Type sourceType); - } -} diff --git a/AzureSearchToolkit/Mapping/MappingHelper.cs b/AzureSearchToolkit/Mapping/MappingHelper.cs deleted file mode 100644 index 620056b..0000000 --- a/AzureSearchToolkit/Mapping/MappingHelper.cs +++ /dev/null @@ -1,78 +0,0 @@ -using AzureSearchToolkit.Utilities; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; - -namespace AzureSearchToolkit.Mapping -{ - /// - /// Common techniques for re-mapping names used between the various mappings. - /// - public static class MappingHelper - { - private static readonly char[] Delimeters = { ' ', '-', '_' }; - - public static string ToCamelCase(string source) - { - Argument.EnsureNotNull(nameof(source), source); - - return SymbolsPipe( - source, - '\0', - (s, disableFrontDelimeter) => - { - if (disableFrontDelimeter) - { - return new char[] { char.ToLowerInvariant(s) }; - } - - return new char[] { char.ToUpperInvariant(s) }; - }); - } - - private static string SymbolsPipe(string source, char mainDelimeter, Func newWordSymbolHandler) - { - var builder = new StringBuilder(); - - var nextSymbolStartsNewWord = true; - var disableFrontDelimeter = true; - - for (var i = 0; i < source.Length; i++) - { - var symbol = source[i]; - if (Delimeters.Contains(symbol)) - { - if (symbol == mainDelimeter) - { - builder.Append(symbol); - disableFrontDelimeter = true; - } - - nextSymbolStartsNewWord = true; - } - else if (!char.IsLetterOrDigit(symbol)) - { - builder.Append(symbol); - disableFrontDelimeter = true; - nextSymbolStartsNewWord = true; - } - else - { - if (nextSymbolStartsNewWord || char.IsUpper(symbol)) - { - builder.Append(newWordSymbolHandler(symbol, disableFrontDelimeter)); - disableFrontDelimeter = false; - nextSymbolStartsNewWord = false; - } - else - { - builder.Append(symbol); - } - } - } - - return builder.ToString(); - } - } -} diff --git a/AzureSearchToolkit/Request/AzureSearchQueryTranslator.cs b/AzureSearchToolkit/Request/AzureSearchQueryTranslator.cs index 3292fa3..911ef26 100644 --- a/AzureSearchToolkit/Request/AzureSearchQueryTranslator.cs +++ b/AzureSearchToolkit/Request/AzureSearchQueryTranslator.cs @@ -1,34 +1,28 @@ -using AzureSearchToolkit.Mapping; +using Azure.Search.Documents.Models; using AzureSearchToolkit.Request.Criteria; using AzureSearchToolkit.Request.Expressions; using AzureSearchToolkit.Response.Materializers; using AzureSearchToolkit.Utilities; -using Microsoft.Azure.Search.Models; using System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; +using System.Text.Json; namespace AzureSearchToolkit.Request.Visitors { - class AzureSearchQueryTranslator : CriteriaExpressionVisitor + class AzureSearchQueryTranslator : CriteriaExpressionVisitor { readonly AzureSearchRequest searchRequest = new AzureSearchRequest(); Type finalItemType; - Func itemProjector; - IAzureSearchMaterializer materializer; + Func itemProjector; + IAzureSearchMaterializer materializer; - Func DefaultItemProjector - { - get { return document => Mapping.Materialize(document, SourceType); } - } + Func DefaultItemProjector => document => document; - AzureSearchQueryTranslator(IAzureSearchMapping mapping, Type sourceType) - : base(mapping, sourceType) - { - } + AzureSearchQueryTranslator(JsonSerializerOptions jsonOptions) : base(jsonOptions) { } static Type FindSourceType(Expression e) { @@ -42,30 +36,19 @@ static Type FindSourceType(Expression e) return sourceQuery.ElementType; } - internal static AzureSearchTranslateResult Translate(IAzureSearchMapping mapping, Expression e) + internal static AzureSearchTranslateResult Translate(Expression e, JsonSerializerOptions jsonOptions) { - return new AzureSearchQueryTranslator(mapping, FindSourceType(e)).Translate(e); + return new AzureSearchQueryTranslator(jsonOptions).Translate(e); } - AzureSearchTranslateResult Translate(Expression e) + AzureSearchTranslateResult Translate(Expression e) { var evaluated = PartialEvaluator.Evaluate(e); CompleteHitTranslation(evaluated); CheckCriteriaForIllegalState(); - ApplyTypeSelectionCriteria(); - - return new AzureSearchTranslateResult(searchRequest, materializer); - } - - void ApplyTypeSelectionCriteria() - { - var typeCriteria = Mapping.GetTypeSelectionCriteria(SourceType); - - searchRequest.Criteria = searchRequest.Criteria == null || searchRequest.Criteria == ConstantCriteria.True - ? typeCriteria - : AndCriteria.Combine(typeCriteria, searchRequest.Criteria); + return new AzureSearchTranslateResult(searchRequest, materializer); } void CompleteHitTranslation(Expression evaluated) @@ -74,11 +57,11 @@ void CompleteHitTranslation(Expression evaluated) if (materializer == null) { - materializer = new ListAzureSearchMaterializer(itemProjector ?? DefaultItemProjector, finalItemType ?? SourceType); + materializer = new ListAzureSearchMaterializer(itemProjector ?? DefaultItemProjector, finalItemType ?? typeof(T)); } - else if (materializer is ChainMaterializer && ((ChainMaterializer)materializer).Next == null) + else if (materializer is ChainMaterializer chainMaterializer && chainMaterializer.Next == null) { - ((ChainMaterializer)materializer).Next = new ListAzureSearchMaterializer(itemProjector ?? DefaultItemProjector, finalItemType ?? SourceType); + chainMaterializer.Next = new ListAzureSearchMaterializer(itemProjector ?? DefaultItemProjector, finalItemType ?? typeof(T)); } } @@ -129,14 +112,14 @@ internal Expression VisitAzureSearchExtensionsMethodCall(MethodCallExpression m) case "LuceneQuery": if (m.Arguments.Count > 1) { - return VisitSearchQuery(QueryType.Full, m.Arguments[0], m.Arguments[1], m.Arguments[2], m.Arguments[3]); + return VisitSearchQuery(SearchQueryType.Full, m.Arguments[0], m.Arguments[1], m.Arguments[2], m.Arguments[3]); } throw GetOverloadUnsupportedException(m.Method); case "SimpleQuery": if (m.Arguments.Count > 1) { - return VisitSearchQuery(QueryType.Simple, m.Arguments[0], m.Arguments[1], m.Arguments[2], m.Arguments[3]); + return VisitSearchQuery(SearchQueryType.Simple, m.Arguments[0], m.Arguments[1], m.Arguments[2], m.Arguments[3]); } throw GetOverloadUnsupportedException(m.Method); @@ -219,12 +202,12 @@ internal Expression VisitQueryableMethodCall(MethodCallExpression m) case "LongCount": if (m.Arguments.Count == 1) { - return VisitCount(m.Arguments[0], null, m.Method.ReturnType); + return VisitCount(m.Arguments[0] as Expression, null, m.Method.ReturnType); } if (m.Arguments.Count == 2) { - return VisitCount(m.Arguments[0], m.Arguments[1], m.Method.ReturnType); + return VisitCount(m.Arguments[0] as Expression, m.Arguments[1], m.Method.ReturnType); } throw GetOverloadUnsupportedException(m.Method); @@ -232,12 +215,12 @@ internal Expression VisitQueryableMethodCall(MethodCallExpression m) case "Any": if (m.Arguments.Count == 1) { - return VisitAny(m.Arguments[0], null); + return VisitAny(m.Arguments[0] as Expression, null); } if (m.Arguments.Count == 2) { - return VisitAny(m.Arguments[0], m.Arguments[1]); + return VisitAny(m.Arguments[0] as Expression, m.Arguments[1]); } throw GetOverloadUnsupportedException(m.Method); @@ -254,8 +237,9 @@ static NotSupportedException GetOverloadUnsupportedException(MethodInfo methodIn Expression VisitAny(Expression source, Expression predicate) { - materializer = new AnyAzureSearchMaterializer(); - searchRequest.SearchParameters.Top = 1; + materializer = new AnyAzureSearchMaterializer(); + searchRequest.SearchOptions.IncludeTotalCount = true; + searchRequest.SearchOptions.Size = 0; return predicate != null ? VisitWhere(source, predicate) @@ -265,9 +249,9 @@ Expression VisitAny(Expression source, Expression predicate) Expression VisitCount(Expression source, Expression predicate, Type returnType) { - materializer = new CountAzureSearchMaterializer(returnType); - searchRequest.SearchParameters.IncludeTotalResultCount = true; - searchRequest.SearchParameters.Top = 0; + materializer = new CountAzureSearchMaterializer(returnType); + searchRequest.SearchOptions.IncludeTotalCount = true; + searchRequest.SearchOptions.Size = 0; return predicate != null ? VisitWhere(source, predicate) @@ -279,9 +263,9 @@ Expression VisitFirstOrSingle(Expression source, Expression predicate, string me var single = methodName.StartsWith("Single", StringComparison.Ordinal); var orDefault = methodName.EndsWith("OrDefault", StringComparison.Ordinal); - searchRequest.SearchParameters.Top = single ? 2 : 1; + searchRequest.SearchOptions.Size = single ? 2 : 1; finalItemType = source.Type; - materializer = new OneAzureSearchMaterializer(itemProjector ?? DefaultItemProjector, finalItemType, single, orDefault); + materializer = new OneAzureSearchMaterializer(single, orDefault); return predicate != null ? VisitWhere(source, predicate) @@ -334,7 +318,7 @@ Expression VisitOrderBy(Expression source, Expression orderByExpression, bool as if (memberExpr != null) { - var fieldName = Mapping.GetFieldName(SourceType, memberExpr); + var fieldName = GetFieldName(typeof(T), memberExpr); if (!string.IsNullOrWhiteSpace(fieldName)) { @@ -359,7 +343,7 @@ Expression VisitOrderBy(Expression source, Expression orderByExpression, bool as { orderBy += " desc"; } - + searchRequest.AddOrderByField(orderBy); } @@ -410,9 +394,9 @@ Expression VisitSelect(Expression source, Expression selectExpression) return Visit(source); } - Expression VisitSearchQuery(QueryType searchType, Expression source, Expression searchTextExpression, Expression searchModeExpression, Expression searchFieldsExpression) + Expression VisitSearchQuery(SearchQueryType searchType, Expression source, Expression searchTextExpression, Expression searchModeExpression, Expression searchFieldsExpression) { - var searchText = ((ConstantExpression)searchTextExpression).Value as string; + var searchText = ((ConstantExpression)searchTextExpression).Value as string; Argument.EnsureNotBlank(nameof(searchTextExpression), searchText); Argument.EnsureNotNull(nameof(searchModeExpression), ((ConstantExpression)searchModeExpression).Value); @@ -423,10 +407,10 @@ Expression VisitSearchQuery(QueryType searchType, Expression source, Expression { throw new NotSupportedException("Only one SimpleQuery or LuceneQuery per LINQ expression is supported!"); } - + searchRequest.SearchText = searchText; - searchRequest.SearchParameters.SearchMode = searchMode; - searchRequest.SearchParameters.QueryType = searchType; + searchRequest.SearchOptions.SearchMode = searchMode; + searchRequest.SearchOptions.QueryType = searchType; if (searchFieldsExpression is NewArrayExpression) { @@ -434,9 +418,16 @@ Expression VisitSearchQuery(QueryType searchType, Expression source, Expression if (searchFieldArrayExpressions?.Any() == true) { - searchRequest.SearchParameters.SearchFields = searchFieldArrayExpressions - .Select(q => Mapping.GetFieldName(TypeHelper.GetSequenceElementType(source.Type), q.GetLambda().Body as MemberExpression)) - .ToList(); + var fields = searchFieldArrayExpressions + .AsParallel() + .Select(q => GetFieldName( + TypeHelper.GetSequenceElementType(source.Type), + q.GetLambda().Body as MemberExpression)); + var searchFields = searchRequest.SearchOptions.SearchFields; + foreach (var field in fields) + { + searchFields.Add(field); + } } } @@ -465,7 +456,7 @@ void RebindSelectBody(Expression selectExpression, IEnumerable argum /// Parameter that references the whole entity. void RebindElasticFieldsAndChainProjector(Expression selectExpression, ParameterExpression entityParameter) { - var projection = AzureSearchFieldsExpressionVisitor.Rebind(SourceType, Mapping, selectExpression); + var projection = AzureSearchFieldsExpressionVisitor.Rebind(typeof(T), selectExpression); var compiled = Expression.Lambda(projection.Item1, entityParameter, projection.Item2).Compile(); itemProjector = h => compiled.DynamicInvoke(DefaultItemProjector(h), h); @@ -478,7 +469,7 @@ void RebindElasticFieldsAndChainProjector(Expression selectExpression, Parameter /// Select expression to re-bind. void RebindPropertiesAndElasticFields(Expression selectExpression) { - var projection = MemberProjectionExpressionVisitor.Rebind(SourceType, Mapping, selectExpression); + var projection = MemberProjectionExpressionVisitor.Rebind(typeof(T), selectExpression); var compiled = Expression.Lambda(projection.Expression, projection.Parameter).Compile(); itemProjector = h => compiled.DynamicInvoke(h); @@ -492,7 +483,7 @@ Expression VisitSkip(Expression source, Expression skipExpression) if (skipConstant != null) { - searchRequest.SearchParameters.Skip = (int)skipConstant.Value; + searchRequest.SearchOptions.Skip = (int)skipConstant.Value; } return Visit(source); @@ -506,8 +497,8 @@ Expression VisitTake(Expression source, Expression takeExpression) { var takeValue = (int)takeConstant.Value; - searchRequest.SearchParameters.Top = searchRequest.SearchParameters.Top.HasValue - ? Math.Min(searchRequest.SearchParameters.Top.GetValueOrDefault(), takeValue) + searchRequest.SearchOptions.Size = searchRequest.SearchOptions.Size.HasValue + ? Math.Min(searchRequest.SearchOptions.Size.GetValueOrDefault(), takeValue) : takeValue; } diff --git a/AzureSearchToolkit/Request/AzureSearchRequest.cs b/AzureSearchToolkit/Request/AzureSearchRequest.cs index addb8a6..b55e51e 100644 --- a/AzureSearchToolkit/Request/AzureSearchRequest.cs +++ b/AzureSearchToolkit/Request/AzureSearchRequest.cs @@ -1,5 +1,6 @@ -using AzureSearchToolkit.Request.Criteria; -using Microsoft.Azure.Search.Models; +using Azure.Search.Documents; +using Azure.Search.Documents.Models; +using AzureSearchToolkit.Request.Criteria; using System; using System.Collections.Generic; using System.Linq; @@ -11,15 +12,15 @@ class AzureSearchRequest { public string SearchText { get; set; } public ICriteria Criteria { get; set; } - public SearchParameters SearchParameters { get; } + public SearchOptions SearchOptions { get; } public AzureSearchRequest() { - SearchParameters = new SearchParameters + SearchOptions = new SearchOptions { - QueryType = QueryType.Simple, + QueryType = SearchQueryType.Simple, SearchMode = SearchMode.All, - Top = 200 + Size = 200 }; SearchText = "*"; } @@ -31,50 +32,35 @@ public void AddOrderByField(string orderByField, bool addToStart = true) public void AddOrderByFields(string[] orderByFields, bool addToStart = true) { - if (SearchParameters.OrderBy == null) - { - SearchParameters.OrderBy = new List(); - } - if (addToStart) { foreach (var field in orderByFields.Reverse()) { - SearchParameters.OrderBy.Insert(0, field); + SearchOptions.OrderBy.Insert(0, field); } } else { foreach (var field in orderByFields) { - SearchParameters.OrderBy.Add(field); + SearchOptions.OrderBy.Add(field); } } } public void AddRangeToSelect(params string[] fields) { - if (SearchParameters.Select == null) - { - SearchParameters.Select = new List(); - } - foreach (var field in fields) { - SearchParameters.Select.Add(field); + SearchOptions.Select.Add(field); } } public void AddRangeToSearchFields(params string[] searchFields) { - if (SearchParameters.Select == null) - { - SearchParameters.SearchFields = new List(); - } - foreach (var field in searchFields) { - SearchParameters.SearchFields.Add(field); + SearchOptions.SearchFields.Add(field); } } } diff --git a/AzureSearchToolkit/Request/Formatters/SearchRequestFormatter.cs b/AzureSearchToolkit/Request/Formatters/SearchRequestFormatter.cs index 61c1e7a..d45c3e8 100644 --- a/AzureSearchToolkit/Request/Formatters/SearchRequestFormatter.cs +++ b/AzureSearchToolkit/Request/Formatters/SearchRequestFormatter.cs @@ -1,6 +1,5 @@ -using AzureSearchToolkit.Mapping; +using Azure.Search.Documents.Models; using AzureSearchToolkit.Request.Criteria; -using Microsoft.Azure.Search.Models; using System; using System.Collections.Generic; using System.Linq; @@ -14,17 +13,13 @@ namespace AzureSearchToolkit.Request.Formatters class SearchRequestFormatter { - readonly IAzureSearchMapping mapping; - /// /// The formatted SearchRequest sent to AzureSearch. /// public AzureSearchRequest SearchRequest { get; } - public SearchRequestFormatter(IAzureSearchMapping mapping, AzureSearchRequest searchRequest) + public SearchRequestFormatter(AzureSearchRequest searchRequest) { - this.mapping = mapping; - SearchRequest = searchRequest; Build(searchRequest.Criteria); @@ -57,7 +52,7 @@ private void Build(ICriteria criteria) return; } - + /* if (criteria is RegexpCriteria) { @@ -65,14 +60,14 @@ private void Build(ICriteria criteria) return; } - + if (criteria is PrefixCriteria) { Build((PrefixCriteria)criteria); return; }*/ - + if (criteria is TermCriteria) { @@ -80,21 +75,21 @@ private void Build(ICriteria criteria) return; } - + if (criteria is TermsCriteria) { SimpleBuild(criteria); return; } - + if (criteria is QueryStringCriteria) { Build((QueryStringCriteria)criteria); return; } - + // Base class formatters using name property /* if (criteria is SingleFieldCriteria) @@ -103,30 +98,30 @@ private void Build(ICriteria criteria) return; }*/ - + if (criteria is CompoundCriteria) { Build((CompoundCriteria)criteria); return; } - + throw new InvalidOperationException($"Unknown criteria type '{criteria.GetType()}'"); } private void Build(TermCriteria criteria) { - SearchRequest.SearchParameters.Filter = criteria.ToString(); + SearchRequest.SearchOptions.Filter = criteria.ToString(); } private void Build(TermsCriteria criteria) { - SearchRequest.SearchParameters.Filter = criteria.ToString(); + SearchRequest.SearchOptions.Filter = criteria.ToString(); } private void Build(QueryStringCriteria criteria) { - SearchRequest.SearchParameters.QueryType = QueryType.Full; + SearchRequest.SearchOptions.QueryType = SearchQueryType.Full; SearchRequest.SearchText = criteria.Value; if (criteria.Fields?.Any() == true) @@ -137,7 +132,7 @@ private void Build(QueryStringCriteria criteria) private void Build(ComparisonCriteria criteria) { - SearchRequest.SearchParameters.Filter = criteria.ToString(); + SearchRequest.SearchOptions.Filter = criteria.ToString(); } private void Build(CompoundCriteria criteria) @@ -174,7 +169,7 @@ private void Build(CompoundCriteria criteria) private void SimpleBuild(ICriteria criteria) { - SearchRequest.SearchParameters.Filter = criteria.ToString(); + SearchRequest.SearchOptions.Filter = criteria.ToString(); } } } diff --git a/AzureSearchToolkit/Request/Visitors/AzureSearchFieldsExpressionVisitor.cs b/AzureSearchToolkit/Request/Visitors/AzureSearchFieldsExpressionVisitor.cs index f8830fc..fda16a6 100644 --- a/AzureSearchToolkit/Request/Visitors/AzureSearchFieldsExpressionVisitor.cs +++ b/AzureSearchToolkit/Request/Visitors/AzureSearchFieldsExpressionVisitor.cs @@ -1,6 +1,5 @@ -using AzureSearchToolkit.Mapping; +using Azure.Search.Documents.Models; using AzureSearchToolkit.Utilities; -using Microsoft.Azure.Search.Models; using System; using System.Collections.Generic; using System.Linq.Expressions; @@ -15,23 +14,20 @@ namespace AzureSearchToolkit.Request.Visitors class AzureSearchFieldsExpressionVisitor : ExpressionVisitor { protected readonly ParameterExpression BindingParameter; - protected readonly IAzureSearchMapping Mapping; protected readonly Type SourceType; - public AzureSearchFieldsExpressionVisitor(Type sourcetype, ParameterExpression bindingParameter, IAzureSearchMapping mapping) + public AzureSearchFieldsExpressionVisitor(Type sourcetype, ParameterExpression bindingParameter) { Argument.EnsureNotNull(nameof(bindingParameter), bindingParameter); - Argument.EnsureNotNull(nameof(mapping), mapping); SourceType = sourcetype; BindingParameter = bindingParameter; - Mapping = mapping; } - internal static Tuple Rebind(Type sourceType, IAzureSearchMapping mapping, Expression selector) + internal static Tuple Rebind(Type sourceType, Expression selector) { - var parameter = Expression.Parameter(typeof(Document), "h"); - var visitor = new AzureSearchFieldsExpressionVisitor(sourceType, parameter, mapping); + var parameter = Expression.Parameter(typeof(SearchDocument), "h"); + var visitor = new AzureSearchFieldsExpressionVisitor(sourceType, parameter); Argument.EnsureNotNull(nameof(selector), selector); diff --git a/AzureSearchToolkit/Request/Visitors/AzureSearchTranslateResult.cs b/AzureSearchToolkit/Request/Visitors/AzureSearchTranslateResult.cs index 7ed9b11..6b0a760 100644 --- a/AzureSearchToolkit/Request/Visitors/AzureSearchTranslateResult.cs +++ b/AzureSearchToolkit/Request/Visitors/AzureSearchTranslateResult.cs @@ -11,13 +11,13 @@ namespace AzureSearchToolkit.Request.Visitors /// and the local necessary to /// instantiate objects. /// - class AzureSearchTranslateResult + class AzureSearchTranslateResult { public AzureSearchRequest SearchRequest { get; } - public IAzureSearchMaterializer Materializer { get; } + public IAzureSearchMaterializer Materializer { get; } - public AzureSearchTranslateResult(AzureSearchRequest searchRequest, IAzureSearchMaterializer materializer) + public AzureSearchTranslateResult(AzureSearchRequest searchRequest, IAzureSearchMaterializer materializer) { SearchRequest = searchRequest; Materializer = materializer; diff --git a/AzureSearchToolkit/Request/Visitors/CriteriaExpressionVisitor.cs b/AzureSearchToolkit/Request/Visitors/CriteriaExpressionVisitor.cs index ddd28e3..9b5f855 100644 --- a/AzureSearchToolkit/Request/Visitors/CriteriaExpressionVisitor.cs +++ b/AzureSearchToolkit/Request/Visitors/CriteriaExpressionVisitor.cs @@ -1,27 +1,86 @@ -using AzureSearchToolkit.Mapping; -using AzureSearchToolkit.Request; -using AzureSearchToolkit.Request.Criteria; +using AzureSearchToolkit.Request.Criteria; using AzureSearchToolkit.Request.Expressions; using AzureSearchToolkit.Utilities; -using Microsoft.Azure.Search.Models; + using Microsoft.Spatial; + using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; namespace AzureSearchToolkit.Request.Visitors { - internal abstract class CriteriaExpressionVisitor: ExpressionVisitor + internal abstract class CriteriaExpressionVisitor : ExpressionVisitor { - protected readonly IAzureSearchMapping Mapping; - protected readonly Type SourceType; + private static Dictionary> mappedPropertiesCache = new Dictionary>(); - protected CriteriaExpressionVisitor(IAzureSearchMapping mapping, Type sourceType) + private readonly JsonSerializerOptions jsonOptions; + + protected CriteriaExpressionVisitor(JsonSerializerOptions jsonOptions) { - Mapping = mapping; - SourceType = sourceType; + this.jsonOptions = jsonOptions; + } + + private Dictionary GetMappedPropertiesForType(Type sourceType) + { + if (!mappedPropertiesCache.ContainsKey(sourceType)) + { + mappedPropertiesCache.Add(sourceType, new Dictionary()); + var camelCasePropertyAttribute = sourceType.GetCustomAttribute(inherit: true); + foreach (var property in sourceType.GetProperties()) + { + var propertyName = jsonOptions.PropertyNamingPolicy.ConvertName(property.Name); + mappedPropertiesCache[sourceType].Add(property.Name, propertyName); + } + } + return mappedPropertiesCache[sourceType]; + } + + /// + /// Get the AzureSearch field name for a given member. + /// + /// The prefix to put in front of this field name, if the field is + /// an ongoing part of the document search. + /// The member whose field name is required. + /// The AzureSearch field name that matches the member. + public virtual string GetFieldName(Type type, MemberInfo memberInfo) + { + Argument.EnsureNotNull(nameof(type), type); + Argument.EnsureNotNull(nameof(memberInfo), memberInfo); + + var propertyName = memberInfo.Name; + + var mappedProperties = GetMappedPropertiesForType(type); + + var mappedProperty = mappedProperties[propertyName]; + + if (!mappedProperties.TryGetValue(propertyName, out var name)) + { + return name; + } + else + { + throw new KeyNotFoundException($"Property {propertyName} was not found on {type}."); + } + } + + /// + public string GetFieldName(Type type, MemberExpression memberExpression) + { + Argument.EnsureNotNull(nameof(memberExpression), memberExpression); + switch (memberExpression.Expression.NodeType) + { + case ExpressionType.MemberAccess: + case ExpressionType.Parameter: + return GetFieldName(type, memberExpression.Member); + default: + throw new NotSupportedException($"Unknown expression type {memberExpression.Expression.NodeType} for left hand side of expression {memberExpression}"); + } } protected override Expression VisitMethodCall(MethodCallExpression node) @@ -29,19 +88,19 @@ protected override Expression VisitMethodCall(MethodCallExpression node) if (node.Method.DeclaringType == typeof(string)) { return VisitStringMethodCall(node); - } + } if (node.Method.DeclaringType == typeof(Enumerable)) { return VisitEnumerableMethodCall(node); } - + if (node.Method.DeclaringType == typeof(AzureSearchMethods)) { return VisitAzureSearchMethodsMethodCall(node); } - + return VisitDefaultMethodCall(node); } @@ -54,12 +113,12 @@ Expression VisitDefaultMethodCall(MethodCallExpression m) { return VisitEquals(Visit(m.Object), Visit(m.Arguments[0])); } - + if (m.Arguments.Count == 2) { return VisitEquals(Visit(m.Arguments[0]), Visit(m.Arguments[1])); } - + break; case "Contains": @@ -67,7 +126,7 @@ Expression VisitDefaultMethodCall(MethodCallExpression m) { return VisitEnumerableContainsMethodCall(m.Object, m.Arguments[0]); } - + break; } @@ -83,7 +142,7 @@ protected Expression VisitAzureSearchMethodsMethodCall(MethodCallExpression m) { return VisitContains("ContainsAny", m.Arguments[0], m.Arguments[1], TermsOperator.Any); } - + break; case "ContainsAll": @@ -91,7 +150,7 @@ protected Expression VisitAzureSearchMethodsMethodCall(MethodCallExpression m) { return VisitContains("ContainsAll", m.Arguments[0], m.Arguments[1], TermsOperator.All); } - + break; case "Distance": if (m.Arguments.Count == 2) @@ -127,7 +186,7 @@ protected Expression VisitStringMethodCall(MethodCallExpression m) { return VisitStringPatternCheckMethodCall(m.Object, m.Arguments[0], "/.*{0}.*/", m.Method.Name); } - + break; case "StartsWith": // Where(x => x.StringProperty.StartsWith(value)) @@ -135,7 +194,7 @@ protected Expression VisitStringMethodCall(MethodCallExpression m) { return VisitStringPatternCheckMethodCall(m.Object, m.Arguments[0], "{0}*", m.Method.Name); } - + break; case "EndsWith": // Where(x => x.StringProperty.EndsWith(value)) @@ -143,7 +202,7 @@ protected Expression VisitStringMethodCall(MethodCallExpression m) { return VisitStringPatternCheckMethodCall(m.Object, m.Arguments[0], "/.*{0}/", m.Method.Name); } - + break; } @@ -164,7 +223,7 @@ protected override Expression VisitUnary(UnaryExpression node) { return new CriteriaExpression(NotCriteria.Create(subExpression.Criteria)); } - + break; } @@ -186,7 +245,7 @@ protected override Expression VisitMember(MemberExpression node) { memberName = node.Member.DeclaringType.Name + "." + node.Member.Name; } - + throw new NotSupportedException($"{memberName} is of unsupported type {node.Expression.NodeType}"); } } @@ -236,7 +295,7 @@ protected Expression BooleanMemberAccessBecomesEquals(Expression e) { return new CriteriaExpression(ConstantCriteria.True); } - + if (c.Value.Equals(false)) { return new CriteriaExpression(ConstantCriteria.False); @@ -262,7 +321,7 @@ Expression VisitEnumerableContainsMethodCall(Expression source, Expression match if (source is ConstantExpression && matched is MemberExpression) { var memberExpression = (MemberExpression)matched; - var field = Mapping.GetFieldName(SourceType, memberExpression); + var field = GetFieldName(typeof(T), memberExpression); var containsSource = ((IEnumerable)((ConstantExpression)source).Value); // If criteria contains a null create an Or criteria with Terms on one @@ -286,7 +345,7 @@ Expression VisitEnumerableContainsMethodCall(Expression source, Expression match var memberExpression = (MemberExpression)source; var value = ((ConstantExpression)matched).Value; - var field = Mapping.GetFieldName(SourceType, memberExpression); + var field = GetFieldName(typeof(T), memberExpression); return new CriteriaExpression(TermsCriteria.Build(field, memberExpression.Member, value)); } @@ -302,7 +361,7 @@ protected virtual Expression VisitStringPatternCheckMethodCall(Expression source if (source is MemberExpression && matched is ConstantExpression) { - var field = Mapping.GetFieldName(SourceType, (MemberExpression)source); + var field = GetFieldName(typeof(T), (MemberExpression)source); var value = ((ConstantExpression)matched).Value; return new CriteriaExpression(new QueryStringCriteria(string.Format(pattern, value), field)); @@ -325,14 +384,14 @@ Expression VisitOrElse(BinaryExpression b) OrCriteria.Combine(CombineExpressions(b.Left, b.Right).Select(f => f.Criteria).ToArray())); } - IEnumerable CombineExpressions(params Expression[] expressions) where T : Expression + IEnumerable CombineExpressions(params Expression[] expressions) where TExpr : Expression { foreach (var expression in expressions.Select(BooleanMemberAccessBecomesEquals)) { - if ((expression as T) == null) + if ((expression as TExpr) == null) throw new NotSupportedException($"Unexpected binary expression '{expression}'"); - yield return (T)expression; + yield return (TExpr)expression; } } @@ -344,7 +403,7 @@ Expression VisitContains(string methodName, Expression left, Expression right, T { var values = ((IEnumerable)cm.ConstantExpression.Value).Cast().ToArray(); - return new CriteriaExpression(TermsCriteria.Build(executionMode, Mapping.GetFieldName(SourceType, cm.MemberExpression), cm.MemberExpression.Member, values)); + return new CriteriaExpression(TermsCriteria.Build(executionMode, GetFieldName(typeof(T), cm.MemberExpression), cm.MemberExpression.Member, values)); } throw new NotSupportedException(methodName + " must be between a Member and a Constant"); @@ -358,7 +417,7 @@ Expression VisitDistance(Expression left, Expression right) { var value = ((GeographyPoint)cm.ConstantExpression.Value); - return new CriteriaExpression(new DistanceCriteria(Mapping.GetFieldName(SourceType, cm.MemberExpression), cm.MemberExpression.Member, value, null)); + return new CriteriaExpression(new DistanceCriteria(GetFieldName(typeof(T), cm.MemberExpression), cm.MemberExpression.Member, value, null)); } throw new NotSupportedException("Distance must be between a Member and a Constant"); @@ -366,7 +425,7 @@ Expression VisitDistance(Expression left, Expression right) Expression CreateExists(ConstantMemberPair cm, bool positiveTest) { - var fieldName = Mapping.GetFieldName(SourceType, UnwrapNullableMethodExpression(cm.MemberExpression)); + var fieldName = GetFieldName(typeof(T), UnwrapNullableMethodExpression(cm.MemberExpression)); var value = cm.ConstantExpression.Value ?? false; @@ -390,16 +449,16 @@ Expression VisitEquals(Expression left, Expression right) if (booleanEquals != null) { return booleanEquals; - } + } var cm = ConstantMemberPair.Create(left, right); if (cm != null) { - + return cm.IsNullTest ? CreateExists(cm, true) - : new CriteriaExpression(new ComparisonCriteria(Mapping.GetFieldName(SourceType, cm.MemberExpression), + : new CriteriaExpression(new ComparisonCriteria(GetFieldName(typeof(T), cm.MemberExpression), cm.MemberExpression.Member, Comparison.Equal, cm.ConstantExpression.Value)); } @@ -415,12 +474,12 @@ static Expression VisitCriteriaEquals(Expression left, Expression right, bool po { return null; } - + if (constant.Value.Equals(positiveCondition)) { return criteria; } - + if (constant.Value.Equals(!positiveCondition)) { return new CriteriaExpression(NotCriteria.Create(criteria.Criteria)); @@ -454,7 +513,7 @@ Expression VisitNotEqual(Expression left, Expression right) return cm.IsNullTest ? CreateExists(cm, false) - : new CriteriaExpression(new ComparisonCriteria(Mapping.GetFieldName(SourceType, cm.MemberExpression), + : new CriteriaExpression(new ComparisonCriteria(GetFieldName(typeof(T), cm.MemberExpression), cm.MemberExpression.Member, Comparison.NotEqual, cm.ConstantExpression.Value)); } @@ -473,7 +532,7 @@ Expression VisitRange(Comparison rangeComparison, Expression left, Expression ri throw new NotSupportedException("A {0} must test a constant against a member"); } - var field = Mapping.GetFieldName(SourceType, cm.MemberExpression); + var field = GetFieldName(typeof(T), cm.MemberExpression); var comparisonCriteria = new ComparisonCriteria(field, cm.MemberExpression.Member, rangeComparison, cm.ConstantExpression.Value); return new CriteriaExpression(inverted ? comparisonCriteria.Negate() : comparisonCriteria); @@ -481,7 +540,7 @@ Expression VisitRange(Comparison rangeComparison, Expression left, Expression ri else { var distanceCriteria = existingCriteriaExpression.Criteria as DistanceCriteria; - + if (distanceCriteria != null) { var constantExpression = right as ConstantExpression; diff --git a/AzureSearchToolkit/Request/Visitors/MemberProjectionExpressionVisitor.cs b/AzureSearchToolkit/Request/Visitors/MemberProjectionExpressionVisitor.cs index 49170a3..9da7ca3 100644 --- a/AzureSearchToolkit/Request/Visitors/MemberProjectionExpressionVisitor.cs +++ b/AzureSearchToolkit/Request/Visitors/MemberProjectionExpressionVisitor.cs @@ -1,6 +1,5 @@ -using AzureSearchToolkit.Mapping; +using Azure.Search.Documents.Models; using AzureSearchToolkit.Utilities; -using Microsoft.Azure.Search.Models; using System; using System.Collections.Generic; using System.Linq.Expressions; @@ -15,15 +14,15 @@ class MemberProjectionExpressionVisitor : AzureSearchFieldsExpressionVisitor readonly HashSet fieldNames = new HashSet(); - MemberProjectionExpressionVisitor(Type sourceType, ParameterExpression bindingParameter, IAzureSearchMapping mapping) - : base(sourceType, bindingParameter, mapping) + MemberProjectionExpressionVisitor(Type sourceType, ParameterExpression bindingParameter) + : base(sourceType, bindingParameter) { } - internal new static RebindCollectionResult Rebind(Type sourceType, IAzureSearchMapping mapping, Expression selector) + internal new static RebindCollectionResult Rebind(Type sourceType, Expression selector) { - var parameter = Expression.Parameter(typeof(Document), "h"); - var visitor = new MemberProjectionExpressionVisitor(sourceType, parameter, mapping); + var parameter = Expression.Parameter(typeof(SearchDocument), "h"); + var visitor = new MemberProjectionExpressionVisitor(sourceType, parameter); Argument.EnsureNotNull(nameof(selector), selector); diff --git a/AzureSearchToolkit/Request/Visitors/QuerySourceExpressionVisitor.cs b/AzureSearchToolkit/Request/Visitors/QuerySourceExpressionVisitor.cs index 31720d6..987d6ff 100644 --- a/AzureSearchToolkit/Request/Visitors/QuerySourceExpressionVisitor.cs +++ b/AzureSearchToolkit/Request/Visitors/QuerySourceExpressionVisitor.cs @@ -26,7 +26,7 @@ protected override Expression VisitConstant(ConstantExpression node) { sourceQueryable = ((IQueryable)node.Value); } - + return node; } } diff --git a/AzureSearchToolkit/Response/Materializers/AnyAzureSearchMaterializer.cs b/AzureSearchToolkit/Response/Materializers/AnyAzureSearchMaterializer.cs index 836ef7a..124a063 100644 --- a/AzureSearchToolkit/Response/Materializers/AnyAzureSearchMaterializer.cs +++ b/AzureSearchToolkit/Response/Materializers/AnyAzureSearchMaterializer.cs @@ -1,5 +1,5 @@ -using Microsoft.Azure.Search.Models; -using Microsoft.Rest.Azure; +using Azure; +using Azure.Search.Documents.Models; using System; using System.Collections.Generic; using System.Text; @@ -9,21 +9,21 @@ namespace AzureSearchToolkit.Response.Materializers /// /// Materializes true or false depending on whether any results matched the query or not. /// - class AnyAzureSearchMaterializer : IAzureSearchMaterializer + class AnyAzureSearchMaterializer : IAzureSearchMaterializer { /// /// Determine whether at a given query response contains any hits. /// /// The to check for emptiness. /// true if the source sequence contains any elements; otherwise, false. - public object Materialize(AzureOperationResponse> response) + public object Materialize(Response> response) { - if (response.Body.Count < 0) + if (response.Value.TotalCount < 0) { throw new ArgumentOutOfRangeException(nameof(response), "Contains a negative number of hits."); } - return response.Body.Count > 0; + return response.Value.TotalCount > 0; } } } diff --git a/AzureSearchToolkit/Response/Materializers/ChainMaterializer.cs b/AzureSearchToolkit/Response/Materializers/ChainMaterializer.cs index 65cbaf3..db435e3 100644 --- a/AzureSearchToolkit/Response/Materializers/ChainMaterializer.cs +++ b/AzureSearchToolkit/Response/Materializers/ChainMaterializer.cs @@ -1,17 +1,17 @@ -using AzureSearchToolkit.Utilities; -using Microsoft.Azure.Search.Models; -using Microsoft.Rest.Azure; +using Azure; +using Azure.Search.Documents.Models; +using AzureSearchToolkit.Utilities; namespace AzureSearchToolkit.Response.Materializers { - abstract class ChainMaterializer : IAzureSearchMaterializer + abstract class ChainMaterializer : IAzureSearchMaterializer { - protected ChainMaterializer(IAzureSearchMaterializer next) + protected ChainMaterializer(IAzureSearchMaterializer next) { Next = next; } - public IAzureSearchMaterializer Next + public IAzureSearchMaterializer Next { get; set; } @@ -21,7 +21,7 @@ public IAzureSearchMaterializer Next /// /// AzureOperationResponse to obtain the existence of a result. /// Return result of previous materializer, previously processed by self - public virtual object Materialize(AzureOperationResponse> response) + public object Materialize(Response> response) { Argument.EnsureNotNull("Next materializer must be setted.", Next); diff --git a/AzureSearchToolkit/Response/Materializers/CountAzureSearchMaterializer.cs b/AzureSearchToolkit/Response/Materializers/CountAzureSearchMaterializer.cs index f1c7530..4b356f1 100644 --- a/AzureSearchToolkit/Response/Materializers/CountAzureSearchMaterializer.cs +++ b/AzureSearchToolkit/Response/Materializers/CountAzureSearchMaterializer.cs @@ -1,5 +1,5 @@ -using Microsoft.Azure.Search.Models; -using Microsoft.Rest.Azure; +using Azure; +using Azure.Search.Documents.Models; using System; namespace AzureSearchToolkit.Response.Materializers @@ -7,10 +7,9 @@ namespace AzureSearchToolkit.Response.Materializers /// /// Materializes a count operation by obtaining the total document count from the response. /// - class CountAzureSearchMaterializer : IAzureSearchMaterializer + class CountAzureSearchMaterializer : IAzureSearchMaterializer { - readonly Type returnType; - + private readonly Type returnType; public CountAzureSearchMaterializer(Type returnType) { this.returnType = returnType; @@ -21,14 +20,14 @@ public CountAzureSearchMaterializer(Type returnType) /// /// The to obtain the count value from. /// The result count expressed as either an int or long depending on the size of the count. - public object Materialize(AzureOperationResponse> response) + public object Materialize(Response> response) { - if (response.Body.Count < 0) + if (response.Value.TotalCount < 0) { throw new ArgumentOutOfRangeException(nameof(response), "Contains a negative number of documents."); - } + } - return Convert.ChangeType(response.Body.Count, returnType); + return Convert.ChangeType(response.Value.TotalCount, returnType); } } } diff --git a/AzureSearchToolkit/Response/Materializers/IAzureSearchMaterializer.cs b/AzureSearchToolkit/Response/Materializers/IAzureSearchMaterializer.cs index 5f89256..159e568 100644 --- a/AzureSearchToolkit/Response/Materializers/IAzureSearchMaterializer.cs +++ b/AzureSearchToolkit/Response/Materializers/IAzureSearchMaterializer.cs @@ -1,8 +1,9 @@ -using Microsoft.Azure.Search.Models; -using Microsoft.Rest.Azure; +using Azure; +using Azure.Search.Documents.Models; using System; using System.Collections.Generic; using System.Text; +using System.Threading.Tasks; namespace AzureSearchToolkit.Response.Materializers { @@ -10,13 +11,13 @@ namespace AzureSearchToolkit.Response.Materializers /// Interface for all materializers responsible for turning the AzureOperationResponse into desired /// CLR objects. /// - interface IAzureSearchMaterializer + interface IAzureSearchMaterializer { /// /// Materialize the AzureOperationResponse into the desired CLR objects. /// /// The received from AzureSearch. /// List or a single CLR object as requested. - object Materialize(AzureOperationResponse> response); + object Materialize(Response> response); } } diff --git a/AzureSearchToolkit/Response/Materializers/ListAzureSearchMaterializer.cs b/AzureSearchToolkit/Response/Materializers/ListAzureSearchMaterializer.cs index b31a109..8d3b2b9 100644 --- a/AzureSearchToolkit/Response/Materializers/ListAzureSearchMaterializer.cs +++ b/AzureSearchToolkit/Response/Materializers/ListAzureSearchMaterializer.cs @@ -1,22 +1,23 @@ -using AzureSearchToolkit.Utilities; -using Microsoft.Azure.Search.Models; -using Microsoft.Rest.Azure; +using Azure; +using Azure.Search.Documents.Models; +using AzureSearchToolkit.Utilities; using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text; +using System.Threading.Tasks; namespace AzureSearchToolkit.Response.Materializers { /// /// Materializes multiple hits into a list of CLR objects. /// - class ListAzureSearchMaterializer : IAzureSearchMaterializer + class ListAzureSearchMaterializer : IAzureSearchMaterializer { - static readonly MethodInfo manyMethodInfo = typeof(ListAzureSearchMaterializer).GetMethodInfo(f => f.Name == "Many" && f.IsStatic); + static readonly MethodInfo manyMethodInfo = typeof(ListAzureSearchMaterializer).GetMethodInfo(f => f.Name == "Many" && f.IsStatic); - readonly Func projector; + readonly Func projector; readonly Type elementType; /// @@ -24,7 +25,7 @@ class ListAzureSearchMaterializer : IAzureSearchMaterializer /// /// A function to turn a hit into a desired CLR object. /// The type of CLR object being materialized. - public ListAzureSearchMaterializer(Func projector, Type elementType) + public ListAzureSearchMaterializer(Func projector, Type elementType) { this.projector = projector; this.elementType = elementType; @@ -35,25 +36,26 @@ public ListAzureSearchMaterializer(Func projector, Type elemen /// /// The containing the hits to materialize. /// List of objects as constructed by the . - public object Materialize(AzureOperationResponse> response) + public object Materialize(Response> response) { Argument.EnsureNotNull(nameof(response), response); - var responseBody = response.Body; + var responseBody = response.Value; - if (responseBody?.Results == null || !responseBody.Results.Any()) + var results = responseBody.GetResults(); + if (!results.Any()) { return Activator.CreateInstance(typeof(List<>).MakeGenericType(elementType)); } return manyMethodInfo .MakeGenericMethod(elementType) - .Invoke(null, new object[] { responseBody.Results.Select(q => q.Document), projector }); + .Invoke(null, new object[] { results.Select(q => q.Document), projector }); } - internal static IReadOnlyList Many(IEnumerable documents, Func projector) + internal static IReadOnlyList Many(IEnumerable documents, Func projector) { - return documents.Select(projector).Cast().ToReadOnlyBatchedList(); + return documents.Select(projector).Cast().ToReadOnlyBatchedList(); } } } diff --git a/AzureSearchToolkit/Response/Materializers/OneAzureSearchMaterializer.cs b/AzureSearchToolkit/Response/Materializers/OneAzureSearchMaterializer.cs index c49436e..10f39eb 100644 --- a/AzureSearchToolkit/Response/Materializers/OneAzureSearchMaterializer.cs +++ b/AzureSearchToolkit/Response/Materializers/OneAzureSearchMaterializer.cs @@ -1,6 +1,6 @@ -using AzureSearchToolkit.Utilities; -using Microsoft.Azure.Search.Models; -using Microsoft.Rest.Azure; +using Azure; +using Azure.Search.Documents.Models; +using AzureSearchToolkit.Utilities; using System; namespace AzureSearchToolkit.Response.Materializers @@ -8,40 +8,34 @@ namespace AzureSearchToolkit.Response.Materializers /// /// Materializes one hit into a CLR object throwing necessary exceptions as required to ensure First/Single semantics. /// - class OneAzureSearchMaterializer : IAzureSearchMaterializer + class OneAzureSearchMaterializer : IAzureSearchMaterializer { - readonly Func projector; - readonly Type elementType; readonly bool throwIfMoreThanOne; readonly bool defaultIfNone; /// /// Create an instance of the OneAzureSearchMaterializer with the given parameters. /// - /// A function to turn a hit into a desired CLR object. - /// The type of CLR object being materialized. /// Whether to throw an InvalidOperationException if there are multiple hits. /// Whether to throw an InvalidOperationException if there are no hits. - public OneAzureSearchMaterializer(Func projector, Type elementType, bool throwIfMoreThanOne, bool defaultIfNone) + public OneAzureSearchMaterializer(bool throwIfMoreThanOne, bool defaultIfNone) { - this.projector = projector; - this.elementType = elementType; this.throwIfMoreThanOne = throwIfMoreThanOne; this.defaultIfNone = defaultIfNone; } - public object Materialize(AzureOperationResponse> response) + public object Materialize(Response> response) { Argument.EnsureNotNull(nameof(response), response); - using (var enumerator = response.Body.Results.GetEnumerator()) + using (var enumerator = response.Value.GetResults().GetEnumerator()) { if (!enumerator.MoveNext()) { if (defaultIfNone) { - return TypeHelper.CreateDefault(elementType); - } + return TypeHelper.CreateDefault(typeof(T)); + } else { throw new InvalidOperationException("Sequence contains no elements"); @@ -53,9 +47,9 @@ public object Materialize(AzureOperationResponse> if (throwIfMoreThanOne && enumerator.MoveNext()) { throw new InvalidOperationException("Sequence contains more than one element"); - } + } - return projector(current); + return current; } } } diff --git a/AzureSearchToolkit/SearchClientExtensions.cs b/AzureSearchToolkit/SearchClientExtensions.cs new file mode 100644 index 0000000..9e4211d --- /dev/null +++ b/AzureSearchToolkit/SearchClientExtensions.cs @@ -0,0 +1,39 @@ +using Azure; +using Azure.Search.Documents; +using Azure.Search.Documents.Models; +using AzureSearchToolkit.Request.Formatters; +using AzureSearchToolkit.Request.Visitors; +using System.Linq; +using System.Linq.Expressions; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace AzureSearchToolkit +{ + public static class SearchClientExtensions + { + /// + /// Gets a query that can search for documents of type . + /// + /// The document type. + /// The query that can search for documents of the given type. + public static IQueryable Query(this SearchClient searchClient, JsonSerializerOptions jsonSerializerOptions = null) + { + return new AzureSearchQuery(new AzureSearchQueryProvider(searchClient, jsonSerializerOptions ?? new JsonSerializerOptions())); + } + + public static Task>> SearchAsync(this SearchClient searchClient, IQueryable query, JsonSerializerOptions jsonSerializerOptions = null, CancellationToken cancellationToken = default) + { + return searchClient.SearchAsync(query.Expression, jsonSerializerOptions, cancellationToken); + } + + public static Task>> SearchAsync(this SearchClient searchClient, Expression expression, JsonSerializerOptions jsonSerializerOptions = null, CancellationToken cancellationToken = default) + { + var translation = AzureSearchQueryTranslator.Translate(expression, jsonSerializerOptions ?? new JsonSerializerOptions()); + var formatter = new SearchRequestFormatter(translation.SearchRequest); + var searchRequest = formatter.SearchRequest; + return searchClient.SearchAsync(searchRequest.SearchText, searchRequest.SearchOptions, cancellationToken); + } + } +} diff --git a/AzureSearchToolkit/Utilities/Argument.cs b/AzureSearchToolkit/Utilities/Argument.cs index af43996..1f8e6e6 100644 --- a/AzureSearchToolkit/Utilities/Argument.cs +++ b/AzureSearchToolkit/Utilities/Argument.cs @@ -65,7 +65,7 @@ public static void EnsureIsDefinedEnum(string argumentName, TEnum value) if (!Enum.IsDefined(typeof(TEnum), value)) { throw new ArgumentOutOfRangeException(argumentName, $"Must be a defined {typeof(TEnum)} enum value."); - } + } } /// @@ -92,7 +92,7 @@ public static void EnsureNotEmpty(string argumentName, IEnumerable values) // if (values == null || values.Count < 1) // { // throw new ArgumentOutOfRangeException(argumentName, "Must contain one or more values."); - // } + // } //} } } diff --git a/AzureSearchToolkit/Utilities/AsyncHelper.cs b/AzureSearchToolkit/Utilities/AsyncHelper.cs deleted file mode 100644 index 5a6b047..0000000 --- a/AzureSearchToolkit/Utilities/AsyncHelper.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace AzureSearchToolkit.Utilities -{ - static class AsyncHelper - { - public static T RunSync(Func> action) - { - return SynchronizationContext.Current == null - ? action().GetAwaiter().GetResult() - : Task.Run(async () => await action()).GetAwaiter().GetResult(); - } - } -} diff --git a/AzureSearchToolkit/Utilities/DictionaryHelper.cs b/AzureSearchToolkit/Utilities/DictionaryHelper.cs index 1c051c1..7c7077f 100644 --- a/AzureSearchToolkit/Utilities/DictionaryHelper.cs +++ b/AzureSearchToolkit/Utilities/DictionaryHelper.cs @@ -37,7 +37,7 @@ public static object ToObject(IDictionary source, Type typeToCon if (foundProperty != null) { foundProperty.SetValue(someObject, item.Value, null); - } + } } return someObject; diff --git a/AzureSearchToolkit/Utilities/EnumerableExtensions.cs b/AzureSearchToolkit/Utilities/EnumerableExtensions.cs index 3168f70..36a943b 100644 --- a/AzureSearchToolkit/Utilities/EnumerableExtensions.cs +++ b/AzureSearchToolkit/Utilities/EnumerableExtensions.cs @@ -12,7 +12,7 @@ public static SortedDictionary ToSortedDictionary(this IEnumerab return new SortedDictionary(source.ToDictionary(keySelector)); } - public static SortedDictionary ToSortedDictionary(this IEnumerable source, + public static SortedDictionary ToSortedDictionary(this IEnumerable source, Func keySelector, Func elementSelector) { return new SortedDictionary(source.ToDictionary(keySelector, elementSelector)); diff --git a/AzureSearchToolkit/Utilities/ExpressionExtensions.cs b/AzureSearchToolkit/Utilities/ExpressionExtensions.cs index 80dc87d..086c0ed 100644 --- a/AzureSearchToolkit/Utilities/ExpressionExtensions.cs +++ b/AzureSearchToolkit/Utilities/ExpressionExtensions.cs @@ -18,7 +18,7 @@ public static Expression StripQuotes(this Expression expression) { expression = ((UnaryExpression)expression).Operand; } - + return expression; } diff --git a/AzureSearchToolkit/Utilities/ExpressionHelper.cs b/AzureSearchToolkit/Utilities/ExpressionHelper.cs index 7d4668c..130e8c8 100644 --- a/AzureSearchToolkit/Utilities/ExpressionHelper.cs +++ b/AzureSearchToolkit/Utilities/ExpressionHelper.cs @@ -16,7 +16,7 @@ public static PropertyInfo GetPropertyInfo(Expression propertyLambda) public static PropertyInfo GetPropertyInfo(Expression> propertyLambda) { Argument.EnsureNotNull(nameof(propertyLambda), propertyLambda); - + var type = typeof(TSource); MemberExpression member = propertyLambda.Body as MemberExpression; @@ -24,7 +24,7 @@ public static PropertyInfo GetPropertyInfo(Expression).MakeGenericType(sequenceType.GetElementType()); } - + var sequenceTypeInfo = sequenceType.GetTypeInfo(); while (true) @@ -128,7 +128,7 @@ public static Type FindIEnumerable(Type sequenceType) if (candidateIEnumerable.IsAssignableFrom(sequenceType)) { return candidateIEnumerable; - } + } } foreach (var candidateInterface in sequenceTypeInfo.ImplementedInterfaces.Select(FindIEnumerable)) @@ -137,7 +137,7 @@ public static Type FindIEnumerable(Type sequenceType) { return candidateInterface; } - } + } if (sequenceTypeInfo.BaseType == null || sequenceTypeInfo.BaseType == typeof(object)) { @@ -160,7 +160,7 @@ public static bool IsPropertyNonStringEnumerable(Type type) { return false; } - + return typeof(IEnumerable).IsAssignableFrom(type); } @@ -188,7 +188,7 @@ public static bool IsNullable(this Type type) } /// - /// Create a default value for either a value type or reference type. + /// Create a default value for either a value type or reference type. /// /// Type of the value to create. /// Default value for this type.