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;
}
Related
Given the following set up where there are many Teams and there are many LeagueSessions. Each Team belongs to zero or more LeagueSessions but only ever one LeagueSession is active. LeagueSessions have many teams, and the teams will be repeated. Many-to-many relationship is established between Teams and LeagueSessions with a join table called TeamsSessions.
Team model looks like this:
public class Team
{
public string Id { get; set; }
public string Name { get; set; }
public League League { get; set; }
public string LeagueID { get; set; }
public bool Selected { get; set; }
public ICollection<Match> Matches { get; set; }
public virtual ICollection<TeamSession> TeamsSessions { get; set; }
}
Team model fluent api configuration:
`
public class TeamConfiguration
{
public TeamConfiguration(EntityTypeBuilder<Team> model)
{
// The data for this model will be generated inside ThePLeagueDataCore.DataBaseInitializer.DatabaseBaseInitializer.cs class
// When generating data for models in here, you have to provide it with an ID, and it became mildly problematic to consistently get
// a unique ID for all the teams. In ThePLeagueDataCore.DataBaseInitializer.DatabaseBaseInitializer.cs we can use dbContext to generate
// unique ids for us for each team.
model.HasOne(team => team.League)
.WithMany(league => league.Teams)
.HasForeignKey(team => team.LeagueID);
}
}
`
Each team belongs to a single League. League model looks like this:
`public class League
{
public string Id { get; set; }
public string Type { get; set; }
public string Name { get; set; }
public IEnumerable<Team> Teams { get; set; }
public bool Selected { get; set; }
public string SportTypeID { get; set; }
public SportType SportType { get; set; }
public IEnumerable<LeagueSessionSchedule> Sessions { get; set; }
}`
fluent API for the League:
`public LeagueConfiguration(EntityTypeBuilder<League> model)
{
model.HasOne(league => league.SportType)
.WithMany(sportType => sportType.Leagues)
.HasForeignKey(league => league.SportTypeID);
model.HasMany(league => league.Teams)
.WithOne(team => team.League)
.HasForeignKey(team => team.LeagueID);
model.HasData(leagues);
}`
SessionScheduleBase class looks like this:
public class SessionScheduleBase
{
public string LeagueID { get; set; }
public bool ByeWeeks { get; set; }
public long? NumberOfWeeks { get; set; }
public DateTime SessionStart { get; set; }
public DateTime SessionEnd { get; set; }
public ICollection<TeamSession> TeamsSessions { get; set; } = new Collection<TeamSession>();
public ICollection<GameDay> GamesDays { get; set; } = new Collection<GameDay>();
}
Note: LeagueSessionSchedule inherits from SessionScheduleBase
The TeamSession model looks like this:
`public class TeamSession
{
public string Id { get; set; }
public string TeamId { get; set; }
public Team Team { get; set; }
public string LeagueSessionScheduleId { get; set; }
public LeagueSessionSchedule LeagueSessionSchedule { get; set; }
}`
I then configure the relationship with the fluent API like this:
`public TeamSessionConfiguration(EntityTypeBuilder<TeamSession> model)
{
model.HasKey(ts => new { ts.TeamId, ts.LeagueSessionScheduleId });
model.HasOne(ts => ts.Team)
.WithMany(t => t.TeamsSessions)
.HasForeignKey(ts => ts.TeamId);
model.HasOne(ts => ts.LeagueSessionSchedule)
.WithMany(s => s.TeamsSessions)
.HasForeignKey(ts => ts.LeagueSessionScheduleId);
}`
The problem arises whenever I attempt to insert a new LeagueSessionSchedule. The way I am adding a new TeamSession object onto the new LeagueSessionSchedule is like this:
`foreach (TeamSessionViewModel teamSession in newSchedule.TeamsSessions)
{
Team team = await this._teamRepository.GetByIdAsync(teamSession.TeamId, ct);
if(team != null)
{
TeamSession newTeamSession = new TeamSession()
{
Team = team,
LeagueSessionSchedule = leagueSessionSchedule
};
leagueSessionSchedule.TeamsSessions.Add(newTeamSession);
}
}`
Saving the new LeagueSessionSchedule code:
public async Task<LeagueSessionSchedule> AddScheduleAsync(LeagueSessionSchedule newLeagueSessionSchedule, CancellationToken ct = default)
{
this._dbContext.LeagueSessions.Add(newLeagueSessionSchedule);
await this._dbContext.SaveChangesAsync(ct);
return newLeagueSessionSchedule;
}
Saving the new LeagueSessionSchedule object throws an error by Entity Framework Core that it cannot INSERT a duplicate primary key value into the dbo.Teams table. I have no idea why its attempting to add to dbo.Teams table and not into TeamsSessions table.
ERROR:
INSERT INTO [LeagueSessions] ([Id], [Active], [ByeWeeks], [LeagueID], [NumberOfWeeks], [SessionEnd], [SessionStart])
VALUES (#p0, #p1, #p2, #p3, #p4, #p5, #p6);
INSERT INTO [Teams] ([Id], [Discriminator], [LeagueID], [Name], [Selected])
VALUES (#p7, #p8, #p9, #p10, #p11),
(#p12, #p13, #p14, #p15, #p16),
(#p17, #p18, #p19, #p20, #p21),
(#p22, #p23, #p24, #p25, #p26),
(#p27, #p28, #p29, #p30, #p31),
(#p32, #p33, #p34, #p35, #p36),
(#p37, #p38, #p39, #p40, #p41),
(#p42, #p43, #p44, #p45, #p46);
System.Data.SqlClient.SqlException (0x80131904): Violation of PRIMARY KEY constraint 'PK_Teams'. Cannot insert duplicate key in object 'dbo.Teams'. The duplicate key value is (217e2e11-0603-4239-aab5-9e2f1d3ebc2c).
My goal is to create a new LeagueSessionSchedule object. Along with the creation of this object, I also have to create a new TeamSession entry to the join table (or not if join table is not necessary) to then be able to pick any given team and see what session it is currently a part of.
My entire PublishSchedule method is the following:
`
public async Task<bool> PublishSessionsSchedulesAsync(List<LeagueSessionScheduleViewModel> newLeagueSessionsSchedules, CancellationToken ct = default(CancellationToken))
{
List<LeagueSessionSchedule> leagueSessionOperations = new List<LeagueSessionSchedule>();
foreach (LeagueSessionScheduleViewModel newSchedule in newLeagueSessionsSchedules)
{
LeagueSessionSchedule leagueSessionSchedule = new LeagueSessionSchedule()
{
Active = newSchedule.Active,
LeagueID = newSchedule.LeagueID,
ByeWeeks = newSchedule.ByeWeeks,
NumberOfWeeks = newSchedule.NumberOfWeeks,
SessionStart = newSchedule.SessionStart,
SessionEnd = newSchedule.SessionEnd
};
// leagueSessionSchedule = await this._sessionScheduleRepository.AddScheduleAsync(leagueSessionSchedule, ct);
// create game day entry for all configured game days
foreach (GameDayViewModel gameDay in newSchedule.GamesDays)
{
GameDay newGameDay = new GameDay()
{
GamesDay = gameDay.GamesDay
};
// leagueSessionSchedule.GamesDays.Add(newGameDay);
// create game time entry for every game day
foreach (GameTimeViewModel gameTime in gameDay.GamesTimes)
{
GameTime newGameTime = new GameTime()
{
GamesTime = DateTimeOffset.FromUnixTimeSeconds(gameTime.GamesTime).DateTime.ToLocalTime(),
// GameDayId = newGameDay.Id
};
// newGameTime = await this._sessionScheduleRepository.AddGameTimeAsync(newGameTime, ct);
newGameDay.GamesTimes.Add(newGameTime);
}
leagueSessionSchedule.GamesDays.Add(newGameDay);
}
// update teams sessions
foreach (TeamSessionViewModel teamSession in newSchedule.TeamsSessions)
{
// retrieve the team with the corresponding id
Team team = await this._teamRepository.GetByIdAsync(teamSession.TeamId, ct);
if(team != null)
{
TeamSession newTeamSession = new TeamSession()
{
Team = team,
LeagueSessionSchedule = leagueSessionSchedule
};
leagueSessionSchedule.TeamsSessions.Add(newTeamSession);
}
}
// update matches for this session
foreach (MatchViewModel match in newSchedule.Matches)
{
Match newMatch = new Match()
{
DateTime = match.DateTime,
HomeTeamId = match.HomeTeam.Id,
AwayTeamId = match.AwayTeam.Id,
LeagueID = match.LeagueID
};
leagueSessionSchedule.Matches.Add(newMatch);
}
try
{
leagueSessionOperations.Add(await this._sessionScheduleRepository.AddScheduleAsync(leagueSessionSchedule, ct));
}
catch(Exception ex)
{
}
}
// ensure all leagueSessionOperations did not return any null values
return leagueSessionOperations.All(op => op != null);
}
`
This is not a many-to-many relationship.
It is two separate one-to-many relationships, which happen to refer to the same table on one end of the relationship.
While it is true that on the database level, both use cases are represented by three tables, i.e. Foo 1->* FooBar *<-1 Bar, these two cases are treated differently by Entity Framework's automated behavior - and this is very important.
EF only handles the cross table for you if it is a direct many-to-many, e.g.
public class Foo
{
public virtual ICollection<Bar> Bars { get; set; }
}
public class Bar
{
public virtual ICollection<Foo> Foos { get; set; }
}
EF handles the cross table behind the scenes, and you are never made aware of the existence of the cross table (from the code perspective).
Importantly, EF Core does not yet support implicit cross tables! There is currently no way to do this in EF Core, but even if there were, you're not using it anyway, so the answer to your problem remains the same regardless of whether you're using EF or EF Core.
However, you have defined your own cross table. While this is still representative of a many-to-many relationship in database terms, it has ceased to be a many-to-many relationship as far as EF is concerned, and any documentation you find on EF's many-to-many relationships no longer applies to your scenario.
Unattached but indirectly added objects are assumed to be new.
By "indirectly added", I mean you that it was added to the context as part of another entity (which you directly added to the context). In the following example, foo is directly added and bar is indirectly added:
var foo = new Foo();
var bar = new Bar();
foo.Bar = bar;
context.Foos.Add(foo); // directly adding foo
// ... but not bar
context.SaveChanges();
When you add (and commit) a new entity to the context, EF adds it for you. However, EF also looks at any related entities that the first entity contains. During the commit in the above example, EF will look at both the foo and bar entities and will handle them accordingly. EF is smart enough to realize that you want bar to be stored in the database since you put it inside the foo object and you explicitly asked EF to add foo to the database.
It is important to realize that you've told EF that foo should be created (since you called Add(), which implies a new item), but you never told EF what it should do with bar. It's unclear (to EF) what you expect EF to do with this, and thus EF is left guessing at what to do.
If you never explained to EF whether bar already exists or not, Entity Framework defaults to assuming it needs to create this entity in the database.
Saving the new LeagueSessionSchedule object throws an error by Entity Framework Core that it cannot INSERT a duplicate primary key value into the dbo.Teams table. I have no idea why its attempting to add to dbo.Teams table
Knowing what you now know, the error becomes clearer. EF is trying to add this team (which was the bar object in my example) because it has no information on this team object and what its state in the database is.
There are a few solutions here.
1. Use the FK property instead of the navigational property
This is my preferred solution because it leaves no room for error. If the team ID does not yet exist, you get an error. At no point will EF try to create a team, since it doesn't even know the team's data, it only knows the (alleged) ID you're trying to create a relationship with.
Note: I am omitting LeagueSessionSchedule as it is unrelated to the current error - but it's essentially the same behavior for both Team and LeagueSessionSchedule.
TeamSession newTeamSession = new TeamSession()
{
TeamId = team.Id
};
By using the FK property instead of the nav prop, you are informing EF that this is an existing team - and therefore EF no longer tries to (re)create this team.
2. Ensure that the team is tracked by the current context
Note: I am omitting LeagueSessionSchedule as it is unrelated to the current error - but it's essentially the same behavior for both Team and LeagueSessionSchedule.
context.Teams.Attach(team);
TeamSession newTeamSession = new TeamSession()
{
Team = team
};
By attaching the object to the context, you are informing it of its existence. The default state of a newly attached entity is Unchanged, meaning "this already exists in the database and has not been changed - so you don't need to update it when we commit the context".
If you have actually made changes to your team that you want to be updated during commit, you should instead use:
context.Entry(team).State = EntityState.Modified;
Entry() inherently also attaches the entity, and by setting its state to Modified you ensure that the new values will be committed to the database when you call SaveChanges().
Note that I prefer solution 1 over solution 2 because it's foolproof and much less likely to lead to unexpected behavior or runtime exceptions.
String primary keys are undesirable
I'm not going to say that it doesn't work, but strings cannot be autogenerated by Entity Framework, making them undesirable as the type of your entity's PK. You will need to manually set your entity PK values.
Like I said, it's not impossible, but your code shows that you're not explicitly setting PK values:
if(team != null)
{
TeamSession newTeamSession = new TeamSession()
{
Team = team,
LeagueSessionSchedule = leagueSessionSchedule
};
leagueSessionSchedule.TeamsSessions.Add(newTeamSession);
}
If you want your PK's to be automatically generated, use an appropriate type. int and Guid are by far the most commonly used types for this.
Otherwise, you're going to have to start setting your own PK values, because if you don't (and the Id value thus defaults to null), your code is going to fail when you add a second TeamSession object using the above code (even though you're doing everything else correctly), since PK null is already taken by the first entity you added to the table.
Given an ASP.NET Core webapp using Entity Framework Core and an SQL database.
An absolute simple action is throwing this exception when trying to update an entity in the database. First noticed by a bug report in production.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(string id, [Bind("Group")] EditViewModel model)
{
if (id != model.Group.Id) return NotFound();
if (!ModelState.IsValid) return View(model);
_context.Update(model.Group);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
Exception is thrown at the line: _context.Update(model.Group);
InvalidOperationException: The instance of entity type 'Group' cannot be tracked because another instance of this type with the same key is already being tracked. When adding new entities, for most key types a unique temporary key value will be created if no key is set (i.e. if the key property is assigned the default value for its type). If you are explicitly setting key values for new entities, ensure they do not collide with existing entities or temporary values generated for other new entities. When attaching existing entities, ensure that only one entity instance with a given key value is attached to the context.
Clearly there is no other instance. I was able to reproduce the exception in my development environment when I stopped the code with a breakpoint on that line and expanded the Results property of the _context.Group object:
It's understandable, that when expanding the Results, it loads the instance needed to be updated and that's why the exception is thrown. But what's about the deployed production environment?
Thanks for the help!
UPDATE1 Group model:
public class Group
{
[Display(Name = "ID")]
public string Id { get; set; }
public virtual Country Country { get; set; }
[Required]
[Display(Name = "Country")]
[ForeignKey("Country")]
public string CountryCode { get; set; }
[Required]
[Display(Name = "Name")]
public string Name { get; set; }
}
UPDATE2 Based on #Mithgroth's answer, I was able to override the function _context.Update() to not need try-catch every time I use it:
public interface IEntity
{
string Id { get; }
}
public override EntityEntry<TEntity> Update<TEntity>(TEntity entity)
{
if (entity == null)
{
throw new System.ArgumentNullException(nameof(entity));
}
try
{
return base.Update(entity);
}
catch (System.InvalidOperationException)
{
var originalEntity = Find(entity.GetType(), ((IEntity)entity).Id);
Entry(originalEntity).CurrentValues.SetValues(entity);
return Entry((TEntity)originalEntity);
}
}
Use the following instead:
var group = _context.Group.First(g => g.Id == model.Group.Id);
_context.Entry(group).CurrentValues.SetValues(model.Group);
await _context.SaveChangesAsync();
The exception can be caused by many different scenarios but the thing is, you are trying to change the state of an object which is already marked differently.
For instance, this would produce the same exception:
var group = new Group() { Id = model.Id, ... };
db.Update(group);
Or you might have detached N-tier children, that's all possible.
This ensures that you are just overwriting an existing entity's values.
Thanks all for the ideas.
I made this overridden function on my context working without Exception, which is imho a slightly better approach. Also, the primary key name is retrieved using the defined model.
Also, it is EntityFramework 3.0 .NET Core
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using System.Linq;
public override EntityEntry<TEntity> Update<TEntity>(TEntity entity) where TEntity : class
{
if (entity == null)
{
throw new System.ArgumentNullException(nameof(entity));
}
var type = entity.GetType();
var et = this.Model.FindEntityType(type);
var key = et.FindPrimaryKey();
var keys = new object[key.Properties.Count];
var x = 0;
foreach(var keyName in key.Properties)
{
var keyProperty = type.GetProperty(keyName.Name, BindingFlags.Public | BindingFlags.Instance);
keys[x++] = keyProperty.GetValue(entity);
}
var originalEntity = Find(type, keys);
if (Entry(originalEntity).State == EntityState.Modified)
{
return base.Update(entity);
}
Entry(originalEntity).CurrentValues.SetValues(entity);
return Entry((TEntity)originalEntity);
}
I'm using the validation of the Entity Framework which relies on System.ComponentModel.DataAnnotations.ValidationAttributes. So when I call DbContext.SaveChanges() and an entity property fails validation, a DbEntityValidationException is thrown.
What I need to know is which ValidationAttribute exactly caused that validation error. I.e. I need to know the Type of ValidationAttribute causing the DbEntityValidationException within my program.
I already know how to iterate through the collections of validation errors inside the DbEntityValidationException. However the required information is not in there.
Example
Say I have this simple model with a single property which has two data annotations...
class Model
{
[Required]
[MaxLength(3)]
string Code { ... }
}
...and want to add a new instance of it like so:
try
{
var model = new Model { Code = "ThisIsTooLong" };
dbContext.Set<Model>().Add(model);
dbContext.SaveChanges();
}
catch (DbEntityValidationException e)
{
Type unsatisfiedValidationAttribute = MagicFunction();
}
In the above case an DbEntityValidationException is thrown and the variable unsatisfiedValidationAttribute should be equal to typeof(MaxLengthAttribute).
What does MagicFunction() has to do to know whether the Required or the MaxLength annotation triggered the validation error?
I think what you looking for is Validator.TryValidateObject static method:
var modelToSave = new Model { Code = "ThisIsTooLong" };
var results = new List<ValidationResult>();
bool isValid=Validator.TryValidateObject( modelToSave, context, results, true);
In case your entity is not valid you are going to hold each failed validation in results list.
Update
Well, a generic solution to get the ValidationAttribute using the ErrorMessage could be doing this:
public static ValidationAttribute GetAttribute(Type entityType, string property, string errorMessage)
{
var attributes = typeof(entityType)
.GetProperty(property)
.GetCustomAttributes(false)
.OfType<ValidationAttribute>()
.ToArray();
var attribute= attributes.FirstOrDefault(a => a.ErrorMessage == errorMessage);
return attribute;
}
This might help you If you are using Entity Framework 4.1+. You can implement your own validation logic in your entity and return your own error codes for different validation issues.
IValidatableObject is an interface that lives in System.ComponentModel.DataAnnotations. While it is not part of the Entity Framework API, you can still leverage it for server-side validation in your Entity Framework classes. IValidatableObject provides a Validate method that Entity Framework will call during SaveChanges or you can call yourself any time you want to validate the classes.
Configurations such as Required and MaxLength perform validaton on a single field. In the Validate method you can have even more complex logic, for example, comparing two fields.
public class Blog : IValidatableObject
{
public int Id { get; set; }
[Required]
public string Title { get; set; }
public string BloggerName { get; set; }
public DateTime DateCreated { get; set; }
public virtual ICollection<Post> Posts { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Title == BloggerName)
{
yield return new ValidationResult
("Blog Title cannot match Blogger Name", new[] { "Title", “BloggerName” });
}
}
}
Entity Framework Validation
I didn't find any relevant answer here so I will trigger you, thanks in advance :
I have a controller with 2 methods of the Edit action, (I simplified it for better understanding):
MrSaleBeta01.Controllers
{
public class PostsController : Controller
{
private MrSaleDB db = new MrSaleDB();
...
// GET: Posts/Edit/5
public ActionResult Edit(int? id)
{
...
}
// POST: Posts/Edit/5
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit( Post post, int? CategoryIdLevel1, int? CategoryIdLevel2, int? originalCategoryId)
{
...
Category cnew = db.Categories.Find(post.CategoryId);
MoveFromCategory(post, originalCategoryId);
...
db.Entry(post).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
//move post from his old category (fromCategoryId) to a new one (post.CategoryId):
//returns true on success, false on failure.
public bool MoveFromCategory(Post post, int? fromCategoryId)
{
try
{
if (post.CategoryId == fromCategoryId)
return true;
Category cold = null, cnew = null;
if (fromCategoryId!=null)
cold = db.Categories.Find(fromCategoryId);
if (post.CategoryId != 0)
cnew = db.Categories.Find(post.CategoryId);
if (cold != null)
{
cold.Posts.Remove(post);
}
if( cnew != null)
cnew.Posts.Add(post);
db.Entry(cold).State = EntityState.Modified;
db.Entry(cnew).State = EntityState.Modified;
//db.Entry(p).State = EntityState.Modified;
//db.SaveChanges();
return true;
}
catch (Exception)
{
return false;
//throw;
}
}
}
}
So, the idea is very default: The first method is called by Get and returns the View of Edit. Then I need to save the changes by sending the post object from the view to the HttpPost Edit method.
My Model is something like that (I simplified it for better understanding):
MrSaleBeta01.Models
{
public class Post
{
public int Id { get; set; }
[ForeignKey("Category")]
public virtual int CategoryId { get; set; }
public virtual Category Category { get; set; }
}
public class Category
{
public Category()
{
this.Categories = new List<Category>();
this.Posts = new List<Post>();
}
#region Primitive Properties
public int CategoryId { get; set; }
public string Name { get; set; }
#endregion
#region Navigation Properties
public virtual IList<Post> Posts { get; set; }
#endregion
}
}
The idea: Every Post needs to have it's Category. Every Category can have multiple Posts or none. (1-N relationship).
The problem:
In the Edit (HttpPost) method, after I update the Category's objects (move the Post from it's category to a different category object. After that I do some other modifications on post object), I get an error in the line of the edit method:
db.Entry(post).State = EntityState.Modified;
saying that:
{"Attaching an entity of type 'MrSaleBeta01.Models.Post' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate."}
The error is beacuse there is a conflict to the line:
cold.Posts.Remove(post);
And even to the line:
cnew.Posts.Add(post);
I tried to use the solution of AsNoTracking() but without success,
I also tried to change the line "db.Entry(post).State = EntityState.Modified" line to:
db.As.Attach(post)
but that line is even cannot be compiled.
What am I doing wrong? How can I solve that issue?
1) You dont have to call .Attach() nor .State = anything.
You have your Entity created as proxy object (cold = db.Categories.Find(fromCategoryId);), its proxy responsibility to track any changes. As exception say, this COULD be your problem.
2) public int CategoryId { get; set; } should be marked with [Key] (i am not sure if convention mark it as primary key, but i doubt it - i think EF conventions take this PK as FK to Category, which could confuse object graph and behave strangely...)
3) Uh, just noticed... Why are you using your FromCategory method at all? I may overlook something, but looks like it just remove Category from collection and add it to another... EF proxy does this automatically for you, right after post.CategoryId = newCatId;
Edit1:
4) Change public virtual IList<Post> Posts { get; set; } to public virtual ICollection<Post> Posts { get; set; }
Edit2:
1) that was created automatically while I scaffold the PostsController according to the Post model. So I guess I need it?
3) It's not just remove Category from collection and add it to another, but remove the post from the collection of posts in one category to another. So I don't think that EF proxy does this automatically.
I am not famillier with ASP, i work with desktop MVP/MVVM, so i am not sure here - but from my point of view, you really dont need to touch EntityState as long as you are using var x = db.Set<X>().Create(); (== db.X.Create();) (NOT var x = new X();) for new entities and db.Set<X>().FetchMeWhatever(); (== db.X.FetchMeWhatever();) for everything else (Otherwise you get only POCO without proxy. From your example, it looks like you are doing it right ;) ).
Then you have entity with proxy (thats why you have your reference properties on model virtual - this new emitted proxy type override them) and this proxy will take care for 1:n, m:n, 1:1 relations for you. I think this is why folks are using mappers (not only EF and not only DB mappers) mainly :) For me it looks like, you are trying to do this manually and it is unnecessary and its just making a mess.
Proxy also take care of change tracking (so as i say, you dont need to set EntityState manually, only in extreme cases - I can not think of any right now... Even with concurrency.)
So my advice is:
Use only ICollection<> for referencing collections
Check and get rid of any var entity = new Entity(); (as i say, looks like you are doing this)
Throw away every db.Entry(x).State = EntityState.whatever; (trust EF and his change tracker)
Set only one side of reference - it doesnt matter if Category.Posts or Post.Category or even Post.CategoryId - and let mapper do the work. Please note that this will work only with proxy types (as i say above) on entities with virtual referencing & id & ICollection<> properties.
Btw, there are 2 types of change tracking, snippet and proxy - snippet have original values in RAM and is comparing them at SaveChanges() time, for proxy tracking, you need to have all your properties marked virtual - and comparing them at x.Prop = "x" time. But thats off-topic ;)
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.