diff --git a/src/Directory.Build.props b/src/Directory.Build.props index e05c1d5..3752165 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,10 +5,15 @@ 0.8.0 - -preview-1 + 8.0.2 9.0.5 + + 1.0.2 + 1.0.0 + 1.0.0-preview-4.3 + diff --git a/src/RoyalCode.SmartSearch.Abstractions/AllEntitiesExtensions.cs b/src/RoyalCode.SmartSearch.Abstractions/AllEntitiesExtensions.cs deleted file mode 100644 index 269a2f1..0000000 --- a/src/RoyalCode.SmartSearch.Abstractions/AllEntitiesExtensions.cs +++ /dev/null @@ -1,180 +0,0 @@ -using System.Linq.Expressions; - -namespace RoyalCode.SmartSearch.Abstractions; - -/// -/// Extension methods for . -/// -public static class AllEntitiesExtensions -{ - /// - /// Apply the filters and sorting and update all entities that meet the criteria. - /// - /// - /// - /// - /// - /// - public static void UpdateAll( - this IAllEntities allEntities, - ICollection data, - Action updateAction) - where TEntity : class - where TData : class - { - UpdateAllWithHelper.Execute(allEntities, data, updateAction); - } - - /// - /// Apply the filters and sorting and update all entities that meet the criteria. - /// - /// - /// - /// - /// - /// - /// - public static Task UpdateAllWith( - this IAllEntities allEntities, - ICollection data, - Action updateAction) - where TEntity : class - where TData : class - { - return UpdateAllWithHelper.ExecuteAsync(allEntities, data, updateAction); - } - - private static class UpdateAllWithHelper - where TEntity : class - where TData : class - { - private static Action, ICollection, Action>? execute; - private static Func, ICollection, Action, Task>? executeAsync; - - public static void Execute( - IAllEntities allEntities, - ICollection data, - Action updateAction) - { - execute ??= Create(); - execute(allEntities, data, updateAction); - } - - public static Task ExecuteAsync( - IAllEntities allEntities, - ICollection data, - Action updateAction) - { - executeAsync ??= CreateAsync(); - return executeAsync(allEntities, data, updateAction); - } - - private static Action, ICollection, Action> Create() - { - // get the property Id from TEntity - var idProperty = typeof(TEntity).GetProperty("Id") - ?? throw new InvalidOperationException("The entity does not have a property Id."); - - // get the property Id from TData - var dataIdProperty = typeof(TData).GetProperty("Id") - ?? throw new InvalidOperationException("The data does not have a property Id."); - - // validate types of the id properties - if (idProperty.PropertyType != dataIdProperty.PropertyType) - throw new InvalidOperationException("The type of the id properties are different."); - - // create a function expression to get the id from TEntity - var entityParameter = Expression.Parameter(typeof(TEntity), "entity"); - var lambdaType = typeof(Func<,>).MakeGenericType(typeof(TEntity), idProperty.PropertyType); - var entityLambda = Expression.Lambda(lambdaType, - Expression.Property(entityParameter, idProperty), - entityParameter); - - // create a function expression to get the id from TData - var dataParameter = Expression.Parameter(typeof(TData), "data"); - var dataLambda = Expression.Lambda(lambdaType, - Expression.Property(dataParameter, dataIdProperty), - dataParameter); - - // create expression paramters for allEntities, data, and updateAction - var allEntitiesParameter = Expression.Parameter(typeof(IAllEntities), "allEntities"); - dataParameter = Expression.Parameter(typeof(ICollection), "data"); - var updateActionParameter = Expression.Parameter(typeof(Action), "updateAction"); - - // create a call to the UpdateAll method - var updateAllMethod = typeof(IAllEntities).GetMethods() - .First(m => m.Name == nameof(IAllEntities.UpdateAll) && m.GetParameters().Length == 4); - var updateAllCall = Expression.Call( - allEntitiesParameter, - updateAllMethod, - dataParameter, - entityLambda, - dataLambda, - updateActionParameter); - - // create a lambda expression - var lambda = Expression.Lambda, ICollection, Action>>( - updateAllCall, - allEntitiesParameter, - dataParameter, - updateActionParameter); - - // compile the lambda expression - return lambda.Compile(); - } - - private static Func,ICollection,Action,Task> CreateAsync() - { - // get the property Id from TEntity - var idProperty = typeof(TEntity).GetProperty("Id") - ?? throw new InvalidOperationException("The entity does not have a property Id."); - - // get the property Id from TData - var dataIdProperty = typeof(TData).GetProperty("Id") - ?? throw new InvalidOperationException("The data does not have a property Id."); - - // validate types of the id properties - if (idProperty.PropertyType != dataIdProperty.PropertyType) - throw new InvalidOperationException("The type of the id properties are different."); - - // create a function expression to get the id from TEntity - var entityParameter = Expression.Parameter(typeof(TEntity), "entity"); - var lambdaType = typeof(Func<,>).MakeGenericType(typeof(TEntity), idProperty.PropertyType); - var entityLambda = Expression.Lambda(lambdaType, - Expression.Property(entityParameter, idProperty), - entityParameter); - - // create a function expression to get the id from TData - var dataParameter = Expression.Parameter(typeof(TData), "data"); - var dataLambda = Expression.Lambda(lambdaType, - Expression.Property(dataParameter, dataIdProperty), - dataParameter); - - // create expression paramters for allEntities, data, and updateAction - var allEntitiesParameter = Expression.Parameter(typeof(IAllEntities), "allEntities"); - dataParameter = Expression.Parameter(typeof(ICollection), "data"); - var updateActionParameter = Expression.Parameter(typeof(Action), "updateAction"); - - // create a call to the UpdateAllAsync method - var updateAllMethod = typeof(IAllEntities).GetMethods() - .First(m => m.Name == nameof(IAllEntities.UpdateAllAsync) && m.GetParameters().Length == 5); - var updateAllCall = Expression.Call( - allEntitiesParameter, - updateAllMethod, - dataParameter, - entityLambda, - dataLambda, - updateActionParameter); - - // create a lambda expression - var lambda = Expression.Lambda,ICollection,Action,Task>>( - updateAllCall, - allEntitiesParameter, - dataParameter, - updateActionParameter); - - // compile the lambda expression - return lambda.Compile(); - } - } -} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Abstractions/AsyncResultList.cs b/src/RoyalCode.SmartSearch.Abstractions/AsyncResultList.cs index 14172dd..27d5f11 100644 --- a/src/RoyalCode.SmartSearch.Abstractions/AsyncResultList.cs +++ b/src/RoyalCode.SmartSearch.Abstractions/AsyncResultList.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace RoyalCode.SmartSearch.Abstractions; +namespace RoyalCode.SmartSearch; /// /// @@ -22,9 +22,15 @@ public sealed class AsyncResultList : IAsyncResultList /// public int Pages { get; init; } + /// + public int Skipped { get; init; } + + /// + public int Taken { get; init; } + /// [JsonConverter(typeof(SortingsConverter))] - public IEnumerable Sortings { get; init; } = null!; + public IReadOnlyList Sortings { get; init; } = null!; /// public Dictionary Projections { get; init; } = null!; diff --git a/src/RoyalCode.SmartSearch.Abstractions/CriterionAttribute.cs b/src/RoyalCode.SmartSearch.Abstractions/CriterionAttribute.cs index e93c029..2f8f44b 100644 --- a/src/RoyalCode.SmartSearch.Abstractions/CriterionAttribute.cs +++ b/src/RoyalCode.SmartSearch.Abstractions/CriterionAttribute.cs @@ -1,4 +1,4 @@ -namespace RoyalCode.SmartSearch.Abstractions; +namespace RoyalCode.SmartSearch; /// /// diff --git a/src/RoyalCode.SmartSearch.Abstractions/CriterionOperator.cs b/src/RoyalCode.SmartSearch.Abstractions/CriterionOperator.cs index a37dc7c..b8125b6 100644 --- a/src/RoyalCode.SmartSearch.Abstractions/CriterionOperator.cs +++ b/src/RoyalCode.SmartSearch.Abstractions/CriterionOperator.cs @@ -1,4 +1,4 @@ -namespace RoyalCode.SmartSearch.Abstractions; +namespace RoyalCode.SmartSearch; /// /// diff --git a/src/RoyalCode.SmartSearch.Abstractions/Exceptions/OrderByException.cs b/src/RoyalCode.SmartSearch.Abstractions/Exceptions/OrderByException.cs new file mode 100644 index 0000000..3bc22b6 --- /dev/null +++ b/src/RoyalCode.SmartSearch.Abstractions/Exceptions/OrderByException.cs @@ -0,0 +1,22 @@ +namespace RoyalCode.SmartSearch.Exceptions; + +/// +/// Exception thrown when an error occurs related to the sorting of results in a query. +/// +/// Message about the sorting property with problems. +/// The name of the property that caused the sorting error. +/// The type of data to be filtered and sorted. +/// Internal exception that caused the problem. +public sealed class OrderByException(string message, string propertyName, string typeName, Exception inner) + : Exception(message, inner) +{ + /// + /// The name of the property that caused the sorting error. + /// + public string PropertyName { get; } = propertyName; + + /// + /// The type of data to be filtered and sorted. + /// + public string TypeName { get; } = typeName; +} diff --git a/src/RoyalCode.SmartSearch.Abstractions/IAllEntities.cs b/src/RoyalCode.SmartSearch.Abstractions/IAllEntities.cs deleted file mode 100644 index dc45a87..0000000 --- a/src/RoyalCode.SmartSearch.Abstractions/IAllEntities.cs +++ /dev/null @@ -1,198 +0,0 @@ -namespace RoyalCode.SmartSearch.Abstractions; - -/// -/// -/// A search that returns all entities with purpose to edit them. -/// -/// -/// Filters and sorting can be applied, but the search engine must be able to apply them. -/// -/// -/// When used with a unit of work, all changes made to the entities must be saved. -/// -/// -/// The entity type -public interface IAllEntities - where TEntity : class -{ - /// - /// - /// Adds a filter object to the search. - /// - /// - /// The search engine must be able to apply this filter, otherwise an exception will be throwed. - /// - /// - /// The filter type. - /// The filter object. - /// The same instance for chaining calls. - IAllEntities FilterBy(TFilter filter) - where TFilter : class; - - /// - /// - /// Adds a sorting object to be applied to the search. - /// - /// - /// The sorting object. - /// The same instance for chaining calls. - IAllEntities OrderBy(ISorting sorting); - - /// - /// Apply the filters and sorting and get all the entities that meet the criteria. - /// - /// - /// A collection of the entities. - /// - ICollection Collect(); - - /// - /// Apply the filters and sorting and get all the entities that meet the criteria. - /// - /// The cancellation token. - /// - /// A collection of the entities. - /// - Task> CollectAsync(CancellationToken cancellationToken = default); - - /// - /// Apply the filters and sorting and verify if there are any entities that meet the criteria. - /// - /// - /// True if there are entities that meet the criteria, otherwise false. - /// - bool Exists(); - - /// - /// Apply the filters and sorting and verify if there are any entities that meet the criteria. - /// - /// The cancellation token. - /// - /// True if there are entities that meet the criteria, otherwise false. - /// - Task ExistsAsync(CancellationToken cancellationToken = default); - - // Opções First, FirstAsync, Single, SingleAsync, e OrDefault. - - /// - /// Apply the filters and sorting and get the first entity that meets the criteria. - /// - /// - /// The entity or null if there are no entities that meet the criteria. - /// - TEntity? First(); - - /// - /// Apply the filters and sorting and get the first entity that meets the criteria. - /// - /// The cancellation token. - /// - /// The entity or null if there are no entities that meet the criteria. - /// - Task FirstAsync(CancellationToken cancellationToken = default); - - /// - /// Apply the filters and sorting and get the first entity that meets the criteria, - /// or throw an exception if there are no entities that meet the criteria or more than one. - /// - /// - /// The entity that meets the criteria - /// or throw an exception if there are no entities that meet the criteria or more than one. - /// - TEntity Single(); - - /// - /// Apply the filters and sorting and get the first entity that meets the criteria, - /// or throw an exception if there are no entities that meet the criteria or more than one. - /// - /// The cancellation token. - /// - /// The entity that meets the criteria - /// or throw an exception if there are no entities that meet the criteria or more than one. - /// - Task SingleAsync(CancellationToken cancellationToken = default); - - /// - /// Apply the filters and sorting and delete all the entities that meet the criteria. - /// - void DeleteAll(); - - /// - /// Apply the filters and sorting and delete all the entities that meet the criteria. - /// - /// The cancellation token. - /// A task representing the asynchronous operation. - Task DeleteAllAsync(CancellationToken cancellationToken = default); - - /// - /// Apply the filters and sorting and update all the entities that meet the criteria. - /// - /// The action to update the entities. - void UpdateAll(Action updateAction); - - /// - /// Apply the filters and sorting and update all entities that meet the criteria. - /// - /// The data used to update the entities. - /// The action to update the entities. - /// The type of the data used to update the entities. - void UpdateAll(TData data, Action updateAction); - - /// - /// Apply the filters and sorting and update all entities that meet the criteria. - /// - /// A collection of data used to update the entities. - /// The function to get the entity id. - /// The function to get the data id. - /// The action to update the entities. - /// The type of the data used to update the entities. - /// The type of the id. - void UpdateAll( - ICollection collection, - Func entityIdGet, - Func dataIdGet, - Action updateAction) - where TData : class; - - /// - /// Apply the filters and sorting and update all the entities that meet the criteria. - /// - /// The action to update the entities. - /// The cancellation token. - /// A task representing the asynchronous operation. - Task UpdateAllAsync( - Action updateAction, - CancellationToken cancellationToken = default); - - /// - /// Apply the filters and sorting and update all entities that meet the criteria. - /// - /// The data used to update the entities. - /// The action to update the entities. - /// The cancellation token. - /// The type of the data used to update the entities. - /// A task representing the asynchronous operation. - Task UpdateAllAsync( - TData data, - Action updateAction, - CancellationToken cancellationToken = default); - - /// - /// Apply the filters and sorting and update all entities that meet the criteria. - /// - /// The collection of data used to update the entities. - /// The function to get the entity id. - /// The function to get the data id. - /// The action to update the entities. - /// The cancellation token. - /// The type of the data used to update the entities. - /// The type of the id. - /// A task that represents the asynchronous operation. - Task UpdateAllAsync( - ICollection collection, - Func entityIdGet, - Func dataIdGet, - Action updateAction, - CancellationToken cancellationToken = default) - where TData : class; -} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Abstractions/IAsyncResultList.cs b/src/RoyalCode.SmartSearch.Abstractions/IAsyncResultList.cs index 65e7d9b..20e66bf 100644 --- a/src/RoyalCode.SmartSearch.Abstractions/IAsyncResultList.cs +++ b/src/RoyalCode.SmartSearch.Abstractions/IAsyncResultList.cs @@ -1,4 +1,4 @@ -namespace RoyalCode.SmartSearch.Abstractions; +namespace RoyalCode.SmartSearch; /// /// Component interface for async listing the result of a search. diff --git a/src/RoyalCode.SmartSearch.Abstractions/ICriteria.cs b/src/RoyalCode.SmartSearch.Abstractions/ICriteria.cs new file mode 100644 index 0000000..ceba2d3 --- /dev/null +++ b/src/RoyalCode.SmartSearch.Abstractions/ICriteria.cs @@ -0,0 +1,177 @@ +using System.Linq.Expressions; + +namespace RoyalCode.SmartSearch; + +/// +/// +/// Represents a set of criteria for querying entities, allowing the application of multiple filters, +/// sorting, projections, and selection of specific data. The interface +/// is designed for scenarios where entities are to be collected or obtained directly, and these entities +/// will be tracked by the change tracker (e.g., in an ORM context such as Entity Framework). +/// +/// +/// When using methods like , , or , +/// the returned entities are tracked by the change tracker, enabling change detection and persistence. +/// +/// +/// To perform a search where entities are not tracked (i.e., detached from the change tracker), +/// convert the criteria to a search using or . +/// The resulting or will return +/// entities or DTOs that are not tracked by the change tracker. +/// +/// +/// The entity type. +public interface ICriteria : ICriteriaOptions> + where TEntity : class +{ + /// + /// + /// Adds a filter object to the criteria. + /// + /// + /// The search engine must be able to apply this filter, otherwise an exception will be throwed. + /// + /// + /// The filter type. + /// The filter object. + /// The same instance for chaining calls. + ICriteria FilterBy(TFilter filter) + where TFilter : class; + + /// + /// + /// Adds a sorting object to be applied to the criteria. + /// + /// + /// The sorting object. + /// The same instance for chaining calls. + ICriteria OrderBy(ISorting sorting); + + /// + /// + /// Adds multiple sorting objects to be applied to the criteria. + /// + /// + /// The array of sorting objects. + /// The same instance for chaining calls. + ICriteria OrderBy(ISorting[]? sorting); + + /// + /// + /// Convert the criteria to a search. + /// + /// + /// When this method is called, the criteria will be converted to a search, + /// and the entities will not be tracked by a unit of work or a context. + /// + /// + /// new instance of with the same filters. + ISearch AsSearch(); + + /// + /// + /// Requires a Select, adapting the Entity to a DTO. + /// + /// + /// In this method the adaptation of the entity to the DTO will be done by the search engine. + /// + /// + /// When this method is called, the criteria will be converted to a search, + /// and the entities will not be tracked by a unit of work or a context. + /// + /// + /// The Data Transfer Object type. + /// new instance of with the same filters. + ISearch Select() + where TDto : class; + + /// + /// + /// Requires a Select, adapting the Entity to a DTO. + /// + /// + /// In this method, the expression is required to adapt the entity to the DTO. + /// + /// + /// When this method is called, the criteria will be converted to a search, + /// and the entities will not be tracked by a unit of work or a context. + /// + /// + /// The Data Transfer Object type. + /// Expression to adapt the entity to the DTO. + /// new instance of with the same filters. + ISearch Select(Expression> selectExpression) + where TDto : class; + + /// + /// Apply the filters and sorting and get all the entities that meet the criteria. + /// + /// + /// A collection of the entities. + /// + IReadOnlyList Collect(); + + /// + /// Apply the filters and sorting and get all the entities that meet the criteria. + /// + /// The cancellation token. + /// + /// A collection of the entities. + /// + Task> CollectAsync(CancellationToken cancellationToken = default); + + /// + /// Apply the filters and sorting and verify if there are any entities that meet the criteria. + /// + /// + /// True if there are entities that meet the criteria, otherwise false. + /// + bool Exists(); + + /// + /// Apply the filters and sorting and verify if there are any entities that meet the criteria. + /// + /// The cancellation token. + /// + /// True if there are entities that meet the criteria, otherwise false. + /// + Task ExistsAsync(CancellationToken cancellationToken = default); + + /// + /// Apply the filters and sorting and get the first entity that meets the criteria. + /// + /// + /// The entity or null if there are no entities that meet the criteria. + /// + TEntity? FirstOrDefault(); + + /// + /// Apply the filters and sorting and get the first entity that meets the criteria. + /// + /// The cancellation token. + /// + /// The entity or null if there are no entities that meet the criteria. + /// + Task FirstOrDefaultAsync(CancellationToken cancellationToken = default); + + /// + /// Apply the filters and sorting and get the first entity that meets the criteria, + /// or throw an exception if there are no entities that meet the criteria or more than one. + /// + /// + /// The entity that meets the criteria + /// or throw an exception if there are no entities that meet the criteria or more than one. + /// + TEntity Single(); + + /// + /// Apply the filters and sorting and get the first entity that meets the criteria, + /// or throw an exception if there are no entities that meet the criteria or more than one. + /// + /// The cancellation token. + /// + /// The entity that meets the criteria + /// or throw an exception if there are no entities that meet the criteria or more than one. + /// + Task SingleAsync(CancellationToken cancellationToken = default); +} diff --git a/src/RoyalCode.SmartSearch.Abstractions/ICriteriaOptions.cs b/src/RoyalCode.SmartSearch.Abstractions/ICriteriaOptions.cs new file mode 100644 index 0000000..8079046 --- /dev/null +++ b/src/RoyalCode.SmartSearch.Abstractions/ICriteriaOptions.cs @@ -0,0 +1,88 @@ +namespace RoyalCode.SmartSearch; + +/// +/// Options that can be applied into criteria or search components. +/// +/// The search component type. +public interface ICriteriaOptions +{ + /// + /// + /// Defines that the query will be paged and determines the number of items per page. + /// + /// + /// The default value is 10 items per page. + /// + /// + /// When zero (0) is entered, it will not be paged. + /// + /// + /// Items per page. + /// The page number. + /// The same instance of the search for chaining calls. + TSearch UsePages(int itemsPerPage = 10, int pageNumber = 1); + + /// + /// The number of the page to be searched. + /// + /// The same instance of the search for chaining calls. + TSearch FetchPage(int pageNumber); + + /// + /// + /// Defines the number of records to be skipped in the search. + /// + /// + /// When zero (0) is entered, it will not skip any records. + /// + /// + /// When is informed, this property is ignored. + /// + /// + /// The number of records to skip. + /// The same instance of the search for chaining calls. + TSearch Skip(int skip); + + /// + /// + /// Limits the number of results returned by the search query. + /// + /// + /// When is informed, this property is ignored. + /// + /// + /// The maximum number of results to return. Must be a positive integer. + /// The updated search query with the specified limit applied. + TSearch Take(int take); + + /// + /// + /// Defines the number of records to be skipped and the number of records to be returned in the search. + /// + /// + /// When is informed, this property is ignored. + /// + /// + /// The number of records to skip. + /// The maximum number of results to return. Must be a positive integer. + /// The same instance of the search for chaining calls. + TSearch SkipTake(int skip, int take); + + /// + /// + /// Updates the last record count. + /// + /// + /// Used to not count the records again. + /// + /// + /// The same instance of the search for chaining calls. + TSearch UseLastCount(int lastCount); + + /// + /// Whether to apply record counting. + /// + /// Whether to apply record counting. + /// The same instance of the search for chaining calls. + TSearch UseCount(bool useCount = true); +} diff --git a/src/RoyalCode.SmartSearch.Abstractions/IResultList.cs b/src/RoyalCode.SmartSearch.Abstractions/IResultList.cs index 404540b..a937f77 100644 --- a/src/RoyalCode.SmartSearch.Abstractions/IResultList.cs +++ b/src/RoyalCode.SmartSearch.Abstractions/IResultList.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace RoyalCode.SmartSearch.Abstractions; +namespace RoyalCode.SmartSearch; /// /// @@ -33,16 +33,26 @@ public interface IResultList /// int Pages { get; } + /// + /// Number of items skipped in the result set. + /// + int Skipped { get; } + + /// + /// Number of items taken from the result set. + /// + int Taken { get; } + /// /// The sort objects applied to the search. /// [JsonConverter(typeof(SortingsConverter))] - IEnumerable Sortings { get; } + IReadOnlyList Sortings { get; } /// /// Projections carried out during the research. /// - Dictionary Projections { get; } + Dictionary? Projections { get; } } /// diff --git a/src/RoyalCode.SmartSearch.Abstractions/ISearch.cs b/src/RoyalCode.SmartSearch.Abstractions/ISearch.cs index ea27a23..b1e78d1 100644 --- a/src/RoyalCode.SmartSearch.Abstractions/ISearch.cs +++ b/src/RoyalCode.SmartSearch.Abstractions/ISearch.cs @@ -1,6 +1,6 @@ using System.Linq.Expressions; -namespace RoyalCode.SmartSearch.Abstractions; +namespace RoyalCode.SmartSearch; /// /// @@ -9,32 +9,9 @@ namespace RoyalCode.SmartSearch.Abstractions; /// /// /// The entity type. -public interface ISearch : ISearchOptions> +public interface ISearch where TEntity : class { - /// - /// - /// Adds a filter object to the search. - /// - /// - /// The search engine must be able to apply this filter, otherwise an exception will be throwed. - /// - /// - /// The filter type. - /// The filter object. - /// The same instance for chaining calls. - ISearch FilterBy(TFilter filter) - where TFilter : class; - - /// - /// - /// Adds a sorting object to be applied to the search. - /// - /// - /// The sorting object. - /// The same instance for chaining calls. - ISearch OrderBy(ISorting sorting); - /// /// /// Requires a Select, adapting the Entity to a DTO. @@ -81,6 +58,44 @@ ISearch Select(Expression> selectExpres /// The task cancellation token. /// A task to wait for the async list of results. Task> ToAsyncListAsync(CancellationToken token); + + /// + /// Apply the filters and sorting and get the first entity that meets the criteria. + /// + /// + /// The entity or null if there are no entities that meet the criteria. + /// + TEntity? FirstOrDefault(); + + /// + /// Apply the filters and sorting and get the first entity that meets the criteria. + /// + /// The cancellation token. + /// + /// The entity or null if there are no entities that meet the criteria. + /// + Task FirstDefaultAsync(CancellationToken cancellationToken = default); + + /// + /// Apply the filters and sorting and get the first entity that meets the criteria, + /// or throw an exception if there are no entities that meet the criteria or more than one. + /// + /// + /// The entity that meets the criteria + /// or throw an exception if there are no entities that meet the criteria or more than one. + /// + TEntity Single(); + + /// + /// Apply the filters and sorting and get the first entity that meets the criteria, + /// or throw an exception if there are no entities that meet the criteria or more than one. + /// + /// The cancellation token. + /// + /// The entity that meets the criteria + /// or throw an exception if there are no entities that meet the criteria or more than one. + /// + Task SingleAsync(CancellationToken cancellationToken = default); } /// @@ -94,27 +109,10 @@ ISearch Select(Expression> selectExpres /// /// The entity type. /// The Data Transfer Object type. -public interface ISearch : ISearchOptions> +public interface ISearch where TEntity : class where TDto : class { - /// - /// - /// Adds a filter object to the search. - /// - /// - /// The search engine must be able to apply this filter, otherwise an exception will be throwed. - /// - /// - /// This filter will be applied against the query type. - /// - /// - /// The filter type. - /// The filter object. - /// The same instance for chaining calls. - ISearch FilterBy(TFilter filter) - where TFilter : class; - /// /// It searches for the entities and returns them in a list of results. /// @@ -134,4 +132,42 @@ ISearch FilterBy(TFilter filter) /// The task cancellation token. /// A task to wait for the async list of results. Task> ToAsyncListAsync(CancellationToken token); + + /// + /// Apply the filters and sorting and get the first entity that meets the criteria. + /// + /// + /// The entity or null if there are no entities that meet the criteria. + /// + TDto? FirstOrDefault(); + + /// + /// Apply the filters and sorting and get the first entity that meets the criteria. + /// + /// The cancellation token. + /// + /// The entity or null if there are no entities that meet the criteria. + /// + Task FirstOrDefaultAsync(CancellationToken cancellationToken = default); + + /// + /// Apply the filters and sorting and get the first entity that meets the criteria, + /// or throw an exception if there are no entities that meet the criteria or more than one. + /// + /// + /// The entity that meets the criteria + /// or throw an exception if there are no entities that meet the criteria or more than one. + /// + TDto Single(); + + /// + /// Apply the filters and sorting and get the first entity that meets the criteria, + /// or throw an exception if there are no entities that meet the criteria or more than one. + /// + /// The cancellation token. + /// + /// The entity that meets the criteria + /// or throw an exception if there are no entities that meet the criteria or more than one. + /// + Task SingleAsync(CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Abstractions/ISearchManager.cs b/src/RoyalCode.SmartSearch.Abstractions/ISearchManager.cs index 3bff921..1d7dbac 100644 --- a/src/RoyalCode.SmartSearch.Abstractions/ISearchManager.cs +++ b/src/RoyalCode.SmartSearch.Abstractions/ISearchManager.cs @@ -1,4 +1,4 @@ -namespace RoyalCode.SmartSearch.Abstractions; +namespace RoyalCode.SmartSearch; /// /// @@ -12,37 +12,15 @@ public interface ISearchManager { /// /// - /// Creates a new search for the entity. + /// Creates a new criteria for the entity. /// /// - /// There must be a search component for the persistence unit, otherwise an exception will be thrown. + /// With the criteria, it is possible to apply filters, sorting, projections, + /// and pagination, or collect entities directly from the persistence unit. /// /// /// The entity type. - /// A new instance of . - /// - /// If entity is not part of the persistence unit or there is no search component for it. - /// - ISearch Search() where TEntity : class; - - /// - /// - /// Gets all entities of the persistence unit, where it is possible to add filters and sorters. - /// - /// - /// The purpose of is to query entities for update or delete. - /// - /// - /// When used with a unit of work, all changes made to the entities must be saved. - /// - /// - /// The entity type. - /// - /// A new instance of . - /// - /// - /// If entity is not part of the persistence unit or there is no search component for it. - /// - IAllEntities All() where TEntity : class; + /// A new instance of . + ICriteria Criteria() where TEntity : class; } diff --git a/src/RoyalCode.SmartSearch.Abstractions/ISearchOptions.cs b/src/RoyalCode.SmartSearch.Abstractions/ISearchOptions.cs deleted file mode 100644 index 57463c8..0000000 --- a/src/RoyalCode.SmartSearch.Abstractions/ISearchOptions.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace RoyalCode.SmartSearch.Abstractions; - -/// -/// Options that can be applied into searches. -/// -/// The search component type. -public interface ISearchOptions -{ - /// - /// - /// Defines that the query will be paged and determines the number of items per page. - /// - /// - /// The default value is 10 items per page. - /// - /// - /// When zero (0) is entered, it will not be paged. - /// - /// - /// Items per page. - /// The same instance of the search for chaining calls. - TSearch UsePages(int itemsPerPage = 10); - - /// - /// The number of the page to be searched. - /// - /// The same instance of the search for chaining calls. - TSearch FetchPage(int pageNumber); - - /// - /// - /// Updates the last record count. - /// - /// - /// Used to not count the records again. - /// - /// - /// The same instance of the search for chaining calls. - TSearch UseLastCount(int lastCount); - - /// - /// Whether to apply record counting. - /// - /// Whether to apply record counting. - /// The same instance of the search for chaining calls. - TSearch UseCount(bool useCount = true); -} diff --git a/src/RoyalCode.SmartSearch.Abstractions/ISorting.cs b/src/RoyalCode.SmartSearch.Abstractions/ISorting.cs index 286adab..78b8855 100644 --- a/src/RoyalCode.SmartSearch.Abstractions/ISorting.cs +++ b/src/RoyalCode.SmartSearch.Abstractions/ISorting.cs @@ -1,6 +1,6 @@ using System.ComponentModel; -namespace RoyalCode.SmartSearch.Abstractions; +namespace RoyalCode.SmartSearch; /// /// Component that contains information about the sorting applied to a search/query. diff --git a/src/RoyalCode.SmartSearch.Abstractions/ResultList.cs b/src/RoyalCode.SmartSearch.Abstractions/ResultList.cs index 9f82cef..88ad04a 100644 --- a/src/RoyalCode.SmartSearch.Abstractions/ResultList.cs +++ b/src/RoyalCode.SmartSearch.Abstractions/ResultList.cs @@ -2,7 +2,7 @@ using System.Text.Json.Serialization; -namespace RoyalCode.SmartSearch.Abstractions; +namespace RoyalCode.SmartSearch; /// /// @@ -24,16 +24,22 @@ public class ResultList : IResultList /// public int Pages { get; init; } + /// + public int Skipped { get; init; } + + /// + public int Taken { get; init; } + /// [JsonConverter(typeof(SortingsConverter))] - public IEnumerable Sortings { get; init; } = null!; + public IReadOnlyList Sortings { get; init; } = null!; /// - public Dictionary Projections { get; init; } = null!; + public Dictionary? Projections { get; init; } = null!; /// public IReadOnlyList Items { get; init; } = null!; - + /// public virtual T GetProjection(string name, T? defaultValue = default) { diff --git a/src/RoyalCode.SmartSearch.Abstractions/RoyalCode.SmartSearch.Abstractions.csproj b/src/RoyalCode.SmartSearch.Abstractions/RoyalCode.SmartSearch.Abstractions.csproj index 3e09872..1c91913 100644 --- a/src/RoyalCode.SmartSearch.Abstractions/RoyalCode.SmartSearch.Abstractions.csproj +++ b/src/RoyalCode.SmartSearch.Abstractions/RoyalCode.SmartSearch.Abstractions.csproj @@ -4,6 +4,7 @@ $(LibTargets) + RoyalCode.SmartSearch diff --git a/src/RoyalCode.SmartSearch.Abstractions/SearchExtensions.cs b/src/RoyalCode.SmartSearch.Abstractions/SearchExtensions.cs index 568f19d..bbde4c4 100644 --- a/src/RoyalCode.SmartSearch.Abstractions/SearchExtensions.cs +++ b/src/RoyalCode.SmartSearch.Abstractions/SearchExtensions.cs @@ -1,56 +1,45 @@ // Ignore Spelling: sortings -namespace RoyalCode.SmartSearch.Abstractions; +namespace RoyalCode.SmartSearch; /// /// -/// Extensions methods for , +/// Extensions methods for , /// and . /// /// public static class SearchExtensions { /// - /// Applies the to the . + /// Applies the to the . /// /// The search object type. - /// The search. + /// The criteria. /// The options. /// The search with the options applied. - public static ISearch WithOptions(this ISearch search, SearchOptions options) + public static ICriteria WithOptions(this ICriteria criteria, SearchOptions options) where T : class { if (options.ItemsPerPage.HasValue) - search = search.UsePages(options.ItemsPerPage.Value); + criteria = criteria.UsePages(options.ItemsPerPage.Value); if (options.Page.HasValue) - search = search.FetchPage(options.Page.Value); + criteria = criteria.FetchPage(options.Page.Value); if (options.LastCount.HasValue) - search = search.UseLastCount(options.LastCount.Value); + criteria = criteria.UseLastCount(options.LastCount.Value); if (options.Count.HasValue) - search = search.UseCount(options.Count.Value); + criteria = criteria.UseCount(options.Count.Value); - OrderBy(search, options.Sortings); + if (options.Skip.HasValue) + criteria = criteria.Skip(options.Skip.Value); - return search; - } + if (options.Take.HasValue) + criteria = criteria.Take(options.Take.Value); - /// - /// Applies a set of sorting to the search. - /// - /// The search object type. - /// The search. - /// The sortings. - /// The search with the sortings applied. - public static ISearch OrderBy(this ISearch search, ISorting[]? sortings) - where T : class - { - if (sortings is not null) - foreach (var sorting in sortings) - search.OrderBy(sorting); + criteria.OrderBy(options.Sortings); - return search; + return criteria; } } \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Abstractions/SearchOptions.cs b/src/RoyalCode.SmartSearch.Abstractions/SearchOptions.cs index 712abe5..9232607 100644 --- a/src/RoyalCode.SmartSearch.Abstractions/SearchOptions.cs +++ b/src/RoyalCode.SmartSearch.Abstractions/SearchOptions.cs @@ -1,20 +1,25 @@ // Ignore Spelling: sortings -namespace RoyalCode.SmartSearch.Abstractions; +namespace RoyalCode.SmartSearch; /// /// -/// A class that contains the options for a search. +/// A class that contains the options for a criteria and search. /// It is used to define the search parameters. /// /// -/// It is design to retrieve the options from a query string and apply them to the search. +/// It is designed to retrieve the options from a query string and apply them to the search. /// /// public sealed class SearchOptions { private List? sortings; + /// + /// The number of the page to be searched. + /// + public int? Page { get; set; } + /// /// /// Defines that the query will be paged and determines the number of items per page. @@ -29,9 +34,29 @@ public sealed class SearchOptions public int? ItemsPerPage { get; set; } /// - /// The number of the page to be searched. + /// + /// Defines the number of records to be skipped in the search. + /// When zero (0) is entered, it will not skip any records. + /// + /// + /// When is informed, this property is ignored. + /// /// - public int? Page { get; set; } + public int? Skip { get; set; } + + /// + /// + /// Defines the number of records to be returned in the search. + /// + /// + /// This property must be use with to define the number of records to be returned + /// after skipping the defined number of records. + /// + /// + /// When is informed, this property is ignored. + /// + /// + public int? Take { get; set; } /// /// diff --git a/src/RoyalCode.SmartSearch.Abstractions/Sorting.cs b/src/RoyalCode.SmartSearch.Abstractions/Sorting.cs index 0ad60f4..c65c204 100644 --- a/src/RoyalCode.SmartSearch.Abstractions/Sorting.cs +++ b/src/RoyalCode.SmartSearch.Abstractions/Sorting.cs @@ -1,7 +1,7 @@ using System.ComponentModel; using System.Text.Json; -namespace RoyalCode.SmartSearch.Abstractions; +namespace RoyalCode.SmartSearch; /// /// diff --git a/src/RoyalCode.SmartSearch.Abstractions/SortingsConverter.cs b/src/RoyalCode.SmartSearch.Abstractions/SortingsConverter.cs index 5f08b72..0021a34 100644 --- a/src/RoyalCode.SmartSearch.Abstractions/SortingsConverter.cs +++ b/src/RoyalCode.SmartSearch.Abstractions/SortingsConverter.cs @@ -1,16 +1,16 @@ using System.Text.Json.Serialization; using System.Text.Json; -namespace RoyalCode.SmartSearch.Abstractions; +namespace RoyalCode.SmartSearch; -internal sealed class SortingsConverter : JsonConverter> +internal sealed class SortingsConverter : JsonConverter> { - public override IEnumerable? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + public override IReadOnlyList? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { return JsonSerializer.Deserialize>(ref reader, options); } - public override void Write(Utf8JsonWriter writer, IEnumerable value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, IReadOnlyList value, JsonSerializerOptions options) { JsonSerializer.Serialize(writer, value, options); } diff --git a/src/RoyalCode.SmartSearch.AspNetCore/Extensions/SearchExtensions.cs b/src/RoyalCode.SmartSearch.AspNetCore/Extensions/SearchExtensions.cs new file mode 100644 index 0000000..db55c65 --- /dev/null +++ b/src/RoyalCode.SmartSearch.AspNetCore/Extensions/SearchExtensions.cs @@ -0,0 +1,554 @@ +using Microsoft.AspNetCore.Builder; +using RoyalCode.SmartSearch; +using RoyalCode.SmartSearch.AspNetCore.Internals; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.AspNetCore.Routing; + +public static class SearchExtensions +{ + #region MapSearch + + /// + /// Maps a GET endpoint to paginated, sorted and filtered search, returning entities. + /// + /// The type of entity to be consulted. + /// The type of filter applied. + /// The route builder. + /// The route pattern. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapSearch(this IEndpointRouteBuilder builder, [StringSyntax("Route")] string pattern) + where TEntity : class + where TFilter : class + { + var search = new SearchEntityEndpoint(); + return builder.MapGet(pattern, search.Search); + } + + /// + /// Maps a GET endpoint to paginated, sorted and filtered search, returning entities. + /// + /// The type of entity to be queried. + /// The type of the applied filter. + /// The route builder. + /// The route pattern. + /// Custom action to configure the search. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapSearch( + this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TFilter : class + { + var search = new SearchEntityEndpoint(searchAction); + return builder.MapGet(pattern, search.Search); + } + + /// + /// Maps a GET endpoint to paginated, sorted and filtered search, returning entities with an additional identifier. + /// + /// The type of entity to be queried. + /// The type of the applied filter. + /// The type of the additional identifier. + /// The route builder. + /// The route pattern. + /// Custom action to configure the search. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapSearch( + this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TFilter : class + { + var search = new SearchEntityEndpoint(searchAction); + return builder.MapGet(pattern, search.Search); + } + + /// + /// Maps a GET endpoint to paginated, sorted and filtered search, returning entities with two additional identifiers. + /// + /// The type of entity to be queried. + /// The type of the applied filter. + /// The type of the first additional identifier. + /// The type of the second additional identifier. + /// The route builder. + /// The route pattern. + /// Custom action to configure the search. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapSearch( + this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TFilter : class + { + var search = new SearchEntityEndpoint(searchAction); + return builder.MapGet(pattern, search.Search); + } + + /// + /// Maps a GET endpoint to paginated, sorted and filtered search, returning DTOs. + /// + /// The type of the entity to be queried. + /// The type of the DTO to be returned. + /// The type of the applied filter. + /// The route builder. + /// The route pattern. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapSearch(this IEndpointRouteBuilder builder, [StringSyntax("Route")] string pattern) + where TEntity : class + where TDto : class + where TFilter : class + { + var search = new SearchModelEndpoint(); + return builder.MapGet(pattern, search.Search); + } + + /// + /// Maps a GET endpoint to paginated, sorted and filtered search, returning DTOs. + /// + /// The type of the entity to be queried. + /// The type of the DTO to be returned. + /// The type of the applied filter. + /// The route builder. + /// The route pattern. + /// Action to apply additional logic to the search criteria. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapSearch(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TDto : class + where TFilter : class + { + var search = new SearchModelEndpoint(searchAction); + return builder.MapGet(pattern, search.Search); + } + + /// + /// Maps a GET endpoint to paginated, sorted and filtered search, returning DTOs with an additional identifier. + /// + /// The type of the entity to be queried. + /// The type of the DTO to be returned. + /// The type of the applied filter. + /// The type of the additional identifier. + /// The route builder. + /// The route pattern. + /// Action to apply additional logic to the search criteria. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapSearch(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TDto : class + where TFilter : class + { + var search = new SearchModelEndpoint(searchAction); + return builder.MapGet(pattern, search.Search); + } + + /// + /// Maps a GET endpoint to paginated, sorted and filtered search, returning DTOs with two additional identifiers. + /// + /// The type of the entity to be queried. + /// The type of the DTO to be returned. + /// The type of the applied filter. + /// The type of the first additional identifier. + /// The type of the second additional identifier. + /// The route builder. + /// The route pattern. + /// Action to apply additional logic to the search criteria. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapSearch(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TDto : class + where TFilter : class + { + var search = new SearchModelEndpoint(searchAction); + return builder.MapGet(pattern, search.Search); + } + + #endregion + + #region MapList + + /// + /// Maps a GET endpoint to a filtered and sorted list of entities. + /// + /// The type of the entity to be queried. + /// The type of the applied filter. + /// The route builder. + /// The route pattern. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapList(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern) + where TEntity : class + where TFilter : class + { + var list = new ListEntityEndpoint(); + return builder.MapGet(pattern, list.List); + } + + /// + /// Maps a GET endpoint to a filtered and sorted list of entities. + /// + /// The type of the entity to be queried. + /// The type of the applied filter. + /// The route builder. + /// The route pattern. + /// Action to apply additional logic to the search criteria. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapList(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TFilter : class + { + var list = new ListEntityEndpoint(searchAction); + return builder.MapGet(pattern, list.List); + } + + /// + /// Maps a GET endpoint to a filtered and sorted list of entities with an additional identifier. + /// + /// The type of the entity to be queried. + /// The type of the applied filter. + /// The type of the additional identifier. + /// The route builder. + /// The route pattern. + /// Action to apply additional logic to the search criteria. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapList(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TFilter : class + { + var list = new ListEntityEndpoint(searchAction); + return builder.MapGet(pattern, list.List); + } + + /// + /// Maps a GET endpoint to a filtered and sorted list of entities with two additional identifiers. + /// + /// The type of the entity to be queried. + /// The type of the applied filter. + /// The type of the first additional identifier. + /// The type of the second additional identifier. + /// The route builder. + /// The route pattern. + /// Action to apply additional logic to the search criteria. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapList(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TFilter : class + { + var list = new ListEntityEndpoint(searchAction); + return builder.MapGet(pattern, list.List); + } + + /// + /// Maps a GET endpoint to a filtered and sorted list of DTOs. + /// + /// The type of the entity to be queried. + /// The type of the DTO to be returned. + /// The type of the applied filter. + /// The route builder. + /// The route pattern. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapList(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern) + where TEntity : class + where TDto : class + where TFilter : class + { + var list = new ListModelEndpoint(); + return builder.MapGet(pattern, list.List); + } + + /// + /// Maps a GET endpoint to a filtered and sorted list of DTOs. + /// + /// The type of the entity to be queried. + /// The type of the DTO to be returned. + /// The type of the applied filter. + /// The route builder. + /// The route pattern. + /// Action to apply additional logic to the search criteria. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapList(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TDto : class + where TFilter : class + { + var list = new ListModelEndpoint(searchAction); + return builder.MapGet(pattern, list.List); + } + + /// + /// Maps a GET endpoint to a filtered and sorted list of DTOs with an additional identifier. + /// + /// The type of the entity to be queried. + /// The type of the DTO to be returned. + /// The type of the applied filter. + /// The type of the additional identifier. + /// The route builder. + /// The route pattern. + /// Action to apply additional logic to the search criteria. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapList(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TDto : class + where TFilter : class + { + var list = new ListModelEndpoint(searchAction); + return builder.MapGet(pattern, list.List); + } + + /// + /// Maps a GET endpoint to a filtered and sorted list of DTOs with two additional identifiers. + /// + /// The type of the entity to be queried. + /// The type of the DTO to be returned. + /// The type of the applied filter. + /// The type of the first additional identifier. + /// The type of the second additional identifier. + /// The route builder. + /// The route pattern. + /// Action to apply additional logic to the search criteria. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapList(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TDto : class + where TFilter : class + { + var list = new ListModelEndpoint(searchAction); + return builder.MapGet(pattern, list.List); + } + + #endregion + + #region MapFirst + + /// + /// Maps a GET endpoint to a first entity based on the applied filter. + /// + /// The type of the entity to be queried. + /// The type of the applied filter. + /// The route builder. + /// The route pattern. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapFirst(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern) + where TEntity : class + where TFilter : class + { + var first = new FirstEntityEndpoint(); + return builder.MapGet(pattern, first.First); + } + + /// + /// Maps a GET endpoint to a first entity based on the applied filter. + /// + /// The type of the entity to be queried. + /// The type of the applied filter. + /// The route builder. + /// The route pattern. + /// Action to apply additional logic to the search criteria. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapFirst(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TFilter : class + { + var first = new FirstEntityEndpoint(searchAction); + return builder.MapGet(pattern, first.First); + } + + /// + /// Maps a GET endpoint to a first entity based on the applied filter with an additional identifier. + /// + /// The type of the entity to be queried. + /// The type of the applied filter. + /// The type of the additional identifier. + /// The route builder. + /// The route pattern. + /// Action to apply additional logic to the search criteria. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapFirst(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TFilter : class + { + var first = new FirstEntityEndpoint(searchAction); + return builder.MapGet(pattern, first.First); + } + + /// + /// Maps a GET endpoint to a first entity based on the applied filter with two additional identifiers. + /// + /// The type of the entity to be queried. + /// The type of the applied filter. + /// The type of the first additional identifier. + /// The type of the second additional identifier. + /// The route builder. + /// The route pattern. + /// Action to apply additional logic to the search criteria. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapFirst(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TFilter : class + { + var first = new FirstEntityEndpoint(searchAction); + return builder.MapGet(pattern, first.First); + } + + /// + /// Maps a GET endpoint to a first entity based on the applied filter with three additional identifiers. + /// + /// The type of the entity to be queried. + /// The type of the applied filter. + /// The type of the first additional identifier. + /// The type of the second additional identifier. + /// The type of the third additional identifier. + /// The route builder. + /// The route pattern. + /// Action to apply additional logic to the search criteria. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapFirst(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TFilter : class + { + var first = new FirstEntityEndpoint(searchAction); + return builder.MapGet(pattern, first.First); + } + + /// + /// Maps a GET endpoint to a first DTO based on the applied filter. + /// + /// The type of the entity to be queried. + /// The type of the DTO to be returned. + /// The type of the applied filter. + /// The route builder. + /// The route pattern. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapSelectFirst(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern) + where TEntity : class + where TDto : class + where TFilter : class + { + var first = new FirstModelEndpoint(); + return builder.MapGet(pattern, first.First); + } + + /// + /// Maps a GET endpoint to a first DTO based on the applied filter. + /// + /// The type of the entity to be queried. + /// The type of the DTO to be returned. + /// The type of the applied filter. + /// The route builder. + /// The route pattern. + /// Action to apply additional logic to the search criteria. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapSelectFirst(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TDto : class + where TFilter : class + { + var first = new FirstModelEndpoint(searchAction); + return builder.MapGet(pattern, first.First); + } + + /// + /// Maps a GET endpoint to a first DTO based on the applied filter with an additional identifier. + /// + /// The type of the entity to be queried. + /// The type of the DTO to be returned. + /// The type of the applied filter. + /// The type of the first additional identifier. + /// The route builder. + /// The route pattern. + /// Action to apply additional logic to the search criteria. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapSelectFirst(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TDto : class + where TFilter : class + { + var first = new FirstModelEndpoint(searchAction); + return builder.MapGet(pattern, first.First); + } + + /// + /// Maps a GET endpoint to a first DTO based on the applied filter with two additional identifiers. + /// + /// The type of the entity to be queried. + /// The type of the DTO to be returned. + /// The type of the applied filter. + /// The type of the first additional identifier. + /// The type of the second additional identifier. + /// The route builder. + /// The route pattern. + /// Action to apply additional logic to the search criteria. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapSelectFirst(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TDto : class + where TFilter : class + { + var first = new FirstModelEndpoint(searchAction); + return builder.MapGet(pattern, first.First); + } + + /// + /// Maps a GET endpoint to a first DTO based on the applied filter with three additional identifiers. + /// + /// The type of the entity to be queried. + /// The type of the DTO to be returned. + /// The type of the applied filter. + /// The type of the first additional identifier. + /// The type of the second additional identifier. + /// The type of the third additional identifier. + /// The route builder. + /// The route pattern. + /// Action to apply additional logic to the search criteria. + /// A builder for additional endpoint configuration. + public static RouteHandlerBuilder MapSelectFirst(this IEndpointRouteBuilder builder, + [StringSyntax("Route")] string pattern, + Action> searchAction) + where TEntity : class + where TDto : class + where TFilter : class + { + var first = new FirstModelEndpoint(searchAction); + return builder.MapGet(pattern, first.First); + } + + #endregion +} diff --git a/src/RoyalCode.SmartSearch.AspNetCore/HttpResults/MatchFirst.cs b/src/RoyalCode.SmartSearch.AspNetCore/HttpResults/MatchFirst.cs new file mode 100644 index 0000000..ab5bc28 --- /dev/null +++ b/src/RoyalCode.SmartSearch.AspNetCore/HttpResults/MatchFirst.cs @@ -0,0 +1,128 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Http.Metadata; +using RoyalCode.SmartProblems; +using RoyalCode.SmartProblems.HttpResults; +using System.Net.Mime; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace RoyalCode.SmartSearch.AspNetCore.HttpResults; + +public class MatchFirst : IResult, INestedHttpResult, IEndpointMetadataProvider + where TModel : class +{ + /// + /// Conversão implícita de para . + /// + /// Resultado NoContent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator MatchFirst(NoContent result) => new(result); + + /// + /// Conversão implícita de para . + /// + /// Resultado Ok com lista de modelos. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator MatchFirst(Ok result) => new(result); + + /// + /// Conversão implícita de para . + /// + /// Resultado Ok com lista de modelos. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator MatchFirst(TModel result) => new(result); + + /// + /// Conversão implícita de para . + /// + /// Problema ocorrido na operação. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator MatchFirst(Problem problem) => new(problem); + + /// + /// Conversão implícita de para . + /// + /// Exceção lançada na operação. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator MatchFirst(Exception ex) => new(ex); + + /// + /// Cria um resultado de lista sem conteúdo (204). + /// + public MatchFirst() + { + Result = TypedResults.NoContent(); + } + + /// + /// Cria um resultado de lista a partir de uma lista de modelos (200). + /// + /// Modelo retornado. + public MatchFirst(TModel model) + { + Result = TypedResults.Ok(model); + } + + /// + /// Cria um resultado de lista a partir de uma exceção (500). + /// + /// Exceção lançada. + public MatchFirst(Exception ex) + { + MatchErrorResult match = ex; + Result = match; + } + + /// + /// Cria um resultado de lista a partir de um problema (400/500). + /// + /// Problema ocorrido. + public MatchFirst(Problem problem) + { + MatchErrorResult match = problem; + Result = match; + } + + /// + /// Cria um resultado de lista a partir de um resultado Ok com lista de modelos (200). + /// + /// Resultado Ok com lista de modelos. + public MatchFirst(Ok result) + { + Result = result; + } + + /// + /// Cria um resultado de lista a partir de um resultado NoContent (204). + /// + /// Resultado NoContent. + public MatchFirst(NoContent result) + { + Result = result; + } + + /// + /// Resultado HTTP encapsulado (Ok, NoContent, Problem, etc). + /// + public IResult Result { get; } + + /// + /// Adiciona metadados de resposta para documentação e OpenAPI. + /// + /// Método associado ao endpoint. + /// Construtor do endpoint. + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + { + OkMatch.PopulateMetadata(method, builder); + builder.Metadata.Add(new ResponseTypeMetadata(StatusCodes.Status204NoContent, MediaTypeNames.Application.Json)); + } + + /// + /// Executa o resultado HTTP no contexto da requisição. + /// + /// Contexto HTTP. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task ExecuteAsync(HttpContext httpContext) => Result.ExecuteAsync(httpContext); +} diff --git a/src/RoyalCode.SmartSearch.AspNetCore/HttpResults/MatchList.cs b/src/RoyalCode.SmartSearch.AspNetCore/HttpResults/MatchList.cs new file mode 100644 index 0000000..a363fa6 --- /dev/null +++ b/src/RoyalCode.SmartSearch.AspNetCore/HttpResults/MatchList.cs @@ -0,0 +1,132 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Http.Metadata; +using RoyalCode.SmartProblems; +using RoyalCode.SmartProblems.HttpResults; +using System.Net.Mime; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace RoyalCode.SmartSearch.AspNetCore.HttpResults; + +/// +/// +/// Representa um resultado HTTP para uma listagem de entidades ou DTOs em operações de busca. +/// +/// +/// Permite retornar resultados padronizados (200, 204, 400, 500) em Minimal APIs e Controllers, +/// encapsulando listas, erros e exceções de forma consistente. +/// +/// +/// Tipo do modelo dos itens retornados na lista. +public class MatchList : IResult, INestedHttpResult, IEndpointMetadataProvider + where TModel : class +{ + /// + /// Conversão implícita de para . + /// + /// Resultado NoContent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator MatchList(NoContent result) => new(result); + + /// + /// Conversão implícita de para . + /// + /// Resultado Ok com lista de modelos. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator MatchList(Ok> result) => new(result); + + /// + /// Conversão implícita de para . + /// + /// Problema ocorrido na operação. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator MatchList(Problem problem) => new(problem); + + /// + /// Conversão implícita de para . + /// + /// Exceção lançada na operação. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator MatchList(Exception ex) => new(ex); + + /// + /// Cria um resultado de lista sem conteúdo (204). + /// + public MatchList() + { + Result = TypedResults.NoContent(); + } + + /// + /// Cria um resultado de lista a partir de uma lista de modelos (200). + /// + /// Lista de modelos retornados. + public MatchList(IReadOnlyList result) + { + Result = TypedResults.Ok(result); + } + + /// + /// Cria um resultado de lista a partir de uma exceção (500). + /// + /// Exceção lançada. + public MatchList(Exception ex) + { + MatchErrorResult match = ex; + Result = match; + } + + /// + /// Cria um resultado de lista a partir de um problema (400/500). + /// + /// Problema ocorrido. + public MatchList(Problem problem) + { + MatchErrorResult match = problem; + Result = match; + } + + /// + /// Cria um resultado de lista a partir de um resultado Ok com lista de modelos (200). + /// + /// Resultado Ok com lista de modelos. + public MatchList(Ok> result) + { + Result = result; + } + + /// + /// Cria um resultado de lista a partir de um resultado NoContent (204). + /// + /// Resultado NoContent. + public MatchList(NoContent result) + { + Result = result; + } + + /// + /// Resultado HTTP encapsulado (Ok, NoContent, Problem, etc). + /// + public IResult Result { get; } + + /// + /// Adiciona metadados de resposta para documentação e OpenAPI. + /// + /// Método associado ao endpoint. + /// Construtor do endpoint. + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + { + builder.Metadata.Add(new ResponseTypeMetadata(typeof(IReadOnlyList), StatusCodes.Status200OK, MediaTypeNames.Application.Json)); + builder.Metadata.Add(new ResponseTypeMetadata(StatusCodes.Status204NoContent, MediaTypeNames.Application.Json)); + MatchErrorResult.PopulateMetadata(method, builder); + } + + /// + /// Executa o resultado HTTP no contexto da requisição. + /// + /// Contexto HTTP. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task ExecuteAsync(HttpContext httpContext) => Result.ExecuteAsync(httpContext); +} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.AspNetCore/HttpResults/MatchSearch.cs b/src/RoyalCode.SmartSearch.AspNetCore/HttpResults/MatchSearch.cs new file mode 100644 index 0000000..0781a89 --- /dev/null +++ b/src/RoyalCode.SmartSearch.AspNetCore/HttpResults/MatchSearch.cs @@ -0,0 +1,130 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.AspNetCore.Http.Metadata; +using RoyalCode.SmartProblems; +using RoyalCode.SmartProblems.HttpResults; +using System.Net.Mime; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace RoyalCode.SmartSearch.AspNetCore.HttpResults; + +/// +/// Representa um resultado HTTP para uma pesquisa paginada de entidades ou DTOs, incluindo informações de paginação e ordenação. +/// +/// Permite retornar resultados padronizados (200, 204, 400, 500) em Minimal APIs e Controllers, +/// encapsulando listas paginadas, erros e exceções de forma consistente. +/// +/// +/// Tipo do modelo dos itens retornados na pesquisa. +public class MatchSearch : IResult, INestedHttpResult, IEndpointMetadataProvider + where TModel : class +{ + /// + /// Conversão implícita de para . + /// + /// Resultado NoContent. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator MatchSearch(NoContent result) => new(result); + + /// + /// Conversão implícita de para . + /// + /// Resultado Ok com lista paginada de modelos. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator MatchSearch(Ok> result) => new(result); + + /// + /// Conversão implícita de para . + /// + /// Problema ocorrido na operação. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator MatchSearch(Problem problem) => new(problem); + + /// + /// Conversão implícita de para . + /// + /// Exceção lançada na operação. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator MatchSearch(Exception ex) => new(ex); + + /// + /// Cria um resultado de pesquisa sem conteúdo (204). + /// + public MatchSearch() + { + Result = TypedResults.NoContent(); + } + + /// + /// Cria um resultado de pesquisa a partir de uma lista paginada de modelos (200). + /// + /// Lista paginada de modelos retornados. + public MatchSearch(IResultList result) + { + Result = TypedResults.Ok(result); + } + + /// + /// Cria um resultado de pesquisa a partir de uma exceção (500). + /// + /// Exceção lançada. + public MatchSearch(Exception ex) + { + MatchErrorResult match = ex; + Result = match; + } + + /// + /// Cria um resultado de pesquisa a partir de um problema (400/500). + /// + /// Problema ocorrido. + public MatchSearch(Problem problem) + { + MatchErrorResult match = problem; + Result = match; + } + + /// + /// Cria um resultado de pesquisa a partir de um resultado Ok com lista paginada de modelos (200). + /// + /// Resultado Ok com lista paginada de modelos. + public MatchSearch(Ok> result) + { + Result = result; + } + + /// + /// Cria um resultado de pesquisa a partir de um resultado NoContent (204). + /// + /// Resultado NoContent. + public MatchSearch(NoContent result) + { + Result = result; + } + + /// + /// Resultado HTTP encapsulado (Ok, NoContent, Problem, etc). + /// + public IResult Result { get; } + + /// + /// Adiciona metadados de resposta para documentação e OpenAPI. + /// + /// Método associado ao endpoint. + /// Construtor do endpoint. + public static void PopulateMetadata(MethodInfo method, EndpointBuilder builder) + { + builder.Metadata.Add(new ResponseTypeMetadata(typeof(IResultList), StatusCodes.Status200OK, MediaTypeNames.Application.Json)); + builder.Metadata.Add(new ResponseTypeMetadata(StatusCodes.Status204NoContent, MediaTypeNames.Application.Json)); + MatchErrorResult.PopulateMetadata(method, builder); + } + + /// + /// Executa o resultado HTTP no contexto da requisição. + /// + /// Contexto HTTP. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Task ExecuteAsync(HttpContext httpContext) => Result.ExecuteAsync(httpContext); +} diff --git a/src/RoyalCode.SmartSearch.AspNetCore/HttpResults/ResponseTypeMetadata.cs b/src/RoyalCode.SmartSearch.AspNetCore/HttpResults/ResponseTypeMetadata.cs new file mode 100644 index 0000000..0d612ac --- /dev/null +++ b/src/RoyalCode.SmartSearch.AspNetCore/HttpResults/ResponseTypeMetadata.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Http.Metadata; +using System.Net.Mime; + +namespace RoyalCode.SmartSearch.AspNetCore.HttpResults; + +/// +/// Metadados de resposta para endpoints HTTP, utilizados para documentação e geração de OpenAPI/Swagger. +/// +internal sealed class ResponseTypeMetadata : IProducesResponseTypeMetadata +{ + /// + /// Inicializa uma nova instância de com tipo de retorno, status e tipos de conteúdo. + /// + /// Tipo do objeto retornado. + /// Código de status HTTP. + /// Tipos de conteúdo suportados (ex: application/json). + public ResponseTypeMetadata(Type? type, int statusCode, params string[]? contentTypes) + { + Type = type; + StatusCode = statusCode; + ContentTypes = contentTypes ?? [MediaTypeNames.Application.Json]; + } + /// + /// Inicializa uma nova instância de apenas com status e tipos de conteúdo. + /// + /// Código de status HTTP. + /// Tipos de conteúdo suportados (ex: application/json). + public ResponseTypeMetadata(int statusCode, params string[]? contentTypes) + { + StatusCode = statusCode; + ContentTypes = contentTypes ?? [MediaTypeNames.Application.Json]; + } + /// + /// Tipo do objeto retornado na resposta HTTP. + /// + public Type? Type { get; } + /// + /// Código de status HTTP da resposta. + /// + public int StatusCode { get; } + /// + /// Tipos de conteúdo suportados pela resposta. + /// + public IEnumerable ContentTypes { get; } +} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.AspNetCore/Internals/FirstEntityEndpoint.cs b/src/RoyalCode.SmartSearch.AspNetCore/Internals/FirstEntityEndpoint.cs new file mode 100644 index 0000000..9e78ff1 --- /dev/null +++ b/src/RoyalCode.SmartSearch.AspNetCore/Internals/FirstEntityEndpoint.cs @@ -0,0 +1,293 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using RoyalCode.SmartProblems; +using RoyalCode.SmartSearch.AspNetCore.HttpResults; +using RoyalCode.SmartSearch.Exceptions; + +namespace RoyalCode.SmartSearch.AspNetCore.Internals; + +/// +/// Endpoint to get the first item in the entity that meets the filter and sort criteria. +/// +/// The type of entity to be queried. +/// The type of filter applied. +public class FirstEntityEndpoint + where TEntity : class + where TFilter : class +{ + private readonly Action>? searchAction; + + /// + /// Initializes a new instance of . + /// + /// Custom action to configure the search. + public FirstEntityEndpoint(Action>? searchAction = null) + { + this.searchAction = searchAction; + } + + /// + /// Executes the search for the first item in the entity that meets the filter and sort criteria. + /// + /// Filter object with search criteria. + /// Sorting criteria. + /// Search service for the entity. + /// Cancellation token. + /// HTTP 200 result with the item, 204 if not found, or 400 in case of sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> First( + [AsParameters] TFilter filtro, + [FromQuery] Sorting[]? orderby, + ICriteria searchable, + CancellationToken ct) + { + try + { + var search = searchable + .OrderBy(orderby) + .FilterBy(filtro); + + if (searchAction is not null) + searchAction(search); + + var entity = await search.FirstOrDefaultAsync(ct); + + if (entity is null) + { + return TypedResults.NoContent(); + } + + return entity; + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return Problems.InternalError(ex); + } + } +} + +/// +/// Endpoint to get the first entity item with additional identifier. +/// +/// The type of entity to be queried. +/// The type of the applied filter. +/// The type of the additional identifier. +public class FirstEntityEndpoint + where TEntity : class + where TFilter : class +{ + private readonly Action>? searchAction; + + /// + /// Initializes a new instance of . + /// + /// Custom action to configure the search with identifier. + public FirstEntityEndpoint(Action>? searchAction = null) + { + this.searchAction = searchAction; + } + + /// + /// Executes the search for the first item in the entity with additional identifier. + /// + /// Additional identifier for the search. + /// Filter object with search criteria. + /// Sorting criteria. + /// Search service for the entity. + /// Cancellation token. + /// HTTP 200 result with the item, 204 if not found, or 400 in case of sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> First( + [FromRoute] TId id, + [AsParameters] TFilter filtro, + [FromQuery] Sorting[]? orderby, + ICriteria searchable, + CancellationToken ct) + { + try + { + var search = searchable + .OrderBy(orderby) + .FilterBy(filtro); + + if (searchAction is not null) + searchAction(id, search); + + var entity = await search.FirstOrDefaultAsync(ct); + + if (entity is null) + { + return TypedResults.NoContent(); + } + + return entity; + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return Problems.InternalError(ex); + } + } +} + +/// +/// Endpoint to get the first entity item with two additional identifiers. +/// +/// The type of entity to be queried. +/// The type of the applied filter. +/// The type of the first additional identifier. +/// The type of the second additional identifier. +public class FirstEntityEndpoint + where TEntity : class + where TFilter : class +{ + private readonly Action>? searchAction; + + /// + /// Initializes a new instance of . + /// + /// Custom action to configure the search with identifiers. + public FirstEntityEndpoint(Action>? searchAction = null) + { + this.searchAction = searchAction; + } + + /// + /// Executes the search for the first item in the entity with two additional identifiers. + /// + /// Additional primary identifier for the search. + /// Additional identifier, related to the first, for the search. + /// Filter object with search criteria. + /// Sorting criteria. + /// Search service for the entity. + /// Cancellation token. + /// HTTP 200 result with the item, 204 if not found, or 400 in case of sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> First( + [FromRoute] TId1 id, + [FromRoute] TId2 relatedId, + [AsParameters] TFilter filtro, + [FromQuery] Sorting[]? orderby, + ICriteria searchable, + CancellationToken ct) + { + try + { + var search = searchable + .OrderBy(orderby) + .FilterBy(filtro); + + if (searchAction is not null) + searchAction(id, relatedId, search); + + var entity = await search.FirstOrDefaultAsync(ct); + + if (entity is null) + { + return TypedResults.NoContent(); + } + + return entity; + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return Problems.InternalError(ex); + } + } +} + +/// +/// Endpoint to get the first entity item with three additional identifiers. +/// +/// The type of entity to be queried. +/// The type of the applied filter. +/// The type of the first additional identifier. +/// The type of the second additional identifier. +/// The type of the third additional identifier. +public class FirstEntityEndpoint + where TEntity : class + where TFilter : class +{ + private readonly Action>? searchAction; + + /// + /// Initializes a new instance of . + /// + /// Custom action to configure the search with identifiers. + public FirstEntityEndpoint(Action>? searchAction = null) + { + this.searchAction = searchAction; + } + + /// + /// Executes the search for the first item in the entity with three additional identifiers. + /// + /// Additional primary identifier for the search. + /// Additional identifier, related to the first, for the search. + /// Third additional identifier for the search. + /// Filter object with search criteria. + /// Sorting criteria. + /// Search service for the entity. + /// Cancellation token. + /// HTTP 200 result with the item, 204 if not found, or 400 in case of sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> First( + [FromRoute] TId1 id, + [FromRoute] TId2 relatedId, + [FromRoute] TId3 subRelatedId, + [AsParameters] TFilter filtro, + [FromQuery] Sorting[]? orderby, + ICriteria searchable, + CancellationToken ct) + { + try + { + var search = searchable + .OrderBy(orderby) + .FilterBy(filtro); + + if (searchAction is not null) + searchAction(id, relatedId, subRelatedId, search); + + var entity = await search.FirstOrDefaultAsync(ct); + + if (entity is null) + { + return TypedResults.NoContent(); + } + + return entity; + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return Problems.InternalError(ex); + } + } +} diff --git a/src/RoyalCode.SmartSearch.AspNetCore/Internals/FirstModelEndpoint.cs b/src/RoyalCode.SmartSearch.AspNetCore/Internals/FirstModelEndpoint.cs new file mode 100644 index 0000000..06bd6cf --- /dev/null +++ b/src/RoyalCode.SmartSearch.AspNetCore/Internals/FirstModelEndpoint.cs @@ -0,0 +1,301 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using RoyalCode.SmartProblems; +using RoyalCode.SmartSearch.AspNetCore.HttpResults; +using RoyalCode.SmartSearch.Exceptions; + +namespace RoyalCode.SmartSearch.AspNetCore.Internals; + +/// +/// Endpoint to get the first item in the entity that meets the filter and sort criteria. +/// +/// The type of entity to be queried. +/// The type of DTO to be returned. +/// The type of filter applied. +public class FirstModelEndpoint + where TEntity : class + where TDto : class + where TFilter : class +{ + private readonly Action>? searchAction; + + /// + /// Initializes a new instance of . + /// + /// Custom action to configure the search. + public FirstModelEndpoint(Action>? searchAction = null) + { + this.searchAction = searchAction; + } + + /// + /// Executes the search for the first item in the entity that meets the filter and sort criteria. + /// + /// Filter object with search criteria. + /// Sorting criteria. + /// Search service for the entity. + /// Cancellation token. + /// HTTP 200 result with the item, 204 if not found, or 400 in case of sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> First( + [AsParameters] TFilter filtro, + [FromQuery] Sorting[]? orderby, + ICriteria searchable, + CancellationToken ct) + { + try + { + var search = searchable + .OrderBy(orderby) + .FilterBy(filtro); + + if (searchAction is not null) + searchAction(search); + + var dto = await search.Select().FirstOrDefaultAsync(ct); + + if (dto is null) + { + return TypedResults.NoContent(); + } + + return dto; + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return Problems.InternalError(ex); + } + } +} + +/// +/// Endpoint to get the first entity item with additional identifier. +/// +/// The type of entity to be queried. +/// The type of DTO to be returned. +/// The type of the applied filter. +/// The type of the additional identifier. +public class FirstModelEndpoint + where TEntity : class + where TDto : class + where TFilter : class +{ + private readonly Action>? searchAction; + + /// + /// Initializes a new instance of . + /// + /// Custom action to configure the search with identifier. + public FirstModelEndpoint(Action>? searchAction = null) + { + this.searchAction = searchAction; + } + + /// + /// Executes the search for the first item in the entity with additional identifier. + /// + /// Additional identifier for the search. + /// Filter object with search criteria. + /// Sorting criteria. + /// Search service for the entity. + /// Cancellation token. + /// HTTP 200 result with the item, 204 if not found, or 400 in case of sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> First( + [FromRoute] TId id, + [AsParameters] TFilter filtro, + [FromQuery] Sorting[]? orderby, + ICriteria searchable, + CancellationToken ct) + { + try + { + var search = searchable + .OrderBy(orderby) + .FilterBy(filtro); + + if (searchAction is not null) + searchAction(id, search); + + var dto = await search.Select().FirstOrDefaultAsync(ct); + + if (dto is null) + { + return TypedResults.NoContent(); + } + + return dto; + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return Problems.InternalError(ex); + } + } +} + +/// +/// Endpoint to get the first entity item with two additional identifiers. +/// +/// The type of entity to be queried. +/// The type of DTO to be returned. +/// The type of the applied filter. +/// The type of the first additional identifier. +/// The type of the second additional identifier. +public class FirstModelEndpoint + where TEntity : class + where TDto : class + where TFilter : class +{ + private readonly Action>? searchAction; + + /// + /// Initializes a new instance of . + /// + /// Custom action to configure the search with identifiers. + public FirstModelEndpoint(Action>? searchAction = null) + { + this.searchAction = searchAction; + } + + /// + /// Executes the search for the first item in the entity with two additional identifiers. + /// + /// Additional primary identifier for the search. + /// Additional identifier, related to the first, for the search. + /// Filter object with search criteria. + /// Sorting criteria. + /// Search service for the entity. + /// Cancellation token. + /// HTTP 200 result with the item, 204 if not found, or 400 in case of sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> First( + [FromRoute] TId1 id, + [FromRoute] TId2 relatedId, + [AsParameters] TFilter filtro, + [FromQuery] Sorting[]? orderby, + ICriteria searchable, + CancellationToken ct) + { + try + { + var search = searchable + .OrderBy(orderby) + .FilterBy(filtro); + + if (searchAction is not null) + searchAction(id, relatedId, search); + + var dto = await search.Select().FirstOrDefaultAsync(ct); + + if (dto is null) + { + return TypedResults.NoContent(); + } + + return dto; + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return Problems.InternalError(ex); + } + } +} + +/// +/// Endpoint to get the first entity item with three additional identifiers. +/// +/// The type of entity to be queried. +/// The type of DTO to be returned. +/// The type of the applied filter. +/// The type of the first additional identifier. +/// The type of the second additional identifier. +/// The type of the third additional identifier. +public class FirstModelEndpoint + where TEntity : class + where TDto : class + where TFilter : class +{ + private readonly Action>? searchAction; + + /// + /// Initializes a new instance of . + /// + /// Custom action to configure the search with identifiers. + public FirstModelEndpoint(Action>? searchAction = null) + { + this.searchAction = searchAction; + } + + /// + /// Executes the search for the first item in the entity with three additional identifiers. + /// + /// Additional primary identifier for the search. + /// Additional identifier, related to the first, for the search. + /// Third additional identifier for the search. + /// Filter object with search criteria. + /// Sorting criteria. + /// Search service for the entity. + /// Cancellation token. + /// HTTP 200 result with the item, 204 if not found, or 400 in case of sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> First( + [FromRoute] TId1 id, + [FromRoute] TId2 relatedId, + [FromRoute] TId3 subRelatedId, + [AsParameters] TFilter filtro, + [FromQuery] Sorting[]? orderby, + ICriteria searchable, + CancellationToken ct) + { + try + { + var search = searchable + .OrderBy(orderby) + .FilterBy(filtro); + + if (searchAction is not null) + searchAction(id, relatedId, subRelatedId, search); + + var dto = await search.Select().FirstOrDefaultAsync(ct); + + if (dto is null) + { + return TypedResults.NoContent(); + } + + return dto; + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return Problems.InternalError(ex); + } + } +} diff --git a/src/RoyalCode.SmartSearch.AspNetCore/Internals/ListEntityEndpoint.cs b/src/RoyalCode.SmartSearch.AspNetCore/Internals/ListEntityEndpoint.cs new file mode 100644 index 0000000..a22bc08 --- /dev/null +++ b/src/RoyalCode.SmartSearch.AspNetCore/Internals/ListEntityEndpoint.cs @@ -0,0 +1,211 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using RoyalCode.SmartProblems; +using RoyalCode.SmartSearch.AspNetCore.HttpResults; +using RoyalCode.SmartSearch.Exceptions; + +namespace RoyalCode.SmartSearch.AspNetCore.Internals; + +/// +/// Endpoint for filtered and sorted listing of Entities. +/// +/// The type of entity to be queried. +/// The type of filter applied. +public class ListEntityEndpoint + where TEntity : class + where TFilter : class +{ + private readonly Action>? listAction; + + /// + /// Initializes a new instance of . + /// + /// Action to customize the listing criteria. + public ListEntityEndpoint(Action>? listAction = null) + { + this.listAction = listAction; + } + + /// + /// Performs a filtered and sorted list of Entities. + /// + /// A filter object with search criteria. + /// Ordering criteria. + /// A search service for the entity. + /// The cancellation token. + /// HTTP 200 result with list, 204 if empty, or 400 on sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> List( + [AsParameters] TFilter filtro, + [FromQuery] Sorting[]? orderby, + ICriteria searchable, + CancellationToken ct) + { + try + { + var search = searchable + .OrderBy(orderby) + .FilterBy(filtro); + + if (listAction is not null) + listAction(search); + + var result = await search.AsSearch().ToListAsync(ct); + + if (result.Count == 0) + return TypedResults.NoContent(); + + return TypedResults.Ok(result.Items); + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return ex; + } + } +} + +/// +/// Endpoint for filtered and sorted listing of entities with additional identifier. +/// +/// The type of entity to be queried. +/// The type of filter applied. +/// The type of the additional identifier. +public class ListEntityEndpoint + where TEntity : class + where TFilter : class +{ + private readonly Action>? listAction; + + /// + /// Initializes a new instance of . + /// + /// Action to customize the listing criteria with an additional identifier. + public ListEntityEndpoint(Action>? listAction = null) + { + this.listAction = listAction; + } + + /// + /// Performs a filtered and sorted list of entities with an additional identifier. + /// + /// The additional identifier for the list. + /// A filter object with search criteria. + /// Ordering criteria. + /// A search service for the entity. + /// The cancellation token. + /// HTTP 200 result with list, 204 if empty, or 400 on sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> List( + [FromRoute] TId id, + [AsParameters] TFilter filtro, + [FromQuery] Sorting[]? orderby, + ICriteria searchable, + CancellationToken ct) + { + try + { + var search = searchable + .OrderBy(orderby) + .FilterBy(filtro); + + if (listAction is not null) + listAction(id, search); + + var result = await search.AsSearch().ToListAsync(ct); + + if (result.Count == 0) + return TypedResults.NoContent(); + + return TypedResults.Ok(result.Items); + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return ex; + } + } +} + +/// +/// Endpoint for filtered and sorted listing of entities with two additional identifiers. +/// +/// The type of entity to be queried. +/// The type of filter applied. +/// The type of the first additional identifier. +/// The type of the second additional identifier. +public class ListEntityEndpoint + where TEntity : class + where TFilter : class +{ + private readonly Action>? listAction; + + /// + /// Initializes a new instance of . + /// + /// Action to customize the listing criteria with two additional identifiers. + public ListEntityEndpoint(Action>? listAction = null) + { + this.listAction = listAction; + } + + /// + /// Performs a filtered and sorted list of entities with two additional identifiers. + /// + /// The first additional identifier for the list. + /// The second additional identifier for the list. + /// A filter object with search criteria. + /// Ordering criteria. + /// A search service for the entity. + /// The cancellation token. + /// HTTP 200 result with list, 204 if empty, or 400 on sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> List( + [FromRoute] TId1 id, + [FromRoute] TId2 relatedId, + [AsParameters] TFilter filtro, + [FromQuery] Sorting[]? orderby, + ICriteria searchable, + CancellationToken ct) + { + try + { + var search = searchable + .OrderBy(orderby) + .FilterBy(filtro); + + if (listAction is not null) + listAction(id, relatedId, search); + + var result = await search.AsSearch().ToListAsync(ct); + + if (result.Count == 0) + return TypedResults.NoContent(); + + return TypedResults.Ok(result.Items); + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return ex; + } + } +} diff --git a/src/RoyalCode.SmartSearch.AspNetCore/Internals/ListModelEndpoint.cs b/src/RoyalCode.SmartSearch.AspNetCore/Internals/ListModelEndpoint.cs new file mode 100644 index 0000000..5ec43d0 --- /dev/null +++ b/src/RoyalCode.SmartSearch.AspNetCore/Internals/ListModelEndpoint.cs @@ -0,0 +1,220 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using RoyalCode.SmartProblems; +using RoyalCode.SmartSearch.AspNetCore.HttpResults; +using RoyalCode.SmartSearch.Exceptions; + +namespace RoyalCode.SmartSearch.AspNetCore.Internals; + +/// +/// Endpoint for filtered and sorted listing of DTOs. +/// +/// The type of entity to be queried. +/// The type of DTO to be returned. +/// The type of filter applied. +public class ListModelEndpoint + where TEntity : class + where TDto : class + where TFilter : class +{ + private readonly Action>? listAction; + + /// + /// Initializes a new instance of . + /// + /// Action to customize the listing criteria. + public ListModelEndpoint(Action>? listAction = null) + { + this.listAction = listAction; + } + + /// + /// Performs a filtered and sorted list of DTOs. + /// + /// A filter object with search criteria. + /// Ordering criteria. + /// A search service for the entity. + /// The cancellation token. + /// HTTP 200 result with list, 204 if empty, or 400 on sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> List( + [AsParameters] TFilter filtro, + [FromQuery] Sorting[]? orderby, + ICriteria searchable, + CancellationToken ct) + { + try + { + var search = searchable + .OrderBy(orderby) + .FilterBy(filtro); + + if (listAction is not null) + listAction(search); + + var select = search.Select(); + var result = await select.ToListAsync(ct); + + if (result.Count == 0) + return TypedResults.NoContent(); + + return TypedResults.Ok(result.Items); + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return ex; + } + } +} + +/// +/// Endpoint for filtered and sorted listing of DTOs with additional identifier. +/// +/// The type of entity to be queried. +/// The type of DTO to be returned. +/// The type of filter applied. +/// The type of the additional identifier. +public class ListModelEndpoint + where TEntity : class + where TDto : class + where TFilter : class +{ + private readonly Action>? listAction; + + /// + /// Initializes a new instance of . + /// + /// Action to customize the listing criteria with an additional identifier. + public ListModelEndpoint(Action>? listAction = null) + { + this.listAction = listAction; + } + + /// + /// Performs a filtered and sorted list of DTOs with an additional identifier. + /// + /// The additional identifier for the list. + /// A filter object with search criteria. + /// Ordering criteria. + /// A search service for the entity. + /// The cancellation token. + /// HTTP 200 result with list, 204 if empty, or 400 on sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> List( + [FromRoute] TId id, + [AsParameters] TFilter filtro, + [FromQuery] Sorting[]? orderby, + ICriteria searchable, + CancellationToken ct) + { + try + { + var search = searchable + .OrderBy(orderby) + .FilterBy(filtro); + + if (listAction is not null) + listAction(id, search); + + var select = search.Select(); + var result = await select.ToListAsync(ct); + + if (result.Count == 0) + return TypedResults.NoContent(); + + return TypedResults.Ok(result.Items); + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return ex; + } + } +} + +/// +/// Endpoint for filtered and sorted listing of DTOs with two additional identifiers. +/// +/// The type of entity to be queried. +/// The type of DTO to be returned. +/// The type of filter applied. +/// The type of the first additional identifier. +/// The type of the second additional identifier. +public class ListModelEndpoint + where TEntity : class + where TDto : class + where TFilter : class +{ + private readonly Action>? listAction; + + /// + /// Initializes a new instance of . + /// + /// Action to customize the listing criteria with two additional identifiers. + public ListModelEndpoint(Action>? listAction = null) + { + this.listAction = listAction; + } + + /// + /// Performs a filtered and sorted list of DTOs with two additional identifiers. + /// + /// The first additional identifier for the list. + /// The second additional identifier for the list. + /// A filter object with search criteria. + /// Ordering criteria. + /// A search service for the entity. + /// The cancellation token. + /// HTTP 200 result with list, 204 if empty, or 400 on sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> List( + [FromRoute] TId1 id, + [FromRoute] TId2 relatedId, + [AsParameters] TFilter filtro, + [FromQuery] Sorting[]? orderby, + ICriteria searchable, + CancellationToken ct) + { + try + { + var search = searchable + .OrderBy(orderby) + .FilterBy(filtro); + + if (listAction is not null) + listAction(id, relatedId, search); + + var select = search.Select(); + var result = await select.ToListAsync(ct); + + if (result.Count == 0) + return TypedResults.NoContent(); + + return TypedResults.Ok(result.Items); + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return ex; + } + } +} diff --git a/src/RoyalCode.SmartSearch.AspNetCore/Internals/SearchEntityEndpoint.cs b/src/RoyalCode.SmartSearch.AspNetCore/Internals/SearchEntityEndpoint.cs new file mode 100644 index 0000000..3a43877 --- /dev/null +++ b/src/RoyalCode.SmartSearch.AspNetCore/Internals/SearchEntityEndpoint.cs @@ -0,0 +1,226 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using RoyalCode.SmartProblems; +using RoyalCode.SmartSearch.AspNetCore.HttpResults; +using RoyalCode.SmartSearch.Exceptions; + +namespace RoyalCode.SmartSearch.AspNetCore.Internals; + +/// +/// It allows you to customize the paged, sorted and filtered search for entities, applying additional logic via delegate. +/// +/// The type of the entity to be queried. +/// The type of the applied filter. +public class SearchEntityEndpoint + where TEntity : class + where TFilter : class +{ + private readonly Action>? searchAction; + + /// + /// Initializes a new instance of . + /// + /// Custom action to configure the search. + public SearchEntityEndpoint(Action>? searchAction = null) + { + this.searchAction = searchAction; + } + + /// + /// Executes the custom search, applying filters, sorting, pagination, and additional logic, returning paginated entities. + /// + /// Filter object with search criteria. + /// Pagination and counting parameters. + /// Sorting criteria. + /// Search service for the entity. + /// Cancellation token. + /// HTTP 200 result with paginated list, 204 if empty, or 400 on sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> Search( + [AsParameters] TFilter filtro, + [AsParameters] SearchOptions options, + [FromQuery] Sorting[]? orderby, + ICriteria searchable, + CancellationToken ct) + { + try + { + var search = searchable + .WithOptions(options) + .OrderBy(orderby) + .FilterBy(filtro); + + if (searchAction != null) + searchAction(search); + + var result = await search.AsSearch().ToListAsync(ct); + + if (result.Count == 0) + { + return TypedResults.NoContent(); + } + + return TypedResults.Ok(result); + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return ex; + } + } +} + +/// +/// It allows you to customize the paginated, sorted and filtered search for entities, using an additional identifier and custom logic. +/// +/// The type of the entity to be queried. +/// The type of the applied filter. +/// The type of the additional identifier. +public class SearchEntityEndpoint + where TEntity : class + where TFilter : class +{ + private readonly Action>? searchAction; + + /// + /// Initializes a new instance of . + /// + /// Custom action to configure the search with identifier. + public SearchEntityEndpoint(Action>? searchAction = null) + { + this.searchAction = searchAction; + } + + /// + /// Executes the custom search, applying filters, sorting, pagination, and additional logic, returning paginated entities. + /// + /// Additional identifier for the search. + /// Filter object with search criteria. + /// Pagination and counting parameters. + /// Sorting criteria. + /// Search service for the entity. + /// Cancellation token. + /// HTTP 200 result with paginated list, 204 if empty, or 400 on sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> Search( + TId id, + [AsParameters] TFilter filtro, + [AsParameters] SearchOptions options, + [FromQuery] Sorting[]? orderby, + ICriteria searchable, + CancellationToken ct) + { + try + { + var search = searchable + .WithOptions(options) + .OrderBy(orderby) + .FilterBy(filtro); + + if (searchAction != null) + searchAction(id, search); + + var result = await search.AsSearch().ToListAsync(ct); + + if (result.Count == 0) + { + return TypedResults.NoContent(); + } + + return TypedResults.Ok(result); + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return ex; + } + } +} + +/// +/// It allows you to customize the paginated, sorted and filtered search for entities, using two additional identifiers and custom logic. +/// +/// The type of the entity to be queried. +/// The type of the applied filter. +/// The type of the first additional identifier. +/// The type of the second additional identifier. +public class SearchEntityEndpoint + where TEntity : class + where TFilter : class +{ + private readonly Action>? searchAction; + + /// + /// Initializes a new instance of . + /// + /// Custom action to configure the search with identifiers. + public SearchEntityEndpoint(Action>? searchAction = null) + { + this.searchAction = searchAction; + } + + /// + /// Executes the custom search, applying filters, sorting, pagination, and additional logic, returning paginated entities. + /// + /// The first additional identifier for the search. + /// The second additional identifier for the search. + /// Filter object with search criteria. + /// Pagination and counting parameters. + /// Sorting criteria. + /// Search service for the entity. + /// Cancellation token. + /// HTTP 200 result with paginated list, 204 if empty, or 400 on sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> Search( + TId1 id, + TId2 relatedId, + [AsParameters] TFilter filtro, + [AsParameters] SearchOptions options, + [FromQuery] Sorting[]? orderby, + ICriteria searchable, + CancellationToken ct) + { + try + { + var search = searchable + .WithOptions(options) + .OrderBy(orderby) + .FilterBy(filtro); + + if (searchAction != null) + searchAction(id, relatedId, search); + + var result = await search.AsSearch().ToListAsync(ct); + + if (result.Count == 0) + { + return TypedResults.NoContent(); + } + + return TypedResults.Ok(result); + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return ex; + } + } +} diff --git a/src/RoyalCode.SmartSearch.AspNetCore/Internals/SearchModelEndpoint.cs b/src/RoyalCode.SmartSearch.AspNetCore/Internals/SearchModelEndpoint.cs new file mode 100644 index 0000000..ae217cd --- /dev/null +++ b/src/RoyalCode.SmartSearch.AspNetCore/Internals/SearchModelEndpoint.cs @@ -0,0 +1,236 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using RoyalCode.SmartProblems; +using RoyalCode.SmartSearch.AspNetCore.HttpResults; +using RoyalCode.SmartSearch.Exceptions; + +namespace RoyalCode.SmartSearch.AspNetCore.Internals; + +/// +/// It allows you to customize the paginated, sorted and filtered search of entities, with projection to DTO, applying additional logic via delegate. +/// +/// The type of the entity to be queried. +/// The type of the DTO to be returned. +/// The type of the applied filter. +public class SearchModelEndpoint + where TEntity : class + where TDto : class + where TFilter : class +{ + private readonly Action>? searchAction; + + /// + /// Initializes a new instance of . + /// + /// Custom action to configure the search. + public SearchModelEndpoint(Action>? searchAction = null) + { + this.searchAction = searchAction; + } + + /// + /// Performs customized searches, applying filters, sorting, pagination and additional logic, returning paginated DTOs. + /// + /// Filter object with search criteria. + /// Paging and counting parameters. + /// Sorting criteria. + /// Search service for the entity. + /// Cancellation token. + /// HTTP result 200 with paginated list, 204 if empty, or 400 in sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> Search( + [AsParameters] TFilter filtro, + [AsParameters] SearchOptions options, + [FromQuery] Sorting[]? orderby, + ICriteria criteria, + CancellationToken ct) + { + try + { + var search = criteria + .WithOptions(options) + .OrderBy(orderby) + .FilterBy(filtro); + + if (searchAction is not null) + searchAction(search); + + var select = search.Select(); + + var result = await select.ToListAsync(ct); + + if (result.Count == 0) + { + return TypedResults.NoContent(); + } + + return TypedResults.Ok(result); + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return ex; + } + } +} + +/// +/// It allows you to customize the paginated, sorted and filtered search for entities, with projection to DTO, using an additional identifier and custom logic. +/// +/// The type of the entity to be queried. +/// The type of the DTO to be returned. +/// The type of the applied filter. +/// The type of the additional identifier. +public class SearchModelEndpoint + where TEntity : class + where TDto : class + where TFilter : class +{ + private readonly Action> searchAction; + + /// + /// Initializes a new instance of . + /// + /// Custom action to configure the search with identifier. + public SearchModelEndpoint(Action> searchAction) + { + this.searchAction = searchAction; + } + + /// + /// Executes the customized search, applying filters, sorting, pagination and additional logic, returning paginated DTOs. + /// + /// The additional identifier for the search. + /// Filter object with search criteria. + /// Paging and counting parameters. + /// Sorting criteria. + /// Search service for the entity. + /// Cancellation token. + /// HTTP result 200 with paginated list, 204 if empty, or 400 in sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> Search( + [FromRoute] TId id, + [AsParameters] TFilter filtro, + [AsParameters] SearchOptions options, + [FromQuery] Sorting[]? orderby, + ICriteria criteria, + CancellationToken ct) + { + try + { + var search = criteria + .WithOptions(options) + .OrderBy(orderby) + .FilterBy(filtro); + + searchAction(id, search); + + var select = search.Select(); + + var result = await select.ToListAsync(ct); + + if (result.Count == 0) + { + return TypedResults.NoContent(); + } + + return TypedResults.Ok(result); + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return ex; + } + } +} + +/// +/// It allows you to customize the paginated, sorted and filtered search for entities, with projection to DTO, using an additional identifier and custom logic. +/// +/// The type of the entity to be queried. +/// The type of the DTO to be returned. +/// The type of the applied filter. +/// The type of the first additional identifier. +/// The type of the second additional identifier. +public class SearchModelEndpoint + where TEntity : class + where TDto : class + where TFilter : class +{ + private readonly Action> searchAction; + + /// + /// Initializes a new instance of . + /// + /// Custom action to configure the search with identifiers. + public SearchModelEndpoint(Action> searchAction) + { + this.searchAction = searchAction; + } + + /// + /// Executes the customized search, applying filters, sorting, pagination and additional logic, returning paginated DTOs. + /// + /// The primary additional identifier for the search. + /// The additional identifier, related to the first, for the search. + /// Filter object with search criteria. + /// Paging and counting parameters. + /// Sorting criteria. + /// Search service for the entity. + /// Cancellation token. + /// HTTP result 200 with paginated list, 204 if empty, or 400 in sorting error. + [ProduceProblems(ProblemCategory.InvalidParameter, ProblemCategory.InternalServerError)] + public async Task> Search( + [FromRoute] TId1 id, + [FromRoute] TId2 relatedId, + [AsParameters] TFilter filtro, + [AsParameters] SearchOptions options, + [FromQuery] Sorting[]? orderby, + ICriteria searchable, + CancellationToken ct) + { + try + { + var search = searchable + .WithOptions(options) + .OrderBy(orderby) + .FilterBy(filtro); + + searchAction(id, relatedId, search); + + var select = search.Select(); + + var result = await select.ToListAsync(ct); + + if (result.Count == 0) + { + return TypedResults.NoContent(); + } + + return TypedResults.Ok(result); + } + catch (OrderByException obex) + { + return Problems.InvalidParameter(obex.Message, nameof(orderby)) + .With("propertyName", obex.PropertyName) + .With("typeName", obex.TypeName) + .With("orderby", orderby); + } + catch (Exception ex) + { + return ex; + } + } +} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.AspNetCore/RoyalCode.SmartSearch.AspNetCore.csproj b/src/RoyalCode.SmartSearch.AspNetCore/RoyalCode.SmartSearch.AspNetCore.csproj new file mode 100644 index 0000000..591df40 --- /dev/null +++ b/src/RoyalCode.SmartSearch.AspNetCore/RoyalCode.SmartSearch.AspNetCore.csproj @@ -0,0 +1,21 @@ + + + + + + $(LibTargets) + + + + + + + + + + + + + + + diff --git a/src/RoyalCode.SmartSearch.Core/Defaults/Criteria.cs b/src/RoyalCode.SmartSearch.Core/Defaults/Criteria.cs new file mode 100644 index 0000000..c26e53c --- /dev/null +++ b/src/RoyalCode.SmartSearch.Core/Defaults/Criteria.cs @@ -0,0 +1,155 @@ + +using RoyalCode.SmartSearch.Mappings; +using RoyalCode.SmartSearch.Services; +using System.Linq.Expressions; + +namespace RoyalCode.SmartSearch.Defaults; + +/// +/// Default implementation of the interface. +/// +/// The entity type. +public class Criteria : ICriteria + where TEntity : class +{ + private readonly ICriteriaPerformer performer; + + /// + /// + /// Creates a new with the service to perform the criteria. + /// + /// + /// + /// A service capable of accessing the database and running a query using the Criteria options. + /// + public Criteria(ICriteriaPerformer performer) + { + this.performer = performer; + } + + private readonly CriteriaOptions options = new(); + + /// + public ICriteria UsePages(int itemsPerPage = 10, int pageNumber = 1) + { + options.ItemsPerPage = itemsPerPage; + options.Page = pageNumber; + return this; + } + + /// + public ICriteria FetchPage(int pageNumber) + { + options.Page = pageNumber; + return this; + } + + /// + public ICriteria Skip(int skip) + { + options.Skip = skip; + return this; + } + + /// + public ICriteria Take(int take) + { + options.Take = take; + return this; + } + + /// + public ICriteria SkipTake(int skip, int take) + { + options.Skip = skip; + options.Take = take; + return this; + } + + /// + public ICriteria UseLastCount(int lastCount) + { + options.LastCount = lastCount; + options.UseCount = true; + return this; + } + + /// + public ICriteria UseCount(bool useCount = true) + { + options.UseCount = useCount; + return this; + } + + /// + public ICriteria FilterBy(TFilter filter) + where TFilter : class + { + options.AddFilter(filter); + return this; + } + + /// + public ICriteria OrderBy(ISorting sorting) + { + options.AddSorting(sorting); + return this; + } + + /// + public ICriteria OrderBy(ISorting[]? sorting) + { + options.AddSorting(sorting); + return this; + } + + /// + public ISearch AsSearch() + { + return new Search(performer, options); + } + + /// + public ISearch Select() + where TDto : class + { + var select = new SearchSelect(); + return new Search(performer, options, select); + } + + /// + public ISearch Select(Expression> selectExpression) + where TDto : class + { + var select = new SearchSelect(selectExpression); + return new Search(performer, options, select); + } + + /// + public IReadOnlyList Collect() => performer.Prepare(options).ToList(); + + /// + public async Task> CollectAsync(CancellationToken cancellationToken = default) + => await performer.Prepare(options).ToListAsync(cancellationToken); + + /// + public bool Exists() => performer.Prepare(options).Exists(); + + /// + public async Task ExistsAsync(CancellationToken cancellationToken = default) + => await performer.Prepare(options).ExistsAsync(cancellationToken); + + /// + public TEntity? FirstOrDefault() => performer.Prepare(options).FirstOrDefault(); + + /// + public async Task FirstOrDefaultAsync(CancellationToken cancellationToken = default) + => await performer.Prepare(options).FirstOrDefaultAsync(cancellationToken); + + /// + public TEntity Single() => performer.Prepare(options).Single(); + + /// + public async Task SingleAsync(CancellationToken cancellationToken = default) + => await performer.Prepare(options).SingleAsync(cancellationToken); +} diff --git a/src/RoyalCode.SmartSearch.Core/SearchCriteria.cs b/src/RoyalCode.SmartSearch.Core/Defaults/CriteriaOptions.cs similarity index 60% rename from src/RoyalCode.SmartSearch.Core/SearchCriteria.cs rename to src/RoyalCode.SmartSearch.Core/Defaults/CriteriaOptions.cs index bacf24f..46cd6ec 100644 --- a/src/RoyalCode.SmartSearch.Core/SearchCriteria.cs +++ b/src/RoyalCode.SmartSearch.Core/Defaults/CriteriaOptions.cs @@ -1,32 +1,29 @@ -// Ignore Spelling: Sortings +using RoyalCode.SmartSearch.Filtering; -using System.Linq.Expressions; -using RoyalCode.SmartSearch.Abstractions; - -namespace RoyalCode.SmartSearch.Core; +namespace RoyalCode.SmartSearch.Defaults; /// -/// The criteria for performing the search. +/// The criteria options for performing the search. /// -public sealed class SearchCriteria +public sealed class CriteriaOptions { - private List? filters; + private List? filters; private List? sortings; /// - /// Get all filters. + /// Gets or sets a value indicating whether tracking is enabled. /// - public IReadOnlyList Filters => filters ?? []; + public bool TrackingEnabled { get; private set; } = true; /// - /// Get all sortings. + /// Get all filters. /// - public IReadOnlyList Sortings => sortings ?? []; + public IReadOnlyList Filters => filters ?? []; /// - /// Information about the select expression. + /// Get all sorting. /// - public SearchSelect? Select { get; private set; } + public IReadOnlyList Sortings => sortings ?? []; /// /// @@ -46,6 +43,16 @@ public sealed class SearchCriteria /// public int Page { get; set; } + /// + /// The number of records to be skipped in the search. + /// + public int Skip { get; set; } + + /// + /// The number of records to be returned in the search. + /// + public int Take { get; set; } + /// /// /// Updates the last record count. @@ -61,6 +68,14 @@ public sealed class SearchCriteria /// public bool UseCount { get; set; } = Defaults.DefaultUseCount; + /// + /// Disables entity tracking for the query, which can improve performance for read-only operations. + /// + public void NoTracking() + { + TrackingEnabled = false; + } + /// /// Adds a new filter to specify the search. /// @@ -69,7 +84,7 @@ public void AddFilter(TFilter filter) where TFilter : class { filters ??= []; - filters.Add(new SearchFilter(filter)); + filters.Add(new EntityFilter(filter)); } /// @@ -83,23 +98,21 @@ public void AddSorting(ISorting sorting) } /// - /// Set the select expression. + /// Adds a sorting definition. /// - /// The select expression. - /// The query entity type. - /// The select type. - /// If expression is null. - public void SetSelectExpression(Expression> selectExpression) + /// The sorting definitions. + public void AddSorting(IEnumerable? sorting) { - ArgumentNullException.ThrowIfNull(selectExpression); - - Select = new SearchSelect(selectExpression); + if (sorting is null) + return; + sortings ??= []; + sortings.AddRange(sorting); } /// /// Whether the query should be paginated. /// - public bool Paginate => ItemsPerPage > 0; + public bool Paginate => Page > 0; /// /// The number of the page that should be listed. @@ -119,10 +132,24 @@ public void SetSelectExpression(Expression> s /// /// /// The number of records that must be skipped. - public int GetSkipCount() => Paginate ? ItemsPerPage * (GetPageNumber() - 1) : 0; + public int GetSkipCount() => Paginate ? ItemsPerPage * (GetPageNumber() - 1) : Skip; + + /// + /// Determines the number of items to retrieve in a paginated query. + /// + /// + /// The number of items to retrieve. + ///
+ /// If pagination is enabled, returns the value of . + ///
+ /// If pagination is disabled and is greater than 0, returns the value of . + ///
+ /// Otherwise, returns 0. + ///
+ public int GetTakeCount() => Paginate ? ItemsPerPage : Take > 0 ? Take : 0; /// - /// Default values for each new created. + /// Default values for each new created. /// public static class Defaults { diff --git a/src/RoyalCode.SmartSearch.Core/Defaults/Search.cs b/src/RoyalCode.SmartSearch.Core/Defaults/Search.cs new file mode 100644 index 0000000..8078f12 --- /dev/null +++ b/src/RoyalCode.SmartSearch.Core/Defaults/Search.cs @@ -0,0 +1,115 @@ + +using RoyalCode.SmartSearch.Mappings; +using RoyalCode.SmartSearch.Services; +using System.Linq.Expressions; + +namespace RoyalCode.SmartSearch.Defaults; + +/// +/// Default implementation of . +/// +/// The entity type. +public class Search : ISearch + where TEntity : class +{ + private readonly ICriteriaPerformer performer; + private readonly CriteriaOptions options; + + /// + /// Creates a new search. + /// + /// + /// + public Search(ICriteriaPerformer performer, CriteriaOptions options) + { + this.performer = performer; + this.options = options; + options.NoTracking(); + } + + /// + public ISearch Select() + where TDto : class + { + var select = new SearchSelect(); + return new Search(performer, options, select); + } + + /// + public ISearch Select(Expression> selectExpression) + where TDto : class + { + var select = new SearchSelect(selectExpression); + return new Search(performer, options, select); + } + + /// + public IResultList ToList() => performer.Prepare(options).ToResultList(); + + /// + public Task> ToListAsync(CancellationToken token) + => performer.Prepare(options).ToResultListAsync(token); + + /// + public Task> ToAsyncListAsync(CancellationToken token) + => performer.Prepare(options).ToAsyncListAsync(token); + + /// + public TEntity? FirstOrDefault() => performer.Prepare(options).FirstOrDefault(); + + /// + public Task FirstDefaultAsync(CancellationToken cancellationToken = default) + => performer.Prepare(options).FirstOrDefaultAsync(cancellationToken); + + /// + public TEntity Single() => performer.Prepare(options).Single(); + + /// + public Task SingleAsync(CancellationToken cancellationToken = default) + => performer.Prepare(options).SingleAsync(cancellationToken); +} + +public class Search : ISearch + where TEntity : class + where TDto : class +{ + private readonly ICriteriaPerformer performer; + private readonly CriteriaOptions options; + private readonly ISearchSelect searchSelect; + + public Search( + ICriteriaPerformer performer, + CriteriaOptions options, + ISearchSelect searchSelect) + { + this.performer = performer; + this.options = options; + this.searchSelect = searchSelect; + options.NoTracking(); + } + + /// + public IResultList ToList() => performer.Prepare(options).Select(searchSelect).ToResultList(); + + /// + public Task> ToListAsync(CancellationToken token) + => performer.Prepare(options).Select(searchSelect).ToResultListAsync(token); + + /// + public Task> ToAsyncListAsync(CancellationToken token) + => performer.Prepare(options).Select(searchSelect).ToAsyncListAsync(token); + + /// + public TDto? FirstOrDefault() => performer.Prepare(options).Select(searchSelect).FirstOrDefault(); + + /// + public Task FirstOrDefaultAsync(CancellationToken cancellationToken = default) + => performer.Prepare(options).Select(searchSelect).FirstOrDefaultAsync(cancellationToken); + + /// + public TDto Single() => performer.Prepare(options).Select(searchSelect).Single(); + + /// + public Task SingleAsync(CancellationToken cancellationToken = default) + => performer.Prepare(options).Select(searchSelect).SingleAsync(cancellationToken); +} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Core/SearchFilter.cs b/src/RoyalCode.SmartSearch.Core/Filtering/EntityFilter.cs similarity index 56% rename from src/RoyalCode.SmartSearch.Core/SearchFilter.cs rename to src/RoyalCode.SmartSearch.Core/Filtering/EntityFilter.cs index 0aac4df..9889aa5 100644 --- a/src/RoyalCode.SmartSearch.Core/SearchFilter.cs +++ b/src/RoyalCode.SmartSearch.Core/Filtering/EntityFilter.cs @@ -1,16 +1,16 @@ -namespace RoyalCode.SmartSearch.Core; +namespace RoyalCode.SmartSearch.Filtering; /// /// Information about a filter to be applied to a query. /// /// The filter instance. /// The filter type. -public class SearchFilter(TFilter Filter) : ISearchFilter +public sealed class EntityFilter(TFilter Filter) : IFilter where TFilter : class { /// - public void ApplyFilter(ISpecifierHandler handler) + public void ApplyFilter(IFilterHandler specifier) { - handler.Handle(Filter); + specifier.Specify(Filter); } } diff --git a/src/RoyalCode.SmartSearch.Core/Filtering/IFilter.cs b/src/RoyalCode.SmartSearch.Core/Filtering/IFilter.cs new file mode 100644 index 0000000..70d4205 --- /dev/null +++ b/src/RoyalCode.SmartSearch.Core/Filtering/IFilter.cs @@ -0,0 +1,13 @@ +namespace RoyalCode.SmartSearch.Filtering; + +/// +/// Information about a filter to be applied to a query. +/// +public interface IFilter +{ + /// + /// Applies the filter to the query by passing the filter to the handler. + /// + /// A specifier handler for applying filters to queries. + void ApplyFilter(IFilterHandler specifier); +} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Core/ISpecifierHandler.cs b/src/RoyalCode.SmartSearch.Core/Filtering/IFilterHandler.cs similarity index 68% rename from src/RoyalCode.SmartSearch.Core/ISpecifierHandler.cs rename to src/RoyalCode.SmartSearch.Core/Filtering/IFilterHandler.cs index 730087c..ee899be 100644 --- a/src/RoyalCode.SmartSearch.Core/ISpecifierHandler.cs +++ b/src/RoyalCode.SmartSearch.Core/Filtering/IFilterHandler.cs @@ -1,20 +1,20 @@ -namespace RoyalCode.SmartSearch.Core; +namespace RoyalCode.SmartSearch.Filtering; /// /// /// A handler for applying filters to queries. /// /// -/// This component is used by the , +/// This component is used by the , /// which stores the filter that will be used in the query specification. /// /// -public interface ISpecifierHandler +public interface IFilterHandler { /// /// Receives the filter object that will be used to specify the query. /// /// The filter object. /// The filter type. - void Handle(TFilter filter) where TFilter : class; + void Specify(TFilter filter) where TFilter : class; } \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Core/IFilterSpecifier.cs b/src/RoyalCode.SmartSearch.Core/IFilterSpecifier.cs deleted file mode 100644 index 0d357db..0000000 --- a/src/RoyalCode.SmartSearch.Core/IFilterSpecifier.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace RoyalCode.SmartSearch.Core; - -/// -/// Component that applies the filtering conditions to the query. -/// -/// The query type. -/// The filter type. -public interface IFilterSpecifier -{ - /// - /// Specify a query, apply the filter conditions to the query. - /// - /// The query object. - /// The filter object. - /// A new (or the same) query object. - TQuery Specify(TQuery query, TFilter filter); -} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Core/ISearchFilter.cs b/src/RoyalCode.SmartSearch.Core/ISearchFilter.cs deleted file mode 100644 index d95017d..0000000 --- a/src/RoyalCode.SmartSearch.Core/ISearchFilter.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace RoyalCode.SmartSearch.Core; - -/// -/// Information about a filter to be applied to a query. -/// -public interface ISearchFilter -{ - /// - /// Applies the filter to the query by passing the filter to the handler. - /// - /// A handler for applying filters to queries. - public abstract void ApplyFilter(ISpecifierHandler handler); -} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Core/Mappings/ISearchSelect.cs b/src/RoyalCode.SmartSearch.Core/Mappings/ISearchSelect.cs new file mode 100644 index 0000000..e8f9ad4 --- /dev/null +++ b/src/RoyalCode.SmartSearch.Core/Mappings/ISearchSelect.cs @@ -0,0 +1,30 @@ +using System.Linq.Expressions; + +namespace RoyalCode.SmartSearch.Mappings; + +/// +/// +/// Defines a select mapping between an entity and a DTO for search projections. +/// +/// +/// Used to apply a select expression or use the default mapping for the given types. +/// +/// +/// The entity type to select from. +/// The data transfer object (DTO) type to project to. +public interface ISearchSelect + where TEntity : class + where TDto : class +{ + /// + /// + /// Gets the expression used to project an entity of type into a data transfer + /// object (DTO) of type . + /// + /// + /// This expression is optional and can be used to customize the selection of properties from the entity + /// to the DTO. When not provided, the default mapping will be used if available. + /// + /// + Expression>? SelectExpression { get; } +} diff --git a/src/RoyalCode.SmartSearch.Core/Mappings/SearchSelect.cs b/src/RoyalCode.SmartSearch.Core/Mappings/SearchSelect.cs new file mode 100644 index 0000000..eae6fe8 --- /dev/null +++ b/src/RoyalCode.SmartSearch.Core/Mappings/SearchSelect.cs @@ -0,0 +1,43 @@ +using System.Linq.Expressions; + +namespace RoyalCode.SmartSearch.Mappings; + +/// +/// +/// Represents a select mapping between an entity and a DTO for search projections. +/// +/// +/// Implements to provide a way to apply a select expression +/// or use the default mapping for the given types. +/// +/// +/// The entity type to select from. +/// The DTO type to project to. +public sealed class SearchSelect : ISearchSelect + where TEntity : class + where TDto : class +{ + /// + /// The select expression to project from to . + /// + private readonly Expression>? selectExpression; + + /// + /// Initializes a new instance of the class with a select expression. + /// + /// The expression to select from the entity to the DTO. + /// Thrown if is null. + public SearchSelect(Expression> selectExpression) + { + ArgumentNullException.ThrowIfNull(selectExpression); + this.selectExpression = selectExpression; + } + + /// + /// Initializes a new instance of the class using the default mapping. + /// + public SearchSelect() { } + + /// + public Expression>? SelectExpression => selectExpression; +} diff --git a/src/RoyalCode.SmartSearch.Core/Pipeline/AllEntities.cs b/src/RoyalCode.SmartSearch.Core/Pipeline/AllEntities.cs deleted file mode 100644 index 29cd816..0000000 --- a/src/RoyalCode.SmartSearch.Core/Pipeline/AllEntities.cs +++ /dev/null @@ -1,173 +0,0 @@ -using RoyalCode.SmartSearch.Abstractions; -using System.Runtime.CompilerServices; - -namespace RoyalCode.SmartSearch.Core.Pipeline; - -/// -public class AllEntities : IAllEntities - where TEntity : class -{ - private readonly IPipelineFactory factory; - private readonly SearchCriteria criteria = new(); - - /// - /// Creates a new search with the to execute the query. - /// - /// The pipeline factory for create the all entities pipeline. - public AllEntities(IPipelineFactory factory) - { - this.factory = factory; - } - - /// - public IAllEntities FilterBy(TFilter filter) where TFilter : class - { - criteria.AddFilter(filter); - return this; - } - - /// - public IAllEntities OrderBy(ISorting sorting) - { - criteria.AddSorting(sorting); - return this; - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ICollection Collect() - { - var pipeline = factory.CreateAllEntities(); - return pipeline.Execute(criteria); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Task> CollectAsync(CancellationToken cancellationToken = default) - { - var pipeline = factory.CreateAllEntities(); - return pipeline.ExecuteAsync(criteria, cancellationToken); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Exists() - { - var pipeline = factory.CreateAllEntities(); - return pipeline.Any(criteria); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Task ExistsAsync(CancellationToken cancellationToken = default) - { - var pipeline = factory.CreateAllEntities(); - return pipeline.AnyAsync(criteria, cancellationToken); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TEntity? First() - { - var pipeline = factory.CreateAllEntities(); - return pipeline.First(criteria); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Task FirstAsync(CancellationToken cancellationToken = default) - { - var pipeline = factory.CreateAllEntities(); - return pipeline.FirstAsync(criteria, cancellationToken); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TEntity Single() - { - var pipeline = factory.CreateAllEntities(); - return pipeline.Single(criteria); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Task SingleAsync(CancellationToken cancellationToken = default) - { - var pipeline = factory.CreateAllEntities(); - return pipeline.SingleAsync(criteria, cancellationToken); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void DeleteAll() - { - var pipeline = factory.CreateAllEntities(); - pipeline.RemoveAll(criteria); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Task DeleteAllAsync(CancellationToken cancellationToken = default) - { - var pipeline = factory.CreateAllEntities(); - return pipeline.RemoveAllAsync(criteria, cancellationToken); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void UpdateAll(Action updateAction) - { - var pipeline = factory.CreateAllEntities(); - pipeline.UpdateAll(criteria, updateAction); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void UpdateAll(TData data, Action updateAction) - { - var pipeline = factory.CreateAllEntities(); - pipeline.UpdateAll(criteria, data, updateAction); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void UpdateAll( - ICollection collection, - Func entityIdGet, - Func dataIdGet, - Action updateAction) where TData : class - { - var pipeline = factory.CreateAllEntities(); - pipeline.UpdateAll(criteria, collection, entityIdGet, dataIdGet, updateAction); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Task UpdateAllAsync(Action updateAction, CancellationToken cancellationToken = default) - { - var pipeline = factory.CreateAllEntities(); - return pipeline.UpdateAllAsync(criteria, updateAction, cancellationToken); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Task UpdateAllAsync(TData data, Action updateAction, - CancellationToken cancellationToken = default) - { - var pipeline = factory.CreateAllEntities(); - return pipeline.UpdateAllAsync(criteria, data, updateAction, cancellationToken); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Task UpdateAllAsync( - ICollection collection, - Func entityIdGet, - Func dataIdGet, - Action updateAction, - CancellationToken cancellationToken = default) where TData : class - { - var pipeline = factory.CreateAllEntities(); - return pipeline.UpdateAllAsync(criteria, collection, entityIdGet, dataIdGet, updateAction, cancellationToken); - } -} diff --git a/src/RoyalCode.SmartSearch.Core/Pipeline/IAllEntitiesPipeline.cs b/src/RoyalCode.SmartSearch.Core/Pipeline/IAllEntitiesPipeline.cs deleted file mode 100644 index 1f7b5ce..0000000 --- a/src/RoyalCode.SmartSearch.Core/Pipeline/IAllEntitiesPipeline.cs +++ /dev/null @@ -1,173 +0,0 @@ - -namespace RoyalCode.SmartSearch.Core.Pipeline; - -/// -/// -/// A search pipeline for executing queries from the input criteria and get all entities. -/// -/// -/// This component will perform the various steps necessary to execute the query. -/// -/// -/// The entity type to query. -public interface IAllEntitiesPipeline - where TEntity : class -{ - /// - /// Execute the query and collect all entities. - /// - /// The criteria for the query. - /// A collection of the entities. - ICollection Execute(SearchCriteria searchCriteria); - - /// - /// Execute the query and collect all entities. - /// - /// The criteria for the query. - /// The cancellation token. - /// A collection of the entities. - Task> ExecuteAsync( - SearchCriteria searchCriteria, - CancellationToken cancellationToken = default); - - /// - /// Execute the query and verify if there are any entities. - /// - /// The criteria for the query. - /// True if there are any entities, otherwise false. - bool Any(SearchCriteria searchCriteria); - - /// - /// Execute the query and verify if there are any entities. - /// - /// The criteria for the query. - /// The cancellation token. - /// True if there are any entities, otherwise false. - Task AnyAsync(SearchCriteria searchCriteria, CancellationToken cancellationToken = default); - - /// - /// Execute the query and get the first entity. - /// - /// The criteria for the query. - /// The first entity that meets the criteria. - TEntity? First(SearchCriteria searchCriteria); - - /// - /// Execute the query and get the first entity. - /// - /// The criteria for the query. - /// The cancellation token. - /// The first entity that meets the criteria. - Task FirstAsync(SearchCriteria searchCriteria, CancellationToken cancellationToken = default); - - /// - /// Execute the query and get the first entity or throw an exception if there are no entities or more than one. - /// - /// The criteria for the query. - /// The first entity that meets the criteria, or an exception if there are no entities or more than one. - TEntity Single(SearchCriteria searchCriteria); - - /// - /// Execute the query and get the first entity or throw an exception if there are no entities or more than one. - /// - /// The criteria for the query. - /// The cancellation token. - /// The first entity that meets the criteria, or an exception if there are no entities or more than one. - Task SingleAsync(SearchCriteria searchCriteria, CancellationToken cancellationToken = default); - - /// - /// Execute the query and remove all entities that meet the criteria. - /// - /// The criteria for the query. - void RemoveAll(SearchCriteria searchCriteria); - - /// - /// Execute the query and remove all entities that meet the criteria. - /// - /// The criteria for the query. - /// The cancellation token. - /// A task that represents the asynchronous operation. - Task RemoveAllAsync(SearchCriteria searchCriteria, CancellationToken cancellationToken = default); - - /// - /// Execute the query and update all entities that meet the criteria. - /// - /// The criteria for the query. - /// The action to update the entities. - void UpdateAll(SearchCriteria searchCriteria, Action updateAction); - - /// - /// Execute the query and update all entities that meet the criteria. - /// - /// The criteria for the query. - /// The data used to update the entities. - /// The action to update the entities. - /// The type of the data used to update the entities. - void UpdateAll(SearchCriteria searchCriteria, TData data, Action updateAction); - - /// - /// Update all entities that meet the criteria. - /// - /// The criteria for the query. - /// A collection of data used to update the entities. - /// A function to get the entity id. - /// A function to get the data id. - /// The action to update the entities. - /// The type of the data used to update the entities. - /// The type of the id. - void UpdateAll( - SearchCriteria searchCriteria, - ICollection collection, - Func entityIdGet, - Func dataIdGet, - Action updateAction) - where TData : class; - - /// - /// Execute the query and update all entities that meet the criteria. - /// - /// The criteria for the query. - /// The action to update the entities. - /// The cancellation token. - /// A task that represents the asynchronous operation. - Task UpdateAllAsync( - SearchCriteria searchCriteria, - Action updateAction, - CancellationToken cancellationToken = default); - - /// - /// Execute the query and update all entities that meet the criteria. - /// - /// The criteria for the query. - /// The data used to update the entities. - /// The action to update the entities. - /// The cancellation token. - /// The type of the data used to update the entities. - /// A task that represents the asynchronous operation. - Task UpdateAllAsync( - SearchCriteria searchCriteria, - TData data, - Action updateAction, - CancellationToken cancellationToken = default); - - /// - /// Update all entities that meet the criteria. - /// - /// The criteria for the query. - /// The collection of data used to update the entities. - /// The function to get the entity id. - /// The function to get the data id. - /// The action to update the entities. - /// The cancellation token. - /// The type of the data used to update the entities. - /// The type of the id. - /// A task that represents the asynchronous operation. - Task UpdateAllAsync( - SearchCriteria searchCriteria, - ICollection collection, - Func entityIdGet, - Func dataIdGet, - Action updateAction, - CancellationToken cancellationToken = default) - where TData : class; -} diff --git a/src/RoyalCode.SmartSearch.Core/Pipeline/IPipelineFactory.cs b/src/RoyalCode.SmartSearch.Core/Pipeline/IPipelineFactory.cs deleted file mode 100644 index 48f7f35..0000000 --- a/src/RoyalCode.SmartSearch.Core/Pipeline/IPipelineFactory.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace RoyalCode.SmartSearch.Core.Pipeline; - -/// -/// -/// Factory to create search pipelines (), -/// and searches for all entities (). -/// -/// -public interface IPipelineFactory -{ - /// - /// Creates a to query entities. - /// - /// The entity type. - /// A new instance of a pipeline to execute the search. - ISearchPipeline Create() - where TEntity : class; - - /// - /// Creates a new to select DTOs from the query of entities. - /// - /// The entity type. - /// The DTO (to be selected) type. - /// A new instance of a pipeline to execute the search. - ISearchPipeline Create() - where TEntity : class - where TDto : class; - - /// - /// Creates a to query all entities. - /// - /// The entity type. - /// - /// A new instance of a pipeline to execute the search - /// - IAllEntitiesPipeline CreateAllEntities() - where TEntity : class; -} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Core/Pipeline/ISearchPipeline.cs b/src/RoyalCode.SmartSearch.Core/Pipeline/ISearchPipeline.cs deleted file mode 100644 index 034fd60..0000000 --- a/src/RoyalCode.SmartSearch.Core/Pipeline/ISearchPipeline.cs +++ /dev/null @@ -1,39 +0,0 @@ -using RoyalCode.SmartSearch.Abstractions; - -namespace RoyalCode.SmartSearch.Core.Pipeline; - -/// -/// -/// A search pipeline for executing queries from the input criteria. -/// -/// -/// This component will perform the various steps necessary to execute the query. -/// -/// -/// The query model type. -public interface ISearchPipeline - where TModel : class -{ - /// - /// Execute the search and it returns a list of results. - /// - /// The criteria for the search. - /// A list of results. - IResultList Execute(SearchCriteria criteria); - - /// - /// Async execute the search and it returns a list of results. - /// - /// The criteria for the search. - /// The task cancellation token. - /// A task of a list of results. - Task> ExecuteAsync(SearchCriteria criteria, CancellationToken token); - - /// - /// Async execute the search and it returns a list of results. - /// - /// The criteria for the search. - /// The task cancellation token. - /// A task of an async list of results. - Task> AsyncExecuteAsync(SearchCriteria criteria, CancellationToken token); -} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Core/Pipeline/Search.cs b/src/RoyalCode.SmartSearch.Core/Pipeline/Search.cs deleted file mode 100644 index a464831..0000000 --- a/src/RoyalCode.SmartSearch.Core/Pipeline/Search.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Linq.Expressions; -using RoyalCode.SmartSearch.Abstractions; - -namespace RoyalCode.SmartSearch.Core.Pipeline; - -/// -public class Search : SearchBase - where TEntity : class -{ - private readonly IPipelineFactory factory; - - /// - /// Creates a new search with the to execute the query. - /// - /// A search pipeline factory. - public Search(IPipelineFactory factory) - { - this.factory = factory; - } - - /// - public override ISearch Select() - { - return new Search(factory, criteria); - } - - /// - public override ISearch Select(Expression> selectExpression) - { - criteria.SetSelectExpression(selectExpression); - return new Search(factory, criteria); - } - - /// - public override IResultList ToList() - { - var pipeline = factory.Create(); - return pipeline.Execute(criteria); - } - - /// - public override Task> ToListAsync(CancellationToken token) - { - var pipeline = factory.Create(); - return pipeline.ExecuteAsync(criteria, token); - } - - /// - public override Task> ToAsyncListAsync(CancellationToken token) - { - var pipeline = factory.Create(); - return pipeline.AsyncExecuteAsync(criteria, token); - } -} - -/// -public class Search : SearchBase - where TEntity : class - where TDto : class -{ - private readonly IPipelineFactory factory; - - /// - /// Creates a new search. - /// - /// The pipeline factory for create the search pipeline. - /// The previous criteria. - public Search(IPipelineFactory factory, SearchCriteria criteria) : base(criteria) - { - this.factory = factory; - } - - /// - public override IResultList ToList() - { - var pipeline = factory.Create(); - return pipeline.Execute(criteria); - } - - /// - public override Task> ToListAsync(CancellationToken token) - { - var pipeline = factory.Create(); - return pipeline.ExecuteAsync(criteria, token); - } - - /// - public override Task> ToAsyncListAsync(CancellationToken token) - { - var pipeline = factory.Create(); - return pipeline.AsyncExecuteAsync(criteria, token); - } -} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Core/RoyalCode.SmartSearch.Core.csproj b/src/RoyalCode.SmartSearch.Core/RoyalCode.SmartSearch.Core.csproj index e8b785a..3476e59 100644 --- a/src/RoyalCode.SmartSearch.Core/RoyalCode.SmartSearch.Core.csproj +++ b/src/RoyalCode.SmartSearch.Core/RoyalCode.SmartSearch.Core.csproj @@ -4,6 +4,7 @@ $(AspTargets) + RoyalCode.SmartSearch @@ -16,7 +17,7 @@ - + diff --git a/src/RoyalCode.SmartSearch.Core/SearchBase.cs b/src/RoyalCode.SmartSearch.Core/SearchBase.cs deleted file mode 100644 index 015da5e..0000000 --- a/src/RoyalCode.SmartSearch.Core/SearchBase.cs +++ /dev/null @@ -1,139 +0,0 @@ -using System.Linq.Expressions; -using RoyalCode.SmartSearch.Abstractions; - -namespace RoyalCode.SmartSearch.Core; - -/// -public abstract class SearchBase : ISearch - where TEntity : class -{ - /// - /// The criteria for performing the search. - /// - protected readonly SearchCriteria criteria = new(); - - /// - public ISearch UsePages(int itemsPerPage = 10) - { - criteria.ItemsPerPage = itemsPerPage; - return this; - } - - /// - public ISearch FetchPage(int pageNumber) - { - criteria.Page = pageNumber; - return this; - } - - /// - public ISearch UseLastCount(int lastCount) - { - criteria.LastCount = lastCount; - return this; - } - - /// - public ISearch UseCount(bool useCount = true) - { - criteria.UseCount = useCount; - return this; - } - - /// - public ISearch FilterBy(TFilter filter) - where TFilter : class - { - criteria.AddFilter(filter); - return this; - } - - /// - public ISearch OrderBy(ISorting sorting) - { - criteria.AddSorting(sorting); - return this; - } - - /// - public abstract ISearch Select() - where TDto : class; - - /// - public abstract ISearch Select(Expression> selectExpression) - where TDto : class; - - /// - public abstract IResultList ToList(); - - /// - public abstract Task> ToListAsync(CancellationToken token); - - /// - public abstract Task> ToAsyncListAsync(CancellationToken token); -} - -/// -public abstract class SearchBase : ISearch - where TEntity : class - where TDto : class -{ - /// - /// The criteria for performing the search. - /// - protected readonly SearchCriteria criteria; - - /// - /// Initialize the base criteria with the . - /// - /// - protected SearchBase(SearchCriteria criteria) - { - this.criteria = criteria; - } - - /// - public ISearch UsePages(int itemsPerPage = 10) - { - criteria.ItemsPerPage = itemsPerPage; - return this; - } - - /// - public ISearch FetchPage(int pageNumber) - { - criteria.Page = pageNumber; - return this; - } - - /// - public ISearch UseLastCount(int lastCount) - { - criteria.LastCount = lastCount; - return this; - } - - /// - public ISearch UseCount(bool useCount = true) - { - criteria.UseCount = useCount; - return this; - } - - /// - public ISearch FilterBy(TFilter filter) - where TFilter : class - { - criteria.AddFilter(filter); - return this; - } - - /// - public abstract IResultList ToList(); - - /// - public abstract Task> ToListAsync(CancellationToken token); - - /// - public abstract Task> ToAsyncListAsync(CancellationToken token); -} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Core/SearchSelect.cs b/src/RoyalCode.SmartSearch.Core/SearchSelect.cs deleted file mode 100644 index 1d2f492..0000000 --- a/src/RoyalCode.SmartSearch.Core/SearchSelect.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Linq.Expressions; - -namespace RoyalCode.SmartSearch.Core; - -/// -/// Information about the select to be applied to the query. -/// -public sealed class SearchSelect -{ - /// - /// Creates a new instance of . - /// - /// - public SearchSelect(Expression selectExpression) - { - SelectExpression = selectExpression; - } - - /// - /// The expression to be used in the select operation. - /// - public Expression SelectExpression { get; } -} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Core/Services/ICriteriaPerformer.cs b/src/RoyalCode.SmartSearch.Core/Services/ICriteriaPerformer.cs new file mode 100644 index 0000000..0b431c5 --- /dev/null +++ b/src/RoyalCode.SmartSearch.Core/Services/ICriteriaPerformer.cs @@ -0,0 +1,18 @@ +using RoyalCode.SmartSearch.Defaults; + +namespace RoyalCode.SmartSearch.Services; + +/// +/// A service capable of accessing the database and running a query using the Criteria options. +/// +/// The entity type to query. +public interface ICriteriaPerformer + where TEntity : class +{ + /// + /// Prepares the query with the criteria options for execution. + /// + /// The criteria options for performing the search. + /// A prepared query to perform. + IPreparedQuery Prepare(CriteriaOptions options); +} diff --git a/src/RoyalCode.SmartSearch.Core/Services/IPreparedQuery.cs b/src/RoyalCode.SmartSearch.Core/Services/IPreparedQuery.cs new file mode 100644 index 0000000..fbd0ad1 --- /dev/null +++ b/src/RoyalCode.SmartSearch.Core/Services/IPreparedQuery.cs @@ -0,0 +1,101 @@ +using RoyalCode.SmartSearch.Mappings; + +namespace RoyalCode.SmartSearch.Services; + +/// +/// +/// Represents a prepared query for a specific entity type, allowing execution of search operations +/// such as existence checks, retrieval of single or multiple results, and projection to DTOs. +/// +/// +/// This interface provides both synchronous and asynchronous methods for querying, as well as support +/// for result list abstractions and custom select projections. +/// +/// +/// The entity type for which the query is prepared. +public interface IPreparedQuery + where T : class +{ + /// + /// Determines whether any entities exist that match the prepared query. + /// + /// True if any entities exist; otherwise, false. + bool Exists(); + + /// + /// Asynchronously determines whether any entities exist that match the prepared query. + /// + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. The task result contains true if any entities exist; otherwise, false. + Task ExistsAsync(CancellationToken ct = default); + + /// + /// Returns the first entity that matches the prepared query, or null if no entity is found. + /// + /// The first entity found, or null if no entity matches the query. + T? FirstOrDefault(); + + /// + /// Asynchronously returns the first entity that matches the prepared query, or null if no entity is found. + /// + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. The task result contains the first entity found, or null if no entity matches the query. + Task FirstOrDefaultAsync(CancellationToken ct = default); + + /// + /// Returns the single entity that matches the prepared query. + /// + /// The single entity found. + /// Thrown if no entity or more than one entity matches the query. + T Single(); + + /// + /// Asynchronously returns the single entity that matches the prepared query. + /// + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. The task result contains the single entity found. + /// Thrown if no entity or more than one entity matches the query. + Task SingleAsync(CancellationToken ct = default); + + /// + /// Returns a read-only list of all entities that match the prepared query. + /// + /// A read-only list of entities. + IReadOnlyList ToList(); + + /// + /// Asynchronously returns a read-only list of all entities that match the prepared query. + /// + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. The task result contains a read-only list of entities. + Task> ToListAsync(CancellationToken ct); + + /// + /// Returns a result list abstraction containing all entities that match the prepared query. + /// + /// An containing the entities and additional result metadata. + IResultList ToResultList(); + + /// + /// Asynchronously returns a result list abstraction containing all entities that match the prepared query. + /// + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. The task result contains an . + Task> ToResultListAsync(CancellationToken ct); + + /// + /// Asynchronously returns an asynchronous result list abstraction containing all entities that match the prepared query. + /// + /// A cancellation token to observe while waiting for the task to complete. + /// A task that represents the asynchronous operation. The task result contains an . + Task> ToAsyncListAsync(CancellationToken ct); + + /// + /// Projects the prepared query to a different DTO type using the specified select mapping. + /// + /// The DTO type to project to. + /// The select mapping to apply for the projection. + /// A new representing the projected query. + IPreparedQuery Select(ISearchSelect select) + where TDto : class; +} diff --git a/src/RoyalCode.SmartSearch.EntityFramework/AllEntitiesPipeline.cs b/src/RoyalCode.SmartSearch.EntityFramework/AllEntitiesPipeline.cs deleted file mode 100644 index 9795a4e..0000000 --- a/src/RoyalCode.SmartSearch.EntityFramework/AllEntitiesPipeline.cs +++ /dev/null @@ -1,196 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using RoyalCode.SmartSearch.Core; -using RoyalCode.SmartSearch.Core.Pipeline; -using RoyalCode.SmartSearch.Linq; -using RoyalCode.SmartSearch.Linq.Filter; -using System.Runtime.CompilerServices; - -namespace RoyalCode.SmartSearch.EntityFramework; - -/// -/// Default implementation of . -/// -/// The entity type -public sealed class AllEntitiesPipeline : SearchPipelineBase, IAllEntitiesPipeline - where TEntity : class -{ - /// - public AllEntitiesPipeline( - IQueryableProvider queryableProvider, - ISpecifierFactory specifierFactory, - ISorter sorter) - : base(queryableProvider, specifierFactory, sorter) - { } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Any(SearchCriteria searchCriteria) - { - return PrepareQuery(searchCriteria).Any(); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Task AnyAsync(SearchCriteria searchCriteria, CancellationToken cancellationToken = default) - { - return PrepareQuery(searchCriteria).AnyAsync(cancellationToken); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public ICollection Execute(SearchCriteria searchCriteria) - { - return PrepareQuery(searchCriteria).ToList(); - } - - /// - public async Task> ExecuteAsync( - SearchCriteria searchCriteria, - CancellationToken cancellationToken = default) - { - return await PrepareQuery(searchCriteria).ToListAsync(cancellationToken); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TEntity? First(SearchCriteria searchCriteria) - { - return PrepareQuery(searchCriteria).FirstOrDefault(); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Task FirstAsync(SearchCriteria searchCriteria, CancellationToken cancellationToken = default) - { - return PrepareQuery(searchCriteria).FirstOrDefaultAsync(cancellationToken); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void RemoveAll(SearchCriteria searchCriteria) - { - var query = PrepareQuery(searchCriteria); - var removable = queryableProvider.GetRemovable(); - removable.RemoveAll(query); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Task RemoveAllAsync(SearchCriteria searchCriteria, CancellationToken cancellationToken = default) - { - var query = PrepareQuery(searchCriteria); - var removable = queryableProvider.GetRemovable(); - return removable.RemoveAllAsync(query, cancellationToken); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public TEntity Single(SearchCriteria searchCriteria) - { - return PrepareQuery(searchCriteria).Single(); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Task SingleAsync(SearchCriteria searchCriteria, CancellationToken cancellationToken = default) - { - return PrepareQuery(searchCriteria).SingleAsync(cancellationToken); - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void UpdateAll(SearchCriteria searchCriteria, Action updateAction) - { - var query = PrepareQuery(searchCriteria); - foreach(var entity in query) - { - updateAction(entity); - } - } - - /// - public async Task UpdateAllAsync( - SearchCriteria searchCriteria, - Action updateAction, - CancellationToken cancellationToken = default) - { - var query = PrepareQuery(searchCriteria); - await foreach (var entity in query.AsAsyncEnumerable().WithCancellation(cancellationToken)) - { - updateAction(entity); - } - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void UpdateAll(SearchCriteria searchCriteria, TData data, Action updateAction) - { - var query = PrepareQuery(searchCriteria); - foreach(var entity in query) - { - updateAction(entity, data); - } - } - - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public async Task UpdateAllAsync( - SearchCriteria searchCriteria, - TData data, - Action updateAction, - CancellationToken cancellationToken = default) - { - var query = PrepareQuery(searchCriteria); - await foreach (var entity in query.AsAsyncEnumerable().WithCancellation(cancellationToken)) - { - updateAction(entity, data); - } - } - - /// - public void UpdateAll( - SearchCriteria searchCriteria, - ICollection collection, - Func entityIdGet, - Func dataIdGet, - Action updateAction) - where TData : class - { - var query = PrepareQuery(searchCriteria); - foreach (var entity in query) - { - var entityId = entityIdGet(entity); - var data = collection.FirstOrDefault(d => Equals(dataIdGet(d), entityId)); - if (data is not null) - updateAction(entity, data); - else - throw new ArgumentOutOfRangeException( - nameof(collection), - $"The collection does not contain any data with the id '{entityId}'"); - } - } - - /// - public async Task UpdateAllAsync( - SearchCriteria searchCriteria, - ICollection collection, - Func entityIdGet, - Func dataIdGet, - Action updateAction, - CancellationToken cancellationToken = default) - where TData : class - { - var query = PrepareQuery(searchCriteria); - await foreach (var entity in query.AsAsyncEnumerable().WithCancellation(cancellationToken)) - { - var entityId = entityIdGet(entity); - var data = collection.FirstOrDefault(d => Equals(dataIdGet(d), entityId)); - if (data is not null) - updateAction(entity, data); - else - throw new ArgumentOutOfRangeException( - nameof(collection), - $"The collection does not contain any data with the id '{entityId}'"); - } - } -} diff --git a/src/RoyalCode.SmartSearch.EntityFramework/Configurations/ISearchConfigurations.cs b/src/RoyalCode.SmartSearch.EntityFramework/Configurations/ISearchConfigurations.cs index 88caa04..77ddf75 100644 --- a/src/RoyalCode.SmartSearch.EntityFramework/Configurations/ISearchConfigurations.cs +++ b/src/RoyalCode.SmartSearch.EntityFramework/Configurations/ISearchConfigurations.cs @@ -9,12 +9,4 @@ namespace RoyalCode.SmartSearch.EntityFramework.Configurations; /// The type of the database context. public interface ISearchConfigurations : ISearchConfigurations where TDbContext : DbContext -{ - /// - /// Add a search for an entity as a service, related to used by the unit of work. - /// - /// The entity type. - /// The same instance. - ISearchConfigurations Add() - where TEntity : class; -} +{ } diff --git a/src/RoyalCode.SmartSearch.EntityFramework/Configurations/SearchConfigurations.cs b/src/RoyalCode.SmartSearch.EntityFramework/Configurations/SearchConfigurations.cs index 8a05b59..5b27533 100644 --- a/src/RoyalCode.SmartSearch.EntityFramework/Configurations/SearchConfigurations.cs +++ b/src/RoyalCode.SmartSearch.EntityFramework/Configurations/SearchConfigurations.cs @@ -1,8 +1,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Extensions; -using RoyalCode.SmartSearch.Abstractions; -using RoyalCode.SmartSearch.EntityFramework.Internals; +using RoyalCode.SmartSearch.EntityFramework.Services; using RoyalCode.SmartSearch.Linq; namespace RoyalCode.SmartSearch.EntityFramework.Configurations; @@ -20,43 +18,28 @@ public sealed class SearchConfigurations : ISearchConfigurations, PipelineFactory>(); - services.TryAddTransient, SearchManager>(); } /// - public ISearchConfigurations Add() where TEntity : class + public ISearchConfigurations Add() + where TEntity : class { - // add search as a service for the respective context - var searchType = typeof(ISearch<>).MakeGenericType(typeof(TEntity)); - var dbSearchType = typeof(Internals.ISearch<,>).MakeGenericType(typeof(TDbContext), typeof(TEntity)); - var searchImplType = typeof(InternalSearch<,>).MakeGenericType(typeof(TDbContext), typeof(TEntity)); - - services.Add(ServiceDescriptor.Describe( - dbSearchType, - searchImplType, - ServiceLifetime.Transient)); + // add criteria as a service for the respective context + var serviceType = typeof(ICriteria<>).MakeGenericType(typeof(TEntity)); + var implType = typeof(InternalCriteria<,>).MakeGenericType(typeof(TDbContext), typeof(TEntity)); services.Add(ServiceDescriptor.Describe( - searchType, - sp => sp.GetService(dbSearchType)!, + serviceType, + implType, ServiceLifetime.Transient)); - // add all entities as a service for the respective context - var allType = typeof(IAllEntities<>).MakeGenericType(typeof(TEntity)); - var dbAllType = typeof(IAllEntities<,>).MakeGenericType(typeof(TDbContext), typeof(TEntity)); - var allImplType = typeof(InternalAllEntities<,>).MakeGenericType(typeof(TDbContext), typeof(TEntity)); - - services.Add(ServiceDescriptor.Describe( - dbAllType, - allImplType, - ServiceLifetime.Transient)); + // add criteria performer as a service for the respective context + serviceType = typeof(ICriteriaPerformer<,>).MakeGenericType(typeof(TDbContext), typeof(TEntity)); + implType = typeof(CriteriaPerformer<,>).MakeGenericType(typeof(TDbContext), typeof(TEntity)); services.Add(ServiceDescriptor.Describe( - allType, - sp => sp.GetService(dbAllType)!, + serviceType, + implType, ServiceLifetime.Transient)); return this; diff --git a/src/RoyalCode.SmartSearch.EntityFramework/Extensions/EFSearchesExtensions.cs b/src/RoyalCode.SmartSearch.EntityFramework/Extensions/EFSearchesExtensions.cs new file mode 100644 index 0000000..5d6d21e --- /dev/null +++ b/src/RoyalCode.SmartSearch.EntityFramework/Extensions/EFSearchesExtensions.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore.Infrastructure; +using RoyalCode.SmartSearch; +using RoyalCode.SmartSearch.Defaults; +using RoyalCode.SmartSearch.EntityFramework.Services; +using RoyalCode.SmartSearch.Linq.Services; +using RoyalCode.SmartSearch.Linq.Sortings; + +namespace Microsoft.EntityFrameworkCore; + +/// +/// Extensions methods for to create . +/// +public static class EFSearchesExtensions +{ + /// + /// + /// Creates a new for the entity + /// using the used by the unit of work. + /// + /// + /// The entity type to create the criteria for. + /// The to use for performing the searches. + /// A new instance. + public static ICriteria Criteria(this DbContext db) + where TEntity : class + { + var specifierFactory = db.GetService(); + var orderByFactory = db.GetService(); + var selectorFactory = db.GetService(); + + var preparer = new CriteriaPerformer( + db, + specifierFactory, + orderByFactory, + selectorFactory); + + var criteria = new Criteria(preparer); + + return criteria; + } +} diff --git a/src/RoyalCode.SmartSearch.EntityFramework/Extensions/EntityFrameworkSearchesServiceCollectionExtensions.cs b/src/RoyalCode.SmartSearch.EntityFramework/Extensions/EntityFrameworkSearchesServiceCollectionExtensions.cs index 701605b..e84d2ee 100644 --- a/src/RoyalCode.SmartSearch.EntityFramework/Extensions/EntityFrameworkSearchesServiceCollectionExtensions.cs +++ b/src/RoyalCode.SmartSearch.EntityFramework/Extensions/EntityFrameworkSearchesServiceCollectionExtensions.cs @@ -1,14 +1,9 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.DependencyInjection.Extensions; -using RoyalCode.SmartSearch.Abstractions; -using RoyalCode.SmartSearch.EntityFramework; +using RoyalCode.SmartSearch; using RoyalCode.SmartSearch.EntityFramework.Configurations; -using RoyalCode.SmartSearch.EntityFramework.Internals; +using RoyalCode.SmartSearch.EntityFramework.Services; using RoyalCode.SmartSearch.Linq; -using RoyalCode.SmartSearch.Linq.Filter; -using RoyalCode.SmartSearch.Linq.Selector; -using RoyalCode.SmartSearch.Linq.Sorter; namespace Microsoft.Extensions.DependencyInjection; @@ -19,12 +14,12 @@ public static class EntityFrameworkSearchesServiceCollectionExtensions { /// /// - /// Add services for and + /// Add services for /// using entity framework and defining the for performing the searches. /// /// /// You can also use a to create - /// and + /// /// of entities related to the . /// /// @@ -41,18 +36,20 @@ public static IServiceCollection AddEntityFrameworkSearches(this ISe { ArgumentNullException.ThrowIfNull(configureAction); + services.AddSearchManager(); + configureAction(new SearchConfigurations(services)); return services; } /// /// - /// Adds services to work with and + /// Adds services to work with and /// using the entity framework. /// /// /// It will be necessary to use or - /// to create and + /// to create and /// of entities related to the . /// /// @@ -62,34 +59,13 @@ public static IServiceCollection AddEntityFrameworkSearches(this ISe public static IServiceCollection AddSearchManager(this IServiceCollection services) where TDbContext : DbContext { + if (services.Any(d => d.ServiceType == typeof(ISearchManager))) + return services; + services.AddSmartSearchLinq(); - services.TryAddTransient, PipelineFactory>(); services.TryAddTransient, SearchManager>(); services.TryAddTransient>(); return services; } - - /// - /// - /// Creates a new for the entity - /// using the used by the unit of work. - /// - /// - /// - /// - /// - public static ISearch Search(this DbContext db) - where TEntity : class - { - var specifierFactory = db.GetService(); - var orderByFactory = db.GetService(); - var selectorFactory = db.GetService(); - - var pipelineFacotry = new PipelineFactory(db, specifierFactory, orderByFactory, selectorFactory); - - var search = new InternalSearch(pipelineFacotry); - - return search; - } } diff --git a/src/RoyalCode.SmartSearch.EntityFramework/Internals/IAllEntities.cs b/src/RoyalCode.SmartSearch.EntityFramework/Internals/IAllEntities.cs deleted file mode 100644 index 627e09c..0000000 --- a/src/RoyalCode.SmartSearch.EntityFramework/Internals/IAllEntities.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using RoyalCode.SmartSearch.Abstractions; - -#pragma warning disable S2326 // Unused type parameters should be removed - -namespace RoyalCode.SmartSearch.EntityFramework.Internals; - -/// -/// -/// Represents a search for all entities of a specific type using the Entity Framework Core. -/// -/// -/// The type of the database context. -/// The type of the entity. -public interface IAllEntities : IAllEntities - where TEntity : class - where TDbContext : DbContext -{ } \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.EntityFramework/Internals/IPipelineFactory.cs b/src/RoyalCode.SmartSearch.EntityFramework/Internals/IPipelineFactory.cs deleted file mode 100644 index d791c03..0000000 --- a/src/RoyalCode.SmartSearch.EntityFramework/Internals/IPipelineFactory.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using RoyalCode.SmartSearch.Core.Pipeline; - -#pragma warning disable S2326 // Unused type parameters should be removed - -namespace RoyalCode.SmartSearch.EntityFramework.Internals; - -/// -/// Factory to create search pipelines and searches for all entities using a specific . -/// -/// -/// The type to use for the search pipelines and searches for all entities. -/// -public interface IPipelineFactory : IPipelineFactory - where TDbContext : DbContext -{ } diff --git a/src/RoyalCode.SmartSearch.EntityFramework/Internals/ISearch.cs b/src/RoyalCode.SmartSearch.EntityFramework/Internals/ISearch.cs deleted file mode 100644 index 274b897..0000000 --- a/src/RoyalCode.SmartSearch.EntityFramework/Internals/ISearch.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using RoyalCode.SmartSearch.Abstractions; - -#pragma warning disable S2326 // Unused type parameters should be removed - -namespace RoyalCode.SmartSearch.EntityFramework.Internals; - -/// -/// -/// Represents a search for a specific entity type using the Entity Framework Core. -/// -/// -/// The type of the database context. -/// The type of the entity. -public interface ISearch : ISearch - where TEntity : class - where TDbContext : DbContext -{ } diff --git a/src/RoyalCode.SmartSearch.EntityFramework/Internals/InternalAllEntities.cs b/src/RoyalCode.SmartSearch.EntityFramework/Internals/InternalAllEntities.cs deleted file mode 100644 index f3750de..0000000 --- a/src/RoyalCode.SmartSearch.EntityFramework/Internals/InternalAllEntities.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using RoyalCode.SmartSearch.Core.Pipeline; - -namespace RoyalCode.SmartSearch.EntityFramework.Internals; - -internal sealed class InternalAllEntities : AllEntities, IAllEntities - where TEntity : class - where TDbContext : DbContext -{ - public InternalAllEntities(IPipelineFactory factory) : base(factory) { } -} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.EntityFramework/Internals/InternalSearch.cs b/src/RoyalCode.SmartSearch.EntityFramework/Internals/InternalSearch.cs deleted file mode 100644 index 594a3e3..0000000 --- a/src/RoyalCode.SmartSearch.EntityFramework/Internals/InternalSearch.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using RoyalCode.SmartSearch.Core.Pipeline; - -namespace RoyalCode.SmartSearch.EntityFramework.Internals; - -internal sealed class InternalSearch : Search, ISearch - where TEntity : class - where TDbContext : DbContext -{ - public InternalSearch(IPipelineFactory factory) : base(factory) { } -} diff --git a/src/RoyalCode.SmartSearch.EntityFramework/PipelineFactory.cs b/src/RoyalCode.SmartSearch.EntityFramework/PipelineFactory.cs deleted file mode 100644 index fb5e5b9..0000000 --- a/src/RoyalCode.SmartSearch.EntityFramework/PipelineFactory.cs +++ /dev/null @@ -1,69 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using RoyalCode.OperationHint.Abstractions; -using RoyalCode.SmartSearch.Core.Pipeline; -using RoyalCode.SmartSearch.EntityFramework.Internals; -using RoyalCode.SmartSearch.Linq.Filter; -using RoyalCode.SmartSearch.Linq.Selector; -using RoyalCode.SmartSearch.Linq.Sorter; - -namespace RoyalCode.SmartSearch.EntityFramework; - -/// -public sealed class PipelineFactory : IPipelineFactory - where TDbContext : DbContext -{ - private readonly TDbContext db; - private readonly ISpecifierFactory specifierFactory; - private readonly IOrderByProvider orderByProvider; - private readonly ISelectorFactory selectorFactory; - private readonly IHintPerformer? hintPerformer; - - /// - /// - /// Create a new instance of . - /// - /// - /// The database context. - /// The specifier factory. - /// The order by provider. - /// The selector factory. - /// Optional, the hint performer. - public PipelineFactory( - TDbContext db, - ISpecifierFactory specifierFactory, - IOrderByProvider orderByProvider, - ISelectorFactory selectorFactory, - IHintPerformer? hintPerformer = null) - { - this.db = db; - this.specifierFactory = specifierFactory; - this.orderByProvider = orderByProvider; - this.selectorFactory = selectorFactory; - this.hintPerformer = hintPerformer; - } - - /// - public ISearchPipeline Create() where TEntity : class - { - var queryableProvider = new QueryableProvider(db); - var sorter = new DefaultSorter(orderByProvider); - return new SearchPipeline(queryableProvider, specifierFactory, sorter); - } - - /// - public ISearchPipeline Create() where TEntity : class where TDto : class - { - var queryableProvider = new QueryableProvider(db); - var sorter = new DefaultSorter(orderByProvider); - var selector = selectorFactory.Create(); - return new SearchPipeline(queryableProvider, specifierFactory, sorter, selector); - } - - /// - public IAllEntitiesPipeline CreateAllEntities() where TEntity : class - { - var queryableProvider = new QueryableProvider(db, true, hintPerformer); - var sorter = new DefaultSorter(orderByProvider); - return new AllEntitiesPipeline(queryableProvider, specifierFactory, sorter); - } -} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.EntityFramework/QueryableProvider.cs b/src/RoyalCode.SmartSearch.EntityFramework/QueryableProvider.cs deleted file mode 100644 index 3e6f5c6..0000000 --- a/src/RoyalCode.SmartSearch.EntityFramework/QueryableProvider.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using RoyalCode.OperationHint.Abstractions; -using RoyalCode.SmartSearch.Linq; - -namespace RoyalCode.SmartSearch.EntityFramework; - -/// -internal sealed class QueryableProvider : IQueryableProvider - where TDbContext : DbContext - where TEntity : class -{ - private readonly TDbContext db; - private readonly bool tracking; - private readonly IHintPerformer? hintPerformer; - - public QueryableProvider(TDbContext db, bool tracking = false, IHintPerformer? hintPerformer = null) - { - this.db = db; - this.tracking = tracking; - this.hintPerformer = hintPerformer; - } - - /// - public IQueryable GetQueryable() - { - if (tracking) - { - IQueryable query = db.Set(); - return hintPerformer is null - ? query - : hintPerformer.Perform(query); - } - - return db.Set().AsNoTracking(); - } - - /// - public IRemovable GetRemovable() - { - return new Removable(db); - } -} diff --git a/src/RoyalCode.SmartSearch.EntityFramework/Removable.cs b/src/RoyalCode.SmartSearch.EntityFramework/Removable.cs deleted file mode 100644 index 41b7238..0000000 --- a/src/RoyalCode.SmartSearch.EntityFramework/Removable.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using RoyalCode.SmartSearch.Linq; - -namespace RoyalCode.SmartSearch.EntityFramework; - -/// -internal sealed class Removable : IRemovable - where TEntity : class -{ - private readonly DbContext db; - - public Removable(DbContext db) - { - this.db = db; - } - - /// - public void RemoveAll(IQueryable entities) - { - db.RemoveRange(entities); - } - - /// - public async Task RemoveAllAsync(IQueryable entities, CancellationToken cancellationToken = default) - { - db.RemoveRange(await entities.ToListAsync(cancellationToken: cancellationToken)); - } -} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.EntityFramework/RoyalCode.SmartSearch.EntityFramework.csproj b/src/RoyalCode.SmartSearch.EntityFramework/RoyalCode.SmartSearch.EntityFramework.csproj index 208e70e..1c7e9c6 100644 --- a/src/RoyalCode.SmartSearch.EntityFramework/RoyalCode.SmartSearch.EntityFramework.csproj +++ b/src/RoyalCode.SmartSearch.EntityFramework/RoyalCode.SmartSearch.EntityFramework.csproj @@ -18,7 +18,7 @@ - + diff --git a/src/RoyalCode.SmartSearch.EntityFramework/SearchManager.cs b/src/RoyalCode.SmartSearch.EntityFramework/SearchManager.cs deleted file mode 100644 index 857fee6..0000000 --- a/src/RoyalCode.SmartSearch.EntityFramework/SearchManager.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using RoyalCode.SmartSearch.Abstractions; -using RoyalCode.SmartSearch.EntityFramework.Internals; - -#pragma warning disable S2326 // Unused type parameters should be removed - -namespace RoyalCode.SmartSearch.EntityFramework; - -/// -/// Default implementation for . -/// -/// -public sealed class SearchManager : ISearchManager - where TDbContext : DbContext -{ - private readonly IPipelineFactory pipelineFactory; - - /// - /// Creates a new search manager with the pipeline factory for the . - /// - /// - public SearchManager(IPipelineFactory pipelineFactory) - { - this.pipelineFactory = pipelineFactory; - } - - /// - public IAllEntities All() where TEntity : class - { - return new InternalAllEntities(pipelineFactory); - } - - /// - public ISearch Search() where TEntity : class - { - return new InternalSearch(pipelineFactory); - } -} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.EntityFramework/SearchPipeline.cs b/src/RoyalCode.SmartSearch.EntityFramework/SearchPipeline.cs deleted file mode 100644 index 16d8e94..0000000 --- a/src/RoyalCode.SmartSearch.EntityFramework/SearchPipeline.cs +++ /dev/null @@ -1,291 +0,0 @@ -using System.Linq.Expressions; -using System.Runtime.CompilerServices; -using Microsoft.EntityFrameworkCore; -using RoyalCode.SmartSearch.Abstractions; -using RoyalCode.SmartSearch.Core; -using RoyalCode.SmartSearch.Core.Pipeline; -using RoyalCode.SmartSearch.Linq; -using RoyalCode.SmartSearch.Linq.Filter; - -namespace RoyalCode.SmartSearch.EntityFramework; - -#pragma warning disable S3358 // ifs ternarios should not be nested - -/// -/// -/// Default implementation of . -/// -/// -/// The entity type. -public sealed class SearchPipeline : SearchPipelineBase, ISearchPipeline - where TEntity : class -{ - /// - public SearchPipeline( - IQueryableProvider queryableProvider, - ISpecifierFactory specifierFactory, - ISorter sorter) - : base( - queryableProvider, - specifierFactory, - sorter) - { } - - /// - public IResultList Execute(SearchCriteria criteria) - { - var sortedQuery = PrepareQuery(criteria); - - var executableQuery = criteria.Paginate - ? sortedQuery - .Skip(criteria.GetSkipCount()) - .Take(criteria.ItemsPerPage + 1) - : sortedQuery; - - var list = executableQuery.ToList(); - var hasNextPage = list.Count > criteria.ItemsPerPage; - var items = hasNextPage && criteria.Paginate ? list.Take(criteria.ItemsPerPage).ToList() : list; - - var count = criteria.LastCount > 0 - ? criteria.LastCount - : criteria.UseCount - ? hasNextPage - ? sortedQuery.Count() - : criteria.GetSkipCount() + list.Count - : 0; - - var pages = CountPages(count, criteria.ItemsPerPage); - - var result = new ResultList() - { - Page = criteria.GetPageNumber(), - Count = count, - ItemsPerPage = criteria.ItemsPerPage, - Pages = pages, - Sortings = criteria.Sortings, - Projections = [], - Items = items - }; - - return result; - } - - /// - public async Task> ExecuteAsync(SearchCriteria criteria, CancellationToken token) - { - var sortedQuery = PrepareQuery(criteria); - - var executableQuery = criteria.Paginate - ? sortedQuery - .Skip(criteria.GetSkipCount()) - .Take(criteria.ItemsPerPage + 1) - : sortedQuery; - - var list = await executableQuery.ToListAsync(token); - var hasNextPage = list.Count > criteria.ItemsPerPage; - var items = hasNextPage && criteria.Paginate ? list.Take(criteria.ItemsPerPage).ToList() : list; - - var count = criteria.LastCount > 0 - ? criteria.LastCount - : criteria.UseCount - ? hasNextPage - ? await sortedQuery.CountAsync(token) - : criteria.GetSkipCount() + list.Count - : 0; - - var pages = CountPages(count, criteria.ItemsPerPage); - - var result = new ResultList() - { - Page = criteria.GetPageNumber(), - Count = count, - ItemsPerPage = criteria.ItemsPerPage, - Pages = pages, - Sortings = criteria.Sortings, - Projections = [], - Items = items - }; - - return result; - } - - /// - public async Task> AsyncExecuteAsync(SearchCriteria criteria, CancellationToken token) - { - var sortedQuery = PrepareQuery(criteria); - - var executableQuery = criteria.Paginate - ? sortedQuery - .Skip(criteria.GetSkipCount()) - .Take(criteria.ItemsPerPage) - : sortedQuery; - - var items = executableQuery.AsAsyncEnumerable(); - - var count = criteria.LastCount > 0 - ? criteria.LastCount - : criteria.UseCount - ? await sortedQuery.CountAsync(token) - : 0; - - var pages = CountPages(count, criteria.ItemsPerPage); - - var result = new AsyncResultList() - { - Page = criteria.GetPageNumber(), - Count = count, - ItemsPerPage = criteria.ItemsPerPage, - Pages = pages, - Sortings = criteria.Sortings, - Projections = [], - Items = items - }; - - return result; - } -} - -/// -/// -/// Default implementation of that selects a DTO from the entity. -/// -/// -/// The entity type. -/// The selected DTO type. -public sealed class SearchPipeline : SearchPipelineBase, ISearchPipeline - where TEntity : class - where TDto : class -{ - private readonly ISelector? selector; - - /// - public SearchPipeline( - IQueryableProvider queryableProvider, - ISpecifierFactory specifierFactory, - ISorter sorter, - ISelector? selector = null) - : base( - queryableProvider, - specifierFactory, - sorter) - { - this.selector = selector; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private IQueryable Select(IQueryable query, SearchCriteria criteria) - { - var selectExpression = (Expression>?)criteria.Select?.SelectExpression - ?? selector?.GetSelectExpression() - ?? throw new SelectorNotFoundException(typeof(TEntity), typeof(TDto)); - - return query.Select(selectExpression); - } - - /// - public IResultList Execute(SearchCriteria criteria) - { - var sortedQuery = PrepareQuery(criteria); - var selectQuery = Select(sortedQuery, criteria); - var executableQuery = criteria.Paginate - ? selectQuery.Skip(criteria.GetSkipCount()).Take(criteria.ItemsPerPage + 1) - : selectQuery; - - var list = executableQuery.ToList(); - var hasNextPage = list.Count > criteria.ItemsPerPage; - var items = hasNextPage && criteria.Paginate ? list.Take(criteria.ItemsPerPage).ToList() : list; - - var count = criteria.LastCount > 0 - ? criteria.LastCount - : criteria.UseCount - ? hasNextPage - ? sortedQuery.Count() - : criteria.GetSkipCount() + list.Count - : 0; - - var pages = CountPages(count, criteria.ItemsPerPage); - - var result = new ResultList() - { - Page = criteria.GetPageNumber(), - Count = count, - ItemsPerPage = criteria.ItemsPerPage, - Pages = pages, - Sortings = criteria.Sortings, - Projections = [], - Items = items - }; - - return result; - } - - /// - public async Task> ExecuteAsync(SearchCriteria criteria, CancellationToken token) - { - var sortedQuery = PrepareQuery(criteria); - var selectQuery = Select(sortedQuery, criteria); - var executableQuery = criteria.Paginate - ? selectQuery.Skip(criteria.GetSkipCount()).Take(criteria.ItemsPerPage + 1) - : selectQuery; - - var list = await executableQuery.ToListAsync(token); - var hasNextPage = list.Count > criteria.ItemsPerPage; - var items = hasNextPage && criteria.Paginate ? list.Take(criteria.ItemsPerPage).ToList() : list; - - var count = criteria.LastCount > 0 - ? criteria.LastCount - : criteria.UseCount - ? hasNextPage - ? await sortedQuery.CountAsync(token) - : criteria.GetSkipCount() + list.Count - : 0; - - var pages = CountPages(count, criteria.ItemsPerPage); - - var result = new ResultList() - { - Page = criteria.GetPageNumber(), - Count = count, - ItemsPerPage = criteria.ItemsPerPage, - Pages = pages, - Sortings = criteria.Sortings, - Projections = [], - Items = items - }; - - return result; - } - - /// - public async Task> AsyncExecuteAsync(SearchCriteria criteria, CancellationToken token) - { - var sortedQuery = PrepareQuery(criteria); - var selectQuery = Select(sortedQuery, criteria); - var executableQuery = criteria.Paginate - ? selectQuery.Skip(criteria.GetSkipCount()).Take(criteria.ItemsPerPage) - : selectQuery; - - var items = executableQuery.AsAsyncEnumerable(); - - var count = criteria.LastCount > 0 - ? criteria.LastCount - : criteria.UseCount - ? await sortedQuery.CountAsync(token) - : 0; - - var pages = CountPages(count, criteria.ItemsPerPage); - - var result = new AsyncResultList() - { - Page = criteria.GetPageNumber(), - Count = count, - ItemsPerPage = criteria.ItemsPerPage, - Pages = pages, - Sortings = criteria.Sortings, - Projections = [], - Items = items - }; - - return result; - } -} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.EntityFramework/SearchPipelineBase.cs b/src/RoyalCode.SmartSearch.EntityFramework/SearchPipelineBase.cs deleted file mode 100644 index 3a6733d..0000000 --- a/src/RoyalCode.SmartSearch.EntityFramework/SearchPipelineBase.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System.Runtime.CompilerServices; -using RoyalCode.SmartSearch.Core; -using RoyalCode.SmartSearch.Core.Pipeline; -using RoyalCode.SmartSearch.Linq; -using RoyalCode.SmartSearch.Linq.Filter; - -namespace RoyalCode.SmartSearch.EntityFramework; - -/// -/// -/// A base implementation for to share commons methods/operations. -/// -/// -/// The query source model type. -public abstract class SearchPipelineBase - where TEntity : class -{ - /// - /// - /// The component that will provide the to be used in the pipeline. - /// - /// - protected readonly IQueryableProvider queryableProvider; - - /// - /// - /// Factory to create the to be used in the pipeline. - /// - /// - protected readonly ISpecifierFactory specifierFactory; - - /// - /// - /// Component to apply the order by instructions. - /// - /// - protected readonly ISorter sorter; - - /// - /// Creates a new search pipeline that receives the dependencies. - /// - /// Provides the query. - /// Provides the specifiers. - /// Sort the query. - protected SearchPipelineBase( - IQueryableProvider queryableProvider, - ISpecifierFactory specifierFactory, - ISorter sorter) - { - this.queryableProvider = queryableProvider; - this.specifierFactory = specifierFactory; - this.sorter = sorter; - } - - /// - /// Creates the query, apply the filters and sort. - /// - /// Search criteria. - /// A prepared query. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected IQueryable PrepareQuery(SearchCriteria criteria) - { - var baseQuery = queryableProvider.GetQueryable(); - - var queryFilters = criteria.Filters; - if (queryFilters.Count is not 0) - { - var handler = new SpecifierHandler(specifierFactory, baseQuery); - foreach (var searchFilter in queryFilters) - { - searchFilter.ApplyFilter(handler); - } - - baseQuery = handler.Query; - } - - if (criteria.Sortings.Count is not 0) - return sorter.OrderBy(baseQuery, criteria.Sortings); - - return criteria.Paginate - ? sorter.DefaultOrderBy(baseQuery) - : baseQuery; - } - - /// - /// Count the pages. - /// - /// The record count. - /// How many items is listed per page. - /// The total number of pages. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected static int CountPages(int count, int itemsPerPage) - { - return count == 0 || itemsPerPage < 1 - ? 0 - : (int)Math.Floor((double)count / itemsPerPage); - } -} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.EntityFramework/Services/CriteriaPerformer.cs b/src/RoyalCode.SmartSearch.EntityFramework/Services/CriteriaPerformer.cs new file mode 100644 index 0000000..f20c90b --- /dev/null +++ b/src/RoyalCode.SmartSearch.EntityFramework/Services/CriteriaPerformer.cs @@ -0,0 +1,49 @@ +using Microsoft.EntityFrameworkCore; +using RoyalCode.SmartSearch.Linq.Services; +using RoyalCode.SmartSearch.Linq.Sortings; + +namespace RoyalCode.SmartSearch.EntityFramework.Services; + +/// +/// Provides functionality to prepare queries based on specified criteria for a given entity type. +/// +/// +/// This class is designed to facilitate the creation of queries by applying filters, sorting, and +/// selection criteria to an underlying data source. +/// It uses various providers and factories to construct the query dynamically. +/// +/// +/// The type of the used to access the database. +/// +/// +/// The type of the entity for which queries are prepared. Must be a reference type. +/// +public sealed class CriteriaPerformer : CriteriaPerformerBase, ICriteriaPerformer + where TDbContext : DbContext + where TEntity : class +{ + private readonly TDbContext db; + + /// + /// Creates a new instance of . + /// + /// The to get the queryable for the entity type. + /// The factory to create specifiers for the query. + /// The provider for ordering the results. + /// The factory for selecting results. + public CriteriaPerformer( + TDbContext db, + ISpecifierFactory specifierFactory, + IOrderByProvider orderByProvider, + ISelectorFactory selectorFactory) + : base(specifierFactory, orderByProvider, selectorFactory) + { + this.db = db; + } + + /// + protected override IQueryable GetQueryable(bool trackingEnabled) + => trackingEnabled + ? db.Set() + : db.Set().AsNoTracking(); +} diff --git a/src/RoyalCode.SmartSearch.EntityFramework/Services/CriteriaPerformerBase.cs b/src/RoyalCode.SmartSearch.EntityFramework/Services/CriteriaPerformerBase.cs new file mode 100644 index 0000000..ab11f53 --- /dev/null +++ b/src/RoyalCode.SmartSearch.EntityFramework/Services/CriteriaPerformerBase.cs @@ -0,0 +1,87 @@ +using Microsoft.EntityFrameworkCore; +using RoyalCode.SmartSearch.Defaults; +using RoyalCode.SmartSearch.Linq.Services; +using RoyalCode.SmartSearch.Linq.Sortings; +using RoyalCode.SmartSearch.Services; + +namespace RoyalCode.SmartSearch.EntityFramework.Services; + +/// +/// Provides functionality to prepare queries based on specified criteria for a given entity type. +/// +/// +/// This class is designed to facilitate the creation of queries by applying filters, sorting, and +/// selection criteria to an underlying data source. +/// It uses various providers and factories to construct the query dynamically. +/// +/// +/// The type of the entity for which queries are prepared. Must be a reference type. +/// +public abstract class CriteriaPerformerBase : ICriteriaPerformer + where TEntity : class +{ + private readonly ISpecifierFactory specifierFactory; + private readonly IOrderByProvider orderByProvider; + private readonly ISelectorFactory selectorFactory; + + /// + /// Creates a new instance of . + /// + /// The factory to create specifiers for the query. + /// The provider for ordering the results. + /// The factory for selecting results. + protected CriteriaPerformerBase( + ISpecifierFactory specifierFactory, + IOrderByProvider orderByProvider, + ISelectorFactory selectorFactory) + { + this.specifierFactory = specifierFactory; + this.orderByProvider = orderByProvider; + this.selectorFactory = selectorFactory; + } + + /// + public IPreparedQuery Prepare(CriteriaOptions options) + { + var entities = GetQueryable(options.TrackingEnabled); + + var criteriaQuery = new CriteriaQuery( + entities, + specifierFactory, + orderByProvider, + selectorFactory); + + foreach (var filter in options.Filters) + filter.ApplyFilter(criteriaQuery); + + criteriaQuery.OrderBy(options.Sortings); + criteriaQuery.SetPageSkipTakeCount( + options.GetPageNumber(), + options.GetSkipCount(), + options.GetTakeCount(), + options.UseCount, + options.LastCount); + + return criteriaQuery; + } + + /// + /// Provides an for querying entities of type . + /// + /// + /// Use this method to retrieve a queryable source for entities, optionally enabling or disabling + /// change tracking. Disabling tracking is recommended for scenarios where entities are only read and not + /// modified. + /// + /// + /// A value indicating whether change tracking is enabled for the returned query. + ///
+ /// enables change tracking, allowing entity modifications to be tracked by the context. + ///
+ /// disables change tracking, which can improve performance for read-only operations. + /// + /// + /// An that can be used to query entities of type . + /// + protected abstract IQueryable GetQueryable(bool trackingEnabled); +} diff --git a/src/RoyalCode.SmartSearch.EntityFramework/Services/CriteriaQuery.cs b/src/RoyalCode.SmartSearch.EntityFramework/Services/CriteriaQuery.cs new file mode 100644 index 0000000..2f5a1ac --- /dev/null +++ b/src/RoyalCode.SmartSearch.EntityFramework/Services/CriteriaQuery.cs @@ -0,0 +1,298 @@ +using Microsoft.EntityFrameworkCore; +using RoyalCode.SmartSearch.Filtering; +using RoyalCode.SmartSearch.Linq.Services; +using RoyalCode.SmartSearch.Linq.Sortings; +using RoyalCode.SmartSearch.Mappings; +using RoyalCode.SmartSearch.Services; +using System.Runtime.CompilerServices; + +#pragma warning disable S3358 // ifs ternaries should not be nested + +namespace RoyalCode.SmartSearch.EntityFramework.Services; + +/// +/// +/// Represents a query builder for constructing and executing queries +/// with filtering, sorting, pagination, and projection capabilities for a specified entity type. +/// +/// +/// +/// +/// The class provides a flexible and extensible way to build +/// queries by combining filtering criteria, sorting rules, pagination settings, and projection expressions. +///
+/// It is designed to work with LINQ and supports both synchronous and asynchronous query execution. +///
+/// This class is particularly useful for scenarios where dynamic query construction is required, +/// such as implementing search functionality or handling complex data retrieval requirements. +///
+/// +/// Thread safety: Instances of this class are not guaranteed to be thread-safe. +/// If multiple threads need to access the same instance, synchronization mechanisms should be used. +/// +///
+/// The type of the entity being queried. Must be a reference type. +public class CriteriaQuery : IPreparedQuery, IFilterHandler + where TEntity : class +{ + private readonly ISpecifierFactory specifierFactory; + private readonly IOrderByProvider orderByProvider; + private readonly ISelectorFactory selectorFactory; + + private IQueryable query; + + private int pageNumber; + private int skip; + private int take; + private bool count; + private int lastCount; + + private readonly List appliedSorting = []; + + /// + /// Initializes a new instance of the class, + /// providing the necessary components for building and executing queries with specified criteria. + /// + /// + /// This constructor is designed to facilitate the creation of complex queries by combining + /// filtering, ordering, and projection capabilities. Ensure that all dependencies are properly + /// initialized before passing them to this constructor. + /// + /// The initial queryable instance representing the data source for the entity type. + /// The factory used to create specifiers for filtering criteria. + /// The provider used to define ordering rules for the query results. + /// The factory used to create selectors for projecting query results. + public CriteriaQuery( + IQueryable query, + ISpecifierFactory specifierFactory, + IOrderByProvider orderByProvider, + ISelectorFactory selectorFactory) + { + this.query = query; + this.specifierFactory = specifierFactory; + this.orderByProvider = orderByProvider; + this.selectorFactory = selectorFactory; + } + + /// + public void Specify(TFilter filter) + where TFilter : class + { + var specifier = specifierFactory.GetSpecifier(); + query = specifier.Specify(query, filter); + } + + internal void OrderBy(IReadOnlyList sortings) + { + var orderByBuilder = new OrderByBuilder(query); + + foreach (var sorting in sortings) + { + var handler = orderByProvider.GetHandler(sorting.OrderBy); + if (handler is null) + continue; + + appliedSorting.Add(sorting); + + orderByBuilder.CurrentDirection = sorting.Direction; + orderByBuilder = handler.Handle(orderByBuilder); + } + + query = orderByBuilder.OrderedQueryable; + + CheckSorting(); + } + + internal void SetPageSkipTakeCount(int pageNumber, int skip, int take, bool count, int lastCount) + { + this.pageNumber = pageNumber; + this.skip = skip; + this.take = take; + this.count = count; + this.lastCount = lastCount; + + CheckSorting(); + } + + /// + public IPreparedQuery Select(ISearchSelect select) + where TDto : class + { + var expression = select.SelectExpression ?? selectorFactory.Create().GetSelectExpression(); + var queryable = query.Select(expression); + var criteriaQuery = new CriteriaQuery( + queryable, + specifierFactory, + orderByProvider, + selectorFactory); + + criteriaQuery.SetPageSkipTakeCount(pageNumber, skip, take, count, lastCount); + return criteriaQuery; + } + + private void CheckSorting() + { + if (appliedSorting.Count is 0 && skip > 0) + { + var orderByBuilder = new OrderByBuilder(query); + var handler = orderByProvider.GetDefaultHandler(); + orderByBuilder = handler.Handle(orderByBuilder); + query = orderByBuilder.OrderedQueryable; + appliedSorting.Add(DefaultSorting.Instance); + } + } + + private IQueryable GetQueryableWithSkip() + { + var queryable = query; + if (skip > 0) + queryable = queryable.Skip(skip); + return queryable; + } + + private IQueryable GetQueryableWithSkipAndTake(bool count) + { + var queryable = GetQueryableWithSkip(); + if (take > 0) + queryable = queryable.Take(take + (count ? 1 : 0)); + return queryable; + } + + private int CountPages(int count) + { + return count == 0 || take < 1 + ? 0 + : (int)Math.Floor((double)count / take); + } + + /// + public bool Exists() + => GetQueryableWithSkip().Any(); + + /// + public Task ExistsAsync(CancellationToken ct = default) + => GetQueryableWithSkip().AnyAsync(ct); + + /// + public TEntity? FirstOrDefault() + => GetQueryableWithSkip().FirstOrDefault(); + + /// + public Task FirstOrDefaultAsync(CancellationToken ct = default) + => GetQueryableWithSkip().FirstOrDefaultAsync(ct); + + /// + public TEntity Single() + => GetQueryableWithSkip().Single(); + + /// + public Task SingleAsync(CancellationToken ct = default) + => GetQueryableWithSkip().SingleAsync(ct); + + /// + public IReadOnlyList ToList() => GetQueryableWithSkipAndTake(false).ToList(); + + /// + public async Task> ToListAsync(CancellationToken ct) + => await GetQueryableWithSkipAndTake(false).ToListAsync(ct); + + /// + public IResultList ToResultList() + { + var executableQuery = GetQueryableWithSkipAndTake(true); + + var items = executableQuery.ToList(); + var hasNextPage = items.Count > take; + if (hasNextPage) + items = items.Take(take).ToList(); + + var queryCount = lastCount > 0 + ? lastCount + : count + ? hasNextPage + ? query.Count() + : skip + items.Count + : 0; + + var pages = CountPages(queryCount); + + var result = new ResultList() + { + Page = pageNumber, + Count = queryCount, + ItemsPerPage = take, + Pages = pages, + Skipped = skip, + Taken = hasNextPage ? take : items.Count, + Sortings = appliedSorting, + Items = items + }; + + return result; + } + + /// + public async Task> ToResultListAsync(CancellationToken ct) + { + var executableQuery = GetQueryableWithSkipAndTake(true); + + var items = await executableQuery.ToListAsync(ct); + var hasNextPage = items.Count > take; + if (hasNextPage) + items = items.Take(take).ToList(); + + var queryCount = lastCount > 0 + ? lastCount + : count + ? hasNextPage + ? await query.CountAsync(ct) + : skip + items.Count + : 0; + + var pages = CountPages(queryCount); + + var result = new ResultList() + { + Page = pageNumber, + Count = queryCount, + ItemsPerPage = take, + Pages = pages, + Skipped = skip, + Taken = hasNextPage ? take : items.Count, + Sortings = appliedSorting, + Items = items + }; + + return result; + } + + /// + public async Task> ToAsyncListAsync(CancellationToken ct) + { + var executableQuery = GetQueryableWithSkipAndTake(false); + + var queryCount = lastCount > 0 + ? lastCount + : count + ? await query.CountAsync(ct) + : 0; + + var items = executableQuery.AsAsyncEnumerable(); + + var pages = CountPages(queryCount); + + var result = new AsyncResultList() + { + Page = pageNumber, + Count = queryCount, + ItemsPerPage = take, + Pages = pages, + Skipped = skip, + Taken = take, + Sortings = appliedSorting, + Items = items + }; + + return result; + } +} diff --git a/src/RoyalCode.SmartSearch.EntityFramework/Services/ICriteriaPerformer.cs b/src/RoyalCode.SmartSearch.EntityFramework/Services/ICriteriaPerformer.cs new file mode 100644 index 0000000..de63158 --- /dev/null +++ b/src/RoyalCode.SmartSearch.EntityFramework/Services/ICriteriaPerformer.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore; +using RoyalCode.SmartSearch.Services; + +namespace RoyalCode.SmartSearch.EntityFramework.Services; + +/// +/// Defines functionality for performing criteria-based operations on entities within a specific database context. +/// +/// +/// The type of the database context, which must derive from . +/// +/// +/// The type of the entity on which criteria-based operations are performed, which must be a reference type. +/// +public interface ICriteriaPerformer : ICriteriaPerformer + where TDbContext : DbContext + where TEntity : class +{ +} diff --git a/src/RoyalCode.SmartSearch.EntityFramework/ISearchManager.cs b/src/RoyalCode.SmartSearch.EntityFramework/Services/ISearchManager.cs similarity index 84% rename from src/RoyalCode.SmartSearch.EntityFramework/ISearchManager.cs rename to src/RoyalCode.SmartSearch.EntityFramework/Services/ISearchManager.cs index 3fe95c1..a5b9813 100644 --- a/src/RoyalCode.SmartSearch.EntityFramework/ISearchManager.cs +++ b/src/RoyalCode.SmartSearch.EntityFramework/Services/ISearchManager.cs @@ -1,9 +1,8 @@ using Microsoft.EntityFrameworkCore; -using RoyalCode.SmartSearch.Abstractions; #pragma warning disable S2326 // Unused type parameters should be removed -namespace RoyalCode.SmartSearch.EntityFramework; +namespace RoyalCode.SmartSearch.EntityFramework.Services; /// /// diff --git a/src/RoyalCode.SmartSearch.EntityFramework/Services/InternalCriteria.cs b/src/RoyalCode.SmartSearch.EntityFramework/Services/InternalCriteria.cs new file mode 100644 index 0000000..ff23743 --- /dev/null +++ b/src/RoyalCode.SmartSearch.EntityFramework/Services/InternalCriteria.cs @@ -0,0 +1,11 @@ +using Microsoft.EntityFrameworkCore; +using RoyalCode.SmartSearch.Defaults; + +namespace RoyalCode.SmartSearch.EntityFramework.Services; + +internal sealed class InternalCriteria : Criteria + where TDbContext : DbContext + where TEntity : class +{ + public InternalCriteria(ICriteriaPerformer performer) : base(performer) { } +} diff --git a/src/RoyalCode.SmartSearch.EntityFramework/Services/SearchManager.cs b/src/RoyalCode.SmartSearch.EntityFramework/Services/SearchManager.cs new file mode 100644 index 0000000..d3b81ae --- /dev/null +++ b/src/RoyalCode.SmartSearch.EntityFramework/Services/SearchManager.cs @@ -0,0 +1,61 @@ +using Microsoft.EntityFrameworkCore; +using RoyalCode.SmartSearch.Linq.Services; +using RoyalCode.SmartSearch.Linq.Sortings; + +#pragma warning disable S2326 // Unused type parameters should be removed + +namespace RoyalCode.SmartSearch.EntityFramework.Services; + +/// +/// Default implementation for . +/// +/// +public sealed class SearchManager : ISearchManager + where TDbContext : DbContext +{ + private readonly TDbContext db; + private readonly ISpecifierFactory specifierFactory; + private readonly IOrderByProvider orderByProvider; + private readonly ISelectorFactory selectorFactory; + + /// + /// Initializes a new instance of the class, + /// providing the necessary dependencies for performing database searches. + /// + /// + /// The database context used to query the underlying data source. Must not be null. + /// + /// + /// The factory responsible for creating specifiers that define search criteria. Must not be null. + /// + /// + /// The provider used to specify ordering rules for search results. Must not be null. + /// + /// + /// The factory responsible for creating selectors that determine which fields to include in the search results. + /// Must not be null. + /// + public SearchManager( + TDbContext db, + ISpecifierFactory specifierFactory, + IOrderByProvider orderByProvider, + ISelectorFactory selectorFactory) + { + this.db = db; + this.specifierFactory = specifierFactory; + this.orderByProvider = orderByProvider; + this.selectorFactory = selectorFactory; + } + + /// + public ICriteria Criteria() where TEntity : class + { + var performer = new CriteriaPerformer( + db, + specifierFactory, + orderByProvider, + selectorFactory); + + return new InternalCriteria(performer); + } +} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Linq/Filter/CriterionResolution.cs b/src/RoyalCode.SmartSearch.Linq/Filtering/CriterionResolution.cs similarity index 95% rename from src/RoyalCode.SmartSearch.Linq/Filter/CriterionResolution.cs rename to src/RoyalCode.SmartSearch.Linq/Filtering/CriterionResolution.cs index b43f0ee..d53c049 100644 --- a/src/RoyalCode.SmartSearch.Linq/Filter/CriterionResolution.cs +++ b/src/RoyalCode.SmartSearch.Linq/Filtering/CriterionResolution.cs @@ -1,9 +1,8 @@ using RoyalCode.Extensions.PropertySelection; -using RoyalCode.SmartSearch.Abstractions; using System.Linq.Expressions; using System.Reflection; -namespace RoyalCode.SmartSearch.Linq.Filter; +namespace RoyalCode.SmartSearch.Linq.Filtering; internal sealed class CriterionResolution { diff --git a/src/RoyalCode.SmartSearch.Linq/Filter/DefaultSpecifierFunctionGenerator.cs b/src/RoyalCode.SmartSearch.Linq/Filtering/DefaultSpecifierFunctionGenerator.cs similarity index 99% rename from src/RoyalCode.SmartSearch.Linq/Filter/DefaultSpecifierFunctionGenerator.cs rename to src/RoyalCode.SmartSearch.Linq/Filtering/DefaultSpecifierFunctionGenerator.cs index 16506b3..31a4ff8 100644 --- a/src/RoyalCode.SmartSearch.Linq/Filter/DefaultSpecifierFunctionGenerator.cs +++ b/src/RoyalCode.SmartSearch.Linq/Filtering/DefaultSpecifierFunctionGenerator.cs @@ -1,11 +1,10 @@ using RoyalCode.Extensions.PropertySelection; -using RoyalCode.SmartSearch.Abstractions; using RoyalCode.SmartSearch.Core.Extensions; using System.Collections; using System.Linq.Expressions; using System.Reflection; -namespace RoyalCode.SmartSearch.Linq.Filter; +namespace RoyalCode.SmartSearch.Linq.Filtering; /// /// diff --git a/src/RoyalCode.SmartSearch.Linq/Filter/DefaultSpecifierGenerator.cs b/src/RoyalCode.SmartSearch.Linq/Filtering/DefaultSpecifierGenerator.cs similarity index 93% rename from src/RoyalCode.SmartSearch.Linq/Filter/DefaultSpecifierGenerator.cs rename to src/RoyalCode.SmartSearch.Linq/Filtering/DefaultSpecifierGenerator.cs index 30a9c18..c9ee9be 100644 --- a/src/RoyalCode.SmartSearch.Linq/Filter/DefaultSpecifierGenerator.cs +++ b/src/RoyalCode.SmartSearch.Linq/Filtering/DefaultSpecifierGenerator.cs @@ -1,5 +1,4 @@ - -namespace RoyalCode.SmartSearch.Linq.Filter; +namespace RoyalCode.SmartSearch.Linq.Filtering; /// /// diff --git a/src/RoyalCode.SmartSearch.Linq/Filter/SpecifierHandler.cs b/src/RoyalCode.SmartSearch.Linq/Filtering/FilterHandler.cs similarity index 73% rename from src/RoyalCode.SmartSearch.Linq/Filter/SpecifierHandler.cs rename to src/RoyalCode.SmartSearch.Linq/Filtering/FilterHandler.cs index 829f34d..2034a8b 100644 --- a/src/RoyalCode.SmartSearch.Linq/Filter/SpecifierHandler.cs +++ b/src/RoyalCode.SmartSearch.Linq/Filtering/FilterHandler.cs @@ -1,17 +1,18 @@ -using RoyalCode.SmartSearch.Core; +using RoyalCode.SmartSearch.Filtering; +using RoyalCode.SmartSearch.Linq.Services; -namespace RoyalCode.SmartSearch.Linq.Filter; +namespace RoyalCode.SmartSearch.Linq.Filtering; /// /// -/// Default implementation of +/// Default implementation of /// that applies the filters to a , /// retrieving the filters specifiers from the , /// which in turn knows how to apply the filter specification to the query. /// /// /// The query model type. -public sealed class SpecifierHandler : ISpecifierHandler +public sealed class FilterHandler : IFilterHandler where TModel : class { private readonly ISpecifierFactory factory; @@ -21,7 +22,7 @@ public sealed class SpecifierHandler : ISpecifierHandler /// /// The specifier factory to create the specifiers. /// The query to apply the filters. - public SpecifierHandler(ISpecifierFactory factory, IQueryable query) + public FilterHandler(ISpecifierFactory factory, IQueryable query) { this.factory = factory; Query = query; @@ -33,7 +34,7 @@ public SpecifierHandler(ISpecifierFactory factory, IQueryable query) public IQueryable Query { get; private set; } /// - public void Handle(TFilter filter) where TFilter : class + public void Specify(TFilter filter) where TFilter : class { var specifier = factory.GetSpecifier(); if (specifier is not null) diff --git a/src/RoyalCode.SmartSearch.Linq/Filtering/ISpecifier.cs b/src/RoyalCode.SmartSearch.Linq/Filtering/ISpecifier.cs new file mode 100644 index 0000000..7248499 --- /dev/null +++ b/src/RoyalCode.SmartSearch.Linq/Filtering/ISpecifier.cs @@ -0,0 +1,20 @@ +namespace RoyalCode.SmartSearch.Linq.Filtering; + +/// +/// Component that applies the filtering conditions to the query, +/// where the query object is a . +/// +/// The model of the . +/// The filter type. +public interface ISpecifier + where TModel : class + where TFilter : class +{ + /// + /// Applies the specified filter to the given query, returning a filtered result set. + /// + /// The initial query to which the filter will be applied. Cannot be null. + /// The filter criteria used to refine the query results. Cannot be null. + /// An containing the filtered results based on the provided criteria. + IQueryable Specify(IQueryable query, TFilter filter); +} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Linq/Filter/ISpecifierFunctionGenerator.cs b/src/RoyalCode.SmartSearch.Linq/Filtering/ISpecifierFunctionGenerator.cs similarity index 93% rename from src/RoyalCode.SmartSearch.Linq/Filter/ISpecifierFunctionGenerator.cs rename to src/RoyalCode.SmartSearch.Linq/Filtering/ISpecifierFunctionGenerator.cs index 6b8fdb1..39340e3 100644 --- a/src/RoyalCode.SmartSearch.Linq/Filter/ISpecifierFunctionGenerator.cs +++ b/src/RoyalCode.SmartSearch.Linq/Filtering/ISpecifierFunctionGenerator.cs @@ -1,5 +1,4 @@ - -namespace RoyalCode.SmartSearch.Linq.Filter; +namespace RoyalCode.SmartSearch.Linq.Filtering; /// /// diff --git a/src/RoyalCode.SmartSearch.Linq/Filter/ISpecifierGenerator.cs b/src/RoyalCode.SmartSearch.Linq/Filtering/ISpecifierGenerator.cs similarity index 93% rename from src/RoyalCode.SmartSearch.Linq/Filter/ISpecifierGenerator.cs rename to src/RoyalCode.SmartSearch.Linq/Filtering/ISpecifierGenerator.cs index c5eeed3..e3facf7 100644 --- a/src/RoyalCode.SmartSearch.Linq/Filter/ISpecifierGenerator.cs +++ b/src/RoyalCode.SmartSearch.Linq/Filtering/ISpecifierGenerator.cs @@ -1,5 +1,4 @@ - -namespace RoyalCode.SmartSearch.Linq.Filter; +namespace RoyalCode.SmartSearch.Linq.Filtering; /// /// diff --git a/src/RoyalCode.SmartSearch.Linq/Filter/ISpecifierGeneratorOptions.cs b/src/RoyalCode.SmartSearch.Linq/Filtering/ISpecifierGeneratorOptions.cs similarity index 96% rename from src/RoyalCode.SmartSearch.Linq/Filter/ISpecifierGeneratorOptions.cs rename to src/RoyalCode.SmartSearch.Linq/Filtering/ISpecifierGeneratorOptions.cs index 6c2ffc4..8657b72 100644 --- a/src/RoyalCode.SmartSearch.Linq/Filter/ISpecifierGeneratorOptions.cs +++ b/src/RoyalCode.SmartSearch.Linq/Filtering/ISpecifierGeneratorOptions.cs @@ -1,6 +1,6 @@ using System.Linq.Expressions; -namespace RoyalCode.SmartSearch.Linq.Filter; +namespace RoyalCode.SmartSearch.Linq.Filtering; /// /// diff --git a/src/RoyalCode.SmartSearch.Linq/Filter/InternalSpecifier.cs b/src/RoyalCode.SmartSearch.Linq/Filtering/InternalSpecifier.cs similarity index 93% rename from src/RoyalCode.SmartSearch.Linq/Filter/InternalSpecifier.cs rename to src/RoyalCode.SmartSearch.Linq/Filtering/InternalSpecifier.cs index abab7ba..f22f3af 100644 --- a/src/RoyalCode.SmartSearch.Linq/Filter/InternalSpecifier.cs +++ b/src/RoyalCode.SmartSearch.Linq/Filtering/InternalSpecifier.cs @@ -1,6 +1,6 @@ using System.Runtime.CompilerServices; -namespace RoyalCode.SmartSearch.Linq.Filter; +namespace RoyalCode.SmartSearch.Linq.Filtering; internal sealed class InternalSpecifier : ISpecifier where TModel : class diff --git a/src/RoyalCode.SmartSearch.Linq/Filter/InternalSpecifierGeneratorOptions.cs b/src/RoyalCode.SmartSearch.Linq/Filtering/InternalSpecifierGeneratorOptions.cs similarity index 97% rename from src/RoyalCode.SmartSearch.Linq/Filter/InternalSpecifierGeneratorOptions.cs rename to src/RoyalCode.SmartSearch.Linq/Filtering/InternalSpecifierGeneratorOptions.cs index 258d291..36ea804 100644 --- a/src/RoyalCode.SmartSearch.Linq/Filter/InternalSpecifierGeneratorOptions.cs +++ b/src/RoyalCode.SmartSearch.Linq/Filtering/InternalSpecifierGeneratorOptions.cs @@ -2,7 +2,7 @@ using System.Linq.Expressions; using System.Reflection; -namespace RoyalCode.SmartSearch.Linq.Filter; +namespace RoyalCode.SmartSearch.Linq.Filtering; internal sealed class InternalSpecifierGeneratorOptions : ISpecifierGeneratorOptions where TModel : class diff --git a/src/RoyalCode.SmartSearch.Linq/Filter/SpecifierFactory.cs b/src/RoyalCode.SmartSearch.Linq/Filtering/SpecifierFactory.cs similarity index 93% rename from src/RoyalCode.SmartSearch.Linq/Filter/SpecifierFactory.cs rename to src/RoyalCode.SmartSearch.Linq/Filtering/SpecifierFactory.cs index 693a23d..d9b7ae2 100644 --- a/src/RoyalCode.SmartSearch.Linq/Filter/SpecifierFactory.cs +++ b/src/RoyalCode.SmartSearch.Linq/Filtering/SpecifierFactory.cs @@ -1,7 +1,8 @@ -using System.Diagnostics.CodeAnalysis; +using RoyalCode.SmartSearch.Linq.Services; +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; -namespace RoyalCode.SmartSearch.Linq.Filter; +namespace RoyalCode.SmartSearch.Linq.Filtering; /// /// @@ -26,7 +27,7 @@ public SpecifierFactory(SpecifiersMap specifiers, this.functionGenerator = functionGenerator; } - public ISpecifier? GetSpecifier() + public ISpecifier GetSpecifier() where TModel : class where TFilter : class { diff --git a/src/RoyalCode.SmartSearch.Linq/Filter/SpecifierGeneratorOptions.cs b/src/RoyalCode.SmartSearch.Linq/Filtering/SpecifierGeneratorOptions.cs similarity index 96% rename from src/RoyalCode.SmartSearch.Linq/Filter/SpecifierGeneratorOptions.cs rename to src/RoyalCode.SmartSearch.Linq/Filtering/SpecifierGeneratorOptions.cs index 8aef7a7..ed780a3 100644 --- a/src/RoyalCode.SmartSearch.Linq/Filter/SpecifierGeneratorOptions.cs +++ b/src/RoyalCode.SmartSearch.Linq/Filtering/SpecifierGeneratorOptions.cs @@ -1,6 +1,6 @@ using System.Diagnostics.CodeAnalysis; -namespace RoyalCode.SmartSearch.Linq.Filter; +namespace RoyalCode.SmartSearch.Linq.Filtering; internal static class SpecifierGeneratorOptions { diff --git a/src/RoyalCode.SmartSearch.Linq/Filter/SpecifierGeneratorPropertyOptions.cs b/src/RoyalCode.SmartSearch.Linq/Filtering/SpecifierGeneratorPropertyOptions.cs similarity index 96% rename from src/RoyalCode.SmartSearch.Linq/Filter/SpecifierGeneratorPropertyOptions.cs rename to src/RoyalCode.SmartSearch.Linq/Filtering/SpecifierGeneratorPropertyOptions.cs index 6df4558..128a77e 100644 --- a/src/RoyalCode.SmartSearch.Linq/Filter/SpecifierGeneratorPropertyOptions.cs +++ b/src/RoyalCode.SmartSearch.Linq/Filtering/SpecifierGeneratorPropertyOptions.cs @@ -1,7 +1,7 @@ using System.Linq.Expressions; using System.Reflection; -namespace RoyalCode.SmartSearch.Linq.Filter; +namespace RoyalCode.SmartSearch.Linq.Filtering; #pragma warning disable S2326 diff --git a/src/RoyalCode.SmartSearch.Linq/Filter/SpecifiersMap.cs b/src/RoyalCode.SmartSearch.Linq/Filtering/SpecifiersMap.cs similarity index 97% rename from src/RoyalCode.SmartSearch.Linq/Filter/SpecifiersMap.cs rename to src/RoyalCode.SmartSearch.Linq/Filtering/SpecifiersMap.cs index beab3f9..718d48a 100644 --- a/src/RoyalCode.SmartSearch.Linq/Filter/SpecifiersMap.cs +++ b/src/RoyalCode.SmartSearch.Linq/Filtering/SpecifiersMap.cs @@ -1,7 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -namespace RoyalCode.SmartSearch.Linq.Filter; +namespace RoyalCode.SmartSearch.Linq.Filtering; /// /// diff --git a/src/RoyalCode.SmartSearch.Linq/IQueryableProvider.cs b/src/RoyalCode.SmartSearch.Linq/IQueryableProvider.cs deleted file mode 100644 index f07e759..0000000 --- a/src/RoyalCode.SmartSearch.Linq/IQueryableProvider.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace RoyalCode.SmartSearch.Linq; - -/// -/// Component to provide a for an entity. -/// -/// The entity type. -public interface IQueryableProvider - where TEntity : class -{ - /// - /// Get a new queryable for the entity. - /// - /// An instance. - IQueryable GetQueryable(); - - /// - /// Get a new queryable for the entity embedded in a removable component. - /// - /// - IRemovable GetRemovable(); -} diff --git a/src/RoyalCode.SmartSearch.Linq/IRemovable.cs b/src/RoyalCode.SmartSearch.Linq/IRemovable.cs deleted file mode 100644 index c8d9382..0000000 --- a/src/RoyalCode.SmartSearch.Linq/IRemovable.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace RoyalCode.SmartSearch.Linq; - -/// -/// Componente to provide a queryable and methods to remove entities from the database. -/// -/// The entity type. -public interface IRemovable - where TEntity : class -{ - /// - /// Remove all entities from the database filter by the queryable. - /// - /// The query to filter the entities to be removed. - void RemoveAll(IQueryable entities); - - /// - /// Remove all entities from the database filter by the queryable. - /// - /// The query to filter the entities to be removed. - /// The cancellation token. - /// A task that represents the asynchronous operation. - Task RemoveAllAsync(IQueryable entities, CancellationToken cancellationToken = default); -} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Linq/ISearchConfigurations.cs b/src/RoyalCode.SmartSearch.Linq/ISearchConfigurations.cs index 65f9550..5a2269a 100644 --- a/src/RoyalCode.SmartSearch.Linq/ISearchConfigurations.cs +++ b/src/RoyalCode.SmartSearch.Linq/ISearchConfigurations.cs @@ -1,6 +1,6 @@ -using RoyalCode.SmartSearch.Linq.Filter; -using RoyalCode.SmartSearch.Linq.Selector; -using RoyalCode.SmartSearch.Linq.Sorter; +using RoyalCode.SmartSearch.Linq.Filtering; +using RoyalCode.SmartSearch.Linq.Mappings; +using RoyalCode.SmartSearch.Linq.Sortings; using System.Linq.Expressions; namespace RoyalCode.SmartSearch.Linq; @@ -125,5 +125,14 @@ ISearchConfigurations AddSelector( SelectorsMap.Instance.Add(selector); return this; } + + /// + /// Add a for an entity as a service, + /// related to the current search configurations and unit of work. + /// + /// The entity type. + /// The same instance. + ISearchConfigurations Add() + where TEntity : class; } diff --git a/src/RoyalCode.SmartSearch.Linq/ISorter.cs b/src/RoyalCode.SmartSearch.Linq/ISorter.cs deleted file mode 100644 index 40d26fb..0000000 --- a/src/RoyalCode.SmartSearch.Linq/ISorter.cs +++ /dev/null @@ -1,41 +0,0 @@ -using RoyalCode.SmartSearch.Abstractions; -using RoyalCode.SmartSearch.Linq.Sorter; - -namespace RoyalCode.SmartSearch.Linq; - -/// -/// -/// Interface of the component to apply the sort operation to a query. -/// -/// -/// It has a default implementation, , -/// which makes use of other abstract components to automate sorting. -/// -/// -/// The query model type. -public interface ISorter - where TModel : class -{ - /// - /// - /// Applies sorting to the - /// according to the definitions of . - /// - /// - /// The query to sort. - /// The soring definitions. - /// A ordered query. - IQueryable OrderBy(IQueryable query, IEnumerable sortings); - - /// - /// - /// Applies a default sort to the query so that it can be possible to execute the paged query. - /// - /// - /// Normally this default sorting is done on top of the Id. - /// - /// - /// The query to sort. - /// A ordered query. - IQueryable DefaultOrderBy(IQueryable query); -} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Linq/ISpecifier.cs b/src/RoyalCode.SmartSearch.Linq/ISpecifier.cs deleted file mode 100644 index 4f118b8..0000000 --- a/src/RoyalCode.SmartSearch.Linq/ISpecifier.cs +++ /dev/null @@ -1,14 +0,0 @@ -using RoyalCode.SmartSearch.Core; - -namespace RoyalCode.SmartSearch.Linq; - -/// -/// Component that applies the filtering conditions to the query, -/// where the query object is a . -/// -/// The model of the . -/// The filter type. -public interface ISpecifier : IFilterSpecifier, TFilter> - where TModel : class - where TFilter : class -{ } \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Linq/Selector/Converters/EnumSelectorPropertyConverter.cs b/src/RoyalCode.SmartSearch.Linq/Mappings/Converters/EnumSelectorPropertyConverter.cs similarity index 97% rename from src/RoyalCode.SmartSearch.Linq/Selector/Converters/EnumSelectorPropertyConverter.cs rename to src/RoyalCode.SmartSearch.Linq/Mappings/Converters/EnumSelectorPropertyConverter.cs index 654cfaf..a7b0178 100644 --- a/src/RoyalCode.SmartSearch.Linq/Selector/Converters/EnumSelectorPropertyConverter.cs +++ b/src/RoyalCode.SmartSearch.Linq/Mappings/Converters/EnumSelectorPropertyConverter.cs @@ -1,7 +1,7 @@ using RoyalCode.Extensions.PropertySelection; using System.Linq.Expressions; -namespace RoyalCode.SmartSearch.Linq.Selector.Converters; +namespace RoyalCode.SmartSearch.Linq.Mappings.Converters; internal sealed class EnumSelectorPropertyConverter : ISelectorPropertyConverter, ISelectorPropertyResolver { diff --git a/src/RoyalCode.SmartSearch.Linq/Selector/Converters/EnumerableSelectorPropertyResolver.cs b/src/RoyalCode.SmartSearch.Linq/Mappings/Converters/EnumerableSelectorPropertyResolver.cs similarity index 98% rename from src/RoyalCode.SmartSearch.Linq/Selector/Converters/EnumerableSelectorPropertyResolver.cs rename to src/RoyalCode.SmartSearch.Linq/Mappings/Converters/EnumerableSelectorPropertyResolver.cs index 850347f..af9b71f 100644 --- a/src/RoyalCode.SmartSearch.Linq/Selector/Converters/EnumerableSelectorPropertyResolver.cs +++ b/src/RoyalCode.SmartSearch.Linq/Mappings/Converters/EnumerableSelectorPropertyResolver.cs @@ -3,7 +3,7 @@ using System.Linq.Expressions; using System.Reflection; -namespace RoyalCode.SmartSearch.Linq.Selector.Converters; +namespace RoyalCode.SmartSearch.Linq.Mappings.Converters; internal sealed class EnumerableSelectorPropertyResolver : ISelectorPropertyResolver { diff --git a/src/RoyalCode.SmartSearch.Linq/Selector/Converters/ISelectResolver.cs b/src/RoyalCode.SmartSearch.Linq/Mappings/Converters/ISelectResolver.cs similarity index 95% rename from src/RoyalCode.SmartSearch.Linq/Selector/Converters/ISelectResolver.cs rename to src/RoyalCode.SmartSearch.Linq/Mappings/Converters/ISelectResolver.cs index 4bf84cd..9f20f4b 100644 --- a/src/RoyalCode.SmartSearch.Linq/Selector/Converters/ISelectResolver.cs +++ b/src/RoyalCode.SmartSearch.Linq/Mappings/Converters/ISelectResolver.cs @@ -1,7 +1,7 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; -namespace RoyalCode.SmartSearch.Linq.Selector.Converters; +namespace RoyalCode.SmartSearch.Linq.Mappings.Converters; /// /// A component that solves the mapping between properties of two types to create a select expression diff --git a/src/RoyalCode.SmartSearch.Linq/Selector/Converters/ISelectorPropertyConverter.cs b/src/RoyalCode.SmartSearch.Linq/Mappings/Converters/ISelectorPropertyConverter.cs similarity index 93% rename from src/RoyalCode.SmartSearch.Linq/Selector/Converters/ISelectorPropertyConverter.cs rename to src/RoyalCode.SmartSearch.Linq/Mappings/Converters/ISelectorPropertyConverter.cs index 98298fd..af3885d 100644 --- a/src/RoyalCode.SmartSearch.Linq/Selector/Converters/ISelectorPropertyConverter.cs +++ b/src/RoyalCode.SmartSearch.Linq/Mappings/Converters/ISelectorPropertyConverter.cs @@ -1,7 +1,8 @@ using RoyalCode.Extensions.PropertySelection; +using RoyalCode.SmartSearch.Linq.Mappings; using System.Linq.Expressions; -namespace RoyalCode.SmartSearch.Linq.Selector.Converters; +namespace RoyalCode.SmartSearch.Linq.Mappings.Converters; /// /// diff --git a/src/RoyalCode.SmartSearch.Linq/Selector/Converters/ISelectorPropertyResolver.cs b/src/RoyalCode.SmartSearch.Linq/Mappings/Converters/ISelectorPropertyResolver.cs similarity index 93% rename from src/RoyalCode.SmartSearch.Linq/Selector/Converters/ISelectorPropertyResolver.cs rename to src/RoyalCode.SmartSearch.Linq/Mappings/Converters/ISelectorPropertyResolver.cs index 620659a..ca2e961 100644 --- a/src/RoyalCode.SmartSearch.Linq/Selector/Converters/ISelectorPropertyResolver.cs +++ b/src/RoyalCode.SmartSearch.Linq/Mappings/Converters/ISelectorPropertyResolver.cs @@ -1,6 +1,6 @@ using RoyalCode.Extensions.PropertySelection; -namespace RoyalCode.SmartSearch.Linq.Selector.Converters; +namespace RoyalCode.SmartSearch.Linq.Mappings.Converters; /// /// diff --git a/src/RoyalCode.SmartSearch.Linq/Selector/Converters/NullableSelectorPropertyConverter.cs b/src/RoyalCode.SmartSearch.Linq/Mappings/Converters/NullableSelectorPropertyConverter.cs similarity index 96% rename from src/RoyalCode.SmartSearch.Linq/Selector/Converters/NullableSelectorPropertyConverter.cs rename to src/RoyalCode.SmartSearch.Linq/Mappings/Converters/NullableSelectorPropertyConverter.cs index 5f40609..961893d 100644 --- a/src/RoyalCode.SmartSearch.Linq/Selector/Converters/NullableSelectorPropertyConverter.cs +++ b/src/RoyalCode.SmartSearch.Linq/Mappings/Converters/NullableSelectorPropertyConverter.cs @@ -2,7 +2,7 @@ using RoyalCode.SmartSearch.Core.Extensions; using System.Linq.Expressions; -namespace RoyalCode.SmartSearch.Linq.Selector.Converters; +namespace RoyalCode.SmartSearch.Linq.Mappings.Converters; internal sealed class NullableSelectorPropertyConverter : ISelectorPropertyConverter, ISelectorPropertyResolver { diff --git a/src/RoyalCode.SmartSearch.Linq/Selector/Converters/SelectResolution.cs b/src/RoyalCode.SmartSearch.Linq/Mappings/Converters/SelectResolution.cs similarity index 88% rename from src/RoyalCode.SmartSearch.Linq/Selector/Converters/SelectResolution.cs rename to src/RoyalCode.SmartSearch.Linq/Mappings/Converters/SelectResolution.cs index 1e24e13..5cbd888 100644 --- a/src/RoyalCode.SmartSearch.Linq/Selector/Converters/SelectResolution.cs +++ b/src/RoyalCode.SmartSearch.Linq/Mappings/Converters/SelectResolution.cs @@ -1,6 +1,6 @@ using RoyalCode.Extensions.PropertySelection; -namespace RoyalCode.SmartSearch.Linq.Selector.Converters; +namespace RoyalCode.SmartSearch.Linq.Mappings.Converters; /// /// It contains the resolved properties for creating a select expression of the . diff --git a/src/RoyalCode.SmartSearch.Linq/Selector/Converters/SubSelectSelectorPropertyResolver.cs b/src/RoyalCode.SmartSearch.Linq/Mappings/Converters/SubSelectSelectorPropertyResolver.cs similarity index 97% rename from src/RoyalCode.SmartSearch.Linq/Selector/Converters/SubSelectSelectorPropertyResolver.cs rename to src/RoyalCode.SmartSearch.Linq/Mappings/Converters/SubSelectSelectorPropertyResolver.cs index cd96a28..8c8ed30 100644 --- a/src/RoyalCode.SmartSearch.Linq/Selector/Converters/SubSelectSelectorPropertyResolver.cs +++ b/src/RoyalCode.SmartSearch.Linq/Mappings/Converters/SubSelectSelectorPropertyResolver.cs @@ -3,7 +3,7 @@ using System.Linq.Expressions; using System.Reflection; -namespace RoyalCode.SmartSearch.Linq.Selector.Converters; +namespace RoyalCode.SmartSearch.Linq.Mappings.Converters; internal sealed class SubSelectSelectorPropertyResolver : ISelectorPropertyResolver { diff --git a/src/RoyalCode.SmartSearch.Linq/Selector/DefaultSelectorExpressionGenerator.cs b/src/RoyalCode.SmartSearch.Linq/Mappings/DefaultSelectorExpressionGenerator.cs similarity index 97% rename from src/RoyalCode.SmartSearch.Linq/Selector/DefaultSelectorExpressionGenerator.cs rename to src/RoyalCode.SmartSearch.Linq/Mappings/DefaultSelectorExpressionGenerator.cs index 2fdd9d2..87a71e5 100644 --- a/src/RoyalCode.SmartSearch.Linq/Selector/DefaultSelectorExpressionGenerator.cs +++ b/src/RoyalCode.SmartSearch.Linq/Mappings/DefaultSelectorExpressionGenerator.cs @@ -1,10 +1,10 @@ using RoyalCode.Extensions.PropertySelection; -using RoyalCode.SmartSearch.Linq.Selector.Converters; +using RoyalCode.SmartSearch.Linq.Mappings.Converters; using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Reflection; -namespace RoyalCode.SmartSearch.Linq.Selector; +namespace RoyalCode.SmartSearch.Linq.Mappings; /// /// diff --git a/src/RoyalCode.SmartSearch.Linq/Selector/DefaultSelectorGenerator.cs b/src/RoyalCode.SmartSearch.Linq/Mappings/DefaultSelectorGenerator.cs similarity index 95% rename from src/RoyalCode.SmartSearch.Linq/Selector/DefaultSelectorGenerator.cs rename to src/RoyalCode.SmartSearch.Linq/Mappings/DefaultSelectorGenerator.cs index 396f902..3f8aaaa 100644 --- a/src/RoyalCode.SmartSearch.Linq/Selector/DefaultSelectorGenerator.cs +++ b/src/RoyalCode.SmartSearch.Linq/Mappings/DefaultSelectorGenerator.cs @@ -1,4 +1,4 @@ -namespace RoyalCode.SmartSearch.Linq.Selector; +namespace RoyalCode.SmartSearch.Linq.Mappings; /// /// diff --git a/src/RoyalCode.SmartSearch.Linq/ISelector.cs b/src/RoyalCode.SmartSearch.Linq/Mappings/ISelector.cs similarity index 95% rename from src/RoyalCode.SmartSearch.Linq/ISelector.cs rename to src/RoyalCode.SmartSearch.Linq/Mappings/ISelector.cs index 547ec08..b1dbc85 100644 --- a/src/RoyalCode.SmartSearch.Linq/ISelector.cs +++ b/src/RoyalCode.SmartSearch.Linq/Mappings/ISelector.cs @@ -1,6 +1,6 @@ using System.Linq.Expressions; -namespace RoyalCode.SmartSearch.Linq; +namespace RoyalCode.SmartSearch.Linq.Mappings; /// /// diff --git a/src/RoyalCode.SmartSearch.Linq/Selector/ISelectorExpressionGenerator.cs b/src/RoyalCode.SmartSearch.Linq/Mappings/ISelectorExpressionGenerator.cs similarity index 94% rename from src/RoyalCode.SmartSearch.Linq/Selector/ISelectorExpressionGenerator.cs rename to src/RoyalCode.SmartSearch.Linq/Mappings/ISelectorExpressionGenerator.cs index 15c9c32..e0d6051 100644 --- a/src/RoyalCode.SmartSearch.Linq/Selector/ISelectorExpressionGenerator.cs +++ b/src/RoyalCode.SmartSearch.Linq/Mappings/ISelectorExpressionGenerator.cs @@ -1,6 +1,6 @@ using System.Linq.Expressions; -namespace RoyalCode.SmartSearch.Linq.Selector; +namespace RoyalCode.SmartSearch.Linq.Mappings; /// /// diff --git a/src/RoyalCode.SmartSearch.Linq/Selector/ISelectorGenerator.cs b/src/RoyalCode.SmartSearch.Linq/Mappings/ISelectorGenerator.cs similarity index 91% rename from src/RoyalCode.SmartSearch.Linq/Selector/ISelectorGenerator.cs rename to src/RoyalCode.SmartSearch.Linq/Mappings/ISelectorGenerator.cs index 4346857..1ac0118 100644 --- a/src/RoyalCode.SmartSearch.Linq/Selector/ISelectorGenerator.cs +++ b/src/RoyalCode.SmartSearch.Linq/Mappings/ISelectorGenerator.cs @@ -1,5 +1,4 @@ - -namespace RoyalCode.SmartSearch.Linq.Selector; +namespace RoyalCode.SmartSearch.Linq.Mappings; /// /// diff --git a/src/RoyalCode.SmartSearch.Linq/Selector/InternalSelector.cs b/src/RoyalCode.SmartSearch.Linq/Mappings/InternalSelector.cs similarity index 91% rename from src/RoyalCode.SmartSearch.Linq/Selector/InternalSelector.cs rename to src/RoyalCode.SmartSearch.Linq/Mappings/InternalSelector.cs index a98ea99..906755e 100644 --- a/src/RoyalCode.SmartSearch.Linq/Selector/InternalSelector.cs +++ b/src/RoyalCode.SmartSearch.Linq/Mappings/InternalSelector.cs @@ -1,7 +1,7 @@ using System.Linq.Expressions; using System.Runtime.CompilerServices; -namespace RoyalCode.SmartSearch.Linq.Selector; +namespace RoyalCode.SmartSearch.Linq.Mappings; internal sealed class InternalSelector : ISelector where TEntity : class diff --git a/src/RoyalCode.SmartSearch.Linq/Selector/SelectorFactory.cs b/src/RoyalCode.SmartSearch.Linq/Mappings/SelectorFactory.cs similarity index 95% rename from src/RoyalCode.SmartSearch.Linq/Selector/SelectorFactory.cs rename to src/RoyalCode.SmartSearch.Linq/Mappings/SelectorFactory.cs index c0b023d..7cfd2aa 100644 --- a/src/RoyalCode.SmartSearch.Linq/Selector/SelectorFactory.cs +++ b/src/RoyalCode.SmartSearch.Linq/Mappings/SelectorFactory.cs @@ -1,8 +1,9 @@ -using System.Diagnostics.CodeAnalysis; +using RoyalCode.SmartSearch.Linq.Services; +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Reflection; -namespace RoyalCode.SmartSearch.Linq.Selector; +namespace RoyalCode.SmartSearch.Linq.Mappings; /// /// Default implementation of . diff --git a/src/RoyalCode.SmartSearch.Linq/Selector/SelectorNotFoundException.cs b/src/RoyalCode.SmartSearch.Linq/Mappings/SelectorNotFoundException.cs similarity index 88% rename from src/RoyalCode.SmartSearch.Linq/Selector/SelectorNotFoundException.cs rename to src/RoyalCode.SmartSearch.Linq/Mappings/SelectorNotFoundException.cs index c2d1d17..a804ac5 100644 --- a/src/RoyalCode.SmartSearch.Linq/Selector/SelectorNotFoundException.cs +++ b/src/RoyalCode.SmartSearch.Linq/Mappings/SelectorNotFoundException.cs @@ -1,5 +1,4 @@ - -namespace RoyalCode.SmartSearch.Linq.Selector; +namespace RoyalCode.SmartSearch.Linq.Mappings; /// /// Exception thrown when a selector is not found. diff --git a/src/RoyalCode.SmartSearch.Linq/Selector/SelectorsMap.cs b/src/RoyalCode.SmartSearch.Linq/Mappings/SelectorsMap.cs similarity index 97% rename from src/RoyalCode.SmartSearch.Linq/Selector/SelectorsMap.cs rename to src/RoyalCode.SmartSearch.Linq/Mappings/SelectorsMap.cs index 2e3b716..2b3b02a 100644 --- a/src/RoyalCode.SmartSearch.Linq/Selector/SelectorsMap.cs +++ b/src/RoyalCode.SmartSearch.Linq/Mappings/SelectorsMap.cs @@ -2,7 +2,7 @@ using System.Linq.Expressions; using System.Runtime.CompilerServices; -namespace RoyalCode.SmartSearch.Linq.Selector; +namespace RoyalCode.SmartSearch.Linq.Mappings; /// /// A class that maps the types of the entity and Dto to the selector (). diff --git a/src/RoyalCode.SmartSearch.Linq/RoyalCode.SmartSearch.Linq.csproj b/src/RoyalCode.SmartSearch.Linq/RoyalCode.SmartSearch.Linq.csproj index c47aed2..8422814 100644 --- a/src/RoyalCode.SmartSearch.Linq/RoyalCode.SmartSearch.Linq.csproj +++ b/src/RoyalCode.SmartSearch.Linq/RoyalCode.SmartSearch.Linq.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/RoyalCode.SmartSearch.Linq/SearchesServiceCollectionExtensions.cs b/src/RoyalCode.SmartSearch.Linq/SearchesServiceCollectionExtensions.cs index 4537104..22f9f76 100644 --- a/src/RoyalCode.SmartSearch.Linq/SearchesServiceCollectionExtensions.cs +++ b/src/RoyalCode.SmartSearch.Linq/SearchesServiceCollectionExtensions.cs @@ -1,7 +1,8 @@ using Microsoft.Extensions.DependencyInjection; -using RoyalCode.SmartSearch.Linq.Filter; -using RoyalCode.SmartSearch.Linq.Selector; -using RoyalCode.SmartSearch.Linq.Sorter; +using RoyalCode.SmartSearch.Linq.Filtering; +using RoyalCode.SmartSearch.Linq.Mappings; +using RoyalCode.SmartSearch.Linq.Services; +using RoyalCode.SmartSearch.Linq.Sortings; namespace RoyalCode.SmartSearch.Linq; diff --git a/src/RoyalCode.SmartSearch.Linq/Selector/ISelectorFactory.cs b/src/RoyalCode.SmartSearch.Linq/Services/ISelectorFactory.cs similarity index 85% rename from src/RoyalCode.SmartSearch.Linq/Selector/ISelectorFactory.cs rename to src/RoyalCode.SmartSearch.Linq/Services/ISelectorFactory.cs index 02c1ad8..6b310dd 100644 --- a/src/RoyalCode.SmartSearch.Linq/Selector/ISelectorFactory.cs +++ b/src/RoyalCode.SmartSearch.Linq/Services/ISelectorFactory.cs @@ -1,5 +1,6 @@ - -namespace RoyalCode.SmartSearch.Linq.Selector; +using RoyalCode.SmartSearch.Linq.Mappings; + +namespace RoyalCode.SmartSearch.Linq.Services; /// /// A factory to create diff --git a/src/RoyalCode.SmartSearch.Linq/Filter/ISpecifierFactory.cs b/src/RoyalCode.SmartSearch.Linq/Services/ISpecifierFactory.cs similarity index 57% rename from src/RoyalCode.SmartSearch.Linq/Filter/ISpecifierFactory.cs rename to src/RoyalCode.SmartSearch.Linq/Services/ISpecifierFactory.cs index a1e959c..c9edbce 100644 --- a/src/RoyalCode.SmartSearch.Linq/Filter/ISpecifierFactory.cs +++ b/src/RoyalCode.SmartSearch.Linq/Services/ISpecifierFactory.cs @@ -1,5 +1,6 @@ +using RoyalCode.SmartSearch.Linq.Filtering; -namespace RoyalCode.SmartSearch.Linq.Filter; +namespace RoyalCode.SmartSearch.Linq.Services; /// /// A factory to create for a given model type and filter type. @@ -11,14 +12,14 @@ public interface ISpecifierFactory /// Creates a new specifier for a given model type and filter type. /// /// - /// May be returned null if no specifier is configured for the model and filter, or throw an exception. + /// Will throw an exception if no specifier is configured for the model and filter. /// /// /// The model type. /// The filter type. - /// A new specifier or null if not exists. - /// Optional, if no specifier is configured for the model and filter. - ISpecifier? GetSpecifier() + /// A new specifier. + /// Thrown if no specifier is configured for the model and filter. + ISpecifier GetSpecifier() where TModel : class where TFilter : class; } \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Linq/Sorter/DefaultSorter.cs b/src/RoyalCode.SmartSearch.Linq/Sorter/DefaultSorter.cs deleted file mode 100644 index 1e87bee..0000000 --- a/src/RoyalCode.SmartSearch.Linq/Sorter/DefaultSorter.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Ignore Spelling: sortings - -using System.ComponentModel; -using RoyalCode.SmartSearch.Abstractions; - -namespace RoyalCode.SmartSearch.Linq.Sorter; - -/// -/// -/// Default implementation of using additional abstract componentes: -/// -/// -/// -/// : -/// Used to find . -/// -/// -/// : -/// Used to apply the "Order By" through the . -/// -/// -/// : -/// Receives the "Order By" expression and applies it to the query. -/// -/// -/// -/// The query source model type. -public sealed class DefaultSorter : ISorter - where TModel : class -{ - private readonly IOrderByProvider provider; - - /// - /// Creates a new sorter with the to get the handlers. - /// - /// The order by handlers provider. - public DefaultSorter(IOrderByProvider provider) - { - this.provider = provider; - } - - /// - public IQueryable OrderBy(IQueryable query, IEnumerable sortings) - { - var builder = new OrderByBuilder(query); - - foreach (var sorting in sortings) - { - var handler = provider.GetHandler(sorting.OrderBy); - if (handler is null) - continue; - - builder.CurrentDirection = sorting.Direction; - handler.Handle(builder); - } - - return builder.OrderedQueryable; - } - - /// - public IQueryable DefaultOrderBy(IQueryable query) - { - var handler = provider.GetDefaultHandler(); - var builder = new OrderByBuilder(query) - { - CurrentDirection = ListSortDirection.Ascending - }; - handler.Handle(builder); - return builder.OrderedQueryable; - } -} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Linq/Sorter/IOrderByBuilder.cs b/src/RoyalCode.SmartSearch.Linq/Sorter/IOrderByBuilder.cs deleted file mode 100644 index d7463c1..0000000 --- a/src/RoyalCode.SmartSearch.Linq/Sorter/IOrderByBuilder.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Linq.Expressions; - -namespace RoyalCode.SmartSearch.Linq.Sorter; - -/// -/// -/// Component for applying the sort expressions to the query. -/// -/// -/// This component is called by the , which is liable for the expression. -/// -/// -/// The query source model type. -public interface IOrderByBuilder -{ - /// - /// Applies the sort expression to the query. - /// - /// The sort expression. - /// Selected property type. - void Add(Expression> keySelector); -} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Linq/Sorter/DefaultOrderByGenerator.cs b/src/RoyalCode.SmartSearch.Linq/Sortings/DefaultOrderByGenerator.cs similarity index 94% rename from src/RoyalCode.SmartSearch.Linq/Sorter/DefaultOrderByGenerator.cs rename to src/RoyalCode.SmartSearch.Linq/Sortings/DefaultOrderByGenerator.cs index 4230107..afbcc71 100644 --- a/src/RoyalCode.SmartSearch.Linq/Sorter/DefaultOrderByGenerator.cs +++ b/src/RoyalCode.SmartSearch.Linq/Sortings/DefaultOrderByGenerator.cs @@ -1,7 +1,7 @@ using RoyalCode.Extensions.PropertySelection; using System.Linq.Expressions; -namespace RoyalCode.SmartSearch.Linq.Sorter; +namespace RoyalCode.SmartSearch.Linq.Sortings; /// /// Default implementation of , using for diff --git a/src/RoyalCode.SmartSearch.Linq/Sortings/DefaultSorting.cs b/src/RoyalCode.SmartSearch.Linq/Sortings/DefaultSorting.cs new file mode 100644 index 0000000..2220545 --- /dev/null +++ b/src/RoyalCode.SmartSearch.Linq/Sortings/DefaultSorting.cs @@ -0,0 +1,25 @@ +using System.ComponentModel; + +namespace RoyalCode.SmartSearch.Linq.Sortings; + +/// +/// Represents the default sorting configuration, typically by the "Id" property in ascending order. +/// +public sealed class DefaultSorting : ISorting +{ + /// + /// The default property name used for sorting. + /// + public const string DefaultOrderBy = "Id"; + + /// + /// Singleton instance of . + /// + public static readonly DefaultSorting Instance = new(); + + /// + public string OrderBy => DefaultOrderBy; + + /// + public ListSortDirection Direction => ListSortDirection.Ascending; +} \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Linq/Sorter/IOrderByGenerator.cs b/src/RoyalCode.SmartSearch.Linq/Sortings/IOrderByGenerator.cs similarity index 93% rename from src/RoyalCode.SmartSearch.Linq/Sorter/IOrderByGenerator.cs rename to src/RoyalCode.SmartSearch.Linq/Sortings/IOrderByGenerator.cs index 42c80a7..e9f0c75 100644 --- a/src/RoyalCode.SmartSearch.Linq/Sorter/IOrderByGenerator.cs +++ b/src/RoyalCode.SmartSearch.Linq/Sortings/IOrderByGenerator.cs @@ -1,6 +1,6 @@ using System.Linq.Expressions; -namespace RoyalCode.SmartSearch.Linq.Sorter; +namespace RoyalCode.SmartSearch.Linq.Sortings; /// /// diff --git a/src/RoyalCode.SmartSearch.Linq/Sorter/IOrderByHandler.cs b/src/RoyalCode.SmartSearch.Linq/Sortings/IOrderByHandler.cs similarity index 88% rename from src/RoyalCode.SmartSearch.Linq/Sorter/IOrderByHandler.cs rename to src/RoyalCode.SmartSearch.Linq/Sortings/IOrderByHandler.cs index 54c8bc9..3116c06 100644 --- a/src/RoyalCode.SmartSearch.Linq/Sorter/IOrderByHandler.cs +++ b/src/RoyalCode.SmartSearch.Linq/Sortings/IOrderByHandler.cs @@ -1,4 +1,4 @@ -namespace RoyalCode.SmartSearch.Linq.Sorter; +namespace RoyalCode.SmartSearch.Linq.Sortings; /// /// @@ -21,5 +21,5 @@ public interface IOrderByHandler /// Use the to add a sort expression to the query. /// /// Used to apply the expressions to the query. - void Handle(IOrderByBuilder builder); + OrderByBuilder Handle(OrderByBuilder builder); } \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Linq/Sorter/IOrderByProvider.cs b/src/RoyalCode.SmartSearch.Linq/Sortings/IOrderByProvider.cs similarity index 97% rename from src/RoyalCode.SmartSearch.Linq/Sorter/IOrderByProvider.cs rename to src/RoyalCode.SmartSearch.Linq/Sortings/IOrderByProvider.cs index ba49fda..0bec5b1 100644 --- a/src/RoyalCode.SmartSearch.Linq/Sorter/IOrderByProvider.cs +++ b/src/RoyalCode.SmartSearch.Linq/Sortings/IOrderByProvider.cs @@ -1,4 +1,4 @@ -namespace RoyalCode.SmartSearch.Linq.Sorter; +namespace RoyalCode.SmartSearch.Linq.Sortings; /// /// diff --git a/src/RoyalCode.SmartSearch.Linq/Sorter/InvalidOrderByExpressionException.cs b/src/RoyalCode.SmartSearch.Linq/Sortings/InvalidOrderByExpressionException.cs similarity index 92% rename from src/RoyalCode.SmartSearch.Linq/Sorter/InvalidOrderByExpressionException.cs rename to src/RoyalCode.SmartSearch.Linq/Sortings/InvalidOrderByExpressionException.cs index f05f76e..db7cf3f 100644 --- a/src/RoyalCode.SmartSearch.Linq/Sorter/InvalidOrderByExpressionException.cs +++ b/src/RoyalCode.SmartSearch.Linq/Sortings/InvalidOrderByExpressionException.cs @@ -1,5 +1,4 @@ - -namespace RoyalCode.SmartSearch.Linq.Sorter; +namespace RoyalCode.SmartSearch.Linq.Sortings; /// /// Exception thrown when the expression is not valid for the . diff --git a/src/RoyalCode.SmartSearch.Linq/Sorter/OrderByBuilder.cs b/src/RoyalCode.SmartSearch.Linq/Sortings/OrderByBuilder.cs similarity index 64% rename from src/RoyalCode.SmartSearch.Linq/Sorter/OrderByBuilder.cs rename to src/RoyalCode.SmartSearch.Linq/Sortings/OrderByBuilder.cs index 35b2edc..2630a13 100644 --- a/src/RoyalCode.SmartSearch.Linq/Sorter/OrderByBuilder.cs +++ b/src/RoyalCode.SmartSearch.Linq/Sortings/OrderByBuilder.cs @@ -1,15 +1,15 @@ using System.ComponentModel; using System.Linq.Expressions; -using RoyalCode.SmartSearch.Abstractions; -namespace RoyalCode.SmartSearch.Linq.Sorter; +#pragma warning disable S3358 // ternary operator + +namespace RoyalCode.SmartSearch.Linq.Sortings; /// -/// Internal implementation of . -/// Used by the . +/// Builder for "Order By" clauses in a LINQ query. /// /// -internal sealed class OrderByBuilder : IOrderByBuilder +public ref struct OrderByBuilder { private readonly IQueryable query; private IOrderedQueryable? ordered; @@ -27,19 +27,16 @@ public OrderByBuilder(IQueryable query) /// /// The of the current . /// - /// - /// Used internally by . - /// /// - internal ListSortDirection CurrentDirection { get; set; } + public ListSortDirection CurrentDirection { get; set; } /// /// Returns the ordered query. /// - internal IQueryable OrderedQueryable => ordered ?? query; + public readonly IQueryable OrderedQueryable => ordered ?? query; /// - public void Add(Expression> keySelector) + public OrderByBuilder Add(Expression> keySelector) { ordered = CurrentDirection == ListSortDirection.Ascending ? ordered is null @@ -48,5 +45,7 @@ public void Add(Expression> keySelector) : ordered is null ? query.OrderByDescending(keySelector) : ordered.ThenByDescending(keySelector); + + return this; } } \ No newline at end of file diff --git a/src/RoyalCode.SmartSearch.Linq/Sorter/OrderByHandler.cs b/src/RoyalCode.SmartSearch.Linq/Sortings/OrderByHandler.cs similarity index 90% rename from src/RoyalCode.SmartSearch.Linq/Sorter/OrderByHandler.cs rename to src/RoyalCode.SmartSearch.Linq/Sortings/OrderByHandler.cs index 6a84a8f..7a845c0 100644 --- a/src/RoyalCode.SmartSearch.Linq/Sorter/OrderByHandler.cs +++ b/src/RoyalCode.SmartSearch.Linq/Sortings/OrderByHandler.cs @@ -1,7 +1,7 @@ using System.Linq.Expressions; using System.Runtime.CompilerServices; -namespace RoyalCode.SmartSearch.Linq.Sorter; +namespace RoyalCode.SmartSearch.Linq.Sortings; internal sealed class OrderByHandler : IOrderByHandler where TModel : class @@ -14,7 +14,7 @@ public OrderByHandler(Expression> expression) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public void Handle(IOrderByBuilder builder) => builder.Add(expression); + public OrderByBuilder Handle(OrderByBuilder builder) => builder.Add(expression); } internal static class OrderByHandler @@ -36,7 +36,7 @@ public static IOrderByHandler Create(Expression expression) var propertyType = delegateGenerics[1]; var handlerType = typeof(OrderByHandler<,>).MakeGenericType(modelType, propertyType); - var handler = handlerType.GetConstructors().First().Invoke(new[] { expression }); + var handler = handlerType.GetConstructors().First().Invoke([expression]); return (IOrderByHandler)handler; } } diff --git a/src/RoyalCode.SmartSearch.Linq/Sorter/OrderByHandlersMap.cs b/src/RoyalCode.SmartSearch.Linq/Sortings/OrderByHandlersMap.cs similarity index 97% rename from src/RoyalCode.SmartSearch.Linq/Sorter/OrderByHandlersMap.cs rename to src/RoyalCode.SmartSearch.Linq/Sortings/OrderByHandlersMap.cs index ebabc29..adb3b0e 100644 --- a/src/RoyalCode.SmartSearch.Linq/Sorter/OrderByHandlersMap.cs +++ b/src/RoyalCode.SmartSearch.Linq/Sortings/OrderByHandlersMap.cs @@ -2,7 +2,7 @@ using System.Linq.Expressions; using System.Runtime.CompilerServices; -namespace RoyalCode.SmartSearch.Linq.Sorter; +namespace RoyalCode.SmartSearch.Linq.Sortings; internal sealed class OrderByHandlersMap { diff --git a/src/RoyalCode.SmartSearch.Linq/Sorter/OrderByNotSupportedException.cs b/src/RoyalCode.SmartSearch.Linq/Sortings/OrderByNotSupportedException.cs similarity index 92% rename from src/RoyalCode.SmartSearch.Linq/Sorter/OrderByNotSupportedException.cs rename to src/RoyalCode.SmartSearch.Linq/Sortings/OrderByNotSupportedException.cs index f718df2..290353a 100644 --- a/src/RoyalCode.SmartSearch.Linq/Sorter/OrderByNotSupportedException.cs +++ b/src/RoyalCode.SmartSearch.Linq/Sortings/OrderByNotSupportedException.cs @@ -1,5 +1,4 @@ - -namespace RoyalCode.SmartSearch.Linq.Sorter; +namespace RoyalCode.SmartSearch.Linq.Sortings; /// /// Exception thrown when the order by is not supported for the type. diff --git a/src/RoyalCode.SmartSearch.Linq/Sorter/OrderByProvider.cs b/src/RoyalCode.SmartSearch.Linq/Sortings/OrderByProvider.cs similarity index 79% rename from src/RoyalCode.SmartSearch.Linq/Sorter/OrderByProvider.cs rename to src/RoyalCode.SmartSearch.Linq/Sortings/OrderByProvider.cs index 169b5a2..766af5d 100644 --- a/src/RoyalCode.SmartSearch.Linq/Sorter/OrderByProvider.cs +++ b/src/RoyalCode.SmartSearch.Linq/Sortings/OrderByProvider.cs @@ -1,4 +1,4 @@ -namespace RoyalCode.SmartSearch.Linq.Sorter; +namespace RoyalCode.SmartSearch.Linq.Sortings; internal sealed class OrderByProvider : IOrderByProvider { @@ -11,7 +11,9 @@ public OrderByProvider(OrderByHandlersMap handlers, IOrderByGenerator? generator this.generator = generator; } - public IOrderByHandler GetDefaultHandler() where TModel : class => GetHandler("Id")!; + public IOrderByHandler GetDefaultHandler() + where TModel : class + => GetHandler(DefaultSorting.DefaultOrderBy)!; public IOrderByHandler? GetHandler(string orderBy) where TModel : class @@ -26,4 +28,4 @@ public OrderByProvider(OrderByHandlersMap handlers, IOrderByGenerator? generator handlers.Add((typeof(TModel), orderBy), handler); return handler; } -} \ No newline at end of file +} diff --git a/src/RoyalCode.SmartSearch.Tests/AllEntitiesTests.cs b/src/RoyalCode.SmartSearch.Tests/AllEntitiesTests.cs index 01f02e7..3569b51 100644 --- a/src/RoyalCode.SmartSearch.Tests/AllEntitiesTests.cs +++ b/src/RoyalCode.SmartSearch.Tests/AllEntitiesTests.cs @@ -1,8 +1,6 @@ - -using FluentAssertions; +using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using RoyalCode.SmartSearch.Abstractions; namespace RoyalCode.SmartSearch.Tests; @@ -39,10 +37,10 @@ public void Must_CollectAll_WhenNoFilterOrSorting() context.Add(new SimpleModel { Id = 3, Name = "C" }); context.SaveChanges(); - var all = scope.ServiceProvider.GetRequiredService>(); + var all = scope.ServiceProvider.GetRequiredService>(); // act - ICollection result = all.Collect(); + IReadOnlyList result = all.Collect(); // assert result.Should().HaveCount(3); @@ -62,10 +60,10 @@ public async Task Must_CollectAll_WhenNoFilterOrSortingAsync() context.Add(new SimpleModel { Id = 3, Name = "C" }); await context.SaveChangesAsync(); - var all = scope.ServiceProvider.GetRequiredService>(); + var all = scope.ServiceProvider.GetRequiredService>(); // act - ICollection result = await all.CollectAsync(); + IReadOnlyList result = await all.CollectAsync(); // assert result.Should().HaveCount(3); @@ -85,11 +83,11 @@ public void Must_CollectOne_WhenFilterByName() context.Add(new SimpleModel { Id = 3, Name = "C" }); context.SaveChanges(); - var all = scope.ServiceProvider.GetRequiredService>(); + var all = scope.ServiceProvider.GetRequiredService>(); // act var filter = new SimpleFilter { Name = "B" }; - ICollection result = all.FilterBy(filter).Collect(); + IReadOnlyList result = all.FilterBy(filter).Collect(); // assert result.Should().HaveCount(1); @@ -110,11 +108,11 @@ public async Task Must_CollectOne_WhenFilterByNameAsync() context.Add(new SimpleModel { Id = 3, Name = "C" }); await context.SaveChangesAsync(); - var all = scope.ServiceProvider.GetRequiredService>(); + var all = scope.ServiceProvider.GetRequiredService>(); // act var filter = new SimpleFilter { Name = "B" }; - ICollection result = await all.FilterBy(filter).CollectAsync(); + IReadOnlyList result = await all.FilterBy(filter).CollectAsync(); // assert result.Should().HaveCount(1).And.ContainSingle(x => x.Id == 2); @@ -134,7 +132,7 @@ public void Must_Exists_WhenFilterByName() context.Add(new SimpleModel { Id = 3, Name = "C" }); context.SaveChanges(); - var all = scope.ServiceProvider.GetRequiredService>(); + var all = scope.ServiceProvider.GetRequiredService>(); // act var filter = new SimpleFilter { Name = "B" }; @@ -158,7 +156,7 @@ public async Task Must_Exists_WhenFilterByNameAsync() context.Add(new SimpleModel { Id = 3, Name = "C" }); await context.SaveChangesAsync(); - var all = scope.ServiceProvider.GetRequiredService>(); + var all = scope.ServiceProvider.GetRequiredService>(); // act var filter = new SimpleFilter { Name = "B" }; @@ -182,7 +180,7 @@ public void Must_NotExists_WhenFilterByName() context.Add(new SimpleModel { Id = 3, Name = "C" }); context.SaveChanges(); - var all = scope.ServiceProvider.GetRequiredService>(); + var all = scope.ServiceProvider.GetRequiredService>(); // act var filter = new SimpleFilter { Name = "D" }; @@ -206,7 +204,7 @@ public async Task Must_NotExists_WhenFilterByNameAsync() context.Add(new SimpleModel { Id = 3, Name = "C" }); await context.SaveChangesAsync(); - var all = scope.ServiceProvider.GetRequiredService>(); + var all = scope.ServiceProvider.GetRequiredService>(); // act var filter = new SimpleFilter { Name = "D" }; @@ -230,11 +228,11 @@ public void Must_First_WhenFilterByName() context.Add(new SimpleModel { Id = 3, Name = "C" }); context.SaveChanges(); - var all = scope.ServiceProvider.GetRequiredService>(); + var all = scope.ServiceProvider.GetRequiredService>(); // act var filter = new SimpleFilter { Name = "B" }; - SimpleModel? result = all.FilterBy(filter).First(); + SimpleModel? result = all.FilterBy(filter).FirstOrDefault(); // assert @@ -256,11 +254,11 @@ public async Task Must_First_WhenFilterByNameAsync() context.Add(new SimpleModel { Id = 3, Name = "C" }); await context.SaveChangesAsync(); - var all = scope.ServiceProvider.GetRequiredService>(); + var all = scope.ServiceProvider.GetRequiredService>(); // act var filter = new SimpleFilter { Name = "B" }; - SimpleModel? result = await all.FilterBy(filter).FirstAsync(); + SimpleModel? result = await all.FilterBy(filter).FirstOrDefaultAsync(); // assert result.Should().NotBeNull().And.Match(x => x.Id == 2); @@ -280,11 +278,11 @@ public void Must_FirstBeNull_WhenFilterByName() context.Add(new SimpleModel { Id = 3, Name = "C" }); context.SaveChanges(); - var all = scope.ServiceProvider.GetRequiredService>(); + var all = scope.ServiceProvider.GetRequiredService>(); // act var filter = new SimpleFilter { Name = "D" }; - SimpleModel? result = all.FilterBy(filter).First(); + SimpleModel? result = all.FilterBy(filter).FirstOrDefault(); // assert result.Should().BeNull(); @@ -304,11 +302,11 @@ public async Task Must_FirstBeNull_WhenFilterByNameAsync() context.Add(new SimpleModel { Id = 3, Name = "C" }); await context.SaveChangesAsync(); - var all = scope.ServiceProvider.GetRequiredService>(); + var all = scope.ServiceProvider.GetRequiredService>(); // act var filter = new SimpleFilter { Name = "D" }; - SimpleModel? result = await all.FilterBy(filter).FirstAsync(); + SimpleModel? result = await all.FilterBy(filter).FirstOrDefaultAsync(); // assert result.Should().BeNull(); @@ -328,7 +326,7 @@ public void Must_Single_WhenFilterByName() context.Add(new SimpleModel { Id = 3, Name = "C" }); context.SaveChanges(); - var all = scope.ServiceProvider.GetRequiredService>(); + var all = scope.ServiceProvider.GetRequiredService>(); // act var filter = new SimpleFilter { Name = "B" }; @@ -352,7 +350,7 @@ public async Task Must_Single_WhenFilterByNameAsync() context.Add(new SimpleModel { Id = 3, Name = "C" }); await context.SaveChangesAsync(); - var all = scope.ServiceProvider.GetRequiredService>(); + var all = scope.ServiceProvider.GetRequiredService>(); // act var filter = new SimpleFilter { Name = "B" }; @@ -376,7 +374,7 @@ public void Must_Throw_WhenSingle_HasMoreThenOne() context.Add(new SimpleModel { Id = 3, Name = "C" }); context.SaveChanges(); - var all = scope.ServiceProvider.GetRequiredService>(); + var all = scope.ServiceProvider.GetRequiredService>(); // act Action act = () => all.Single(); @@ -399,7 +397,7 @@ public async Task Must_Throw_WhenSingle_HasMoreThenOneAsync() context.Add(new SimpleModel { Id = 3, Name = "C" }); await context.SaveChangesAsync(); - var all = scope.ServiceProvider.GetRequiredService>(); + var all = scope.ServiceProvider.GetRequiredService>(); // act Func act = () => all.SingleAsync(); @@ -422,7 +420,7 @@ public void Must_Throw_WhenSingle_HasNoOne() context.Add(new SimpleModel { Id = 3, Name = "C" }); context.SaveChanges(); - var all = scope.ServiceProvider.GetRequiredService>(); + var all = scope.ServiceProvider.GetRequiredService>(); // act var filter = new SimpleFilter { Name = "D" }; @@ -446,7 +444,7 @@ public async Task Must_Throw_WhenSingle_HasNoOneAsync() context.Add(new SimpleModel { Id = 3, Name = "C" }); await context.SaveChangesAsync(); - var all = scope.ServiceProvider.GetRequiredService>(); + var all = scope.ServiceProvider.GetRequiredService>(); // act var filter = new SimpleFilter { Name = "D" }; diff --git a/src/RoyalCode.SmartSearch.Tests/SelectorExpressionGeneratorTests.cs b/src/RoyalCode.SmartSearch.Tests/SelectorExpressionGeneratorTests.cs index bba0129..af05898 100644 --- a/src/RoyalCode.SmartSearch.Tests/SelectorExpressionGeneratorTests.cs +++ b/src/RoyalCode.SmartSearch.Tests/SelectorExpressionGeneratorTests.cs @@ -1,4 +1,4 @@ -using RoyalCode.SmartSearch.Linq.Selector; +using RoyalCode.SmartSearch.Linq.Mappings; namespace RoyalCode.SmartSearch.Tests; diff --git a/src/RoyalCode.SmartSearch.Tests/SortingTests.cs b/src/RoyalCode.SmartSearch.Tests/SortingTests.cs index 53c9123..408a19c 100644 --- a/src/RoyalCode.SmartSearch.Tests/SortingTests.cs +++ b/src/RoyalCode.SmartSearch.Tests/SortingTests.cs @@ -1,5 +1,4 @@ using FluentAssertions; -using RoyalCode.SmartSearch.Abstractions; using System.ComponentModel; using System.Text.Json; diff --git a/src/RoyalCode.SmartSearch.Tests/SpecifierFunctionGeneratorTests.cs b/src/RoyalCode.SmartSearch.Tests/SpecifierFunctionGeneratorTests.cs index 7490fff..ad2acdf 100644 --- a/src/RoyalCode.SmartSearch.Tests/SpecifierFunctionGeneratorTests.cs +++ b/src/RoyalCode.SmartSearch.Tests/SpecifierFunctionGeneratorTests.cs @@ -1,9 +1,7 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Query.Internal; using Microsoft.Extensions.DependencyInjection; -using RoyalCode.SmartSearch.Abstractions; using RoyalCode.SmartSearch.Linq; -using RoyalCode.SmartSearch.Linq.Filter; +using RoyalCode.SmartSearch.Linq.Filtering; using System.Collections; using System.Linq.Expressions; @@ -52,14 +50,14 @@ public void Generate_Must_GenerateTheFilter_When_ConfiguredWithDbContext() ServiceProvider provider = services.BuildServiceProvider(); // act - var search = provider.GetService>(); + var search = provider.GetRequiredService>(); // assert Assert.NotNull(search); // act search!.FilterBy(new SimpleFilter()); - var resultList = search.ToList(); + var resultList = search.AsSearch().ToList(); // assert Assert.NotNull(resultList); @@ -524,7 +522,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) } } -file class SearchConfigurer : ISearchConfigurations { } +file class SearchConfigurer : ISearchConfigurations +{ + public ISearchConfigurations Add() where TEntity : class + { + throw new NotImplementedException(); + } +} file class ConfigurableEntity { diff --git a/src/RoyalCode.SmartSearch.sln b/src/SmartSearch.sln similarity index 87% rename from src/RoyalCode.SmartSearch.sln rename to src/SmartSearch.sln index 7db142c..3fe5965 100644 --- a/src/RoyalCode.SmartSearch.sln +++ b/src/SmartSearch.sln @@ -10,8 +10,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".items", ".items", "{45312F base.targets = base.targets Directory.Build.props = Directory.Build.props icon.png = icon.png - tests.targets = tests.targets ..\.github\workflows\smart-search.yml = ..\.github\workflows\smart-search.yml + tests.targets = tests.targets EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RoyalCode.SmartSearch.Linq", "RoyalCode.SmartSearch.Linq\RoyalCode.SmartSearch.Linq.csproj", "{DBEBDD2C-37A5-4D17-9DB9-1ACD67749E13}" @@ -26,6 +26,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{932A3AFB EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RoyalCode.SmartSearch.Tests", "RoyalCode.SmartSearch.Tests\RoyalCode.SmartSearch.Tests.csproj", "{92A0A603-CFF6-498E-BED8-8D0D21561AC2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RoyalCode.SmartSearch.AspNetCore", "RoyalCode.SmartSearch.AspNetCore\RoyalCode.SmartSearch.AspNetCore.csproj", "{BC54B3A6-736A-4459-9F33-1101013627F6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -52,6 +54,10 @@ Global {92A0A603-CFF6-498E-BED8-8D0D21561AC2}.Debug|Any CPU.Build.0 = Debug|Any CPU {92A0A603-CFF6-498E-BED8-8D0D21561AC2}.Release|Any CPU.ActiveCfg = Release|Any CPU {92A0A603-CFF6-498E-BED8-8D0D21561AC2}.Release|Any CPU.Build.0 = Release|Any CPU + {BC54B3A6-736A-4459-9F33-1101013627F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BC54B3A6-736A-4459-9F33-1101013627F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BC54B3A6-736A-4459-9F33-1101013627F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BC54B3A6-736A-4459-9F33-1101013627F6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -62,6 +68,7 @@ Global {DDAB919D-3519-4208-A2B1-DCE122228400} = {EB20012D-6B82-492A-A7C8-9B45D10C0364} {DEAF76E8-950F-4D38-B98B-9099C9F190ED} = {EB20012D-6B82-492A-A7C8-9B45D10C0364} {92A0A603-CFF6-498E-BED8-8D0D21561AC2} = {932A3AFB-78D0-4A2D-BADD-54A9BC593B60} + {BC54B3A6-736A-4459-9F33-1101013627F6} = {EB20012D-6B82-492A-A7C8-9B45D10C0364} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D75591B5-4D8A-4ECD-94CE-D38BBC572272} diff --git a/src/tests.targets b/src/tests.targets index 63f9365..136b8b1 100644 --- a/src/tests.targets +++ b/src/tests.targets @@ -11,9 +11,9 @@ 8.0.0 - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -22,7 +22,7 @@ all - +