EF Core - many-to-many: how to add middle entity when one entity does not yet exist? - entity-framework-core

I am curios if I can make this easier/better.
The user is able to create new tickets and with each ticket can be several files associated, one file can be related to multiple tickets.
Now, when the user creates a ticket, he can already add files. Meaning I have no Id for the ticket and thus no way to build a relation. How should I solve this?
public class FilesPerTicket
{
public int TickedId;
public Ticket Ticket;
public int FileId;
public File File;
}
public class File
{
public ICollection<FilesPerTicket> FilesperTicket;
}
public class Ticket
{
public ICollection<FilesPerTicket> FilesperTicket;
}
Now the user creates the ticket and adds several files to it.
public IActionResult Create(MyModel model)
{
// .....
var filesPerTicket = model.Files.Select(x => new FilesPerTicket() { FileId = x.Value }).ToList();
var newTicket = new Ticket() { //...... };
newTicket.FilesPerTicket = filesPerTicket;
// ....
context.Add(newTicket);
context.SaveChanges();
}
This doesn't work because we haven't provided a Ticketid and therefore every TicketId in FilesPerTicket is 0.
I know that I just can save the ticket and afterwards it will have the primary key in it. Which I then can use to fill the FilesPerTicket and save that one.
But then I would have those in separate transactions (because SaveChanges was called) and use another try/catch.
Is there an way to tell EF Core to fill this TicketId automatically when this entity gets saved at the same time as a ticket? Or some other way to save that entity?

Related

Issues with Automatically setting created and modified date on each record in EF Core

Using ASP.NET Core 2.2 with EF Core, I have followed various guides in trying to implement the automatic creation of date/time values when creating either a new record or editing/updating an existing one.
The current result is when i initially create a new record, the CreatedDate & UpdatedDate column will be populated with the current date/time.
However first time I edit this same record, the UpdatedDate column is then given a new date/time value (as expected) BUT for some reason, EF Core is wiping out the value of the original CreatedDate which results in SQL assigning a default value.
Required result I need as follows:
Step 1: New row created, both CreatedDate & UpdatedDate column is given a date/time value (this already works)
Step 2: When editing and saving an existing row, I want EF Core to update the UpdatedDate column with the updated date/time only, BUT leave the other CreatedDate column unmodified with the original creation date.
I'm using EF Core code first, and do no want to go down the fluent API route.
One of the guides i was partially following is https://www.entityframeworktutorial.net/faq/set-created-and-modified-date-in-efcore.aspx but neither this or other solutions I've tried is giving the result I am after.
Baseclass:
public class BaseEntity
{
public DateTime? CreatedDate { get; set; }
public DateTime? UpdatedDate { get; set; }
}
DbContext Class:
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken))
{
var entries = ChangeTracker.Entries().Where(E => E.State == EntityState.Added || E.State == EntityState.Modified).ToList();
foreach (var entityEntry in entries)
{
if (entityEntry.State == EntityState.Modified)
{
entityEntry.Property("UpdatedDate").CurrentValue = DateTime.Now;
}
else if (entityEntry.State == EntityState.Added)
{
entityEntry.Property("CreatedDate").CurrentValue = DateTime.Now;
entityEntry.Property("UpdatedDate").CurrentValue = DateTime.Now;
}
}
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
}
UPDATE FOLLOWING ADVICE FROM STEVE IN COMMENTS BELOW
I spent a bit more time debugging today, turns out the methods I posted above are appear to be functioning as expected i.e. when editing an existing row and saving it, only the entityEntry.State == EntityState.Modified IF statement is being called. So what I'm finding is that after saving the entity, the CreatedDate column is being overwitten with a Null value, I can see this by watching the SQL explorer after a refresh. I believe the issue is along the lines of what Steve mentions below "If it is #null then this might also explain the behavior in that it is not being loaded with the entity for whatever reason."
But i'm a little lost in tracing where this CreatedDate value is being dropped somewhere through edit/save process.
Image below shows the result at the point just before the entity is saved following an update. In the debugger I'm not quite sure where to find the entry of the CreatedDate to see what value is held at this step, but it appears to be missing from the debugger list so wandering whether somehow it doesn't know about the existence of this field when saving.
Below is the method I have in my form 'Edit' Razor page model class:
public class EditModel : PageModel
{
private readonly MyProject.Data.ApplicationDbContext _context;
public EditModel(MyProject.Data.ApplicationDbContext context)
{
_context = context;
}
[BindProperty]
public RuleParameters RuleParameters { get; set; }
public async Task<IActionResult> OnGetAsync(int? id)
{
if (id == null)
{
return NotFound();
}
RuleParameters = await _context.RuleParameters
.Include(r => r.SystemMapping).FirstOrDefaultAsync(m => m.ID == id);
if (RuleParameters == null)
{
return NotFound();
}
ViewData["SystemMappingID"] = new SelectList(_context.SystemMapping, "ID", "MappingName");
return Page();
}
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Attach(RuleParameters).State = EntityState.Modified;
try
{
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!RuleParametersExists(RuleParameters.ID))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToPage("./Index");
}
private bool RuleParametersExists(int id)
{
return _context.RuleParameters.Any(e => e.ID == id);
}
}
Possibly one of the reasons for this issue is the fact that I have not included the CreatedDate field in my Edit Razor Page form, so when I update the entity which in turn will run the PostAsync method server side, there is no value stored for the CreatedDate field and therefore nothing in the bag by the tine the savechangesasync method is called in my DbContext Class. But I also didn't think this was necessary? otherwise I'd struggle to see what value there is in the this process of using an inherited BaseEntity class i.e. not having to manually add the CreatedDate & UpdatedDate attribute to every model class where I want to use it...
It may be easier to just give your BaseEntity a constructor:
public BaseEntity()
{
UpdatedDate = DateTime.Now;
CreatedDate = CreatedDate ?? UpdatedDate;
}
Then you can have your DbContext override SaveChangesAsync like:
public override Task<int> SaveChangesAsync(
bool acceptAllChangesOnSuccess,
CancellationToken token = default)
{
foreach (var entity in ChangeTracker
.Entries()
.Where(x => x.Entity is BaseEntity && x.State == EntityState.Modified)
.Select(x => x.Entity)
.Cast<BaseEntity>())
{
entity.UpdatedDate = DateTime.Now;
}
return base.SaveChangesAsync(acceptAllChangesOnSuccess, token);
}
Possibly one of the reasons for this issue is the fact that I have not included the CreatedDate field in my Edit Razor Page form, so when I update the entity which in turn will run the PostAsync method server side, there is no value stored for the CreatedDate field and therefore nothing in the bag by the tine the savechangesasync method is called in my DbContext Class.
That's true.Your post data does not contains the original CreatedDate,so when save to database, it is null and could not know what the exact value unless you assign it before saving.It is necessary.
You could just add below code in your razor form.
<input type="hidden" asp-for="CreatedDate" />
Update:
To overcome it in server-side,you could assign data manually:
public async Task<IActionResult> OnPostAsync()
{
RuleParameters originalData = await _context.RuleParameters.FirstOrDefaultAsync(m => m.ID == RuleParameters.ID);
RuleParameters.CreatedDate = originalData.CreatedDate;
_context.Attach(RuleParameters).State = EntityState.Modified;
await _context.SaveChangesAsync();
}
I don't suspect EF is doing this, but rather your database, or you're inadvertently inserting records instead of updating them.
A simple test: Put break-points in your SaveChangesAsnc method within both the Modified and Added handlers and then run a unit test that loads an entity, edits it, and saves. Which breakpoint is hit? If the behavior seems to be normal with a simple unit test, repeat again with your code.
If the Modified breakpoint is hit, and only the Modified handler is hit then check the state of the CreatedDate value in the entity modified. Does it still reflect the original CreatedDate? If yes, then it would appear that something in your schema will be overwriting it on save. If no then you have a bug in your code that has caused it to update. If it is #null then this might also explain the behaviour in that it is not being loaded with the entity for whatever reason. Check that the property has not been configured as something like a Computed property.
If the Added breakpoint is hit at all, then this would point at a scenario where you're dealing with a detached entity, such as an entity that was read from a different DB Context and being associated to another entity in the current DB Context and saved as a byproduct. When a DbContext encounters an entity that was loaded and disassociated with a different DbContext, it will treat that entity as a completely new entity and insert a new record. The biggest single culprit for this is invariably MVC code where people pass entities to/from views. Entity references are loaded in one request, serialized to the view, and then passed back on another request. Devs assume they are receiving an entity that they can just associate to a new entity and save, but the Context of this request doesn't know about that entity, and that "entity" isn't actually an entity, it is now a POCO shell of data that the serializer created. It's no different to you newing up a new class and populating fields. EF won't know the difference. The result of this is you will trip the Added condition for your entity, and after completion you will have a duplicate record. (with different PK if EF is configured to treat PKs as Identity)
So an example is an Order screen: When presenting a screen to create a new order I may have loaded the Customer and passed that to the view to display customer information and will want to associate to the new order:
var customer = context.Customers.Single(x => x.CustomerId == 15);
var newOrder = new Order { Customer = customer };
return View(newOrder);
This looks innocent enough. When we go to save the new order after setting their details:
public ActionResult Save(Order newOrder)
{
context.Orders.Add(newOrder);
newOrder.Customer.Orders.Add(newOrder);
context.SaveChanges();
// ...
}
newOrder had a reference to Customer #14, so all looks good. We're even associating the new order to the customer's order collection. We might even want to have updated fields on the customer record to reflect a change to the Modified date. However, newOrder in this case, and all associated data including .Customer are plain 'ol C# objects at this point. We've added the new order to the Context, but as far as the context is concerned, the Customer referenced is also a new record. It will ignore the Customer ID if that is set as an Identity column and it will save a brand new Customer record (ID #15 for example) with all of the same details as Customer ID 14 and associate that to the new order. It can be subtle and easy to miss until you start querying Customers and spotting duplicate looking rows.
If you are passing entities to/from views, I'd be very wary of this gotcha. Attaching and setting modified state is one option, but that involves trusting that the data has not been tampered with. As a general rule, calls to update entities should never pass entities & attach them, but rather re-load those entities, validate row version, validate the data coming in, and only copy across fields you expect should ever be modified before saving the entity associated to the DbContext.
Hopefully that gives you a few ideas on things to check to get to the bottom of the issue.

