Delete entity but keep one of the related one - c#

I need to delete a record of an entity but keep all the records of another entity that is related with it:
Entity record to remove is:
public class Ask
{
// Primary properties
public int Id { get; set; }
public string Title { get; set; }
public string Message { get; set; }
public DateTime DateCreated { get; set; }
}
The related records that I want to keep after deleting an Ask record is of type MessageAsk :
public class Message
{
// Primary properties
public int Id { get; set; }
public string NameFrom { get; set; }
public string EmailFrom { get; set; }
public string TelephoneFrom { get; set; }
public string Title { get; set; }
public string MessageText { get; set; }
public bool? Approved { get; set; }
public DateTime? DateCreated { get; set; }
public DateTime? DateRead { get; set; }
// Navigation properties
public Member MemberFrom { get; set; }
public Member MemberTo { get; set; }
public MessageType MessageType { get; set; }
public Message MessageParent { get; set; }
}
public class MessageAsk : Message
{
public Ask Ask { get; set; }
}
Resuming, I want to delete an Ask and keep all related MessageAsk's.
EDIT:
I use the service Delete:
private readonly IRepository<Ask> _askRepository;
private readonly IRepository<MessageAsk> _messageAskRepository;
public bool Delete(int askId)
{
try
{
Ask askToDelete = _askRepository.GetById(askId);
IList<MessageAsk> relatedMessageAsks = _messageAskRepository.Query.Where(m => m.Ask.Id == askId).ToList();
_askRepository.Delete(askToDelete);
_askRepository.Save();
}
catch
{
return false;
}
return true;
}
And I use a repository to Delete the Entity:
public class Repository<T> : IRepository<T> where T : class
{
protected DbContext _dataContext;
protected DbSet<T> _dbSet;
public Repository(DbContext context)
{
_dataContext = context;
_dbSet = _dataContext.Set<T>();
}
public T NewEntityInstance()
{
return _dbSet.Create();
}
public void Delete(T entity)
{
if (_dataContext.Entry(entity).State == EntityState.Detached)
{
_dbSet.Attach(entity);
}
_dbSet.Remove(entity);
}
public virtual void Delete(object id)
{
T entity = _dbSet.Find(id);
Delete(entity);
}
public T GetById(int id)
{
return _dbSet.Find(id);
}
public virtual IQueryable<T> Query
{
get
{
return _dbSet.AsNoTracking(); <------ SOURCE OF THE PROBLEM - I HAD TO REMOVE THE ASNOTRACKING OPTION TO SOLVE THE PROBLEM
}
}
}
Error I get now:
"The DELETE statement conflicted with the REFERENCE constraint "FK_Messages_Asks". The conflict occurred in database "Heelp", table "dbo.Messages", column 'Ask_Id'.
Thanks

