У меня простая модель:
public class Post
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { set; get; }
//non-important properties stripped, to focus on problem
public virtual Resource Resource { set; get; }
public virtual ICollection<Tag> Tags { set; get; }
}
public class Resource
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { set; get; }
[Url]
public string Url { set; get; }
}
и DbContext (я использую идентификатор ASP.NET в этом проекте, если это имеет значение):
public class DbContext : IdentityDbContext<User>
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
var postEntity = modelBuilder.Entity<Post>();
postEntity.Reference(p => p.Resource).InverseCollection(); //no navigation property on Resource side
postEntity.Collection(p => p.Tags).InverseReference(tag => tag.Post);
postEntity.ToTable("Posts");
var resourceEntity = modelBuilder.Entity<Resource>();
resourceEntity.ToTable("Resources");
var tagEntity = modelBuilder.Entity<Tag>();
tagEntity.Reference(t => t.Post).InverseCollection(p => p.Tags).Required(false);
tagEntity.ToTable("Tags");
}
}
После миграции (SQL Server) таблицы базы данных выглядят как ожидаемые. В таблице Post есть внешний ключ для ResourceId.
Создание работ Post отлично, когда я прикрепляю post.Resource (уже созданный Resource).
Моя проблема возникает, когда я хочу заменить post.Resource . Вместо этого я хочу выбрать один из уже существующих ресурсов и назначить его для публикации.
var resource2 = Database.Resources.First(r=>r.Url == "xyz");
Я пытался:
post.Resource = resource2; Database.Entry(post).State = EntityState.Modified;
Database.Entry(post).Property(p => p.Resource).CurrentValue = resource2;
post.Resource = null;
С их различными комбинациями , но ни одна из них не работает. После вызова await SaveChangesAsync();
и поиск в базе данных - изменений нет. Как правильно выполнить замену (обновление внешнего ключа)?
// Обновление 14.09.2015 Проблема вызвана дополнительным выбором, выполняемым для обновления отношений «Один-ко-многим». Полный код:
var Database new DbContext();
var resource2 = Database.Resources.First(r=>r.Url == "xyz");
var oldAssignedTags = Database.Posts.Include(p=>p.Tags).FirstOrDefault(p => p.Id == post.Id).Tags;
var tags = newTags as List<Tag> ?? newTags.ToList();
//TagComparer is irrelevant here
var toRemove = oldAssignedTags.Except(tags, TagComparer);
var toAdd = tags.Except(oldAssignedTags, TagComparer);
foreach (var tag in toRemove)
{
Database.Entry(tag).State = EntityState.Deleted; //Database.Tags.Remove(tag);
}
foreach (var tag in toAdd)
{
tag.Post = post;
post.Tags.Add(tag);
}
post.Resource = resource2;
await Database.SaveChangesAsync();
Я думал, что это может иметь какое-то отношение к Eager Loading , однако я не могу воспроизвести вашу проблему с AspNet.Identity
или без AspNet.Identity
. Выполнение приведенного ниже кода приводит к постоянно обновляемому ресурсу. Использование EntityFramework 7.0.0-beta7
.
Код
static void Main(string[] args)
{
Init();
WithEagerLoading();
CleanUp();
Init();
WithoutEagerLoading();
CleanUp();
}
private static void WithoutEagerLoading()
{
var db = new MyContext();
var post = db.Posts.First(); // no eager loading of child
post.Resource = db.Resources.First(p => p.Url == "http://trend.atqu.in");
db.SaveChanges();
Console.WriteLine($"2nd Resource.Id: {new MyContext().Posts.Include(p => p.Resource).First().Resource.Id}");
}
private static void WithEagerLoading()
{
var db = new MyContext();
var post = db.Posts
.Include(p => p.Resource) // eager loading
.First();
post.Resource = db.Resources.First(p => p.Url == "http://trend.atqu.in");
db.SaveChanges();
Console.WriteLine($"2nd Resource.Id: {new MyContext().Posts.Include(p => p.Resource).First().Resource.Id}");
}
private static void CleanUp()
{
var db = new MyContext();
db.Posts.RemoveRange(db.Posts);
db.Resources.RemoveRange(db.Resources);
db.SaveChanges();
}
private static void Init()
{
var db = new MyContext();
var resource = new Resource { Url = "http://atqu.in" };
var resource2 = new Resource { Url = "http://trend.atqu.in" };
var post = new Post { Resource = resource };
db.Add(post);
db.Add(resource);
db.Add(resource2);
db.SaveChanges();
db = new MyContext();
post = db.Posts.Include(p => p.Resource).First();
resource = db.Resources.First(p => p.Url == "http://trend.atqu.in");
Console.WriteLine($"1st Resource.Id: {post.Resource.Id}");
}
результат
1st Resource.Id: 0f4d222b-4184-4a4e-01d1-08d2bc9cea9b
2nd Resource.Id: 00ccae9c-f0da-43e6-01d2-08d2bc9cea9b
1st Resource.Id: 749f08f0-2426-4043-01d3-08d2bc9cea9b
2nd Resource.Id: 2e83b512-e8bd-4583-01d4-08d2bc9cea9b
Редактировать 16/9
Проблема в редактированном коде вопроса заключается в том, что вы создаете Database
после того, как вы получили post
. post
не привязан к этому экземпляру Database
, поэтому, когда вы присоединяете к нему Resource
и вызываете SaveChangesAsync
он ничего не делает, потому что post
в то время имеет отношение к базе Database
вы сохраняете. Вот почему ваше обходное решение для выбора post
снова (после создания экземпляра) заставляет его фиксировать - потому что тогда прикрепляется экземпляр post
. Если вы не хотите снова выбирать post
, вы должны использовать тот же экземпляр DbContext
чтобы выполнить DbContext
выше работу, которую вы использовали для первоначального получения post
.