Skip to content

Commit f92b4e8

Browse files
committed
Code polishing
1 parent b36a8a6 commit f92b4e8

File tree

7 files changed

+156
-227
lines changed

7 files changed

+156
-227
lines changed

NorthwindCRUD/Program.cs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,6 @@ public static void Main(string[] args)
4343
{
4444
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
4545
options.SerializerSettings.Converters.Add(new StringEnumConverter());
46-
options.SerializerSettings.Converters.Add(new QueryConverter());
47-
options.SerializerSettings.Converters.Add(new QueryFilterConverter());
48-
options.SerializerSettings.Converters.Add(new QueryFilterConditionConverter());
4946
});
5047

5148
builder.Services.AddEndpointsApiExplorer();
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace QueryBuilder;
2+
3+
public enum FilterType
4+
{
5+
And = 0,
6+
Or = 1,
7+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace QueryBuilder;
2+
3+
public class Query
4+
{
5+
public string Entity { get; set; }
6+
7+
public string[] ReturnFields { get; set; }
8+
9+
public FilterType Operator { get; set; }
10+
11+
public QueryFilter[] FilteringOperands { get; set; }
12+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace QueryBuilder;
2+
3+
public class QueryFilter
4+
{
5+
// Basic condition
6+
public string? FieldName { get; set; }
7+
8+
public bool? IgnoreCase { get; set; }
9+
10+
public QueryFilterCondition? Condition { get; set; }
11+
12+
public object? SearchVal { get; set; }
13+
14+
public Query? SearchTree { get; set; }
15+
16+
// And/Or
17+
public FilterType? Operator { get; set; }
18+
19+
public QueryFilter[] FilteringOperands { get; set; }
20+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace QueryBuilder;
2+
3+
public class QueryFilterCondition
4+
{
5+
public string Name { get; set; }
6+
7+
public bool IsUnary { get; set; }
8+
9+
public string IconName { get; set; }
10+
}

NorthwindCRUD/Controllers/QueryExecutor.cs renamed to NorthwindCRUD/QueryBuilder/QueryExecutor.cs

Lines changed: 42 additions & 224 deletions
Original file line numberDiff line numberDiff line change
@@ -7,125 +7,17 @@
77
using AutoMapper.Internal;
88
using Microsoft.EntityFrameworkCore;
99
using Microsoft.EntityFrameworkCore.Infrastructure;
10-
using Newtonsoft.Json;
1110
using NorthwindCRUD;
1211
using Swashbuckle.AspNetCore.SwaggerGen;
1312

14-
public enum FilterType
15-
{
16-
And = 0,
17-
Or = 1,
18-
}
19-
20-
public interface IQuery
21-
{
22-
public string Entity { get; set; }
23-
24-
public string[] ReturnFields { get; set; }
25-
26-
[SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "required name")]
27-
public FilterType Operator { get; set; }
28-
29-
public IQueryFilter[] FilteringOperands { get; set; }
30-
}
31-
32-
public interface IQueryFilter
33-
{
34-
// Basic condition
35-
public string? FieldName { get; set; }
36-
37-
public bool? IgnoreCase { get; set; }
38-
39-
public IQueryFilterCondition? Condition { get; set; }
40-
41-
public object? SearchVal { get; set; }
42-
43-
public IQuery? SearchTree { get; set; }
44-
45-
// And/Or
46-
[SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "required name")]
47-
public FilterType? Operator { get; set; }
48-
49-
public IQueryFilter[] FilteringOperands { get; set; }
50-
}
51-
52-
public interface IQueryFilterCondition
53-
{
54-
public string Name { get; set; }
55-
56-
public bool IsUnary { get; set; }
57-
58-
public string IconName { get; set; }
59-
}
60-
61-
public class Query : IQuery
62-
{
63-
public string Entity { get; set; }
64-
65-
public string[] ReturnFields { get; set; }
66-
67-
public FilterType Operator { get; set; }
68-
69-
public IQueryFilter[] FilteringOperands { get; set; }
70-
}
71-
72-
public class QueryFilter : IQueryFilter
73-
{
74-
// Basic condition
75-
public string? FieldName { get; set; }
76-
77-
public bool? IgnoreCase { get; set; }
78-
79-
public IQueryFilterCondition? Condition { get; set; }
80-
81-
public object? SearchVal { get; set; }
82-
83-
public IQuery? SearchTree { get; set; }
84-
85-
// And/Or
86-
public FilterType? Operator { get; set; }
87-
88-
public IQueryFilter[] FilteringOperands { get; set; }
89-
}
90-
91-
public class QueryFilterCondition : IQueryFilterCondition
92-
{
93-
public string Name { get; set; }
94-
95-
public bool IsUnary { get; set; }
96-
97-
public string IconName { get; set; }
98-
}
99-
100-
public abstract class InterfaceToConcreteClassConverter<TI, TC> : JsonConverter
101-
{
102-
public override bool CanConvert(Type objectType) => objectType == typeof(TI);
103-
104-
public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) => serializer.Deserialize<TC>(reader);
105-
106-
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => serializer.Serialize(writer, value);
107-
108-
}
109-
110-
public class QueryConverter : InterfaceToConcreteClassConverter<IQuery, Query>
111-
{
112-
}
113-
114-
public class QueryFilterConverter : InterfaceToConcreteClassConverter<IQueryFilter, QueryFilter>
115-
{
116-
}
117-
118-
public class QueryFilterConditionConverter : InterfaceToConcreteClassConverter<IQueryFilterCondition, QueryFilterCondition>
119-
{
120-
}
121-
12213
/// <summary>
12314
/// A generic query executor that can be used to execute queries on IQueryable data sources.
12415
/// </summary>
12516
[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1009:Closing parenthesis should be spaced correctly", Justification = "...")]
17+
[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "...")]
12618
public static class QueryExecutor
12719
{
128-
public static TEntity[] Run<TEntity>(this IQueryable<TEntity> source, IQuery? query)
20+
public static TEntity[] Run<TEntity>(this IQueryable<TEntity> source, Query? query)
12921
{
13022
var infrastructure = source as IInfrastructure<IServiceProvider>;
13123
var serviceProvider = infrastructure!.Instance;
@@ -134,7 +26,7 @@ public static TEntity[] Run<TEntity>(this IQueryable<TEntity> source, IQuery? qu
13426
return db is not null ? BuildQuery(db, source, query).ToArray() : Array.Empty<TEntity>();
13527
}
13628

137-
private static IQueryable<TEntity> BuildQuery<TEntity>(DataContext db, IQueryable<TEntity> source, IQuery? query)
29+
private static IQueryable<TEntity> BuildQuery<TEntity>(DataContext db, IQueryable<TEntity> source, Query? query)
13830
{
13931
if (query is null)
14032
{
@@ -154,7 +46,7 @@ private static IQueryable<TEntity> BuildQuery<TEntity>(DataContext db, IQueryabl
15446
}
15547
}
15648

157-
private static Expression<Func<TEntity, bool>> BuildExpression<TEntity>(DataContext db, IQueryable<TEntity> source, IQueryFilter[] filters, FilterType filterType)
49+
private static Expression<Func<TEntity, bool>> BuildExpression<TEntity>(DataContext db, IQueryable<TEntity> source, QueryFilter[] filters, FilterType filterType)
15850
{
15951
var parameter = Expression.Parameter(typeof(TEntity), "entity");
16052
var finalExpression = null as Expression;
@@ -178,7 +70,7 @@ private static Expression<Func<TEntity, bool>> BuildExpression<TEntity>(DataCont
17870
: (TEntity _) => true;
17971
}
18072

181-
private static Expression BuildConditionExpression<TEntity>(DataContext db, IQueryable<TEntity> source, IQueryFilter filter, ParameterExpression parameter)
73+
private static Expression BuildConditionExpression<TEntity>(DataContext db, IQueryable<TEntity> source, QueryFilter filter, ParameterExpression parameter)
18274
{
18375
if (filter.FieldName is not null && filter.IgnoreCase is not null && filter.Condition is not null)
18476
{
@@ -190,26 +82,26 @@ private static Expression BuildConditionExpression<TEntity>(DataContext db, IQue
19082
var emptyValue = GetEmptyValue(targetType);
19183
Expression condition = filter.Condition.Name switch
19284
{
193-
"null" => targetType.IsNullableType() ? Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(false),
194-
"notNull" => targetType.IsNullableType() ? Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(true),
195-
"empty" => Expression.Or(Expression.Equal(field, emptyValue), targetType.IsNullableType() ? Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(false)),
196-
"notEmpty" => Expression.And(Expression.NotEqual(field, emptyValue), targetType.IsNullableType() ? Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(true)),
197-
"equals" => Expression.Equal(field, searchValue),
198-
"doesNotEqual" => Expression.NotEqual(field, searchValue),
199-
"in" => BuildInExpression(db, filter.SearchTree, field),
200-
"notIn" => Expression.Not(BuildInExpression(db, filter.SearchTree, field)),
201-
"contains" => Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue),
202-
"doesNotContain" => Expression.Not(Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue)),
203-
"startsWith" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, searchValue),
204-
"endsWith" => Expression.Call(field, typeof(string).GetMethod("EndsWith", new[] { typeof(string) })!, searchValue),
205-
"greaterThan" => Expression.GreaterThan(field, searchValue),
206-
"lessThan" => Expression.LessThan(field, searchValue),
207-
"greaterThanOrEqualTo" => Expression.GreaterThanOrEqual(field, searchValue),
208-
"lessThanOrEqualTo" => Expression.LessThanOrEqual(field, searchValue),
209-
"all" => throw new NotImplementedException("Not implemented"),
210-
"true" => Expression.IsTrue(field),
211-
"false" => Expression.IsFalse(field),
212-
_ => throw new NotImplementedException("Not implemented"),
85+
"null" => targetType.IsNullableType() ? Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(false),
86+
"notNull" => targetType.IsNullableType() ? Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(true),
87+
"empty" => Expression.Or(Expression.Equal(field, emptyValue), targetType.IsNullableType() ? Expression.Equal(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(false)),
88+
"notEmpty" => Expression.And(Expression.NotEqual(field, emptyValue), targetType.IsNullableType() ? Expression.NotEqual(field, Expression.Constant(targetType.GetDefaultValue())) : Expression.Constant(true)),
89+
"equals" => Expression.Equal(field, searchValue),
90+
"doesNotEqual" => Expression.NotEqual(field, searchValue),
91+
"in" => BuildInExpression(db, filter.SearchTree, field),
92+
"notIn" => Expression.Not(BuildInExpression(db, filter.SearchTree, field)),
93+
"contains" => Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue),
94+
"doesNotContain" => Expression.Not(Expression.Call(field, typeof(string).GetMethod("Contains", new[] { typeof(string) })!, searchValue)),
95+
"startsWith" => Expression.Call(field, typeof(string).GetMethod("StartsWith", new[] { typeof(string) })!, searchValue),
96+
"endsWith" => Expression.Call(field, typeof(string).GetMethod("EndsWith", new[] { typeof(string) })!, searchValue),
97+
"greaterThan" => Expression.GreaterThan(field, searchValue),
98+
"lessThan" => Expression.LessThan(field, searchValue),
99+
"greaterThanOrEqualTo" => Expression.GreaterThanOrEqual(field, searchValue),
100+
"lessThanOrEqualTo" => Expression.LessThanOrEqual(field, searchValue),
101+
"all" => throw new NotImplementedException("Not implemented"),
102+
"true" => Expression.IsTrue(field),
103+
"false" => Expression.IsFalse(field),
104+
_ => throw new NotImplementedException("Not implemented"),
213105
};
214106
if (filter.IgnoreCase.Value && field.Type == typeof(string))
215107
{
@@ -232,7 +124,7 @@ private static Expression BuildConditionExpression<TEntity>(DataContext db, IQue
232124
}
233125
}
234126

235-
private static Expression BuildInExpression(DataContext db, IQuery? query, MemberExpression field)
127+
private static Expression BuildInExpression(DataContext db, Query? query, MemberExpression field)
236128
{
237129
if (field.Type == typeof(string))
238130
{
@@ -270,40 +162,28 @@ private static Expression BuildInExpression(DataContext db, IQuery? query, Membe
270162
}
271163
}
272164

273-
private static IEnumerable<dynamic> RunSubquery(DataContext db, IQuery? query)
165+
private static IEnumerable<dynamic> RunSubquery(DataContext db, Query? query)
274166
{
275167
// var t = query?.Entity.ToLower(CultureInfo.InvariantCulture);
276168
// var p = db.GetType().GetProperty(t, BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? throw new InvalidOperationException($"Property '{t}' not found on type '{db.GetType()}'");
277169
// var q = p.GetValue(db) as IQueryable<dynamic>;
278170
// return q is null ? Array.Empty<dynamic>() : q.Run(query).ToArray();
279171
var t = query?.Entity.ToLower(CultureInfo.InvariantCulture) ?? string.Empty;
280-
switch (t)
281-
{
282-
case "addresses":
283-
return db.Suppliers.Run(query).ToArray();
284-
case "categories":
285-
return db.Categories.Run(query).ToArray();
286-
case "products":
287-
return db.Products.Run(query).ToArray();
288-
case "regions":
289-
return db.Regions.Run(query).ToArray();
290-
case "territories":
291-
return db.Territories.Run(query).ToArray();
292-
case "employees":
293-
return db.Employees.Run(query).ToArray();
294-
case "customers":
295-
return db.Customers.Run(query).ToArray();
296-
case "orders":
297-
return db.Orders.Run(query).ToArray();
298-
case "orderdetails":
299-
return db.OrderDetails.Run(query).ToArray();
300-
case "shippers":
301-
return db.Shippers.Run(query).ToArray();
302-
case "suppliers":
303-
return db.Suppliers.Run(query).ToArray();
304-
default:
305-
return Array.Empty<dynamic>();
306-
}
172+
return t switch
173+
{
174+
"addresses" => db.Suppliers.Run(query).ToArray(),
175+
"categories" => db.Categories.Run(query).ToArray(),
176+
"products" => db.Products.Run(query).ToArray(),
177+
"regions" => db.Regions.Run(query).ToArray(),
178+
"territories" => db.Territories.Run(query).ToArray(),
179+
"employees" => db.Employees.Run(query).ToArray(),
180+
"customers" => db.Customers.Run(query).ToArray(),
181+
"orders" => db.Orders.Run(query).ToArray(),
182+
"orderdetails" => db.OrderDetails.Run(query).ToArray(),
183+
"shippers" => db.Shippers.Run(query).ToArray(),
184+
"suppliers" => db.Suppliers.Run(query).ToArray(),
185+
_ => Array.Empty<dynamic>(),
186+
};
307187
}
308188

309189
private static dynamic? ProjectField(dynamic? obj, string field)
@@ -343,65 +223,3 @@ private static Expression<Func<TEntity, dynamic>> BuildProjectionExpression<TEnt
343223
return Expression.Lambda<Func<TEntity, dynamic>>(body, parameter);
344224
}
345225
}
346-
347-
[SuppressMessage("StyleCop.CSharp.SpacingRules", "SA1025:Code should not contain multiple whitespace in a row", Justification = "...")]
348-
public static class SqlGenerator
349-
{
350-
public static string GenerateSql(IQuery query)
351-
{
352-
var selectClause = BuildSelectClause(query);
353-
var whereClause = BuildWhereClause(query.FilteringOperands, query.Operator);
354-
return $"{selectClause} {whereClause};";
355-
}
356-
357-
private static string BuildSelectClause(IQuery query)
358-
{
359-
var fields = query.ReturnFields != null && query.ReturnFields.Any()
360-
? string.Join(", ", query.ReturnFields)
361-
: "*";
362-
return $"SELECT {fields} FROM {query.Entity}";
363-
}
364-
365-
private static string BuildWhereClause(IQueryFilter[] filters, FilterType filterType)
366-
{
367-
if (filters == null || !filters.Any())
368-
{
369-
return string.Empty;
370-
}
371-
372-
var conditions = filters.Select(BuildCondition).ToArray();
373-
var conjunction = filterType == FilterType.And ? " AND " : " OR ";
374-
return $"WHERE {string.Join(conjunction, conditions)}";
375-
}
376-
377-
private static string BuildCondition(IQueryFilter filter)
378-
{
379-
var field = filter.FieldName;
380-
var condition = filter.Condition?.Name;
381-
var value = filter.SearchVal != null ? $"'{filter.SearchVal}'" : "NULL";
382-
var subquery = filter.SearchTree != null ? $"({GenerateSql(filter.SearchTree)})" : string.Empty;
383-
return condition switch
384-
{
385-
"null" => $"{field} IS NULL",
386-
"notNull" => $"{field} IS NOT NULL",
387-
"empty" => $"{field} = ''",
388-
"notEmpty" => $"{field} <> ''",
389-
"equals" => $"{field} = {value}",
390-
"doesNotEqual" => $"{field} <> {value}",
391-
"in" => $"{field} IN ({subquery})",
392-
"notIn" => $"{field} NOT IN ({subquery})",
393-
"contains" => $"{field} LIKE '%{filter.SearchVal}%'",
394-
"doesNotContain" => $"{field} NOT LIKE '%{filter.SearchVal}%'",
395-
"startsWith" => $"{field} LIKE '{filter.SearchVal}%'",
396-
"endsWith" => $"{field} LIKE '%{filter.SearchVal}'",
397-
"greaterThan" => $"{field} > {value}",
398-
"lessThan" => $"{field} < {value}",
399-
"greaterThanOrEqualTo" => $"{field} >= {value}",
400-
"lessThanOrEqualTo" => $"{field} <= {value}",
401-
"all" => throw new NotImplementedException("Not implemented"),
402-
"true" => $"{field} = TRUE",
403-
"false" => $"{field} = FALSE",
404-
_ => throw new NotImplementedException($"Condition '{condition}' is not implemented"),
405-
};
406-
}
407-
}

0 commit comments

Comments
 (0)