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;