Skip to main content
All docs
V24.1
.NET 6.0+

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.

NonAggregatedOneToManyObject_Win

NonAggregatedOneToManyObject_Blazor

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.

AggregatedOneToManyObject_Win

AggregatedOneToManyObject_Blazor

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.

NonAggregatedManyToManyObject_Win

NonAggregatedManyToManyObject_blazor

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.

Address_Win

OneToOneObject_Blazor

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.

See Also