Relations between complex type not getting updated

So with entity framework I'm trying to update two existing entities.
There I've the main object something like:
public class MainObject
{
public string Name { get; set; }
public virtual SmallObject Part { get; set;}
}
public class SmallObject
{
public string Name { get; set; }
}
In the repository I first check if the SmallObject already exists in the database by:
MainObject.Part = (from s in repoSmallObject.GetAll()
where s.name == MainObject.Part.Name
select s).FirstOrDefault();
Then finally I call the update method in my GenericRepository
repoMainObject.Update(MainObject)
which is defined as a generic repository method:
dbSet.Attach(entity)
context.Entry(entity).State = EntityState.Modified;
context.SaveChanges();
But the relationship doesn't get updated. Why is that? Both objects are attached to context not?
*Edit: The two repo's are injected with the same Context.
And strangely enough the Add method works and also updates the relationship.
When you set
context.Entry(entity).State = EntityState.Modified;
you need at least to set the state after and before updates (i.e. context.Entry(mainObject).CurrentValues and OriginalValues) so EF can build the right UPDATE query (with right WHERE clause).
It works if you set
context.Entry(entity).State = EntityState.Added;
because EF needs just to generate an INSERT query.
I don't know exactly why you need it but usually I prefer to attach the object to the DbSet and modify the properties so EF handles various states.
dbSet.Attach(MainObject)
MainObject.Part = (from s in repoSmallObject.GetAll()
where s.name == MainObject.Part.Name
select s).FirstOrDefault();
(In your case does not work because MainObject.Part.Name does not change)
The attached object should have the same values of the database otherwise you have a concurrency exception.
BTW, why you don't read the old object (MainObject) from the DB than work on it???

