I feel like I'm missing something obvious here; I'm using .Net 5 with Entity Framework Core. The problem is that the foreign key is correct, but the associated navigation property is always empty and has no data. Do I have to do something with the fluent framework, or do something special with my includes?
I have 3 simplified entities and a database context method in this example, the project is much too large to include entirely. In the method, CalendarEvents is a DbSet:
public class CalendarEvent: IJsonSerializable
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
/// <summary>
/// Gets the personnel associated with this event
/// </summary>
public virtual List<SchedulePerson> SchedulePeople { get; set; } = new List<SchedulePerson>();
}
public class SchedulePerson : IJsonSerializable, ICloneable
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; set; }
public int EmployeeId { get; set; }
public virtual Employee Employee { get; set; }
public virtual CalendarEvent AssociatedCalendarEvent { get; set; }
}
public class Employee : IJsonSerializable
{
[Key]
public int Id { get; set; }
public virtual List<SchedulePerson> AssociatedSchedulePeople { get; set; } = new List<SchedulePerson>();
}
public class DbContext : DbContext
{
public DbSet<CalendarEvent> CalendarEvents { get; set; }
public DbSet<SchedulePerson> SchedulePeople { get; set; }
public DbSet<Employee> Employees { get; set; }
public DbContext(DbContextOptions<VibrationContext> options): base(options)
{
}
public CalendarEvent GetEvent(int calendarEventId)
{
var currentCalEvent = this.CalendarEvents.Where(x => x.Id == calendarEventId);
var dummy1 = currentCalEvent.FirstOrDefault();
var dummy2 = currentCalEvent.Include(calEvent => calEvent.SchedulePeople).ToList();
var dummy3 = currentCalEvent.Include(calEvent => calEvent.SchedulePeople).ThenInclude(people => people.Employee).ToList();
return currentCalEvent.FirstOrDefault();
}
}
In this case Employee is the associated navigation property, and the associated key EmployeeId, is correct. For the moment I've added extra logic that will populate each employee per schedule person separately, manually based on the foreign key EmployeeId, when I get the event, but I'd rather not have to add logic like that each time. If that's something unavoidable then that's fine, but I'd like to do things properly and let Entity Framework Core handle as much as possible.
For additional context:
SchedulePerson -> CalendarEvent is a many-to-one relationship
Employee -> SchedulePerson is a many-to-one relationship
In other words a calendar event can contain many schedule persons, but a schedule person can only be associated with one calendar event.
Employee can be associated with many schedule persons, but each schedule person is only associated with one employee.
There should only be one employee for each real person, but there can be multiple SchedulePersons for each real person.
Thank you for your help and let me know if there is any more information I can provide.
Also if anything else looks bad or wrong in these code snippets please let me know.
Edit, this is what I have to do if I want to get the employees in my request:
private void UpdateEmployeeContents(CalendarEvent calendarEvent)
{
foreach (SchedulePerson person in calendarEvent.SchedulePeople)
{
person.Employee = this.Employees.Where(x => x.Id == person.EmployeeId).FirstOrDefault();
}
}
This question already has answers here:
Create code first, many to many, with additional fields in association table
(7 answers)
Closed 2 years ago.
i m working on EF 6 (mapping with many to many relationship) ,see
https://www.codeproject.com/Articles/234606/Creating-a-Many-To-Many-Mapping-Using-Code-First
where it created "PersonCourses" as middle table ,now i have two problem with that In a many-to-many relationship EF manages the join table internally and hidden. It's a table without an Entity class in your model.
so what if i need to access "PersonCourses" in my code (project) and what if i need to add certain columns with it ??
EF can auto-manage the joining table so long as it just contains the two FKs as a composite PK. If you want to add to that then you need to declare the joining table as an entity with a one-to-many from each side.
So instead of:
[Table("Persons")]
public class Person
{
// ...
public virtual ICollection<Course> Courses { get; set; } = new List<Course>();
}
[Table("Courses")]
public class Course
{
// ...
public virtual ICollection<Person> People { get; set; } = new List<Person>();
}
modelBuilder.Entity<Person>()
.HasMany(x => x.Courses)
.WithMany(x => x.People);
you would have:
[Table("Persons")]
public class Person
{
// ...
public virtual ICollection<PersonCourse> PersonCourses { get; set; } = new List<PersonCourse>();
}
[Table("Courses")]
public class Course
{
// ...
public virtual ICollection<PersonCourse> PersonCourses { get; set; } = new List<PersonCourse>();
}
[Table("PersonCourses")]
public class PersonCourse
{
[Key, Column(Order=0), ForeignKey("Person")]
public int PersonId { get; set; }
[Key, Column(Order=1), ForeignKey("Course")]
public int CourseId { get; set; }
// ... any additional properties for the entity.
public virtual Person Person { get; set; }
public virtual Course Course { get; set; }
}
modelBuilder.Entity<Person>()
.HasMany(x => x.PersonCourses)
.WithRequired(x => x.Person);
modelBuilder.Entity<Course>()
.HasMany(x => x.PersonCourses)
.WithRequired(x => x.Course);
The main disadvantage of this is that Person no longer has a collection of Courses, but of PersonCourses so you have to dive the extra level in your projections every time you want to get details about course names. It might be tempting to leave the PersonCourses collection on Person named as "Courses" but I found that this can get misleading as you can end up with collections on some objects called Persons being Person vs. PersonCourse or Courses being Course vs. PersonCourse. It's generally less confusing when the collection name reflects the type.
So instead of:
var courses = context.Persons
.Where(x => x.PersonId == personId)
.SelectMany(x => x.Courses)
.ToList();
You need to change that to:
var courses = context.Persons
.Where(x => x.PersonId == personId)
.SelectMany(x => x.PersonCourses.Select(pc => pc.Course))
.ToList();
Update: To have an Id column on PersonCourse:
[Table("PersonCourses")]
public class PersonCourse
{
[Key]
public int Id { get; set; }
[ForeignKey("Person")]
public int PersonId { get; set; }
[ForeignKey("Course")]
public int CourseId { get; set; }
// ... any additional properties for the entity.
public virtual Person Person { get; set; }
public virtual Course Course { get; set; }
}
... or better, do away with the FK fields in the entity and map them via configuration:
[Table("PersonCourses")]
public class PersonCourse
{
[Key]
public int Id { get; set; }
// ... any additional properties for the entity.
public virtual Person Person { get; set; }
public virtual Course Course { get; set; }
}
EF6 may map these automatically by convention, but explicitly you can use:
modelBuilder.Entity<Person>()
.HasMany(x => x.PersonCourses)
.WithRequired(x => x.Person)
.Map(x => x.Mpakey("PersonId");
modelBuilder.Entity<Course>()
.HasMany(x => x.PersonCourses)
.WithRequired(x => x.Course)
.Map(x => x.Mpakey("CourseId");
I recommend this approach, like Shadow Properties with EF Core to avoid having 2 sources of truth for the Person ID or Course ID.
I.e. PersonCourse.PersonId vs. PersonCourse.Person.Id
When updating entities with navigation properties, you should update references via the navigation property, (personCourse.Course = newCourse) not via a FK property. (personCourse.CourseId = newCourseId) Doing so, or intermixing the source of truth for the FK can lead to weird results depending on what the DbContext is tracking at the time.
I've been wrestling with this for a while and can't quite figure out what's happening. I have a Card entity which contains Sides (usually 2) - and both Cards and Sides have a Stage. I'm using EF Codefirst migrations and the migrations are failing with this error:
Introducing FOREIGN KEY constraint 'FK_dbo.Sides_dbo.Cards_CardId' on
table 'Sides' may cause cycles or multiple cascade paths. Specify ON
DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY
constraints.
Here's my Card entity:
public class Card
{
public Card()
{
Sides = new Collection<Side>();
Stage = Stage.ONE;
}
[Key]
[Required]
public virtual int CardId { get; set; }
[Required]
public virtual Stage Stage { get; set; }
[Required]
[ForeignKey("CardId")]
public virtual ICollection<Side> Sides { get; set; }
}
Here's my Side entity:
public class Side
{
public Side()
{
Stage = Stage.ONE;
}
[Key]
[Required]
public virtual int SideId { get; set; }
[Required]
public virtual Stage Stage { get; set; }
[Required]
public int CardId { get; set; }
[ForeignKey("CardId")]
public virtual Card Card { get; set; }
}
And here's my Stage entity:
public class Stage
{
// Zero
public static readonly Stage ONE = new Stage(new TimeSpan(0, 0, 0), "ONE");
// Ten seconds
public static readonly Stage TWO = new Stage(new TimeSpan(0, 0, 10), "TWO");
public static IEnumerable<Stage> Values
{
get
{
yield return ONE;
yield return TWO;
}
}
public int StageId { get; set; }
private readonly TimeSpan span;
public string Title { get; set; }
Stage(TimeSpan span, string title)
{
this.span = span;
this.Title = title;
}
public TimeSpan Span { get { return span; } }
}
What's odd is that if I add the following to my Stage class:
public int? SideId { get; set; }
[ForeignKey("SideId")]
public virtual Side Side { get; set; }
The migration runs successfully. If I open up SSMS and look at the tables, I can see that Stage_StageId has been added to Cards (as expected/desired), however Sides contains no reference to Stage (not expected).
If I then add
[Required]
[ForeignKey("StageId")]
public virtual Stage Stage { get; set; }
public int StageId { get; set; }
To my Side class, I see StageId column added to my Side table.
This is working, but now throughout my application, any reference to Stage contains a SideId, which is in some cases totally irrelevant. I'd like to just give my Card and Side entities a Stage property based on the above Stage class without polluting the stage class with reference properties if possible... what am I doing wrong?
Because Stage is required, all one-to-many relationships where Stage is involved will have cascading delete enabled by default. It means, if you delete a Stage entity
the delete will cascade directly to Side
the delete will cascade directly to Card and because Card and Side have a required one-to-many relationship with cascading delete enabled by default again it will then cascade from Card to Side
So, you have two cascading delete paths from Stage to Side - which causes the exception.
You must either make the Stage optional in at least one of the entities (i.e. remove the [Required] attribute from the Stage properties) or disable cascading delete with Fluent API (not possible with data annotations):
modelBuilder.Entity<Card>()
.HasRequired(c => c.Stage)
.WithMany()
.WillCascadeOnDelete(false);
modelBuilder.Entity<Side>()
.HasRequired(s => s.Stage)
.WithMany()
.WillCascadeOnDelete(false);
I had a table that had a circular relationship with others and I was getting the same error. Turns out it is about the foreign key which was not nullable. If the key is not nullable the related object must be deleted, and circular relations don't allow that. So use nullable foreign key.
[ForeignKey("StageId")]
public virtual Stage Stage { get; set; }
public int? StageId { get; set; }
Anybody wondering how to do it in EF core:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
foreach (var relationship in modelBuilder.Model.GetEntityTypes().SelectMany(e => e.GetForeignKeys()))
{
relationship.DeleteBehavior = DeleteBehavior.Restrict;
}
..... rest of the code.....
I was getting this error for lots of entities when I was migrating down from an EF7 model to an EF6 version. I didn't want to have to go through each entity one at a time, so I used:
builder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
builder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
You can set cascadeDelete to false or true (in your migration Up() method). Depends upon your requirement.
AddForeignKey("dbo.Stories", "StatusId", "dbo.Status", "StatusID", cascadeDelete: false);
In .NET Core I changed the onDelete option to ReferencialAction.NoAction
constraints: table =>
{
table.PrimaryKey("PK_Schedule", x => x.Id);
table.ForeignKey(
name: "FK_Schedule_Teams_HomeId",
column: x => x.HomeId,
principalTable: "Teams",
principalColumn: "Id",
onDelete: ReferentialAction.NoAction);
table.ForeignKey(
name: "FK_Schedule_Teams_VisitorId",
column: x => x.VisitorId,
principalTable: "Teams",
principalColumn: "Id",
onDelete: ReferentialAction.NoAction);
});
I had this issue also, I solved it instantly with this answer from a similar thread
In my case, I didn't want to delete the dependent record on key deletion. If this is the case in your situation just simply change the Boolean value in the migration to false:
AddForeignKey("dbo.Stories", "StatusId", "dbo.Status", "StatusID", cascadeDelete: false);
Chances are, if you are creating relationships which throw this compiler error but DO want to maintain cascade delete; you have an issue with your relationships.
I fixed this. When you add the migration, in the Up() method there will be a line like this:
.ForeignKey("dbo.Members", t => t.MemberId, cascadeDelete:True)
If you just delete the cascadeDelete from the end it will work.
Just for documentation purpose, to someone that comes on the future, this thing can be solved as simple as this, and with this method, you could do a method that disabled one time, and you could access your method normally
Add this method to the context database class:
protected override void OnModelCreating(DbModelBuilder modelBuilder) {
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
}
In .NET Core I played with all upper answers - but without any success.
I made changes a lot in DB structure and every time added new migration attempting to update-database, but received the same error.
Then I started to remove-migration one by one until Package Manager Console threw me exception:
The migration '20170827183131_***' has already been applied to the database
After that, I added new migration (add-migration) and update-database successfully
So my suggestion would be: clear out all your temp migrations, until your current DB state.
public partial class recommended_books : DbMigration
{
public override void Up()
{
CreateTable(
"dbo.RecommendedBook",
c => new
{
RecommendedBookID = c.Int(nullable: false, identity: true),
CourseID = c.Int(nullable: false),
DepartmentID = c.Int(nullable: false),
Title = c.String(),
Author = c.String(),
PublicationDate = c.DateTime(nullable: false),
})
.PrimaryKey(t => t.RecommendedBookID)
.ForeignKey("dbo.Course", t => t.CourseID, cascadeDelete: false) // was true on migration
.ForeignKey("dbo.Department", t => t.DepartmentID, cascadeDelete: false) // was true on migration
.Index(t => t.CourseID)
.Index(t => t.DepartmentID);
}
public override void Down()
{
DropForeignKey("dbo.RecommendedBook", "DepartmentID", "dbo.Department");
DropForeignKey("dbo.RecommendedBook", "CourseID", "dbo.Course");
DropIndex("dbo.RecommendedBook", new[] { "DepartmentID" });
DropIndex("dbo.RecommendedBook", new[] { "CourseID" });
DropTable("dbo.RecommendedBook");
}
}
When your migration fails you are given a couple of options:
'Introducing FOREIGN KEY constraint 'FK_dbo.RecommendedBook_dbo.Department_DepartmentID' on table 'RecommendedBook' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
Could not create constraint or index. See previous errors.'
Here is an example of using the 'modify other FOREIGN KEY constraints' by setting 'cascadeDelete' to false in the migration file and then run 'update-database'.
Make your Foreign key attributes nullable. That will work.
This sounds weird and I don't know why, but in my case that was happening because my ConnectionString was using "." in "data source" attribute. Once I changed it to "localhost" it workded like a charm. No other change was needed.
The existing answers are great I just wanted to add that I ran into this error because of a different reason. I wanted to create an Initial EF migration on an existing DB but I didn't use the -IgnoreChanges flag and applied the Update-Database command on an empty Database (also on the existing fails).
Instead I had to run this command when the current db structure is the current one:
Add-Migration Initial -IgnoreChanges
There is likely a real problem in the db structure but save the world one step at a time...
In .NET 5 < and .NET Core 2.0 < you can use .OnDelete(DeleteBehavior.Restrict) in OnModelCreating like #Nexus23 answer but you do not need to disable cascade for every model.
Example with join entity type configuration many-to-many:
internal class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options)
: base(options)
{
}
public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<PostTag>(
j => j
.HasOne(pt => pt.Tag)
.WithMany(t => t.PostTags)
.HasForeignKey(pt => pt.TagId)
.OnDelete(DeleteBehavior.Restrict),
j => j
.HasOne(pt => pt.Post)
.WithMany(p => p.PostTags)
.HasForeignKey(pt => pt.PostId)
.OnDelete(DeleteBehavior.Restrict),
j =>
{
j.Property(pt => pt.PublicationDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
j.HasKey(t => new { t.PostId, t.TagId });
});
}
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public ICollection<Tag> Tags { get; set; }
public List<PostTag> PostTags { get; set; }
}
public class Tag
{
public string TagId { get; set; }
public ICollection<Post> Posts { get; set; }
public List<PostTag> PostTags { get; set; }
}
public class PostTag
{
public DateTime PublicationDate { get; set; }
public int PostId { get; set; }
public Post Post { get; set; }
public string TagId { get; set; }
public Tag Tag { get; set; }
}
Sources:
https://learn.microsoft.com/en-us/ef/core/modeling/relationships?tabs=fluent-api%2Cfluent-api-simple-key%2Csimple-key#join-entity-type-configuration
https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.deletebehavior?view=efcore-5.0
This does require you to remove the many to many relationship yourself or you will receive the following error when you remove a parent entity:
The association between entity types '' and '' has been severed, but
the relationship is either marked as required or is implicitly
required because the foreign key is not nullable. If the
dependent/child entity should be deleted when a required relationship
is severed, configure the relationship to use cascade deletes.
Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to
see the key values
You can solve this by using DeleteBehavior.ClientCascade instead which will allow EF to perform cascade deletes on loaded entities.
internal class MyContext : DbContext
{
public MyContext(DbContextOptions<MyContext> options)
: base(options)
{
}
public DbSet<Post> Posts { get; set; }
public DbSet<Tag> Tags { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Post>()
.HasMany(p => p.Tags)
.WithMany(p => p.Posts)
.UsingEntity<PostTag>(
j => j
.HasOne(pt => pt.Tag)
.WithMany(t => t.PostTags)
.HasForeignKey(pt => pt.TagId)
.OnDelete(DeleteBehavior.Cascade),
j => j
.HasOne(pt => pt.Post)
.WithMany(p => p.PostTags)
.HasForeignKey(pt => pt.PostId)
.OnDelete(DeleteBehavior.ClientCascade),
j =>
{
j.Property(pt => pt.PublicationDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
j.HasKey(t => new { t.PostId, t.TagId });
});
}
}
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public ICollection<Tag> Tags { get; set; }
public List<PostTag> PostTags { get; set; }
}
public class Tag
{
public string TagId { get; set; }
public ICollection<Post> Posts { get; set; }
public List<PostTag> PostTags { get; set; }
}
public class PostTag
{
public DateTime PublicationDate { get; set; }
public int PostId { get; set; }
public Post Post { get; set; }
public string TagId { get; set; }
public Tag Tag { get; set; }
}
https://learn.microsoft.com/en-us/dotnet/api/microsoft.entityframeworkcore.deletebehavior?view=efcore-5.0
None of the aforementioned solutions worked for me. What I had to do was use a nullable int (int?) on the foreign key that was not required (or not a not null column key) and then delete some of my migrations.
Start by deleting the migrations, then try the nullable int.
Problem was both a modification and model design. No code change was necessary.
The simple way is to, Edit your migration file (cascadeDelete: true) into (cascadeDelete: false) then after assign the Update-Database command in your Package Manager Console.if it's problem with your last migration then all right. Otherwise check your earlier migration history, copy those things, paste into your last migration file, after that do it the same thing. it perfectly works for me.
You could add this in your DataContext.cs, this works for me...
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>();
modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>();
}
I ran into the same problem and stuck for a long. The following steps saved me.
Go through the constraints and change the onDelete ReferentialAction to NoAction from Cascade
constraints: table =>
{
table.PrimaryKey("PK_table1", x => x.Id);
table.ForeignKey(
name: "FK_table1_table2_table2Id",
column: x => x.table2Id,
principalTable: "table2",
principalColumn: "Id",
onDelete: ReferentialAction.NoAction);
});
This is probably just because my knowledge with the EF Code First fluent API is lacking, but I'm stumped.
I want to model the following:
A Groups collection with Id and Name
A Users collection with Id and Name
Each user is assigned to exactly one primary group
Each user may have zero or many secondary groups
The table structure I'm going for would look like:
Groups
Id
Name
Users
Id
Name
PrimaryGroupId
SecondaryGroupAssignments
UserId
GroupId
I've been beating my head against a wall trying to model this with EF Code First, but I can't get it to accept both relationships between User and Group. Sorry for not posting any .NET code (I'm happy to), but it's probably all wrong anyway.
Is there a way to make EF model this? I'm assuming I have to do some sort of configuration with the Fluent API. Maybe a better question is: is there any good, definitive reference for the Fluent API?
Thanks!
Try this (untested):
public class Group
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<User> PrimaryUsers { get; set; }
public virtual ICollection<User> SecondaryUsers { get; set; }
}
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public int PrimaryGroupId { get; set; }
public virtual Group PrimaryGroup { get; set; }
public virtual ICollection<Group> SecondaryGroups { get; set; }
}
public class Context : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Group> Groups { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Entity<User>()
.HasRequired(u => u.PrimaryGroup)
.WithMany(g => g.PrimaryUsers)
.HasForeignKey(u => u.PrimaryGroupId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<User>()
.HasMany(u => u.SecondaryGroups)
.WithMany(g => g.SecondaryUsers)
.Map(m => m.MapLeftKey("UserId")
.MapRightKey("GroupId")
.ToTable("SecondaryGroupAssignments"));
}
}
Based on Ladislav's excellent answer, here's how to do it without using any mappings - just attributes applied to the Model classes themselves:
public class Group
{
public int Id { get; set; }
[MaxLength(300)]
public string Name { get; set; }
public ICollection<User> Users { get; set; }
}
public class User
{
public int Id { get; set; }
[MaxLength(300)]
public string Name { get; set; }
[ForeignKey("PrimaryGroup")]
public int PrimaryGroupId { get; set; }
[Required]
public Group PrimaryGroup { get; set; }
[InverseProperty("Users")]
public ICollection<Group> SecondaryGroups { get; set; }
}
Notes
If you want, you can add the virtual keyword to the 2 ICollections and the Group. This allows lazy-loading. Performance-wise, I don't recommend it, but it is possible.
I included MaxLength attributes with an arbitrary (but safe) length of 300, because putting strings out in EF without a MaxLength gets you low-performance NVarChar(MAX) columns. Totally irrelevant to what's being asked but better to post good code.
I recommend against class names "User" and "Group" for your EF classes. They're going to complicate any SQL you attempt to run later, having to type [User] and [Group] to access them, and complicate using these classes in MVC Controllers where your class User will conflict with the Context property User that gives you access to the Asp.Net Identity library.
This is most likely one for all you sexy DBAs out there:
How would I effieciently model a relational database whereby I have a field in an "Event" table which defines a "SportType"?
This "SportsType" field can hold a link to different sports tables E.g. "FootballEvent", "RubgyEvent", "CricketEvent" and "F1 Event".
Each of these Sports tables have different fields specific to that sport.
My goal is to be able to genericly add sports types in the future as required, yet hold sport specific event data (fields) as part of my Event Entity.
Is it possible to use an ORM such as NHibernate / Entity framework / DataObjects.NET which would reflect such a relationship?
I have thrown together a quick C# example to express my intent at a higher level:
public class Event<T> where T : new()
{
public T Fields { get; set; }
public Event()
{
EventType = new T();
}
}
public class FootballEvent
{
public Team CompetitorA { get; set; }
public Team CompetitorB { get; set; }
}
public class TennisEvent
{
public Player CompetitorA { get; set; }
public Player CompetitorB { get; set; }
}
public class F1RacingEvent
{
public List<Player> Drivers { get; set; }
public List<Team> Teams { get; set; }
}
public class Team
{
public IEnumerable<Player> Squad { get; set; }
}
public class Player
{
public string Name { get; set; }
public DateTime DOB { get; set;}
}
DataObjects.Net supports automatic mappings for open generics. Some details on this are described here.
You can do this by having all the Event types inherit from an abstract Event base class. This make sense to me because all the events share some common properties: date, venue, etc. You can use a table per concrete class or table per subclass strategy to store the objects in a relational database. Here are some links to articles describing inheritance mapping with NHibernate:
Chapter 8. Inheritance Mapping
Fluent NHibernate and Inheritance Mapping
NHibernate Mapping – Inheritance
The example converted to DO4 must look as follows:
// I'd add this type - adding an abstract base makes design more clean + allows you to share
// the behavior among all the descendants
[Serializable]
[HierarchyRoot]
public abstract class EventBase : Entity
{
[Key]
Guid Id { get; private set; } // Or some other type
}
[Serializable]
public class Event<T> : EventBase
where T : IEntity, new() // IEntity indicates DO4 must try to map its descendants automatically
// Although I'd put some stronger requirement, e.g. by using IEventData instead of IEntity here
{
public T Data { get; set; }
public Event(T data)
{
Data = data;
}
}
[Serializable]
[HierarchyRoot]
public class FootballEvent
{
// You need [Key] here
public Team CompetitorA { get; set; }
public Team CompetitorB { get; set; }
}
[Serializable]
[HierarchyRoot]
public class TennisEvent
{
// You need [Key] here
public Player CompetitorA { get; set; }
public Player CompetitorB { get; set; }
}
[Serializable]
[HierarchyRoot]
public class F1RacingEvent
{
// You need [Key] here
public EntitySet<Player> Drivers { get; private set; }
public EntitySet<Team> Teams { get; private set; }
}
[Serializable]
[HierarchyRoot]
public class Team
{
// You need [Key] here
public EntitySet<Player> Squad { get; set; }
}
[Serializable]
[HierarchyRoot]
public class Player
{
public string Name { get; set; }
public DateTime DOB { get; set; }
}
In this case Event instances will be available (= mapped automatically) for all suitable Ts from model. E.g. in this case they'll be:
- EventBase // Yes, even it, coz it's suitable
- FootballEvent
- TennisEvent
- F1RacingEvent
- Team
- Player
If you'd like to restrict this to just certain types, you must do the following:
- Add an interface inherited from IEntity all these types will support, e.g. IEventData.
- Use it as generic parameter constraint for generic parameter T in Event.
Cross Posted from: http://forum.x-tensive.com/viewtopic.php?f=29&t=5820,
Answer By Alex Yakunin,
Chief Executive DataObjects.NET
There are a bunch of options like XML columns and EAV (also known as database within a database), but none of which will translate well with an ORM to traditional static object-oriented languages, and all of which have drawbacks with respect to data type safety and referential integrity at the database level.
If you need this level of dynamic structure in both the database and the client, you might need to go with an object or document database (and language) which is much more dynamic by design - relational databases tend to work best with static relationships and data models.