EF - Updating records with children - c#

I've been working on this problem for a week now and I'm getting so frustrated with EF. First off I have a super table -> sub table pattern going on in the database. It was designed with a code-first approach. The super type is called the WorkflowTask and is defined as follows:
<!-- language: c# -->
public abstract class WorkflowTask
{
public int WorkflowTaskId { get; set; }
public int Order { get; set; }
public WorkflowTaskType WorkflowTaskType { get; set; }
public WorkFlowTaskState State { get; set; }
public ParentTask ParentTask { get; set; }
public WorkflowDefinition WorkflowDefinition { get; set; }
}
An example sub task would inherit from this task and provide additional properties:
<!-- language: c# -->
public class DelayTask : WorkflowTask
{
public int Duration { get; set; }
}
This is mapped to the database as follows:
<!-- language: c# -->
public class WorkflowTaskEntityConfiguration : EntityTypeConfiguration<WorkflowTask>
{
public WorkflowTaskEntityConfiguration()
{
HasKey(w => w.WorkflowTaskId);
Property(w => w.WorkflowTaskId).HasColumnName("Id");
Property(w => w.Order).HasColumnName("Order");
Property(w => w.WorkflowTaskType).HasColumnName("TaskTypeId");
Property(w => w.State).HasColumnName("TaskStateId");
HasOptional(c => c.ParentTask).WithMany()
.Map(c => c.MapKey("ParentTaskId"));
}
}
The delay task is mapped as follows:
<!-- language: c# -->
public class DelayTaskEntityConfiguration : EntityTypeConfiguration<DelayTask>
{
public DelayTaskEntityConfiguration()
{
Property(d => d.WorkflowTaskId).HasColumnName("DelayTaskId");
Property(d => d.Duration).HasColumnName("Duration");
}
}
Hopefully you get the idea. Now I have another sub type called a container task. This task will hold other tasks and can potentially hold other container tasks. Here is what it looks like as well as the mapping:
<!-- language: c# -->
public class ContainerTask : ParentTask
{
public ContainerTask()
{
base.WorkflowTaskType = WorkflowTaskType.Container;
base.ParentTaskType = ParentTaskType.ContainerTask;
}
public List<WorkflowTask> ChildTasks { get; set; }
}
public class ContainerTaskEntityConfiguration : EntityTypeConfiguration<ContainerTask>
{
public ContainerTaskEntityConfiguration()
{
Property(x => x.WorkflowTaskId).HasColumnName("ContainerTaskId");
HasMany(c => c.ChildTasks).WithMany()
.Map(c => c.ToTable("ContainerTaskChildren", WorkflowContext.SCHEMA_NAME)
.MapLeftKey("ContainerTaskId")
.MapRightKey("ChildTaskId"));
}
}
And to make sure I include everything; here is the ParentTask object as well as it's mapping:
<!-- language: c# -->
public abstract class ParentTask : WorkflowTask
{
public ParentTaskType ParentTaskType {get; set;}
}
public class ParentTaskEntityConfiguration : EntityTypeConfiguration<ParentTask>
{
public ParentTaskEntityConfiguration()
{
Property(w => w.WorkflowTaskId).HasColumnName("ParentTaskId");
Property(w => w.ParentTaskType).HasColumnName("ParentTaskTypeId");
}
}
Now the item I'm trying to save is the WorkflowDefinition object. It will execute a bunch of tasks in order. It is defined as follows:
<!-- language: c# -->
public class WorkflowDefinition
{
public int WorkflowDefinitionId { get; set; }
public string WorkflowName { get; set; }
public bool Enabled { get; set; }
public List<WorkflowTask> WorkflowTasks { get; set; }
}
public class WorkflowDefinitionEntityConfiguration :
EntityTypeConfiguration<WorkflowDefinition>
{
public WorkflowDefinitionEntityConfiguration()
{
Property(w => w.WorkflowDefinitionId).HasColumnName("Id");
HasMany(w => w.WorkflowTasks)
.WithRequired(t=> t.WorkflowDefinition)
.Map(c => c.MapKey("WorkflowDefinitionId"));
Property(w => w.Enabled).HasColumnName("Enabled");
Property(w => w.WorkflowName).HasColumnName("WorkflowName");
}
}
So with all that defined I am passing a WorkflowDefinition object into my data repository layer and want to save it using EF. Since the object has lost it's context while working on it in the UI; I have to re-associate it so that it knows what to save. Within the UI I can add new tasks to the workflow, edit existing tasks as well as delete tasks. If there was only one level of tasks (definition => tasks) this would be cake. My problem lies with there being the possibility of infinite levels (definition => tasks => childtasks => childtasks, etc...).
Currently I retrieve the existing workflow from the database and assign the values over (workflow is the value being passed in and is of type WorkflowDefinition):
<!-- language: c# -->
// retrieve the workflow definition from the database so that it's within our context
var dbWorkflow = context.WorkflowDefinitions
.Where(w => w.WorkflowDefinitionId ==workflow.WorkflowDefinitionId)
.Include(c => c.WorkflowTasks).Single();
// transfer the values of the definition to the one we retrieved.
context.Entry(dbWorkflow).CurrentValues.SetValues(workflow);
I then loop through the list of tasks and either add them to the definition or find them and set their values. I added a function to the WorkflowTask object called SetDefinition which sets the WorkflowDefinition to the workflow within the context (previously I would get a key error because it thought the parent workflow was a different one even though the Ids matched). If it`s a container I run a recursive function to try and add all the children to the context.
<!-- language: c# -->
foreach (var task in workflow.WorkflowTasks)
{
task.SetDefinition(dbWorkflow);
if (task.WorkflowTaskId == 0)
{
dbWorkflow.WorkflowTasks.Add(task);
}
else
{
WorkflowTask original = null;
if (task is ContainerTask)
{
original = context.ContainerTasks.Include("ChildTasks")
.Where(w => w.WorkflowTaskId == task.WorkflowTaskId)
.FirstOrDefault();
var container = task as ContainerTask;
var originalContainer = original as ContainerTask;
AddChildTasks(container, dbWorkflow, context, originalContainer);
}
else
{
original = dbWorkflow.WorkflowTasks.Find(t => t.WorkflowTaskId ==
task.WorkflowTaskId);
}
context.Entry(original).CurrentValues.SetValues(task);
}
}
The AddChildTasks function looks like this:
<!-- language: c# -->
private void AddChildTasks(ContainerTask container, WorkflowDefinition workflow,
WorkflowContext context, ContainerTask original)
{
if (container.ChildTasks == null) return;
foreach (var task in container.ChildTasks)
{
if (task is ContainerTask)
{
var subContainer = task as ContainerTask;
AddChildTasks(subContainer, workflow, context, container);
}
if (task.WorkflowTaskId == 0)
{
if (container.ChildTasks == null)
container.ChildTasks = new List<WorkflowTask>();
original.ChildTasks.Add(task);
}
else
{
var originalChild = original.ChildTasks
.Find(t => t.WorkflowTaskId == task.WorkflowTaskId);
context.Entry(originalChild).CurrentValues.SetValues(task);
}
}
}
To delete tasks Ive found Ive had to do a two step process. Step 1 involves going through the original definition and marking tasks that are no longer in the passed in definition for deletion. Step 2 is simply setting the state for those tasks as deleted.
<!-- language: c# -->
var deletedTasks = new List<WorkflowTask>();
foreach (var task in dbWorkflow.WorkflowTasks)
{
if (workflow.WorkflowTasks.Where(t => t.WorkflowTaskId ==
task.WorkflowTaskId).FirstOrDefault() == null)
deletedTasks.Add(task);
}
foreach (var task in deletedTasks)
context.Entry(task).State = EntityState.Deleted;
Here is where I run into problems. If I delete a container I get a constraint error because the container contains children. The UI holds all changes in memory until I hit save so even if I deleted the children first it still throws the constraint error. I`m thinking I need to map the children differently, maybe with a cascade delete or something. Also, when I loop through the tasks in the delete loop, both the container and child get flagged for deletion when I only expect the container to be flagged and the child to be deleted as a result.
Finally, the save portion above took me a good week to figure out and it looks complicated as hell. Is there an easier way to do this? I'm pretty new to EF and I'm starting to think it would be easier to have the code generate SQL statements and run those in the order I want.
This is my first question here so I apologize for the length as well as the formatting... hopefully it's legible :-)