Entity Framework 6 - How do you add an entity with foreign key IdentityRole

I have a code first model with a mapping table so that I can map MenuItem to an IdentityRole, enabling the production of a menu based on the logged in users role assignment.
public class MenuItem
{
public int Id { get; set; }
public string Text { get; set; }
}
public class MenuRoleMap
{
[Key]
public int Id { get; set; }
public virtual MenuItem MenuItem { get; set; }
public virtual IdentityRole Role { get; set; }
}
The IdentityRole and rest of Identity is auto wired via IdentityDbContext which I've inherited through my ApplicationDbContext like this, then the context should be consistent.
public class ApplicationDbContext : IdentityDbContext<User>
All of the tables look right, they have the expected columns and foreign keys, here is the MenuRoleMap table
I have an valid existing instance of MenuItem and IdentityRole which I use to try and add a new entity item to this table
foreach (IdentityRole role in selectedRoles)
{
MenuRoleMap mrm = new MenuRoleMap();
mrm.MenuItem = menuItem;
mrm.Role = role;
db.MenuRoleMaps.Add(mrm);
}
db.SaveChanges(); /// <<<=== HERE ERROR BECAUSE THE role IS ALREADY IN DB
Which throws this error
A first chance exception of type 'System.Data.Entity.Validation.DbEntityValidationException' occurred in EntityFramework.dll
Role: Role SystemsAdministrator already exists.
Which of course, it does exist, I know that, it's already in the database. Surely the EF should not be trying to add a new entity item for the foreign key entity if it already exists?
It doesn't do it for the MenuItem, only the IdentityRole.
I thought the problem was proxy creation since the IdentityRole was a proxy object, so I turned that off
this.Configuration.ProxyCreationEnabled = false;
but I still get the same error.
My question is, how do you add an entity where the foreign key is an IdentityRole?
Thank you stackers.
ANSWER TO MY OWN QUESTION
After assistance from those below I discovered while investigating the various solutions that the problem wasn't one of context per se but the validity of an object. The object looked right, what I hadn't realised is that it wasn't the object from the context, it was a facsimile. By trying to add this facsimile to the model, the context quite rightly says it already exists, you can't add it again. By trying to override the state of the item I created a different kind of error.
The resolution was simply to reload the object from the context and then add that to the parent item like so
foreach (IdentityRole role in selectedRoles)
{
// Here I'm getting the role from the context using the ID I have from the facsimile
IdentityRole roleToUse = db.Roles.Where(x => x.Id == role.Id).FirstOrDefault();
// carry on as normal
MenuRoleMap mrm = new MenuRoleMap();
mrm.MenuItem = menuItem;
mrm.Role = roleToUse; // note I'm using the retrieved 'roleToUse'
db.MenuRoleMaps.Add(mrm);
}
db.SaveChanges();
Hey presto it all works.
It looks like you've retrieved the entity from another context and then assigned it to an entity which is then added to a different context. It would then try to insert the Role entity too.
Are you returning the Role from another method where the lifetime of the context is scoped to that method?
You may find the following link useful in regards to updating the state of objects:
Entity states and SaveChanges
The problem is that when you use db.Set<MyEntity>.Add you will mark all entities that are attached to the entity that is added as being added too. You have to explicitly mark them as being unchanged:
foreach (IdentityRole role in selectedRoles)
{
MenuRoleMap mrm = new MenuRoleMap();
mrm.MenuItem = menuItem;
mrm.Role = role;
db.MenuRoleMaps.Add(mrm);
db.Entry(role).State=EntityState.Unchanged;
}
db.SaveChanges();
My answer is you can't or at least should not.
Authentication (Roles), and Business (Menu) are different concerns of the application.
For me you have to bring in the ApplicationDb, the part of IdentityDb that you need and organize the synchronisation.
To illustrate my saying: Imaging you use Google or LiveID as authentification provider: can you imagine navigation properties from you ApplicationDd to Google or Microsoft Dbs ?
Clearly not.
So create a AppRole replicating the Role of the authentication database and use this table from your application database to build your menus.
In pseudo code this looks like:
List<Int32> l = IdentityContext.GetRolesForUser(currentUserId);
foreach (AppRole role in AppContext.Roles.Where(r => l.Contains(r.Id)))
{
MenuRoleMap mrm = new MenuRoleMap();
mrm.MenuItem = menuItem;
mrm.Role = role;
appContext.MenuRoleMaps.Add(mrm);
}
appContext.SaveChanges();
Another solution would be to use the same context for Application and Identity.
Inheritance of context seems fine, but I never tested it.

