Conditionally Populate Class Field in LINQ To Entities - c#

I have an EF class that looks like this:
public class Item
public string ItemId{ get; set; }
public string NormalDescription { get; set; }
public string LongDescription { get; set; }
public string ShortDescription { get; set; }
.. <snip>
In addition, I have a DTO that looks like this:
public class ItemDTO
public string Id { get; set; }
public string DisplayName { get; set; }
.. <snip>
Upon loading the data from the 'Item' class into the DTO, I need to conditionally set 'DisplayName' based on a configuration setting. In other words, I'm looking for something similar to:
return _repo.GetAsQueryable<Item>()
.Select(i=> new ItemDTO
{
Id = i.ItemId,
DisplayName = (setting == 1) ? i.NormalDescription :
(setting == 2) ? i.LongDescription :
(setting == 3) ? i.ShortDescription :
String.Empty
}
Of course, this results in some very inefficient SQL (using 'CASE' to evaluate each possible value) being sent to the database. This is a performance issue as there's a TON of description fields on the Item.
That being said, is there a way to select ONLY the field that's required to populate the 'DisplayName' value?
In other words, instead of a query filled with 'CASE WHEN' logic, I'd like to ONLY retrieve one of the Description values based on my application configuration setting.

You should create lambda Expression dynamically:
var typeOfItem = typeof(Item);
var argParam = Expression.Parameter(typeOfItem, "x");
var itemIdProperty = Expression.Property(argParam, "ItemId");
var properties = typeOfItem.GetProperties();
Expression descriptionProperty;
if (setting < properties.Count())
descriptionProperty = Expression.Property(argParam, properties[setting].Name);
else
descriptionProperty = Expression.Constant(string.Empty);
var ItemDTOType = typeof(ItemDTO);
var newInstance = Expression.MemberInit(
Expression.New(ItemDTOType),
new List<MemberBinding>()
{
Expression.Bind(ItemDTOType.GetMember("Id")[0], itemIdProperty),
Expression.Bind(ItemDTOType.GetMember("DisplayName")[0], descriptionProperty),
}
);
var lambda = Expression.Lambda<Func<Item, ItemDTO>>(newInstance, argParam);
return _repo.GetAsQueryable<Item>().Select(lambda);

Something like this?
var repo = _repo.GetAsQueryable<Item>();
if (setting == 1)
{
return repo.Select(i => new ItemDTO
{
Id = i.ItemId,
DisplayName = i.NormalDescription
});
}
if (setting == 2)
{
return repo.Select(i => new ItemDTO
{
Id = i.ItemId,
DisplayName = i.LongDescription
});
}
if (setting == 3)
{
return repo.Select(i => new ItemDTO
{
Id = i.ItemId,
DisplayName = i.ShortDescription
});
}
return repo.Select(i => new ItemDTO
{
Id = i.ItemId,
DisplayName = String.Empty
});
EDIT
You can create the expression dynamically as Slava Utesinov showed. If you do not want to build the whole expression, you can replace just the parts you want:
public class UniRebinder : ExpressionVisitor
{
readonly Func<Expression, Expression> replacement;
UniRebinder(Func<Expression, Expression> replacement)
{
this.replacement = replacement;
}
public static Expression Replace(Expression exp, Func<Expression, Expression> replacement)
{
return new UniRebinder(replacement).Visit(exp);
}
public override Expression Visit(Expression p)
{
return base.Visit(replacement(p));
}
}
Expression<Func<Item, ItemDTO>> ReplaceProperty(
int setting, Expression<Func<Item, ItemDTO>> value)
{
Func<MemberExpression, Expression> SettingSelector(int ss)
{
switch (ss)
{
case 1: return x => Expression.MakeMemberAccess(x.Expression, typeof(Item).GetProperty(nameof(Item.NormalDescription)));
case 2: return x => Expression.MakeMemberAccess(x.Expression, typeof(Item).GetProperty(nameof(Item.LongDescription)));
case 3: return x => Expression.MakeMemberAccess(x.Expression, typeof(Item).GetProperty(nameof(Item.ShortDescription)));
default: return x => Expression.Constant(String.Empty);
}
}
return (Expression<Func<Item, ItemDTO>>)UniRebinder.Replace(
value,
x =>
{
if (x is MemberExpression memberExpr
&& memberExpr.Member.Name == nameof(Item.NormalDescription))
{
return SettingSelector(setting)(memberExpr);
}
return x;
});
}
private void Test()
{
var repo = (new List<Item>() {
new Item() {
ItemId ="1",
LongDescription = "longd1",
NormalDescription = "normald1",
ShortDescription = "shortd1" },
new Item() {
ItemId ="2",
LongDescription = "longd2",
NormalDescription = "normald2",
ShortDescription = "shortd2" }
}).AsQueryable();
for (int selector = 1; selector < 5; ++selector)
{
var tst = repo.Select(ReplaceProperty(selector,
i => new ItemDTO
{
Id = i.ItemId,
DisplayName = i.NormalDescription
})).ToList();
Console.WriteLine(selector + ": " + string.Join(", ", tst.Select(x => x.DisplayName)));
//Output:
//1: normald1, normald2
//2: longd1, longd2
//3: shortd1, shortd2
//4: ,
}
}

Related

combine multiple lambda expressions with different types to one expression

I want to combine some separated lambda expressions and build one final expression of them.
example classes :
class Address {
public string city { get; set; }
public string country { get; set; }
}
class ClassA {
public int Id { get; set; }
public Address address { get; set; }
}
class ClassB {
public int Id { get; set; }
public ClassA objectA { get; set; }
}
each class have one lambda expression :
Expression<Func<ClassA,bool>> classARule = a =>
a.Id > 1 && a.address.city == "city1" || a.address.country == "us"
Expression<Func<ClassB,bool>> classBRule = b => b.Id == 100
because ClassB has one property of ClassA it's possible to create an expression with both conditions. example :
// I want to create this expected object at runtime using classARule and classBRule
Expression<Func<ClassB,bool>> expected = b =>
(b.Id == 100) &&
(b.objectA.Id > 1 && b.objectA.address.city == "city1" || b.objectA.address.country == "us")
if I want to generate expected expression at runtime I should somehow convert a parameter of classARule to b.objectA
the problem is I know how to combine two expressions but I don't know how to replace a parameter with some other object. in this case b.objectA
Update - To avoid more confusion
the goal is to achieve Expression<Func<ClassB,bool>> expected expression at runtime using classARule and classBRule
Fortunately, I solved the problem.
The final result here is for others if they encounter such a problem.
public static Expression<Func<B, bool>> Combine<B, A>(this Expression<Func<B, bool>> expr1, Expression<Func<A, bool>> expr2, Expression<Func<B, A>> property)
{
// this is (q) parameter of my property
var replaceParameter = property.Parameters[0];
// replacing all (b) parameter with the (q)
// these two lines converts `b => b.Id == 100` to `q => q.Id == 100`
// using ReplaceExpVisitor class
var leftVisitor = new ReplaceExpVisitor(replaceParameter);
var left = leftVisitor.Visit(expr1.Body);
// the property body is 'q.objectA'
var replaceBody = property.Body;
// now i'm replacing every (a) parameter of my second expression to 'q.objectA'
// these two lines convert this statement:
// a.Id > 1 && a.address.city == "city1" || a.address.country == "us"
// to this :
// q.objectA.Id > 1 && q.objectA.address.city == "city1" || q.objectA.address.country == "us"
var rightVisitor = new ReplaceExpVisitor(replaceBody);
var right = rightVisitor.Visit(expr2.Body);
// creating new expression and pass (q) reference to it (replaceParameter).
return Expression.Lambda<Func<B, bool>>(Expression.AndAlso(left, right), replaceParameter);
}
// this is a simple class to replace all parameters with new expression
private class ReplaceExpVisitor : ExpressionVisitor
{
private readonly Expression _newval;
public ReplaceExpVisitor(Expression newval) => _newval = newval;
protected override Expression VisitParameter(ParameterExpression node)
{
return _newval;
}
}
usage :
var result = classBRule.Combine(classARule, q => q.objectA);
// or
Expression<Func<ClassB,bool>> result =
Combine<ClassB, ClassA>(classBRule, classARule, q => q.objectA);
/*
result is equal to the expected expression in the first example now
result output :
q =>
((q.Id == 100) &&
(((q.objectA.Id > 1) && (q.objectA.address.city == "city1")) ||
(q.objectA.address.country == "us")))
*/
https://dotnetfiddle.net/KnV3Dz
You'll need to compile the expression:
class Address
{
public string city { get; set; }
public string country { get; set; }
}
class ObjectA
{
public int Id { get; set; }
public Address address { get; set; }
}
class ObjectB
{
public int Id { get; set; }
public ObjectA objectA { get; set; }
}
Expression<Func<ObjectB, bool>> expected = b =>
(b.Id == 100) &&
(b.objectA.Id > 1 && b.objectA.address.city == "City1" || b.objectA.address.country == "US");
// Compile the Expression
var expectedItems = expected.Compile();
List<ObjectB> objBs = new List<ObjectB>();
var address = new Address();
var objA = new ObjectA();
var objB = new ObjectB();
address.city = "City1";
address.country = "US";
objA.Id = 1;
objB.Id = 100;
objA.address = address;
objB.objectA = objA;
objBs.Add(objB);
address = new Address();
objA = new ObjectA();
objB = new ObjectB();
address.city = "City2";
address.country = "US";
objA.Id = 3;
objB.Id = 100;
objA.address = address;
objB.objectA = objA;
objBs.Add(objB);
// Use expectedItems
var result = objBs.FirstOrDefault(b => expectedItems(b));

Build an lambda Expression tree with a specific field in a linked entity

Data MODEL
public class TABLE
{
[Key]
public int ID { get; set; }
public int ONEFIELD { get; set; }
// -------------------- ForeignKey --------------------
[ForeignKey("People")]
public long PeopleID { get; set; }
public virtual People People { get; set; }
}
public class People
{
[Key]
public long PeopleID { get; set; }
public int CountryID { get; set; }
}
I need to build a lambda to query this MODEL :
Get TABLE.ONEFIELD = 1 AND TABLE.PEOPLE.COUNTRYID = 6
LINQ equivalent
_context.TABLEs
.Where(e => e.ONEFIELD == 1)
.Include(e => e.People)
.Where(i=>i.People.CountryID == 6);
My try
public static Expression<Func<TEntity, bool>> BuildLambda<TEntity>(OBJTYPE obj)
{
var item = Expression.Parameter(typeof(TEntity), "table");
Expression query = null;
// 1
var prop1 = Expression.Property(item, "ONEFIELD");
var value1 = Expression.Constant(1);
var equal1 = Expression.Equal(prop1, value1);
var lambdaFIELDONE = Expression.Lambda<Func<TEntity, bool>>(equal1, item);
query = lambdaFIELDONE.Body;
// 2
var prop2 = Expression.Property(item, typeof(People).Name + ".CountryID");
var value2 = Expression.Constant(6);
var equal2 = Expression.Equal(prop2, value2);
var lambdaCOUNTRYID = Expression.Lambda<Func<TEntity, bool>>(equal2, item);
query = Expression.And(query, lambdaCOUNTRYID);
}
but I receive this error
System.ArgumentException: Instance property 'People.CountryID' is not defined for type 'SOLUTION.Models.TABLE'
I don't need Generic, just a fixed lambda (and I couldn't use LINQ).
I tried several things to catch People.CountryID like
Expression.Property(item1, typeof(People).GetProperty("CountryID"));
Expression.Property(item, typeof(People).Name+"." + typeof(People).GetProperty("CountryID"));
Expression.Property(item, typeof(People).Name + "." + typeof(People).GetProperties().Where(x => x.Name == "CountryID").FirstOrDefault().Name);
no success
Any ideas ? thanks
So, to build a nested property access, you must nest the Expressions that access each level. Then you can combine the tests into a body and finally create the lambda for the result:
public static Expression<Func<TEntity, bool>> BuildLambda<TEntity>(OBJTYPE obj) {
// (TEntity table)
var parmTable = Expression.Parameter(typeof(TEntity), "table");
// table.ONEFIELD
var prop1 = Expression.Property(parmTable, "ONEFIELD");
// table.ONEFIELD == 1
var equal1 = Expression.Equal(prop1, Expression.Constant(1));
// table.People
var prop2_1 = Expression.Property(parmTable, nameof(People));
// table.People.CountryID
var prop2_2 = Expression.Property(prop2_1, "CountryID");
// table.People.CountryID == 6
var equal2 = Expression.Equal(prop2_2, Expression.Constant(6));
// table.ONEFIELD == 1 && table.People.CountryID == 6
var finalBody = Expression.AndAlso(equal1, equal2);
// table => table.ONEFIELD == 1 && table.People.CountryID == 6
return Expression.Lambda<Func<TEntity, bool>>(finalBody, parmTable);
}
Using LINQPad, you could create a sample lambda and then use the Dump method and you would see that a nested FieldExpression is created, which is what gets created when you call Expression.Property:
Expression<Func<TEntity, int>> f = t => t.People.CountryID;
f.Dump();

