EF change detection does not seem to recognize my updates on an entity - c#

I am trying to create an AddOrUpdate method. Unfortunately, it seems that the change detection is not kicking in. I just don't see what I am doing wrong here.
Consider the following model:
public class Order
{
public long Id { get; set; }
public string OrderId { get; set; }
// ...
public OrderDetails OrderDetails { get; set; }
}
public class OrderDetails
{
public long Id { get; set; }
// ...
public Order Order { get; set; }
}
This entities should have a one-to-one relationship to each other. An order can't exist without OrderDetails and the other way round.
So this is what my EntityTypeConfigurations looks like:
public class OrderEntityTypeConfiguration : EntityTypeConfiguration<Order>
{
public OrderEntityTypeConfiguration()
{
this.HasKey(e => e.Id);
this.HasRequired(e => e.OrderDetails)
.WithRequiredPrincipal(e => e.Order);
this.ToTable("Orders");
}
}
public class OrderDetailEntityTypeConfiguration : EntityTypeConfiguration<OrderDetails>
{
public OrderDetailEntityTypeConfiguration()
{
this.HasKey(e => e.Id);
this.ToTable("OrderDetails");
}
}
Now I've got a repository called OrderRepository, with a method called AddOrUpdate().
public class OrderRepository
{
public OrderRepository(OrderDbContext context)
{
// ...
}
public bool Contains(Order order)
{
var result = this.context.OrderSet.SingleOrDefault(e => e.OrderId == order.OrderId);
return result != null;
}
public Order GetOrderByOrderId(string orderId)
{
return this.context.OrderSet
.Include(e => e.OrderDetails)
.SingleOrDefault(e => e.OrderId == orderId);
}
public void AddOrUpdate(Order order)
{
if (this.Contains(order))
{
var result = this.GetOrderByOrderId(order.OrderId);
result = order;
}
else
{
this.context.OrderSet.Add(order);
}
this.context.SaveChanges();
}
}
However in the case where this.Contains(order) evaluates to true the assignment from order to result does not get detected from the ChangeDetection mechanism. The call to SaveChanges() returns 0. Why is that?
The following approach seems to work, but feels kind of hacky.
public void AddOrUpdate(Order order)
{
if (this.Contains(order))
{
var result = this.GetOrderByOrderId(order.OrderId);
order.Id = result.Id;
order.OrderDetails.Id = result.OrderDetails.Id;
this.context.Entry(result).CurrentValues.SetValues(order);
this.context.Entry(result.OrderDetails).CurrentValues.SetValues(order.OrderDetails);
}
else
{
this.context.OrderSet.Add(order);
}
this.context.SaveChanges();
}

Simple assignment result = order will not work, cause EF is tracking(if chage tracking is enabled) object fetched via this.GetOrderByOrderId(order.OrderId) call and stored in result variable and not the order one. So you will need either copy needed fields from order to result somehow or play with Attach(usually would not recommend though).

Related

MongoDB C# Bulk partial update