How can I prevent EF from inserting an object that already exists in the db when adding one that contains this first one?

It's quite self-explainatory.
I have a class that contains another
Let's call them Subject and Classroom
public class Subject
{
public Classroom Class {get; set;}
}
I'm using stateless facades, wich means my DbContext is disposed right after recovering the objects and is created to store the new ones.
Shouldn't it know that Classroom isn't a new object since it's ID is already in the DB?
Using the debugger I can track to the point right before I call the SaveChanges method and Classroom.id is the one I have on the database.
What's the problem? EF adds a new Classroom with the exact properties as the previous one, but with a new PK.
What am I doing wrong here?
This is the code used for the general CRUD operations (They are in my DbContext) Both update and delete work just fine:
public void Update(DbSet MySet, object Obj)
{
MySet.Attach(Obj);
var Entry = this.Entry(Obj);
Entry.State = EntityState.Modified;
this.SaveChanges();
}
public void Insert(DbSet MySet, object Obj)
{
MySet.Add(Obj);
this.SaveChanges();
}
public void Delete(DbSet MySet, object Obj)
{
MySet.Attach(Obj);
var Entry = this.Entry(Obj);
Entry.State = EntityState.Deleted;
this.SaveChanges();
}
Without seeing you're actual code on how you're either updating or creating your Subject entity, it's hard to tell. However, you're probably not attaching the Classroom so EF is assuming that the entity is new, when it's really not.
using (Model m = new Model())
{
m.Subject.Add(subject);
m.Classrooms.Attach(subject.Class);
m.SaveChanges();
}
Even though the PK is the same, without attaching to the Context, EF has no way of figuring out what you're intention is. Attaching the entity explicitly tells your context what you want.

