Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Geo Distance Filtering #21

Merged
merged 8 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ jobs:
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0.x


- name: Install SQLite Spatial
run: sudo apt-get install -y sqlite3 libsqlite3-dev libsqlite3-mod-spatialite

- name: Add Deveel GitHub NuGet Source
run: dotnet nuget add source "https://nuget.pkg.github.com/deveel/index.json" -n "Deveel GitHub" -u ${{ secrets.DEVEEL_NUGET_USER }} -p ${{ secrets.DEVEEL_NUGET_TOKEN }} --store-password-in-clear-text

Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/pr-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ jobs:
uses: actions/setup-dotnet@v3
with:
dotnet-version: 6.0.x

- name: Install SQLite Spatial
run: sudo apt-get install -y sqlite3 libsqlite3-dev libsqlite3-mod-spatialite

- name: Add Deveel GitHub NuGet Source
run: dotnet nuget add source "https://nuget.pkg.github.com/deveel/index.json" -n "Deveel GitHub" -u ${{ secrets.DEVEEL_NUGET_USER }} -p ${{ secrets.DEVEEL_NUGET_TOKEN }} --store-password-in-clear-text
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ jobs:
uses: dawidd6/action-get-tag@v1
with:
strip_v: true

- name: Install SQLite Spatial
run: sudo apt-get install -y sqlite3 libsqlite3-dev libsqlite3-mod-spatialite

