I'm struggling a bit with a many-to-many relation update using Entity Framework. My tables looks like this:
CREATE TABLE Agreement
(
ID int NOT NULL IDENTITY(1,1),
CONSTRAINT [PK_Agreement] PRIMARY KEY CLUSTERED (ID),
);
CREATE TABLE Price
(
ID int NOT NULL IDENTITY(1,1),
ProductPrice decimal(18,5),
CONSTRAINT [PK_Price] PRIMARY KEY CLUSTERED (ID),
);
CREATE TABLE AgreementPriceLine
(
AgreementID int NOT NULL,
PriceID int NOT NULL,
CONSTRAINT [PK_AgreementPriceLine] PRIMARY KEY NONCLUSTERED (AgreementID, PriceID),
CONSTRAINT [FK_AgreementPriceLine_Agreement]
FOREIGN KEY (AgreementID) REFERENCES Agreement (ID)
ON DELETE NO ACTION ON UPDATE CASCADE,
CONSTRAINT [FK_AgreementPriceLine_PriceID]
FOREIGN KEY (PriceID) REFERENCES Price (ID)
);
Which Entity Framework mapped like so (using generate model from database, the .edmx file):
public partial class Agreement
{
public Agreement()
{
this.Prices = new HashSet<Price>();
}
public virtual ICollection<Price> Prices { get; set; }
}
public partial class Price
{
public Price()
{
this.Agreements = new HashSet<Agreement>();
}
public virtual ICollection<Agreement> Agreements { get; set; }
}
Now, if I want to update two prices on an agreement, how would I go about that? I tried the following:
public void UpdateAgreementPriceLines(List<Price> prices, Agreement agreement)
{
try
{
using (var ctx = new MyEntities())
{
if (agreement != null)
{
// Make sure the number of prices are equal before attempting to "update" anything
if(agreement.Prices.Count == prices.Count)
{
// Clear old prices
agreement.Prices.Clear();
// Add new prices
foreach (var price in prices)
{
agreement.Prices.Add(price);
}
ctx.SaveChanges();
}
}
}
}
catch (Exception e)
{
Logging.Instance.Fatal(e.ToString());
}
}
But it's rather 'hacky' to first empty the collection and then add the new prices. Also, I can't get it to work, the prices are not removed/added at all (and no exception is caught)
Any help/hint on this is greatly appreciated :-)
Thanks in advance.
You need to remove the items from the colletion, like this:
agreement.Prices.Remove(priceItem);
To remove all objects you can do the following:
var prices = agreement.Prices.ToList();
agreement.Prices.RemoveRange(prices);
If you don't want to load all items to the memory before the removal method, you can do the following:
//supposing that you want to deleted the Price which id = 10
Price deletedPrice = new Price { PriceId = 10 };
Context.Entry(deletedPrice).State = EntityState.Deleted;
Context.SaveChanges();
Related
I using Entity Framework Core, and I have a table:
public class BlogComment
{
public int Id { get; set; }
public BlogPost Post { get; set; }
[StringLength(100)]
public string AuthorName { get; set; }
[StringLength(254)]
public string AuthorEmail { get; set; }
public bool SendMailOnReply { get; set; }
[StringLength(2000)]
public string Content { get; set; }
public DateTime CreatedTime { get; set; }
public int? ReplyToId { get; set; }
public BlogComment ReplyTo { get; set; }
}
From this, EFC generates the following table:
CREATE TABLE [dbo].[BlogComment] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[AuthorEmail] NVARCHAR (254) NULL,
[AuthorName] NVARCHAR (100) NULL,
[Content] NVARCHAR (2000) NULL,
[CreatedTime] DATETIME2 (7) NOT NULL,
[PostId] INT NULL,
[ReplyToId] INT NULL,
[SendMailOnReply] BIT NOT NULL,
CONSTRAINT [PK_BlogComment] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_BlogComment_BlogPost_PostId] FOREIGN KEY ([PostId]) REFERENCES [dbo].[BlogPost] ([Id]),
CONSTRAINT [FK_BlogComment_BlogComment_ReplyToId] FOREIGN KEY ([ReplyToId]) REFERENCES [dbo].[BlogComment] ([Id])
);
GO
CREATE NONCLUSTERED INDEX [IX_BlogComment_PostId]
ON [dbo].[BlogComment]([PostId] ASC);
GO
CREATE UNIQUE NONCLUSTERED INDEX [IX_BlogComment_ReplyToId]
ON [dbo].[BlogComment]([ReplyToId] ASC) WHERE ([ReplyToId] IS NOT NULL);
Some comments are send as a reply to another, but not all. When the original comment is deleted, the reply becomes a normal comment. So, following this tutorial, the configuration looks is this:
modelBuilder.Entity<BlogComment>()
.HasOne(p => p.ReplyTo)
.WithOne()
.HasForeignKey<BlogComment>(c => c.ReplyToId)
.IsRequired(false)
.OnDelete(DeleteBehavior.SetNull);
The delete method is pretty simple:
var comment = await context.BlogComment.Include(c => c.ReplyTo).SingleAsync(m => m.Id == id);
context.BlogComment.Remove(comment);
await context.SaveChangesAsync();
But I can't run it, I get an error:
System.Data.SqlClient.SqlException: The DELETE statement conflicted with the SAME TABLE REFERENCE constraint "FK_BlogComment_BlogComment_ReplyToId".
How can I fix this?
To wrap up the conversation in the comments:
First, the self reference is a 1:n association:
modelBuilder.Entity<BlogComment>()
.HasOne(p => p.ReplyTo)
.WithMany(c => c.Replies)
.HasForeignKey(c => c.ReplyToId)
.IsRequired(false)
.OnDelete(<we'll get to that>);
So, just for convenience, BlogComment now also has a property
public ICollection<BlogComment> Replies { get; set; }
However, I can't create the table using
.OnDelete(DeleteBehavior.SetNull);
It gives me
Introducing FOREIGN KEY constraint 'FK_BlogComments_BlogComments_ReplyToId' on table 'BlogComments' may cause cycles or multiple cascade paths.
This is a Sql Server restriction we just have to accept, no way to evade it. The only way to get the desired cascade behavior is
.OnDelete(DeleteBehavior.ClientSetNull);
Which is:
For entities being tracked by the DbContext, the values of foreign key properties in dependent entities are set to null. This helps keep the graph of entities in a consistent state while they are being tracked, such that a fully consistent graph can then be written to the database. (...) This is the default for optional relationships.
I.e.: the client executes SQL to nullify the foreign key values. The child records should be tracked though. To remove a BlogComment parent the delete action should look like:
using (var db = new MyContext(connectionString))
{
var c1 = db.BlogComments
.Include(c => c.Replies) // Children should be included
.SingleOrDefault(c => c.Id == 1);
db.BlogComments.Remove(c1);
db.SaveChanges();
}
As you see, you don't have to set ReplyToId = null, that's something EF takes care of.
For me, I had to Include() the entities I needed to be "dealt with" when I deleted an entity. EF cant manage things it is not currently tracking.
var breedToDelete = context.Breed
.Include(x => x.Cats)
.Single(x => x.Id == testBreedId);
context.Breed.Remove(breedToDelete);
context.SaveChanges();
I could get it working by manually setting ReplyTo to null. I'm still looking for a better solution, or an explanation why is it needed. Isn't it what OnDelete(DeleteBehavior.SetNull) supposed to do?
var comment = await context.BlogComment.Include(c => c.ReplyTo).SingleAsync(m => m.Id == id);
var reply = await context.BlogComment.SingleOrDefaultAsync(m => m.ReplyToId == id);
if (reply != null)
{
reply.ReplyTo = null;
reply.ReplyToId = null;
context.Entry(reply).State = EntityState.Modified;
}
context.BlogComment.Remove(comment);
I have the following classes generated from an edmx model:
public partial class A
{
public int Id { get; set; }
public virtual B B { get; set; }
}
public partial class B
{
public int Id { get; set; }
public virtual A A { get; set; }
}
The existing db doesn't use the EF default which expects A.Id to be the primary key of table B:
CREATE TABLE [dbo].[B] (
[Id] INT IDENTITY (1, 1) NOT NULL,
PRIMARY KEY CLUSTERED ([Id] ASC)
);
CREATE TABLE [dbo].[A] (
[Id] INT IDENTITY (1, 1) NOT NULL,
[BId] INT NULL,
CONSTRAINT [fk] FOREIGN KEY ([BId]) REFERENCES [dbo].[B] ([Id])
);
With an edmx model, I can explicitly configure the multiplicity of each end, but I haven't found how to get the equivalent model using the fluent-api. When I do something like the following and generate a new db, the foreign key gets placed in table A instead of table B.
modelBuilder.Entity<A>().HasOptional(a => a.B).WithRequired(b => b.A);
I'm guessing I need to use a convention, but so far I've been unable to get the desired output.
UPDATE:
The closest solution I've found so far is to use the following which generates the correct SQL in the db:
modelBuilder.Entity<A>()
.HasOptional(a => a.B)
.WithOptionalDependent(b => b.A)
.Map(c => c.MapKey("BId"));
However, it's conceptually modeled as a 0..1:0..1 relationship and I haven't found how to set a CASCADE delete rule that deletes B when A is deleted.
I wasn't able to find a direct solution, but using the following code seems to meet my requirements of preserving the existing schema and creating a conceptual model that has the same multiplicities & delete behaviors as my original edmx model.
I'd still be interested in any solutions that don't require updating the conceptual model during the post-processing IStoreModelConvention.
{
var overridesConvention = new OverrideAssociationsConvention();
modelBuilder.Conventions.Add(overridesConvention);
modelBuilder.Conventions.Add(new OverrideMultiplictyConvention(overridesConvention));
}
private class OverrideAssociationsConvention : IConceptualModelConvention<AssociationType>
{
...
public List<AssociationEndMember> MultiplicityOverrides { get; } = new List<AssociationEndMember>();
public void Apply(AssociationType item, DbModel model)
{
if (multiplicityOverrides.Contains(item.Name))
{
// Defer actually updating the multiplicity until the store model is generated
// so that foreign keys are placed in the desired tables.
MultiplicityOverrides.Add(item.AssociationEndMembers.Last());
}
if (cascadeOverrides.Contains(item.Name))
{
item.AssociationEndMembers.Last().DeleteBehavior = OperationAction.Cascade;
}
}
}
private class OverrideMultiplictyConvention : IStoreModelConvention<EdmModel>
{
private readonly OverrideAssociationsConvention overrides;
public OverrideMultiplictyConvention(OverrideAssociationsConvention overrides)
{
this.overrides = overrides;
}
public void Apply(EdmModel item, DbModel model)
{
overrides.MultiplicityOverrides.ForEach(o => o.RelationshipMultiplicity = RelationshipMultiplicity.One);
}
}
I have 2 tables: an Orders table and an OrderActivity table. If no activity has been taken on an order, there will be no record in the OrderActivity table. I currently have the OrderActivity table mapped as an optional nav property on the Order entity and I handle updates to OrderActivity like this:
if (order.OrderActivity == null)
{
order.OrderActivity = new OrderActivity();
}
order.OrderActivity.LastAccessDateTime = DateTime.Now;
Is it possible to consolidate this such that the columns of the OrderActivity table are mapped to properties on the Orders entity, and will default if there is no OrderActivity record? Configuration for entity splitting only appears to work if records exist in both tables. If it is not possible, what is the best practice to obscure the child entity from my domain model? My goal is to keep the model as clean as possible while interacting with a DB schema that I have no control over.
You can create the mapping and specify the type of LastAccessDate as Nullable<DateTime>. The mapping will create one-to-one with LastAccessDate being optional.
public class Order {
[Key]
public int Id { get; set; }
public string Name { get; set; }
public DateTime? LastAccessDate { get; set; }
}
modelBuilder.Entity<Order>().Map(m => {
m.Properties(a => new { a.Id, a.Name });
m.ToTable("Order");
}).Map(m => {
m.Properties(b => new { b.Id, b.LastAccessDate });
m.ToTable("OrderActivity");
});
In this case, specifying LastAccessDate property is optional when inserting new orders.
var order = new Order();
order.Name = "OrderWithActivity";
order.LastAccessDate = DateTime.Now;
db.Orders.Add(order);
db.SaveChanges();
order = new Order();
order.Name = "OrderWithoutActivity";
db.Orders.Add(order);
db.SaveChanges();
Note this will always create one entry in each table. This is necessary because EF creates INNER JOIN when you retrieve Orders and you want to get all orders in this case. LastAccessDate will either have a value or be null.
// Gets both orders
var order = db.Orders.ToList();
// Gets only the one with activity
var orders = db.Orders.Where(o => o.LastAccessDate != null);
I use EF 3.5 and have a db with a category table. I've created a partial class to expand the class created by EF. I have CategoryId as a key in the db and it is set to Identity in the model. This is my partial class:
public partial class Category
{
public Category(string name, bool isChild)
{
this.CatName = name;
this.IsChild = isChild;
}
public bool Save()
{
try
{
using (var context = new PhonebookEntities())
{
context.AddToCategories(this);
context.SaveChanges();
}
return true;
}
catch (System.Exception)
{
return false;
}
}
}
But when I try to create a new Category object and save it..:
var category = new Category("Test", false);
category.Save();
I get this exception: "Violation of UNIQUE KEY constraint 'IX_Category'. Cannot insert duplicate key in object 'dbo.Category'.\r\nThe statement has been terminated."
I should mention that a category has a reference to itself because it can have a parent category through a nullable int which points to the categoryid to the parent category.
What is the contents of IX_Category? If it is not needed, delete it. Is there another category with "Test" as its CatName in your database? Is IX_Category a unique key constraint on the CatName field?
I'm overriding SaveChanges on my DbContext in order to implement an audit log. Working with many-to-many relationships or independent associations is relatively easy as EF creates ObjectStateEntries for any changes to those kinds of relationships.
I am using foreign key associations, and when a relationship between entities changes all you get is an ObjectStateEnty that says for example entity "Title" has "PublisherID" property changed. To a human this is obviously a foreign key in Title entity, but how do I determine this in runtime? Is there a way to translate this change to a "PublisherID" property to let's an EntityKey for the entity that foreign key represents?
I assume I'm dealing with entities that look like this:
public sealed class Publisher
{
public Guid ID { get; set; }
public string Name { get; set; }
public ICollection<Title> Titles { get; set; }
}
public class Title
{
public Guid ID { get; set; }
public string Name { get; set; }
public Guid? PublisherID { get; set; }
public Publisher Publisher { get; set; }
}
There is also EF EntityConfiguration code that defines the relationship and foreign key:
public TitleConfiguration()
{
HasOptional<Publisher>(t => t.Publisher).WithMany(
p => p.Titles).HasForeignKey(t => t.PublisherID);
}
What I'm doing now seems a bit too complicated. I'm hoping there is more elegant way to achieve my goal. For every modified property from ObjectStateEntry I look through all ReferentialConstraints for current entity and see if any of those use it as a foreign key. The code below is called from SaveChanges():
private void HandleProperties(ObjectStateEntry entry,
ObjectContext ctx)
{
string[] changedProperties = entry.GetModifiedProperties().ToArray();
foreach (string propertyName in changedProperties)
{
HandleForeignKey(entry, ctx, propertyName);
}
}
private void HandleForeignKey(ObjectStateEntry entry,
ObjectContext ctx, string propertyName)
{
IEnumerable<IRelatedEnd> relatedEnds =
entry.RelationshipManager.GetAllRelatedEnds();
foreach (IRelatedEnd end in relatedEnds)
{
// find foreign key relationships
AssociationType elementType = end.RelationshipSet.ElementType as
AssociationType;
if (elementType == null || !elementType.IsForeignKey) continue;
foreach (ReferentialConstraint constraint in
elementType.ReferentialConstraints)
{
// Multiplicity many means we are looking at a foreign key in a
// dependent entity
// I assume that ToRole will point to a dependent entity, don't
// know if it can be FromRole
Debug.Assert(constraint.ToRole.RelationshipMultiplicity ==
RelationshipMultiplicity.Many);
// If not 1 then it is a composite key I guess.
// Becomes a lot more difficult to handle.
Debug.Assert(constraint.ToProperties.Count == 1);
EdmProperty prop = constraint.ToProperties[0];
// entity types of current entity and foreign key entity
// must be the same
if (prop.DeclaringType == entry.EntitySet.ElementType
&& propertyName == prop.Name)
{
EntityReference principalEntity = end as EntityReference;
if (principalEntity == null) continue;
EntityKey newEntity = principalEntity.EntityKey;
// if there is more than one, the foreign key is composite
Debug.Assert(newEntity.EntityKeyValues.Length == 1);
// create an EntityKey for the old foreign key value
EntityKey oldEntity = null;
if (entry.OriginalValues[prop.Name] is DBNull)
{
oldEntity = new EntityKey();
oldEntity.EntityKeyValues = new[] {
new EntityKeyMember("ID", "NULL")
};
oldEntity.EntitySetName = newEntity.EntitySetName;
}
else
{
Guid oldGuid = Guid.Parse(
entry.OriginalValues[prop.Name].ToString());
oldEntity = ctx.CreateEntityKey(newEntity.EntitySetName,
new Publisher()
{
ID = oldGuid
});
}
Debug.WriteLine(
"Foreign key {0} changed from [{1}: {2}] to [{3}: {4}]",
prop.Name,
oldEntity.EntitySetName, oldEntity.EntityKeyValues[0],
newEntity.EntitySetName, newEntity.EntityKeyValues[0]);
}
}
}
}
I hope this helps to illustrate better what I am trying to achieve. Any input is welcome.
Thanks!
Looks like my code is the right solution to this problem :/
I did end up using independent associations to avoid this problem altogether.