A 'Required' Collection field could not be updated via `AddOrUpdate` - c#

I'm using Asp.net web api 2 + entity framework 6.
Basically I have 2 models:
public class MyOrderModel
{
public int Id { get; set; }
public string OrderNumber { get; set;}
public string AuthCode { get; set; }
[Required]
public List<MyOrderDetailModel> Details { get; set; }
}
public class MyOrderDetailModel
{
public int Id { get; set; }
public decimal Amount{ get; set;}
}
After ran the Package Manager Console command Enable-Migration, in Configuration.Seed(WaynyCloudTest.Models.ApplicationDbContext context), I was trying to add some pre-loaded data:
context.MyOrderModels.AddOrUpdate(
s => s.OrderNumber,
new MyOrderModel
{
OrderNumber = "0001",
AuthCode = "ABCDE",
Details = new List<MyOrderDetailModel>()
{
new MyOrderDetailModel()
{
Amount = 5.67M
}
}
};
After the first(initial) Update-database command, everything is fine and I can see above data persisted to database 2 tables.
Later, I want to update the AuthCode property value from ABCDE to ABCDEXXX,
the only change is the value assignment:
context.MyOrderModels.AddOrUpdate(
s => s.OrderNumber,
new MyOrderModel
{
OrderNumber = "0001",
// THE ONLY CHANGE!
AuthCode = "ABCDEXXX",
Details = new List<MyOrderDetailModel>()
{
new MyOrderDetailModel()
{
Amount = 5.67M
}
}
};
I would expect the EntityFramework will find the target data row in database by searching condition on OrderNumber and then update the AuthCode, but now I always got this exception in Seed function:
Entity Validation Failed - errors follow:
MyTest.Models.MyOrderModel failed validation
Details : The Details field is required.
Obviously the value was supplied for field Details, so what I've missed?

The problem is with the Id field of PostPayQRCodeFuelOrderModel. In your environment, the database uses this field as an identity (primary key) field and wants to generate the value itself.
In your case, there is an easy workaround:
context.MyOrderModel.AddOrUpdate(
p => p.OrderNumber,
new PostPayQRCodeFuelOrderModel
{
OrderNumber = "00001",
Details = new List<MyOrderDetailModel>()
{
new MyOrderDetailModel()
{
Amount = 5.67M
}
}
}
);
Assumably, OrderNumber is unique, so this should work fine. In addition, running the seed again will not duplicate this data.
UPDATE:
It is possible to keep the original MyOrderModel.AddOrUpdate(), i.e., give the Id explicitly:
First, you need to prevent the auto-generation of the primary key value for MyOrderModel:
public class MyOrderModel
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int Id { get; set; }
Then, you need to re-create the MyOrderModel table. NOTE: the usual approach of adding a migration just to modify the Id field will not work, you need to re-create the table.
Running the seed multiple times works, i.e., there are no duplicates (just checked it myself).
UPDATE 2:
I don't have a full explanation, why your code is not working, but with the code below it should be possible for you to construct the creation and the update of the database objects as you wish.
This code can be run in Seed(), multiple times without duplication. The update of AuthCode is, of course, artificial, but my point was to separate the creation and the update (just in case these need to be separated in the final implementation).
The whole project is available in https://github.com/masa67/AspNetNg, branch SO34252506-1.
Here's the code:
var mom = context.MyOrderModel.Where(m => m.OrderNumber == "00001").FirstOrDefault();
if (mom == null)
{
mom = new PostPayQRCodeFuelOrderModel
{
OrderNumber = "00001",
AuthCode = "ABCDE",
Details = new List<MyOrderDetailModel>() {
new MyOrderDetailModel
{
Amount = 5.67M
}
}
};
context.MyOrderModel.AddOrUpdate(p => p.OrderNumber, mom);
context.SaveChanges();
}
mom.AuthCode = "ABCDEXXX";
context.SaveChanges();
UPDATE 3:
A couple of suggestions, if this is still not working:
Consider dropping the Required constraint for Details and handle the consistency programmatically. I would not use this constraint on navigation properties anyways (but I am only familiar with EF to the extent of how we are using it in our current project at work, so there might be different views on this).
Test your code by re-creating the database first.
Test my Solution (link above). It is working for me, so there might be a difference in configuration somewhere.
Since Details is not virtual, EF is not using lazy loading. I was expecting this to cause problems, as Details becomes null when the object is read from the database, but that was not the case in my environment. You might try eager loading, but I doubt if this has any impact:
Eager loading:
var mom = context.MyOrderModel.Where(m => m.OrderNumber == "00001").Include(m => m.Details).FirstOrDefault();
UPDATE 4:
If this is still not working, then delete the database, but re-create the migrations in addition:
Delete the existing migrations.
Do not let EF make the assumption that it knows the state of your database, but force it to create the migrations from scratch (see other SO questions for advice). However, BEFORE doing that, please do notice that this operation will most probably overwrite the Seed() function as well, so take a copy of that file before the operation.

Related

EF Core Attach/AttachRange methods not working

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;
}

Entity framework not detecting jsonb properties changes in c#

I am using Entity Framework Core with npgsql postgresql for Entity Framework Core.
and i'm working with .net core 3
My question is, when i try to update a MyTableRelated element from the MyTableClass and saving the context to the database, no changes are detected.
For example, lets suppose we have the following classes:
public class MyTableClass
{
public int Id { get; set; }
[Column(TypeName = "jsonb")]
public virtual List<MyTableRelated> Data { get; set; }
}
public class MyTableRelated
{
public int Id { get; set; }
public string prop1 { get; set; }
public string prop2 { get; set; }
}
and some code like this (this is not actual code, its just to get the ideia):
var context = dbContext;
var newMyTableClass = new MyTableClass() {
Id = 1;
};
var newMyTableRelated = new MyTableRelated(){
Id=1;
prop1 = "";
prop2 = "";
}
newMyTableClass.Data.Add(newMyTableRelated);
context.SaveChanges();
This works, and the entry is saved on the database.
Now somewhere on the application, i want to access that entry and change values on Data:
var context = dbContext;
var updateMyTableClass = context.MyTableClass.FirstOrDefault(x => x.Id == 1);
var tableRelated = updateMyTableClass.Data.FirstOrDefault(y => y.Id == 1);
tableRelated.prop1 = "prop1";
tableRelated.prop2 = "prop2";
context.SaveChanges();
I would suppose this would change values on database, like it does for other types of properties. But nothing happens.
A solution i found, was using this:
var entry = context.Entry(updateMyTableClass);
if (entry.State == EntityState.Unchanged)
{
entry.State = EntityState.Modified;
}
This is more of a temporary solution for that case.
How can we then make the EF automatically detect changes on jsonb properties?
Someone pointed to me that i should look at coase grained lock.
https://www.martinfowler.com/eaaCatalog/coarseGrainedLock.html
How can something like that be implemented?
Automatic change detection would mean that EF Core would take a snapshot of the JSON document when it loads the property (duplicating the entire tree), and then do a complete structural comparison of the original and current tree whenever SaveChanges is called. As this can be very heavy perf-wise, it is not done by default.
However, if you wish to do so, you can create a value comparer to implement precisely this - see the EF docs on how to do that. I've opened an issue on the Npgsql provider repo in case someone wishes to contribute this.
For perf reasons, I'd recommend manually flagging properties when they change, similar to what you have done. Note that you're marking the entire entity instance as changed - so all properties will be saved. You can use the following to only mark the JSON property:
ctx.Entry(entry).Property(e => e.SomeJsonProperty).IsModified = true;

Why is Entity Framework Core attempting to insert records into one of the tables from many to many relationships and NOT the join table?

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.

Entity framework attach: object with same key exists

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();

Entity Framework - Migrations - Code First - Seeding per Migration

I am looking into Migrations in an effort to clean up our deployment processes. The less manual intervention required when pushing a change to production the better.
I have run into 3 major snags with the migrations system. They are show stoppers if I can not figure out a clean way around them.
1. How do I add Seed data per migration:
I execute the command "add-migration" which scaffolds a new migration file with Up and Down functions. Now, I want to automatically make changes to the data with both Up and Down changes. I don't want to add the Seed data to the Configuration.Seed method as this runs for all migrations which ends in all sorts of duplication problems.
2. If the above is not possible, how do I avoid duplications?
I have an enum that I loop through to add the values to the database.
foreach(var enumValue in Enum.GetValues(typeof(Access.Level)))
{
context.Access.AddOrUpdate(
new Access { AccessId = ((int)enumValue), Name = enumValue.ToString() }
);
}
context.SaveChanges();
Even though I am using AddOrUpdate, I still get duplicates in the database. The above code brings me to my 3rd and final problem:
3. How can I seed Primary Keys?
My enumerable with the above code is:
public class Access
{
public enum Level
{
None = 10,
Read = 20,
ReadWrite = 30
}
public int AccessId { get; set; }
public string Name { get; set; }
}
I am specifying the values that I want as my primary key, but Entity Framework seems to ignore it. They still end up being 1,2,3. How do I get it to be 10,20,30?
Are these limitations of EF at the moment or are they intentional constraints to prevent some other kind of catastrophe I am not seeing?
When I have fixed data that I want to insert with a migration, I put the inserts directly in the Up() migration using calls to Sql("Insert ..."). See the note halfway down this page: how to insert fixed data
You prevent duplicates in the Seed method by calling the AddOrUpdate overload that takes an identifier expression specifying the natural key - see this answer and this blog entry.
Primary keys that are integers are created as identity fields by default. To specify otherwise use the [DatabaseGenerated(DatabaseGeneratedOption.None)] attribute
I think this is a good explanation of Initializer and Seed methods
Here is an example of how to use the AddOrUpdate method:
foreach(var enumValue in Enum.GetValues(typeof(Access.Level)))
{
context.Access.AddOrUpdate(
x => x.Name, //the natural key is "Name"
new Access { AccessId = ((int)enumValue), Name = enumValue.ToString() }
);
}
As a possible solution to item 1, I made an implementation of the IDatabaseInitializer strategy which will run the Seed method of each pending migration only, you will need to implement a custom IMigrationSeed interface in each of your DbMigration classes, the Seed method will then be implemented right after Up and Down methods of every migration class.
This helps to solve two problems for me:
Group Database Model Migration with Database Data Migration (or Seeding)
Check what part of the Seed migration code should really be running, not checking data in the database but using already known data which is the database model that was just created.
The interface looks like this
public interface IMigrationSeed<TContext>
{
void Seed(TContext context);
}
Below is the new implementation that will call this Seed method
public class CheckAndMigrateDatabaseToLatestVersion<TContext, TMigrationsConfiguration>
: IDatabaseInitializer<TContext>
where TContext : DbContext
where TMigrationsConfiguration : DbMigrationsConfiguration<TContext>, new()
{
public virtual void InitializeDatabase(TContext context)
{
var migratorBase = ((MigratorBase)new DbMigrator(Activator.CreateInstance<TMigrationsConfiguration>()));
var pendingMigrations = migratorBase.GetPendingMigrations().ToArray();
if (pendingMigrations.Any()) // Is there anything to migrate?
{
// Applying all migrations
migratorBase.Update();
// Here all migrations are applied
foreach (var pendingMigration in pendingMigrations)
{
var migrationName = pendingMigration.Substring(pendingMigration.IndexOf('_') + 1);
var t = typeof(TMigrationsConfiguration).Assembly.GetType(
typeof(TMigrationsConfiguration).Namespace + "." + migrationName);
if (t != null
&& t.GetInterfaces().Any(x => x.IsGenericType
&& x.GetGenericTypeDefinition() == typeof(IMigrationSeed<>)))
{
// Apply migration seed
var seedMigration = (IMigrationSeed<TContext>)Activator.CreateInstance(t);
seedMigration.Seed(context);
context.SaveChanges();
}
}
}
}
}
The good thing here is you have a real EF context to manipulate Seed Data, just like standard EF Seed implementation. However this can get strange if for example you decide to delete a table that was Seeded in a previous migration, you will have to refactor your existing Seed code accordingly.
EDIT:
As an alternative to implement the seed method after the Up and Down, you can create a partial class of the same Migration class, I found this useful as it allows me to safely delete the migration class when I want to re-seed the same migration.
Hi I have found a very useful information for your problem in this link:
Safari Books Online
"1. How do I add Seed data per migration:"
As you see in the example you need to create a new confiugration for seeding.
This seed Configuration must be called after migration.
public sealed class Configuration : DbMigrationsConfiguration
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
}
protected override void Seed(SafariCodeFirst.SeminarContext context)
{
// This method will be called after migrating to the latest version.
// You can use the DbSet<T>.AddOrUpdate() helper extension method
// to avoid creating duplicate seed data. E.g.
//
// context.People.AddOrUpdate(
// p => p.FullName,
// new Person { FullName = "Andrew Peters" },
// new Person { FullName = "Brice Lambson" },
// new Person { FullName = "Rowan Miller" }
// );
//
}
}
"2. If the above is not possible, how do I avoid duplications?"
AddOrUpdate Must help you exactly to avoding the duplicates if you get an error here you might have a configuration error post the call stack please. See the example!
"3. How can I seed Primary Keys?"
Here it is also on your key definition. If your key DatabaseGenerated(DatabaseGeneratedOption.Identity) than you do not have to provide it. In some other senarios you need to create a new one it is depending on the key type.
"Are these limitations of EF at the moment or are they intentional constraints to prevent some other kind of catastrophe I am not seeing?"
Not that I know!
OK, so with a bit of bashing I have managed to bash EF into submission.
Here is what I did:
1. There is no way that I found to see data for a specific migration. It all must go into the common Configuration.Seed method.
2. To avoid duplicates I had to do 2 things.
For my enums, I wrote the following seed code:
foreach (var enumValue in Enum.GetValues(typeof(Access.Level)))
{
var id = (int)enumValue;
var val = enumValue.ToString();
if(!context.Access.Any(e => e.AccessId == id))
context.Access.Add(
new Access { AccessId = id, Name = val }
);
}
context.SaveChanges();
So basically, just checking if it exists and adding if not
3. In order for the above to work, you need to be able to insert Primary Key Values. Luckily for me this table will always have the same static data so I could deactivate the auto increment. To do that, the code looks like:
public class Access
{
public enum Level
{
None = 10,
Read = 20,
ReadWrite = 30
}
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int AccessId { get; set; }
public string Name { get; set; }
}

Categories

Resources