Entity model:
public class DocumentType : CodeBase
{
[Required]
[MaxLength(100)]
public string Name { get; set; }
public TimeSpan? Productiontime { get; set; }
public bool IsDeliverable { get; set; }
public virtual ICollection<DocumentTypeRetractRelation> DocumentTypes { get; set; }
public virtual ICollection<DocumentTypeRetractRelation> RetractDocumentTypes { get; set; }
}
Relation model:
/// <summary>
/// Relationship between document types showing which documenttypes can
/// retracted when delivering a new document.
/// </summary>
[Table("DocumentTypeRetractRelation")]
public class DocumentTypeRetractRelation
{
public int DocumentTypeId { get; set; }
public virtual DocumentType DocumentType { get; set; }
public int RetractDocumentTypeId { get; set; }
public virtual DocumentType RetractDocumentType { get; set; }
}
Model builder:
modelBuilder.Entity<DocumentTypeRetractRelation>().HasKey(x => new { x.DocumentTypeId, x.RetractDocumentTypeId });
modelBuilder.Entity<DocumentTypeRetractRelation>()
.HasOne(x => x.DocumentType)
.WithMany(x => x.DocumentTypes)
.HasForeignKey(x => x.DocumentTypeId);
modelBuilder.Entity<DocumentTypeRetractRelation>()
.HasOne(x => x.RetractDocumentType)
.WithMany(x => x.RetractDocumentTypes)
.HasForeignKey(x => x.RetractDocumentTypeId);
Update writer:
public async Task<DocumentType> UpdateAsync(DocumentTypeUpdateDto documentTypeUpdateDto)
{
using (IUnitOfWork uow = UowProvider.CreateUnitOfWork<EntityContext>())
{
var documentTypeRepo = uow.GetCustomRepository<IDocumentTypeRepository>();
var existingDocument = await documentTypeRepo.GetAsync(documentTypeUpdateDto.Id);
if (existingDocument == null)
throw new EntityNotFoundException("DocumentType", existingDocument.Id);
foreach (var retractDocumentTypeId in documentTypeUpdateDto.RetractDocumentTypeIds)
{
existingDocument.RetractDocumentTypes.Add(new DocumentTypeRetractRelation()
{
DocumentTypeId = existingDocument.Id,
RetractDocumentTypeId = retractDocumentTypeId
});
}
documentTypeRepo.Update(existingDocument);
await uow.SaveChangesAsync();
return existingDocument;
}
}
When trying to update the existingDocument I get the following error:
The instance of entity type 'DocumentTypeRetractRelation' cannot be
tracked because another instance of this type with the same key is
already being tracked. When adding new entities, for most key types a
unique temporary key value will be created if no key is set (i.e. if
the key property is assigned the default value for its type). If you
are explicitly setting key values for new entities, ensure they do not
collide with existing entities or temporary values generated for other
new entities. When attaching existing entities, ensure that only one
entity instance with a given key value is attached to the context.
The problem is not the self referencing, but applying the many-to-many collection modifications which generate different DocumentTypeRetractRelation objects with the same PK as stated in the exception message.
The correct way currently in EF Core is to make sure RetractDocumentTypes of the existingDocument are loaded (contains the original values), then merge the changes by either using an existing or create new DocumentTypeRetractRelation objects.
Replace the following code
foreach (var retractDocumentTypeId in documentTypeUpdateDto.RetractDocumentTypeIds)
{
existingDocument.RetractDocumentTypes.Add(new DocumentTypeRetractRelation()
{
DocumentTypeId = existingDocument.Id,
RetractDocumentTypeId = retractDocumentTypeId
});
}
with
// existingDocument.RetractDocumentTypes should be loaded (either eager or explicit)
existingDocument.RetractDocumentTypes = (
from retractDocumentTypeId in documentTypeUpdateDto.RetractDocumentTypeIds
join existingRelation in existingDocument.RetractDocumentTypes
on retractDocumentTypeId equals existingRelation.RetractDocumentTypeId
into existingRelations
select existingRelations.FirstOrDefault() ?? new DocumentTypeRetractRelation()
{
DocumentTypeId = existingDocument.Id,
RetractDocumentTypeId = retractDocumentTypeId
}).ToList();
This would handle both added, removed and unchanged relations. You can do similar to DocumentTypes.
Actually looking at your model, the above code should be for DocumentTypes collection (since you a receiving the RetractDocumentTypeIds, which in combination with the document Id form the DocumentTypes collection content). So simply replace the RetractDocumentTypes with DocumentTypes.
Related
I am trying to update a database row by reading the entity (using Entity Framework 6), mapping new properties onto that entity (using AutoMapper) and then calling context.SaveChanges().
The problem I am having surrounds the navigational properties within my entity. It appears that during the mapping, a new object is being created and assigned to the navigational property, rather than the existing one's properties simply being updated.
Here's are my domain objects:
public class ParagraphComponent : IReportComponent
{
public Guid ComponentId { get; set; }
public float LineHeight { get; set; }
public ReportTextList TextItems { get; set; } = new ReportTextList();
}
public class ReportTextList : IList<ReportText>
{
private readonly IList<ReportText> _list = new List<ReportText>();
public int Count => _list.Count;
public bool IsReadOnly => _list.IsReadOnly;
// Overrides for IList...
public void Add(ReportText item)
{
_list.Add(item);
}
// ...Remove(), Insert() etc.
}
public class ReportText
{
public Guid Id { get; set; }
public string Content { get; set; } = "";
}
Entity Framework entities:
public partial class ParagraphComponentEntity
{
public System.Guid ComponentId { get; set; } // ComponentId (Primary key)
public double LineHeight { get; set; } // LineHeight
public virtual System.Collections.Generic.ICollection<ReportTextEntity> ReportTexts { get; set; }
}
public partial class ReportTextEntity
{
public System.Guid Id { get; set; } // Id (Primary key)
public string Content { get; set; } // Content
}
What I am doing: I am taking data for a ParagraphComponent from an API endpoint to perform an update. I load the existing component based on ParagraphComponent.Id and then I map the new properties onto the existing entity.
This works fine:
public async Task<bool> EditComponent(IReportComponent editedComponent)
{
var currentParagraphComponentEntity = await Context
.ParagraphComponents
.FirstOrDefaultAsync(x => x.ComponentId == editedComponent.ComponentId)
.ConfigureAwait(false);
Mapper.Map(editedComponent as ParagraphComponent, currentParagraphComponentEntity);
Context.SaveChanges();
}
I can see in debug that the properties are mapped correctly, but when SaveChanges() is called I get the following error:
Violation of PRIMARY KEY constraint 'PK_ReportText'. Cannot insert duplicate key in object 'dbo.ReportText'
It appears that the mapping process is assigning a new object to the ParagraphComponentEntity.ReportTexts property, thus Entity Framework sees it as an "Add" rather than an "Update", so it tries adding a new row to that table which errors because of the primary key enforcing the Id to be unique.
My AutoMapper configuration:
CreateMap<ParagraphComponent, ParagraphComponentEntity>()
.ForMember(dest => dest.LineHeight, src => src.MapFrom(s => s.LineHeight))
.ForMember(dest => dest.ReportTexts, src => src.MapFrom(s => s.TextItems))
.ForMember(dest => dest.ComponentId, src => src.MapFrom(s => s.ComponentId))
.ForAllOtherMembers(src => src.Ignore());
If AutoMapper is creating a new instance for the ReportTexts navigational property is the issue, how do I get around it?
It seems that your code which is fetching data from database is only fetching the main resource ParagraphComponent. Collection of ReportTexts by default will not be fetched into EF context because as far as I know EF is lazy loading - you need to eagerly load referenced entities with .Include(..) for instance.
If I am right - then before mapping your data into entity ReportTexts collection is empty and your Mapping code is really creating new items in that collection. These entities are comming from outside of EF context (read about EF ChangeTracking) so 'it' thinks that these are new entities which needs to be inserted into database. These objects obviously already contains Id set with existing keys - so that's where you get conflicts.
I think if you will eager load your entity, then EF should perform Update instead
I have a Net Core 3.1 MVC project where a Playlist can be created and added to an existing Team. I run in to problems when I try to save the Playlist entity with _context.Add(playlist) and then _context.SaveChangesAsync() with the following error:
Cannot insert duplicate key row in object 'dbo.Teams' with unique index 'IX_Teams_TeamName'. The duplicate key value is (Team Name Four).
The statement has been terminated.
My code:
PlaylistDTO dto = new PlaylistDTO();
dto.Name = "new playlist with related team";
dto.Team = _context.Team.FirstOrDefault(t => t.Id == id) // id comes from viewmodel
Playlist storeToDb = _mapper.Map<Playlist>(dto)
_context.Playlists.Add(storeToDb);
await _context.SaveChangesAsync(); // throws error cannot insert duplicates.
My entities:
public class Playlist: AuditEntity
{
// id comes from AuditEntity
public string PlayListName { get; set; }
public string Description { get; set; }
public Team Team { get; set; }
}
public class Team: AuditEntity
{
// id comes from AuditEntity
public string TeamName {get; set; }
// other properties, including other related entities
public ICollection<Playlist> Playlists {get; set;}
}
My DbConfig file
public class TeamDbConfig : IEntityTypeConfiguration<Team>
{
public void Configure(EntityTypeBuilder<Team> builder)
{
builder.ToTable("Teams");
builder.HasIndex(t => t.TeamName)
.IsUnique();
// one2many rel for playlist. One team can have multiple playlists
builder.HasMany(t => t.Playlists)
.WithOne(pl => pl.Team)
.IsRequired(false)
.OnDelete(DeleteBehavior.Restrict);
}
}
In a similar post it was explained that with .Add() EF will treat any objects as new (and so it cannot add an entity with restricted columns twice). However, I don't know how to get this to work. Loading it untracked vs tracked, setting the Entry().EntityState to Modified or Unchanged doesn't seem to do anything.
This seems to be a pretty standard thing to do, yet I cannot get it done. So, I have a few questions:
Given what I want (a user can add an existing team to a new playlist), do I have the correct relationships defined between Team and Playlist?
What do I need to use as a statement instead (or in addition to) the Add() statement that I now have?
Since you are using net 3.1 you have to add TeamId to
public class Playlist: AuditEntity
{
// id comes from AuditEntity
public string PlayListName { get; set; }
public string Description { get; set; }
public int? TeamId { get; set; }
[ForeignKey(nameof(TeamId))]
[InverseProperty("Playlists")]
public virtual Team Team { get; set; }
}
public class Team: AuditEntity
{
// id comes from AuditEntity
public string TeamName {get; set; }
// other properties, including other related entities
[InverseProperty(nameof(Playlist.Team))]
public ICollection<Playlist> Playlists {get; set;}
}
[InverseProperty(nameof(FirstClass.SecondClass))]
modelBuilder.Entity<PlayList>(entity =>
{
entity.HasOne(d => d.Team)
.WithMany(p => p.Playlists)
.HasForeignKey(d => d.TeamId);
});
and use it to assign to playlist
PlaylistDTO dto = new PlaylistDTO();
dto.Name = "new playlist with related team";
dto.TeamId=id; // id comes from viewmodel
Playlist storeToDb = _mapper.Map<Playlist>(dto)
_context.Playlists.Add(storeToDb);
await _context.SaveChangesAsync();
I'm using EF Core 2.1, database first approach. I'm trying to Include() a foreign key entity when fetching my target entity collection, but something strange is happening.
The entity structure is Job -> JobStatus. I'm fetching some Job entities, and want to include the Job's JobStatus foreign key property. The issue is that the JobStatus entity has a ICollection[Job] property that is populating every single Job from the database. This is causing the payload to be gigabytes in size.
When I include the JobStatus on the Job, I'd like to satisfy one of the following solutions. I'm also open to other solutions or workarounds I haven't thought of.
*how can I prevent the JobStatus' ICollection property from populating?
*Or can I prevent Entity Framework from generating that property in the first place?
I've already explored Ignoring the ReferenceLoopHandling
services.AddMvc().AddJsonOptions(options => {
options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore;
});
Here are the entities, automatically generated by Entity Framework.
public partial class Job
{
public long Id { get; set; }
public long StatusId { get; set; }
public JobStatus Status { get; set; }
}
public partial class JobStatus
{
public JobStatus()
{
Job = new HashSet<Job>();
}
public long Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public ICollection<Job> Job { get; set; }
}
Example code that is causing the problem
var jobs = _context.Set<Job>()
.Where(job => job.Id == 1)
.Include(job => job.Status);
One way to avoid the Job collection from being populated is to explicitly select the columns that you want returned, either through a defined or anonymous type:
var jobs = _context.Set<Job>()
.Where(job => job.Id == 1)
.Include(job => job.Status)
.Select(job => new
{
Id = job.Id,
StatusName = job.Status.Name
});
Add a "virtual" keyword. Any virtual ICollections will be lazy-loaded unless you specifically mark them otherwise.
public virtual ICollection<Job> Job { get; set; }
I'm using EFCore 1.1.0 in a netstandard1.6 application.
I have configured my model with a composite key
public class ClassExtension
{
public int Id { get; set; }
public string Name { get; set; }
public int ClassId { get; set; }
public MyClass Class { get; set; }
}
public class MyClass
{
public int Id { get; set; }
public string Name { get; set; }
public List<ClassExtension> Extensions { get; set; }
public Class()
{
Extensions = new List<ClassExtension>();
}
}
Then in my dbcontext OnModelCreating()
modelBuilder.Entity<ClassExtension>()
.HasKey(p => new { p.Id, p.ClassId });
I then get from the database the existing instance of a particular composite key, then create a new instance of ClassExtension in code based on user input and finally assign those values to the entity then save it.
var existingSpec = await db.ClassSpecs.Where(
c => c.ClassId == existingClass.Id && c.Id == input.Id)
.FirstOrDefaultAsync();
var newExtension = new ClassExtension()
{
Id = input.Id,
Name = input.Name
};
db.Entry(existingSpec).CurrentValues.SetValues(newSpec); // Errors on this line
db.Entry(existingSpec).Property(p => p.ClassId).IsModified = false;
await db.SaveChangesAsync()
The application generates the following error when trying to assign the values using SetValues()
{System.InvalidOperationException: The property 'ClassId' on entity type 'ClassExtension'
is part of a key and so cannot be modified or marked as modified.
... removed ...
at MyApplication...
When I do this on a table without a composite key it works just fine, not overwriting the existing foreign key Id with null, just using its existing value.
Is there something I'm missing, or is this just a quirk with using composite keys?
I have two tables in my database. One is called Users, and the other is called Widgets. The Widgets table represents 3 entities in my code model. One of the entities, Widget, is a parent class for the other two entities, WidgetTypeA and WidgetTypeB. Both WidgetTypeA and WidgetTypeB have navigation properties to the User entity, which is persisted to the Users table in the database. I'm having trouble getting Code First to use the same foreign key for both the WidgetTypeA and WidgetTypeB entities (UserId). Does anyone know how to do this? It seems like it should be a common problem with Table Per Hierarchy mapping.
My entity classes are as follows:
public class Widget
{
public int Id { get; set; }
public string Name { get; set; }
}
class WidgetMap : EntityTypeConfiguration<Widget>
{
public WidgetMap()
{
ToTable("Widgets");
HasKey(w => w.Id);
Property(w => w.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
Property(w => w.Name)
.IsRequired()
.HasMaxLength(75)
.IsUnicode(true);
}
}
public class WidgetTypeA : Widget
{
public int UserId { get; set; }
public virtual User User { get; set; }
public string Color { get; set; }
public int DepthLevel { get; set; }
}
class WidgetTypeAMap : EntityTypeConfiguration<WidgetTypeA>
{
public WidgetTypeAMap()
{
Map(w => w.Requires("WidgetTypeId").HasValue(1));
HasRequired(w => w.User)
.WithMany(u => u.WidgetTypeAs)
.HasForeignKey(w => w.UserId)
.WillCascadeOnDelete(false);
Property(w => w.Color)
.IsOptional()
.IsUnicode(true)
.HasMaxLength(75);
Property(w => w.DepthLevel)
.IsOptional();
}
}
public class WidgetTypeB : Widget
{
public int UserId { get; set; }
public virtual User User { get; set; }
}
class WidgetTypeBMap : EntityTypeConfiguration<WidgetTypeB>
{
public WidgetTypeBMap()
{
Map(w => w.Requires("WidgetTypeId").HasValue(2));
HasRequired(w => w.User)
.WithMany(u => u.WidgetTypeBs)
.HasForeignKey(w => w.UserId)
.WillCascadeOnDelete(false);
}
}
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public int Age { get; set; }
public virtual ICollection<WidgetTypeA> WidgetTypeAs { get; set; }
public virtual ICollection<WidgetTypeB> WidgetTypeBs { get; set; }
}
class UserMap : EntityTypeConfiguration<User>
{
public UserMap()
{
ToTable("Users");
HasKey(u => u.Id);
Property(u => u.Username)
.IsRequired()
.HasMaxLength(75)
.IsUnicode(true);
Property(u => u.Age)
.IsRequired();
}
}
At any rate, I keep getting the error
Invalid column name 'UserId1'
when I try to perform the following operations:
using (var entities = new MyEntities())
{
User u = new User
{
Username = "Frank",
Age = 14
};
entities.Users.Add(u);
entities.SaveChanges();
WidgetTypeA wa1 = new WidgetTypeA
{
Name = "0SDF81",
UserId = u.Id,
DepthLevel = 6
};
entities.WidgetTypeAs.Add(wa1);
entities.SaveChanges();
}
Not sure if this can be fixed or not. I can always specify a second UserId foreign key for the Widgets table, but that seems pointless. Perhaps there's a way to do this using Fluent API?
You cannot map properties defined in different derived entities to the same column. That is limitation in EF. If your WidgetTypeA has UserId property and your WidgetTypeB has UserId property they must be different columns in the database. It should work if you move both UserId and User properties from derived types to the parent Widget type.
I know its a long way late, but hopefully may help other readers.
Although Ladislav was correct that using a mapped Foreign Key is not supported in EF6, I did find a useful workaround.
It is possible to define a computed column specification whose expression simply refers to the original column. Userid in the description above. This can be used as the discriminator for the TPH mapping. With this approach, the column need not be persisted, but can be used for TPH, with the original column being available for use as a foreign key.