How to convent viewmodel to Expression<Func<T,bool>>?

Piggybacking off of a very similar question...
I need to generate an Expression from a ViewModel to pass as a search predicate for IQueryable.Where. I need to be able to include/exclude query parameters based on what is provided by the user. Example:
public class StoresFilter
{
public int[] Ids { get; set; }
[StringLength(150)]
public string Name { get; set; }
[StringLength(5)]
public string Abbreviation { get; set; }
[Display(Name = "Show all")]
public bool ShowAll { get; set; } = true;
public Expression<Func<Store, bool>> ToExpression()
{
List<Expression<Func<Store, bool>>> expressions = new List<Expression<Func<Store, bool>>>();
if (Ids != null && Ids.Length > 0)
{
expressions.Add(x => Ids.Contains(x.Id));
}
if (Name.HasValue())
{
expressions.Add(x => x.Name.Contains(Name));
}
if (Abbreviation.HasValue())
{
expressions.Add(x => x.Abbreviation.Contains(Abbreviation));
}
if (!ShowAll)
{
expressions.Add(x => x.Enabled == true);
}
if (expressions.Count == 0)
{
return x => true;
}
// how to combine list of expressions into composite expression???
return compositeExpression;
}
}
Is there a simple way to build a composite expression from a list of expressions? Or do I need to go through the process of manually building out the expression using ParameterExpression, Expression.AndAlso, ExpressionVisitor, etc?
You should not build and combine Expressions, but instead of this you should do it through IQuerable<Store> via .Where chain. Moreover, source.Expression will contain desired expression:
public IQueryable<Store> ApplyFilter(IQueryable<Store> source)
{
if (Ids != null && Ids.Length > 0)
source = source.Where(x => Ids.Contains(x.Id));
if (Name.HasValue())
source = source.Where(x => x.Name.Contains(Name));
if (Abbreviation.HasValue())
source = source.Where(x => x.Abbreviation.Contains(Abbreviation));
if (!ShowAll)
source = source.Where(x => x.Enabled == true);
//or return source.Expression as you wanted
return source;
}
Usage:
var filter = new StoresFilter { Name = "Market" };
var filteredStores = filter.ApplyFilter(context.Stores).ToList();
void Main()
{
var store = new Store
{
Id = 1,
Abbreviation = "ABC",
Enabled = true,
Name = "DEF"
};
var filter = new Filter<Store>
{
Ids = new HashSet<int>(new [] {1,2,3,4}),
Abbreviation = "GFABC",
Enabled = true,
Name = "SDEFGH",
ShowAll = false
}
var expression = filter.ToExpression(store);
var parameterType = Expression.Parameter(typeof(Store), "obj");
// Generate Func from the Expression Tree
Func<Store,bool> func = Expression.Lambda<Func<Store,bool>>(expression,parameterType).Compile();
}
public class Store
{
public int Id {get; set;}
public string Name {get; set;}
public string Abbreviation { get; set; }
public bool Enabled { get; set; }
}
public class Filter<T> where T : Store
{
public HashSet<int> Ids { get; set; }
public string Name { get; set; }
public string Abbreviation { get; set; }
public bool Enabled {get; set;}
public bool ShowAll { get; set; } = true;
public Expression ToExpression(T data)
{
var parameterType = Expression.Parameter(typeof(T), "obj");
var expressionList = new List<Expression>();
if (Ids != null && Ids.Count > 0)
{
MemberExpression idExpressionColumn = Expression.Property(parameterType, "Id");
ConstantExpression idConstantExpression = Expression.Constant(data.Id, typeof(int));
MethodInfo filtersMethodInfo = typeof(HashsetExtensions).GetMethod("Contains", new[] { typeof(HashSet<int>), typeof(int) });
var methodCallExpression = Expression.Call(null, filtersMethodInfo, idExpressionColumn, idConstantExpression);
expressionList.Add(methodCallExpression);
}
if (!string.IsNullOrEmpty(Name))
{
MemberExpression idExpressionColumn = Expression.Property(parameterType, "Name");
ConstantExpression idConstantExpression = Expression.Constant(data.Name, typeof(string));
MethodInfo filtersMethodInfo = typeof(StringExtensions).GetMethod("Contains", new[] { typeof(string), typeof(string) });
var methodCallExpression = Expression.Call(null, filtersMethodInfo, idExpressionColumn, idConstantExpression);
expressionList.Add(methodCallExpression);
}
if (!string.IsNullOrEmpty(Abbreviation))
{
MemberExpression idExpressionColumn = Expression.Property(parameterType, "Abbreviation");
ConstantExpression idConstantExpression = Expression.Constant(data.Abbreviation, typeof(string));
MethodInfo filtersMethodInfo = typeof(StringExtensions).GetMethod("Contains", new[] { typeof(string), typeof(string) });
var methodCallExpression = Expression.Call(null, filtersMethodInfo, idExpressionColumn, idConstantExpression);
expressionList.Add(methodCallExpression);
}
if (!ShowAll)
{
MemberExpression idExpressionColumn = Expression.Property(parameterType, "Enabled");
var binaryExpression = Expression.Equal(idExpressionColumn, Expression.Constant(true, typeof(bool)));
expressionList.Add(binaryExpression);
}
if (expressionList.Count == 0)
{
expressionList.Add(BinaryExpression.Constant(true));
}
// Aggregate List<Expression> data into single Expression
var returnExpression = expressionList.Skip(1).Aggregate(expressionList.First(), (expr1,expr2) => Expression.And(expr1,expr2));
return returnExpression;
// Generate Func<T,bool> - Expression.Lambda<Func<T,bool>>(returnExpression,parameterType).Compile();
}
}
public static class StringExtensions
{
public static bool Contains(this string source, string subString)
{
return source?.IndexOf(subString, StringComparison.OrdinalIgnoreCase) >= 0;
}
}
public static class HashsetExtensions
{
public static bool Contains(this HashSet<string> source, string subString)
{
return source.Contains(subString,StringComparer.OrdinalIgnoreCase);
}
}
How it works ?
Only in simple equality cases you can use BinaryExpression like Expression.Equal, Expression.GreaterThan, which is shown for the property like "ShowAll"
For other cases like string / Array / List Contains, you need extension method, which can take two types and provide the result. A separate Contains for string to make it case neutral. Also for collection Hashset has a better choice, it has O(1) time complexity, unlike O(N) for an array
We use MethodCallExpression to call the extension methods
Finally we aggreagte all the expressions, which can be compiled to create Func<T,bool>
In case you need something like x => true, then BinaryExpression.Constant(true) is sufficient
I have provided a Sample implementation using the Store class that you have defined

Building a dynamic expression tree to filter on a collection property 2

Maybe this is a duplicate thread, but I am going to try, because there is a tiny difference.
I am trying to build a dynamic expression to filter a collection property.
The code:
public class TestEntity
{
public int ID { get; set; }
public string Name { get; set; }
public IEnumerable<string> Values { get; set; }
}
public class TestFilter
{
public TestFilter()
{
var itens = new List<TestEntity>();
itens.Add(new TestEntity { ID = 1, Name = "Test1", Values = new List<string> { "V1", "V2" } });
itens.Add(new TestEntity { ID = 2, Name = "Test2", Values = new List<string> { "V6", "V3" } });
itens.Add(new TestEntity { ID = 3, Name = "Test3", Values = new List<string> { "V4", "V5" } });
itens.Add(new TestEntity { ID = 4, Name = "Test4", Values = new List<string> { "V2", "V3" } });
itens = itens.Where(e => e.Values.Any(c => c.Equals("V2"))).ToList();
**//Result IDs: 1, 4**
}
}
The filter above will give me IDs 1 and 4 as result.
I want to filter entities where exists a certain value in the collection "Values".
So far, I have tried this thread, but didnt realize how it can be done.
Any help would be apreciated.
So you are seeking for a method which given a collection property name and value will produce Where predicate like e => e.Collection.Any(c => c == value).
You can use the following extension method (hope the code is self explanatory):
public static class QueryableExtensions
{
public static IQueryable<T> WhereAnyEquals<T>(this IQueryable<T> source, string collectionName, object value)
{
var e = Expression.Parameter(typeof(T), "e");
var collection = Expression.PropertyOrField(e, collectionName);
var itemType = (collection.Type.IsIEnumerableT() ? collection.Type :
collection.Type.GetInterfaces().Single(IsIEnumerableT))
.GetGenericArguments()[0];
var c = Expression.Parameter(itemType, "c");
var itemPredicate = Expression.Lambda(
Expression.Equal(c, Expression.Constant(value)),
c);
var callAny = Expression.Call(
typeof(Enumerable), "Any", new Type[] { itemType },
collection, itemPredicate);
var predicate = Expression.Lambda<Func<T, bool>>(callAny, e);
return source.Where(predicate);
}
private static bool IsIEnumerableT(this Type type)
{
return type.IsInterface && type.IsConstructedGenericType &&
type.GetGenericTypeDefinition() == typeof(IEnumerable<>);
}
}
like this:
itens = itens.AsQueryable().WhereAnyEquals("Values", "V2").ToList();
If you step through the code, the variable predicate contains the expression you are asking for.