- name: Add Deveel GitHub NuGet Source
run: dotnet nuget add source "https://nuget.pkg.github.com/deveel/index.json" -n "Deveel GitHub" -u ${{ secrets.DEVEEL_NUGET_USER }} -p ${{ secrets.DEVEEL_NUGET_TOKEN }} --store-password-in-clear-text
Expand Down
4 changes: 2 additions & 2 deletions Deveel.Repository.sln
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Deveel.Repository.Manager.X
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Deveel.Repository.Manager.DynamicLinq", "src\Deveel.Repository.Manager.DynamicLinq\Deveel.Repository.Manager.DynamicLinq.csproj", "{638851EF-B000-490C-9035-A962279A3E9B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deveel.Repository.Manager.EasyCaching", "src\Deveel.Repository.Manager.EasyCaching\Deveel.Repository.Manager.EasyCaching.csproj", "{84D55BE2-7DAD-4CA3-A3E2-EFB4D41FA3CA}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Deveel.Repository.Manager.EasyCaching", "src\Deveel.Repository.Manager.EasyCaching\Deveel.Repository.Manager.EasyCaching.csproj", "{84D55BE2-7DAD-4CA3-A3E2-EFB4D41FA3CA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deveel.Repository.Manager.EasyCaching.XUnit", "test\Deveel.Repository.Manager.EasyCaching.XUnit\Deveel.Repository.Manager.EasyCaching.XUnit.csproj", "{F47C196B-758D-4C05-8918-FB63C61649EB}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Deveel.Repository.Manager.EasyCaching.XUnit", "test\Deveel.Repository.Manager.EasyCaching.XUnit\Deveel.Repository.Manager.EasyCaching.XUnit.csproj", "{F47C196B-758D-4C05-8918-FB63C61649EB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand Down
8 changes: 5 additions & 3 deletions src/Deveel.Repository.Core/Data/ExpressionQueryFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ namespace Deveel.Data {
/// </summary>
/// <typeparam name="TEntity">The type of entity to construct
/// the field</typeparam>
public sealed class ExpressionQueryFilter<TEntity> : IExpressionQueryFilter where TEntity : class {
public sealed class ExpressionQueryFilter<TEntity> : IExpressionQueryFilter, IQueryableFilter<TEntity> where TEntity : class {
/// <summary>
/// Constructs the filter with the given expression
/// </summary>
Expand All @@ -39,9 +39,11 @@ public ExpressionQueryFilter(Expression<Func<TEntity, bool>> expr) {
/// </summary>
public Expression<Func<TEntity, bool>> Expression { get; }

/// <inheritdoc/>
IQueryable<TEntity> IQueryableFilter<TEntity>.Apply(IQueryable<TEntity> queryable) {
return queryable.Where(Expression);
}

Expression<Func<T, bool>> IExpressionQueryFilter.AsLambda<T>()
where T : class
=> Expression.As<T>();
}
}
34 changes: 34 additions & 0 deletions src/Deveel.Repository.Core/Data/IQueryableFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright 2023 Deveel AS
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

namespace Deveel.Data {
/// <summary>
/// A filter that can be applied to a <see cref="IQueryable{T}"/>
/// object to restrict the results of a query.
/// </summary>
/// <typeparam name="TEntity"></typeparam>
public interface IQueryableFilter<TEntity> : IQueryFilter where TEntity : class {
/// <summary>
/// Applies the filter to the given queryable object.
/// </summary>
/// <param name="queryable">
/// The queryable object to apply the filter to.
/// </param>
/// <returns>
/// Returns an instance of <see cref="IQueryable{TEntity}"/> that
/// is filtered by the conditions of this object.
/// </returns>
IQueryable<TEntity> Apply(IQueryable<TEntity> queryable);
}
}
45 changes: 45 additions & 0 deletions src/Deveel.Repository.Core/Data/QueryFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ public static IQueryable<TEntity> Apply<TEntity>(this IQueryFilter filter, IQuer
return queryable;
}

if (filter is IQueryableFilter<TEntity> filterQueryable)
return filterQueryable.Apply(queryable);
if (filter is IExpressionQueryFilter filterExpr)
return queryable.Where(filterExpr.AsLambda<TEntity>());

Expand Down Expand Up @@ -222,6 +224,49 @@ public static CombinedQueryFilter Combine(IQueryFilter filter1, IQueryFilter fil
return new CombinedQueryFilter(filters);
}

public static long LongCount<TEntity>(this IQueryable<TEntity> queriable, IQueryFilter filter) where TEntity : class {
ArgumentNullException.ThrowIfNull(queriable, nameof(queriable));
ArgumentNullException.ThrowIfNull(filter, nameof(filter));

if (filter.IsEmpty())
return queriable.LongCount();

return filter.Apply(queriable).LongCount();
}

public static IList<TEntity> ToList<TEntity>(this IQueryable<TEntity> queriable, IQueryFilter filter)
where TEntity : class {
ArgumentNullException.ThrowIfNull(queriable, nameof(queriable));
ArgumentNullException.ThrowIfNull(filter, nameof(filter));

if (filter.IsEmpty())
return queriable.ToList();

return filter.Apply(queriable).ToList();
}

public static TEntity? FirstOrDefault<TEntity>(this IQueryable<TEntity> queryable, IQueryFilter filter)
where TEntity : class {
ArgumentNullException.ThrowIfNull(queryable, nameof(queryable));
ArgumentNullException.ThrowIfNull(filter, nameof(filter));

if (filter.IsEmpty())
return queryable.FirstOrDefault();

return filter.Apply(queryable).FirstOrDefault();
}

public static bool Any<TEntity>(this IQueryable<TEntity> queryable, IQueryFilter filter)
where TEntity : class {
ArgumentNullException.ThrowIfNull(queryable, nameof(queryable));
ArgumentNullException.ThrowIfNull(filter, nameof(filter));

if (filter.IsEmpty())
return queryable.Any();

return filter.Apply(queryable).Any();
}

readonly struct EmptyQueryFilter : IQueryFilter {
}
}
Expand Down
22 changes: 19 additions & 3 deletions src/Deveel.Repository.Core/Deveel.Repository.Core.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 33 additions & 41 deletions src/Deveel.Repository.EntityFramework/Data/EntityRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
using Finbuckle.MultiTenant.EntityFrameworkCore;

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
Expand Down Expand Up @@ -347,15 +346,6 @@ public virtual async Task<bool> UpdateAsync(TEntity entity, CancellationToken ca
}


async Task<TEntity?> IFilterableRepository<TEntity>.FindAsync(IQueryFilter filter, CancellationToken cancellationToken)
=> await FindAsync(AssertExpression(filter), cancellationToken);

async Task<IList<TEntity>> IFilterableRepository<TEntity>.FindAllAsync(IQueryFilter filter, CancellationToken cancellationToken)
=> await FindAllAsync(AssertExpression(filter), cancellationToken);

async Task<bool> IFilterableRepository<TEntity>.ExistsAsync(IQueryFilter filter, CancellationToken cancellationToken)
=> await ExistsAsync(AssertExpression(filter), cancellationToken);

/// <summary>
/// Checks if the repository contains an entity that matches
/// the given filter.
Expand All @@ -371,9 +361,14 @@ async Task<bool> IFilterableRepository<TEntity>.ExistsAsync(IQueryFilter filter,
/// that matches the given filter, otherwise <c>false</c>.
/// </returns>
/// <exception cref="RepositoryException"></exception>
public virtual async Task<bool> ExistsAsync(Expression<Func<TEntity, bool>>? filter = null, CancellationToken cancellationToken = default) {
public virtual async Task<bool> ExistsAsync(IQueryFilter filter, CancellationToken cancellationToken = default) {
try {
return await Entities.AnyAsync(EnsureFilter(filter), cancellationToken);
var query = Entities.AsQueryable();
if (filter != null) {
query = filter.Apply(query);
}

return await query.AnyAsync(cancellationToken);
} catch (Exception ex) {
Logger.LogUnknownError(ex, typeof(TEntity));
throw new RepositoryException("Unable to determine the existence of an entity", ex);
Expand All @@ -391,11 +386,19 @@ public virtual async Task<bool> ExistsAsync(Expression<Func<TEntity, bool>>? fil
/// A token used to cancel the operation.
/// </param>
/// <returns></returns>
public virtual async Task<long> CountAsync(Expression<Func<TEntity, bool>>? filter = null, CancellationToken cancellationToken = default)
=> await Entities.LongCountAsync(EnsureFilter(filter), cancellationToken);
public virtual async Task<long> CountAsync(IQueryFilter filter, CancellationToken cancellationToken = default) {
try {
var query = Entities.AsQueryable();
if (filter != null) {
query = filter.Apply(query);
}

Task<long> IFilterableRepository<TEntity>.CountAsync(IQueryFilter filter, CancellationToken cancellationToken)
=> CountAsync(AssertExpression(filter), cancellationToken);
return await query.LongCountAsync(cancellationToken);
} catch (Exception ex) {

throw new RepositoryException("Unable to count the entities", ex);
}
}

/// <summary>
/// Finds the first entity in the repository that matches the given filter.
Expand All @@ -410,36 +413,20 @@ Task<long> IFilterableRepository<TEntity>.CountAsync(IQueryFilter filter, Cancel
/// Returns the first entity that matches the given filter, or <c>null</c>
/// if no entity is found.
/// </returns>
public virtual async Task<TEntity?> FindAsync(Expression<Func<TEntity, bool>>? filter = null, CancellationToken cancellationToken = default) {
public virtual async Task<TEntity?> FindAsync(IQueryFilter filter, CancellationToken cancellationToken = default) {
try {
return await Entities.FirstOrDefaultAsync(EnsureFilter(filter), cancellationToken);
var query = Entities.AsQueryable();
if (filter != null) {
query = filter.Apply(query);
}

return await query.FirstOrDefaultAsync(cancellationToken);
} catch (Exception ex) {
Logger.LogUnknownError(ex, typeof(TEntity));
throw new RepositoryException("Unknown error while trying to find an entity", ex);
}
}

private static Expression<Func<TEntity, bool>> EnsureFilter(Expression<Func<TEntity, bool>>? filter) {
if (filter == null)
filter = e => true;

return filter;
}

private Expression<Func<TEntity, bool>> AssertExpression(IQueryFilter filter) {
if (filter == null || filter.IsEmpty())
return x => true;

if (!(filter is IExpressionQueryFilter exprFilter))
throw new RepositoryException($"The filter of type {filter.GetType()} is not supported");

try {
return exprFilter.AsLambda<TEntity>();
} catch (Exception ex) {
throw new RepositoryException("Unable to trasnform the provided filter to an expression", ex);
}
}

/// <summary>
/// Finds all the entities in the repository that match the given filter.
/// </summary>
Expand All @@ -452,9 +439,14 @@ private Expression<Func<TEntity, bool>> AssertExpression(IQueryFilter filter) {
/// <returns>
/// Returns a list of entities that match the given filter.
/// </returns>
public virtual async Task<IList<TEntity>> FindAllAsync(Expression<Func<TEntity, bool>> filter, CancellationToken cancellationToken = default) {
public virtual async Task<IList<TEntity>> FindAllAsync(IQueryFilter filter, CancellationToken cancellationToken = default) {
try {
return await Entities.Where(filter).ToListAsync(cancellationToken);
var query = Entities.AsQueryable();
if (filter != null) {
query = filter.Apply(query);
}

return await query.ToListAsync(cancellationToken);
} catch (Exception ex) {
Logger.LogUnknownError(ex, typeof(TEntity));
throw new RepositoryException("Unable to list the entities", ex);
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading