Relationships
Entities relate to each other through reference properties (foreign keys) and collections (inverse navigation). The relationship system supports cascading deletes, one-to-one and one-to-many patterns, and virtual references.
[ReferenceProperty]
Defines a foreign key relationship to another entity.
[ReferenceProperty("Label", DeleteAction)]
public virtual partial TargetEntity? PropertyName { get; set; }
Reference properties must be virtual partial and are typically nullable.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
label |
string |
Yes | Display label for the relationship |
deleteAction |
DeleteAction |
Yes | What happens when the referenced entity is deleted |
referenceType |
ReferenceType |
No | Relationship cardinality (default: OneToMany) |
Description |
string? |
No | User-facing description |
DeleteAction
Controls what happens to this entity when the entity it references is deleted:
| Value | Behavior |
|---|---|
DeleteAction.Restrict |
Block deletion — cannot delete the referenced entity while this reference exists |
DeleteAction.Cascade |
Delete together — delete this entity when the referenced entity is deleted |
DeleteAction.SetNull |
Clear reference — set this property to null when the referenced entity is deleted |
// Cannot delete a customer while sales orders reference it
[Required]
[ReferenceProperty("Customer", DeleteAction.Restrict)]
public virtual partial Customer? SellToCustomer { get; set; }
// Delete details when the parent order is deleted
[Required]
[ReferenceProperty("Sales order", DeleteAction.Cascade)]
public virtual partial SalesOrder? SalesOrder { get; set; }
// Clear the billing customer reference if that customer is deleted
[ReferenceProperty("Billing customer", DeleteAction.SetNull,
Description = "This is the account that is being billed.")]
public virtual partial Customer? BillingCustomer { get; set; }
ReferenceType
| Value | Description |
|---|---|
ReferenceType.OneToMany |
Default. Many entities can reference the same target |
ReferenceType.OneToOne |
Only one entity can reference this target |
ReferenceType.OwnedType |
Complex owned type embedded in the parent |
// One-to-one: only one product can have this image
[ReferenceProperty("Image", DeleteAction.Restrict, ReferenceType.OneToOne,
Description = "An image of the product")]
public virtual partial Blob? Image { get; set; }
[OppositeSideCollection]
Placed on the same reference property, this generates a collection navigation on the target entity — the inverse side of the relationship.
[OppositeSideCollection("PropertyName", "Label", CollectionLoadMode)]
Parameters
| Parameter | Type | Description |
|---|---|---|
propertyName |
string |
Name of the collection property generated on the target entity |
propertyLabel |
string |
Display label for the collection |
loadMode |
CollectionLoadMode |
How the collection is loaded |
Description |
string? |
User-facing description |
TechnicalDescription |
string? |
Developer notes |
Collection loading mode
This is important for performance! If the collection will be large, use paged. There are limitations with events when Paged is selected. See Compute event and Collection changed event.
| Value | When to Use |
|---|---|
LoadAll |
Small collections always loaded with the parent (e.g., order details, product UOMs) |
Paged |
Large collections loaded on demand with paging (e.g., customer's orders) |
Example: Parent-Child with LoadAll
// On SalesOrderDetail:
[Required]
[ReferenceProperty("Sales order", DeleteAction.Cascade)]
[OppositeSideCollection("Details", "Details", CollectionLoadMode.LoadAll,
Description = "Sales order details",
TechnicalDescription = "Details of an invoice can be both materials or non-materials")]
public virtual partial SalesOrder? SalesOrder { get; set; }
This generates a Details collection property on SalesOrder that loads all detail lines when the order is loaded.
Example: Paged Collection
// On SalesOrder (via ISalesDoc interface):
[Required]
[ReferenceProperty("Customer", DeleteAction.Restrict)]
[OppositeSideCollection("SalesOrders", "Sales orders", CollectionLoadMode.Paged)]
public virtual partial Customer? SellToCustomer { get; set; }
This generates a paged SalesOrders collection on Customer — fetched separately with OData $skip/$top.
Placeholders
When multiple entity types implement the same interface, use placeholders to generate unique collection names:
| Placeholder | Replaced With |
|---|---|
[EntityName] |
The implementing entity's class name |
[EntityLabel] |
The implementing entity's display label |
// In ISalesDoc interface — works for SalesOrder, Invoice, etc.
[ReferenceProperty("Customer", DeleteAction.Restrict)]
[OppositeSideCollection("[EntityName]s", "[EntityLabel]s", CollectionLoadMode.Paged)]
public virtual partial Customer? SellToCustomer { get; set; }
When SalesOrder implements ISalesDoc, this generates a SalesOrders collection on Customer. See Interfaces for more.
Self-Referencing Relationships
An entity can reference itself:
[ApiEntity]
public partial class SalesOrderDetail : EntityBase
{
[ReferenceProperty("Accessory parent", DeleteAction.Cascade)]
[OppositeSideCollection("Accessories", "Accessories", CollectionLoadMode.LoadAll)]
public virtual partial SalesOrderDetail? AccessoryParent { get; set; }
}
[VirtualReferenceProperty]
A computed reference that is not persisted to the database. The referenced entity is resolved at runtime by business logic.
[VirtualReferenceProperty("Default selling unit")]
public partial ProductUom? DefaultSellingUnit { get; set; }
[VirtualReferenceProperty("Main unit")]
public partial ProductUom? MainUnit { get; set; }
Virtual references:
- Have no foreign key column in the database
- Are resolved by compute events in business logic
- Can reference entities from any collection on the parent
[OppositeSideProperty]
Like [OppositeSideCollection] but generates a single navigation property instead of a collection (for one-to-one inverse navigation). Used less commonly.
Complete Example
namespace Benevia.ERP.Model.Products;
[ApiEntity]
public partial class ProductUom : EntityBase
{
// Parent reference — deleting the product deletes all its UOMs
[Required]
[ReferenceProperty("Product", DeleteAction.Cascade)]
[OppositeSideCollection("Uoms", "Uoms", CollectionLoadMode.LoadAll,
Description = "Units of measure for the product")]
public virtual partial Product? Product { get; set; }
[MaxLength(10)]
[Property<DataTypes.Text>("Name",
Description = "Name of the unit such as ea, lb, kg, or case")]
public partial string Name { get; set; }
[Property<DataTypes.Enum>("Operation")]
[DefaultValue(UomOperation.Multiply)]
public partial UomOperation Operation { get; set; }
[DefaultValue(1)]
[Property<DataTypes.PositiveDecimal>("Factor",
Description = "The factor to use in the operation")]
public partial decimal Factor { get; set; }
}