I have the following Entity Data models, simplified for brevity;
public abstract class Entity<T> : BaseEntity, IEntity<T>
{
[Key]
public virtual T Id { get; set; }
}
public class User : Entity<int>
{
public List<Category> Categories { get; set; }
}
public class Category : Entity<int>
{
public string Description { get; set; }
}
public class List : Entity<int>
{
public Category Category { get; set; }
}
These are accessed from the DbContext using a DbContext exposed by either a generic service, or a more customised implementation to provide business logic.
When I publish the database and add the following code to the Seed() method, all is well and the data looks good directly in the database.
var user = new User
{
Email = "",
Categories = new List<Category>
{
new Category
{
Description = "Category 1",
},
new Category
{
Description = "Category 2",
}
}
};
context.Users.AddOrUpdate(u => u.Email, user);
var list = new List()
{
Id = 1,
Description = "Test List",
UserId = 1,
Category = user.Categories.FirstOrDefault()
};
context.Lists.AddOrUpdate(u =>u.Id, list);
Please note that the User owns the categories and you can (should only be able to) create them by accessing the Categories Property.
This gives me;
I am using these objects in my controller as such;
public ActionResult Edit(int id)
{
var categories = _usersService.GetUser(User.Id).Categories;
categories.Insert(0, new Category {Description = "", Id = 0});
var list = _listsService.GetList(id);
var viewModel = new EditViewModel
{
Id = list.Id,
Reference = list.Reference,
Description = list.Description,
CategoryId = list.Category?.Id ?? 0,
Categories = new SelectList(categories.AsEnumerable(), "Id", "Description")
};
return View(viewModel);
}
In the above test, I am using the List inserted during the Seed and I can see the List does indeed have a Category, and the values are correct.
For information, I am using the following ViewModel. I have been investigation methods to be able to 'select' the User.Categories from a DropDown and this appeared to work the best at present.
public class EditViewModel
{
[Required]
public int Id { get; set; }
[Required]
public string Description { get; set; }
public IEnumerable<SelectListItem> Categories { get; set; }
[ScaffoldColumn(false)]
public int CategoryId { get; set; }
[ScaffoldColumn(false)]
public Guid Reference { get; set; }
}
The populated ViewModel looks like this;
and finally, the POST method;
[HttpPost]
public ActionResult Edit(EditViewModel model)
{
if (ModelState.IsValid)
{
var categories = _usersService.GetUser(User.Id).Categories;
var list = _listsService.GetList(model.Id);
list.Description = model.Description;
list.Category = categories.FirstOrDefault(x => x.Id == model.CategoryId);
_listsService.Update(list);
categories.Insert(0, new Category { Description = "", Id = 0 });
model.Categories = new SelectList(categories.AsEnumerable(), "Id", "Description");
return View(model);
}
return View(model);
}
So, in the following scenarios, this is what happens. For clarity, each time I am doing this, I go back to the Lists Index and GET Edit again;
Select '' from the Dropdown - NO Categories INSERT,UPDATE on Lists table only, setting [Category_Id] = NULL - Correct
SELECT 'Category 1' from DropDown. INSERT categories, UPDATE lists - NOT Correct
The code being used to update the List is;
public void Update(T entity)
{
if (entity == null) throw new ArgumentNullException(nameof(entity));
_context.Entry(entity).State = EntityState.Modified;
_context.SaveChanges();
}
Now, I know this is something I am doing, but being new to EF, I have no idea.
The problem was down to how the values were being set.
I needed to set the Foreign Key and assign the value to this.
public class List : Entity<int>
{
public int CategoryId { get; set; }
[ForeignKey("CategoryId")
public Category Category { get; set; }
}
and the value was then set as
var list = _listsService.GetList(model.Id);
list.Description = model.Description;
list.CategoryId = model.CategoryId;
list.Category = null;
_listsService.Update(list);
After this, when getting the list from the repository, both the Category and CategoryID would be populated correctly.
The issue was down the setting the Entity as modified, this internally indicated that the Category was 'new' when in fact it was not. You could also 'attach' and existing category to the entity/context but decided the method above was better.
A slightly better approach to the above would be to create a new 'UpdateList' method which could be called rather than the generic update. This method would perform the setting of the relevant properties outside of the controller method.
I am not sure but possible you must write your update method as follow:
public void Update(T entity)
{
if (entity == null) throw new ArgumentNullException(nameof(entity));
var item = _collection.Find(item.Id);
if (item == null) throw new ArgumentNullException(nameof(item ));
_context.Entry(item).CurrentValues.SetValues(entity);
_context.SaveChanges();
}
The problem can be in view, of you don't have the I'd the EF will add new record instead of update the existing record.
In general: Watch out for existing classes when you name your custom class (like List):
public class List : Entity<int>
{
public Category Category { get; set; }
}
Be shoure that you use the right class in the right namespace.
var user = new User
{
Email = "",
Categories = new YourOwnNamespace.List<Category>
{
new Category
{
Description = "Category 1",
},
new Category
{
Description = "Category 2",
}
}
};
Avoid naming classes and properties to existing names. Beter change List class in e.g. 'MyList'.
Related
I'm using vs2017 with entityframework 6.1.0 and winforms . As for entityframework i use code-first. I need to make a movie app, I have made all classes for them Movie, Genre and Cast(actors).
All Genres are pre inserted in the database. When using update-database everything is created including joining tables moviegenre and movie cast and also foreignkeys. When i insert a movie object. it links the id's from genre cast and movies but it also reinserts every genre which means i have duplicates. I only want the linking of course. So far this is my code.
movie class:
public class Movie
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int MovieId { get; set; }
[Required]
[StringLength(255)]
public string Name { get; set; }
//[ForeignKey("GenreId")]
public virtual List<Genre> Genre { get; set; }
//[ForeignKey("CastId")]
public virtual List<Cast> cast { get; set; }
[Required]
public int GenreId { get; set; }
[Required]
public int CastId { get; set; }
[Display(Name = "Release Date")]
public DateTime ReleaseDate { get; set; }
public Movie()
{
Genre = new List<Genre>();
cast = new List<Cast>();
}
}
Genre and cast (the same for both classes)
public class Genre
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int GenreId { get; set; }
[Required]
[StringLength(255)]
public string Name { get; set; }
public List<Movie> Movies { get; set; }
}
and of course my code (this piece of code is from the button click event to add a movie into the db.):
private void btnAddMovie_Click(object sender, EventArgs e)
{
List<Genre> genrelist = new List<Genre>();
List<Cast> castlist = new List<Cast>();
var movie = new Movie();
movie.Name = txtTitle.Text;
movie.ReleaseDate = released;
//creating lists
foreach (string item in lbgenre2.Items)
{
var genre = new Genre();
genre.Name = item;
genrelist.Add(genre);
}
foreach (string item in lbCast2.Items)
{
var cast = new Cast();
cast.Name = item;
castlist.Add(cast);
}
movie.Genre = genrelist;
movie.cast = castlist;
_context.movies.Add(movie);
Save();
}
private async void Save()
{
await _context.SaveChangesAsync();
}
What am I doing wrong that it links and reinserts it?
Your problem is because you are creating the Generes again, and again every time you add a new movie and you have a Identity Key, so, no exception will throw.
foreach (string item in lbgenre2.Items)
{
//Here is your problem, you are creating a new Genere instead of using the already created
var genre = new Genre();
genre.Name = item;
genrelist.Add(genre);
}
So, instead of creating, use ef to get the existing ones
foreach (string item in lbgenre2.Items)
{
//try to get the genere from database
var genre = _context.generes.FirstOrDefault(x => x.Name == item);
//if it doesn't exist..
if(genre == null)
{
//Create it
genre = new Genre();
//And set the name
genre.Name = item;
}
//but, if it already exist, you dont create a new one, just use the existing one
genrelist.Add(genre);
}
Entity Framework cannot figure out if a Genre or Cast already exists, even if you make an instance of one of them, with identical properties to one which exists in the database. Instead you need to get the existing genres and cast from the database and apply them. And only create a new instance, if it is a completely new genre or cast, which is not in the database:
Pseudo-code:
SaveMovie(Movie movie)
{
var existingGenres = GetGenresFromDatabase();
var movieGenres = GetGenresFromListBox();
foreach (var genre in movieGenres)
{
if (genre in existingGenres)
{
existingGenre = existingGenres.Where(genreId = genre.Id);
movie.Genres.Add(existingGenre)
}
else
{
movies.Add(genre)
}
}
dbcontext.Add(movie);
dbcontext.SaveChanges();
}
I'm trying to send data with a view and print it out, but I'm struggling really hard since I'm very new to C#.
So here's my model ViewModel:
namespace P104.Models
{
public class ViewModel
{
}
public class Location
{
public int loc_id { get; set; }
public string loc_name { get; set; }
}
public class Competentie
{
public int Comp_id { get; set; }
public string competentie { get; set; }
}
public class MyViewModel
{
public User User { get; set; }
public IEnumerable<Locations> Locations { get; set; }
public IEnumerable<Competenties> Competenties { get; set; }
}
}
This is the function I have in the controller
public ActionResult Details(int? id)
{
if (id == null)
{
return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
}
User user = db.user.Find(id);
if (user == null)
{
return HttpNotFound();
}
var competenties =
from usercomp in dbE.UserComp
join comp in dbE.Competenties on usercomp.fk_Comp_id equals comp.comp_id
where usercomp.fk_user_id == id
select new Competenties { competentie = comp.competentie };
var locations =
from userloc in dbE.UserLoc
join loc in dbE.Locations on userloc.fk_location_id equals loc.loc_id
where userloc.fk_user_id == id
select new Locations { loc_name = loc.loc_name };
var model = new MyViewModel
{
User = user,
Locations = locations.ToList(), // eagerly fetch the data that will be needed in the view
Competenties = competenties.ToList(), // eagerly fetch the data that will be needed in the view
};
return View(model);
}
And he's how I try to print it out in the view:
#foreach (var location in Model.Locations)
{
<dt>#location.locname</dt>
<dd>#location.locname</dd>
}
#foreach (var competentie in Model.Competenties)
{
<dt>#competentie.competentie</dt>
<dd>#competentie.competentie</dd>
}
I always recevie this error
The entity or complex type 'P104.Models.Locations' cannot be
constructed in a LINQ to Entities query.
I've found a few solutions, but I'm struggling to apply them to my code so they don't work.
Thanks in advance for your help
You seem to have got a typo here:
select new Locations { loc_name = loc.loc_name };
This should be:
select new Location { loc_name = loc.loc_name };
The model you are projecting against is called Location, not Locations. It's unclear what the Locations model is since you haven't shown that in your question.
And of course adapt your view accordingly:
#foreach (var location in Model.Locations)
{
<dt>#location.loc_name</dt>
<dd>#location.loc_name</dd>
}
By the way I will recommend you following standard C# naming conventions. This convention dictates that in C# a property name should start with an uppercase letter and not contain _. So basically you would rather use LocationName or just Name instead of loc_name. Same remark for your Competentie model.
I have an object with a child collection as such:
public class ClaimGroup : BaseModel
{
public int Id { get; set; }
[Required,StringLength(100)]
public string Name { get; set; }
public int Order { get; set; }
public bool Include { get; set; }
public virtual ICollection<ClaimGroupItem> Items { get; set; }
}
The ClaimGroupItem is:
public class ClaimGroupItem : BaseModel
{
[Key,Column(Order = 0),DatabaseGenerated(DatabaseGeneratedOption.None)]
public int ClaimGroupId { get; set; }
[ForeignKey("ClaimGroupId")]
public virtual ClaimGroup ClaimGroup { get; set; }
[Key,Column(Order = 1),DatabaseGenerated(DatabaseGeneratedOption.None)]
public int MenuItemId { get; set; }
[ForeignKey("MenuItemId")]
public virtual MenuItem MenuItem { get; set; }
public string ClaimValue { get; set; }
}
As you can see, it has a composite primary key: MenuItemId and ClaimGroupId.
On updating, it creates duplicates of the ClaimGroupItem objects, with POCO objects being set correctly, but then it creates dynamic proxy items with the same values, hence duplicating the objects.
E.g.:
var items = viewModel.Items.Where(c => !string.IsNullOrEmpty(c.ClaimValue));
The items collection above containts 10 ClaimGroupItemViewModel objects as shown in the image below.
However, when I map the viewModel objects to the Model objects, the collection then has 20 objects, 10 of which are proxy items as seen below:
itemToSave.Items = (from i in items
select new ClaimGroupItem
{
ClaimValue = i.ClaimValue,
MenuItemId = i.MenuItemId,
})
.ToList();
Then when the object goes to save, I get the following error:
_repository.Update<ClaimGroup>(itemToSave);
Violation of PRIMARY KEY constraint 'PK_dbo.ClaimGroupItems'. Cannot
insert duplicate key in object 'dbo.ClaimGroupItems'. The duplicate
key value is (20, 6). The statement has been terminated.
The error makes sense, EF is trying to save 10 duplicate objects.
Why is Entity Framework creating 10 new objects and hence duplicates?
Here is the code on POST that gets the whole list of 79 items in viewModel.Items, then we select just the ones with claimvalue not null. There are NO duplicates at this stage.
[HttpPost, ValidateAntiForgeryToken]
public ActionResult Group([Bind(Include = "Id,Name,Order,Include,Items")] ClaimGroupViewModel viewModel)
{
if (ModelState.IsValid)
{
ClaimGroup itemToSave = _repository.Get<ClaimGroup>(viewModel.Id);
itemToSave.Include = viewModel.Include;
itemToSave.Name = viewModel.Name;
itemToSave.Order = viewModel.Order;
var items = viewModel.Items.Where(c => !string.IsNullOrEmpty(c.ClaimValue));
// There is just 10 items in items variable at this point
itemToSave.Items = (from i in items
select new ClaimGroupItem
{
ClaimValue = i.ClaimValue,
MenuItem = new MenuItem { Id = i.MenuItemId}
})
.ToList();
_repository.Update<ClaimGroup>(itemToSave);
return RedirectToAction("Groups", new { updated = true });
}
return View(viewModel);
}
If you don't need Proxies, during construction of your DBContext set ProxyCreationEnabled = false and check. I have seen cases when we don't even need Proxies and EF by default creates one for all Entities.
I finally got it working.
It was as simple as calling the
itemToSave.Items.Clear()
method to make sure it doesn't load the old proxy objects. What a pain that was to figure out! Here is my working code. Thanks for everyone's help.
[HttpPost, ValidateAntiForgeryToken]
public ActionResult Group([Bind(Include = "Id,Name,Order,Include,Items")] ClaimGroupViewModel viewModel)
{
if (ModelState.IsValid)
{
ClaimGroup itemToSave = _repository.Get<ClaimGroup>(viewModel.Id);
itemToSave.Include = viewModel.Include;
itemToSave.Name = viewModel.Name;
itemToSave.Order = viewModel.Order;
itemToSave.Items.Clear();// This needs to be done, otherwise it will try and load the list from the DB again.
itemToSave.Items = (from i in viewModel.Items
where !string.IsNullOrEmpty(i.ClaimValue)
select new ClaimGroupItem
{
ClaimValue = i.ClaimValue,
MenuItemId = i.MenuItemId,
})
.ToList();
_repository.Update<ClaimGroup>(itemToSave);
return RedirectToAction("Groups", new { updated = true });
}
return View(viewModel);
}
I'm using Entity Framework with MVC 4 to develop a web application. I'm also using a ViewModel named VehicleTypeViewModel which is created like this :
public class VehicleTypeViewModel
{
public VehicleType VehicleType { get; set; }
public ProductType ProductType { get; set; }
[RegularExpression(#"^[a-zA-Zàéèêçñ\s][a-zA-Zàéèêçñ\s-]+$", ErrorMessage = "Invalid name !")]
public string Name { get; set; }
[Range(0, 300)]
public int CO2 { get; set; }
public List<SelectListItem> ProductCompanies { get; set; }
public List<SelectListItem> MotorTypes { get; set; }
}
In my Edit Action, everything's good but one thing : when I debug, arriving to the db.Attach(...) step, my app throws an exception which says :
The object cannot be attached because it is already in the object context. An object can only be reattached when it is in an unchanged state.
Here are my post action :
[HttpPost]
public ActionResult Edit(VehicleTypeViewModel vtvm)
{
ViewBag.Id_VehicleMotorType = new SelectList(db.VehicleMotorTypes, "Id_VehicleMotorType", "Name", vtvm.VehicleType.Id_VehicleMotorType);
ViewBag.Id_ProductCompany = new SelectList(db.ProductCompanies, "Id_ProductCompany", "Name", vtvm.ProductType.Id_ProductCompany);
vtvm.ProductCompanies = db.ProductCompanies.ToList().Select(c => new SelectListItem { Text = c.Name, Value = c.Id_ProductCompany.ToString() }).ToList();
vtvm.MotorTypes = db.VehicleMotorTypes.ToList().Select(v => new SelectListItem { Text = v.Name, Value = v.Id_VehicleMotorType.ToString() }).ToList();
VehicleType vehicleType = db.VehicleTypes.Single(v => v.Id_VehicleType == vtvm.VehicleType.Id_VehicleType);
ProductType productType = db.ProductTypes.Single(p => p.Id_ProductType == vtvm.ProductType.Id_ProductType);
VehicleMotorType vehicleMotorType = null;
ModelStateDictionary errors = Validator.isValid(vtvm.ProductType);
if (ModelState.IsValid)
{
if (errors.Count > 0)
{
ModelState.Merge(errors);
return View(vtvm);
}
productType.Model = vtvm.ProductType.Model;
productType.CatalogPrice = vtvm.ProductType.CatalogPrice;
productType.Id_ProductCompany = vtvm.ProductType.Id_ProductCompany;
if (!string.IsNullOrWhiteSpace(vtvm.Name) && (vtvm.CO2 > 0))
{
vehicleMotorType = new VehicleMotorType()
{
CO2 = vtvm.CO2,
Name = vtvm.Name
};
vehicleType.CO2 = vtvm.VehicleType.CO2;
vehicleType.VehicleMotorType = vehicleMotorType;
vehicleType.Id_ProductType = vtvm.ProductType.Id_ProductType;
}
else
{
vehicleType.CO2 = vtvm.VehicleType.CO2;
vehicleType.Id_ProductType = vtvm.ProductType.Id_ProductType;
vehicleType.Id_VehicleMotorType = vtvm.VehicleType.Id_VehicleMotorType;
}
db.VehicleTypes.Attach(vehicleType);
db.ProductTypes.Attach(productType);
db.ObjectStateManager.ChangeObjectState(vehicleType, EntityState.Modified);
db.ObjectStateManager.ChangeObjectState(productType, EntityState.Modified);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(vtvm);
}
I have no idea why I'm dealing with this kind of error. Any idea to solve this please?
Traffy,
Short answer, you should remove this 2 lines
db.VehicleTypes.Attach(vehicleType);
db.ProductTypes.Attach(productType);
When you retrieve any entity from an EntityFramework instance and modifies any property the Entity Framework is tracking the changes automatically.
To apply the changes to the database you just need to call
db.SaveChanges();
To understand better when to use Add and Attach methods you should read this
Entity Framework 4 - AddObject vs Attach.
I hope it helps.
Just remove the "Attach" as the object is already in the database, no need to attach again, just alter the state and save changes
var comp = (from com in db.tbl_CompGroup where com.Group_Id == GroupID select com).FirstOrDefault();
comp.Survey_Est = surveyPost.Survey_Est;
//db.tbl_CompGroup.Attach(comp);
db.ObjectStateManager.ChangeObjectState(comp, System.Data.EntityState.Modified);
db.SaveChanges();
I have the following poco class:
public class Category : IDisplayName
{
private ICollection<Category> children;
private Category parent;
public Category()
{
children = new List<Category>();
}
public int Id { get; set; }
public string Name { get; set; }
public virtual Category Parent
{
get { return parent; }
set
{
parent = value;
// if (value != null && parent.Children.Contains(this) == false)
// {
// parent.Children.Add(this);
// }
}
}
public virtual ICollection<Category> Children
{
get { return children; }
set { children = value; }
}
}
This is the Mapping file (I am not sure if this is correct.. but I am out of ideas and there is bugger all documentation out there...)
public class CategoryEntityConfiguration : EntityConfiguration<Category>
{
public CategoryEntityConfiguration()
{
Property(x => x.Name).IsRequired();
HasMany(x => x.Children).WithOptional(x => x.Parent);
HasOptional(x => x.Parent).WithMany(x => x.Children);
}
}
Notice the "Parent" property and how I am not adding them each using the "Children" collection.
var cat_0 = new Category { Name = "Root" };
var cat_1 = new Category { Name = "Property", Parent = cat_0 };
var cat_2 = new Category { Name = "Property Services", Parent = cat_1 };
var cat_3 = new Category { Name = "Housing Association", Parent = cat_2 };
var cat_4 = new Category { Name = "Mortgages & Conveyancing", Parent = cat_2 };
var cat_5 = new Category { Name = "Property Management", Parent = cat_2 };
var cat_6 = new Category { Name = "Property Auctions", Parent = cat_2 };
var cat_7 = new Category { Name = "Landlords Wanted", Parent = cat_2 };
context.Set<Category>().Add(cat_0);
When I save the cat_0 to the database only 1 row is inserted and Entity Framework does not pick up the fact the cat_0 is the parent of a whole bunch of other objects and does not realise that they need to be persisted. I have a workaround which is the commented out code in the "Parent" category property.. but I would rather not have to do this as is does not feel right.
Any help would be much appreciated
Jake
It is possible but you have to use tracking proxies. To do that modify your Category class so that all persisted properties are virtual.
public class Category
{
public virtual int Id { get; set; }
public virtual string Name { get; set; }
public virtual Category Parent { get; set; }
public virtual ICollection<Category> Children { get; set; }
}
Create context and check that creation of dynamic proxy is allowed. On such context you can use CreateObject method to get your category instance. You will not get instance of type Category but dynamic type inherited from Category. This dynamic proxy is responsible for lazy loading (if enabled) and for change tracking to existing context. If you modify navigation property on the one side it will automatically modify navigation property on the other side.
using (var context = new ObjectContext(connectionString))
{
// This should be default value
context.ContextOptions.ProxyCreationEnabled = true;
var cat0 = context.CreateObject<Category>();
cat0.Name = "A";
var cat1 = context.CreateObject<Category>();
cat1.Name = "B";
cat1.Parent = cat0;
context.CreateObjectSet<Category>().AddObject(cat0);
context.SaveChanges();
}
Edit:
If you don't like approach with tracking proxies (which require existing context) you can reverse the way you create your entities. Instead of setting Parent property on childs you have to fill Childs on parent. In that case it will work.