IDbSetExtensions.AddOrUpdate and relationships - c#

IDbSetExtensions.AddOrUpdate is meant to help write code that works the same whether the database is empty or populated. But linking objects needs different code. When the database is empty, objects don't have IDs yet and you link them by assigning the navigational property. When the objects already exist, however, navigational properties don't work and you need to set the foreign keys directly. Navigational properties do work for proxies in both cases, at the cost of forfeiting POCOs. Edit: Actually, proxies don't work when both entities are old.
This sample crashes in the second SaveChanges call, when EF tries to set CountryID to 0:
public class Country
{
public virtual int ID { get; set; }
public virtual string Name { get; set; }
}
public class Person
{
public virtual int ID { get; set; }
public virtual string Name { get; set; }
public virtual int CountryID { get; set; }
public virtual Country Country { get; set; }
}
public class Context : DbContext
{
public DbSet<Person> Person { get; set; }
public DbSet<Country> Country { get; set; }
}
class Program
{
static void Foo()
{
using (var db = new Context())
{
//var c = new Country();
var c = db.Country.Create();
c.Name = "usa";
db.Country.AddOrUpdate(x => x.Name, c);
//var p = new Person();
var p = db.Person.Create();
p.Name = "billg";
p.Country = c;
db.Person.AddOrUpdate(x => x.Name, p);
db.SaveChanges();
}
}
static void Main()
{
Database.SetInitializer<Context>(new DropCreateDatabaseAlways<Context>());
Foo();
Foo();
}
}
How is AddOrUpdate used?

IDbSetExtensions.AddOrUpdate is meant to help write code that works the same whether the database is empty or populated.
AddOrUpdate is meant to be used only in Seed method of code first migrations. It is not supposed to be used in normal code because it has big overhead and some limitations. Overhead is additional query to database and reflection. Limitation is that it checks only the main entity you are passing but not its relations. Each relation is supposed to be handled by separate call to AddOrUpdate:
static void Foo()
{
using (var db = new Context())
{
var c = new Country() {Name = "abc"};
db.Country.AddOrUpdate(x => x.Name, c);
var p = new Person()
{
Name = "me",
CountryID = c.ID,
Country = c
};
db.Person.AddOrUpdate(x => x.Name, p);
db.SaveChanges();
}
}

Related

Seed in Entity Framework Core Many-to-Many