One suggestion, and I have not used it in the situation where there is this endless possible recursion, but if you want the cascade on delete to work natively and it looks like all tasks belong directly owned by some parent task you could approach it by defining an Identifying Relationship:
modelBuilder.Entity<WorkFlowTask>().HasKey(c => new {c.WorkflowTaskID,
c.ParentTask.WofkflowTaskId});
This question is relevant: Can EF automatically delete data that is orphaned, where the parent is not deleted?
Edit: And this link: https://stackoverflow.com/a/4925040/1803682

Ok, it took me way longer than I had hoped but I think I finally figured it out. At least all my tests are passing and all the definitions I've been saving have been working so far. There might be some combinations that I haven't thought about but for now I can at least move on to something else.
First off, I switched passing in my object as a tree and decided to flatten it out. This just means that all tasks are visible from the root but I still need to set parent properties as well as child properties.
foreach (var task in workflow.WorkflowTasks)
{
taskIds.Add(task.WorkflowTaskId); //adding the ids of all tasks to use later
task.SetDefinition(dbWorkflow); //sets the definition to record in context
SetParent(context, task); //Attempt to set the parent for any task
if (task.WorkflowTaskId == 0)
{
// I found if I added a task as a child it would duplicate if I added it
// here as well so I only add tasks with no parents
if (task.ParentTask == null)
dbWorkflow.WorkflowTasks.Add(task);
}
else
{
var dbTask = dbWorkflow.WorkflowTasks.Find(t => t.WorkflowTaskId == task.WorkflowTaskId);
context.Entry(dbTask).CurrentValues.SetValues(task);
}
}
The SetParent function has to check if a task has a parent and make sure the parent isn't a new task (id == 0). It then tries to find the parent in the context version of the definition so that I don't end up with duplicates (ie - if the parent is not referenced it tries to add a new one even though it exists in the database). Once the parent is identified I check it's children to see if that task is already there, if not I add it.
private void SetParent(WorkflowContext context, WorkflowTask task)
{
if (task.ParentTask != null && task.ParentTask.WorkflowTaskId != 0)
{
var parentTask = context.WorkflowTasks.Where(t => t.WorkflowTaskId == task.ParentTask.WorkflowTaskId).FirstOrDefault();
var parent = parentTask as ParentTask;
task.ParentTask = parent;
if (parentTask is ContainerTask)
{
var container = context.ContainerTasks.Where(c => c.WorkflowTaskId == parentTask.WorkflowTaskId).Include(c => c.ChildTasks).FirstOrDefault() as ContainerTask;
if (container.ChildTasks == null)
container.ChildTasks = new List<WorkflowTask>();
var childTask = container.ChildTasks.Find(t => t.WorkflowTaskId == task.WorkflowTaskId
&& t.Order == task.Order);
if(childTask == null)
container.ChildTasks.Add(task);
}
}
}
One thing you will notice in the SetParent code is that I'm searching for a task by the ID and the Order. I had to do this because if I added two new children to a container both of the Ids would be zero and the second one wouldn't get added since it found the first one. Each task has a unique order so I used that to further differentiate them.
I don't feel super great about this code but I've been working on this problem for so long and this works so I'm going to leave it for now. I hope I covered all the information, I'm not too sure how many people will actually need this but you never know.

