I've been trying to grok EF many-to-many relationships for the past two days now and I'm still missing something even after scouring a dozen different questions here.
I've got a model named Text that can have an arbitrary number of Tag models associated with it, and obviously, at least in theory, each Tag can be associated with an arbitrary number of Texts. Entity Framework seems to understand this well enough to create a table named TextTags in the database without me asking it to do so, and I can access Text.Tags without trouble, but when I attempt to access Tag.Texts in my code, I get a null reference exception.
Now, I could just add every text to every tag manually (or could I? that seems to throw some kind of error), but that would seem to defeat the purpose... Besides which, it also seems error prone. What am I failing to understand?
Code as requested:
Text model:
public class Text
{
public int ID { get; set; }
public Author Author { get; set; }
public string Content { get; set; }
public virtual ICollection<Tag> Tags { get; set; }
}
Tag model:
public class Tag
{
public int ID { get; set; }
public string Name { get; set; }
public ICollection<Text> Texts { get; set; }
}
Data insert:
using (var db = new TextDbContext())
{
db.Authors.Add(new Author()
{
Name = "Poe"
});
db.Tags.Add(new Tag() { Name = "lame" });
db.Tags.Add(new Tag() { Name = "example" });
db.SaveChanges();
db.Texts.Add(new Text()
{
Author = db.Authors.First(),
Tags = db.Tags.ToList(),
Content = "This is the first text by Poe."
});
db.Texts.Add(new Text()
{
Author = db.Authors.First(),
Tags = db.Tags.ToList(),
Content = "This is the second text by Poe."
});
db.Texts.Add(new Text()
{
Author = db.Authors.First(),
Tags = db.Tags.ToList(),
Content = "This is the third text by Poe."
});
db.SaveChanges();
}
Error:
foreach (var tag in db.Tags)
{
foreach (var text in tag.Texts)
{
Console.WriteLine("Tag: {0}\tText: {1}", tag.Name, text.Content);
// Null reference on line above.
}
}
You get a NullReferenceException because your navigation property Tag.Texts is not marked as virtual. As a result lazy loading does not work to load the Tag.Texts collection when you access it and the collection is null. (Text.Tags is virtual, hence no exception here.)
Related
Im trying to save a rating against a place, I have the code below, but it doesnt seems to save rating (to the ratings table) for an existing entity
place.Ratings.Add(rating);
_placeRepository.AddPlaceIfItDoesntExist(place);
_placeRepository.Save();
This is the repository method
public void AddPlaceIfItDoesntExist(Place place)
{
var placeItem = context.Places.FirstOrDefault(x => x.GooglePlaceId == place.GooglePlaceId);
if(placeItem==null)
{
context.Places.Add(place);
}
else
{
context.Entry(placeItem).State = EntityState.Modified;
}
}
and this is the poco
public class Place
{
public Place()
{
Ratings = new List<Rating>();
}
public int Id { get; set; }
public string Name { get; set; }
public string GooglePlaceId { get; set; }
}
I think the crux of the problem is because i need to check if the place exists based on googleplaceid(a string) rather than the id (both are unique per place btw)
Here
context.Entry(placeItem).State = EntityState.Modified;
you just mark the existing placeItem object as modified. But it's a different instance than the passed place object, hence contains the orginal values.
Instead, replace that line with:
context.Entry(placeItem).CurrentValues.SetValues(place);
Alternatively, you can use the DbSetMigrationsExtensions.AddOrUpdate method overload that allows you to pass a custom identification expression:
using System.Data.Entity.Migrations;
public void AddPlaceIfItDoesntExist(Place place)
{
context.Places.AddOrUpdate(p => p.GooglePlaceId, place);
}
I have a trouble with EF (6.1.3)
I have created next classes (with many-to-many relationship):
public class Record
{
[Key]
public int RecordId { get; set; }
[Required]
public string Text { get; set; }
public virtual ICollection<Tag> Tags { get; set; }
}
public class Tag
{
[Key]
public int TagId { get; set; }
[Required]
public string Name { get; set; }
public virtual ICollection<Record> Records{ get; set; }
}
And method:
void AddTags()
{
Record[] records;
Tag[] tags;
using (var context = new AppDbContext())
{
records = context.Records.ToArray();
}//remove line to fix
tags = Enumerable.Range(0, 5).Select(x => new Tag()
{
Name = string.Format("Tag_{0}", x),
Records= records.Skip(x * 5).Take(5).ToArray()
}).ToArray();
using (var context = new AppDbContext()){ //remove line to fix
context.Tags.AddRange(tags);
context.SaveChanges();
}
}
If I use two contexts, the records (which were added to created tags) will be duplicated. If I remove marked rows - problem disappears.
Is there any way to fix this problem without using the same context?
If you can, better reload entities or not detach them at all. Using multiple context instances in application is overall making things much more complicated.
The problem for you comes from the Entity Framework entity change tracker. When you load entitites from your DbContext and dispose that context, entities get detached from entity change tracker, and Entity Framework has no knowledge of any changes made to it.
After you reference detached entity by an attached entity, it (detached entity) immediately gets into entity change tracker, and it has no idea that this entity was loaded before. To give Entity Framework an idea that this detached entity comes from the database, you have to reattach it:
foreach (var record in records) {
dbContext.Entry(record).State = EntityState.Unchanged;
}
This way you will be able to use records to reference in other objects, but if you have any changes made to these records, then all these changes will go away. To make changes apply to database you have to change state to Added:
dbContext.Entry(record).State = EntityState.Modified;
Entity Framework uses your mappings to determine row in database to apply changes to, specifically using your Primary Key settings.
A couple examples:
public class Bird
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
public string Color { get; set; }
}
public class Tree
{
[Key]
public int Id { get; set; }
public string Name { get; set; }
}
public class BirdOnATree
{
[Column(Order = 0), Key, ForeignKey("Bird")]
public int BirdId { get; set; }
public Bird Bird { get; set; }
[Column(Order = 1), Key, ForeignKey("Tree")]
public int TreeId { get; set; }
public Tree Tree { get; set; }
public DateTime SittingSessionStartedAt { get; set; }
}
Here's a small entity structure so that you could see how it works. You can see that Bird and Tree have simple Key - Id. BirdOnATree is a many-to-many table for Bird-Tree pair with additional column SittingSessionStartedAt.
Here's the code for multiple contexts:
Bird bird;
using (var context = new TestDbContext())
{
bird = context.Birds.First();
}
using (var context = new TestDbContext())
{
var tree = context.Trees.First();
var newBirdOnAtree = context.BirdsOnTrees.Create();
newBirdOnAtree.Bird = bird;
newBirdOnAtree.Tree = tree;
newBirdOnAtree.SittingSessionStartedAt = DateTime.UtcNow;
context.BirdsOnTrees.Add(newBirdOnAtree);
context.SaveChanges();
}
In this case, bird was detached from the DB and not attached again. Entity Framework will account this entity as a new entity, which never existed in DB, even though Id property is set to point to existing row to database. To change this you just add this line to second DbContext right in the beginning:
context.Entry(bird).State = EntityState.Unchanged;
If this code is executed, it will not create new Bird entity in DB, but use existing instead.
Second example: instead of getting bird from the database, we create it by ourselves:
bird = new Bird
{
Id = 1,
Name = "Nightingale",
Color = "Gray"
}; // these data are different in DB
When executed, this code will also not create another bird entity, will make a reference to bird with Id = 1 in BirdOnATree table, and will not update bird entity with Id = 1. In fact you can put any data here, just use correct Id.
If we change our code here to make this detached entity update existing row in DB:
context.Entry(bird).State = EntityState.Modified;
This way, correct data will be inserted to table BirdOnATree, but also row with Id = 1 will be updated in table Bird to fit the data you provided in the application.
You can check this article about object state tracking:
https://msdn.microsoft.com/en-US/library/dd456848(v=vs.100).aspx
Overall, if you can avoid this, don't use object state tracking and related code. It might come to unwanted changes that are hard to find source for - fields are updated for entity when you don't expect them to, or are not updated when you expect it.
I'm working on a Rest API using Web API2 and EF6 Code first starting from the guide on http://www.asp.net/web-api/overview/data/using-web-api-with-entity-framework/part-4
I'm basically doing the same thing having a many to many relationship and when i am creating an object, lets call it A, i include an array of B objects as the Bs variable in the post. They all get created as expected on the initial post, however when i add a second Object A which should link to one or more of the same B's as the first object A it instead of matching to the existing B's tries to create new B's but since there is a constraint on the name of the B this wont work. How do i ensure that it does not try to create a new object B every-time and instead link to an existing Object B if there is one?.
Here is the example in more detail.
I have two Models, lets call them A and B. They have a many to many relation
public class A
{
public int Id { get; set; }
[Required, StringLength(100), Index("IDX_Name", 2, IsUnique = true)]
public string Name { get; set; }
[StringLength(300)]
public string Description { get; set; }
public ICollection<B> Bs{ get; set; }
}
Model B
public class B
{
public int Id { get; set; }
[Required, StringLength(100), Index("IDX_Name", 2, IsUnique = true)]
public string Name { get; set; }
public ICollection<B> As{ get; set; }
}
I'm not including the auto generated context.
and in the auto generated Controller scaffolding for the Web API POST method for Model A it looks like this
[ResponseType(typeof(A))]
public async Task<IHttpActionResult> PostGame(A a)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
db.As.Add(a);
await db.SaveChangesAsync();
return CreatedAtRoute("DefaultApi", new { id = a.Id }, a);
}
All tables are created just fine and if i do the first post creating my first object A with the following json:
{
"Name": "FirstA",
"Description": "FirstADesc",
"Bs" : [{"Name":"FirstB"}]
}
It works and both FirstA and FirstB is created.
If i then post a SecondA which is also linked to the FirstB obect
{
"Name": "SecondA",
"Description": "SecondADesc",
"Bs" : [{"Name":"SecondB"},{"Name":"FirstB"}]
}
It will instead of finding the FirstB try to create it again. Which it due to the constraint.
My first guess was that i should use the ID:s instead in the second post. like:
{
"Name": "SecondA",
"Description": "SecondADesc",
"Bs" : [{"Id":"1"},{"Name":"FirstB"}]
}
but this does not work either.
Is the only way of achieving this to replace the scaffolding code from the controller and check each object in Bs manually if it exist already?.
Basically its a "Tags to a post problem"...
I've looked into the Attached vs Detached Data subject and read articles on the matter without finding an answer that i can understand whether this can be done automatically with some proper annotations or if it has to be done "manually" in the controller.
Thanks in Advance!
Not much response on this one,
Ended up looping and manually checking for an existing entry like below.
[ResponseType(typeof(A))]
public async Task<IHttpActionResult> PostGame(A a)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
foreach (var asd in a.Bs.ToList())
{
var t = db.Bs.FirstOrDefault(a => a.Name == asd.Name);
if (t != null)
{
a.Bs.Remove(asd);
a.Bs.Add(t);
}
}
db.As.Add(a);
await db.SaveChangesAsync();
return CreatedAtRoute("DefaultApi", new { id = a.Id }, a);
}
Cant help to feel that there has to be a better way then this though.
Let's say I have 3 models:
[Table("UserProfile")]
public class UserProfile //this is a standard class from MVC4 Internet template
{
[Key]
[DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
public string UserName { get; set; }
}
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<Post> Posts { get; set; }
}
public class Post
{
public int CategoryId { get; set; }
public virtual Category Category { get; set; }
public int UserProfileId { get; set; }
[ForeignKey("UserProfileId")]
public virtual UserProfile UserProfile { get; set; }
}
Now, I'm trying to edit Post
[HttpPost]
public ActionResult Edit(Post post)
{
post.UserProfileId = context.UserProfile.Where(p => p.UserName == User.Identity.Name).Select(p => p.UserId).FirstOrDefault();
//I have to populate post.Category manually
//post.Category = context.Category.Where(p => p.Id == post.CategoryId).Select(p => p).FirstOrDefault();
if (ModelState.IsValid)
{
context.Entry(post.Category).State = EntityState.Modified; //Exception
context.Entry(post.UserProfile).State = EntityState.Modified;
context.Entry(post).State = EntityState.Modified;
context.SaveChanges();
return RedirectToAction("Index");
}
return View(post);
}
And I'm getting ArgumentNullException.
Quick look into debug and I can tell that my Category is null, although CategoryId is set to proper value.
That commented out, nasty-looking trick solves this problem, but I suppose it shouldn't be there at all. So the question is how to solve it properly.
I would say it's something with EF lazy-loading, beacuse I have very similar code for adding Post and in debug there is same scenerio: proper CategoryId, Category is null and despite of that EF automagically resolves that Post <-> Category dependency, I don't have to use any additional tricks.
On edit method, EF has some problem with it, but I cannot figure out what I'm doing wrong.
This is working as intended. Your Post object is not attached to the Context, so it has no reason to do any lazy loading. Is this the full code? I don't understand why you need to set Category as Modified since you're not actually changing anything about it.
Anyway, I recommend you query for the existing post from the Database and assign the relevant fields you want to let the user modify, like such:
[HttpPost]
public ActionResult Edit(Post post)
{
var existingPost = context.Posts
.Where(p => p.Id == post.Id)
.SingleOrDetault();
if (existingPost == null)
throw new HttpException(); // Or whatever you wanna do, since the user send you a bad post ID
if (ModelState.IsValid)
{
// Now assign the values the user is allowed to change
existingPost.SomeProperty = post.SomeProperty;
context.SaveChanges();
return RedirectToAction("Index");
}
return View(post);
}
This way you also make sure that the post the user is trying to edit actually exists. Just because you received some parameters to your Action, doesn't mean they're valid or that the post's Id is real. For example, some ill intended user could decide to edit posts he's not allowed to edit. You need to check for this sort of thing.
UPDATE
On a side note, you can also avoid manually querying for the current user's Id. If you're using Simple Membership you can get the current user's id with WebSecurity.CurrentUserId.
If you're using Forms Authentication you can do Membership.GetUser().ProviderUserKey.
Conclusion in Images, can be found in the bottom
I'm having some trouble to get how Forms work in MVC (as I'm a WebForms Developer and really wanna start using MVC in one big project)
I took the MVC2 Web Project and add a simple ViewModel to it
namespace TestForms.Models
{
public class myFormViewModel
{
public myFormViewModel() { this.Options = new List<myList>(); }
public string Name { get; set; }
public string Email { get; set; }
public List<myList> Options { get; set; }
}
public class myList
{
public myList() { this.Value = this.Name = ""; this.Required = false; }
public string Name { get; set; }
public string Value { get; set; }
public string Type { get; set; }
public bool Required { get; set; }
}
}
Created a Strongly Typed View, passed a new object to the view and run it.
When I press submit, it does not return what's in the Options part... how can I bind that as well?
my view
alt text http://www.balexandre.com/temp/2010-10-11_1357.png
filling up the generated form
alt text http://www.balexandre.com/temp/2010-10-11_1353.png
when I press Submit the Options part is not passed to the Model! What am I forgetting?
alt text http://www.balexandre.com/temp/2010-10-11_1352.png
Conclusion
Changing the View loop to allocate the sequential number, we now have
<%= Html.TextBox("model.Options[" + i + "].Value", option.Value)%>
model is the name of our Model variable that we pass to the View
Options is the property name that is of type List
and then we use the property name
Looking at your UI it seems that you did not put the data from the Options member on it.
<% foreach (myList obj in Model.Options) { %>
// Add the object to your UI. they will be serialized when the form is submitted
<% } %>
Also check that you enclose the data in a form element
EDIT:
Sorry! I did'nt realized that you was filling the object inside the controller. Can you please show the code you have in the view?