Skip to content

Commit 3af1782

Browse files
committed
Demonstrates how to use a database function in a filter query string parameter
1 parent 980a67d commit 3af1782

File tree

8 files changed

+202
-2
lines changed

8 files changed

+202
-2
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System.Text;
2+
using JsonApiDotNetCore.Queries.Expressions;
3+
4+
namespace JsonApiDotNetCoreExample.CustomFunctions.Decrypt;
5+
6+
/// <summary>
7+
/// This expression allows to call the user-defined "decrypt_data" database function. It represents the "decrypt" function, resulting from text such as:
8+
/// <c>
9+
/// decrypt(title)
10+
/// </c>
11+
/// , or:
12+
/// <c>
13+
/// decrypt(owner.lastName)
14+
/// </c>
15+
/// .
16+
/// </summary>
17+
internal sealed class DecryptExpression(ResourceFieldChainExpression targetAttribute) : FunctionExpression
18+
{
19+
public const string Keyword = "decrypt";
20+
21+
/// <summary>
22+
/// The CLR type this function returns, which is always <see cref="string" />.
23+
/// </summary>
24+
public override Type ReturnType { get; } = typeof(string);
25+
26+
/// <summary>
27+
/// The string attribute to decrypt. Chain format: an optional list of to-one relationships, followed by an attribute.
28+
/// </summary>
29+
public ResourceFieldChainExpression TargetAttribute { get; } = targetAttribute;
30+
31+
public override TResult Accept<TArgument, TResult>(QueryExpressionVisitor<TArgument, TResult> visitor, TArgument argument)
32+
{
33+
return visitor.DefaultVisit(this, argument);
34+
}
35+
36+
public override string ToString()
37+
{
38+
return InnerToString(false);
39+
}
40+
41+
public override string ToFullString()
42+
{
43+
return InnerToString(true);
44+
}
45+
46+
private string InnerToString(bool toFullString)
47+
{
48+
var builder = new StringBuilder();
49+
50+
builder.Append(Keyword);
51+
builder.Append('(');
52+
builder.Append(toFullString ? TargetAttribute.ToFullString() : TargetAttribute);
53+
builder.Append(')');
54+
55+
return builder.ToString();
56+
}
57+
58+
public override bool Equals(object? obj)
59+
{
60+
if (ReferenceEquals(this, obj))
61+
{
62+
return true;
63+
}
64+
65+
if (obj is null || GetType() != obj.GetType())
66+
{
67+
return false;
68+
}
69+
70+
var other = (DecryptExpression)obj;
71+
72+
return TargetAttribute.Equals(other.TargetAttribute);
73+
}
74+
75+
public override int GetHashCode()
76+
{
77+
return TargetAttribute.GetHashCode();
78+
}
79+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using JsonApiDotNetCore.Queries.Expressions;
2+
using JsonApiDotNetCore.Queries.Parsing;
3+
using JsonApiDotNetCore.QueryStrings.FieldChains;
4+
using JsonApiDotNetCore.Resources;
5+
using JsonApiDotNetCore.Resources.Annotations;
6+
7+
namespace JsonApiDotNetCoreExample.CustomFunctions.Decrypt;
8+
9+
internal sealed class DecryptFilterParser(IResourceFactory resourceFactory) : FilterParser(resourceFactory)
10+
{
11+
protected override bool IsFunction(string name)
12+
{
13+
if (name == DecryptExpression.Keyword)
14+
{
15+
return true;
16+
}
17+
18+
return base.IsFunction(name);
19+
}
20+
21+
protected override FunctionExpression ParseFunction()
22+
{
23+
if (TokenStack.TryPeek(out Token? nextToken) && nextToken is { Kind: TokenKind.Text, Value: DecryptExpression.Keyword })
24+
{
25+
return ParseDecrypt();
26+
}
27+
28+
return base.ParseFunction();
29+
}
30+
31+
private DecryptExpression ParseDecrypt()
32+
{
33+
EatText(DecryptExpression.Keyword);
34+
EatSingleCharacterToken(TokenKind.OpenParen);
35+
36+
int chainStartPosition = GetNextTokenPositionOrEnd();
37+
38+
ResourceFieldChainExpression targetAttributeChain =
39+
ParseFieldChain(BuiltInPatterns.ToOneChainEndingInAttribute, FieldChainPatternMatchOptions.None, ResourceTypeInScope, null);
40+
41+
ResourceFieldAttribute attribute = targetAttributeChain.Fields[^1];
42+
43+
if (attribute.Property.PropertyType != typeof(string))
44+
{
45+
int position = chainStartPosition + GetRelativePositionOfLastFieldInChain(targetAttributeChain);
46+
throw new QueryParseException("Attribute of type 'String' expected.", position);
47+
}
48+
49+
EatSingleCharacterToken(TokenKind.CloseParen);
50+
51+
return new DecryptExpression(targetAttributeChain);
52+
}
53+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Linq.Expressions;
2+
using JsonApiDotNetCore.Queries.Expressions;
3+
using JsonApiDotNetCore.Queries.QueryableBuilding;
4+
5+
namespace JsonApiDotNetCoreExample.CustomFunctions.Decrypt;
6+
7+
internal sealed class DecryptWhereClauseBuilder : WhereClauseBuilder
8+
{
9+
public override Expression DefaultVisit(QueryExpression expression, QueryClauseBuilderContext context)
10+
{
11+
if (expression is DecryptExpression decryptExpression)
12+
{
13+
return VisitDecrypt(decryptExpression, context);
14+
}
15+
16+
return base.DefaultVisit(expression, context);
17+
}
18+
19+
private Expression VisitDecrypt(DecryptExpression expression, QueryClauseBuilderContext context)
20+
{
21+
Expression propertyAccess = Visit(expression.TargetAttribute, context);
22+
return Expression.Call(null, FunctionStub.DecryptMethod, propertyAccess);
23+
}
24+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
using System.Reflection;
2+
3+
#pragma warning disable AV1008 // Class should not be static
4+
5+
namespace JsonApiDotNetCoreExample.CustomFunctions.Decrypt;
6+
7+
internal static class FunctionStub
8+
{
9+
public static readonly MethodInfo DecryptMethod = typeof(FunctionStub).GetMethod(nameof(Decrypt), [typeof(string)])!;
10+
11+
// ReSharper disable once UnusedParameter.Global
12+
public static string Decrypt(string text)
13+
{
14+
throw new InvalidOperationException($"The '{nameof(Decrypt)}' function cannot be called client-side.");
15+
}
16+
}

src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using JetBrains.Annotations;
2+
using JsonApiDotNetCoreExample.CustomFunctions.Decrypt;
23
using JsonApiDotNetCoreExample.Models;
34
using Microsoft.EntityFrameworkCore;
45
using Microsoft.EntityFrameworkCore.Metadata;
@@ -26,6 +27,9 @@ protected override void OnModelCreating(ModelBuilder builder)
2627
.WithOne(todoItem => todoItem.Owner);
2728

2829
AdjustDeleteBehaviorForJsonApi(builder);
30+
31+
builder.HasDbFunction(FunctionStub.DecryptMethod)
32+
.HasName("decrypt_data");
2933
}
3034

3135
private static void AdjustDeleteBehaviorForJsonApi(ModelBuilder builder)

src/Examples/JsonApiDotNetCoreExample/Data/Seeder.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using JetBrains.Annotations;
22
using JsonApiDotNetCoreExample.Models;
3+
using Microsoft.EntityFrameworkCore;
34

45
namespace JsonApiDotNetCoreExample.Data;
56

@@ -52,5 +53,22 @@ public static async Task CreateSampleDataAsync(AppDbContext dbContext)
5253

5354
dbContext.TodoItems.AddRange(todoItems.Elements);
5455
await dbContext.SaveChangesAsync();
56+
57+
// Just for demo purposes, decryption is defined as: prefix and suffix the incoming value with an underscore.
58+
await dbContext.Database.ExecuteSqlRawAsync("""
59+
CREATE OR REPLACE FUNCTION decrypt_data(value text)
60+
RETURNS text
61+
RETURN '_' || value || '_';
62+
""");
63+
64+
dbContext.TodoItems.Add(new TodoItem
65+
{
66+
Description = "secret",
67+
Priority = priorities.GetNext(),
68+
CreatedAt = DateTimeOffset.UtcNow,
69+
Owner = people.GetNext()
70+
});
71+
72+
await dbContext.SaveChangesAsync();
5573
}
5674
}