how can one seed a many-to-many relation in EF Core, couldn't find anything in this area?
So this is the entities
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
public virtual List<StudentGrade> StudentGrades { get; set; }
}
public class Grade
{
public int Id { get; set; }
public int Grade { get; set; }
public virtual List<StudentGrade> StudentGrades { get; set; }
}
public class StudentGrade
{
public int GradeId { get; set; }
public Grade Grade { get; set; }
public int StudentId { get; set; }
public Student Student { get; set; }
}
so the official documentation says you have to have a joining entity defined (in my case StudentGrade) and this should be referenced within the entities that are in a many-to-many relation.
Ef core documentation for many-to-many.
Now in EF, you wouldn't have to do this, it would figure out those things, and so instead of having the join-entity, you would simply reference each entity into the other.
So how can you seed this type of relation in EF Core?
Thanks
So what worked for me was overriding DbContext::OnModelCreating(ModelBuilder modelBuilder) with something similar to this:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<StudentGrade>()
.HasKey(s => new { s.GradeId , s.StudentId });
var students= new[]
{
new Student{Id=1, Name="John"},
new Student{Id=2, Name="Alex"},
new Student{Id=3, Name="Tom"}
}
var grades = new[]
{
new Grade{Id=1, Grade=5},
new Grade{Id=2, Grade=6}
}
var studentGrades = new[]
{
new StudentGrade{GradeId=1, StudentId=1},
new StudentGrade{GradeId=2, StudentId=2},
// Student 3 relates to grade 1
new StudentGrade{GradeId=1, StudentId=3}
}
modelBuilder.Entity<Student>().HasData(stdudents[0],students[1],students[2]);
modelBuilder.Entity<Grade>().HasData(grades[0],grades[1]);
modelBuilder.Entity<StudentGrade>().HasData(studentGrades[0],studentGrades[1],studentGrades[2]);
base.OnModelCreating( modelBuilder );
}
I've followed the custom initialization logic, as explained here, since my commitment is just data for testing and developing.
I like to do the seeding in a synchronous way, as you'll see in the code.
Important: Previous to this step, I do a 'commit' (context.SaveChanges();) with the entities data that I have to join, so EF will pick them from the DB with the ID inserted. That's nice if the DB backend has an autoincremental ID.
Following with your example (I've pluralized the dbSets), and assuming you have inserted the proper data
var student= context.Students.FirstOrDefault(a => a.Name == "vic");
var grade= context.Grades.FirstOrDefault(b => b.Grade == 2);
var studentGrade = context.StudentsGrades.Include(a => a.Students).Include(b => b.Grades)
.FirstOrDefault(c => c.Students.Name == "vic" && c.Grades.Grade = 2);
if (studentGrade == null)
{
context.StudentsGrades.Add(new StudentGrade
{
StudentId = student.Id,
GradeId = grade.Id
});
}
context.SaveChanges();

Entity Framework 6 doesn't save navigated object (state is Unchanged) if parent object has all properties virtual

I have following classes:
public class A
{
[Key]
public virtual int ID { get; set; } //virtual here raises error!
public virtual B B { get; set; }
}
public class B
{
[Key]
public int ID { get; set; }
[Required]
public string title { get; set; }
}
and code:
var context = new Model1();
var dbSet = context.Set<A>();
var dbSet1 = context.Set<B>();
var a = dbSet.Find(1);
var b = a.B;
b.title = DateTime.Now.Ticks.ToString();
int changes1 = context.SaveChanges();
if (changes1 == 0)
throw new Exception("not updated");
if I remove "virtual" from property ID in class A everything is working. I need the property virtual in order to use the model also in nhibernate.
thanks
I was able to reproduce it, and apparently it's EF6 bug.
I can suggest 2 workarounds. Either (1) make all B members virtual as well, or (2) eager load (lazy and explicit loading doesn't work) the navigation property before editing it.
i.e. instead of
var a = dbSet.Find(1); // doesn't work
use
var a = dbSet.Include(e => e.B).First(e => e.ID == 1); // works

Prevent SELECT N+1 issue in Entity Framework query using Joins

I'm trying to query something from an indirectly related entity into a single-purpose view model. Here's a repro of my entities:
public class Team {
[Key]
public int Id { get; set; }
public string Name { get; set; }
public List<Member> Members { get; set; }
}
public class Member {
[Key]
public int Id { get; set; }
public string Name { get; set; }
}
public class Pet {
[Key]
public int Id { get; set; }
public string Name { get; set; }
public Member Member { get; set; }
}
Each class is in a DbSet<T> in my database context.
This is the view model I want to construct from a query:
public class PetViewModel {
public string Name { get; set; }
public string TeamItIndirectlyBelongsTo { get; set; }
}
I do so with this query:
public PetViewModel[] QueryPetViewModel_1(string pattern) {
using (var context = new MyDbContext(connectionString)) {
return context.Pets
.Where(p => p.Name.Contains(pattern))
.ToArray()
.Select(p => new PetViewModel {
Name = p.Name,
TeamItIndirectlyBelongsTo = "TODO",
})
.ToArray();
}
}
But obviously there's still a "TODO" in there.
Gotcha: I can not change the entities at this moment, so I can't just include a List<Pet> property or a Team property on Member to help out. I want to fix things inside the query at the moment.
Here's my current solution:
public PetViewModel[] QueryPetViewModel_2(string pattern) {
using (var context = new MyDbContext(connectionString)) {
var petInfos = context.Pets
.Where(p => p.Name.Contains(pattern))
.Join(context.Members,
p => p.Member.Id,
m => m.Id,
(p, m) => new { Pet = p, Member = m }
)
.ToArray();
var result = new List<PetViewModel>();
foreach (var info in petInfos) {
var team = context.Teams
.SingleOrDefault(t => t.Members.Any(m => m.Id == info.Member.Id));
result.Add(new PetViewModel {
Name = info.Pet.Name,
TeamItIndirectlyBelongsTo = team?.Name,
});
}
return result.ToArray();
}
}
However, this has a "SELECT N+1" issue in there.
Is there a way to create just one EF query to get the desired result, without changing the entities?
PS. If you prefer a "plug and play" repro containing the above, see this gist.
You've made the things quite harder by not providing the necessary navigation properties, which as #Evk mentioned in the comments do not affect your database structure, but allow EF to supply the necessary joins when you write something like pet.Member.Team.Name (what you need here).
The additional problem with your model is that you don't have a navigation path neither from Team to Pet nor from Pet to Team since the "joining" entity Member has no navigation properties.
Still it's possible to get the information needed with a single query in some not so intuitive way by using the existing navigation properties and unusual join operator like this:
var result = (
from team in context.Teams
from member in team.Members
join pet in context.Pets on member.Id equals pet.Member.Id
where pet.Name.Contains(pattern)
select new PetViewModel
{
Name = pet.Name,
TeamItIndirectlyBelongsTo = team.Name
}).ToArray();

Insert records in many to many not working with Entity Framework

I want to insert 2 new records in their tables and also to create the relation many to many.
There are a lot of questions with this topic, I tried many of the answers and now I have no idea why my code is not working.
Please help me!
This is the code:
class Program
{
static void Main(string[] args)
{
MyContext db = new MyContext();
var st1 = new Student() { Name = "Joe" };
var c1 = new Course() { Name = "Math" };
db.Courses.Attach(c1);
st1.Courses.Add(c1);
db.Students.Add(st1);
db.SaveChanges();
}
}
public class Student
{
public Student()
{
Courses = new HashSet<Course>();
}
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
public class Course
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Student> Students { get; set; }
}
public class MyContext : DbContext
{
public DbSet<Student> Students { get; set; }
public DbSet<Course> Courses { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.HasMany(p => p.Courses)
.WithMany(d => d.Students)
.Map(t =>
{
t.MapLeftKey("studentId");
t.MapRightKey("courseid");
t.ToTable("StudentCourse");
});
base.OnModelCreating(modelBuilder);
}
}
Edit: Like sugested, I initialized the Courses:
public Student()
{
Courses = new HashSet<Course>();
}
and now I get this error on db.SaveChanges();
An error occurred while saving entities that do not expose foreign key properties for their relationships. The EntityEntries property will return null because a single entity cannot be identified as the source of the exception. Handling of exceptions while saving can be made easier by exposing foreign key properties in your entity types. See the InnerException for details.
You're apparently trying to add a new Student to the database and associate it to an existing Course. The problem is that you attach a new Course entity to the context without a proper primary key.
It certainly is a good idea to use a so-called stub entity here, because it saves a roundtrip to the database to fetch an existing Course, but the primary key is required for EF to create a correct association record. It's even the only property you need to set in this Course stub:
var st1 = new Student() { Name = "Joe" };
var c1 = new Course() { CourseId = 123 };
db.Courses.Attach(c1);
st1.Courses.Add(c1);
db.Students.Add(st1);
If you want to add a new course and a new student, you should Add both of them:
db.Courses.Add(c1);
st1.Courses.Add(c1);
db.Students.Add(st1);
Your code isn't initialising the ICollection object in either class.
public class Student
{
private Student()
{
Courses = new List<Course>();
}
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
Edit
Try changing your modelBuilder code to the following
modelBuilder.Entity<Student>()
.HasMany<Course>(s => s.Courses)
.WithMany(c => c.Students)
.Map(cs =>
{
cs.MapLeftKey("StudentRefId");
cs.MapRightKey("CourseRefId");
cs.ToTable("StudentCourse");
});
Code sample taken directly from:
http://www.entityframeworktutorial.net/code-first/configure-many-to-many-relationship-in-code-first.aspx

Can I create nested classes when using Linq-To-Entities?

I'm still learning Entity Framework and Linq-To-Entities, and I was wondering if a statement of this kind is possible:
using (var context = new MyEntities())
{
return (
from a in context.ModelSetA.Include("ModelB")
join c in context.ModelSetC on a.Id equals c.Id
join d in context.ModelSetD on a.Id equals d.Id
select new MyModelA()
{
Id = a.Id,
Name = a.Name,
ModelB = new MyModelB() { Id = a.ModelB.Id, Name = a.ModelB..Name },
ModelC = new MyModelC() { Id = c.Id, Name = c.Name },
ModelD = new MyModelD() { Id = d.Id, Name = d.Name }
}).FirstOrDefault();
}
I have to work with a pre-existing database structure, which is not very optimized, so I am unable to generate EF models without a lot of extra work. I thought it would be easy to simply create my own Models and map the data to them, but I keep getting the following error:
Unable to create a constant value of type 'MyNamespace.MyModelB'. Only
primitive types ('such as Int32, String, and Guid') are supported in
this context.
If I remove the mapping for ModelB, ModelC, and ModelD it runs correctly. Am I unable to create new nested classes with Linq-To-Entities? Or am I just writing this the wrong way?
What you have will work fine with POCOs (e.g., view models). Here's an example. You just can't construct entities this way.
Also, join is generally inappropriate for a L2E query. Use the entity navigation properties instead.
I have created your model (how I understand it) with EF 4.1 in a console application:
If you want to test it, add reference to EntityFramework.dll and paste the following into Program.cs (EF 4.1 creates DB automatically if you have SQL Server Express installed):
using System.Linq;
using System.Data.Entity;
namespace EFNestedProjection
{
// Entities
public class ModelA
{
public int Id { get; set; }
public string Name { get; set; }
public ModelB ModelB { get; set; }
}
public class ModelB
{
public int Id { get; set; }
public string Name { get; set; }
}
public class ModelC
{
public int Id { get; set; }
public string Name { get; set; }
}
public class ModelD
{
public int Id { get; set; }
public string Name { get; set; }
}
// Context
public class MyContext : DbContext
{
public DbSet<ModelA> ModelSetA { get; set; }
public DbSet<ModelB> ModelSetB { get; set; }
public DbSet<ModelC> ModelSetC { get; set; }
public DbSet<ModelD> ModelSetD { get; set; }
}
// ViewModels for projections, not entities
public class MyModelA
{
public int Id { get; set; }
public string Name { get; set; }
public MyModelB ModelB { get; set; }
public MyModelC ModelC { get; set; }
public MyModelD ModelD { get; set; }
}
public class MyModelB
{
public int Id { get; set; }
public string Name { get; set; }
}
public class MyModelC
{
public int Id { get; set; }
public string Name { get; set; }
}
public class MyModelD
{
public int Id { get; set; }
public string Name { get; set; }
}
class Program
{
static void Main(string[] args)
{
// Create some entities in DB
using (var ctx = new MyContext())
{
var modelA = new ModelA { Name = "ModelA" };
var modelB = new ModelB { Name = "ModelB" };
var modelC = new ModelC { Name = "ModelC" };
var modelD = new ModelD { Name = "ModelD" };
modelA.ModelB = modelB;
ctx.ModelSetA.Add(modelA);
ctx.ModelSetB.Add(modelB);
ctx.ModelSetC.Add(modelC);
ctx.ModelSetD.Add(modelD);
ctx.SaveChanges();
}
// Run query
using (var ctx = new MyContext())
{
var result = (
from a in ctx.ModelSetA.Include("ModelB")
join c in ctx.ModelSetC on a.Id equals c.Id
join d in ctx.ModelSetD on a.Id equals d.Id
select new MyModelA()
{
Id = a.Id,
Name = a.Name,
ModelB = new MyModelB() {
Id = a.ModelB.Id, Name = a.ModelB.Name },
ModelC = new MyModelC() {
Id = c.Id, Name = c.Name },
ModelD = new MyModelD() {
Id = d.Id, Name = d.Name }
}).FirstOrDefault();
// No exception here
}
}
}
}
This works without problems. (I have also recreated the model from the database (which EF 4.1 had created) in EF 4.0: It works as well. Not surprising since EF 4.1 doesn't change anything in LINQ to Entities.)
Now the question is why you get an exception? My guess is that there is some important difference in your Models or ViewModels or your query compared to the simple model above which is not visible in your code example in the question.
But the general result is: Projections into nested (non-entity) classes work. (I'm using it in many situations, even with nested collections.) Answer to your question title is: Yes.
What Craig posted does not seem to work for nested entities. Craig, if I am misunderstood what you posted, please correct me.
Here is the workaround I came up with that does work:
using (var context = new MyEntities())
{
var x = (
from a in context.ModelSetA.Include("ModelB")
join c in context.ModelSetC on a.Id equals c.Id
join d in context.ModelSetD on a.Id equals d.Id
select new { a, b, c }).FirstOrDefault();
if (x == null)
return null;
return new MyModelA()
{
Id = x.a.Id,
Name = x.a.Name,
ModelB = new MyModelB() { Id = x.a.ModelB.Id, Name = x.a.ModelB..Name },
ModelC = new MyModelC() { Id = x.c.Id, Name = x.c.Name },
ModelD = new MyModelD() { Id = x.d.Id, Name = x.d.Name }
};
}
Since Entity Framework can't handle creating nested classes from within the query, I simply returned an anonymous object from my query containing the data I wanted, then mapped it to the Model

Categories

Resources