Compositional Hierarchies

[ This document was written for WCF Services Version 1 Service Pack 2 and might not be up to date Please see Release Notes or Changelog for a list of changes since WCF RIA Services ]

Open Ria Services enables you to create application logic for data classes that belong to compositional hierarchies that contain classes associated by “has a” relationships in which the containing object (the whole or the parent) controls the creation and lifetime of the contained object (the part or the descendent). For example, the SalesOrderHeader entity has a SalesOrderDetail entity as the details concerning an order only exist as part of the order. For clarify, the composition of classes may be contrasted with the subtyping of classes, which consists of creating a more specific type (a car) by adding details to a more general type (a vehicle). This results in an inheritance hierarchy in which the detailed (derived) class can still be treated as the general (base) type because, to use the example, a car “is (still) a” vehicle.

After defining the compositional relationship between the relevant classes, you can perform data modification operations on the entities that treat them as a single unit instead of having to deal with them as separate entities. This simplifies middle-tier logic because you can write application logic for the entire hierarchy instead of having to split that logic to apply to each entity and then attempt to coordinate that split logic during data operations.

Understanding Compositional Hierarchies

In a hierarchy of entities, one entity is referred to as the parent entity and the other related entities are referred to as descendant entities. The parent entity is the class that represents data, which is the single root for the data in the descendant entities. For example, the SalesOrderHeader entity is the parent entity, and SalesOrderDetail is a descendant entity. A single record in the SalesOrderHeader entity can be linked to several records in the SalesOrderDetail entity.

Data classes that are part of a hierarchical relationship typically have the following characteristics:

  • The relationship between the entities can be represented as tree with the descendant entities connected to a single parent entity. The descendant entities can extend for any number of levels.

  • The lifetime of a descendant entity is contained within the lifetime of the parent entity.

  • The descendant entity does not have a meaningful identity outside of the context of the parent entity.

  • Data operations on the entities require that the entities be treated as single unit. For example, adding, deleting, or updating a record in the descendant entity requires a corresponding change in the parent entity.

Defining a Compositional Relationship

You define a compositional relationship between entities by applying the CompositionAttribute attribute to the property that represents the association between entities. The following example shows how to define a compositional relationship between SalesOrderHeader and SalesOrderDetail by using a metadata class. The CompositionAttribute attribute is in the System.ComponentModel.DataAnnotations namespace. You have to reference that namespace by using the using or Imports statement to apply the attribute as shown in the following code.

<MetadataTypeAttribute(GetType(SalesOrderHeader.SalesOrderHeaderMetadata))>  _
Partial Public Class SalesOrderHeader

    Friend NotInheritable Class SalesOrderHeaderMetadata

        Private Sub New()
            MyBase.New
        End Sub

        <Include()> _
        <Composition()> _
        Public SalesOrderDetails As EntityCollection(Of SalesOrderDetail)

    End Class
End Class
[MetadataTypeAttribute(typeof(SalesOrderHeader.SalesOrderHeaderMetadata))]
public partial class SalesOrderHeader
{
    internal sealed class SalesOrderHeaderMetadata
    {
        private SalesOrderHeaderMetadata()
        {
        }

        [Include]
        [Composition]
        public EntitySet<SalesOrderDetail> SalesOrderDetails;

    }
}

When you apply the CompositionAttribute attribute to a property, the data from the descendant entity is not automatically retrieved with the parent entity. To include the descendent entity in query results, you must apply the IncludeAttribute attribute to the property that represents the descendant entity and include the descendant entity in the query method. The example at the end of the next section shows how to include the descendant entity in the query method.

Domain Service Operations with Compositional Hierarchy

When you define a compositional hierarchy, you must change the way you interact with the parent and descendant entities. The logic you include in domain services must account for the link between the entities. Typically, you define the logic for the hierarchy through domain service methods for the parent entity. In the domain service operations for the parent entity, you process modifications to the parent entity and any modifications to the descendant entities.

The following rules apply to domain service operations for entities with compositional relationships:

  • Query methods for parent or descendant entities are permitted; however, it is recommended that you retrieve descendant entities in the context of the parent entity. An exception is thrown if you modify a descendant entity that has been loaded without the parent entity.

  • Data modification operations can be added to descendant entities, but the permitted operations on a descendant entity are influenced by the permitted operations on the parent entity.

    • If update is permitted on the parent entity, then update, insert, and delete are permitted on the descendant entity.

    • If a parent entity has a named update method, then all descendants must have update enabled.

    • If insert or delete is permitted on the parent entity, then the corresponding operation is permitted recursively on the descendants.

Within the client project, the following rules apply to using entities that have compositional relationships:

  • When a descendant entity contains a change, notification of the change is propagated up to the parent entity. The HasChanges property on the parent entity is set to true.

  • When a parent entity is modified, all of its descendant entities (even those descendants that have not changed) are included in the change set.

  • A public EntitySet in the domain context is not generated on the client for descendant entities. You must access the descendant entity through the parent entity.

  • A descendant entity can be defined with more than one ancestor at the same level, but you must ensure that it is loaded within the context of only a single ancestor.

Data modification operations are executed according to the following rules:

  • An update, insert, or delete operation is executed first on the parent entity before recursively executing any data modification operations on the descendent entities.

  • If the required data operation is not present in a descendant entity, the recursive execution is stopped.

  • When updating a parent entity, the order of execution for data operations on descendants is not specified.

The following example shows the methods for querying, updating, and deleting the SalesOrderHeader entity. The methods include logic to process changes in the descendant entities.

