I have a version, which is based on @DmitryZyr 's answer, and uses 2 answer of the question How do I create an expression tree calling IEnumerable<TSource>.Any(...)?. Thanks to Aaron Heusser and Barry Kelly:
class Program
{
#region Copied from Expression.Call question
static MethodBase GetGenericMethod(Type type, string name, Type[] typeArgs, Type[] argTypes, BindingFlags flags)
{
int typeArity = typeArgs.Length;
var methods = type.GetMethods()
.Where(m => m.Name == name)
.Where(m => m.GetGenericArguments().Length == typeArity)
.Select(m => m.MakeGenericMethod(typeArgs));
return Type.DefaultBinder.SelectMethod(flags, methods.ToArray(), argTypes, null);
}
static bool IsIEnumerable(Type type)
{
return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>);
}
static Type GetIEnumerableImpl(Type type)
{
// Get IEnumerable implementation. Either type is IEnumerable<T> for some T,
// or it implements IEnumerable<T> for some T. We need to find the interface.
if (IsIEnumerable(type))
return type;
Type[] t = type.FindInterfaces((m, o) => IsIEnumerable(m), null);
Debug.Assert(t.Length == 1);
return t[0];
}
static Expression CallAny(Expression collection, Expression predicateExpression)
{
Type cType = GetIEnumerableImpl(collection.Type);
collection = Expression.Convert(collection, cType); // (see "NOTE" below)
Type elemType = cType.GetGenericArguments()[0];
Type predType = typeof(Func<,>).MakeGenericType(elemType, typeof(bool));
// Enumerable.Any<T>(IEnumerable<T>, Func<T,bool>)
MethodInfo anyMethod = (MethodInfo)
GetGenericMethod(typeof(Enumerable), "Any", new[] { elemType },
new[] { cType, predType }, BindingFlags.Static);
return Expression.Call(anyMethod, collection, predicateExpression);
}
#endregion
public class TestEntity
{
public int Id { get; set; }
public TestEntity[] Value { get; set; }
}
private static TestEntity CreateTestEntity(int id, params TestEntity[] values)
{
return new TestEntity { Id = id, Value = values };
}
static void Main(string[] args)
{
const string connectionString = "mongodb://localhost:27017";
var client = new MongoClient(connectionString);
var db = client.GetDatabase("TestEntities");
IMongoCollection<TestEntity> collection = db.GetCollection<TestEntity>("Entities");
collection.InsertOne(CreateTestEntity(1, CreateTestEntity(2, CreateTestEntity(3, CreateTestEntity(4)))));
const int selectedId = 4;
int searchDepth = 6;
// builds the expression tree of expanding x => x.Value.Any(...)
var filter = GetFilterForDepth(selectedId, searchDepth);
var testEntity = collection.Find(filter).FirstOrDefault();
if (testEntity != null)
{
UpdateItem(testEntity, selectedId);
collection.ReplaceOne(filter, testEntity);
}
}
private static bool UpdateItem(TestEntity testEntity, int selectedId)
{
if (testEntity.Id == selectedId)
{
return true;
}
if (UpdateItem(testEntity.Value[0], selectedId))
testEntity.Value[0] = CreateTestEntity(11);
return false;
}
private static FilterDefinition<TestEntity> GetFilterForDepth(int id, int depth)
{
// item
var idEqualsParam = Expression.Parameter(typeof(TestEntity), "item");
// .Id
var idProp = Expression.Property(idEqualsParam, "Id");
// item.Id == id
var idEquals = Expression.Equal(idProp, Expression.Constant(id));
// item => item.Id == id
var idEqualsLambda = Expression.Lambda<Func<TestEntity, bool>>(idEquals, idEqualsParam);
// x
var anyParam = Expression.Parameter(typeof(TestEntity), "x");
// .Value
var valueProp = Expression.Property(anyParam, "Value");
// Expression.Call would not find easily the appropriate .Any((TestEntity x) => x == id)
// .Value.Any(item => item.Id == id)
var callAny = CallAny(valueProp, idEqualsLambda);
// x => x.Value.Any(item => item.Id == id)
var firstAny = Expression.Lambda<Func<TestEntity, bool>>(callAny, anyParam);
return NestedFilter(Builders<TestEntity>.Filter.Eq(x => x.Id, id), firstAny, depth);
}
static int paramIndex = 0;
private static FilterDefinition<TestEntity> NestedFilter(FilterDefinition<TestEntity> actual, Expression<Func<TestEntity, bool>> whereExpression, int depth)
{
if (depth == 0)
{
return actual;
}
// paramX
var param = Expression.Parameter(typeof(TestEntity), "param" + paramIndex++);
// paramX.Value
var valueProp = Expression.Property(param, "Value");
// paramX => paramX.Value.Any(...)
var callLambda = Expression.Lambda<Func<TestEntity, bool>>(CallAny(valueProp, whereExpression), param);
return NestedFilter(Builders<TestEntity>.Filter.Where(whereExpression), callLambda, depth - 1) | actual;
}
}
It's still fixed length depth search, but the depth can be varied dinamically. Just 1 step to upgrade the code to try 1st level, 2nd level, .... And the depth is infinite