I am trying to define two classes with the same collection-relation, through the same key on a third class, preferably using EF 6 code annotations.
I have three tables;
Currentis a quick reference. It contains only the latest version of each object, and not all of the fields. It is keyed onObjectId(but also has aVersionIdthat ticks up each time an object is saved)Archiveis a history ofCurrent. It has exactly the same fields asCurrent, but hereVersionIdis included in the key. So it is keyed onObjectId+VersionIdChangecontains an XML blob with field-by-field modifications to the objects. It has a complete list of the fields modified between two versions of an object, stored as 'field name', 'old value', 'new value'.
It has anObjectIdcolumn, but you have to parse the XML to get theVersionIds involved (found in the form of 'old value', 'new value')
On both Current and Archive classes, I want a Changes collection with the changes related to that ObjectId. Having reverse relations back to Current and Archive would also be nice.
In SQL terms, the code for populating each of those relations would be:
'Current.Changes' => SELECT * FROM Change WHERE ObjectId = <this.ObjectId>
'Archive.Changes' => SELECT * FROM Change WHERE ObjectId = <this.ObjectId>
'Change.Current' => SELECT * FROM Current WHERE ObjectId = <this.ObjectId>
'Change.ArchivedObjects' => SELECT * FROM Archive WHERE ObjectId = <this.ObjectId>
This is how it looks right now:
[Table(nameof(Current))]
public class Current : ITrObject // = common interface for 'Current' and 'Archive'
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public int ObjectId { get; set; }
public int VersionId { get; set; }
// ... Lots of properties ...
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<Change> Changes { get; set; }
}
[Table(nameof(Archive))]
public class Archive : ITrObject
{
[Key, Column(Order = 0), DatabaseGenerated(DatabaseGeneratedOption.None)]
public int ObjectId { get; set; }
[Key, Column(Order = 1), DatabaseGenerated(DatabaseGeneratedOption.None)]
public int VersionId { get; set; }
// ... Lots of properties ...
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<Change> Changes { get; set; }
}
[Table(nameof(Change))]
public class Change
{
[Key, DatabaseGenerated(DatabaseGeneratedOption.None)]
public int ChangeId { get; set; }
public int? ObjectId { get; set; } // 'int?' because the source system sometimes creates changes that are logged, but not saved yet. Probably some kind of 'autosave' feature.
// ... Lots of properties ...
[ForeignKey(nameof(ObjectId))]
public virtual Current Current { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<Archive> ArchivedObjects { get; set; }
}
Modelbuilder code:
modelBuilder.Entity<Current>()
.HasMany(cur => cur.Changes)
.WithOptional(chg => chg.Current)
.HasForeignKey<int?>(chg => chg.ObjectId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<Archive>()
.HasMany(arch => arch.Changes)
.WithMany(); // Stuck here...
(All code has been cut down and anonymized somewhat, but I hope I caught all typos now.)
Is this at all possible in EF 6? If not, do you have any suggestions for how to do it in some other way, that doesn't completely crap on the rest of the EF 6 data retrieval?
The relations aren't really that crazy, so it would be nice if it could be done.
I have some influence over the database, but it's mostly given.
I have much more influence over the C# code, but can't do too crazy stuff there either - like migrating to another ORM for example...
EDIT: Some of the configurations I have tried, and their SQL errors
modelBuilder.Entity<Archive>()
.HasMany(arch => arch.Changes)
.WithMany();
... gives the error SqlException: Invalid object name 'Change_ChangeId'.
modelBuilder.Entity<Archive>()
.HasMany(arch => arch.Changes)
.WithMany(chg => chg.Archive);
... gives the error SqlException: Invalid object name 'dbo.ArchiveChanges'.
modelBuilder.Entity<Change>()
.HasMany(chg => chg.ArchivedObjects)
.WithMany();
... gives the error SqlException: Invalid column name 'Archive_ObjectId' Invalid column name 'Archive_VersionId'. Invalid column name 'Archive_ObjecId'.... repeated until truncated.
modelBuilder.Entity<Change>()
.HasMany(chg => chg.ArchivedObjects)
.WithMany(arch => arch.Changes);
... gives the error SqlException: Invalid object name 'dbo.ChangeArchivedObjects'..