I am quite new to Cassandra so I have a question that I can not find answer to. In EF Core I can pass a list of LINQ expression as conditions and aggregate them so I can find what I need for example:
public async Task<IEnumerable<string>> GetDataStream(List<Expression<Func<Model, bool>>> predicates)
{
var query = _context.Model.AsQueryable();
if (predicates != null)
{
query = predicates.Aggregate(query, (#event, condition) => #event.Where(condition));
}
return await query.Select(data => data.).ToListAsync();
}
Now I am wondering if there is a such possibility in Cassandra. I tried:
public async Task<IEnumerable<Model>> Find(List<Expression<Func<Model, bool>>> predicates, int assetId)
{
IQueryable<Model> query = _table.AsQueryable();
if (predicates != null)
{
query = predicates.Aggregate(query, (#event, condition) => #event.Where(condition));
}
return await query.Select(data => data); // here is a problem dont know ow to execute this
}
So is such a thing possible?
EDIT:
So I tried with aggregate combination
query.Select(d => d).Execute();
also and got this exception in result
The expression Call
= [SELECT gap_end, gap_start, uuid FROM gaps_state_data.Where(data => (data.EndValue == null))]
is not supported in None parse phase.
It looks expression aggregate is not being format for some reason.
I believe this is what you are looking for:
public static async Task<IEnumerable<Model>> Find(Table<Model> table, List<Expression<Func<Model, bool>>> predicates)
{
CqlQuery<Model> query = table;
if (predicates != null)
{
query = predicates.Aggregate(query, (#event, condition) => #event.Where(condition));
}
return await query.ExecuteAsync();
}
It is basically the same as your answer but you don't actually need the .Select() part, you just need to cast the table to a CqlQuery<Model> object.
Here's the full example that I used to test this (keep in mind that this snippet creates and drops a keyspace and table):
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Net;
using System.Threading.Tasks;
using Cassandra;
using Cassandra.Data.Linq;
using Cassandra.Mapping.Attributes;
using Exception = System.Exception;
namespace OssSandbox
{
public class ProgramLinq
{
public static void Main(string[] args)
{
Cassandra.Diagnostics.CassandraTraceSwitch.Level = TraceLevel.Info;
Trace.AutoFlush = true;
using var cluster = Cassandra.Cluster.Builder()
.AddContactPoint(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9042))
.Build();
using var session = cluster.Connect();
session.CreateKeyspaceIfNotExists("ks1", new Dictionary<string, string> { { "class", "SimpleStrategy"}, { "replication_factor", "1"} });
var table = new Table<Model>(session);
session.Execute("DROP TABLE IF EXISTS ks1.Model");
table.CreateIfNotExists();
table.Insert(new Model { Id = 1, Value1 = "1", Value2 = "2", Value3 = "3" }).Execute();
table.Insert(new Model { Id = 1, Value1 = "1", Value2 = "2", Value3 = "23" }).Execute();
table.Insert(new Model { Id = 1, Value1 = "1", Value2 = "22", Value3 = "23" }).Execute();
table.Insert(new Model { Id = 1, Value1 = "21", Value2 = "22", Value3 = "23" }).Execute();
table.Insert(new Model { Id = 1, Value1 = "31", Value2 = "32", Value3 = "33" }).Execute();
table.Insert(new Model { Id = 1, Value1 = "41", Value2 = "42", Value3 = "43" }).Execute();
table.Insert(new Model { Id = 2, Value1 = "221", Value2 = "222", Value3 = "223" }).Execute();
var results1 = Find(table, new List<Expression<Func<Model, bool>>>
{
m => m.Id == 1,
m => m.Value1 == "1",
m => m.Value2 == "2",
}).GetAwaiter().GetResult();
PrintRowsResult(results1, "Id == 1 && Value1 == 1 && Value2 == 2");
}
public static void PrintRowsResult(IEnumerable<Model> results, string query)
{
Console.WriteLine();
Console.WriteLine(query);
Console.WriteLine();
try
{
Console.WriteLine();
foreach (var row in results)
{
Console.WriteLine("Id: " + row.Id);
Console.WriteLine("Value1: " + row.Value1);
Console.WriteLine("Value2: " + row.Value2);
Console.WriteLine("Value3: " + row.Value3);
Console.WriteLine();
}
}
catch (Exception ex)
{
Console.WriteLine();
Console.WriteLine("####### ERROR: " + ex.Message);
Console.WriteLine();
}
}
public static async Task<IEnumerable<Model>> Find(Table<Model> table, List<Expression<Func<Model, bool>>> predicates)
{
CqlQuery<Model> query = table;
if (predicates != null)
{
query = predicates.Aggregate(query, (#event, condition) => #event.Where(condition));
}
Console.WriteLine(query.ToString()); // just for debug purposes
return await query.ExecuteAsync();
}
[Cassandra.Mapping.Attributes.Table(Keyspace = "ks1")]
public class Model
{
[Cassandra.Mapping.Attributes.PartitionKey]
public int Id { get; set; }
[Cassandra.Mapping.Attributes.ClusteringKey(0)]
public string Value1 { get; set; }
[Cassandra.Mapping.Attributes.ClusteringKey(1)]
public string Value2 { get; set; }
[Cassandra.Mapping.Attributes.ClusteringKey(2)]
public string Value3 { get; set; }
}
}
}
Ok, I figured this one out, using hint in comments, let me show full code of this and how it should work:
public Task<IEnumerable<Model>> Find(List<Expression<Func<Model, bool>>> predicates)
{
CqlQuery<Model> query = _table.Select(d => d);
if (predicates != null)
{
query = predicates.Aggregate(query, (#event, condition) => #event.Where(condition));
}
return query.ExecuteAsync();
}
As I needed to replace table type with IQueryable, but when I do this early on like _table.AsQueryable() select expression compiled itself. So I needed start expression that will change table to IQueryable and that is the role of that Select(). Afterwards I could add expressions from parameter.
Related
Hi I have a scenario to filter the data based sub-object field please help me. From controller as query I pass Expression String.
class MasterDocument
{
private Id;
public ICollection<SubDocument> documents { get; set; }
}
class SubDocument
{
private Id;
public int Age { get; set; }
}
var filterQuery = "documents.Age == 25";
var filteredResult = MasterDocument.Where(filterQuery).ToList();
to filter the Data
how to create Expression from string to filter data from Substructure.
Well, that's quite complicated topic, but i will first give code example and later focus on caveats:
I would follow approach and define it as another extension method:
using System.Linq.Expressions;
namespace ConsoleApp2;
public static class WhereExtensions
{
public static IEnumerable<T> Where<T>(
this IEnumerable<T> collection,
string filterExpression)
{
// Most probably you'd like to have here more sophisticated validations.
var itemsToCompare = filterExpression.Split("==")
.Select(x => x.Trim())
.ToArray();
if (itemsToCompare.Length != 2)
{
throw new InvalidOperationException();
}
var source = Expression.Parameter(typeof(T));
var property = itemsToCompare[0];
var valueToCompareAgainst = itemsToCompare[1];
var memberExpr = source.GetMemberExpression(property);
var comparisonExpr = Expression.Equal(
Expression.Call(memberExpr, typeof(object).GetMethod("ToString")),
Expression.Constant(valueToCompareAgainst)
);
var predicate = Expression.Lambda<Func<T, bool>>(comparisonExpr, source);
return collection.Where(predicate.Compile());
}
public static MemberExpression GetMemberExpression(
this ParameterExpression parameter,
string memberExpression)
{
var properties = memberExpression.Split('.');
if (!properties.Any())
{
throw new InvalidOperationException();
}
var memberExpr = Expression.PropertyOrField(parameter, properties[0]);
foreach (var property in properties.Skip(1))
{
memberExpr = Expression.PropertyOrField(memberExpr, property);
}
return memberExpr;
}
}
and the usage would be:
using ConsoleApp2;
var example = new[]
{
new TestClass() { Id = 1, Description = "a" },
new TestClass() { Id = 2, Description = "a" },
new TestClass() { Id = 3, Description = "b" },
new TestClass() { Id = 4, Description = "b" },
};
var result1 = example.Where("Id == 1").ToList();
var result2 = example.Where("Description == b").ToList();
Console.WriteLine("Items with Id == 1");
result1.ForEach(x => Console.WriteLine($"Id: {x.Id} , Descr: {x.Description}"));
Console.WriteLine("Items with Description == b");
result2.ForEach(x => Console.WriteLine($"Id: {x.Id} , Descr: {x.Description}"));
class TestClass
{
public int Id { get; set; }
public string Description { get; set; }
}
This codes returns:
NOW, THE CAVEATS
It's very tricky to cast value to compare against to an arbitrary type T, that's why i reversed the problem, and I call "ToString" on whetever member we want to compare
Expression.Call(memberExpr, typeof(object).GetMethod("ToString"))
But this also could have it's own issues, as often "ToString" returns default tpye name. But works well with integers and simple value types.
I used mongodb in my backend to store some customer personal information and I need to fetch the user who ages 30, 32 and 35.
I tried below ways to get but it returns zero results and I used C# MongoDB.Driver
C# code
Age = new string[] { "26-30", "31-35" }
DateTime today = DateTime.Today;
var filter = Builders<Customer>.Filter.Empty;
foreach (var item in searchFilterBlock.Age)
{
var ageBetween = item.Split('-');
int.TryParse(ageBetween[0], out int startYear);
int.TryParse(ageBetween[1], out int endYear);
var start = today.AddYears(-startYear);
var end = today.AddYears(-endYear);
filter = filter & (Builders<Customer>.Filter.Gte(x => x.Dob, start)
& Builders<Customer>.Filter.Lte(x=>x.Dob, end));
}
// to execute the filter
var searchResult = _context.Customer.Find(filter).ToList(); // it return 0 result
Need to get who has ages 30, 32 and 35.
you can get customers who are aged 30,32 and 35 by using an $or filter like the following:
db.Customer.find({
"$or": [
{
"DOB": {
"$lte": ISODate("1989-06-22T14:57:50.168Z"),
"$gte": ISODate("1988-06-22T14:57:50.168Z")
}
},
{
"DOB": {
"$lte": ISODate("1987-06-22T14:57:50.168Z"),
"$gte": ISODate("1986-06-22T14:57:50.168Z")
}
},
{
"DOB": {
"$lte": ISODate("1984-06-22T14:57:50.168Z"),
"$gte": ISODate("1983-06-22T14:57:50.168Z")
}
}
]
})
here's the c# code that generated the above find query using the convenience library MongoDB.Entities [disclaimer: i'm the author]
using MongoDB.Driver;
using MongoDB.Entities;
using System;
using System.Collections.Generic;
namespace StackOverflow
{
public class Program
{
public class Customer : Entity
{
public string Name { get; set; }
public DateTime DOB { get; set; }
}
private static void Main(string[] args)
{
new DB("test");
(new[] {
new Customer{ Name = "I am 29", DOB = DateTime.UtcNow.AddYears(-29)},
new Customer{ Name = "I am 30", DOB = DateTime.UtcNow.AddYears(-30)},
new Customer{ Name = "I am 32", DOB = DateTime.UtcNow.AddYears(-32)},
new Customer{ Name = "I am 35", DOB = DateTime.UtcNow.AddYears(-35)},
new Customer{ Name = "I am 36", DOB = DateTime.UtcNow.AddYears(-36)}
}).Save();
var ages = new[] { 30, 32, 35 };
var filters = new List<FilterDefinition<Customer>>();
foreach (var age in ages)
{
var start = DateTime.UtcNow.AddYears(-age);
var end = DateTime.UtcNow.AddYears(-age - 1);
filters.Add(DB.Filter<Customer>()
.Where(c => c.DOB <= start && c.DOB >= end));
}
var customers = DB.Find<Customer>()
.Many(f => f.Or(filters))
.ToArray();
}
}
}
I have a collection where I want to programatically add OR condtions to a linq query. I know how to add AND condtions like the following:
var mySet = SomeFactory.GetData();
foreach(var nameFilter in nameFilters)
{
mySet = mySet.Where(item => item.Name == nameFilter);
}
foreach(var ageFilter in ageFilters)
{
mySet = mySet.Where(item => item.Age == ageFilter)
}
however, if I wanted these to be OR conditions rather than be 'AND' together, how would I do that? I.E. I can do this if I know I will always have both and never an array of different values:
mySet.Where(item => item.Name == nameFilter[0] || item.Name == nameFilter[1] ... || item.Age == ageFilter[0] || item.Age == ageFilter[1] || ...);
TL;DR: I want to be able to chain an unknown number of boolean checks into a single expression evaluated with OR statements. For example, if I have a cross reference of People named Mary or Jim who are either 32 or 51.
PredicateBuilder would help you to apply where clauses in flexibility. You can find extension method here.
var filterOfNames = new List<string> {"Sample", "Sample2"};
var filterOfAges = new List<int> { 21, 33, 45 };
var mySet = SomeFactory.GetData();
var predicate = PredicateBuilder.True<TypeOfmySet>();
foreach (var filterOfName in filterOfNames)
{
//If it is the first predicate, you should apply "And"
if (predicate.Body.NodeType == ExpressionType.Constant)
{
predicate = predicate.And(x => x.Name == filterOfName);
continue;
}
predicate = predicate.Or(x => x.Name == filterOfName);
}
foreach (var filterOfAge in filterOfAges)
{
//If it is the first predicate, you should apply "And"
if (predicate.Body.NodeType == ExpressionType.Constant)
{
predicate = predicate.And(x => x.Age == filterOfAge);
continue;
}
predicate = predicate.Or(x => x.Age == filterOfAge);
}
//I don't know the myset has which type in IQueryable or already retrieved in memory collection. If it is IQueryable, don't compile the predicate otherwise compile it.
//var compiledPredicate = predicate.Compile();
mySet = mySet.Where(predicate);
There is no additive "or" in LINQ. Combining "Where" expressions is always evaluated as "and".
However, you can build predicates, add them to a list, then test them. I think this code does what you want:
using System;
using System.Collections.Generic;
class Item
{
internal string Name { get; set; }
internal short Age { get; set; }
internal string City { get; set; }
}
public static class ExtensionMethods
{
public static List<T> FindAll<T>(this List<T> list, List<Predicate<T>> predicates)
{
List<T> L = new List<T>();
foreach (T item in list)
{
foreach (Predicate<T> p in predicates)
{
if (p(item)) L.Add(item);
}
}
return L;
}
}
class Program
{
static void Main(string[] args)
{
List<Item> items = new List<Item>();
items.Add(new Item { Name = "Bob", Age = 31, City = "Denver" });
items.Add(new Item { Name = "Mary", Age = 44, City = "LA" });
items.Add(new Item { Name = "Sue", Age = 21, City = "Austin" });
items.Add(new Item { Name = "Joe", Age = 55, City = "Redmond" });
items.Add(new Item { Name = "Tom", Age = 81, City = "Portland" });
string nameFilter = "Bob,Mary";
//string ageFilter = "55,21";
string ageFilter = null;
string cityFilter = "Portland";
List<Predicate<Item>> p = new List<Predicate<Item>>();
if (nameFilter != null)
{
p.Add(i => nameFilter.Contains(i.Name));
}
if (cityFilter != null)
{
p.Add(i => cityFilter.Contains(i.City));
}
if (ageFilter != null)
{
p.Add(i => ageFilter.Contains(i.Age.ToString()));
}
var results = items.FindAll(p);
foreach (var result in results)
{
Console.WriteLine($"{result.Name} {result.Age} {result.City}");
}
}
}
As the title states, I would like to build a dynamic expression using propertyInfo and a value.
Currently i have something like this
public static IQueryable<T> WhereValueEquals<T>(this IQueryable<T> q, FieldFor fieldFor, string fieldValue)
where T : ModuleData
{
switch (fieldFor)
{
case FieldFor.Name:
return q.Where(e => e.Name == fieldValue);
case FieldFor.Reference:
return q.Where(e => e.Reference == fieldValue);
case FieldFor.Text0:
return q.Where(e => e.Reference == fieldValue);
default:
return q;
}
}
Now the case statement is only going to get longer and longer, but this was fine during early stages of development.
Now, using the FieldFor enumeration, i can get the propertyInfo using an extension i already wrote. So how can i return an IQueryable from WhereValueEquals using the string value passed in and a propertyInfo
So far i have
public static IQueryable<T> WhereValueEquals<T>(this IQueryable<T> q, FieldFor fieldFor, string fieldValue)
where T : ModuleData
{
PropertyInfo propertyInfo = typeof(T).GetFieldProperties().GetPropertyByFieldFor(fieldFor);
//return q.Where(expression);
}
At some point in the time I had to do something similar, and there are a ton of code on Stackoverflow that show you how to build an expression builder. Well this is my POC, hope it helps you
void Main()
{
var ops = new List<Ops>
{
new Ops
{
//OperandType = typeof(string),
OpType=OpType.Equals,
OperandName = "Name",
ValueToCompare = "MM"
},
new Ops
{
//OperandType = typeof(int),
OpType=OpType.Equals,
OperandName = "ID",
ValueToCompare = 1
},
};
var testClasses = new List<TestClass>
{
new TestClass { ID =1, Name = "MM", Date = new DateTime(2014,12,1)},
new TestClass { ID =2, Name = "BB", Date = new DateTime(2014,12,2)}
};
var funct = ExpressionBuilder.BuildExpressions<TestClass>(ops);
foreach(var item in testClasses.Where(funct))
{
Console.WriteLine("ID " +item.ID);
Console.WriteLine("Name " +item.Name);
Console.WriteLine("Date" + item.Date);
}
}
// Define other methods and classes here
public enum OpType
{
Equals
}
public class Ops
{
//public Type OperandType {get; set;}
public OpType OpType {get; set;}
public string OperandName {get;set;}
public object ValueToCompare {get;set;}
}
public class TestClass
{
public int ID {get;set;}
public string Name {get; set;}
public DateTime Date {get;set;}
}
public class ExpressionBuilder
{
public static Func<T,bool> BuildExpressions<T>( List<Ops> opList)
{
Expression currentExpression= null;
var parameter = Expression.Parameter(typeof(T), "prop");
for(int i =0; i< opList.Count; i++)
{
var op = opList[i];
Expression innerExpression = null;
switch(op.OpType)
{
case OpType.Equals :
{
var innerParameter = Expression.Property(parameter,op.OperandName);
var ConstExpression = Expression.Constant(op.ValueToCompare);
innerExpression = Expression.Equal(innerParameter, ConstExpression);
break;
}
}
if (i >0)
{
currentExpression = Expression.And(currentExpression, innerExpression);
}
else
{
currentExpression = innerExpression;
}
}
var lambdaExpression = Expression.Lambda<Func<T,bool>>(currentExpression, new []{parameter});
Console.WriteLine(lambdaExpression);
return lambdaExpression.Compile() ;
}
}
I am attempting to create a dynamic sort for my basic data class (just a bunch of properties).
The following code demonstrates what I am trying to do. It almost works. The only thing I don't understand is why ThenBy seems to completely resort the list. Can anyone explain why?
private void SortList(string[] sortCols)
{
bool first = true;
IOrderedEnumerable<Data> returnVal = null;
Console.WriteLine("Sorting test data");
if (sortCols.Length < 1)
return;
foreach (string col in sortCols)
{
if (first)
{
returnVal = _testData.OrderBy(p => typeof(Data).GetProperty(col).GetValue(p, null));
//Or OrderByDescending
first = false;
}
else
{
returnVal = returnVal.ThenBy(p => typeof(Data).GetProperty(col).GetValue(p, null));
//Or ThenByDescending
}
}
_testData = new List<Data>(returnVal);
}
_testData is just a List
The Data class looks like this:
public class Data
{
public string Name { get; set; }
public int Age { get; set; }
public double Income { get; set; }
public bool Married { get; set; }
public override string ToString()
{
return Name + "; age=" + Age.ToString() + "; income=" + Income.ToString() + "; married=" + Married.ToString();
}
}
Unable to duplicate your problem. Everything seems to sort properly:
public static void Main()
{
var data1 = new Data() { Name = "Albert", Age = 99 };
var data2 = new Data() { Name = "Zebra", Age = 1};
var data3 = new Data() { Name = "Zebra", Age = 99};
var data4 = new Data() { Name = "Albert", Age = 1 };
_testData.Add(data1);
_testData.Add(data2);
_testData.Add(data3);
_testData.Add(data4);
SortList(new string[] { "Name", "Age" });
foreach(var data in _testData)
{
Console.WriteLine(data.ToString());
}
Console.WriteLine(string.Empty);
SortList(new string[] { "Age", "Name" });
foreach(var data in _testData)
{
Console.WriteLine(data.ToString());
}
}
Results:
Sorting test data
Albert; age=1; income=0; married=False
Albert; age=99; income=0; married=False
Zebra; age=1; income=0; married=False
Zebra; age=99; income=0; married=False
.
Sorting test data
Albert; age=1; income=0; married=False
Zebra; age=1; income=0; married=False
Albert; age=99; income=0; married=False
Zebra; age=99; income=0; married=False
Update (For .Net 4.0)
Changing your code slightly solves the issue (example):
private static void SortList(string[] sortCols)
{
Console.WriteLine("Sorting test data");
if (sortCols.Length < 1)
return;
IOrderedEnumerable<Data> returnVal = _testData.OrderBy(p => typeof(Data).GetProperty(sortCols[0]).GetValue(p, null));
foreach (string col in sortCols.Skip(1))
{
returnVal = returnVal.ThenBy(p => typeof(Data).GetProperty(col).GetValue(p, null));
//Or ThenByDescending
}
_testData = returnVal.ToList();
}
You're better off creating an IComparer that explicitly does a comparison for each property (so you could have a public property on it that specifies the property order). You could then just sort once.
Something like so
public class DataComparer : Comparer<Data>
{
private readonly IList<string> _sortedProperties;
public DataComparer(IEnumerable<string> sortedProperties)
{
_sortedProperties = new List<string>(sortedProperties);
}
public override int Compare(Data x, Data y)
{
int result = 0;
foreach (var property in _sortedProperties)
{
if (property == "Name")
{
result = String.Compare(x.Name, y.Name, StringComparison.Ordinal);
}
else if (property == "Age")
{
result = x.Age.CompareTo(y.Age);
}
// Do other comparisons here
if (result != 0)
return result;
}
return 0;
}
}
This implementation will create a DataComparer with the given collection of properties that act as the properties that need to be compared. Then the Compare() will loop through the properties in the desired sort order until it finds a non-matching comparison and return.
The end result will be a faster sort operation that outputs something like
// "Jim Frost" 13
// "Jim Frost" 24
// "Jim Smith" 11
Where we want to sort on the Name property first, then the Age etc.
Edit:
You can sort it like so,
_testData.Sort(new DataComparer(sortCols));