I'm having a student model in which i have list of phone numbers and addresses.When i update the student the related data(phone and address) needs to be updated. I have written a PUT action in my student controller for that. It works fine, but I'm concerned about the efficiency of the query. Please check the code and suggest me improvisation if any. Thanks
public async Task<IActionResult> Put(long id, [FromBody] Student student)
{
var p = await _Context.Students
.Include(t => t.PhoneNumbers)
.Include(t => t.Addresses)
.SingleOrDefaultAsync(t => t.Id == id);
if (p == null)
{
return NotFound();
}
_Context.Entry(p).CurrentValues.SetValues(student);
#region PhoneNumber
var existingPhoneNumbers = p.PhoneNumbers.ToList();
foreach (var existingPhone in existingPhoneNumbers)
{
var phoneNumber = student.PhoneNumbers.SingleOrDefault(i => i.Id == existingPhone.Id);
if (phoneNumber != null)
_Context.Entry(existingPhone).CurrentValues.SetValues(phoneNumber);
else
_Context.Remove(existingPhone);
}
// add the new items
foreach (var phoneNumber in student.PhoneNumbers)
{
if (existingPhoneNumbers.All(i => i.Id != phoneNumber.Id))
{
p.PhoneNumbers.Add(phoneNumber);
}
}
#endregion
#region Address
var existingAddresses = p.Addresses.ToList();
foreach (var existingAddress in existingAddresses)
{
var address = student.Addresses.SingleOrDefault(i => i.Id == existingAddress.Id);
if (address != null)
_Context.Entry(existingAddress).CurrentValues.SetValues(address);
else
_Context.Remove(existingAddress);
}
// add the new items
foreach (var address in student.Addresses)
{
if (existingAddresses.All(i => i.Id != address.Id))
{
p.Addresses.Add(address);
}
}
#endregion
await _Context.SaveChangesAsync();
return NoContent();
}
Searching over small in-memory collections is not normally something you would worry about. So if a Student has dozens or hundreds of addresses, the repeated lookups are not going to be significant, especially compared with the time required to write to the database.
If you did want to optimize, you can copy the students Addresses to a Dictionary. Like this:
var existingAddresses = p.Addresses.ToList();
var studentAddresses = student.Addresses.ToDictionary(i => i.Id);
foreach (var existingAddress in existingAddresses)
{
if (studentAddresses.TryGetValue(existingAddress.Id, out Address address))
{
_Context.Entry(existingAddress).CurrentValues.SetValues(address);
}
else
{
_Context.Remove(existingAddress);
}
}
A query over an in-memory collection like this:
var address = student.Addresses.SingleOrDefault(i => i.Id == existingAddress.Id);
Will simply iterate all the student.Addresses comparint the Ids. A Dictionary<> acts like an index, providing very fast lookups.
Related
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();
}
}
}
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);
}
I map to a data transformation object when retrieving items from an ASP.NET Web API like so for a list:
public async Task<IList<PromotionDTO>> GetPromotionsList()
{
return await _context.Promotions
.Select(p => new PromotionDTO
{
PromotionId = p.PromotionId,
Is_Active = p.Is_Active,
Created = p.Created,
Title = p.Title,
BusinessName = p.BusinessName,
})
.Where(x => x.Is_Active)
.OrderByDescending(x => x.Created)
.ToListAsync();
}
And like this for getting a single record:
public async Task<PromotionDTO> GetPromotion(int id)
{
return await _context.Promotions
.Select(p => new PromotionDTO
{
PromotionId = p.PromotionId,
Is_Active = p.Is_Active,
Created = p.Created,
Title = p.Title,
BusinessName = p.BusinessName,
})
.Where(x => x.Is_Active && x.PromotionId == id)
.FirstOrDefaultAsync();
}
I'm new to DTO's and I find that I'm using the same DTO transformation code at many places, and was wondering how I can simplify my code to only do this once?
Though it may be enough to map like you've stated, but when your project starts to grow it will just complicated things and cause additional work.
I suggest that you use some kind of mapping library like AutoMapper.
https://github.com/AutoMapper/AutoMapper
static MyRepositoryConstructor()
{
// Define your maps
Mapper.Initialize(cfg => {
cfg.CreateMap<PromotionEntity, PromotionDTO>();
});
}
public async Task<IList<PromotionDTO>> GetPromotionsList()
{
return Mapper.Map<IList<PromotionDTO>>(await _context.Promotions
.Where(x => x.Is_Active)
.OrderByDescending(x => x.Created)
.ToListAsync()
);
}
public async Task<PromotionDTO> GetPromotion(int id)
{
return Mapper.Map<PromotionDTO>(await _context.Promotions
.Where(x => x.Is_Active && x.PromotionId == id)
.FirstOrDefaultAsync()
);
}
One option is to create a method which returns an IQueryable and then use that in each
Private IQueryable<PromotionDTO> Query()
{
return _context.Promotions
.Select(p => new PromotionDTO
{
PromotionId = p.PromotionId,
Is_Active = p.Is_Active,
Created = p.Created,
Title = p.Title,
BusinessName = p.BusinessName,
});
}
public async Task<IList<PromotionDTO>> GetPromotionsList()
{
return await Query()
.Where(x => x.Is_Active)
.OrderByDescending(x => x.Created)
.ToListAsync();
}
public async Task<PromotionDTO> GetPromotion(int id)
{
return await Query()
.Where(x => x.Is_Active && x.PromotionId == id)
.FirstOrDefaultAsync();
}
I at this moment I have repository filled with multiple gets methods.
E.q. Get1() => cxt.Entites.Include(e => e.obj1);
Get2() => cxt.Entities.Include(e => e.obj1).Include(e => e.obj2)
And so on.
Is there good method, pattern to have one GET method where I can send inclues via parameter?
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
string includeProperties = "")
{
IQueryable<TEntity> query = dbSet;
if (filter != null)
{
query = query.Where(filter);
}
foreach (var includeProperty in includeProperties.Split
(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
{
query = query.Include(includeProperty);
}
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
}
See repository pattern in msdn
You can use
_sampleRepostiory.Get(h=>h.Id>1,null,"Employees.Departments");
Including same with lambda
public virtual IEnumerable<TEntity> Get(
Expression<Func<TEntity, bool>> filter = null,
Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
Expression<Func<TEntity, object>>[] includes)
{
IQueryable<TEntity> query = dbSet;
if (filter != null)
{
query = query.Where(filter);
}
if (includes != null)
{
query = includes.Aggregate(query,
(current, include) => current.Include(include));
}
if (orderBy != null)
{
return orderBy(query).ToList();
}
else
{
return query.ToList();
}
}
Consume it like this
var query = context.Customers
.Get(x=>x.Id>1,null,
c => c.Address,
c => c.Orders.Select(o => o.OrderItems));
Similar SO question
I did the following in my projects:
public Entity[] GetAll(bool includeObj1, bool includeAllOthers) {
IQueryable<Entity> entity = ctx.Entities;
if (includeObj1)
entity = entity.Include(e => e.obj1);
if (includeAllOthers) {
entity = entity
.Include(e => e.obj2)
.Include(e => e.obj3)
.Include(e => e.obj4)
.Include(e => e.obj5);
}
return entity.ToArray();
}
Providing arguments like includeObj1 and includeObj2 separates a consumer of repository from implementation and encapsulates any data access logic.
Passing direct "include these properties" orders to a repository means that you know how repository works and assume that it is some sort ORM which blurs abstractions.
I have a Apps, AppRoles, UserAppRoles and Users. I'm trying to Get All Users but only want the AppRoles where AppId = 1. How do I filter the child collection?
using (var context = new dbContext())
{
var rv = context.Users
.Include(u => u.AppRoles);
}
I tried this but throws and exception:
The Include path expression must refer to a navigation property defined on the type. Use dotted paths for reference navigation properties and the Select operator for collection navigation properties
public static async Task<List<User>> GetAllAsync()
{
var rv = new List<User>();
using (var context = new dbContext())
{
rv = await (context.Users.AsNoTracking()
.Include(a => a.AppRoles.Where(a2 => a2.AppId == 1)).ToListAsync());
}
return rv;
}
The only way I could figure out how to get it to work is like this which I might as well just use a stored procedure at that point:
var rv = new List<User>();
using (var context = new dbContext())
{
rv = context.Users.AsNoTracking()
.Include(a => a.AppRoles).ToList();
}
foreach (var user in rv)
{
if (user.AppRoles.Any())
{
user.AppRoles = user.AppRoles.Where(r2 => r2.AppId == 1).ToList();
}
}
How do I write this in EF?
SELECT
Users.UserId,
Users.UserName
FROM
Users
INNER JOIN UserAppRoles ON Users.UserId = UserAppRoles.UserId
INNER JOIN AppRoles ON UserAppRoles.AppRoleId = AppRoles.AppRoleId
WHERE AppRoles.AppId = 1
Try this:
context.Entry(user)
.Collection(b => b.AppRoles)
.Query()
.Where(r => r.AppId==1)
.Load();
where user is an AppUser entity from the context (like in your foreach example).
More info here:
https://msdn.microsoft.com/en-us/library/jj574232(v=vs.113).aspx ("Applying filters when explicitly loading related entities" section)
You might subquery the AppRoles
var arQuery = from ar in context.AppRoles
where ar.AppId == 1
select ar;
var query = from u in context.Users
join uar in context.UserAppRoles on u.UserId equals uar.UserId
join ar in arQuery on uar.AppRoleId equals ar.AppRoleId
select u;
I have this working like this but still seems inefficient but guess it's better than round trips to database. Isn't this a common pattern? I can't believe a simple inner join isn't included in EF but I'm mapping to DTOs anyways so guess this will work for now until I look into a few open source Include filters that support filtering.
var rv = context.Users.Include(r => r.AppRoles).ToList().Select(u => new User()
{
UserId = u.UserId,
AppRoles = u.AppRoles.Where(x=>x.AppId == 1).ToList()
});
Disclaimer: I'm the owner of the project Entity Framework Plus
The EF+ Query IncludeFilter allow easily filter included entities.
public static async Task<List<User>> GetAllAsync()
{
var rv = new List<User>();
using (var context = new dbContext())
{
rv = await (context.Users.AsNoTracking()
.IncludeFilter(a => a.AppRoles.Where(a2 => a2.AppId == 1)).ToListAsync());
}
return rv;
}
Wiki: EF+ Query Include Filter