I'm doing an entity update from a Postback in MVC:
ControlChartPeriod period = _controlChartPeriodService.Get(request.PeriodId);
if (period != null)
{
SerializableStringDictionary dict = new SerializableStringDictionary();
dict.Add("lambda", request.Lambda.ToString());
dict.Add("h", request.h.ToString());
dict.Add("k", request.k.ToString());
period.Parameters = dict.ToXmlString();
// ToDo : fails on save every now and then when one of the values changes; fails on Foreign Key being null
try
{
_controlChartPeriodService.Update(period, request.PeriodId);
return Ok(request);
}
The update method looks like this:
public TObject Update(TObject updated, TKey key)
{
if (updated == null)
return null;
TObject existing = _context.Set<TObject>().Find(key);
if (existing != null)
{
_context.Entry(existing).CurrentValues.SetValues(updated);
_context.SaveChanges();
}
return existing;
}
public TObject Get(TKey id)
{
return _context.Set<TObject>().Find(id);
}
The weird thing is first time I run it it usually works fine; if i do a second post back it does not work and fails on an EntityValidation error of a foreign key; however examining the entity the foreign key looks fine and is untouched.
Do I need to somehow synchronize the Context somewhere?
I've been trying to find the differences in when it succeeds or when it does not succeed.
I'm using Injection for the repositories:
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped((_) => new DataContext(ConfigSettings.Database.ConnectionString));
services.AddScoped<ControlChartPeriodService, ControlChartPeriodService>();
}
-- update:
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public DateTime StartDate { get; set; }
public DateTime? EndDate { get; set; }
public virtual ICollection<ControlChartPoint> ControlChartPoints { get; set; }
[Required]
public virtual ControlChart ControlChart { get; set; }
public string Parameters { get; set; }
In the ControlChartMap we have the following:
HasMany(x => x.Periods)
.WithRequired(c => c.ControlChart);
If you are expecting that the loading of the TObject includes the relationship as well, seems to me your problem is in this line:
TObject existing = _context.Set<TObject>().Find(key);
Find will only load the TObject record but not its relationship(s) , and as there is no explicit reference in code to the ControlChart property it doesn't get lazy loaded - which seems to be the behavior that you are expecting , so I'm assuming you have lazy-loading enabled
The fact that the relationship has been configured as
HasMany(x => x.Periods).WithRequired(c => c.ControlChart);
indicates that TObject's ControlChart property is meant to be a related set of Periods but that doesn't mean that it will force the lazy-load of the relationship when loading a TObject.
(I personally try to avoid the reliance on lazy loading due to things like this, and prefer to actually disable it and to eagerly load the relationships via Include, or alternatively in a less measure, an explicit load , by using Load. It may be a matter of personal taste but I've found this way saves you from many headaches)
So If you have lazy loading enabled and like the way it works, make a reference to the ControlChart property at any point between the load and before saving, like
var chart= existing.ControlChart;
or something similar
Or for non-lazy-loading scenarios, although it can be used in any case:
TObject existing = _context.Set<TObject>().Where(x=>x.Id==key).Include(x=> x.ControlChart).FirstOrDefault();
Have you looked at the different object graphs you try to inject. The generic method could deal on a first pass but could fail on update. Simply also because the resulting SQL or Datastore engine language used does not result in the same upon Insert and Update ...
I exp. this and in my case it came from the Model behind. Look at the Foreign Ks constraints etc ... Some might have not be created properly hence the update failure after ...
This is just a stab in the dark but...
In my experience, if Entity Framework is randomly working/not working, it's because I needed to use async/await to ensure data is successfully collected before the method gets to work.
This would perhaps explain why you're getting a null Foreign Key error. If the method gets ahead of itself, it's going to be attempting to work on a new TObject, rather than the one you want to collect from the database.
Try turning it into an async function and turn all the get/set methods into their Async versions.
i.e.
public async Task<TObject> Update(TObject updated, TKey key)
{
if (updated == null)
return null;
TObject existing = await _context.Set<TObject>().FindAsync(key);
if (existing != null)
{
_context.Entry(existing).CurrentValues.SetValues(updated);
_ = await_context.SaveChangesAsync();
}
return existing;
}
Then in the main method (which should also be async)
try
{
_ = await _controlChartPeriodService.Update(period, request.PeriodId);
return Ok(request);
}
Related
I 'm using EF Core 3.1.10. I have the following entities:
public class Request {
public int Id { get; set; }
public string Title { get; set; }
public string ProjectId { get; set; }
public List<RequestAttachment> Attachments { get; set; } = new List<RequestAttachment> ();
}
public class RequestAttachment {
public int Id { get; set; }
public int RequestId { get; set; }
public Request Request { get; set; }
public byte[] FileStream { get; set; }
public string Filename { get; set; }
public RequestAttachmentType RequestAttachmentType { get; set; }
public int RequestAttachmentTypeId { get; set; }
}
public class RequestAttachmentType {
public int Id { get; set; }
public string Name { get; set; }
}
In my repository, I have a simple Update method:
public async Task UpdateRequest (Request aRequest) {
// I'm attaching aRequest.Attachments because they already exist in the database and I don 't want to update them here
// Option 1 Not working
// aRequest.Attachments.ForEach (a => theContext.RequestAttachments.Attach (a));
// Option 2 Not working
// theContext.RequestAttachments.AttachRange (aRequest.Attachments);
// Option 3 Working
aRequest.Attachments.ForEach (a => theContext.Entry (a).State = EntityState.Unchanged);
theContext.Requests.Update(aRequest);
await theContext.SaveChangesAsync ();
}
Note that I'm attaching "aRequest.Attachments" because I don 't want to update Attachments. I only want to update aRequest. "aRequest.Attachments" already exist in the database that's why I 'm using Attach so they don't get re-added. But Attach and AttachRange do not work when a request has more than one attachment. It throws the following error:
The instance of entity type 'RequestAttachmentType' cannot be tracked
because another instance with the key value '{Id: 1}' is already being
tracked. When attaching existing entities, ensure that only one entity
instance with a given key value is attached.
I don 't understand this error because I did not explicitly attach "RequestAttachmentType". The only thing I did was attaching its parent "aRequest.Attachments".
When I set the state manually like I did in Option 3, no error was thrown. I thought Attach is equivalent to theContext.Entry (a).State = EntityState.Unchanged. Why option 3 works but option 1 and 2 do not?
Working with detached entity graphs is going to continue to cause all kinds of headaches like this. Not only do you need to handle the scenario that you don't want to update/duplicate related entities, but you have to also handle cases where the DbContext is already tracking the entity you want to update. Sergey was on the right track there.
The problem is that you have a complete graph:
Request
Atachment
AttachmentType
Attachment
AttachmentType
where you want to update details in Request and the Attachments...
One issue with "Update" is that it will dive the graph to look for entities that might need to be added/updated. On its own with a detached graph this will usually result in duplicate items being created. Hence "attaching" them first. The trouble here is where the DbContext is already tracking one or more entities in the graph. One key detail to remember about EF is that References are everything. Deserializing entity graphs is a painful exercise.
For example lets say we deserialize a Request Id 1, with 2 attachments, #1, and #2, where both have an AttachmentType of "Document" (AttachmentType ID = 14)
What you will end up is something that looks like:
Document
{
ID:1
...
Attachments
{
Attachment
{
ID:1
...
AttachmentType
{
ID: 14
}
}
Attachment
{
ID:2
...
AttachmentType
{
ID: 14
}
}
}
}
Without considering what the DbContext may or may not already be tracking prior to looking at these entities, there is already a problem. Attachment ID 1 and 2 are distinct objects, however they both reference an AttachmentType ID 14. When de-serialized, these will be 2 completely distinct references to objects that have an ID of 14.
A common surprise is where test code appears to work fine because the two attachments had different attachment types, but then fails unexpectedly when they happen to have the same type. The first attachment would have the DbContext tracking the first attachment's "Type". If the second attachment's Type was a different ID, then attaching that 2nd type would succeed so long as the Context wasn't tracking it. However, when set to the same ID the "already tracking entity with the same ID" pops up.
When dealing with disconnected entities you need to be very deliberate about references and explicitly handle whenever the DbContext is tracking a reference. This means consulting the DbSet Local caches:
public async Task UpdateRequest (Request aRequest)
{
var existingRequest = theContext.Requests.Local.SingleOrDefault(x => x.Id = aRequest.Id);
if (existingRequest != null)
{
// copy values from aRequest -> existingRequest or Leverage something like automapper.Map(aRequest, existingRequest)
}
else
{
theContext.Requests.Attach(aRequest);
theContext.Entity(aRequest).State = EntityState.Modified; // Danger Will Robinson, make 100% sure your entity from client is validated!! This overwrites everything.
}
foreach(var attachment in aRequest)
{
var existingAttachment = theContext.Attachments.Local.SingleOrDefault(x => x.Id == attachment.Id);
// Look for a reference to the attachment type. If found, use it, if not attach and use that...
var existingAttachmentType = theContext.AttachmentTypes.Local.SingleOrDefault(x => x.Id == attachment.AttachmentType.Id);
if (existingAttachmentType == null)
{
theContext.AttachmentTypes.Attach(attachment.AttachmentType);
existingAttachmentType = attachment.AttachmentType;
}
if(existingAttachment != null)
{
// copy values across.
AttachmentType = existingAttachmentType; // in case we change the attachment type for this attachment.
}
else
{
theContext.Attachments.Attach(attachment);
theContext.Entity(attachment).State = EntityState.Modified;
attachment.AttachmentType = existingAttachmentType;
}
}
await theContext.SaveChangesAsync ();
}
Needless to say this is a lot of messing around to check and replace references to either get the DbContext to track detached entities or replace the references with tracked entities.
A simpler option is to leverage Automapper to establish a configuration for what fields can be updated from a source (ideally a ViewModel, but you can use an entity graph as a source) to a destination. (Entities tracked by the DbContext)
Step 1: Configure Automapper with the rules about what to update for a Request -> Attachments graph.. (Not shown)
Step 2: Load tracked entity graph, and the applicable AttachmentTypes:
var existingRequest = theContext.Requests
.Include(x => x.Attachments)
.ThenInclude(x => x.AttachmentType)
.Single(x => x.Id == aRequest.Id);
var referencedAttachmentTypeIds = aRequest.Attachments.Select(x => x.AttachmentTypeId)
.Distinct().ToList();
var referencedAttachmentTypes = theContext.AttachmentTypes
.Where(x => referencedAttachmentTypeIds.Contains(x.Id))
.ToList();
Getting the list of attachment types only applies if we can change an attachment's type, or are adding attachments.
Step 3: Leverage Automapper to copy across values
mapper.Map(aRequest, existingRequest);
If Attachments can be updated, added, and/or removed you will need to handle those scenarios against the existingRequest. Here we reference the loaded set of AttachmentTypes.
Step 4: Save Changes.
The primary benefits of this approach is that you do away with the constant checking for existing references and the consequences of missing a check. You also configure the rules about what values can legally be overwritten when calling the Automapper Map call so only values you expect are copied from the source to the existing data record. This also results in faster Update queries as EF will only build statements for the values that actually changed, where using Update or EntityState.Modified result in SQL UPDATE statements that update every column.
Try this:
var itemExist = await theContext.Requests.FirstOrDefaultAsync ( i=>i.Id == aRequest.Id);
if (itemExist !=null)
{
var attachments=aRequest.Attachments;
aRequest.Attachments=null;
theContext.Entry(itemExist ).CurrentValues.SetValues(aRequest);
await theContext.SaveChangesAsync();
aRequest.Attachments=attachments;
}
I'm trying to figure out how to smoothly do a partial update (basically a HTTP PATCH) of an entity, using Entity Framework 6.0, but I'm stumped at the number of examples out there that don't seem to work for me (even those that aren't obviously for another version of EF).
What I'd like to accomplish:
The entity is updated without having to load it first; i.e. there's only one trip to the database
Only the properties that I touch are updated - others are left as is
The closest I've gotten is neatly described by this answer to a very similar question, and illustrated by the following code:
public async Task UpdateMyEntity(int id, int? updatedProperty, string otherProperty)
{
using (var context = new MyDbContext())
{
var entity = new MyEntity { Id = id };
context.MyEntities.Attach(entity);
if (updatedProperty != null) { entity.Property = updatedProperty.Value; }
if (!string.IsNullOrEmpty(otherProperty) { entity.OtherProperty = otherProperty; }
await context.SaveChangesAsync();
}
}
Now, this works for simple entities, but I'm getting entity validation errors because I have a couple of required properties and relations that are not updated and therefore not present in the attached entity. As noted, I'd just like to ignore those.
I've debugged and verified that context.Entry(entity).Property(e => e.Property).IsModified changes to true when that line is run, and that all the properties I never touch still return false for similar checks, so I thought EF would be able to handle this.
Is it possible to resolve this under the two constraints above? How?
Update:
With LSU.Net's answer I understand somewhat what I have to do, but it doesn't work fully. The logic fails for referential properties.
Consider the following domain model:
public class MyEntity
{
public int Id { get; set; }
public int Property { get; set; }
[Required]
public string OtherProperty { get; set; }
[Required]
public OtherEntity Related { get; set; }
}
public class OtherEntity
{
public int Id { get; set; }
public string SomeProperty { get; set; }
}
Now, if I try to update a MyEntity, I do the following:
var entity = new MyEntity { Id = 123 }; // an entity with this id exists in db
context.MyEntities.Attach(entity);
if (updatedProperty != null) { entity.Property = updatedProperty.Value; }
await context.SaveChangesAsync();
In my custom validation method, overridden as in the answer below, the validation error on the required property OtherProperty is correctly removed, since it is not modified. However, I still get a validation error on the Related property, because entityEntry.Member("Related") is DbReferenceEntry, not DbPropertyEntry, and thus the validation error is not marked as a false error.
I tried adding a separate, analogous clause for handling reference properties, but the entityEntry doesn't seem to mark those as changed; with relation = member as DbReferenceEntry, relation doesn't have anything to indicate that the relationship is changed.
What can I check against for false errors in this case? Are there any other cases I need to handle specially (one-to-many relationships, for example)?
Entity Framework validation with partial updates
#Shimmy has written some code here to omit the validation logic for unmodified properties. That may work for you.
protected override DbEntityValidationResult ValidateEntity(
DbEntityEntry entityEntry,
IDictionary<object, object> items)
{
var result = base.ValidateEntity(entityEntry, items);
var falseErrors = result.ValidationErrors
.Where(error =>
{
var member = entityEntry.Member(error.PropertyName);
var property = member as DbPropertyEntry;
if (property != null)
return !property.IsModified;
else
return false;//not false err;
});
foreach (var error in falseErrors.ToArray())
result.ValidationErrors.Remove(error);
return result;
}
I have an application with 4 layers:
-Core (Models)
-Repository (EF DbContext actions)
-Service (Business logic)
-Web (MVC)
I'm trying to update an object with a 1:1 relationship with EF using the following method:
public async Task<bool> UpdateProductTicketing(ProductTicketing ticketing)
{
var product = await GetProductByIdAsync(ticketing.ProductId);
// Validation removed for simplicity
// 'ticketing' passed validation so let's
// just replace it with the existing record.
product.Ticketing = ticketing;
_repo.ProductRepository.Update(product);
return await _repo.SaveAsync();
}
This works for an initial insert, but it doesn't work as I'd expect when I'm updating the record:
A first chance exception of type 'System.Data.Entity.Infrastructure.DbUpdateException' occurred...
The actual error message is:
Violation of PRIMARY KEY constraint 'PK_dbo.ProductTicketing'. Cannot insert duplicate key in object 'dbo.ProductTicketing'. The statement has been terminated.
Obviously the PK and FK "ProductId" doesn't change - so why does EF try to drop and insert my record instead of just updating it, and why does it fail?
But more importantly - how can I prevent this. I know I can manually map the object values and then update it - that works but it's tedious mapping two identical objects together and doesn't feel correct.
My repository for retrieving the Product object is in my Repository layer, while the method above is in my Service layer.
This is how I'm currently resolving this - and it looks as dirty as it feels:
public async Task<bool> UpdateProductTicketing(ProductTicketing ticketing)
{
var product = await GetProductByIdAsync(ticketing.ProductId);
// Validation removed for simplicity
if (product.Ticketing == null)
{
product.Ticketing = ticketing;
}
else
{
product.Ticketing.AllowEventBooking = ticketing.AllowEventBooking;
// Doing the same for all other properties etc
// etc
// etc
}
_repo.ProductRepository.Update(product);
return await _repo.SaveAsync();
}
How can I achieve this without doing all this horrible mapping an object to an identical object?
Edit
Here are the two models referred to above:
[Table(#"Products")]
public class Product
{
[Key]
public int Id { get; set; }
public virtual ProductTicketing Ticketing { get; set; }
// Removed others for clarity
[Timestamp]
public byte[] RowVersion { get; set; }
}
[Table(#"ProductTicketing")]
public class ProductTicketing
{
[Key, ForeignKey("Product")]
public int ProductId { get; set; }
public bool AllowEventBooking { get; set; }
// Removed others for clarity
public virtual Product Product { get; set; }
}
It's also probably worth noting that the "ProductTicketing" object I'm passing into the UpdateProductTicketing method is a new object created from values in my controller - but the ID is the same so I assume it should work.
I think I see the problem now - when you do product.Ticketing = ticketing;, EF treats this as a new insert.
To avoid this, you can do one of these things:
Continue using the workaround (which is not a wokaround actually but just the way EF expects you to tell when to insert vs. when to update).
Now this depends on rest of your code and design, but instead of fetching the product, you can fetch the ticket and update its properties. Of course, this means that if the ticketing is not found, you need to insert it which then kinda looks like what you're already doing with UpdateProductTicketing.
Use the InsertOrUpdate pattern (I made some assumptions about your code but hopefully it gives you the idea - the main thing here is the InsertOrUpdate method):
public class ProductRepository : IRepository
{
private SomeContext context;
public void InsertOrUpdate(ProductTicketing ticketing)
{
context.Entry(ticketing).State = ticketing.ProductId == 0 ?
EntityState.Added :
EntityState.Modified;
context.SaveChanges();
}
}
// And a generic version
public void InsertOrUpdate<T>(T entity) where T : class
{
if (context.Entry(entity).State == EntityState.Detached)
context.Set<T>().Add(entity);
context.SaveChanges();
}
You are getting that error because ef thinks that the ProductTicket is a new entity and is trying to insert the entity into the db. I don't know about the _repo.ProductRepository.Update(product) call but how about you attach the ProductTicket to the context and set the entity state to modified
I am building a windows form application, and I use multiple DBContext instances (mostly one per Business layer call).
After literally days of dealing with an issue (while inserting new entities, the ones they referred to were added as new ones, instead of using the existing entities), I found out that the problem was I had to attach the existing entities to the context.
All was good for about 2 hours, when I then got errors while attaching: the entity with the same key exists in the context.
I tried testing before attaching (similar method for every entity type):
private void attachIfNeeded(POCO.Program myObject, myContext context)
{
if (!context.Set<POCO.Program>().Local.Any(e => e.ID == myObject.ID))
{
context.programs.Attach(myObject);
return true;
}
else
{
myObject = context.Set<POCO.Program>().Local.Single(e => e.ID == myObject.ID);
return false;
}
}
But the tests return false, but it still fails when attaching.
So basically, if I don't attach, it will add a new entity instead of using the existing (and intended) one. If I do attach, there's an error I can't figure out.
I have looked around (doing this the whole day now) and I actually (think I) know what the problem is:
The entity I am trying to add has multiple relationships, and other entities can be reached by multiple paths. Could that cause the problem?
Please help with this, solutions out there really make no sense to me and haven't worked.
I am really close to the point where I will try-catch around the attach statement and be done with it. But I will hate doing it.
Here are my entities (not all of them, but this should be enough):
public class Word
{
[Key]
public int ID {get;set;}
[Required]
public string word { get; set; }
public WordCategories category { get; set; }
public Word parent {get;set;}
public List<Unit> units { get; set; }
public Program program { get; set; }
public List<Lesson> lessons { get; set; }
public Word()
{
units = new List<Unit>();
lessons = new List<Lesson>();
}
}
public class Unit
{
[Key ]
public int ID { get; set; }
[Required]
public string name { get; set; }
public string description { get; set; }
public List<Lesson> lessons { get; set; }
public Program program {get;set;}
public List<Word> words { get; set; }
public Unit()
{
lessons=new List<Lesson>();
words = new List<Word>();
}
}
And here is where I am calling the attach method. The error is thrown on the first attach:
public int addWords(List<POCO.Word > words,int programID, int unitID,int lessonID)
{
CourseHelperDBContext context = getcontext();
int result;
foreach(POCO.Word a in words)
{
foreach (POCO.Unit b in a.units)
attachIfNeeded(b, context);
foreach(POCO.Lesson c in a.lessons )
attachIfNeeded(c,context);
attachIfNeeded(a.program,context);
if (a.parent != null)
attachIfNeeded(a.parent,context);
}
context.words.AddRange(words);
result = context.SaveChanges();
return result;
}
I cannot believe I'm having so many issues with this. I just want to store those entities, add some (I haven't gotten to the point where I would change them) and save it.
So far I've figured:
Some words are new, some exist and some are changed (mostly parent property);
All units exist, as do programs and lessons (so I need to attach them);
The object graph contains multiple paths to entities, some of which exist, some of which are new;
I am using a new context for every request. I run into other issues when I was using the same all the time. I found solutions that pointed to this pattern, and I think it's OK since that's what you'd do on an ASP MVC project.
All these could be causing problems, but I don't know which and how to work around them.
I think I can make this work by adding one word at a time, and pulling programs, lessons and units every time... but that means many many round trips to the DB. This can't be the way.
Back to this after quite some time, the problem in this case was that I needed to retrieve the entities that were present on my relationships.
The solution was neither attach (because it would fail if the entity is already attached) nor add (because it already existed on the DB).
What I should have done was to retrieve every entity related to the one I was adding.
This helped:
Entity Framework creating new entity with relationship to existing entity, results in attempt to create new copy of the existing entity
After attaching the entity, try setting the entity state to modified.
context.programs.Attach(myObject);
context.Entry(myObject).State = EntityState.Modified;
I think there's a mistake in your test logic.
If entity does not exist in database, you should be adding instead of attaching. Your code is attaching if it can't find an entity when it should really be adding.
Code to add a new entity (Create/Insert)
context.Set<T>.Add(entity);
Code to attach an entity (Update)
context.Set<T>.Attach(entity);
context.Entry(entity).State = EntityState.Modified;
If your code is failing on the first attach, that would be attachIfNeeded(b,context); ? I don't think you have shown us the code for this.
I share my experience with the same exception.
First, here is my code:
public void UpdateBulk(IEnumerable<Position> pDokumentPosition, DbDal pCtx)
{
foreach (Position vPos in pDokumentPosition)
{
vPos.LastDateChanged = DateTime.Now;
pCtx.Entry(vPos).State = EntityState.Modified;
}
pCtx.SaveChanges();
}
I got the same exception on the EntityState.Modified line.
In my case the problem was that, when set the vPos state to modified, then all the related objects (vPos.Document and vPos.Produkt) loaded in the context with unchanged state.
In the foreach first step it not makes any exception, just on the second step, because eg. the related Dokument entity has already been loaded in the memory/context (so the key property of the Dokument too).
And how i solve it? (maybe not the best solution):
I detach the related entites in every step with this lines:
if (vPos.Dokument != null)
{
pCtx.Entry(vPos.Dokument).State = EntityState.Detached;
}
if (vPos.Produkt!=null)
{
pCtx.Entry(vPos.Produkt).State = EntityState.Detached;
}
If you have better solution, I'm looking forward to it...
You can try this
context.words.Add(words);
result=context.SaveChanges();
I have a database context with lazy loading disabled. I am using eager loading to load all of my entities. I cannot update many to many relationships.
Here's the repository.
public class GenericRepository<TEntity> : IGenericRepository<TEntity>
where TEntity : class
{
... other code here...
public virtual void Update(TEntity t)
{
Set.Attach(t);
Context.Entry(t).State = EntityState.Modified;
}
...other code here...
}
Here's the User model.
public partial class User
{
public User()
{
this.Locks = new HashSet<Lock>();
this.BusinessModels = new HashSet<BusinessModel>();
}
public int UserId { get; set; }
public string Username { get; set; }
public string Name { get; set; }
public string Phone { get; set; }
public string JobTitle { get; set; }
public string RecoveryEmail { get; set; }
public Nullable<double> Zoom { get; set; }
public virtual ICollection<Lock> Locks { get; set; }
public virtual ICollection<BusinessModel> BusinessModels { get; set; }
}
If I modify the business models collection, it does not save the business models collection although I have attached the entire entity.
Worker.UserRepository.Update(user);
I'm not sure what is going on. I don't want to break my generic repository/unit of work pattern just to update many-to-many relationships.
Edit 2: I've got this working...but it is extremely different from the pattern that I'm going for. Having hard implementations means I will need to create a method for each type that has a many to many relationship. I am investigating now to see if I can make this a generic method.
Edit 3: So the previous implementation I had did not work like I thought it would. But now, I have a slightly working implementation. If someone would please help me so I can move on from this, I will love you forever.
public virtual void Update(TEntity updated,
IEnumerable<object> set,
string navigationProperty,
Expression<Func<TEntity, bool>> filter,
Type propertyType)
{
// Find the existing item
var existing = Context.Set<TEntity>().Include(navigationProperty).FirstOrDefault(filter);
// Iterate through every item in the many-to-many relationship
foreach (var o in set)
{
// Attach it if its unattached
if (Context.Entry(o).State == EntityState.Detached)
// Exception "an object with the same key already exists"
// This is due to the include statement up above. That statement
// is necessary in order to edit the entity's navigation
// property.
Context.Set(propertyType).Attach(o);
}
// Set the new value on the navigation property.
Context.Entry(existing).Collection(navigationProperty).CurrentValue = set;
// Set new primitive property values.
Context.Entry(existing).CurrentValues.SetValues(updated);
Context.Entry(existing).State = EntityState.Modified;
}
I then call it like this:
Worker.UserRepository.Update(user, user.BusinessModels, "BusinessModels", i => i.UserId == user.UserId, typeof (BusinessModel));
Extremely messy, but it lets me update many-to-many relationships with generics. My big problem is the exception when I go to attach new values that already exist. They're already loaded because of the include statement.
This works:
This doesn't:
After many painful hours, I have finally found a way to update many-to-many relationships with a completely generic repository. This will allow me to create (and save) many different types of entities without creating boilerplate code for each one.
This method assumes that:
Your entity already exists
Your many to many relationship is stored in a table with a composite key
You are using eager loading to load your relationships into context
You are using a unit-of-work/generic repository pattern to save your entities.
Here's the Update generic method.
public virtual void Update(Expression<Func<TEntity, bool>> filter,
IEnumerable<object> updatedSet, // Updated many-to-many relationships
IEnumerable<object> availableSet, // Lookup collection
string propertyName) // The name of the navigation property
{
// Get the generic type of the set
var type = updatedSet.GetType().GetGenericArguments()[0];
// Get the previous entity from the database based on repository type
var previous = Context
.Set<TEntity>()
.Include(propertyName)
.FirstOrDefault(filter);
/* Create a container that will hold the values of
* the generic many-to-many relationships we are updating.
*/
var values = CreateList(type);
/* For each object in the updated set find the existing
* entity in the database. This is to avoid Entity Framework
* from creating new objects or throwing an
* error because the object is already attached.
*/
foreach (var entry in updatedSet
.Select(obj => (int)obj
.GetType()
.GetProperty("Id")
.GetValue(obj, null))
.Select(value => Context.Set(type).Find(value)))
{
values.Add(entry);
}
/* Get the collection where the previous many to many relationships
* are stored and assign the new ones.
*/
Context.Entry(previous).Collection(propertyName).CurrentValue = values;
}
Here's a helper method I found online which allows me to create generic lists based on whatever type I give it.
public IList CreateList(Type type)
{
var genericList = typeof(List<>).MakeGenericType(type);
return (IList)Activator.CreateInstance(genericList);
}
And from now on, this is what calls to update many-to-many relationships look like:
Worker.UserRepository.Update(u => u.UserId == user.UserId,
user.BusinessModels, // Many-to-many relationship to update
Worker.BusinessModelRepository.Get(), // Full set
"BusinessModels"); // Property name
Of course, in the end you will need to somewhere call:
Context.SaveChanges();
I hope this helps anyone who never truly found how to use many-to-many relationships with generic repositories and unit-of-work classes in Entity Framework.
#dimgl Your solution worked for me. What I've done in addition was to replace the hard-coded type and name of the primaryKey with dynamically retrieved ones:
ObjectContext objectContext = ((IObjectContextAdapter)context).ObjectContext;
ObjectSet<TEntity> set = objectContext.CreateObjectSet<TEntity>();
IEnumerable<string> keyNames = set.EntitySet.ElementType.KeyMembers.Select(k => k.Name);
var keyName = keyNames.FirstOrDefault();
var keyType = typeof(TEntity).GetProperty(keyName).PropertyType
foreach (var entry in updatedSet
.Select(obj =>
Convert.ChangeType(obj.GetType()
.GetProperty(keyName)
.GetValue(obj, null), keyType))
.Select(value => context.Set<TEntity>().Find(value)))
{
values.Add(entry);
}
Like this your code won't depend on the Entity key's name and type.