I have the following code (does not compile currently)
public async Task<IResult<bool>> UpdateStock(List<Product> products)
{
var bulkOps = products.Select(record => new UpdateOneModel<Product>(
Builders<Product>.Filter.Where(x => x.ProductCode == record.ProductCode),
Builders<Product>.Update.Set(x => x.StockPointStocks.ElementAt(-1), record.StockPointStocks)
)
{
IsUpsert = true
})
.Cast<WriteModel<Product>>()
.ToList();
await _databaseContext.ProductCollection().BulkWriteAsync(bulkOps);
return await Result<bool>.SuccessAsync();
}
What i'm trying to do from a given list of products bulk update the StockPointStocks and insert where we don't currently have the product in the DB, however i'm unsure how to finish off the second part of the Update.Set or if this is even the correct way of doing this.
public class Product
{
public Product()
{
Id = ObjectId.GenerateNewId();
}
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public ObjectId Id { get; set; }
public string ProductCode { get; set; }
public List<StockPointStock> StockPointStocks { get; set; }
public List<FutureStock> FutureStock { get; set; }
}
public class StockPointStock
{
public string Stockpoint { get; set; }
public int Stock { get; set; }
public DateTime LastUpdated { get; set; }
}
In order to replace the StockPointStocks array with the value provided to the parameter, you only need to perform a Set on the list property:
public async Task<IResult<bool>> UpdateStock(List<Product> products)
{
var bulkOps = products.Select(record => new UpdateOneModel<Product>(
Builders<Product>.Filter.Where(x => x.ProductCode == record.ProductCode),
Builders<Product>.Update.Set(x => x.StockPointStocks, record.StockPointStocks)
)
{
IsUpsert = true
})
.ToList();
await _databaseContext.ProductCollection().BulkWriteAsync(bulkOps);
return await Result<bool>.SuccessAsync();
}
This replaces the property with the new value and leaves all other properties unchanged.
For an upsert, ProductCode and StockPointStocks are set automatically, but you will also need to set the remaining properties for the newly created Product document. To do so, you can use SetOnInsert, e.g.:
public async Task<IResult<bool>> UpdateStock(List<Product> products)
{
var bulkOps = products.Select(record => new UpdateOneModel<Product>(
Builders<Product>.Filter.Where(x => x.ProductCode == record.ProductCode),
Builders<Product>.Update
.Set(x => x.StockPointStocks, record.StockPointStocks)
.SetOnInsert(x => x.FutureStock, record.FutureStock)
)
{
IsUpsert = true
})
.ToList();
await _databaseContext.ProductCollection().BulkWriteAsync(bulkOps);
return await Result<bool>.SuccessAsync();
}
SetOnInsert changes the properties only if an insert occurs for an upsert operation.
Please note that you can also omit the Cast<WriteModel<Product>>() call.

Check value of same column in all tables [Entity Framework]