Related

I need to erase collection from Entity Framework

I can add, but not erase any item with the collection - unable to delete.
Found a few partial solutions, but nothing to guide me to a working solution. I can easily add values to the collection; ny help is appreciated.
I have the following:
[HttpPut("updateSOJ4")]
public IActionResult UpdateSOJ4([FromBody] Routing_Tool_SOJ4 Routing_Tool_SOJ4)
{
Routing_Tool_SOJ4 request = new Routing_Tool_SOJ4();
request.Id = Routing_Tool_SOJ4.Id;
request.Routing_Tool_Services = Routing_Tool_SOJ4.Routing_Tool_Services;
request.Routing_ToolId = Routing_Tool_SOJ4.Routing_ToolId;
_repository.UpdateSOJ4(request);
return Ok(request);
}
Here is where I was trying the different solutions, but, I am still stuck:
public void UpdateSOJ4(object routing_Tool_SOJ4)
{
// var missingItem = _context.Routing_Tool_Service.Where(i => i.Routing_Tool_SOJ4Id == _context.Routing_Tool_SOJ4.Id).First(); -- DOES NOT WORK
_context.Update(routing_Tool_SOJ4).State = EntityState.Modified;
_context.SaveChanges();
}
Here is the database structure:
public class Routing_Tool_SOJ4
{
[Key]
[Required]
public int Id { get; set; }
public int Routing_ToolId { get; set; }
[ForeignKey("Routing_ToolId")]
public virtual Routing_Tool Routing_Tool { get; set; }
public virtual ICollection <Routing_Tool_Service> Routing_Tool_Services { get; set; }
}
Collection:
public class Routing_Tool_Service
{
[Key]
[Required]
public int Id { get; set; }
public string ServiceName { get; set; }
[Required]
[ForeignKey("Routing_Tool_SOJ4Id")]
public int Routing_Tool_SOJ4Id { get; set; }
}
What I am deduce from your question is you have a method that accepts an updated Routing Tool object which contains an updated collection of Tool Services. You want to update that tool and it's associated services so that any service within that tool that is new gets added, otherwise updated, and any existing tool in the DB that is no longer in the passed in collection should be deleted..
If this is the case, you need to compare the provided version of the data to the database version of the data. For this example I am not using your Repository instance because I have no idea how it is implemented. Generally this pattern should be avoided unless there is a really good reason to have it.
[HttpPut("updateSOJ4")]
public IActionResult UpdateSOJ4([FromBody] Routing_Tool_SOJ4 updatedRoutingTool)
{
using (var context = new AppDbContext())
{
// Get tool and services from DB.
var existingRoutingTool = context.Routing_Tool_SOJ4s
.Include(x => x.Routing_Tool_Services)
.Single(x => x.Id == updatedRoutingTool.Id);
// Copy values that can be updated from the updatedRoutingTool to existingRoutingTool.
// ...
var updatedServiceIds = updatedRoutingTool.Routing_Tool_Services
.Select(x => x.Id)
.ToList();
var existingServiceIds = existingRoutingTool.Routing_Tool_Services
.Select(x => x.Id)
.ToList();
var serviceIdsToRemove = existingServiceIds
.Except(updatedServiceIds)
.ToList();
foreach (var service in updatedRoutingTool.Routing_Tool_Services)
{
var existingService = existingRoutingTool.Routing_ToolServices
.SingleOrDefault(x => x.Id == service.Id);
if (existingService == null)
existingRoutingTool.Routing_Tool_Services.Add(service);
else
{
// Copy allowed values from service to existingService
}
}
if(serviceIdsToRemove.Any())
{
var servicesToRemove = existingRoutingTool.Routing_Tool_Services
.Where(x => serviceIdsToRemove.Contains(x.Id))
.ToList();
foreach(var serviceToRemove in servicesToRemove)
existingRoutingTool.Routing_Tool_Services.Remove(serviceToRemove);
}
context.SaveChanges();
}
return Ok(request);
}
Normally the DbContext or Unit of Work would be injected into your controller, or the logic would be handed off to a service. This example uses a using block with a DbContext just to outline the minimum viable process flow for the operation.
Essentially load the current data state, compare that with the provided state to determine what needs to be added, updated, or removed.
Generally speaking when it comes to RESTful web services my recommendation is to avoid large update operations like this and instead structure the application to perform more atomic operations such as adding and removing services for a given tool as a distinct operation, working with a persisted copy (i.e. cached instance) of the data if you want the whole related operation to be committed to data state or abandoned at a higher level. This can help keep message sizes small, and server code more compact & worrying about a single responsibility. The risk of performing these large operations is that the passed in data must represent a complete picture of the data state or you could end up deleting/clearing data you don't intend. For example if you later want to optimize your code so that only added and updated services are sent over the wire, not unchanged services (to reduce message size) the above code will not work as it would delete anything not sent.