<EnableClientAccess()>  _
Public Class OrderDomainService
    Inherits LinqToEntitiesDomainService(Of AdventureWorksLT_DataEntities)

    Public Function GetSalesOrders() As IQueryable(Of SalesOrderHeader)
        Return Me.ObjectContext.SalesOrderHeaders.Include("SalesOrderDetails")
    End Function

    Public Sub UpdateSalesOrder(ByVal currentSalesOrderHeader As SalesOrderHeader)
        Dim originalOrder As SalesOrderHeader = Me.ChangeSet.GetOriginal(currentSalesOrderHeader)

        If (currentSalesOrderHeader.EntityState = EntityState.Detached) Then
            If (IsNothing(originalOrder)) Then
                Me.ObjectContext.Attach(currentSalesOrderHeader)
            Else
                Me.ObjectContext.AttachAsModified(currentSalesOrderHeader, Me.ChangeSet.GetOriginal(currentSalesOrderHeader))
            End If
        End If

        For Each detail As SalesOrderDetail In Me.ChangeSet.GetAssociatedChanges(currentSalesOrderHeader, Function(o) o.SalesOrderDetails)
            Dim op As ChangeOperation = Me.ChangeSet.GetChangeOperation(detail)

            Select Case op
                Case ChangeOperation.Insert
                    If ((detail.EntityState = EntityState.Added) _
                    = False) Then
                        If ((detail.EntityState = EntityState.Detached) _
                                    = False) Then
                            Me.ObjectContext.ObjectStateManager.ChangeObjectState(detail, EntityState.Added)
                        Else
                            Me.ObjectContext.AddToSalesOrderDetails(detail)
                        End If
                    End If
                Case ChangeOperation.Update
                    Me.ObjectContext.AttachAsModified(detail, Me.ChangeSet.GetOriginal(detail))
                Case ChangeOperation.Delete
                    If (detail.EntityState = EntityState.Detached) Then
                        Me.ObjectContext.Attach(detail)
                    End If
                    Me.ObjectContext.DeleteObject(detail)
            End Select
        Next
    End Sub

    Public Sub DeleteSalesOrder(ByVal salesOrderHeader As SalesOrderHeader)
        If (salesOrderHeader.EntityState = EntityState.Detached) Then
            Me.ObjectContext.Attach(salesOrderHeader)
        End If

        Select Case salesOrderHeader.Status
            Case 1 ' in process
                Me.ObjectContext.DeleteObject(salesOrderHeader)
            Case 2, 3, 4 ' approved, backordered, rejected
                salesOrderHeader.Status = 6
            Case 5 ' shipped
                Throw New ValidationException("The order has been shipped and cannot be deleted.")
        End Select

    End Sub
End Class
[EnableClientAccess()]
public class OrderDomainService : LinqToEntitiesDomainService<AdventureWorksLT_DataEntities>
{
    public IQueryable<SalesOrderHeader> GetSalesOrders()
    {
        return this.ObjectContext.SalesOrderHeaders.Include("SalesOrderDetails");
    }

    public void UpdateSalesOrder(SalesOrderHeader currentSalesOrderHeader)
    {
        SalesOrderHeader originalOrder = this.ChangeSet.GetOriginal(currentSalesOrderHeader);

        if ((currentSalesOrderHeader.EntityState == EntityState.Detached))
        {
            if (originalOrder != null)
            {
                this.ObjectContext.AttachAsModified(currentSalesOrderHeader, this.ChangeSet.GetOriginal(currentSalesOrderHeader));
            }
            else
            {
                this.ObjectContext.Attach(currentSalesOrderHeader);
            }
        }

        foreach (SalesOrderDetail detail in this.ChangeSet.GetAssociatedChanges(currentSalesOrderHeader, o => o.SalesOrderDetails))
        {
            ChangeOperation op = this.ChangeSet.GetChangeOperation(detail);
            switch (op)
            {
                case ChangeOperation.Insert:
                    if ((detail.EntityState != EntityState.Added))
                    {
                        if ((detail.EntityState != EntityState.Detached))
                        {
                            this.ObjectContext.ObjectStateManager.ChangeObjectState(detail, EntityState.Added);
                        }
                        else
                        {
                            this.ObjectContext.AddToSalesOrderDetails(detail);
                        }
                    }
                    break;
                case ChangeOperation.Update:
                    this.ObjectContext.AttachAsModified(detail, this.ChangeSet.GetOriginal(detail));
                    break;
                case ChangeOperation.Delete:
                    if (detail.EntityState == EntityState.Detached)
                    {
                        this.ObjectContext.Attach(detail);
                    }
                    this.ObjectContext.DeleteObject(detail);
                    break;
                case ChangeOperation.None:
                    break;
                default:
                    break;
            }
        }
    }

    public void DeleteSalesOrder(SalesOrderHeader salesOrderHeader)
    {
        if ((salesOrderHeader.EntityState == EntityState.Detached))
        {
            this.ObjectContext.Attach(salesOrderHeader);
        }

        switch (salesOrderHeader.Status)
        {
            case 1: // in process
                this.ObjectContext.DeleteObject(salesOrderHeader);
                break;
            case 2: // approved
            case 3: // backordered
            case 4: // rejected
                salesOrderHeader.Status = 6;
                break;
            case 5: // shipped
                throw new ValidationException("The order has been shipped and cannot be deleted.");
            default:
                break;
        }

    }
}

Last updated