I have more than 100 tables in data base in which 60+ table's contain column called ShortCode nvarchar(12) which represent globally unique code of that record.
Now is there any way to find that the ShortCode value eg. AST_SHIP_FIRE present in any of the table in database.
Note:ShortCode is user define.
currently I am try below code,it works but I have to code for all table.
if (entities.Table1.Any(x => x.ShortCode.Trim().ToLower() == a.ShortCode.Trim().ToLower())
{return false;}
else if(entities.Table2.Any(x => x.ShortCode.Trim().ToLower() == a.ShortCode.Trim().ToLower()))
{return false;}
else if( entities.Talble3.Any(x => x.ShortCode.Trim().ToLower() == a.ShortCode.Trim().ToLower()))
{return false;}
.
.
.
else
{
//insert code
}
I think there may be more efficient way.
Ok, maybe not very straightforward but lets do it!
First of all define an interface for ShortCode property and implement it by any entity that has it:
public interface ITableWithShortCode
{
public string ShortCode { get; set; }
}
public class Table1 : ITableWithShortCode
{
public long Id { get; set; }
public string ShortCode { get; set; }
}
public class Table2 : ITableWithShortCode
{
public long Id { get; set; }
public string ShortCode { get; set; }
}
Now using power of Reflection you can write a method like this:
public bool IsExistShortCode(string shortCode)
{
using (var context = new AppDbContext())
{
/*
find all tables that are defined in your DbContext and are implemented ITableWithShortCode like:
public DbSet<Table1> Table1 { get; set; }
public DbSet<Table2> Table2 { get; set; }
...
*/
var properties = typeof(AppDbContext).GetProperties()
.Where(p => p.PropertyType.IsGenericType
&& typeof(ITableWithShortCode).IsAssignableFrom(p.PropertyType.GenericTypeArguments[0]));
foreach (var property in properties)
{
var contextProp = (IQueryable<ITableWithShortCode>)property.GetValue(context);
bool isExist = contextProp.Any(p => p.ShortCode == shortCode);
if (isExist)
return true;
}
return false;
}
}
Note: You can do some optimization on this code, I prefered to keep it in its simplest state to show the idea. But in production, for example you can easily cache DbContext properties on startup and use it afterward

How to use function in where with ToListAsync()

I wrote a where function to check if product have some functionality and find a product with max price.
My simply models look like this:
public class Product
{
public long ProductID { get; set; }
[MaxLength(150)]
public string Name { get; set; }
public List<Functionality> Functionalities { get; set; }
public List<Price> Prices { get; set; }
}
public class Functionality
{
public long FunctionalityID { get; set; }
[MaxLength(150)]
public string Name { get; set; }
[Required]
public long ProductID { get; set; }
public Product Product { get; set; }
}
public class Price
{
public long PriceID { get; set; }
public decimal Value { get; set; }
[Required]
public long ProductID { get; set; }
public Product Product { get; set; }
}
then my sync function to find correct product look like this:
public List<Product> GetList(ProductFiltersDto filters)
{
return _context.Product
.Include(x => x.Functionality)
.Include(x => x.Price)
.Where(x =>
CheckCollectionFilter(x.Functionality.Select(f => f.FunctionalityID), filters.Functionalities) &&
CheckMaximumPrice(x.Prices , filters.MaxPrice)
)
.ToList();
}
below my where function:
private bool CheckCollectionFilter<T>(IEnumerable<T> collection, List<T> filterCollection)
{
if (filterCollection != null)
{
var result = true;
foreach (var filterValue in filterCollection)
{
if (!collection.Contains(filterValue))
{
result = false;
break;
}
}
return result;
}
else
{
return true;
}
}
private bool CheckMaximumPrice(List<Price> prices, decimal? avalibleMinPrice)
{
return avalibleMinPrice.HasValue && prices.Count > 0 ? prices.Min(x => x.Value) <= avalibleMinPrice : true;
}
For above code everything work fine. But when i change ToList() to ToListAsync() i got a error
Expression of type 'System.Collections.Generic.IAsyncEnumerable1[System.Int64]'
cannot be used for parameter of type 'System.Collections.Generic.IEnumerable1[System.Int64]'
of method 'Boolean CheckCollectionFilter[Int64](System.Collections.Generic.IEnumerable1[System.Int64],
System.Collections.Generic.List1[System.Int64])'
Parameter name: arg0
I try few things to change IEnumerable to IAsyncEnumerable and modify my function to work with asyn version but stil i get error (i dont find a way to change List<long> to IAsyncEnumerable<long> in where clause).
I read that EF Core Async functions have some limitations but maybe someone know is this can be achiev or for now i should leave this and stick with sync solution?
As soon as you move from IQueriable<T> to IEnumerable<T>, EntityFramework will execute the query so far on the database server pulling all that data into memory and will execute the rest of the query in memory.
If you want to keep it running in the database server, you must keep the query an IQueriable<T> and use expression trees instead of executable code.
You will need to come up with something like this:
private Expression<Func<T, bool>> CheckCollectionFilter<T>(IEnumerable<T> filterCollection);
private Expression<Func<T, bool>> CheckMaximumPrice<T>(decimal? avalibleMinPrice);
and change your query to:
_context.Product
.Include(x => x.Functionality)
.Include(x => x.Price)
.Where(CheckCollectionFilter(filters.Functionalities))
.Where(CheckMaximumPrice(filters.MaxPrice))

LINQ Filter Nth Level Nested list