Entity Framework ObjectQuery.Include()

I have an object with two objects as properties (User, PrimaryNode), both could potentially be null, see below:
public class Item
{
[Key]
public int ItemId { get; set; }
public string ItemName { get; set; }
public Node PrimaryNode { get; set; }
public User User { get; set; }
}
I'm using Entity Framework 6 to populate the Item object and using chained includes to populate the PrimaryNode and User objects within it.
When the first chained Include has a null object then the whole object returns as null, for example:
using (var db = new MyContext())
{
var item = db.Items.Include(i => i.User).Include(n => n.PrimaryNode).FirstOrDefault(i => i.ItemId == id);
}
If in the above example i.User is null then the item variable is null. Whats the best way of populating both the sub-objects in a way that if a sub-object is null then the parent object and the other sub-object will still be populated?
I don't think your issue is due to the Include calls. According with the documentation:
This extension method calls the Include(String) method of the
IQueryable source object, if such a method exists. If the source
IQueryable does not have a matching method, then this method does
nothing.
In other words is going to be translated to:
var item = db.Items.Include("User").Include("PrimaryNode").FirstOrDefault(i => i.ItemId == id);
My question is, are you sure you have an Item with that id properly related with existing rows in Users and PrimaryNodes tables in your DB?. When you call Include method at the end is going to be translated to a join, so if the FK of your relationship doesn't match with the PK that reference, your query should not return what you are expecting.
Anyways, if you want to try another variant to load related properties you can use Explicit Loading:
var item = db.Items.FirstOrDefault(i => i.ItemId == id);
context.Entry(item).Reference(p => p.PrimaryNode).Load();
context.Entry(item).Reference(p => p.User).Load();
I think it would be better if you use Lazy loading int his situation. Just make the User and PrimaryNode virtual:
public class Item
{
[Key]
public int ItemId { get; set; }
public string ItemName { get; set; }
public virtual Node PrimaryNode { get; set; }
public virtual User User { get; set; }
}
And then:
var db = new MyContext();
var item = db.Items.FirstOrDefault(i => i.ItemId == id);
As others have mentioned, I think your issue is not due to the Includes. However, I think the following method has value. It is functionally equivalent to what you are already doing with the chained includes, but I think it has several benefits including making the intention of the code clear to the user.
The includes can be placed in Extension methods:
using System.Data.Entity;
using System.Linq;
namespace Stackoverflow
{
public static class EntityExtensions
{
public static IQueryable<Item> IncludePrimaryNode(this IQueryable<Item> query)
{
// eager loading if this extension method is used
return query.Include(item => item.PrimaryNode);
}
public static IQueryable<Item> IncludeUser(this IQueryable<Item> query)
{
// eager loading if this extension method is used
return query.Include(item => item.User);
}
}
}
Then, you can use the extensions as follows:
using (var db = new MyContext())
{
var itemQuery = db.Items.IncludeUser();
itemQuery = itemQuery.IncludePrimaryNode();
var item = itemQuery.FirstOrDefault(i => i.Id == 1);
}
It's just another way of doing the same thing, but I like the clarity it adds to the code.

Cannot update Entity Framework virtual list objects

I'll try explain simply my Entity Framework model. I have a User object which has a collection of zero or more UserInterest objects. Each user interest object has only three properties, unique ID, User Id and Description.
Whenever the user updates the User object, it should also update the related UserInterest objects but because these are free form (ie not part of a list of allowed interests), I want the user to pass in a list of type "string" to the webmethod of the names of all their interests. The code would ideally then look at the users existing list of interests, remove any that were no longer relevant and add in new ones and leave the ones which already exist.
My object model definitions
[Table("User")]
public class DbUser {
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
public virtual IList<DbUserInterest> Interests { get; set; }
}
[Table("UserInterest")]
public class DbUserInterest : IEntityComparable<DbUserInterest>
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public virtual DbUser User { get; set; }
public int? UserId { get; set; }
}
The context Fluent mappings
modelBuilder.Entity<DbUser>()
.HasKey(u => u.UserId);
modelBuilder.Entity<DbUser>()
.Property(u => u.UserId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<DbUser>()
.HasMany(u => u.Interests)
.WithRequired(p => p.User)
.WillCascadeOnDelete(true);
modelBuilder.Entity<DbUserInterest>()
.HasKey(p => p.Id);
modelBuilder.Entity<DbUserInterest>()
.Property(p => p.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<DbUserInterest>()
.HasRequired(p => p.User)
.WithMany(u => u.Interests)
.WillCascadeOnDelete(true);
And lastly my webmethod code and repository method to do the update
public UpdateUserProfileDetailsResponse UpdateUserProfileDetails(UpdateUserProfileDetailsRequest request)
{
try
{
var dbItem = _userDataRepository.GetItem(request.Header.UserId);
dbItem.Interests.Clear();
foreach (var dbInterest in request.UserInterests)
dbItem.Interests.Add(new DbUserInterest { Name = dbInterest, UserId = dbItem.UserId});
_userDataRepository.UpdateItem(dbItem);
_userDataRepository.Save();
}
catch (Exception ex)
{
}
}
public override bool UpdateItem(DbUser item)
{
var dbItem = GetItem(item.UserId);
if (dbItem == null)
throw new DataRepositoryException("User not found to update", "UserDataRepository.UpdateItem");
var dbInterests = Work.Context.UserInterests.Where(b => b.UserId == item.UserId).ToList();
var interestsToRemove = (from interest in dbInterests let found = item.Interests.Any(p => p.IsSame(interest)) where !found select interest).ToList();
var interestsToAdd = (from interest in item.Interests let found = dbInterests.Any(p => p.IsSame(interest)) where !found select interest).ToList();
foreach (var interest in interestsToRemove)
Work.Context.UserInterests.Remove(interest);
foreach (var interest in interestsToAdd)
{
interest.UserId = item.UserId;
Work.Context.UserInterests.Add(interest);
}
Work.Context.Entry(dbItem).State = EntityState.Modified;
return Work.Context.Entry(dbItem).GetValidationResult().IsValid;
}
When I run this, at the Repository.Save() line I get the exception
Assert.IsTrue failed. An unexpected error occurred: An error occurred while updating the entries. See the inner exception for details.
But interestingly in the webmethod if I comment out the line dbItem.Interests.Clear(); it doesn't throw an error, although then of course you get duplicates or extra items as it thinks everything is a new interest to add. However removing this line is the only way I can get the code to not error
Before, I had the UserId property of the Interest object set to non nullable and then the error was slightly different, something about you cannot change the relationship of a foreign key entity that is non nullable, which is why I changed the property to nullable but still no go.
Any thoughts?
You can't just clear the collection and then try to rebuild it. EF doesn't work that way. The DbContext keeps track of all of the objects that were brought back from the database in its Change Tracker. Doing it your way will of course cause duplicates because EF sees that they're not in the Change Tracker at all so they must be brand new objects necessitating being added to the database.
You'll need to either do the add/remove logic in your UpdateUserProfileDetails method, or else you have to find a way to pass request.UserInterests into your UpdateItem method. Because you need to adjust the existing entities, not the ones found on the request (which EF thinks are new).
you could try in this way
remove
dbItem.Interests.Clear();
then
foreach (var dbInterest in request.UserInterests){
if(dbItem.Interests.Any()){
if (dbItem.Interests.Count(i=> i.Name==dbInterest && i.UserId==dbItem.UserId) == 0){
dbItem.Interests.Add(new DbUserInterest { Name = dbInterest, UserId = dbItem.UserId});
}
}
}

Entity Framework 6.1: navigation properties not loading

This is my first time using Entity Framework 6.1 (code first). I keep running into a problem where my navigation properties are null when I don't expect them to be. I've enabled lazy loading.
My entity looks like this:
public class Ask
{
public Ask()
{
this.quantity = -1;
this.price = -1;
}
public int id { get; set; }
public int quantity { get; set; }
public float price { get; set; }
public int sellerId { get; set; }
public virtual User seller { get; set; }
public int itemId { get; set; }
public virtual Item item { get; set; }
}
It has the following mapper:
class AskMapper : EntityTypeConfiguration<Ask>
{
public AskMapper()
{
this.ToTable("Asks");
this.HasKey(a => a.id);
this.Property(a => a.id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
this.Property(a => a.id).IsRequired();
this.Property(a => a.quantity).IsRequired();
this.Property(a => a.price).IsRequired();
this.Property(a => a.sellerId).IsRequired();
this.HasRequired(a => a.seller).WithMany(u => u.asks).HasForeignKey(a => a.sellerId).WillCascadeOnDelete(true);
this.Property(a => a.itemId).IsRequired();
this.HasRequired(a => a.item).WithMany(i => i.asks).HasForeignKey(a => a.itemId).WillCascadeOnDelete(true);
}
}
Specifically, the problem is that I have an Ask object with a correctly set itemId (which does correspond to an Item in the database), but the navigation property item is null, and as a result I end up getting a NullReferenceException. The exception is thrown in the code below, when I try to access a.item.name:
List<Ask> asks = repo.GetAsksBySeller(userId).ToList();
List<ReducedAsk> reducedAsks = new List<ReducedAsk>();
foreach (Ask a in asks)
{
ReducedAsk r = new ReducedAsk() { id = a.id, sellerName = a.seller.username, itemId = a.itemId, itemName = a.item.name, price = a.price, quantity = a.quantity };
reducedAsks.Add(r);
}
Confusingly, the seller navigation property is working fine there, and I can't find anything I've done differently in the 'User' entity, nor in its mapper.
I have a test which recreates this, but it passes without any problems:
public void canGetAsk()
{
int quantity = 2;
int price = 10;
//add a seller
User seller = new User() { username = "ted" };
Assert.IsNotNull(seller);
int sellerId = repo.InsertUser(seller);
Assert.AreNotEqual(-1, sellerId);
//add an item
Item item = new Item() { name = "fanta" };
Assert.IsNotNull(item);
int itemId = repo.InsertItem(item);
Assert.AreNotEqual(-1, itemId);
bool success = repo.AddInventory(sellerId, itemId, quantity);
Assert.AreNotEqual(-1, success);
//add an ask
int askId = repo.InsertAsk(new Ask() { sellerId = sellerId, itemId = itemId, quantity = quantity, price = price });
Assert.AreNotEqual(-1, askId);
//retrieve the ask
Ask ask = repo.GetAsk(askId);
Assert.IsNotNull(ask);
//check the ask info
Assert.AreEqual(quantity, ask.quantity);
Assert.AreEqual(price, ask.price);
Assert.AreEqual(sellerId, ask.sellerId);
Assert.AreEqual(sellerId, ask.seller.id);
Assert.AreEqual(itemId, ask.itemId);
Assert.AreEqual(itemId, ask.item.id);
Assert.AreEqual("fanta", ask.item.name);
}
Any help would be extremely appreciated; this has been driving me crazy for days.
EDIT:
The database is SQL Server 2014.
At the moment, I have one shared context, instantiated the level above this (my repository layer for the db). Should I be instantiating a new context for each method? Or instantiating one at the lowest possible level (i.e. for every db access)? For example:
public IQueryable<Ask> GetAsksBySeller(int sellerId)
{
using (MarketContext _ctx = new MarketContext())
{
return _ctx.Asks.Where(s => s.seller.id == sellerId).AsQueryable();
}
}
Some of my methods invoke others in the repo layer. Would it better for each method to take a context, which it can then pass to any methods it calls?
public IQueryable<Transaction> GetTransactionsByUser(MarketContext _ctx, int userId)
{
IQueryable<Transaction> buyTransactions = GetTransactionsByBuyer(_ctx, userId);
IQueryable<Transaction> sellTransactions = GetTransactionsBySeller(_ctx, userId);
return buyTransactions.Concat(sellTransactions);
}
Then I could just instantiate a new context whenever I call anything from the repo layer: repo.GetTransactionsByUser(new MarketContext(), userId);
Again, thanks for the help. I'm new to this, and don't know which approach would be best.
Try to add
Include call in your repository call:
public IQueryable<Ask> GetAsksBySeller(int sellerId)
{
using (MarketContext _ctx = new MarketContext())
{
return _ctx.Asks
.Include("seller")
.Include("item")
.Where(s => s.seller.id == sellerId).AsQueryable();
}
}
Also, there is an extension method Include which accepts lambda expression as parameter and provides you type checks on compile time
http://msdn.microsoft.com/en-us/data/jj574232.aspx
As for the context lifespan, your repositories should share one context per request if this is a web application. Else it's a bit more arbitrary, but it should be something like a context per use case or service call.
So the pattern would be: create a context, pass it to the repositories involved in the call, do the task, and dispose the context. The context can be seen as your unit of work, so no matter how many repositories are involved, in the end one SaveChanges() should normally be enough to commit all changes.
I can't tell if this will solve the lazy loading issue, because from what I see I can't explain why it doesn't occur.
But although if I were in your shoes I'd like to get to the bottom of it, lazy loading is something that should not be relied on too much. Take a look at your (abridged) code:
foreach (Ask a in asks)
{
ReducedAsk r = new ReducedAsk()
{
sellerName = a.seller.username,
itemName = a.item.name
};
If lazy loading would work as expected, this would execute two queries against the database for each iteration of the loop. Of course, that's highly inefficient. That's why using Include (as in Anton's answer) is better anyhow, not only to circumvent your issue.
A further optimization is to do the projection (i.e. the new {) in the query itself:
var reducedAsks = repo.GetAsksBySeller(userId)
.Select(a => new ReducedAsk() { ... })
.ToList();
(Assuming – and requiring – that repo.GetAsksBySeller returns IQueryable).
Now only the data necessary to create ReducedAsk will be fetched from the database and it prevents materialization of entities that you're not using anyway and relatively expensive processes as change tracking and relationship fixup.

DBSet.Where(...).Delete() -> "no matching element" which is not true

I am using EF 6.1 with EF.Extended and I am trying to execute the following:
if (allRevisions != null && allRevisions.Any(r => r.Item.Id == itemId))
allRevisions.Where(r => r.Item.Id == itemId).Delete();
allRevisions is a DbSet<Revision> from my current DbContext (this code is inside a generic helper method).
When I execute this I get the following exception:
Sequence contains no matching element.
Which is not true as there is a matching revision and the Any is also true.
Furthermore if I execute the following it works fine:
if (allRevisions != null && allRevisions.Any(r => r.Item.Id == itemId))
{
foreach (var revision in allRevisions.Where(r => r.Item.Id == itemId))
allRevisions.Remove(revision);
}
But that is exactly the way you should be able to avoid with EF.Extended.
Am I doing something wrong or is this a bug in EF.Extended?
P.S.: I know that the Any is pointless - I added that to make shure there are revisions to delete after I got the error the first time. There is also no race-condition as on my dev-machine no one else is hitting the DB.
Better to materialize the query then check if it has items and delete those you need to delete all in memory. => but thats exactly what I want to avoid (and what EF.Extened is good for). I actually don't care if something has changed - I would expect it to simply execute a query like DELETE from Revisions WHERE Item_Id = #Id; in the DB.
UPDATE:
I created a small demo-project to reproduce the problem: HERE
It seems to be connected to inheritance. If I try the same thing with the ContentRevision it works, but with MyRevision, which inherits from it, it does not.
I faced the same problem. So I used your example to locate the issue. It seems to be in inheritance. In class MetadataMappingProvider is following code
// Get the entity set that uses this entity type
var entitySet = metadata
.GetItems<EntityContainer>(DataSpace.CSpace)
.Single()
.EntitySets
.Single(s => s.ElementType.Name == entityType.Name);
and the second Single seems to be the trouble, because in EntitySets property are just entity sets for base classes. There is a simple solution to this problem. Always use the base class (from EF point of view) in query.
For example if we have following mapping:
public class Item
{
public long Id { get; set; }
}
public class ItemWithContent : Item
{
public string Content { get; set; }
}
public class TestContext : DbContext
{
public IDbSet<Item> Items { get; set; }
}
this code will throw an error:
using (var context = new TestContext())
{
context.Items.OfType<ItemWithContent>()
.Where(o => string.IsNullOrWhiteSpace(o.Content)).Delete();
}
but this code will work correctly:
using (var context = new TestContext())
{
context.Items
.Where(o => o is ItemWithContent &&
string.IsNullOrWhiteSpace((o as ItemWithContent).Content)).Delete();
}

Categories

Resources