mirror of https://github.com/sean-m/McRule.git
Compare commits
4 Commits
c806955ce6
...
de550ceee1
Author | SHA1 | Date |
---|---|---|
Sean McArde | de550ceee1 | |
Sean McArde | f17ac2582f | |
Sean McArde | d469df010c | |
Sean McArde | a97a58e094 |
|
@ -0,0 +1,11 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace McRule.Tests {
|
||||
public class PredicateCombination {
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SMM {
|
||||
public static class FilterPatternHelpers {
|
||||
|
||||
private static bool HasFilterCharacters(string input) { return Regex.IsMatch(input, @"(^([~\*]+)|(\*)$)"); }
|
||||
public static string AddFilterOptionsIfNotSpecified(this string Pattern, FilterOptions options = FilterOptions.None) {
|
||||
string result = Pattern.Trim();
|
||||
|
||||
if (HasFilterCharacters(Pattern)) return result;
|
||||
|
||||
if (options.HasFlag(FilterOptions.Contains)) {
|
||||
result = $"*{result}*";
|
||||
} else {
|
||||
if (options.HasFlag(FilterOptions.StartsWith)) {
|
||||
result = $"{result}*";
|
||||
}
|
||||
if (options.HasFlag(FilterOptions.EndsWith)) {
|
||||
result = $"*{result}";
|
||||
}
|
||||
}
|
||||
if (options.HasFlag(FilterOptions.IgnoreCase)) {
|
||||
result = $"~{result}";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public enum FilterOptions {
|
||||
None = 0,
|
||||
IgnoreCase = 1,
|
||||
Contains = 2,
|
||||
StartsWith = 4,
|
||||
EndsWith = 8,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
|
||||
namespace SMM
|
||||
{
|
||||
public class NpgsqlGenerator : McRule.ExpressionGeneratorBase {
|
||||
/// <summary>
|
||||
/// Builds expressions using string member functions StartsWith, EndsWith or Contains as the comparator,
|
||||
/// case-insensitive matches produce an ILike expression for Npgsql.
|
||||
/// </summary>
|
||||
public override Expression<Func<T, bool>> AddStringPropertyExpression<T>(
|
||||
Expression<Func<T, string>> lambda, string filter, string filterType, bool ignoreCase = false) {
|
||||
#if DEBUG
|
||||
if (!(filterType == "StartsWith" || filterType == "EndsWith" || filterType == "Contains" ||
|
||||
filterType == "Equals")) {
|
||||
throw new Exception($"filterType must equal StartsWith, EndsWith or Contains. Passed: {filterType}");
|
||||
}
|
||||
#endif
|
||||
// If a case insensitive comparision is requested, we resolve that against the
|
||||
// Npgsql ILike extension method. EF will translate that into the proper SQL before
|
||||
// sending it to the database.
|
||||
if (ignoreCase) {
|
||||
switch (filterType) {
|
||||
case "StartsWith":
|
||||
filter = $"{filter}%";
|
||||
break;
|
||||
case "EndsWith":
|
||||
filter = $"%{filter}";
|
||||
break;
|
||||
case "Contains":
|
||||
filter = $"%{filter}%";
|
||||
break;
|
||||
}
|
||||
|
||||
// ILike is a virtual static extension method so needs a statically typed
|
||||
// null as the first parameter. Smart people made the type system so
|
||||
// I hope this makes sense to them.
|
||||
var nullExpr = Expression.Constant(null, typeof(DbFunctions));
|
||||
|
||||
var method = typeof(NpgsqlDbFunctionsExtensions).GetMethods().Where(
|
||||
x => (x.Name?.Equals("ILike") ?? false)
|
||||
&& x.GetParameters().Count() == 3); // There's two "ILike" versions, we want the one that takes 3 arguments.
|
||||
|
||||
var likeCall = Expression.Call(method.FirstOrDefault(), nullExpr, lambda.Body, Expression.Constant(filter, typeof(string)));
|
||||
|
||||
return Expression.Lambda<Func<T, bool>>(
|
||||
likeCall,
|
||||
lambda.Parameters);
|
||||
}
|
||||
|
||||
// When case sensitive matches are fine, we can invoke the String extension methods,
|
||||
// EF will do the case sensitive stuff like normal.
|
||||
MethodInfo methodInfo = typeof(string).GetMethod(filterType, new[] { typeof(string) });
|
||||
List<Expression> expressionArgs = new List<Expression>() { Expression.Constant(filter) };
|
||||
|
||||
var strPredicate = Expression.Call(lambda.Body, methodInfo, expressionArgs);
|
||||
|
||||
return Expression.Lambda<Func<T, bool>>(
|
||||
strPredicate,
|
||||
lambda.Parameters);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
using McRule.Tests.Models;
|
||||
using SMM;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using static SMM.FilterPatternHelpers;
|
||||
|
||||
namespace McRule.Tests.McAttributes {
|
||||
public class SearchFilter {
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() {
|
||||
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void SearchCriteriaWithASpaceCombinesSearchFilters() {
|
||||
var fixture = new SearchTestFixture();
|
||||
fixture.SearchCriteria = "Williams Robin ";
|
||||
|
||||
Assert.DoesNotThrow(() => fixture.DoSearch());
|
||||
var query = fixture.SearchExpression?.Compile()?.ToString();
|
||||
Console.WriteLine(query);
|
||||
}
|
||||
}
|
||||
|
||||
internal class SearchTestFixture {
|
||||
public string SearchCriteria { get; set; }
|
||||
|
||||
public Expression<Func<User, bool>> SearchExpression { get; set; }
|
||||
|
||||
public void DoSearch() {
|
||||
SearchExpression = GetUserFilter();
|
||||
}
|
||||
|
||||
private Expression<Func<User, bool>> GetUserFilter() {
|
||||
if (String.IsNullOrEmpty(SearchCriteria)) return PredicateBuilder.False<User>();
|
||||
|
||||
|
||||
var efGenerator = new SMM.NpgsqlGenerator();
|
||||
|
||||
var filter = new ExpressionRuleCollection() {
|
||||
TargetType = nameof(Models.User),
|
||||
};
|
||||
filter.RuleOperator = RuleOperator.Or;
|
||||
var _rules = new List<IExpressionPolicy>();
|
||||
|
||||
IExpressionPolicy subFilter = null;
|
||||
|
||||
// If the search criteria contains spaces, it's likely the intent is to match against display name,
|
||||
// as it's the only property where users likely have spaces in the value.
|
||||
if (SearchCriteria.Trim().Contains(' ')) {
|
||||
_rules.Add(
|
||||
new ExpressionRule((nameof(Models.User), nameof(Models.User.DisplayName), SearchCriteria.AddFilterOptionsIfNotSpecified(FilterOptions.Contains | FilterOptions.IgnoreCase)))
|
||||
);
|
||||
subFilter = new ExpressionRuleCollection();
|
||||
((ExpressionRuleCollection)subFilter).RuleOperator = RuleOperator.And;
|
||||
var subRules = new List<IExpressionPolicy>();
|
||||
foreach (var x in SearchCriteria.Trim().Split()) subRules.Add(new ExpressionRule((nameof(Models.User), nameof(Models.User.Mail), x.Trim()?.AddFilterOptionsIfNotSpecified(FilterOptions.Contains | FilterOptions.IgnoreCase))));
|
||||
((ExpressionRuleCollection)subFilter).Rules = subRules;
|
||||
|
||||
filter.Rules = _rules;
|
||||
|
||||
// TODO make this kind of thing eaiser
|
||||
var metaExpression = new ExpressionRuleCollection() {
|
||||
TargetType = nameof(Models.User),
|
||||
RuleOperator = RuleOperator.Or,
|
||||
Rules = new[] { filter, subFilter }
|
||||
};
|
||||
|
||||
return efGenerator.GetPredicateExpression<User>((IExpressionRuleCollection)metaExpression) ?? PredicateBuilder.False<User>();
|
||||
} else {
|
||||
_rules.AddRange(new[] {
|
||||
new ExpressionRule((nameof(Models.User), nameof(Models.User.Mail), SearchCriteria.AddFilterOptionsIfNotSpecified(FilterOptions.StartsWith | FilterOptions.IgnoreCase))),
|
||||
new ExpressionRule((nameof(Models.User), nameof(Models.User.Upn), SearchCriteria.AddFilterOptionsIfNotSpecified(FilterOptions.StartsWith | FilterOptions.IgnoreCase))),
|
||||
new ExpressionRule((nameof(Models.User), nameof(Models.User.EmployeeId), SearchCriteria.AddFilterOptionsIfNotSpecified(FilterOptions.StartsWith | FilterOptions.IgnoreCase))),
|
||||
new ExpressionRule((nameof(Models.User), nameof(Models.User.PreferredGivenName), SearchCriteria.AddFilterOptionsIfNotSpecified(FilterOptions.StartsWith | FilterOptions.IgnoreCase))),
|
||||
new ExpressionRule((nameof(Models.User), nameof(Models.User.PreferredSurname), SearchCriteria.AddFilterOptionsIfNotSpecified(FilterOptions.StartsWith | FilterOptions.IgnoreCase))),
|
||||
});
|
||||
}
|
||||
filter.Rules = _rules;
|
||||
|
||||
return efGenerator.GetPredicateExpression<User>((IExpressionRuleCollection)filter) ?? PredicateBuilder.False<User>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace McRule.Tests.Models {
|
||||
public class User {
|
||||
|
||||
public DateTime? LastFetched { get; set; } = null;
|
||||
|
||||
public DateTime? Merged { set; get; } = null;
|
||||
|
||||
public DateTime? Modified { get; set; } = null;
|
||||
|
||||
public DateTime? Created { get; set; } = null;
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
public bool Deleted { get; set; }
|
||||
|
||||
public string? Tenant { get; set; }
|
||||
|
||||
public Guid AadId { get; set; }
|
||||
|
||||
public string? Upn { get; set; }
|
||||
|
||||
public string? Mail { get; set; }
|
||||
|
||||
public string? DisplayName { get; set; }
|
||||
|
||||
public string? EmployeeId { get; set; }
|
||||
|
||||
public string? AdEmployeeId { get; set; }
|
||||
|
||||
public string? HrEmployeeId { get; set; }
|
||||
|
||||
public string? Wid { get; set; }
|
||||
|
||||
public string? CreationType { get; set; }
|
||||
|
||||
public string? Company { get; set; }
|
||||
|
||||
public string? Title { get; set; }
|
||||
|
||||
public string? PreferredGivenName { get; set; }
|
||||
|
||||
public string? PreferredSurname { get; set; }
|
||||
|
||||
public string? Articles { get; set; }
|
||||
|
||||
public string? Pronouns { get; set; }
|
||||
|
||||
public string? OnPremiseDn { get; set; }
|
||||
}
|
||||
}
|
|
@ -9,7 +9,10 @@
|
|||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.17" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="7.0.11" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1" />
|
||||
<PackageReference Include="NUnit.Analyzers" Version="3.3.0" />
|
||||
|
|
|
@ -10,6 +10,7 @@ namespace McRule {
|
|||
public class ExpressionRuleCollection : IExpressionRuleCollection, IExpressionPolicy {
|
||||
public Guid Id { get; set; } = Guid.NewGuid();
|
||||
public string Name { get; set; }
|
||||
public IDictionary<string, string[]> Properties { get; set; } = new Dictionary<string, string[]>();
|
||||
public RuleOperator RuleOperator { get; set; } = RuleOperator.And;
|
||||
public IEnumerable<IExpressionPolicy> Rules { get; set; }
|
||||
public string TargetType { get; set; }
|
||||
|
@ -49,10 +50,10 @@ namespace McRule {
|
|||
Property = input.Item2;
|
||||
Value = input.Item3;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Returns an expression tree targeting an object type based on policy parameters.
|
||||
/// </summary>
|
||||
/// </summary>
|
||||
public Expression<Func<T, bool>>? GetPredicateExpression<T>() {
|
||||
if (!(typeof(T).Name.Equals(this.TargetType, StringComparison.CurrentCultureIgnoreCase))) return null;
|
||||
|
||||
|
@ -65,7 +66,7 @@ namespace McRule {
|
|||
|
||||
/// <summary>
|
||||
/// Returns an expression tree targeting an object type based on policy parameters.
|
||||
/// </summary>
|
||||
/// </summary>
|
||||
public Expression<Func<T, bool>>? GeneratePredicateExpression<T>(ExpressionGenerator generator) {
|
||||
if (!(typeof(T).Name.Equals(this.TargetType, StringComparison.CurrentCultureIgnoreCase))) return null;
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ namespace McRule {
|
|||
public interface IExpressionRuleCollection {
|
||||
public Guid Id { get; }
|
||||
public IEnumerable<IExpressionPolicy> Rules { get; }
|
||||
public IDictionary<string, string[]> Properties { get; }
|
||||
public RuleOperator RuleOperator { get; }
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
<UserSecretsId>63a98c68-03bd-4069-b6b9-e0978081c430</UserSecretsId>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<Title>McRule - Rule based expression generator</Title>
|
||||
<Version>0.3.0</Version>
|
||||
<Version>0.3.1</Version>
|
||||
<Company>Sean McArdle</Company>
|
||||
<Description>Library for generating expression trees from simple policy rules.</Description>
|
||||
<Copyright>2023 Sean McArdle</Copyright>
|
||||
|
|
|
@ -410,7 +410,7 @@ public abstract class ExpressionGeneratorBase : ExpressionGenerator
|
|||
}
|
||||
|
||||
// The value may have the right type and should just be returned.
|
||||
if (comparison is Expression<Func<T, bool>> result) {
|
||||
if (comparison is Expression<Func<T, bool>> result && result != default(Expression<Func<T, bool>>)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue