How do check for duplicate before AddRange? - entity-framework-core

This is my sample model,
Name
Age
I have a list of records for which I will use Entity Framework Core AddRange. This is my sample data, say list 1
John, 21
Mike, 18
Rick, 19
Alex, 20
I have another query which is a list of the existing records, say list 2.
John, 21
Alex, 20
I want to compare these 2 lists to get what exists in list 1 but not in list 2.
Mike, 18
Rick, 19
I tried using LINQ except but it can't be used for complex types. I also tried to use .Any like below but it returns nothing.
var result = list1.Where(x=> !list2.Any(y=>x.Name == y.Name));
I do not want to check and insert one record at one time. I want to insert by list of objects using AddRange.
Is there any Nuget package that can easily get this?

For not so big amount of records, you can use the following extension:
var nonExistent = await ctx.Some.GetNonExistentAsync(list2, x => x.Name);
Or with complex key:
var nonExistent = await ctx.Some.GetNonExistentAsync(list2,
x => new { x.Name, x.Other });
It will generate effective SQL to check which records do not exist.
And implementation:
public static class QueryableExtensions
{
public static async Task<List<T>> GetNonExistentAsync<T, TKey>(this IQueryable<T> query, IEnumerable<T> records,
Expression<Func<T, TKey>> keySelector, CancellationToken cancellationToken = default)
{
var recordsList = records.ToList();
var keySelectorCompiled = keySelector.Compile();
var predicate = BuildPredicate(recordsList, keySelector, keySelectorCompiled);
var existent = (await query
.Where(predicate)
.Select(keySelector)
.ToListAsync(cancellationToken))
.ToHashSet();
var result = recordsList.Where(r => !existent.Contains(keySelectorCompiled(r))).ToList();
return result;
}
private static Expression<Func<T, bool>> BuildPredicate<T, TKey>(IEnumerable<T> records, Expression<Func<T, TKey>> keySelector, Func<T, TKey> keySelectorCompiled)
{
var members = CollectMembers(keySelector.Body).ToList();
if (members.Count == 0)
throw new InvalidOperationException("No key found");
Expression? predicate = null;
if (members.Count == 1 && keySelector.Body.NodeType == ExpressionType.MemberAccess)
{
// we can use Contains
var recordsSequence = Expression.Constant(records.Select(keySelectorCompiled));
predicate = Expression.Call(typeof(Enumerable), nameof(Enumerable.Contains), new[] { typeof(TKey) },
recordsSequence, keySelector.Body);
}
else
{
foreach (var record in records)
{
Expression? subPredicate = null;
var recordExpression = Expression.Constant(record);
foreach (var m in members)
{
var memberExpression =
ReplacingExpressionVisitor.Replace(keySelector.Parameters[0], recordExpression, m);
var equality = Expression.Equal(m, memberExpression);
subPredicate = subPredicate == null ? equality : Expression.AndAlso(subPredicate, equality);
}
if (subPredicate == null)
throw new InvalidOperationException(); // should never happen
predicate = predicate == null ? subPredicate : Expression.OrElse(predicate, subPredicate);
}
}
predicate ??= Expression.Constant(false);
return Expression.Lambda<Func<T, bool>>(predicate, keySelector.Parameters);
}
private static IEnumerable<Expression> CollectMembers(Expression expr)
{
switch (expr.NodeType)
{
case ExpressionType.New:
{
var ne = (NewExpression)expr;
foreach (var a in ne.Arguments)
foreach (var m in CollectMembers(a))
{
yield return m;
}
break;
}
case ExpressionType.MemberAccess:
yield return expr;
break;
default:
throw new InvalidOperationException();
}
}
}

Related

System.Linq.Queryable.Except is not supported in Entity Framework Core

Please note this error message :
System.NotSupportedException: 'Could not parse expression
'value(Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable
This overload of the method 'System.Linq.Queryable.Except' is
currently not supported.'
Is supported in newer versions of ef core?
my code :
var tuple = SearchBrand(searchTerm);
var result = GetExceptionsBrand(tuple.Item1, categoryId);
return Json(new
{
iTotalDisplayRecords = tuple.Item2,
iDisplayedBrand = result
.Skip(page * 10)
.Take(10)
.ToList(),
});
public async Task<Tuple<IQueryable<BrandDto>, int, int>>SearchBrand(string searchTerm)
{
var result = _context.Brands
.Where(c => c.IsDeleted == displayIsDeleted)
.WhereDynamic(searchTerm)
return new Tuple<IQueryable<BrandDto>, int, int>(result,
filteredResultsCount, totalResultsCount);
}
public IQueryable<BrandDto> GetExceptionsBrand(IEnumerable<BrandDto> filteredBrand, int categoryId)
{
var query = _context.CategoriesBrands.Where(x => x.CategoryId == categoryId);
var selectedList = new List<BrandDto>();
foreach (var item in query)
{
var cb = new BrandDto()
{
BrandDto_BrandId = item.BrandId
};
selectedList.Add(cb);
}
IQueryable<BrandDto> ExcpetList = filteredBrand.AsQueryable().Except(selectedList, new ComparerBrand());
return ExcpetList;
}

How to update many documents using UpdateManyAsync

I have the following method to update a document in MongoDB:
public async Task UpdateAsync(T entity)
{
await _collection.ReplaceOneAsync(filter => filter.Id == entity.Id, entity);
}
Which works fine - I was just wondering if anybody has an example of how the UpdateManyAsync function works:
public async Task UpdateManyAsync(IEnumerable<T> entities)
{
await _collection.UpdateManyAsync(); // What are the parameters here
}
Any advice is appreciated!
UpdateManyAsync works the same way as update with multi: true in Mongo shell. So you can specify filtering condition and update operation and it will affect multiple documents. For instance to increment all a fields where a is greater than 10 you can use this method:
var builder = Builders<SampleClass>.Update;
await myCollection.UpdateManyAsync(x => x.a > 10, builder.Inc(x => x.a, 1));
I guess you'd like to replace multiple documents. That can be achieved using bulkWrite method. If you need generic method in C# then you can introduce some kind of marker interface to build filter part of replace operation:
public interface IMongoIdentity
{
ObjectId Id { get; set; }
}
Then you can add generic constaint to your class and use BuikWrite in .NET like below:
class YourRepository<T> where T : IMongoIdentity
{
IMongoCollection<T> collection;
public async Task UpdateManyAsync(IEnumerable<T> entities)
{
var updates = new List<WriteModel<T>>();
var filterBuilder = Builders<T>.Filter;
foreach (var doc in entities)
{
var filter = filterBuilder.Where(x => x.Id == doc.Id);
updates.Add(new ReplaceOneModel<T>(filter, doc));
}
await collection.BulkWriteAsync(updates);
}
}
As #mickl answer, you can not use x=> x.Id because it is a Generic
Use as below:
public async Task<string> UpdateManyAsync(IEnumerable<T> entities)
{
var updates = new List<WriteModel<T>>();
var filterBuilder = Builders<T>.Filter;
foreach (var doc in entities)
{
foreach (PropertyInfo prop in typeof(T).GetProperties())
{
if (prop.Name == "Id")
{
var filter = filterBuilder.Eq(prop.Name, prop.GetValue(doc));
updates.Add(new ReplaceOneModel<T>(filter, doc));
break;
}
}
}
BulkWriteResult result = await _collection.BulkWriteAsync(updates);
return result.ModifiedCount.ToString();
}
Or you go by Bson attribute:
public async Task UpdateManyAsync(IEnumerable<TEntity> objs, CancellationToken cancellationToken = default)
{
var updates = new List<WriteModel<TEntity>>();
var filterBuilder = Builders<TEntity>.Filter;
foreach (var obj in objs)
{
foreach (var prop in typeof(TEntity).GetProperties())
{
object[] attrs = prop.GetCustomAttributes(true);
foreach (object attr in attrs)
{
var bsonId = attr as BsonIdAttribute;
if (bsonId != null)
{
var filter = filterBuilder.Eq(prop.Name, prop.GetValue(obj));
updates.Add(new ReplaceOneModel<TEntity>(filter, obj));
break;
}
}
}
}
await _dbCollection.BulkWriteAsync(updates, null, cancellationToken);
}

LINQ to Entity cannot use System.Object.GetValue

Below find a method that does not work. We fail on the line query.Select(...
Below that find a method with hard coded object property names which does work. But, this method is obviously not dynamic, nor flexible. There may be many properties of a Customer I may wish to search on.
The error string is at bottom. I get it that somehow the LINQ to Entity is unable to deal with conversion of GetValue to some sort of TSQL. Would anyone know of how I might code this up?
public List<Customer> GetForQuery(params Tuple<string, string>[] keyValuePairs) {
using (var db = new DBEntities()) {
var availableProperties = typeof(Customer).GetTypeInfo().DeclaredProperties.ToList();
var query = db.Customers.Select(c => c);
foreach (Tuple<string, string> pair in keyValuePairs) {
PropertyInfo pi = availableProperties.First(p => p.Name.Equals(pair.Item1));
if (pi == null)
continue;
query = query.Where(u => pi.GetValue(u, null).ToString().StartsWith(pair.Item2));
}
var results = query.Select(c => c).ToList();
return results;
}
}
How I might call the above:
CustomerController custController = new CustomerController();
List<Customer> results = custController.GetForQuery(Tuple.Create<string, string>("FName", "Bob" ));
The working fixed method:
public List<Customer> GetForQuery(string firstName = "", string lastName = "", string phoneNumber = "") {
using (var db = new DBEntities()) {
var query = db.Customers.Select(c => c);
if (firstName.HasContent())
query = query.Where(u => u.FName.StartsWith(firstName));
if (lastName.HasContent())
query = query.Where(u => u.LName.StartsWith(lastName));
if (phoneNumber.HasContent())
query = query.Where(u => u.EveningPhone.StartsWith(phoneNumber));
var results = query.Select(c => c).ToList();
return results;
}
}
ERROR:
LINQ to Entities does not recognize the method 'System.Object GetValue(System.Object, System.Object[])' method, and this method cannot be translated into a store expression.

Entity framework ordering using reflection

I have an extension method for ordering, the sortExpression can be something like "Description" or "Description DESC", it is working perfectly for columns at the same table:
public static IQueryable<T> OrderBy<T>(this IQueryable<T> source, string sortExpression)
{
if (source == null)
throw new ArgumentNullException("source");
if (string.IsNullOrEmpty(sortExpression))
return source;
var parts = sortExpression.Split(' ');
var isDescending = false;
var propertyName = "";
var type = typeof(T);
if (parts.Length > 0 && parts[0] != "")
{
propertyName = parts[0];
if (parts.Length > 1)
isDescending = parts[1].ToLower().Contains("esc");
var prop = type.GetProperty(propertyName);
if (prop == null)
throw new ArgumentException(string.Format("No property '{0}' on type '{1}'", propertyName, type.Name));
var funcType = typeof(Func<,>)
.MakeGenericType(type, prop.PropertyType);
var lambdaBuilder = typeof(Expression)
.GetMethods()
.First(x => x.Name == "Lambda" && x.ContainsGenericParameters && x.GetParameters().Length == 2)
.MakeGenericMethod(funcType);
var parameter = Expression.Parameter(type);
var propExpress = Expression.Property(parameter, prop);
var sortLambda = lambdaBuilder
.Invoke(null, new object[] { propExpress, new ParameterExpression[] { parameter } });
var sorter = typeof(Queryable)
.GetMethods()
.FirstOrDefault(x => x.Name == (isDescending ? "OrderByDescending" : "OrderBy") && x.GetParameters().Length == 2)
.MakeGenericMethod(new[] { type, prop.PropertyType });
var result = (IQueryable<T>)sorter
.Invoke(null, new object[] { source, sortLambda });
return result;
}
return source;
}
Working Example:
var query = db.Audit.Include("AccessLevel").AsQueryable();
query = query.OrderBy("Description");
Please note that the "Description" column exists at the same table "Audit".
What I'm trying to do is to sort by a column in a relation table:
Like the following
var query = db.Audit.Include("AccessLevel").AsQueryable();
query = query.OrderBy("AccessLevel.Name");
Which is equivalent to:
query = query.OrderBy(o => o.AccessLevel.Name);
What is the required modification on my extension method ?
I solved it by using the following code:
public static IQueryable<T> OrderBy<T>(this IQueryable<T> source, string property)
{
return ApplyOrder<T>(source, parts[0], "OrderBy");
}
public static IQueryable<T> OrderByDescending<T>(this IQueryable<T> source, string property)
{
return ApplyOrder<T>(source, property, "OrderByDescending");
}
public static IQueryable<T> ThenBy<T>(this IOrderedQueryable<T> source, string property)
{
return ApplyOrder<T>(source, property, "ThenBy");
}
public static IQueryable<T> ThenByDescending<T>(this IOrderedQueryable<T> source, string property)
{
return ApplyOrder<T>(source, property, "ThenByDescending");
}
static IQueryable<T> ApplyOrder<T>(IQueryable<T> source, string property, string methodName)
{
if (source == null)
throw new ArgumentNullException("source");
if (string.IsNullOrEmpty(property))
return source;
string[] props = property.Split('.');
Type type = typeof(T);
ParameterExpression arg = Expression.Parameter(type, "x");
Expression expr = arg;
foreach (string prop in props)
{
// use reflection (not ComponentModel) to mirror LINQ
PropertyInfo pi = type.GetProperty(prop);
expr = Expression.Property(expr, pi);
type = pi.PropertyType;
}
Type delegateType = typeof(Func<,>).MakeGenericType(typeof(T), type);
LambdaExpression lambda = Expression.Lambda(delegateType, expr, arg);
object result = typeof(Queryable).GetMethods().Single(
method => method.Name == methodName
&& method.IsGenericMethodDefinition
&& method.GetGenericArguments().Length == 2
&& method.GetParameters().Length == 2)
.MakeGenericMethod(typeof(T), type)
.Invoke(null, new object[] { source, lambda });
return (IQueryable<T>)result;
}

Sort using Expression builder

I am trying to build generic sort method using Expressions.
I came up with the following method. For some reasons the code is breaking at the statement when a nested property is used in the sort expression.
var sortExpression = Expression.Lambda<Func<T, object>>
(Expression.Convert(Expression.Property(sortParam, sortColumn), typeof(object)), sortParam);
private static IQueryable<T> SortQuery<T>(IQueryable<T> query, string sortColumn)
{
if (!string.IsNullOrEmpty(sortColumn))
{
var sortParam = Expression.Parameter(typeof(T), "x");
Expression expr = sortParam;
foreach (var prop in sortColumn.Split('.'))
{
expr = Expression.PropertyOrField(expr, prop);
}
var sortExpression = Expression.Lambda<Func<T, object>>
(Expression.Convert(Expression.Property(sortParam, sortColumn), typeof(object)), sortParam);
return query.OrderBy(sortExpression);
}
return null;
}
Any idea where I am doing it wrong ?
Try:
private static readonly MethodInfo OrderBy = (from x in typeof(Queryable).GetMethods()
where x.Name == "OrderBy"
let pars = x.GetParameters()
where pars.Length == 2
select x).Single();
private static IQueryable<T> SortQuery<T>(IQueryable<T> query, string sortColumn)
{
if (!string.IsNullOrEmpty(sortColumn))
{
var sortParam = Expression.Parameter(typeof(T), "x");
Expression expr = sortParam;
foreach (var prop in sortColumn.Split('.'))
{
expr = Expression.PropertyOrField(expr, prop);
}
var lambda = Expression.Lambda(expr, sortParam);
var orderBy = OrderBy.MakeGenericMethod(typeof(T), expr.Type);
return (IQueryable<T>)orderBy.Invoke(null, new object[] { query, lambda });
}
return null;
}
You have to use reflection to invoke the Queryable.OrderBy. In general, casting to object doesn't work very well with Entity Framework/LINQ to SQL, so you can try it, but there is only a very little chance it will work.