Unable to save many-to-many relationship

I have the following tables/views
Users (View)
-> UserId
-> Name
Roles (Table)
-> RoleId
-> Name
UserRoles (Table)
-> UserId
-> RoleId
and the classes
public class Role{
public int RoleId{get;set}
public string Name{get;set}
}
public class User{
public int UserId{get;set}
public string Name{get;set}
public ICollection<Role> Roles{get;set}
}
and the save method
using (var helper = new DbContext())
{
helper.Users.Attach(user);
helper.SaveChanges();
}
As you can see above, the Users is a view and UserRoles is a mapping table. I am able to retrieve User entities along with the mapped Roles. But while saving it is not throwing any exceptions nor is it saving. I tried checking the db using profiler and it is not even hitting the db.
Since Users is a view I don't want to save the User entity but only the changes made in the Roles collection.
This cannot save anything. Attach puts the whole object graph into the context, but in state Unchanged. If all objects in the context are unchanged SaveChanges won't issue any command to the DB (because for EF nothing has changed).
If you want to make changes which EF recognizes as such you must first attach the object which represents the state in the Db and then make your changes, something like:
using (var helper = new DbContext())
{
helper.Users.Attach(user);
helper.Roles.Attach(myNewRole);
user.Name = myNewName;
user.Roles.Add(myNewRole);
// etc.
helper.SaveChanges();
}
Alternatively you can mark the user as modified:
helper.Entry(user).State = EntityState.Modified;
But I believe this only affects scalar properties of the entity and it doesn't solve the problem to add a new role to the user.