Using EntityFramework 6, I would like to update Customer in the following scenario:
public class Customer
{
public int Id {get; set;}
// 100 more scalar properties
public virtual IList<Consultant> Consultants {get;set;}
}
public class Consultant
{
public int Id {get; set;}
public virtual IList<Customer> Customers{get;set;}
}
This is my ViewModel for the edit view:
public class CustomerViewModel
{
public string[] SelectedConsultants { get; set; }
public IEnumerable<Consultants> AllConsultants{ get; set; }
public Customer Customer{ get; set; }
}
This is my Edit-ActionMethod:
[HttpPost]
public ActionResult Edit(CustomerViewModel vm)
{
if (ModelState.IsValid)
{
// update the scalar properties on the customer
var updatedCustomer = vm.Customer;
_db.Customers.Attach(updatedCustomer );
_db.Entry(updatedCustomer ).State = EntityState.Modified;
_db.SaveChanges();
// update the navigational property [Consultants] on the customer
var customer = _db.Customers
.Include(i => i.Customers)
.Single(i => i.Id == vm.Customer.Id);
Customer.Consultants.Clear();
_db.Consultants.Where(x => vm.SelectedConsultants
.Contains(x.Id)).ToList()
.ForEach(x => customer.Consultants.Add(x));
_db.Entry(customer).State = EntityState.Modified;
_db.SaveChanges();
return RedirectToAction("Index");
}
return View(vm);
}
This works and both scalar properties and consultants are updateable from the edit view. However, I am doing two _db.SaveChanges(); in my controller. Is there a less complex way to update Customer? Because Customer has many properties, I'd preferably not do a manual matching of all parameters on Customer and vm.Customer.
I have found the following resources:
asp.net official seems overly complicated (see section Adding
Course Assignments to the Instructor Edit Page) plus would require
me to explicitly write all parameters of Customer)
this popular thread on SO. Method 3 looks like what I need but I could not get the navigational property updated.
I don't think it's necessary to call the SaveChanges twice.
Have you tried something like this:
var customer = _db.Customers
.Where(c => c.Id== vm.Customer.Id)
.Include(c => c.Consultants)
.SingleOrDefault();
customer.Consultants = _db.Consultants
.Where(x => vm.SelectedConsultants.Contains(x.Id)).ToList();
_db.SaveChanges();
Edit:
Ok, not sure if this will work, but you can try using Automapper:
var customer = Mapper.Map<Customer>(vm.Customer);
_db.Entry(customer).State = EntityState.Modified;
customer.Consultants = _db.Consultants.Where(x => vm.SelectedConsultants.Contains(x.Id)).ToList();
_db.SaveChanges();
Related
I have an entity framework model as:
public class Campaign
{
public string Name { get; set; } = string.Empty;
public ICollection<CampaignStation> Stations { get; set; } = new List<CampaignStation>();
public ICollection<Order> Orders { get; set; } = new List<Order>();
}
public void Configure(EntityTypeBuilder<Campaign> builder)
{
builder.HasMany(x => x.Stations).WithOne(y => y.Campaign);
builder.HasMany(x => x.Orders).WithOne(y => y.Campaign);
}
So I have a get controller to get results, and I get results as shown in the following picture:
Apparently the service is working correctly
Now in the view, the status shows an error:
But the header show status OK
I have no idea what is wrong. If I try to access a model property like
<p>#Model.Select(x=> x.Name).ToString()</p>
It shows:
System.Linq.Enumerable+SelectListIterator`2[Project.Lib.Models.Campaign,System.String]
Controller:
[HttpGet]
public async Task<IActionResult> Index()
{
var results = await _campaignsService.GetCampaignsAsync();
return View(results);
}
Service:
public async Task<IList<Campaign>> GetCampaignsAsync()
{
return await _db.Campaigns
.Include(a => a.Agency)
.Include(s => s.Stations)
.ThenInclude(cs => cs.Station)
.ToListAsync();
}
UPDATE
Apparently is something with the EF Migration, I removed the collections and created a new migration and now it does not throw any error, it shows the Name property, etc. But now the question is, how can I return the list of my other two tables and not make any conflict? for some reason public ICollection<CampaignStation> Stations { get; set; } = new List<CampaignStation>(); it is not working but as in my image, it is returning the list correctly!
I cannot get a table to update correctly that should be linking two of my entities. To explain in more detail...I have two entities, Class and Teacher, with a relationship in the form of:
Teacher can be assigned to many classes
Class can only have one teacher.
Below are these two entities.
public class Teacher
{
[Required, Key]
public Guid Id { get; private set; }
[StringLength(50)]
public string Name { get; set; }
public string Email { get; set; }
public List<Class> Classes = new List<Class>();
public Teacher()
{
Id = new Guid();
}
public Teacher(Guid id)
{
Id = id;
}
public void AssignClass(Class newClass)
{
Classes.Add(newClass);
}
}
public class Class
{
[Required, Key]
public Guid Id { get; private set; }
[Required, StringLength(20)]
public string Name { get; set; }
[Required, Range(5, 30)]
public int Capacity { get; set; }
public Teacher Teacher { get; set; }
public IEnumerable<StudentClass> StudentClasses { get; set; }
public Class()
{
Id = new Guid();
}
public Class(Guid id)
{
Id = id;
}
}
When I generate my migrations I get a foreign key of TeacherId in the Classes table as expected. Here is the SQL:
CREATE TABLE [dbo].[Classes] (
[Id] UNIQUEIDENTIFIER NOT NULL,
[Name] NVARCHAR (20) NOT NULL,
[Capacity] INT NOT NULL,
[TeacherId] UNIQUEIDENTIFIER NULL,
CONSTRAINT [PK_Classes] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_Classes_Teachers_TeacherId] FOREIGN KEY ([TeacherId]) REFERENCES [dbo].[Teachers] ([Id])
);
GO
CREATE NONCLUSTERED INDEX [IX_Classes_TeacherId]
ON [dbo].[Classes]([TeacherId] ASC);
My class derived of DBContext looks like:
public class SchoolDatabaseContext : DbContext
{
public DbSet<Student> Students { get; private set; }
public DbSet<Class> Classes { get; private set; }
public DbSet<Teacher> Teachers { get; private set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
}
public SchoolDatabaseContext(DbContextOptions<SchoolDatabaseContext> options) : base(options)
{
}
}
No configuration for those entities yet. I use DI to serve the DbContext to the controller and that all seems fine.
I have aimed for a DDD type structure, but to make this issue easier to debug I have stripped everything all the way back to the controller so it is basically... controller => DbContext.
Here is my code in the controller:
[HttpPost]
[Route("assign-teacher-to-class")]
public async Task<IActionResult> AssignClass([FromBody] AssignTeacherToClass assignTeacherToClass)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
var teacher = await schoolDatabaseContext.Teachers.FindAsync(assignTeacherToClass.TeacherId);
var classToAssign = await schoolDatabaseContext.Classes.FindAsync(assignTeacherToClass.ClassId);
teacher.AssignClass(classToAssign);
schoolDatabaseContext.Entry(teacher).State = EntityState.Modified;
await schoolDatabaseContext.SaveChangesAsync();
return Ok(teacher);
}
When I debug through the ids are fine from the post body, they are assigned correctly to the DTO AssignClass and the calls to the DbContext to find the data for each type (teacher and class) are fine. I then call a method in my teacher type to add the class to the List Classes property (see teachers entity code at beginning for reference), I then Save the changes with the DbContext method and Problem Defined Here: at no stage does the TeacherId in the database update whilst debugging/completing. I have tried all I can think of like instantiating collections in different ways, changing collection types, looking for config that might help map these entities in this way, stripping out all extra layers, changing accessibility of properties and classes and few more.
Any help would really be appreciated as I am getting a bit defeated on this one and I feel like this relationship should be fairly straight forward. I actually was able to get my many to many working with a bridge class so I was surprised to get stuck on this one :(
Thanks
try this:
var teacher = await schoolDatabaseContext.Teachers.Include(x => x.Classes).SingleOrDefaultAsync(x => x.Id == assignTeacherToClass.TeacherId);
I don't think teacher.Classes gets tracked by DbContext otherwise.
There are multiple ways to accomplish this with EF Core. It is easiest to find if you call it what the docs call it "Related Data".
Here is the parent doc: Related Data
Specifically as #Y Stroli has illustrated the Eager Loading method.
The below example is shown on the eager loading reference to load multiple levels of related data:
using (var context = new BloggingContext())
{
var blogs = context.Blogs
.Include(blog => blog.Posts)
.ThenInclude(post => post.Author)
.ThenInclude(author => author.Photo)
.ToList();
}
As of EF Core 5.0 you can also do filtered includes:
using (var context = new BloggingContext())
{
var filteredBlogs = context.Blogs
.Include(blog => blog.Posts
.Where(post => post.BlogId == 1)
.OrderByDescending(post => post.Title)
.Take(5))
.ToList();
}
As the suggestion from lvan, you should change public List<Class> Classes = new List<Class>(); to public List<Class> Classes { get; set; } = new List<Class>();.
For your current code, it seems you want to add Class and return the teacher, if so, you need to include the exsiting classes to teacher like below, otherwise, it will only return the new adding class.
public async Task<IActionResult> AssignClass()
{
var assignTeacherToClass = new AssignTeacherToClass {
TeacherId = new Guid("52abe5e0-bcd4-4827-893a-26b24ca7b1c4"),
ClassId =new Guid("50354c76-c9e8-4fc3-a7c9-7644d47a6854")
};
var teacher = await _context.Teachers.Include(t => t.Classes).FirstOrDefaultAsync(t => t.Id == assignTeacherToClass.TeacherId);
var classToAssign = await _context.Classes.FindAsync(assignTeacherToClass.ClassId);
teacher.AssignClass(classToAssign);
_context.Entry(teacher).State = EntityState.Modified;
await _context.SaveChangesAsync();
return Ok(teacher);
}
One more note, you need to configure SerializerSettings.ReferenceLoopHandling like
services.AddMvc()
.AddJsonOptions(opt => {
opt.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
You need to define the connection between Teacher and Class.
protected override void OnModelCreating(Modelbuilder modelBuilder)
{
modelBuilder.Entity<Class>()
.HasOne<Teacher>(p => p.Teacher)
.WithMany(q => q.Classes)
.HasForeignKey(r => r.TeacherId);
}
Also add TeacherId prop to Class.
Background
Recently I changed jobs and attached to a Web API project. I am familiar with the concepts of Web API & MVC but have no prior hands-on experince.
I have followed few tutorials and based on them created an empty WebApi project via Visual Studio 2017, hooked up my model from Database and added Controllers.
This is the revised controller:
private MyEntities db = new MyEntities();
//...
[ResponseType(typeof(MyEntityType))]
[Route("api/MyEntity")]
public async Task<IHttpActionResult> GetMyEntityType([FromUri]int parameter)
{
MyEntityType found = db.MyEntity
.OrderByDescending(c => c.CreationTime)
.First(c => c.ParameterColumn == parameter);
if (found == null)
{
return NotFound();
}
return Ok(found );
}
Note : I am querying based on a column other than KEY
When I make a call to .../api/MyEntity?parameter=1 I expect to receive a single item in response. But for reasons unknown to me, the previous call returns all items and it is unsorted.
Please note: If I place a breakpoint on if (found == null), I can confirm that my query has resulted in a single item.
Question
What am I missing here? Why does the response contains all elements instead of single element?
UPDATE 1
I tried the same call from Postman, this is the output. Please note that I have changed the request, controller code etc. in question to omit some private details.
I can see that response contains my desired data, but along with all of inner data in other end of relationship. If I am not mistaken, by default, EF uses lazy loading. Since I have no Include clause, I have no idea why all related data is returned in response.
I think I need to investigate my relationships in Model/DB and make sure Lazy-Loading is enabled.
UPDATE 2
These are my entity classes:
public partial class MyEntity
{
public int Id { get; set; }
public Nullable<int> ForeignKey_ID { get; set; }
public Nullable<int> MyValue { get; set; }
public Nullable<System.DateTime> CreationTime { get; set; }
public Nullable<int> Some_ID { get; set; }
public virtual MyOtherEntity MyOtherEntity { get; set; }
}
public partial class MyOtherEntity
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
public MyOtherEntity()
{
this.MyOtherEntity1 = new HashSet<MyOtherEntity>();
}
//...
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<MyOtherEntity> MyOtherEntity1 { get; set; }
public virtual MyOtherEntity MyOtherEntity2 { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<MyEntity> MyEntity { get; set; }
}
When the MyEntityType instance is returned via Ok it will be converted to JSON which will read the values of all the public properties and fields. This will cause EF to load the entire entity and all relationships. If you only require specific properties to be returned then use Select() as below.
var found = db.MyEntity
.OrderByDescending(c => c.CreationTime)
.Select(c => new { c.CreationTime, c.ParameterColumn })
.First(c => c.ParameterColumn == parameter);
You can tailor the selected properties as required. If you require criteria in First() that does not need to be selected, move the condition into a Where() call before the Select.
var found = db.MyEntity
.OrderByDescending(c => c.CreationTime)
.Where(c => c.ParameterColumn == parameter)
.Select(c => new { c.CreationTime })
.First();
You should probably create a ViewModel of MyEntityType that has only the properties you require, and map these to a new instance of the ViewModel instead. You can then update the [ResponseType(typeof(MyEntityType))] attribute too.
For example, declare:
public class MyEntityTypeViewModel {
public DateTime CreationTime { get; set; }
public int ParameterColumn { get; set; }
}
And then in your controller action:
MyEntityTypeViewModel found = db.MyEntity
.OrderByDescending(c => c.CreationTime)
.Where(c => c.ParameterColumn == parameter)
.Select(c => new MyEntityTypeViewModel {
CreationTime = c.CreationTime,
ParameterColumn = c.ParameterColumn })
.First();
If you're using Entity Framework Core 2, try this:
MyEntityType found = await db.MyEntity.AsNoTracking()
.OrderByDescending(c => c.CreationTime)
.FirstOrDefaultAsync(c => c.ParameterColumn == parameter);
And it's better to include database context via Dependency Injection, not with private field.
I have two EF models/classes that have relation between them: Member and MembershipSeason. One Member can have several MembershipSeasons. One MembershipSeason model has a foreign key reference (MemberID) to Member model in db.
Member.cs
public class Member
{
public int MemberID { get; set; }
//some properties left out
public virtual ICollection<MembershipSeason> MembershipSeasons { get; set; }
}
MembershipSeason.cs
public class MembershipSeason
{
[Key]
public int MembershipSeasonID { get; set; }
//some properties left out
public int MemberID { get; set; }
public virtual Member Member { get; set; }
}
I experimented to post those two models to the same Create method together in the same time. I discovered that EF tracks those two models and saves them into db as new models. It also links those two models by setting MemberID of the new Member model as foreign key to the new MembershipSeason model in db. I guess this is planned behaviour? – I mean the fact EF sets foreign key to the related models automatically seems to be expected behaviour – how things should work. Therefore I guess I don’t need to save Member model first, obtain it’s MemberID and use it for MembershipSeason and save it separately in the Create method? (because EF does the work for you)
db.Members.Add(member);
db.MembershipSeasons.Add(membershipSeason);
await db.SaveChangesAsync();
The above and the below Create method works in the way that no MemberID property is needed to be set directly to MembershipSeason model, because EF does it automatically.
MemberController.cs
public class MemberController : Controller
{
private MembersContext db = new MembersContext();
//some code left out
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create([Bind(Include = "MemberNumber,FirstName,LastName")] Member member,
[Bind(Include = "HasPaidMembership,SeasonId")] MembershipSeason membershipSeason)
{
try
{
if (ModelState.IsValid)
{
db.Members.Add(member);
db.MembershipSeasons.Add(membershipSeason);
await db.SaveChangesAsync();
return RedirectToAction("Index");
}
}
catch (DataException /* dex */)
{
}
return View(member);
}
}
I am quite new with EF and ASP.NET MVC, so I am trying to figure these things out. Any help appreciated – thanks.
It also links those two models by setting MemberID of the new Member model as foreign key to the new MembershipSeason model in db. I guess this is planned behaviour?
TL;DR: Yes
Yes, it has to be the required behavior. Lets start with reads:
public class Organization
{
public Guid Id { get; set; }
}
public class Employee
{
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
public Organization Organization { get; set; }
}
public Employee GetEmployeeWithOrganization(guid id)
{
var result = _context.Employees
.Include(e => e.Organization)
.FirstOrDefault(e => e.Id = id);
}
Basically when you ask EF to include the navigation property you'd get an object graph kinda like:
Employee
- Id : <guid1>
- OrganizationId : <guid2>
- Organization : object
- Id : <guid2>
It would be common sense to assume that because EF should keep track of entities because what happens if you do this:
var employee = GetEmployeeWithOrganization(<guid1>)
var org = new Organization { id = guid.NewGuid() }; //<guid3>
employee.Organization = org;
_context.SaveChanges();
Which one of these is a valid object:
A:
Employee
- Id : <guid1>
- OrganizationId : <guid2> // <-- difference
- Organization : object
- Id : <guid3>
B:
Employee
- Id : <guid1>
- OrganizationId : <guid3> // <-- difference
- Organization : object
- Id : <guid3>
A isn't valid, because you can't rely on the values and programming against that object would not only be a completely nightmare, but at the database level doesn't make sense. B is valid, it is data you can rely on.
This also means you can precache items and EF will write them up automagically. Consider:
var org = GetOrganization(<guid3>);
var emp = GetEmployee(<guid1>);
Assert.That(emp.Organization, Is.Not.Null); // passes
This happens because EF is tracking org and because EF is configured with org as a FK to employee.
I have a collection related to an class but I cannot save them to a database. Other members are saved successfully but collection not.
public ActionResult Edit([Bind(Include = "ProductTypeId,Name,IsActive")] ProductType productType, string[] chkAttributeCategory)
{
productType.AttributeCategories.Clear();
if (chkAttributeCategory != null)
{
foreach (string attributeCategory in chkAttributeCategory)
{
productType.AttributeCategories.Add(db.AttributeCategory.Find(int.Parse(attributeCategory)));
}
}
if (ModelState.IsValid)
{
db.Entry(productType).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(productType);
}
I have a checkbox list on my view whick represents a list of child objects. I've checked the productType object before save changes line in debuger and it contains everything he needs, but associated AttributeCategories are not saved in the database.
Somebody has idea ?
Since you are updating an entity from a disconnected object, the change tracker will not work, changes to non scalar properties will not work.
If you do like this, it should work.
var productTypeDb = db.Set<ProductType>()
.Include(pt => pt.AttributeCategories)
.FirstOrDefault(pt => pt.ProductTypeId == productType.ProductTypeId)
productTypeDb.AttributeCategories.Clear();
if (chkAttributeCategory != null)
{
foreach (string attributeCategory in chkAttributeCategory)
{
productTypeDb.AttributeCategories.Add(db.AttributeCategory
.Find(int.Parse(attributeCategory)));
}
}
productTypeDb.Name = productType.Name;
productTypeDb.IsActive = productType.IsActive;
// other properties
db.SaveChanges();
There is another way to manage the state manually to be able to work with disconnected object, but if that's not mandatory, you can do like above code.
I think you can't have a list or an array in a model that's not a view model, I think you need to have a separate model with each product type linked to attributes:
//model1
public class ProductTypeCategory{
public int ProductTypeID {get; set;}
public int CategoryID {get; set;}
}
//model2
public class Category{
public int ID {get; set;}
public string Name {get; set;}
}
now you can get the attributes this way:
List<Category> attributes = (from p in db.ProductTypeCategories from c in db.Categories where p.ProductTypeID == productTypeID && p.CategoryID == c.ID select c).toList();
just input the id of the product type in the above LINQ call an you'll get its categories