From d792813cd5e695a102ca4df6ef6169d1d713f89c Mon Sep 17 00:00:00 2001 From: Matt Furden Date: Fri, 15 Jun 2018 01:16:36 -0700 Subject: [PATCH] Allow Filters on different sources to share the same name Allows Posts and Comments to both use the IsNew filter with their own implementations. --- Sieve/Extensions/MethodInfoExtended.cs | 147 ++++++++++++++++++ Sieve/Services/SieveProcessor.cs | 28 ++-- SieveUnitTests/Entities/Comment.cs | 8 +- SieveUnitTests/General.cs | 44 ++++++ .../Services/SieveCustomFilterMethods.cs | 10 +- 5 files changed, 225 insertions(+), 12 deletions(-) create mode 100644 Sieve/Extensions/MethodInfoExtended.cs diff --git a/Sieve/Extensions/MethodInfoExtended.cs b/Sieve/Extensions/MethodInfoExtended.cs new file mode 100644 index 0000000..009fdbc --- /dev/null +++ b/Sieve/Extensions/MethodInfoExtended.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using System.Text; + +namespace Sieve.Extensions +{ + // The default GetMethod doesn't allow for generic methods which means + // custom filters for different sources can't share the same name. + // https://stackoverflow.com/questions/4035719/getmethod-for-generic-method + public static partial class MethodInfoExtended + { + /// + /// Search for a method by name and parameter types. + /// Unlike GetMethod(), does 'loose' matching on generic + /// parameter types, and searches base interfaces. + /// + /// + public static MethodInfo GetMethodExt(this Type thisType, + string name, + params Type[] parameterTypes) + { + return GetMethodExt(thisType, + name, + BindingFlags.Instance + | BindingFlags.Static + | BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.FlattenHierarchy, + parameterTypes); + } + + /// + /// Search for a method by name, parameter types, and binding flags. + /// Unlike GetMethod(), does 'loose' matching on generic + /// parameter types, and searches base interfaces. + /// + /// + public static MethodInfo GetMethodExt(this Type thisType, + string name, + BindingFlags bindingFlags, + params Type[] parameterTypes) + { + MethodInfo matchingMethod = null; + + // Check all methods with the specified name, including in base classes + GetMethodExt(ref matchingMethod, thisType, name, bindingFlags, parameterTypes); + + // If we're searching an interface, we have to manually search base interfaces + if (matchingMethod == null && thisType.IsInterface) + { + foreach (Type interfaceType in thisType.GetInterfaces()) + GetMethodExt(ref matchingMethod, + interfaceType, + name, + bindingFlags, + parameterTypes); + } + + return matchingMethod; + } + + private static void GetMethodExt(ref MethodInfo matchingMethod, + Type type, + string name, + BindingFlags bindingFlags, + params Type[] parameterTypes) + { + // Check all methods with the specified name, including in base classes + foreach (MethodInfo methodInfo in type.GetMember(name, + MemberTypes.Method, + bindingFlags)) + { + // Check that the parameter counts and types match, + // with 'loose' matching on generic parameters + ParameterInfo[] parameterInfos = methodInfo.GetParameters(); + if (parameterInfos.Length == parameterTypes.Length) + { + int i = 0; + for (; i < parameterInfos.Length; ++i) + { + if (!parameterInfos[i].ParameterType + .IsSimilarType(parameterTypes[i])) + break; + } + if (i == parameterInfos.Length) + { + if (matchingMethod == null) + matchingMethod = methodInfo; + else + throw new AmbiguousMatchException( + "More than one matching method found!"); + } + } + } + } + + /// + /// Special type used to match any generic parameter type in GetMethodExt(). + /// + public class T + { } + + /// + /// Determines if the two types are either identical, or are both generic + /// parameters or generic types with generic parameters in the same + /// locations (generic parameters match any other generic paramter, + /// but NOT concrete types). + /// + private static bool IsSimilarType(this Type thisType, Type type) + { + // Ignore any 'ref' types + if (thisType.IsByRef) + thisType = thisType.GetElementType(); + if (type.IsByRef) + type = type.GetElementType(); + + // Handle array types + if (thisType.IsArray && type.IsArray) + return thisType.GetElementType().IsSimilarType(type.GetElementType()); + + // If the types are identical, or they're both generic parameters + // or the special 'T' type, treat as a match + if (thisType == type || ((thisType.IsGenericParameter || thisType == typeof(T)) + && (type.IsGenericParameter || type == typeof(T)))) + return true; + + // Handle any generic arguments + if (thisType.IsGenericType && type.IsGenericType) + { + Type[] thisArguments = thisType.GetGenericArguments(); + Type[] arguments = type.GetGenericArguments(); + if (thisArguments.Length == arguments.Length) + { + for (int i = 0; i < thisArguments.Length; ++i) + { + if (!thisArguments[i].IsSimilarType(arguments[i])) + return false; + } + return true; + } + } + + return false; + } + } +} diff --git a/Sieve/Services/SieveProcessor.cs b/Sieve/Services/SieveProcessor.cs index 6070938..7188e55 100644 --- a/Sieve/Services/SieveProcessor.cs +++ b/Sieve/Services/SieveProcessor.cs @@ -315,8 +315,9 @@ namespace Sieve.Services private IQueryable ApplyCustomMethod(IQueryable result, string name, object parent, object[] parameters, object[] optionalParameters = null) { var customMethod = parent?.GetType() - .GetMethod(name, - _options.Value.CaseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); + .GetMethodExt(name, + _options.Value.CaseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance, + new Type[] { typeof(IQueryable), typeof(string), typeof(string) }); if (customMethod != null) { @@ -337,17 +338,24 @@ namespace Sieve.Services throw; } } - catch (ArgumentException) // name matched with custom method for a different type - { - var expected = typeof(IQueryable); - var actual = customMethod.ReturnType; - throw new SieveIncompatibleMethodException(name, expected, actual, - $"{name} failed. Expected a custom method for type {expected} but only found for type {actual}"); - } } else { - throw new SieveMethodNotFoundException(name, $"{name} not found."); + var incompatibleCustomMethod = parent?.GetType() + .GetMethod(name, + _options.Value.CaseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance); + + if (incompatibleCustomMethod != null) + { + var expected = typeof(IQueryable); + var actual = incompatibleCustomMethod.ReturnType; + throw new SieveIncompatibleMethodException(name, expected, actual, + $"{name} failed. Expected a custom method for type {expected} but only found for type {actual}"); + } + else + { + throw new SieveMethodNotFoundException(name, $"{name} not found."); + } } return result; diff --git a/SieveUnitTests/Entities/Comment.cs b/SieveUnitTests/Entities/Comment.cs index 6aa9acc..d00b34b 100644 --- a/SieveUnitTests/Entities/Comment.cs +++ b/SieveUnitTests/Entities/Comment.cs @@ -1,9 +1,15 @@ -namespace SieveUnitTests.Entities +using System; +using Sieve.Attributes; + +namespace SieveUnitTests.Entities { public class Comment { public int Id { get; set; } + [Sieve(CanFilter = true, CanSort = true)] + public DateTimeOffset DateCreated { get; set; } = DateTimeOffset.UtcNow; + public string Text { get; set; } } } diff --git a/SieveUnitTests/General.cs b/SieveUnitTests/General.cs index 0d09912..efa9b00 100644 --- a/SieveUnitTests/General.cs +++ b/SieveUnitTests/General.cs @@ -15,6 +15,7 @@ namespace SieveUnitTests { private readonly SieveProcessor _processor; private readonly IQueryable _posts; + private readonly IQueryable _comments; public General() { @@ -52,6 +53,25 @@ namespace SieveUnitTests CategoryId = 2, }, }.AsQueryable(); + + _comments = new List + { + new Comment() { + Id = 0, + DateCreated = DateTimeOffset.UtcNow.AddDays(-20), + Text = "This is an old comment." + }, + new Comment() { + Id = 1, + DateCreated = DateTimeOffset.UtcNow.AddDays(-1), + Text = "This is a fairly new comment." + }, + new Comment() { + Id = 2, + DateCreated = DateTimeOffset.UtcNow, + Text = "This is a brand new comment." + }, + }.AsQueryable(); } [TestMethod] @@ -180,6 +200,30 @@ namespace SieveUnitTests Assert.IsTrue(result.Count() == 1); } + [TestMethod] + public void CustomFiltersOnDifferentSourcesCanShareName() + { + var postModel = new SieveModel() + { + Filters = "CategoryId==2,Isnew", + }; + + var postResult = _processor.Apply(postModel, _posts); + + Assert.IsTrue(postResult.Any(p => p.Id == 3)); + Assert.AreEqual(1, postResult.Count()); + + var commentModel = new SieveModel() + { + Filters = "Isnew", + }; + + var commentResult = _processor.Apply(commentModel, _comments); + + Assert.IsTrue(commentResult.Any(c => c.Id == 2)); + Assert.AreEqual(2, commentResult.Count()); + } + [TestMethod] public void MethodNotFoundExceptionWork() { diff --git a/SieveUnitTests/Services/SieveCustomFilterMethods.cs b/SieveUnitTests/Services/SieveCustomFilterMethods.cs index 6d67107..aaac842 100644 --- a/SieveUnitTests/Services/SieveCustomFilterMethods.cs +++ b/SieveUnitTests/Services/SieveCustomFilterMethods.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using Sieve.Services; using SieveUnitTests.Entities; @@ -13,6 +14,13 @@ namespace SieveUnitTests.Services return result; } + public IQueryable IsNew(IQueryable source, string op, string value) + { + var result = source.Where(c => c.DateCreated > DateTimeOffset.UtcNow.AddDays(-2)); + + return result; + } + public IQueryable TestComment(IQueryable source, string op, string value) { return source;