I am implementing an api using .net 5.
I have a student class which have a property with address type(value object according to ddd).
public class Student
{
public long Id{ get; private set; }
public string FirstName { get; private set; }
public string LastName { get; private set; }
public Address Address { get; private set; }
}
public class Address
{
public string City { get; private set; }
public string Road { get; private set; }
}
I am using fluent api to configure the database using ef core 5.
class StudentConfiguration : IEntityTypeConfiguration<Student>
{
public void Configure(EntityTypeBuilder<Student> builder)
{
builder.ToTable("Students");
builder.HasKey(x => x.Id);
builder.Property(x => x.Id).ValueGeneratedNever().IsRequired();
builder.Property(x => x.FirstName).HasMaxLength(25).IsRequired();
builder.Property(x => x.LastName).HasMaxLength(50).IsRequired();
builder.OwnsOne(x => x.Address, x =>
{
x.Property(pp => pp.City)
.IsRequired()
.HasColumnName("City")
.HasMaxLength(20);
x.Property(pp => pp.Road)
.IsRequired()
.HasColumnName("Road")
.HasMaxLength(40);
});
}
}
As a result I have one table with columns Id,Fistname,lastname,city,road.
Now I am trying to update only the city and the road(for example a student change house)
but I have different exceptions and I don't know how to update only these 2 columns
public async Task UpdateAddress(Student student)
{
//Firts try
//var studentEntry = context.Entry(student);
//studentEntry.Property(x => x.Address.City).IsModified = true;
//studentEntry.Property(x => x.Address.Road).IsModified = true;
//**Exception** 'The expression 'x => x.Address.City' is not a valid member access expression. The expression should represent a simple property or field access: 't => t.MyProperty'. (Parameter 'memberAccessExpression')'
//Second try
//var studentEntry = context.Entry(student.Address);
//studentEntry.Property(x => x.City).IsModified = true;
//studentEntry.Property(x => x.Road).IsModified = true;
//**Exception** 'Database operation expected to affect 1 row(s) but actually affected 0 row(s). Data may have been modified or deleted since entities were loaded.
//the only method that works is Update but this update the whole object
context.Update(student);
await context.SaveChangesAsync();
}
EDIT
public class StudentDbContext : DbContext
{
public DbSet<Student> Students { get; set; }
public StudentDbContext(DbContextOptions<StudentDbContext> options) : base(options)
{
ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
base.OnModelCreating(modelBuilder);
}
}
How can update only these two properties of an owned entity?
Address is owned entity type, hence Student.Address property by EF Core terminology is not a property, but reference navigation property, thus should be accessed via Reference method rather than Property method (none of them supports property path). Then you can use the returned tracking entry to access its members.
To force updating just some properties of the Student.Address, first attach the Student entity instance (to tell EF that it is existing)
var studentEntry = context.Attach(student);
and then use something like this
var addressEntry = studentEntry.Reference(e => e.Address);
addressEntry.TargetEntry.Property(e => e.City).IsModified = true;
addressEntry.TargetEntry.Property(e => e.Road).IsModified = true;
Since your queries are not tracked by EntityFramework (because you set that in your DBContext configuration) you can try the following flow:
Firstly get student from DB by its ID and then change only those two properties you want to change on that entity directly.
public async Task UpdateAddress(Student student)
{
// Get existing student by its ID from the database
var existingStudent = await context.Students
.Include(x => x.Address)
.SingleOrDefaultAsync(x => x.Id == student.Id);
// To prevent null reference exception
if (existingStudent is null)
return; // Exception maybe? Depends on your app flow
// Edit address value object with the method available in your entity, since you're using DDD approach
existingStudent.ChangeAddress(student.Address);
// Since your query are not tracked you need to explicity tell EF that this entry is being modified
context.Entry(existingStudent).State = EntityState.Modified;
// EF will save only two properties in that case
await context.SaveChangesAsync();
}
Than in your Student entity class add the following method to provide ability to change its address:
public void ChangeAddress(Address newAddress)
{
// Some additional conditions to check the newAddress for nulls, valid values, etc.
Address = newAddress;
}
You can treat your Address as a ValueObject and replace it with the new Address VO.
Related
This is a tale of optional owned entities and foreign keys.
I'm working with EF 5 (code first) and I do this :
public class Parent {
public Guid Id { get; private set; }
public OwnedType1? Owned1 { get; private set; }
public OwnedType2? Owned2 { get; private set; }
public Parent(Guid id, OwnedType1? owned1, OwnedType2? owned2) {
Id = id; Owned1 = owned1; Owned2 = owned2;
}
}
public class OwnedType1 {
public Guid? OptionalExternalId { get; private set; }
public OwnedType1 (Guid? optionalExternalId) {
OptionalExternalId = optionalExternalId;
}
}
public class OwnedType2 {
public Guid? OptionalExternalId { get; private set; }
public OwnedType2 (Guid? optionalExternalId) {
OptionalExternalId = optionalExternalId;
}
}
public class Shared {
public Guid Id { get; private set; }
public Shared (Guid id) {
Id = id;
}
}
Now, the configuration :
//-------- for Parent ------------
public void Configure(EntityTypeBuilder<Parent> builder) {
builder
.ToTable("Parents")
.HasKey(p => p.Id);
builder
.OwnsOne(p => p.Owned1)
.HasOne<Shared>()
.WithMany()
.HasForeignKey(x => x.OptionalExternalId);
builder
.OwnsOne(p => p.Owned2)
.HasOne<Shared>()
.WithMany()
.HasForeignKey(x => x.OptionalExternalId);
}
//-------- for OwnedType1 ------------
// (there's no builder as they're owned and EntityTypeBuilder<Parent> is enough)
//-------- for OwnedType2 ------------
// (there's no builder as they're owned and EntityTypeBuilder<Parent> is enough)
//-------- for Shared ---------------
public void Configure(EntityTypeBuilder<Shared> builder) {
builder
.ToTable("Shareds")
.HasKey(p => p.Id);
}
Side note : If you're wondering why OwnedType1 and OwnedType2 don't each have a property called 'ParentId', it's because it's created implicitly by the "OwnsOne".
My problem is this :
When I create a new Migration, then OwnedType1 works like a charm, but for OwnedType2 (which is quasi-identical), I get his error :
The property 'OptionalExternalId' cannot be added to the type
'MyNameSpace.OwnedType2' because no property type was specified and
there is no corresponding CLR property or field. To add a shadow state
property, the property type must be specified.
I don't understand what it's complaining about. And why it's complaining only for one of them.
I know that you probably can't work it out with this simplified version of my schema, but what I'm asking is what you think it might be (follow your guts of EF guru) :
Some missing constructor?
Incorrect visibility on one of the fields?
Bad navigation definition?
A typo?
Something tricky (like : If you're going to have TWO different entity classes having a one-to-many relation with Shared, then they can't use the same name for external key. Or I need to use a composite key. Or whatnot).
It was a configuration issue that had nothing to do with Owned entities. Another case of "EF error message is obscure but issue is somewhere there in plain sight".
Unfortunately I don't remember how I fixed it. But it was along the lines of "Need an extra constructor with all the paramaters" or "one of the fields had a different name in the constructor parameters" or one of those classic EF mishaps.
Using EF Core, I have a Zone that can have multiple Sol (soils), same Sol can be attached to multiple Zone:
public class Sol
{
// ...
public ICollection<Zone> Zones { get; set; } = new List<Zone>();
}
public class Zone
{
// ...
public ICollection<Sol> Sols { get; set; } = new List<Sol>();
}
public override void Configure(EntityTypeBuilder<Zone> builder)
{
// ...
builder
.HasMany(p => p.Sols)
.WithMany(p => p.Zones);
}
When adding my Sols to a Zone however I get the following exception:
Microsoft.EntityFrameworkCore.DbUpdateException: An error occurred while updating the entries. See the inner exception for details.
Microsoft.Data.SqlClient.SqlException (0x80131904): Violation of PRIMARY KEY constraint 'PK_SolZone'. Cannot insert duplicate key in object 'dbo.SolZone'. The duplicate key value is (1, 2).
Some details of implementation:
In my controller, when retrieving the object I do
public override async Task<IActionResult> Edit(int id, [FromQuery] bool fullView)
{
var currentZone = _repository.SingleOrDefault(new GetZoneWithPaysAndSolsSpecification(id));
where
public class GetZoneWithPaysAndSolsSpecification : Specification<Zone>
{
public GetZoneWithPaysAndSolsSpecification(int zoneId)
{
Query.Where(p => p.Id == zoneId);
Query.Include(p => p.Pays);
Query.Include(p => p.Sols);
}
}
before updating the object, I convert my ZoneDTO to Zone and then add it to the database:
protected override void BeforeUpdate(Zone zone, ZoneDTO sourceDto)
{
zone.Sols.Clear();
foreach (var solId in sourceDto.SolIds)
{
var sol = _repository.GetById<Sol>(solId);
zone.Sols.Add(sol);
}
base.BeforeUpdate(zone, sourceDto);
}
I use the base controller, that uses the BeforeUpdate, like this
[HttpPost]
[ValidateAntiForgeryToken]
public virtual async Task<IActionResult> Edit(TDto dto)
{
try
{
var entity = FromDto(dto);
BeforeUpdate(entity, dto);
await _repository.UpdateAsync(entity);
return RedirectToAction(nameof(Index));
}
catch (Exception ex)
{
_logger.LogError(ex, "when editing an object after submit");
return PartialView();
}
}
The repository code
public Task UpdateAsync<T>(T entity) where T : BaseEntity
{
_dbContext.Entry(entity).State = EntityState.Modified;
return _dbContext.SaveChangesAsync();
}
I use AutoMapper
protected TBussinesModel FromDto(TDto dto)
{
return _mapper.Map<TBussinesModel>(dto);
}
And the mapping is like this
CreateMap<Zone, ZoneDTO>()
.ForMember(p => p.SolIds, o => o.MapFrom(p => p.Sols.Select(s => s.Id).ToArray()))
.ForMember(p => p.SolNoms, o => o.MapFrom(p => p.Sols.Select(s => s.Nom).ToArray()))
.ReverseMap();
When you are mapping from dto to entity, your FromDto method is giving you a Zone entity whose Sols list is not populated with the zone's existing sols. Its an empty list. So, when you are calling -
zone.Sols.Clear();
its doing nothing, and at database level, the zone still holds its sols. Then when you are re-populating the Sols list, you are trying to insert some previously existing Sol to the list.
You have to fetch the existing Zone along with its Sols list from the database, before clearing and repopulating it. How you can do that depends on how your repository is implemented.
On how to update many-to-many entity in EF 5.0, you can check this answer
EDIT :
Try modifying your base controller's Edit method as -
[HttpPost]
[ValidateAntiForgeryToken]
public virtual async Task<IActionResult> Edit(TDto dto)
{
try
{
var zone = _repository.SingleOrDefault(new GetZoneWithPaysAndSolsSpecification(dto.Id));
zone.Sols.Clear();
foreach (var id in dto.SolIds)
{
var sol = _repository.GetById<Sol>(solId);
zone.Sols.Add(sol);
}
await _repository.UpdateAsync(zone);
return RedirectToAction(nameof(Index));
}
catch (Exception ex)
{
_logger.LogError(ex, "when editing an object after submit");
return PartialView();
}
}
Not related to your issue :
You are fetching one Sol a time inside a loop -
foreach (var id in dto.SolIds)
{
var sol = _repository.GetById<Sol>(solId);
zone.Sols.Add(sol);
}
which is not efficient at all. Try something like -
var sols = // fetch all the sols from the database
foreach (var id in dto.SolIds)
{
zone.Sols.Add(sols.FirstOrDefault(p => p.Id == id));
}
Since the relationship between SOL and Zone is a many to many relationship a Separate table containing the primary keys of both the tables will be created.
A table definition is needed for this many to many relationship. Also Try including the UsingEnity to specify the entity used for defining the relationship
modelBuilder
.Entity<Post>()
.HasMany(p => p.SOL)
.WithMany(p => p.ZONE)
.UsingEntity(j => j.ToTable("SOL_ZONE"));
It seems to me that you have many to many relationship. So you need to set up a new entity with primary key to be composite for mapping and to configure the entities the right way:
(In EF Core up to and including 3.x, it is necessary to include an entity in the model to represent the join table, and then add navigation properties to either side of the many-to-many relations that point to the join entity instead:)
public class Sol
{
// ...
public ICollection<SolZone> SolZones { get; set; } = new List<Zone>();
}
public class Zone
{
// ...
public ICollection<SolZone> SolZones { get; set; } = new List<Sol>();
}
public class SolZone
{
public int SolId { get; set; }
public Sol Sol { get; set; }
public int ZoneId { get; set; }
public Zone Zone { get; set; }
}
// And in the OnModelCreating
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<SolZone>().HasKey(sc => new {sc.SolId , sc.ZoneId});
modelBuilder.Entity<SolZone>()
.HasOne<Sol>(s => s.Sol)
.WithMany(sz => sz.SolZones)
.HasForeignKey(s => s.SolId)`
modelBuilder.Entity<SolZone>()
.HasOne<Zone>(z => z.Zone)
.WithMany(sz => sz.SolZones)
.HasForeignKey(z => z.ZoneId);
}
The primary key for the join table is a composite key comprising both of the foreign key values. In addition, both sides of the many-to-many relationship are configured using the HasOne, WithMany and HasForeignKey Fluent API methods.
This is sufficient if you want to access Sol Zone data via the Sol or Zone entities. If you want to query SolZone data directly, you should also add a DbSet for it:
public DbSet<SolZone> SolZones { get; set; }
You can look up different relationships in EF Core here: https://learn.microsoft.com/en-us/ef/core/modeling/relationships?tabs=fluent-api%2Cfluent-api-simple-key%2Csimple-key
I have a simple application, that registers a collection of money from batches of sold tickets from
drivers (which domain expert calls settlements). I try to use DDD approach and EF Core to handle this small app (call it a playground for using EF Core in DDD). I have basically 3 tables in SQL Server (simplified to the absolute minimum):
Table: Collection
-------------+---------
Column | Type
-------------+---------
CollectionId | int (PK)
IsCancelled | bit
Table: Settlement
-------------+---------
Column | Type
-------------+---------
SettlementId | int (PK)
CollectionId | int (FK)
Number | int
Table: CollectionSettlementJoin
-------------+---------
Column | Type
-------------+---------
SettlementId | int (PK)(FK)
CollectionId | int (PK)(FK)
I know there seems to be a redundant join table (since I have the CollectionId on the Settlement table), but it seems to be a design requirement, that I will explain in a moment. So each collection has at least 1 or more settlements. I have 2 domain entities which actually correspond with the tables - my aggregate root Collection and attached to it Settlements property that contains list of Settlement entities.
The extra table is used for auditing purposes as actually does not take real part in the domain. It is populated by a trigger on Settlement.CollectionId update (for non-nulls). Each collection can be cancelled within 5 minutes of its creation by the creator or anytime by a superuser. When a collection is cancelled, I want to reset Settlement.CollectionId to null (when that happens data in CollectionSettlementJoin stays and I can always get back what settlements were cancelled).
My current setup is working fine when comes to creating a collection. The selected settlements are added, saved and successfully persisted in my database. The problem starts when I want to cancel a collection. I get from the database the collection with attached settlements . But when I remove the settlements from my aggregate root, dbContext.SaveChanges() does not persist the changes (does not set Settlement.CollectionId to null).
Here is my setup:
public class Collection : EntityBase, IAggregateRoot
{
private Collection() { }
public Collection(List<Settlement> settlements)
{
_settlements = settlements;
}
private bool _isCancelled;
public bool IsCancelled => _isCancelled;
private List<Settlement> _settlements;
public IReadOnlyCollection<Settlement> Settlements => _settlements.AsReadOnly();
public void CancelCollection()
{
if (_isCancelled != true)
{
_isCancelled = true;
_settlements.Clear();
}
}
}
public class Settlement : EntityBase
{
private Settlement() { }
public Collection? Collection { get;set; }
public int? CollectionId { get; internal set; }
}
public class CollectionEntityTypeConfiguration : IEntityTypeConfiguration<Collection>
{
public void Configure(EntityTypeBuilder<Collection> builder)
{
builder.ToTable("Collection");
builder.HasKey(s => s.Id);
builder.Property(s => s.Id).HasColumnName("CollectionId").UseIdentityColumn();
builder.Property(s => s.IsCancelled).HasDefaultValueSql("(0)");
builder.HasMany(s => s.Settlements)
.WithOne(c => c.Collection)
.HasForeignKey(s => s.CollectionId);
}
}
public class SettlementEntityTypeConfiguration : IEntityTypeConfiguration<Settlement>
{
public void Configure(EntityTypeBuilder<Settlement> builder)
{
builder.ToTable("Settlement");
builder.HasKey(s => s.Id);
builder.Property(s => s.Id).HasColumnName("SettlementId").ValueGeneratedNever();
builder.Property(s => s.CollectionId);
builder.HasOne(s => s.Collection)
.WithMany(c => c.Settlements)
.HasForeignKey(s => s.CollectionId);
}
}
Context (not much here)
public class SettlementCollectionContext: DbContext
{
public SettlementCollectionContext(
DbContextOptions<SettlementCollectionContext> options) : base(options)
{
ChangeTracker.StateChanged += ChangeTracker_StateChanged;
}
private void ChangeTracker_StateChanged(object sender, EntityStateChangedEventArgs e)
{
System.Diagnostics.Debug.WriteLine($"Entity {e.Entry.Entity.GetType().Name} has changed.");
}
public DbSet<Collection> Collections { get; set; }
public DbSet<Settlement> Settlements { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new SettlementEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new CollectionEntityTypeConfiguration());
}
}
And finally my command:
var collection = await _dbContext.Collections
.Include(c => c.Settlements)
.Where(c => c.Id == collectionId)
.SingleOrDefaultAsync();
if (!collection!.IsCancelled)
{
collection.CancelCollection();
}
_dbContext.Update(collection); //without this the change tracker does not register the change
_dbContext.SaveChanges();
I know that the ChangeTracker registeres this change, becuase I added in my context an even handler to ChangeTracker.StateChanged and during debugging I noticed it register that collection has changed (although not the settlement). I also tried to reset Settlement.CollectionId property to null in Collection.CancelCollection() method, but this did not help either. I must be missing something.
So in the end nothing is wrong with the setup. Actually my unit of work was a problem, because it was saving incorrect context, so obviously the changes were never persisted.
I am trying to restrict a couple of generic methods to only be allowed Entities that inherit from the IParentOf<TChildEntity> interface, as well as accessing an Entity's Foreign Key (ParentId) Generically.
To demonstrate;
public void AdoptAll<TParentEntity, TChildEntity>(TParentEntity parent,
TParentEntity adoptee)
where TParentEntity : DataEntity, IParentOf<TChildEntity>
where TChildEntity : DataEntity, IChildOf<TParentEntity>
{
foreach (TChildEntity child in (IParentOf<TChildEntity>)parent.Children)
{
(IChildOf<TParentEntity)child.ParentId = adoptee.Id;
}
}
A child entity class model would look like this,
public class Account : DataEntity, IChildOf<AccountType>, IChildOf<AccountData>
{
public string Name { get; set; }
public string Balance { get; set; }
// Foreign Key and Navigation Property for AccountType
int IChildOf<AccountType>.ParentId{ get; set; }
public virtual AccountType AccountType { get; set; }
// Foreign Key and Navigation Property for AccountData
int IChildOf<AccountData>.ParentId{ get; set; }
public virtual AccountData AccountData { get; set; }
}
First of all, is this possible to do? Or will it breakdown in EF?
Secondly, since the Foreign Keys do not follow convention (and there are multiple) how do I set them via Fluent Api? I can see how to do this in Data Annotations.
I hope this is clear, I have been considering it for a while and trying to work round it, so I can follow my argument, but it may not be clearly conveyed, so please ask for clarification if needed. My reason for wanting to do this is to make the code safe as well as automating a lot of the manual changing of classes necessary to add new associations and entities.
Thanks.
Edit
I decided to create some basic classes to implement this idea and test it, my code is as follows.
public abstract class ChildEntity : DataEntity
{
public T GetParent<T>() where T : ParentEntity
{
foreach (var item in GetType().GetProperties())
{
if (item.GetValue(this) is T entity)
return entity;
}
return null;
}
}
public abstract class ParentEntity : DataEntity
{
public ICollection<T> GetChildren<T>() where T : ChildEntity
{
foreach (var item in GetType().GetProperties())
{
if (item.GetValue(this) is ICollection<T> collection)
return collection;
}
return null;
}
}
public interface IParent<TEntity> where TEntity : ChildEntity
{
ICollection<T> GetChildren<T>() where T : ChildEntity;
}
public interface IChild<TEntity> where TEntity : ParentEntity
{
int ForeignKey { get; set; }
T GetParent<T>() where T : ParentEntity;
}
public class ParentOne : ParentEntity, IParent<ChildOne>
{
public string Name { get; set; }
public decimal Amount { get; set; }
public virtual ICollection<ChildOne> ChildOnes { get; set; }
}
public class ParentTwo : ParentEntity, IParent<ChildOne>
{
public string Name { get; set; }
public decimal Value { get; set; }
public virtual ICollection<ChildOne> ChildOnes { get; set; }
}
public class ChildOne : ChildEntity, IChild<ParentOne>, IChild<ParentTwo>
{
public string Name { get; set; }
public decimal Balance { get; set; }
int IChild<ParentOne>.ForeignKey { get; set; }
public virtual ParentOne ParentOne { get; set; }
int IChild<ParentTwo>.ForeignKey { get; set; }
public virtual ParentTwo ParentTwo { get; set; }
}
Data Entity simply gives each entity an Id property.
I have standard Generic Repositories set up with a Unit of Work class for mediating. The AdoptAll method looks like this in my program.
public void AdoptAll<TParentEntity, TChildEntity>(TParentEntity parent,
TParentEntity adoptee, UoW uoW)
where TParentEntity : DataEntity, IParent<TChildEntity>
where TChildEntity : DataEntity, IChild<TParentEntity>
{
var currentParent = uoW.GetRepository<TParentEntity>().Get(parent.Id);
foreach (TChildEntity child in currentParent.GetChildren<TChildEntity>())
{
child.ForeignKey = adoptee.Id;
}
}
This seems to work correctly and without faults (minimal testing) are there any major flaws in doing this?
Thanks.
Edit Two
This is the OnModelCreating Method in the DbContext, which sets up the foreign key for each entity. Is this problematic?
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<ChildOne>()
.HasOne(p => p.ParentOne)
.WithMany(c => c.ChildOnes)
.HasForeignKey(fk => ((IChild<ParentOne>)fk).ForeignKey);
modelBuilder.Entity<ChildOne>()
.HasOne(p => p.ParentTwo)
.WithMany(c => c.ChildOnes)
.HasForeignKey(fk => ((IChild<ParentTwo>)fk).ForeignKey);
}
According to the updated example, you want to hide the explicit FK from the entity class public interface, and still let it be visible to EF Core and mapped to the FK column in the database.
The first problem is that the explicitly implemented interface member is not directly discoverable by EF. Also it has no good name, so the default conventions don't apply.
For instance, w/o fluent configuration EF Core will correctly create one to many associations between Parent and Child entities, but since it won't discover the int IChild<Parent>.ForeignKey { get; set; } properties, it would maintain the FK property values through ParentOneId / ParentTwoId shadow properties and not through interface explicit properties. In other words, these properties will not be populated by EF Core and also not considered by the change tracker.
To let EF Core use them you need to map both FK property and database column name using respectively HasForeignKey and HasColumnName fluent API method overloads accepting string property name. Note that the string property name must be fully qualified with the namespace. While Type.FullName provides that string for non-generic types, there is no such property/method for generic types like IChild<ParentOne> (the result has to be "Namespace.IChild<Namespace.ParentOne>"), so let first create some helpers for that:
static string ChildForeignKeyPropertyName<TParent>() where TParent : ParentEntity
=> $"{typeof(IChild<>).Namespace}.IChild<{typeof(TParent).FullName}>.{nameof(IChild<TParent>.ForeignKey)}";
static string ChildForeignKeyColumnName<TParent>() where TParent : ParentEntity
=> $"{typeof(TParent).Name}Id";
The next would be creating a helper method for performing the necessary configuration:
static void ConfigureRelationship<TChild, TParent>(ModelBuilder modelBuilder)
where TChild : ChildEntity, IChild<TParent>
where TParent : ParentEntity, IParent<TChild>
{
var childEntity = modelBuilder.Entity<TChild>();
var foreignKeyPropertyName = ChildForeignKeyPropertyName<TParent>();
var foreignKeyColumnName = ChildForeignKeyColumnName<TParent>();
var foreignKey = childEntity.Metadata.GetForeignKeys()
.Single(fk => fk.PrincipalEntityType.ClrType == typeof(TParent));
// Configure FK column name
childEntity
.Property<int>(foreignKeyPropertyName)
.HasColumnName(foreignKeyColumnName);
// Configure FK property
childEntity
.HasOne<TParent>(foreignKey.DependentToPrincipal.Name)
.WithMany(foreignKey.PrincipalToDependent.Name)
.HasForeignKey(foreignKeyPropertyName);
}
As you can see, I'm using EF Core provided metadata services to find the names of the corresponding navigation properties.
But this generic method actually shows the limitation of this design. The generic constrains allow us to use
childEntity.Property(c => c.ForeignKey)
which compiles fine, but doesn't work at runtime. It's not only for fluent API methods, but basically any generic method involving expression trees (like LINQ to Entities query). There is no such problem when the interface property is implemented implicitly with public property.
We'll return to this limitation later. To complete the mapping, add the following to your OnModelCreating override:
ConfigureRelationship<ChildOne, ParentOne>(modelBuilder);
ConfigureRelationship<ChildOne, ParentTwo>(modelBuilder);
And now EF Core will correctly load / take into account your explicitly implemented FK properties.
Now back to limitations. There is no problem to use generic object services like your AdoptAll method or LINQ to Objects. But you can't access these properties generically in expressions used to access EF Core metadata or inside LINQ to Entities queries. In the later case you should access it through navigation property, or in both scenarios you should access in by the name returned from the ChildForeignKeyPropertyName<TParent>() method. Actually queries will work, but will be evaluated locally thus causing performance issues or unexpected behaviors.
E.g.
static IEnumerable<TChild> GetChildrenOf<TChild, TParent>(DbContext db, int parentId)
where TChild : ChildEntity, IChild<TParent>
where TParent : ParentEntity, IParent<TChild>
{
// Works, but causes client side filter evalution
return db.Set<TChild>().Where(c => c.ForeignKey == parentId);
// This correctly translates to SQL, hence server side evaluation
return db.Set<TChild>().Where(c => EF.Property<int>(c, ChildForeignKeyPropertyName<TParent>()) == parentId);
}
To recap shortly, it's possible, but use with care and make sure it's worth for the limited generic service scenarios it allows. Alternative approaches would not use interfaces, but (combination of) EF Core metadata, reflection or Func<...> / Expression<Func<..>> generic method arguments similar to Queryable extension methods.
Edit: Regarding the second question edit, fluent configuration
modelBuilder.Entity<ChildOne>()
.HasOne(p => p.ParentOne)
.WithMany(c => c.ChildOnes)
.HasForeignKey(fk => ((IChild<ParentOne>)fk).ForeignKey);
modelBuilder.Entity<ChildOne>()
.HasOne(p => p.ParentTwo)
.WithMany(c => c.ChildOnes)
.HasForeignKey(fk => ((IChild<ParentTwo>)fk).ForeignKey);
produces the following migration for ChildOne
migrationBuilder.CreateTable(
name: "ChildOne",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
ForeignKey = table.Column<int>(nullable: false),
Name = table.Column<string>(nullable: true),
Balance = table.Column<decimal>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ChildOne", x => x.Id);
table.ForeignKey(
name: "FK_ChildOne_ParentOne_ForeignKey",
column: x => x.ForeignKey,
principalTable: "ParentOne",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ChildOne_ParentTwo_ForeignKey",
column: x => x.ForeignKey,
principalTable: "ParentTwo",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
Note the single ForeignKey column and the attempt to use it as foreign key to both ParentOne and ParentTwo. It suffers the same problems as using a constrained interface property directly, so I would assume it not working.
I'll try explain simply my Entity Framework model. I have a User object which has a collection of zero or more UserInterest objects. Each user interest object has only three properties, unique ID, User Id and Description.
Whenever the user updates the User object, it should also update the related UserInterest objects but because these are free form (ie not part of a list of allowed interests), I want the user to pass in a list of type "string" to the webmethod of the names of all their interests. The code would ideally then look at the users existing list of interests, remove any that were no longer relevant and add in new ones and leave the ones which already exist.
My object model definitions
[Table("User")]
public class DbUser {
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int UserId { get; set; }
public virtual IList<DbUserInterest> Interests { get; set; }
}
[Table("UserInterest")]
public class DbUserInterest : IEntityComparable<DbUserInterest>
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public long Id { get; set; }
public virtual DbUser User { get; set; }
public int? UserId { get; set; }
}
The context Fluent mappings
modelBuilder.Entity<DbUser>()
.HasKey(u => u.UserId);
modelBuilder.Entity<DbUser>()
.Property(u => u.UserId)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<DbUser>()
.HasMany(u => u.Interests)
.WithRequired(p => p.User)
.WillCascadeOnDelete(true);
modelBuilder.Entity<DbUserInterest>()
.HasKey(p => p.Id);
modelBuilder.Entity<DbUserInterest>()
.Property(p => p.Id)
.HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
modelBuilder.Entity<DbUserInterest>()
.HasRequired(p => p.User)
.WithMany(u => u.Interests)
.WillCascadeOnDelete(true);
And lastly my webmethod code and repository method to do the update
public UpdateUserProfileDetailsResponse UpdateUserProfileDetails(UpdateUserProfileDetailsRequest request)
{
try
{
var dbItem = _userDataRepository.GetItem(request.Header.UserId);
dbItem.Interests.Clear();
foreach (var dbInterest in request.UserInterests)
dbItem.Interests.Add(new DbUserInterest { Name = dbInterest, UserId = dbItem.UserId});
_userDataRepository.UpdateItem(dbItem);
_userDataRepository.Save();
}
catch (Exception ex)
{
}
}
public override bool UpdateItem(DbUser item)
{
var dbItem = GetItem(item.UserId);
if (dbItem == null)
throw new DataRepositoryException("User not found to update", "UserDataRepository.UpdateItem");
var dbInterests = Work.Context.UserInterests.Where(b => b.UserId == item.UserId).ToList();
var interestsToRemove = (from interest in dbInterests let found = item.Interests.Any(p => p.IsSame(interest)) where !found select interest).ToList();
var interestsToAdd = (from interest in item.Interests let found = dbInterests.Any(p => p.IsSame(interest)) where !found select interest).ToList();
foreach (var interest in interestsToRemove)
Work.Context.UserInterests.Remove(interest);
foreach (var interest in interestsToAdd)
{
interest.UserId = item.UserId;
Work.Context.UserInterests.Add(interest);
}
Work.Context.Entry(dbItem).State = EntityState.Modified;
return Work.Context.Entry(dbItem).GetValidationResult().IsValid;
}
When I run this, at the Repository.Save() line I get the exception
Assert.IsTrue failed. An unexpected error occurred: An error occurred while updating the entries. See the inner exception for details.
But interestingly in the webmethod if I comment out the line dbItem.Interests.Clear(); it doesn't throw an error, although then of course you get duplicates or extra items as it thinks everything is a new interest to add. However removing this line is the only way I can get the code to not error
Before, I had the UserId property of the Interest object set to non nullable and then the error was slightly different, something about you cannot change the relationship of a foreign key entity that is non nullable, which is why I changed the property to nullable but still no go.
Any thoughts?
You can't just clear the collection and then try to rebuild it. EF doesn't work that way. The DbContext keeps track of all of the objects that were brought back from the database in its Change Tracker. Doing it your way will of course cause duplicates because EF sees that they're not in the Change Tracker at all so they must be brand new objects necessitating being added to the database.
You'll need to either do the add/remove logic in your UpdateUserProfileDetails method, or else you have to find a way to pass request.UserInterests into your UpdateItem method. Because you need to adjust the existing entities, not the ones found on the request (which EF thinks are new).
you could try in this way
remove
dbItem.Interests.Clear();
then
foreach (var dbInterest in request.UserInterests){
if(dbItem.Interests.Any()){
if (dbItem.Interests.Count(i=> i.Name==dbInterest && i.UserId==dbItem.UserId) == 0){
dbItem.Interests.Add(new DbUserInterest { Name = dbInterest, UserId = dbItem.UserId});
}
}
}