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. Events on paged collections are limited. 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
{
[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
{
// 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; }
}