Multi-select lists in MVC don't seem to bind to complex models. Instead, they return an array of selected Id numbers.
I have such a control on a page, and I'm annoyed by the amount of conditional logic I've had to deploy to get it to work. The objects in question are a Staff object who can have TeamMember membership of none or more teams.
My objects are from entity framework. I added this to the Staff object:
public int[] SelectedTeamMembers
{
get; set;
}
I can now bind to this property in my View, and users can edit the multiselect list. On posting back the edit form, I have to do this (comments added for clarity):
//user.TeamMembers not bound, so get existing memberships
IEnumerable<TeamMember> existingTeamMembers = rep.TeamMembers_Get().Where(t => t.UserId == user.UserID);
//if array is empty, remove all team memberships & avoid null checks in else
if(user.SelectedTeamMembers == null)
{
foreach(TeamMember tm in existingTeamMembers)
{
rep.TeamMembers_Remove(tm);
}
}
else
{
// if team members have been deleted, delete them
foreach (TeamMember tm in existingTeamMembers)
{
if (!user.SelectedTeamMembers.Contains(tm.TeamId))
{
rep.TeamMembers_Remove(tm);
}
}
// if there are new team memberships, add them
foreach (int i in user.SelectedTeamMembers)
{
if (!existingTeamMembers.Select(t => t.TeamId).Contains(i))
{
TeamMember tm = new TeamMember { TeamId = i, UserId = user.UserID };
rep.TeamMembers_Change(tm);
}
}
}
I can tidy this up a bit by farming out each bit to a function, of course, but it still feels like a sledgehammer to crack a nut.
Is there a neater way of achieving this?
You should evaluate the possibility of combining your for and foreach loops into a single loop as the first step of simplifying this code.
Also, you know how to use LINQ (as evidenced by you initial Where() statement) so simplify the null conditional action as well, using LINQ and some of its helper extensions:
//user.TeamMembers not bound, so get existing memberships
IEnumerable<TeamMember> existingTeamMembers = rep.TeamMembers_Get().Where(t => t.UserId == user.UserID);
//if array is empty, remove all team memberships & avoid null checks in else
if(user.SelectedTeamMembers == null)
{
existingTeamMembers.ToList().ForEach(tm => rep.TeamMembers_Remove(tm));
}
else
{
// if team members have been deleted, delete them
existingTeamMembers.Where(tm => !user.SelectedTeamMembers.Contains(tm.TeamId)).ToList().ForEach(tm => rep.TeamMembers_Remove(tm));
// if there are new team memberships, add them
user.SelectedTeamMembers.Except(existingTeamMembers.Select(t=> t.TeamId)).ToList().ForEach(i =>
{
TeamMember tm = new TeamMember { TeamId = i, UserId = user.UserID };
rep.TeamMembers_Change(tm);
});
}
While this has not decreased the conditional complexity (as in all the conditionals are still there) the syntax is a lot more readable.
You could do it this way...It relies on using the RemoveRange method.
Entity - I'm using my own for demo purposes
public class User
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public String Name { get; set; }
}
Action
public ActionResult Action(Guid[] selectedTeamMembers)
{
using (var ctx = new DatabaseContext())
{
//
// Start by targeting all users!
//
var usersToRemove = ctx.Users.AsQueryable();
//
// if we have specified a selection then select the inverse.
//
if (selectedTeamMembers != null)
{
usersToRemove = usersToRemove.Where(x => !selectedTeamMembers.Contains(x.Id));
}
//
// Use the Set Generic as this gives us access to the Remove Range method
//
ctx.Set<User>().RemoveRange(usersToRemove);
ctx.SaveChanges();
}
return View();
}
Hope this helps.
Related
Take the following EF Class:
public class Person
{
public int ID { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public IEnumerable<Property> Property { get; set; }
}
public class Property
{
public int ID { get; set; }
public string Name { get; set; }
public bool Lock { get; set; }
public Person Person { get; set; }
public int PersonID
}
I can pretty much make everything work as expected - including a delete action for Person that also deletes all their property. However, as my code gets more complicated, I want to make the logic slightly more advanced.
In the above example, we have something elsewhere that will set the bool lock for property. In this case, I want to disable delete on person when any property for that person has a lock of true.
The default Delete controller code has:
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
var person = await _context.People
.FirstOrDefaultAsync(m => m.ID == id);
if (person== null)
{
return NotFound();
}
return View(person);
}
And the Delete confirm has:
public async Task<IActionResult> DeleteConfirmed(int id)
{
var person= await _context.people.FindAsync(id);
_context.people.Remove(person);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
I know the code to do what I want is:
foreach (var item in person.Property)
{
if item.locked==true
return("error")
}
Now the fun stars! - The old EF4 virtual keyword on properties I'm used to doesn't work - so, I can't iterate over the property because it is currently null. in most instances, I have to call .include()
On the first delete, this modifies:
var person = await _context.People
.FirstOrDefaultAsync(m => m.ID == id);
to
var person = await _context.People.Include(x=>x.property)
.FirstOrDefaultAsync(m => m.ID == id);
which seems to work fine.
However, the second one:
var person = await _context.people.FindAsync(id);
doesn't seem to work. The moment I put the .Include in, it states error CS1061 that there is no definition for FindAsync.
In all honesty, I am not too sure what the need is for two different ways of looking at an ID in the first place... I can only assume that when looking for an ID in the first delete that may not exist, firstordefault is the best and when confirming a delete, find is best.... however, this is what the scaffolding does and I don't feel I know enough to question this.
I however want to be a better developer and would love to understand what is wrong with the code and for future, how do I know what can be combined and what can't as I don't feel I am learning here, I am just randomly trying different things until I find one combination that works.
A few things:
I'd consider checking whether the person is Locked before enabling a Delete button, or immediately on clicking the delete button rather than on confirming a delete.
With this code:
var person = await _context.People
.FirstOrDefaultAsync(m => m.ID == id);
if (person== null)
return NotFound();
return View(person);
Entities should represent data state, not view state. Returning entities to the view will lead to problems. If lazy loading is supported/enabled this can trigger performance issues when lazy-loads get triggered by serialization, it can also lead to errors due to cyclical references. It exposes more information about your data structure, and data in general that the client does not need (more data over the wire and more information for hackers). Instead, leverage a ViewModel POCO class containing just the data your view needs and use .Select() to populate it.
Next, avoid the crutch of FirstOrDefault. This can lead to unintended bugs remaining hidden. If there is 1 entity expected, use Single or SingleOrDefault. Your application should handle exceptions gracefully and impartially. If someone sends an invalid ID, fail and terminate the session. Basically do not trust the client not to tamper.
var person = await _context.People
.Select(x => new PersonViewModel
{
PersonId = x.ID,
Name = x.FirstName + " " + x.LastName,
// etc.
}).SingleAsync(x => x.ID == id);
return View(person);
When checking data state on the confirm, you receive and ID and want to confirm before issuing the delete, you can query the required detail, but then for a delete you don't need the entire entity provided you trust the ID. Something like this isn't needed:
foreach (var item in person.Property)
{
if item.locked==true
return("error")
}
Instead:
var isLocked = context.People.Where(x => x.ID == id)
.Select(x => x.Property.Any(p => p.isLocked))
.Single();
This will throw if the person ID isn't found, and return a Bool True of False if any of the Property entries for that person are locked.
From there you can use a simple "trick" to delete an entity without first loading it:
if (!isLocked)
{
var person = new Person { ID = id };
context.People.Attach(person);
context.People.Remove(person);
context.SaveChanges();
}
Alternatively if you want to load the Person to have access to other properties, such as to create an audit record or may want to display info anyways as part of the error message, then you can substitute the above examples with:
var personData = context.People.Where(x => x.ID == id)
.Select(x => new
{
Person = x,
IsLocked = x.Property.Any(p => p.isLocked))
}).Single();
if (!personData.isLocked)
{
context.People.Remove(personData.Person);
context.SaveChanges();
}
I have two tables, Clients and Physicians. Each Client has a list of primary keys for their physicians (PhysicianIds), which is flattened to a comma-delimited string for database storage (PhysicianStore).
One of my action methods requires finding which client a physician belongs to, but I can't find a way to do this without evaluating the entire table with .ToList(). The below code doesn't work because .Split() doesn't work with LINQ-to-Entities.
Do I need to add a foreign key to Physician?
// Data model / DTO
public class ClientModel
{
public List<int> PhysicianIds { get; set; }
public string PhysicianStore
{
get { return string.Join(",", PhysicianIds.ConvertAll<string>(i => i.ToString())); }
set { PhysicianIds = value.Split(',').Select(str => int.Parse(str)).ToList(); }
//set { PhysicianIds = value.Split(',').ToList().ConvertAll<int>(str => int.Parse(str)); }
}
}
public class PhysiciansController
{
// Disposed in full code
private MyDbContext db = new MyDbContext();
public async Task<ActionResult> Details(int? id)
{
if (id == null)
{
return HttpError(HttpStatusCode.BadRequest);
}
PhysicianModel pm = await db.Physicians.FindAsync(id);
if (pm == null)
{
return HttpError(HttpStatusCode.NotFound);
}
// Have to use PhysicianStore here because PhysicianIds is not a column in the DB
return View(new PhysicianDetailsViewModel(pm, db.Clients.Where(c => c.PhysicianStore.Split(',').Contains(pm.Id.ToString()))
.FirstOrDefault()?.Name));
}
}
Edit: Per comments, I really should be using navigation properties. I'll pursue this and ask another question if needed.
Ignoring the database design, the main issue as I understand is that LINQ to Entities does not support string.Split. But string.Concat, string.Contains and ToString are supported, so you can use the following trick:
var token = "," + pm.Id.ToString() + ",";
var query = db.Clients
.Where(c => ("," + c.PhysicianStore + ",").Contains(token));
The trick is to enclose with "," both the search and target terms. This way it handles correctly the start, end and middle elements of the list, and will not produced false positives when searching for let say "1" inside "12,21".
I have 2 managed objects in my database that look like this.
public class Product : RealmObject
{
public int Id { get; set; }
public string Name { get; set; }
public string Date { get; set; }
public RealmList<Report> Reports { get; } // child objects
}
and
public class Report : RealmObject
{
public int Id { get; set; }
public string Ref { get; set; }
public string Date { get; set; }
public Product Parent { get; set; } // Parent object reference
}
Each time my app is loaded a web hit fetches a list of Products and then starts managing them in a Realm database, it displays the Products in a TableView. When you click one of the products in the table view, you get a list of Reports. The list of reports is fetched by another web hit using the product id. Every time I get a new list of reports from the web I need to remove all the old Report objects from the Realm database that are linked to one specific product (by id).
Herein lies the confusion. According to this https://realm.io/docs/xamarin/latest/#current-limitations cascading deletes is currently not supported. I assume that means deleted objects in a relationship like I have above. So for the time being what is the best approach to remove the child objects (RealmList) without breaking things. I have come up with 2 approaches so far. Here's some code.
Approach A:
// id is passed in as a param by function
var reportsById = realm.All<Report>.Where(r => r.Product.Id == id).ToList();
foreach (var report in reportsById)
{
// Delete an object with a transaction
using (var trans = realm.BeginWrite())
{
realm.Remove(report);
trans.Commit();
}
}
// Then simply add the new reports to my old Product
// Pseudo code
var newreports = getnewreports()
foreach report in newreports
product.Reports.add(report)
Approach B:
// Get the current Product object
var currentProduct = realm.All<Product>.Where(p => p.Id == id).ToList().FirstOrDefault();
foreach (var report in currentProduct.Reports)
{
// Delete an object with a transaction
using (var trans = realm.BeginWrite())
{
realm.Remove(report);
trans.Commit();
}
}
// Add new reports to product again
And finally this is the approach I used to add my child objects (reports from the web) to the parent (product).
// First
var webReports = await FetchWebReport(); // IList<Report> type
/...../
// Then
var currentProduct = Realm.blah()... // get from realm database with query
foreach (var report in webReports)
{
// Manage object with a transaction
using (var trans = realm.BeginWrite())
{
// Add reference to parent product
report.Parent = currentProduct;
// Add to child list in product
currentProduct.Reports.Add(report);
trans.Commit();
}
}
Has anybody got any ideas/input? Feel free to to pick apart my current code. Point out issues. Thanks Realm Devs. =)
Official Realm answer - you were nearly right with B ;-)
Note the following sample uses the Write(lambda) style rather than explicit transaction creation and commit. It's a bit more concise but doing the same work.
I'm also looping inside the transaction rather than doing many transactions. It's faster and means the collection of related updates are in a single transaction.
create some sample hierarchies
realm.Write (() => {
for (var pid = 1; pid <= 4; ++pid) {
var p = realm.CreateObject<Product>();
p.Id = pid;
p.Name = $"Product {pid}";
for (var rid = 1; rid <= 5; ++rid) {
var r = realm.CreateObject<Report>();
r.Id = rid+pid*1000;
r.Ref = $"Report {pid}:{rid}";
p.Reports.Add(r); // child object added to relationship
}
}
});
Do the delete
Find an object we want to do a psuedo-cascading delete on - I'm directly using the LINQ First to get the object.
var delId = 1;
var delP = realm.All<Product>().First(p => p.Id == delId);
if (delP == null)
return;
Important fix to your sample - use ToList
realm.Write(() => {
foreach (var r in delP.Reports.ToList())
realm.Remove(r);
realm.Remove(delP); // lastly remove the parent
});
Your approach in B was nearly correct but it ignores the fact that foreach (var report in currentProduct.Reports) is iterating a live list. Because the Reports container is updated each time you remove something, it will exit the loop before removing all the children.
I think I'm having a senior moment, but I also think I have not run into this situation before. I have two columns in my MVC5 Identity 2.1 Users table.
UserId | BannedBy (and also an IsBanned bool)
Both fields are userid guid strings. However, BannedBy refers to a different user in the same Users table.
When I display my view of banned users (a table and each row is one banned user), I don't want to show the BannedBy guid, I want to show the related UserName for that BannedBy guid. However, I can't seem to figure out what I need to do.
I've tried a ViewModel and method approach:
public ActionResult BannedUsers()
{
var bannedUsers = db.Users.Where(d => d.IsBanned);
var model = new BannedUsersViewModel
{
BannedUsers = bannedUsers,
BannedByUserName = GetUserName(bannedUsers.BannedBy)
};
return View(model);
}
Then like an outer approach to my viewmodel:
var model = new BannedUsersViewModel
{
BannedUsers = bannedUsers
};
model.BannedByUserName = GetUserName(model.bannedUsers.BannedBy);
However, it seems I can't use the bannedUsers.BannedBy (I also tried all that above with a capital B... BannedUsers.BannedBy) data before it's actually been rendered? And now I've scrapped the viewmodel and am trying to do like a related data join on my query:
db.Users.Join(d => d.BannedBy == d.UserId).Where(d => d.IsBanned);
(I'm sure this is way off, I'm just trying to give you an idea)
Does anyone know the proper way of doing this? I was also thinking about calling a method from my view, but seems like that would be breaking the MVC rules?
Thank you.
Update: Here is the GetUserName method:
public string GetUserName(string userId)
{
var result = db.Users.Find(userId);
return result.UserName;
}
Update #2: Here is the BannedUsersViewModel:
public class BannedUsersViewModel
{
public IEnumerable<ApplicationUser> BannedUsers { get; set; }
public string BannedByUserName { get; set; }
}
Update #3: A pic:
I am going to take a stab at this. We can modify it as needed (unless I am completely off base, in which case, I will delete this and we will all pretend it never happened). Does this get you in the ballpark:
public ActionResult BannedUsers()
{
var bannedUsers =
db.Users
.Where(d => d.IsBanned)
.Join(
db.Users,
bannee => bannee.BannedBy,
banner => banner.UserId,
(bannee, banner) => new BannedUser()
{
BannedByUserName = banner.UserName,
BannedUser = bannee
})
.AsEnumerable();
var model = new BannedUsersViewModel
{
BannedUsers = bannedUsers
};
return View(model);
}
public class BannedUsersViewModel
{
public IEnumerable<BannedUser> BannedUsers { get; set; }
}
public class BannedUser
{
public ApplicationUser BannedUser { get; set; }
public string BannedByUserName { get; set; }
}
The idea is that we get all of the banned users, join them to all of the users that banned those users and then group by the user that banned them. You end up with a collection of objects that have the user that banned other users and the users they banned.
So far, I went with #lazy's comment in doing a self join. I'm not sure how good I feel about it though. So essentially, I added this to my IdentityModels.cs under public class ApplicationUser : IdentityUser:
[ForeignKey("BannedBy")]
public virtual ApplicationUser BannedByUser { get; set; }
Then in my controller, changed my original query to a list:
var bannedUsers = db.Users.Where(d => d.IsBanned).ToList();
return View(bannedUsers);
(If I don't convert to list, it complains about having more than one data reader open.) Then in my View in my foreach loop:
#Html.DisplayFor(modelItem => item.BannedByUser.UserName)
And boom:
I'm a little worried of the performance impact?...especially for a page that isn't used that often and is not really that important. Is there an impact if the page isn't being called (like with the Index that was created and such)? I'm also a little leery since there seems to be some magic happening with Identity.
Anyway, I'm still open to other ideas...or thoughts about this one. Thanks again for everyone's help.
What is the correct way to save a graph of objects whose state you don't know? By state I mean whether they are new or existing database entries that are being updated.
For instance, if I have:
public class Person
{
public int Id { get; set; }
public int Name { get; set; }
public virtual ICollection<Automobile> Automobiles { get; set; }
}
public class Automobile
{
public int Id { get; set; }
public int Name { get; set; }
public short Seats { get; set; }
public virtual ICollection<MaintenanceRecord> MaintenanceRecords { get; set ;}
public virtual Person Person { get; set; }
}
public class MaintenanceRecord
{
public int Id { get; set; }
public int AutomobileId { get; set; }
public DateTime DatePerformed { get; set; }
public virtual Automobile Automobile{ get; set; }
}
I'm editing models, similar to these objects above, and then passing those models into the data layer to save, where for this instance I happen to be using entity framework. So I'm translating these models into POCO entities internal to the DAL.
It appears that unless my models have a state indicating whether they are new or updated, I have quite a bit of work to do to "Save" the changes. I have to first select the Person entity, update it, then match any existing Automobiles and update those and add any new, then for each automobile check for any new or updated maintenance records.
Is there a faster/easier way of doing this? It's possible I can keep track of the Model state, which I guess would be helpful with this, but it would mean changes to code outside of the data layer which i would prefer to avoid. I'm just hoping there is a pattern of usage out there that I can follow for updates like this.
I ran into this issue a while back and have been following this thread on the EF Codeplex site. https://entityframework.codeplex.com/workitem/864
Seems like it is being considered for the next release, I'm assuming EF 7, which apparently is a pretty large internal overhaul of EF. This may be worth checking out... http://www.nuget.org/packages/RefactorThis.GraphDiff/
Back when I was working on this I found another EF post on SO, and someone had an example of how to do this manually. At the time I decided to do it manually, not sure why, GraphDiff looks pretty cool. Here is an example of what I did.
public async Task<IHttpActionResult> PutAsync([FromBody] WellEntityModel model)
{
try
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var kne = TheContext.Companies.First();
var entity = TheModelFactory.Create(model);
entity.DateUpdated = DateTime.Now;
var currentWell = TheContext.Wells.Find(model.Id);
// Update scalar/complex properties of parent
TheContext.Entry(currentWell).CurrentValues.SetValues(entity);
//We don't pass back the company so need to attached the associated company... this is done after mapping the values to ensure its not null.
currentWell.Company = kne;
// Updated geometry - ARGHHH NOOOOOO check on this once in a while for a fix from EF-Team https://entityframework.codeplex.com/workitem/864
var geometryItemsInDb = currentWell.Geometries.ToList();
foreach (var geometryInDb in geometryItemsInDb)
{
// Is the geometry item still there?
var geometry = entity.Geometries.SingleOrDefault(i => i.Id == geometryInDb.Id);
if (geometry != null)
// Yes: Update scalar/complex properties of child
TheContext.Entry(geometryInDb).CurrentValues.SetValues(geometry);
else
// No: Delete it
TheContext.WellGeometryItems.Remove(geometryInDb);
}
foreach (var geometry in entity.Geometries)
{
// Is the child NOT in DB?
if (geometryItemsInDb.All(i => i.Id != geometry.Id))
// Yes: Add it as a new child
currentWell.Geometries.Add(geometry);
}
// Update Surveys
var surveyPointsInDb = currentWell.SurveyPoints.ToList();
foreach (var surveyInDb in surveyPointsInDb)
{
// Is the geometry item still there?
var survey = entity.SurveyPoints.SingleOrDefault(i => i.Id == surveyInDb.Id);
if (survey != null)
// Yes: Update scalar/complex properties of child
TheContext.Entry(surveyInDb).CurrentValues.SetValues(survey);
else
// No: Delete it
TheContext.WellSurveyPoints.Remove(surveyInDb);
}
foreach (var survey in entity.SurveyPoints)
{
// Is the child NOT in DB?
if (surveyPointsInDb.All(i => i.Id != survey.Id))
// Yes: Add it as a new child
currentWell.SurveyPoints.Add(survey);
}
// Update Temperatures - THIS IS A HUGE PAIN = HOPE EF is updated to handle updating disconnected graphs.
var temperaturesInDb = currentWell.Temperatures.ToList();
foreach (var tempInDb in temperaturesInDb)
{
// Is the geometry item still there?
var temperature = entity.Temperatures.SingleOrDefault(i => i.Id == tempInDb.Id);
if (temperature != null)
// Yes: Update scalar/complex properties of child
TheContext.Entry(tempInDb).CurrentValues.SetValues(temperature);
else
// No: Delete it
TheContext.WellTemperaturePoints.Remove(tempInDb);
}
foreach (var temps in entity.Temperatures)
{
// Is the child NOT in DB?
if (surveyPointsInDb.All(i => i.Id != temps.Id))
// Yes: Add it as a new child
currentWell.Temperatures.Add(temps);
}
await TheContext.SaveChangesAsync();
return Ok(model);
}
catch (Exception ex)
{
Trace.WriteLine(ex.Message);
}
return InternalServerError();
}
This is a huge pain to me too. I extracted the answer from #GetFuzzy to a more reusable method:
public void UpdateCollection<TCollection, TKey>(
DbContext context, IList<TCollection> databaseCollection,
IList<TCollection> detachedCollection,
Func<TCollection, TKey> keySelector) where TCollection: class where TKey: IEquatable<TKey>
{
var databaseCollectionClone = databaseCollection.ToArray();
foreach (var databaseItem in databaseCollectionClone)
{
var detachedItem = detachedCollection.SingleOrDefault(item => keySelector(item).Equals(keySelector(databaseItem)));
if (detachedItem != null)
{
context.Entry(databaseItem).CurrentValues.SetValues(detachedItem);
}
else
{
context.Set<TCollection>().Remove(databaseItem);
}
}
foreach (var detachedItem in detachedCollection)
{
if (databaseCollectionClone.All(item => keySelector(item).Equals(keySelector(detachedItem)) == false))
{
databaseCollection.Add(detachedItem);
}
}
}
With this method in place I can use it like this:
public void UpdateProduct(Product product)
{
...
var databaseProduct = productRepository.GetById(product.Id);
UpdateCollection(context, databaseProduct.Accessories, product.Accessories, productAccessory => productAcccessory.ProductAccessoryId);
UpdateCollection(context, databaseProduct.Categories, product.Categories, productCategory => productCategory.ProductCategoryId);
...
context.SubmitChanges();
}
However when the graph gets deeper, I have a feeling this will not be sufficient.
What your looking for is the Unit of Work pattern:
http://msdn.microsoft.com/en-us/magazine/dd882510.aspx
You can either track UoW on the client and pass it in with the DTO or have the server figure it out. Both the veritable DataSet and EF Entities have their own internal implementation of UoW. For something stand alone there is this framework, but I have never used it so have no feedback:
http://genericunitofworkandrepositories.codeplex.com/
Alternatively another option is to do real time updates with undo functionality, kind of like when you go into Gmail contacts and it saves the changes as you make them with the option to undo.
It depends HOW you are accomplishing adding/changing the entities.
I think you may be trying to do too much with an entity at any given time. Allowing editing and adding at the same time can get you into a situation where your not sure what is being done with the entity, especially in a disconnected scenario. You should only perform a single action on a single entity at a time, unless you are deleting entities. Does this seem monotonous, sure, but 99% of your users want a clean and easily understandable interface. Many time we end up making screens of our applications "god" screens where everything and anything can be done. Which 9/10 times isn't needed (YAGNI).
This way, when you edit a user, you know you are doing an update operation. If you are adding a new maintenance record, you know you are creating a new record that is attached to an automobile.
To summarize, you should limit how many operations you are making available for a single screen and make sure you provide some type of unique information for the entity so you can try to look up the entity to see if it exists.
I had the similar problem, and couldnt find my own solution. I think that problem is complex. Complete solution for updating graphs in disconected scenario with EF6 I find in extension method RefactoringThis.GraphDiff produced by Brent McKendric.
Exemple brings by author is:
using (var context = new TestDbContext())
{
// Update the company and state that the company 'owns' the collection Contacts.
context.UpdateGraph(company, map => map
.OwnedCollection(p => p.Contacts, with => with
.AssociatedCollection(p => p.AdvertisementOptions))
.OwnedCollection(p => p.Addresses)
);
context.SaveChanges();
}
See more at:
http://blog.brentmckendrick.com/introducing-graphdiff-for-entity-framework-code-first-allowing-automated-updates-of-a-graph-of-detached-entities/