src/Examples/JsonApiDotNetCoreExample/Program.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
using JsonApiDotNetCore.Configuration;
55
using JsonApiDotNetCore.Diagnostics;
66
using JsonApiDotNetCoreExample;
7+
using JsonApiDotNetCore.Queries.Parsing;
8+
using JsonApiDotNetCore.Queries.QueryableBuilding;
9+
using JsonApiDotNetCoreExample.CustomFunctions.Decrypt;
710
using JsonApiDotNetCoreExample.Data;
811
using Microsoft.EntityFrameworkCore;
912
using Microsoft.EntityFrameworkCore.Diagnostics;
@@ -27,6 +30,9 @@ static WebApplication CreateWebApplication(string[] args)
2730

2831
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
2932

33+
builder.Services.AddTransient<IFilterParser, DecryptFilterParser>();
34+
builder.Services.AddTransient<IWhereClauseBuilder, DecryptWhereClauseBuilder>();
35+
3036
// Add services to the container.
3137
ConfigureServices(builder);
3238

src/Examples/JsonApiDotNetCoreExample/Properties/launchSettings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@
1212
"IIS Express": {
1313
"commandName": "IISExpress",
1414
"launchBrowser": true,
15-
"launchUrl": "api/todoItems?include=owner,assignee,tags&filter=equals(priority,'High')",
15+
"launchUrl": "api/todoItems?include=owner,assignee,tags&filter=equals(decrypt(description),'_secret_')",
1616
"environmentVariables": {
1717
"ASPNETCORE_ENVIRONMENT": "Development"
1818
}
1919
},
2020
"Kestrel": {
2121
"commandName": "Project",
2222
"launchBrowser": true,
23-
"launchUrl": "api/todoItems?include=owner,assignee,tags&filter=equals(priority,'High')",
23+
"launchUrl": "api/todoItems?include=owner,assignee,tags&filter=equals(decrypt(description),'_secret_')",
2424
"applicationUrl": "https://localhost:44340;http://localhost:14140",
2525
"environmentVariables": {
2626
"ASPNETCORE_ENVIRONMENT": "Development"

0 commit comments

Comments
 (0)