Relationships Between Entities in Code and UI (EF Core)
- 8 minutes to read
When designing a business model, it can be necessary to set specific relationships between business objects. This topic describes how to set relationships between entities that are available in the WinForms and ASP.NET Core Blazor applications created with Entity Framework Core and demonstrates how these relationships will be organized in a UI.
Tip
To learn about the relationships between objects in XPO, refer to the following help topic: Relationships Between Persistent Objects in Code and UI (XPO).
The “Many” side is a collection property. The UI displays it with the help of the DevExpress.ExpressApp.Editors.ListPropertyEditor
in WinForms and ASP.NET Core Blazor applications. To show the “One” side, XAF uses DevExpress.ExpressApp.Win.Editors.LookupPropertyEditor
and DevExpress.ExpressApp.Blazor.Editors.LookupPropertyEditor
. If ExpandObjectMembersAttribute is applied to the reference property with the ExpandObjectMembers.Never parameter, DevExpress.ExpressApp.Win.Editors.ObjectPropertyEditor
and DevExpress.ExpressApp.Blazor.Editors.ObjectPropertyEditor
are used instead. Each entity collection has an individual Actions set. This set depends on the collection type.
One-to-Many (Non-Aggregated)
The relationship between Department and Contacts illustrates the One-to-Many type. Multiple (“Many”) Contacts can be associated with a single (“One”) Department. In this example, the Department
entity contains a child ContactsCollection
and is the “One” side of its One-to-Many relationship.
The List View that displays the ContactsCollection
is accompanied by a New Action. This Action allows end users to add new Contact
entities to an existing Department
(including the current entity). In addition, the Link and Unlink Actions are available and allow you to add and remove a reference to a Contact
object from another collection.
Note
If you get the “The DELETE statement conflicted with the REFERENCE constraint” exception when you delete a parent object, refer to the following section for a solution: One-to-Many Behavior on Delete.
The following code demonstrates how you can implement this type of relationship:
using DevExpress.Persistent.Base;
using DevExpress.Persistent.BaseImpl.EF;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Component.DataAnnotations;
// ...
[DefaultClassOptions]
public class Department : BaseObject {
public virtual string Name { get; set; }
public virtual IList<Contact> ContactsCollection { get; set; } = new ObservableCollection<Contact>();
}
// Make sure that you use options.UseChangeTrackingProxies() in your DbContext settings.
Important
While it is possible to use different generic types such as ICollection<T>
, IList<T>
, and so on for the public/external collection property declaration, the inner collection must implement the System.Collections.Specialized.INotifyCollectionChanged
interface for change notifications through the INotifyCollectionChanged.CollectionChanged
event (see above). We recommend that you use ObservableCollection<T>
internally as the default option.
One-to-Many (Aggregated)
Let’s assume that a Contact has a collection of Notes, which are aggregated with their parent Contact. In this case, the Note
entity declares the “One” aggregated side of the One-to-Many relationship with the Contact entity.
Note
In Entity Framework Core, the aggregation mechanism doesn’t support cascade deletion. Use the technique described in the following section: One-to-Many Behavior on Delete. Alternatively, you can implement this functionality as described in the following section: Cascade Deletion for Aggregated Entities.
The List View that displays the NotesCollection
is accompanied by the New Action. This Action allows end users to add new Note
entities. Note that in this case, the Contact
property of a new Note
is automatically set to the current Contact
.
A collection is aggregated if it is decorated with the AggregatedAttribute. The following code demonstrates how you can implement this type of relationship:
using DevExpress.Persistent.Base;
using DevExpress.Persistent.BaseImpl.EF;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
// ...
[DefaultClassOptions]
public class Contact {
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
[DevExpress.ExpressApp.DC.Aggregated]
public virtual IList<Note> NotesCollection { get; set; } = new ObservableCollection<Note>();
}
// Make sure that you use options.UseChangeTrackingProxies() in your DbContext settings.
Many-to-Many
Consider the following entity relationship. A Contact
can have a collection of Tasks
and each Task
can be assigned to a number of Contacts
. Thus, the relationship between Contact
and Task
entities is Many-to-Many.
The List View that displays the TasksCollection
is accompanied by the Link Action. This action allows end users to add references to existing Task
objects. The New Action is not applied to this collection due to unique conceptual properties of the Many-to-Many relationship. However, you can create a new Task
in the Link Action’s pop-up window.
End users can also use the Unlink Action to remove references to Task
objects from the collection.
The following code demonstrates how you can implement this type of relationship:
using DevExpress.Persistent.Base;
using DevExpress.Persistent.BaseImpl.EF;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
// ...
[DefaultClassOptions]
public class Contact : BaseObject {
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
public virtual IList<Task> TasksCollection { get; set; } = new ObservableCollection<Task>();
}
// Make sure that you use options.UseChangeTrackingProxies() in your DbContext settings.
Note
XAF does not support usage scenarios where a many-to-many relationship is only defined on one side of a relation. Always define collection navigation properties on both sides of a relation as demonstrated in the code sample above.
One-to-One
Consider the following entity relationship. Each Contact
can have only one unique Address
and one Address
cannot be assigned to many Contacts
. This is a One-to-One relationship.
This relationship doesn’t provide a collection side. Note that in this instance, the Contact
property of the new Address
objects will be automatically set to the current Contact
.
The following code demonstrates how you can implement this type of relationship. In this relationship type, it is important to explicitly declare a Primary Key of a parent entity as a Foreign Key in the related entity.
using DevExpress.Persistent.Base;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
// ...
[DefaultClassOptions]
public class Contact {
[Key, Browsable(false)]
public virtual Guid ID { get; set; }
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
public virtual Address Address { get; set; }
}
// Make sure that you use options.UseChangeTrackingProxies() in your DbContext settings.
One-to-Many Behavior on Delete
When you delete a parent object, Entity Framework Core (in its default configuration) tries to load all related child objects. To delete an object without loading all its associations, add the following code snippet to the OnModelCreating
method of your project’s DbContext
:
namespace YourApplicationName.Module.BusinessObjects;
public class YourApplicationNameDbContext : DbContext {
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
modelBuilder.SetOneToManyAssociationDeleteBehavior(DeleteBehavior.SetNull, DeleteBehavior.Cascade);
// ...
}
}
This code snippet sets up DeleteBehavior.SetNull
for entities bound by the non-aggregated One-to-Many relationship and DeleteBehavior.Cascade
for entities bound by the aggregated relationship.
Note
New projects generated by the Solution Wizard already contain this code snippet.
Your associations may be more complex. For example, they can have links to base types. In such cases, you may need to use Fluent API to configure the association’s behavior. For more information, refer to the following section: Cascade Deletion for Aggregated Entities.
Cascade Deletion for Aggregated Entities
In applications with Entity Framework Core, aggregation does not use a nested IObjectSpace. To implement a cascade deletion mechanism, enforce it in the model builder in the OnModelCreating
method. To do this, call the OnDelete(DeleteBehavior.Cascade) method as shown in the following code snippet:
public class MySolutionDbContext : DbContext {
//...
protected override void OnModelCreating(ModelBuilder modelBuilder) {
modelBuilder.Entity<Contact>()
.HasMany(r => r.NotesCollection)
.WithOne(x => x.Contact)
.OnDelete(DeleteBehavior.Cascade);
}
}
For more information, refer to the following article: Cascade Delete.