I have the following hierarchy in my project :
Activity
Task
Step
Responses
This means an activity has many tasks, which in turn has many steps , a step has many responses.
Here are my POCO classes:
public class Activity
{
public virtual ICollection<Task> Tasks { get; set; }
}
public class Task
{
public virtual ICollection<Step> Steps{ get; set; }
}
public class Step
{
public virtual int DisplayOrder { get; set; }
public virtual ICollection<Response> Responses{ get; set; }
}
public class Response
{
public virtual int UserId { get; set; }
public virtual int ResponseText{ get; set; }
}
Now, I need to return a List<Activity> which is sorted by ActivityId and has Steps ordered by DisplayOrder and also a Responses which only belong to a given UserId.
Here's what I have tried:
Activities.ToList()
.ForEach((activity) => activity.Tasks.ToList()
.ForEach((task) => task.Steps = task.Steps.OrderBy(s => s.DisplayOrder).ToList()
.ForEach((step) => step.Responses = step.Responses.Where(r => r.UserId == RequestorUserId)))
));
Which is giving me an error:
Cannot implicitly convert type 'void' to ICollection<Step>
The ForEach extension method doesn't return anything and you are trying to set the result to task.Steps. You can achieve the desired result by using the Select method to alter the objects in line
Activities.ToList()
.ForEach((activity) => activity.Tasks.ToList()
.ForEach((task) => task.Steps = task.Steps.OrderBy(s => s.DisplayOrder).ToList()
.Select((step) =>
{
step.Responses = step.Responses.Where(r => r.UserId == 1).ToList();
return step;
}).ToList()));
Also It might be worth changing your types if possible to IEnumerable rather than ICollection as you then won't need all of the ToList() calls.
If you need to assign the result then you'll need to replace the first ForEach as well - try something like:
var a = Activities.ToList()
.Select((activity) =>
{
activity.Tasks.ToList()
.ForEach((task) => task.Steps = task.Steps.OrderBy(s => s.DisplayOrder).ToList()
.Select((step) =>
{
step.Responses = step.Responses.Where(r => r.UserId == 1).ToList();
return step;
}).ToList());
return activity;
});

query multi-level entity with filter at the lowest level

So I have 3 entity classes:
public partial class Event
{
public Event()
{
Recurrences = new HashSet<Recurrence>();
}
public int Id { get; set; }
public ICollection<Recurrence> Recurrences { get; set; }
}
public partial class Recurrence
{
public Recurrence()
{
AspNetUsers = new HashSet<AspNetUser>();
}
public int Id { get; set; }
public int EventId { get; set; }
public ICollection<AspNetUser> AspNetUsers { get; set; }
}
public partial class AspNetUser
{
public AspNetUser()
{
Recurrences = new HashSet<Recurrence>();
}
public string Id { get; set; }
public string UserName { get; set; }
public ICollection<Recurrence> Recurrences { get; set; }
}
I would like to get the event given the aspnetuser.id using line to entity. so far this is what I have but it's returning an error:
// GET: api/Events?userId={userId}
public IQueryable<Event> GetEvents(string userId)
{
return db.Events
.Include(e => e.Recurrences
.Select(u => u.AspNetUsers.Where(i => i.Id == userId)));
}
When I exclude the where clause it works fine. Please help.
Thanks in advance!
I don't think Include() means what you think it means. (https://msdn.microsoft.com/en-us/library/bb738708%28v=vs.110%29.aspx) What it does is tell the db set to be sure to bring in relationships for that object. By default (last I checked), the db context will auto pull in all relationships, so this isn't necessary. However, if you've turned off the lazy-loading (http://www.entityframeworktutorial.net/EntityFramework4.3/lazy-loading-with-dbcontext.aspx) then you'll need to .Include() all the relationships you want to have in the query.
This should solve your problem. I don't guarantee the SQL generated won't be silly, though.
If you have lazy-loading turned on:
db.Events.Include("Recurrences").Include("Recurrences.AspNetUsers")
.Where(e => e.Recurrences
.Any(r => r.AspNetUsers
.Any(u => u.Id ==userId)));
If you have lazy-loading turned off:
db.Events
.Where(e => e.Recurrences
.Any(r => r.AspNetUsers
.Any(u => u.Id ==userId)));
Also, if you have trouble seeing errors, you can .ToList() the query before returning so that it fails in your code and not deep inside the Web API stack. Personally, I like to do this so that I can try/catch the query and handle it properly.

Categories