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

Extension for Gridify to be able to use it with Elasticsearch #126

Merged
merged 14 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ node_modules
.temp
.cache
docs/.vuepress/dist
/*.user
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Gridify (A Modern Dynamic LINQ library)

![GitHub](https://img.shields.io/github/license/alirezanet/gridify)
![Nuget](https://img.shields.io/nuget/dt/gridify?color=%239100ff)
![GitHub](https://img.shields.io/github/license/alirezanet/gridify)
![Nuget](https://img.shields.io/nuget/dt/gridify?color=%239100ff)
![Nuget](https://img.shields.io/nuget/v/gridify?label=stable)
![Nuget (with prereleases)](https://img.shields.io/nuget/vpre/gridify?label=latest)
[![NuGet version (Gridify)](https://img.shields.io/nuget/v/Gridify.svg?style=flat-square)](https://www.nuget.org/packages/Gridify/)
Expand Down Expand Up @@ -29,6 +29,7 @@ Gridify is a dynamic LINQ library that simplifies the process of converting stri
- Compatible with ORMs, especially Entity Framework
- Can be used on every collection that LINQ supports
- Compatible with object-mappers like AutoMapper
- Compatible with Elasticsearch

## Documentation

Expand Down
30 changes: 30 additions & 0 deletions gridify.sln
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntityFrameworkSqlProviderI
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EntityFrameworkPostgreSqlIntegrationTests", "test\EntityFrameworkPostgreSqlIntegrationTests\EntityFrameworkPostgreSqlIntegrationTests.csproj", "{7C6699E7-7B6E-48D4-920F-6DD3568FBFD9}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gridify.Elasticsearch", "src\Gridify.Elasticsearch\Gridify.Elasticsearch.csproj", "{21A86C33-1E72-4771-9C40-73EF9A21AFD5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Gridify.Elasticsearch.Tests", "test\Gridify.Elasticsearch.Tests\Gridify.Elasticsearch.Tests.csproj", "{08E66DC6-A741-49D2-978B-E644EF3A5BA8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -129,6 +133,30 @@ Global
{7C6699E7-7B6E-48D4-920F-6DD3568FBFD9}.Release|x64.Build.0 = Release|Any CPU
{7C6699E7-7B6E-48D4-920F-6DD3568FBFD9}.Release|x86.ActiveCfg = Release|Any CPU
{7C6699E7-7B6E-48D4-920F-6DD3568FBFD9}.Release|x86.Build.0 = Release|Any CPU
{21A86C33-1E72-4771-9C40-73EF9A21AFD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{21A86C33-1E72-4771-9C40-73EF9A21AFD5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{21A86C33-1E72-4771-9C40-73EF9A21AFD5}.Debug|x64.ActiveCfg = Debug|Any CPU
{21A86C33-1E72-4771-9C40-73EF9A21AFD5}.Debug|x64.Build.0 = Debug|Any CPU
{21A86C33-1E72-4771-9C40-73EF9A21AFD5}.Debug|x86.ActiveCfg = Debug|Any CPU
{21A86C33-1E72-4771-9C40-73EF9A21AFD5}.Debug|x86.Build.0 = Debug|Any CPU
{21A86C33-1E72-4771-9C40-73EF9A21AFD5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{21A86C33-1E72-4771-9C40-73EF9A21AFD5}.Release|Any CPU.Build.0 = Release|Any CPU
{21A86C33-1E72-4771-9C40-73EF9A21AFD5}.Release|x64.ActiveCfg = Release|Any CPU
{21A86C33-1E72-4771-9C40-73EF9A21AFD5}.Release|x64.Build.0 = Release|Any CPU
{21A86C33-1E72-4771-9C40-73EF9A21AFD5}.Release|x86.ActiveCfg = Release|Any CPU
{21A86C33-1E72-4771-9C40-73EF9A21AFD5}.Release|x86.Build.0 = Release|Any CPU
{08E66DC6-A741-49D2-978B-E644EF3A5BA8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{08E66DC6-A741-49D2-978B-E644EF3A5BA8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{08E66DC6-A741-49D2-978B-E644EF3A5BA8}.Debug|x64.ActiveCfg = Debug|Any CPU
{08E66DC6-A741-49D2-978B-E644EF3A5BA8}.Debug|x64.Build.0 = Debug|Any CPU
{08E66DC6-A741-49D2-978B-E644EF3A5BA8}.Debug|x86.ActiveCfg = Debug|Any CPU
{08E66DC6-A741-49D2-978B-E644EF3A5BA8}.Debug|x86.Build.0 = Debug|Any CPU
{08E66DC6-A741-49D2-978B-E644EF3A5BA8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{08E66DC6-A741-49D2-978B-E644EF3A5BA8}.Release|Any CPU.Build.0 = Release|Any CPU
{08E66DC6-A741-49D2-978B-E644EF3A5BA8}.Release|x64.ActiveCfg = Release|Any CPU
{08E66DC6-A741-49D2-978B-E644EF3A5BA8}.Release|x64.Build.0 = Release|Any CPU
{08E66DC6-A741-49D2-978B-E644EF3A5BA8}.Release|x86.ActiveCfg = Release|Any CPU
{08E66DC6-A741-49D2-978B-E644EF3A5BA8}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{CDFDBB16-1D9F-40FD-B693-96D1D4FB79EE} = {1BBCBA37-25E5-4BFF-A8E8-7EE582E0317F}
Expand All @@ -139,5 +167,7 @@ Global
{9A54A635-2B5B-4EDC-98FE-9BF963903BD4} = {1BBCBA37-25E5-4BFF-A8E8-7EE582E0317F}
{3B9A8E46-1D4D-40EE-89C0-C3C376D9320A} = {1BBCBA37-25E5-4BFF-A8E8-7EE582E0317F}
{7C6699E7-7B6E-48D4-920F-6DD3568FBFD9} = {1BBCBA37-25E5-4BFF-A8E8-7EE582E0317F}
{21A86C33-1E72-4771-9C40-73EF9A21AFD5} = {0FCD2937-1953-465E-8608-42B8EB8757C7}
{08E66DC6-A741-49D2-978B-E644EF3A5BA8} = {1BBCBA37-25E5-4BFF-A8E8-7EE582E0317F}
EndGlobalSection
EndGlobal
47 changes: 47 additions & 0 deletions src/Gridify.Elasticsearch/ExpressionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Linq.Expressions;
using System.Text;

namespace Gridify.Elasticsearch;

internal static class ExpressionExtensions
{
internal static string ToPropertyPath(this Expression expression)
{
var memberAccessList = new StringBuilder();
VisitMemberAccessChain(expression, memberAccessList);

return memberAccessList.ToString();
}

public static Type GetRealType<T>(this Expression<Func<T, object>> expression)
{
if (expression.Body is MemberExpression memberExpression)
return memberExpression.Type;

if (expression.Body is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression)
return unaryExpression.Operand.Type;

throw new InvalidOperationException("Unsupported expression type.");
}

private static void VisitMemberAccessChain(Expression expression, StringBuilder result)
{
if (expression is MemberExpression memberExpression)
{
if (result.Length > 0)
{
result.Insert(0, ".");
}

result.Insert(0, memberExpression.Member.Name);

VisitMemberAccessChain(memberExpression.Expression, result);
}
else if (expression is UnaryExpression { NodeType: ExpressionType.Convert } unaryExpression)
{
// Handle cases when TValue is object and implicit conversion exists
VisitMemberAccessChain(unaryExpression.Operand, result);
}
}
}
34 changes: 34 additions & 0 deletions src/Gridify.Elasticsearch/Gridify.Elasticsearch.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<PackageId>Gridify.Elasticsearch</PackageId>
<Version>0.0.1</Version>
<Authors>Alireza Sabouri; Dzmitry Koush</Authors>
<PackageDescription>Gridify (Elasticsearch), Easy way to apply Filtering, Sorting, and Pagination using text-based data.</PackageDescription>
<RepositoryUrl>https://github.com/alirezanet/Gridify</RepositoryUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<PackageReadmeFile>README.md</PackageReadmeFile>
<TargetFrameworks>net5.0;net6.0;netstandard2.0;netstandard2.1;net7.0</TargetFrameworks>
<LangVersion>default</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Gridify\Gridify.csproj" />
<None Include="..\..\README.md" Pack="true" PackagePath="\" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Gridify\Gridify.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Elastic.Clients.Elasticsearch" Version="8.*" />
</ItemGroup>

</Project>
89 changes: 89 additions & 0 deletions src/Gridify.Elasticsearch/GridifyExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Elastic.Clients.Elasticsearch;
using Elastic.Clients.Elasticsearch.QueryDsl;
using Gridify.Syntax;

namespace Gridify.Elasticsearch;

public static class GridifyExtensions
{
public static Query ToElasticsearchQuery<T>(this string? filter, IGridifyMapper<T>? mapper = null)
alirezanet marked this conversation as resolved.
Show resolved Hide resolved
{
if (string.IsNullOrWhiteSpace(filter))
return new MatchAllQuery();

var syntaxTree = SyntaxTree.Parse(filter, GridifyGlobalConfiguration.CustomOperators.Operators);
Copy link
Owner

@alirezanet alirezanet Oct 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some warnings that if you already have null checks like this example, it is easy to fix using a ! operator.

var syntaxTree = SyntaxTree.Parse(filter!, GridifyGlobalConfiguration.CustomOperators.Operators);

Github analyzer already marked these so I'll skip other ones.

if (syntaxTree.Diagnostics.Any())
throw new GridifyFilteringException(syntaxTree.Diagnostics.Last());

mapper ??= BuildMapperWithNestedProperties<T>(syntaxTree);

var queryExpression = ToElasticsearchConverter.GenerateQuery(syntaxTree.Root, mapper);
return queryExpression;
}

public static ICollection<SortOptions> ToElasticsearchSortOptions<T>(this string? ordering, IGridifyMapper<T>? mapper = null)
{
if (string.IsNullOrWhiteSpace(ordering))
return new List<SortOptions>();

var orderings = ordering.ParseOrderings().ToList();

mapper ??= BuildMapperForSorting<T>(orderings);

var sortOptions = ToElasticsearchConverter.GenerateSortOptions(orderings, mapper);
return sortOptions;
}

private static GridifyMapper<T> BuildMapperWithNestedProperties<T>(SyntaxTree syntaxTree)
{
var mapper = new GridifyMapper<T>();
foreach (var field in syntaxTree.Root.Descendants()
.Where(q => q.Kind == SyntaxKind.FieldExpression)
.Cast<FieldExpressionSyntax>())
try
{
mapper.AddMap(field.FieldToken.Text);
}
catch (Exception)
{
if (!mapper.Configuration.IgnoreNotMappedFields)
throw new GridifyMapperException($"Property '{field.FieldToken.Text}' not found.");
}

return mapper;
}

private static IEnumerable<SyntaxNode> Descendants(this SyntaxNode root)
{
var nodes = new Stack<SyntaxNode>(new[] { root });
while (nodes.Any())
{
var node = nodes.Pop();
yield return node;
foreach (var n in node.GetChildren()) nodes.Push(n);
}
}

private static GridifyMapper<T> BuildMapperForSorting<T>(List<ParsedOrdering> orderings)
{
var mapper = new GridifyMapper<T>();
foreach (var order in orderings)
{
try
{
mapper.AddMap(order.MemberName);
}
catch (Exception)
{
if (!mapper.Configuration.IgnoreNotMappedFields)
throw new GridifyMapperException($"Mapping '{order.MemberName}' not found");
}
}

return mapper;
}
}
Loading