If your relationship is optional (that is, the foreign key from MessageAsk table to Ask table allows NULL values), you can do it this way:
using (var context = new MyContext())
{
var askToDelete = context.Asks.Single(a => a.Id == askToDeleteId);
var relatedMessageAsks = context.MessageAsks
.Where(m => m.Ask.Id == askToDeleteId)
.ToList();
// or just: context.MessageAsks.Where(m => m.Ask.Id == askToDeleteId).Load();
context.Asks.Remove(askToDelete);
// or DeleteObject if you use ObjectContext
context.SaveChanges();
}
(or context.Messages.OfType<MessageAsk>()... instead of context.MessageAsks... if you don't have a set for the derived type in your context)
You don't need to set the MessageAsk.Ask property to null explicitly here. EF will do that automatically when the askToDelete is removed and update the MessageAsk with FK = NULL in the database.
It does not work if the relationship is required (no NULLs for the FK are allowed) as you would violate a referential foreign key constraint in the database when the principal (askToDelete) would be deleted. In that case you need to assign the relatedMessageAsks to another Ask before you delete the askToDelete.

Related

Identity framework tries to insert new object when I want to reference an existing object

I am making a web app using blazer and identity framework. I would like the object BoardYear to have a one-to-one relation with Member as chairman which is an IdentityUser class and selected by a dropdown.
When I try to insert a new BoardYear with an exciting Member as chairman I get the following SQL exception.
SqlException: Violation of PRIMARY KEY constraint 'PK_AspNetUsers'. Cannot insert duplicate key in object 'dbo.AspNetUsers'. The duplicate key value is (de20c079-ed99-4bf9-9474-d8eb1a05b5b6).
It seems like Entity Framework wants to add the referred Member to the database, which is not possible because it already exists.
The member class:
public class Member : IdentityUser
{
public string FirstName { get; set; }
public string LastName { get; set; }
public Guid? ChairmanBoardYearId { get; set; }
public BoardYear ChairmanBoardYear { get; set; }
...
}
Boardyear class:
public class BoardYear
{
[Key]
public Guid BoardYearId { get; set; }
[DataType(DataType.Date)]
public DateTime StartDate { get; set; }
[DataType(DataType.Date)]
public DateTime EndDate { get; set; }
public Member Chairman { get; set; }
}
The relation is configured in the dbcontext like this:
public class ApplicationDbContext : IdentityDbContext<Member>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
builder.Entity<BoardYear>()
.HasOne(m => m.Chairman)
.WithOne(b => b.ChairmanBoardYear)
.HasForeignKey<Member>(m => m.ChairmanBoardYearId);
base.OnModelCreating(builder);
}
public DbSet<BoardYear> BoardYears { get; set; }
public DbSet<Commission> Commissions { get; set; }
public DbSet<CommissionFine> CommissionFines { get; set; }
public DbSet<MemberFine> MemberFines { get; set; }
public DbSet<Meeting> Meetings { get; set; }
public DbSet<MeetingFile> MeetingFiles { get; set; }
public DbSet<MemberSanction> MemberSanctions { get; set; }
public DbSet<CommissionSanction> CommissionSanctions { get; set; }
public DbSet<YearGroup> YearGroups { get; set; }
public DbSet<CommissionList> CommissionLists { get; set; }
}
When the dropdown is edited the chairman is assigned like this in the razor page (I can see it gets the correct member):
async void OnBoardUserChange()
{
Member selectedMember = await MemberService.FindMember(selectedChairmanId);
curBoardYear.Chairman = selectedMember;
}
And then put in the database in the following service:
public class BoardYearService
{
private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;
private readonly DidaDbContext _db;
public BoardYearService(IDbContextFactory<ApplicationDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public async Task<int> Create(BoardYear boardYear)
{
using (var ctx = _contextFactory.CreateDbContext())
{
CommissionList boardYearCommissionList = new CommissionList();
boardYear.CommissionList = boardYearCommissionList;
ctx.BoardYears.Add(boardYear);
return await ctx.SaveChangesAsync();
}
}
}
The problem is that you have retrieved the user from whatever context instance MemberService.FindMember uses, but are referencing it in a BoardYear added to a new context instance.
The second context instance, where the BoardYear is added is not tracking the Member, so assumes it is an addition.
Either perform the whole action using data retrieved and persisted back through the same context instance, or just use an Id field on the BoardYear instead of a navigation property.

EF Core .ThenInclude does not include foreign entity and causes query to retrieve nothing

I have an EF Core project here, but I do have difficulties with multilevel includes. I'm trying to query entries which do relate like this:
There's a mapping table for "friend" relationships from accountid to accountid. So layer one is this mapping entity.
The IDs of the accounts in the mapping table are foreign keys relating to the respective Account entity.
Within the account entity, there's a foreign key to an account online state entity.
So tl;dr; FriendsMappingTable -> Account -> AccountOnlineState.
Here's the code I do use:
public Task<List<TEntity>> Read(Expression<Func<TEntity, bool>> predicate, params Func<IQueryable<TEntity>, IQueryable<TEntity>>[] foreignIncludes)
{
return RunInContextWithResult(async dbSet =>
{
var query = dbSet.Where(predicate);
query = foreignIncludes.Aggregate(query, (current, include) => include(current));
return await query.ToListAsync();
});
}
private async Task<List<TEntity>> RunInContextWithResult([NotNull] Func<DbSet<TEntity>, Task<List<TEntity>>> dbFunc)
{
await using var ctx = GetContext();
return await dbFunc(ctx.Set<TEntity>());
}
and here's my call to that:
var friends = await m_friendsMappingRepository.Read(
x => x.Id == sessionContext.Account.Id,
x => x.Include(y => y.Friend).ThenInclude(y => y.AccountOnlineStateEntity));
However, with this setup, the query will just return nothing at all. If I remove the .ThenInclude(), it will at least return a corresponding friend entity for the given account, with the OnlineState entity set to null.
Here are the (stripped down) entities:
public interface IEntity<TKeyType>
{
[NotNull]
[Key]
[Column("Id")]
public TKeyType Id { get; set; }
}
[Table("FriendsMapping")]
public class FriendsMappingEntity : IEntity<int>
{
[ForeignKey("Account")]
public int Id { get; set; }
public AccountEntity Account {
get;
[UsedImplicitly] private set;
}
[Column("FriendId")]
[ForeignKey("Friend")]
public int FriendId { get; set; }
public AccountEntity Friend
{
get;
[UsedImplicitly] private set;
}
}
public class AccountEntity : IEntity<int>
{
[ForeignKey("AccountOnlineStateEntity")]
public int Id { get; set; }
[CanBeNull]
public AccountOnlineStateEntity AccountOnlineStateEntity { get; set; }
[NotNull]
public List<FriendsMappingEntity> FriendsTo { get; set; }
[NotNull]
public List<FriendsMappingEntity> FriendsFrom { get; set; }
}
public class AccountOnlineStateEntity : IEntity<int>
{
public int Id { get; set; }
[Column("OnlineState")]
public AccountOnlineState OnlineState { get; set; }
}
Update
Building on Ivan's suggestion, add an InverseProperty and remove the ForeignKey from Account.Id.
//[ForeignKey("AccountOnlineStateEntity")]
public int Id { get; set; }
[CanBeNull]
[InverseProperty("Account")
public AccountOnlineStateEntity AccountOnlineStateEntity { get; set; }
And add a property to AccountOnlineStateEntity
[ForeignKey("Id")]
public AccountEntity Account { get; set; }

Entity Framework Core not filling object by foreign key on update

I see this error when updating an Application. The object has two external connections: ApplicationVisitors and ApplicationPlatforms. The properties in Application have been updated, but external connections was not be updated.
What am I doing wrong? How to update Application correctly?
Route
[Route("update")]
[HttpPut]
public async Task<IActionResult> UpdateApplication(ApplicationDTO applicationDTO)
{
if (!ModelState.IsValid)
return BadRequest("Модель не валидна!");
await applicationService.UpdateApplication(applicationDTO);
return Ok();
}
Service
public async Task UpdateApplication(ApplicationDTO applicationDTO)
{
var visitors = Mapper.ToVisitors(applicationDTO.ApplicationVisitors);
var visitorsToCreate = visitors.Where(w => w.Id == 0).ToList();
var createdVisitors = visitors.Where(w => w.Id > 0).ToList();
var resultCreateVisitors = await _wrapper.Visitor.CreateVisitorsAsync(visitorsToCreate);
createdVisitors.AddRange(resultCreateVisitors);
applicationDTO.ApplicationVisitors = Mapper.ToVisitors(createdVisitors);
await _wrapper.Application.UpdateAsync(Mapper.ToApplication(applicationDTO));
}
Repository method
public async Task UpdateAsync(Application application)
{
Update(application);
await SaveAsync();
}
BaseRepository
public void Update(T entity)
{
_repositoryContext.Set<T>().Attach(entity);
_repositoryContext.Entry(entity).State = EntityState.Modified;
}
public async Task SaveAsync()
{
await _repositoryContext.SaveChangesAsync();
}
I have not any exeption in debug. Application was filled by ID, but Platform and Visitor in collections ApplicationPlatforms and ApplicationVisitors does not filling by foreign key.
References is existing in classes.
view result Attach
Application
public class Application
{
public int Id { get; set; }
[Column(TypeName="date")]
public DateTime DateStart { get; set; }
[Column(TypeName = "date")]
public DateTime DateEnd { get; set; }
public int ApproverId { get; set; }
public User Approver { get; set; }
public int StatusId { get; set; }
public ApplicationStatus Status { get; set; }
public string VisitPurpose { get; set; }
public DateTime CreatedAt { get; set; }
public int AuthorId { get;set; }
public User Author { get; set; }
public IList<Draft> Drafts { get; set; }
public IList<ApplicationVisitor> ApplicationVisitors { get; set; }
public IList<ApplicationPlatform> ApplicationPlatforms { get; set; }
public IList<Pass> Passes { get; set; }
}
ApplicationVisitor
public class ApplicationVisitor
{
public int ApplicationId { get; set; }
[ForeignKey("ApplicationId")]
public Application Application { get; set; }
public int VisitorId { get; set; }
[ForeignKey("VisitorId")]
public Visitor Visitor { get; set; }
}
ApplicationPlatform
public class ApplicationPlatform
{
public int ApplicationId { get; set; }
[ForeignKey("ApplicationId")]
public Application Application { get; set; }
public int PlatformId { get; set; }
[ForeignKey("PlatformId")]
public Platform Platform { get; set; }
}
UPD: 22.10.2019. This is my solution what working for me on base selected answer!
I rewrited update method in ApplicationRepository
public async Task UpdateAsync(Application application)
{
var app = await GetAsync(application.Id);
app.DateStart = application.DateStart;
app.DateEnd = application.DateEnd;
app.ApproverId = application.ApproverId;
app.StatusId = application.StatusId;
app.VisitPurpose = application.VisitPurpose;
app.CreatedAt = application.CreatedAt;
app.AuthorId = application.AuthorId;
app.ApplicationVisitors = application.ApplicationVisitors;
app.ApplicationPlatforms = application.ApplicationPlatforms;
Update(app);
await SaveAsync();
}
And rewrided method in my BaseRepository
public void Update(T entity)
{
_repositoryContext.Set<T>().Update(entity);
}
Although you attached the Application object and set its state to Modified, all other objects references by it will be in Unchanged state, as stated here:
For entity types with generated keys if an entity has its primary key
value set then it will be tracked in the Unchanged state. If the
primary key value is not set then it will be tracked in the Added
state. This helps ensure only new entities will be inserted. An entity
is considered to have its primary key value set if the primary key
property is set to anything other than the CLR default for the
property type.
You can either manually set the state of all referenced objects or, A better approach in my option, is to load the objects from the database (and have EF track them for changes) and modify these objects. These way EF will know the exact state for each object when it comes to save it to the database.

Entity Framework Generalise Id and Parent Name but have specific names like "ReferralId" and "ClientID"

I have a lot of models in my program that do similar things but we have a database practice to give each column specific names so "PersonId" instead of "Id"
But I would like to deal with them in a generic way. So for example in a class I have included below I want to do something like:
public virtual List<TModel> GetAll(int parentId)
{
return Context.Where(i => i.ParentID == parentId).ToList();
}
But I can't do that if ParentID unless its mapped to a database column
The specified type member is not supported in LINQ to Entities. Only initializers, entity members, and entity navigation properties are supported
So I am now trying to map it to a database column:
public interface IGenModel
{
int Id { get; set; }
int ParentID { get; set; }
}
public class ChildNote : BaseModel, IGenModel
{
[Key]
[DisplayName("Child Note ID")]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[Column("ChildId")]
public int Id { get; set; }
[DisplayName("Referral")]
[Column("ReferralId")]
public int ParentID { get; set; }
[DisplayName("Cinican Notes")]
public string Notes { get; set; }
[ForeignKey("ReferralId")]
public virtual Referral Referral { get; set; }
/* Finally I tried to add something like this here
[NotMapped]
[Required]
[DisplayName("Referral")]
public int ReferralId => ParentID
*/
/* Original set uup
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
[DisplayName("Child Note ID")]
public int ChildId { get; set; }
[Required]
[DisplayName("Referral")]
public int ReferralId { get; set; }
public int Id { get; set; }
{
get { return ChildId; }
set { ChildId = value; }
}
public int ParentID { get; set; }
{
get { return ReferralId; }
set { ReferralId = value; }
}*/
}
But when I try and build or update the database I get errors due to:
[ForeignKey("ReferralId")]
public virtual Referral Referral { get; set; }
As it says ReferralId does not map to something in the model.
Am I fighting a losing battle here? Is is simply not possible to generalise this? And just accept that all my accesses of context may have to be specialised? I am not going to be able to change the rule that we always prefix "Id" with the model name, apparently its very useful for searches within the database which makes sense to me.
and the classes I want to use
public abstract class GenLoader<TSelf, TModel> where TModel : BaseModel, IGenModel where TSelf : class, new()
{
private static TSelf instance = null;
public static TSelf Instance
{
get
{
if (instance == null)
{
instance = new TSelf();
}
return instance;
}
}
protected abstract DbSet<TModel> Context { get; }
public virtual TModel Get(int id)
{
return Context.FirstOrDefault(i => i.Id == id);
}
public virtual List<TModel> GetAll(int parentId)
{
return Context.Where(i => i.ParentID == parentId).ToList();
}
public virtual void OnAdd(TModel model)
{
}
public virtual void Add(TModel model)
{
model.LastModifiedBy = WebSecurity.CurrentUserId;
model.LastModified = DateTime.Now;
OnAdd(model);
using (TransactionScope scope = new TransactionScope())
{
Context.Add(model);
ModelContext.Current.SaveChanges();
scope.Complete();
}
}
public virtual void OnUpdate(TModel model)
{
}
public void Update(TModel model)
{
model.LastModifiedBy = WebSecurity.CurrentUserId;
model.LastModified = DateTime.Now;
OnUpdate(model);
using (TransactionScope scope = new TransactionScope())
{
ModelContext.Current.Entry(model).State = EntityState.Modified;
ModelContext.Current.SaveChanges();
scope.Complete();
}
}
}
public class ChildNotes : GenLoader<ChildNotes, ChildNote>
{
protected override DbSet<ClinicalNote> Context => ModelContext.Current.ClinicalNotes;
}
When applied to navigation property, ForeignKey attribute expects the FK property name, not the mapped db column name. So the following should fix the issue:
[ForeignKey("ParentID")]
public virtual Referral Referral { get; set; }
The alternative is, since you put many attributes on FK property, remove the ForeignKey attribute from navigation property and apply it on the FK property, in which case it expects the navigation property name as an argument:
[DisplayName("Referral")]
[Column("ReferralId")]
[ForeignKey("Referral")]
public int ParentID { get; set; }
...
public virtual Referral Referral { get; set; }

EF Code first + Delete Child Object from Parent?

I have a one-to-many relationship between my table Case and my other table CaseReplies. I'm using EF Code First and now wants to delete a CaseReply from a Case object, however it seems impossible to do such thing because it just tries to remove the CaseId from the specific CaseReply record and not the record itself..
I've tried to set cascade delete/update at database level without luck...
short: Case just removes the relationship between itself and the CaseReply.. it does not delete the CaseReply.
My code:
// Case.cs (Case Object)
public class Case
{
[Key]
public int Id { get; set; }
public string Topic { get; set; }
public string Message { get; set; }
public DateTime Date { get; set; }
public Guid UserId { get; set; }
public virtual User User { get; set; }
public virtual ICollection<CaseReply> Replies { get; set; }
}
// CaseReply.cs (CaseReply Object)
public class CaseReply
{
[Key]
public int Id { get; set; }
public string Message { get; set; }
public DateTime Date { get; set; }
public int CaseId { get; set; }
public Guid UserId { get; set; }
public virtual User User { get; set; }
public virtual Case Case { get; set; }
}
// RepositoryBase.cs
public class RepositoryBase<T> : IRepository<T> where T : class
{
public IDbContext Context { get; private set; }
public IDbSet<T> ObjectSet { get; private set; }
public RepositoryBase(IDbContext context)
{
Contract.Requires(context != null);
Context = context;
if (context != null)
{
ObjectSet = Context.CreateDbSet<T>();
if (ObjectSet == null)
{
throw new InvalidOperationException();
}
}
}
public IRepository<T> Remove(T entity)
{
ObjectSet.Remove(entity);
return this;
}
public IRepository<T> SaveChanges()
{
Context.SaveChanges();
return this;
}
}
// CaseRepository.cs
public class CaseRepository : RepositoryBase<Case>, ICaseRepository
{
public CaseRepository(IDbContext context)
: base(context)
{
Contract.Requires(context != null);
}
public bool RemoveCaseReplyFromCase(int caseId, int caseReplyId)
{
Case caseToRemoveReplyFrom = ObjectSet.Include(x => x.Replies).FirstOrDefault(x => x.Id == caseId);
var delete = caseToRemoveReplyFrom.Replies.FirstOrDefault(x => x.Id == caseReplyId);
caseToRemoveReplyFrom.Replies.Remove(delete);
return Context.SaveChanges() >= 1;
}
}
Thanks in advance.
If you want EF to remove CaseReply from DB when you remove it from parent's object collection, you have to use Identifying relations. It means that your CaseReply must have composite primary key based on Id and CaseId columns.
I believe you are using EF CT4? I had the same problem too and hated it. I think your code is totally fine.
Since your association is one to many, the only solution is to setting cascading delete at database side. <http://blogs.msdn.com/b/alexj/archive/2009/08/19/tip-33-how-cascade-delete-really-works-in-ef.aspx>
The above solution works for deleting orphans on deleting their parent record. I hope it will solve your issue too.
You have to remove from context.Replies if you want to delete it in db.
What you are doing right now is removing it from case's replies collection which removes the association. So you have to call:
context.Replies.Remove(delete)

Categories