Update/Delete a sub document in mongodb using C# driver

I have 2 classes:
public class Vote
{
public string VoteId { get; set; }
public string Question { get; set; }
public List<VoteAnswer> AnswerList { get; set; }
}
And:
public class VoteOption
{
public string OptionId { get; set; }
public string OptionName { get; set; }
public double VoteCount { get; set; }
}
How can i update/delete a VoteOption in a Vote where VoteId = voteId and OptionId = optionId? Using C# driver.
First I get VoteOption by:
var v = col.FindOneAs<Vote>(Query.EQ("VoteID", voteId));
VoteOption vo = v.AnswerList.Find(x => x.OptionId == optionId);
End set some value to it:
vo.OptionName = "some option chose";
vo.VoteCount = 1000;
But i don't know what next step to update this vo to Vote parent.
And, if i want to delete this vo, show me that way!
Data in MongoDB like that:
{
"_id" : "460b3a7ff100",
"Question" : "this is question?",
"AnswerList" : [{
"OptionId" : "1",
"OptionName" : "Option 1",
"VoteCount" : 0.0
}, {
"OptionId" : "2",
"OptionName" : "Option 2",
"VoteCount" : 0.0
}, {
"OptionId" : "3",
"OptionName" : "Option 3",
"VoteCount" : 0.0
}
}]
}
To update subdocument you can use this:
var update = Update.Set("AnswerList.$.OptionName", "new").Set("AnswerList.$.VoteCount", 5);
collection.Update(Query.And(Query.EQ("_id", new BsonObjectId("50f3c313f216ff18c01d1eb0")), Query.EQ("AnswerList.OptionId", "1")), update);
profiler:
"query" : { "_id" : ObjectId("50f3c313f216ff18c01d1eb0"), "AnswerList.OptionId" : "1" },
"updateobj" : { "$set" : { "AnswerList.$.OptionName" : "new", "AnswerList.$.VoteCount" : 5 } }
And to remove:
var pull = Update<Vote>.Pull(x => x.AnswerList, builder => builder.EQ(q => q.OptionId, "2"));
collection.Update(Query.And(Query.EQ("_id", new BsonObjectId("50f3c313f216ff18c01d1eb0")), Query.EQ("AnswerList.OptionId", "2")), pull);
profiler:
"query" : { "_id" : ObjectId("50f3c313f216ff18c01d1eb0"), "AnswerList.OptionId" : "2" },
"updateobj" : { "$pull" : { "AnswerList" : { "OptionId" : "2" } } }
Another way is to update parent document with modified child collection.
// Example function for update like count add like user using c#
public PostModel LikeComment(LikeModel like)
{
PostModel post = new PostModel();
_client = new MongoClient();
_database = _client.GetDatabase("post");
var collection = _database.GetCollection<PostModel>("post");
var _filter = Builders<PostModel>.Filter.And(
Builders<PostModel>.Filter.Where(x => x.PostId == like.PostId),
Builders<PostModel>.Filter.Eq("Comments.CommentId", like.CommentId));
var _currentLike = collection.Find(Builders<PostModel>.Filter.Eq("PostId", like.PostId)).FirstOrDefault().Comments.Find(f => f.CommentId == like.CommentId).Like;
var update = Builders<PostModel>.Update.Set("Comments.$.Like", _currentLike + 1);
collection.FindOneAndUpdate(_filter, update);
var addUser = Builders<PostModel>.Update.Push("Comments.$.LikeUsers", like.UserId);
collection.FindOneAndUpdate(_filter, addUser);
var _findResult = collection.Find(_filter).FirstOrDefault();
return _findResult;
}
//Delete comment
public PostModel delcomment(int postId, int commentId)
{
_client = new MongoClient();
_database = _client.GetDatabase("post");
var collection = _database.GetCollection<PostModel>("post");
var filter = Builders<PostModel>.Filter.Eq("PostId", postId);
var update = Builders<PostModel>.Update.PullFilter("Comments",
Builders<Comments>.Filter.Eq("CommentId", commentId));
collection.FindOneAndUpdate(filter, update);
var _findResult = collection.Find(filter).FirstOrDefault();
return _findResult;
}
Late answer but this is how you do it without having strings. If you modify your properties code will not compile. First time using expression tries in production code! They are awesome!
Models:
class Phone
{
public string _id { get; set; }
public string Name { get; set; }
public DateTime DateCreated { get; set; }
// Contain multiple lines as subdocument
public List<Line> Lines { get; set; }
}
class Line
{
public string Name { get; set; }
public string PhoneNumber { get; set; }
}
Code: this is how I create my update statements without depending on strings.
var update = new UpdateDocument<Phone>();
// set filter
update.SetFilter(x => x._id == "123456789");
update.AddValueToUpdate(p => p.Name, "New Name");
update.AddValueToUpdate(p => p.Lines[0].Name, "Line 1");
update.AddValueToUpdate(p => p.Lines[1].Name, "Line 2");
update.AddValueToUpdate(p => p.DateCreated, DateTime.UtcNow);
var updateQuery = update.Build();
This creates this! That is what you need to pass to mondo in order to do the update
{ "_id" : "123456789" },
{$set:
{"Name":"New Name","Lines.0.Name":"Line 1","Lines.1.Name":"Line 2","DateCreated":ISODate("2021-04-30T16:04:59.332Z")}
}
If you wish that code to work here are the helper classes:
using MongoDB.Bson;
using System.Linq.Expressions;
using MongoDB.Bson.Serialization;
class UpdateDocument<T>
{
/// <summary>
/// _id of document to update.
/// </summary>
private string _filter;
/// <summary>
/// Example:
/// FirstName, Antonio
/// Education.Elementary.Year, 2004
/// </summary>
private List<KeyValuePair<string, object>> _valuesToUpdate { get; set; } = new List<KeyValuePair<string, object>>();
public void SetFilter(Expression<Func<T, bool>> filterDefinition)
{
var documentSerializer = BsonSerializer.SerializerRegistry.GetSerializer<T>();
var where = Builders<T>.Filter.Where(filterDefinition).Render(documentSerializer, BsonSerializer.SerializerRegistry);
_filter = where.ToJson();
}
public void AddValueToUpdate(string name, object value)
{
_valuesToUpdate.Add(new KeyValuePair<string, object>(name, value));
}
public void AddValueToUpdate(Expression<Func<T, object>> name, object value)
{
var memberExpression = name.Body as MemberExpression;
if (memberExpression == null)
{
var unaryExpression = name.Body as UnaryExpression;
if (unaryExpression != null && unaryExpression.NodeType == ExpressionType.Convert)
memberExpression = unaryExpression.Operand as MemberExpression;
}
var result = memberExpression.ToString();
result = result.Substring(result.IndexOf('.') + 1);
if (result.Contains("get_Item"))
result = Regex.Replace(result, #"(?x) get_Item \( (\d+) \)", m => $"{m.Groups[1].Value}");
AddValueToUpdate(result, value);
}
public string Build()
{
if (_valuesToUpdate.Any() == false)
{
// nothing to update
return null;
}
/*
update({
_id: 7,
"comments._id": ObjectId("4da4e7d1590295d4eb81c0c7")
},{
$set: {"comments.$.type": abc}
}, false, true
);
*/
StringBuilder sb = new StringBuilder();
sb.Append(_filter);
sb.Append(',');
sb.Append("{");
{
sb.Append("$set:{");
foreach (var item in _valuesToUpdate)
{
sb.Append('"');
sb.Append(item.Key);
sb.Append('"');
sb.Append(':');
var value = BsonExtensionMethods.ToJson(item.Value);
sb.Append(value);
sb.Append(',');
}
// remove last comma
sb.Length--;
sb.Append('}');
}
sb.Append("}");
return sb.ToString();
}
}

Categories

Resources