Skip to main content
Version: 0.46

Generic Entity References

A GenericEntityReference is a special property type that allows an entity to reference any other entity without requiring a fixed foreign key relationship. This is useful when a property needs to point to different types of entities depending on the context.

For example, a StoredFile might be owned by a Person, an Organisation, or an Order - all through the same Owner property.

How It Works

At the database level, a GenericEntityReference maps to two or three columns instead of a single foreign key:

ColumnTypeDescription
{Property}Idnvarchar(100)The GUID of the referenced entity
{Property}ClassNamenvarchar(1000)The fully qualified class name (or TypeShortAlias) of the referenced entity
{Property}DisplayNamenvarchar(1000)(Optional) The display name of the referenced entity, cached for quick display on the front-end

The third column (DisplayName) is only added when you opt in via the [EntityReference(true)] attribute. Storing the display name avoids additional API or database calls when rendering the reference in the UI.

When to Use

Use GenericEntityReference when:

  • A property needs to reference different entity types (e.g., an audit log entry that can relate to any entity)
  • You want a polymorphic association without creating a separate foreign key for each possible target type
  • You need to display the referenced entity's name in the UI without extra lookups

Use a standard entity reference (foreign key) when:

  • The property always references the same entity type
  • You need database-level referential integrity (foreign keys are not created for generic references)

Defining a Generic Entity Reference

Step 1: Add the property to your entity

using Shesha.Domain.Attributes;
using Shesha.EntityReferences;

public class AuditEntry : FullAuditedEntity<Guid>
{
public virtual string Action { get; set; }

public virtual string Description { get; set; }

/// <summary>
/// The entity this audit entry relates to.
/// The 'true' parameter stores the display name for quick UI rendering.
/// </summary>
[EntityReference(true)]
public virtual GenericEntityReference RelatedEntity { get; set; }
}

The [EntityReference] attribute accepts the following parameters:

ParameterTypeDescription
storeDisplayNameboolWhen true, adds a DisplayName column to cache the referenced entity's display text. Default is false.
note

The attribute also exposes IdColumnName, ClassNameColumnName, and DisplayNameColumnName properties, which let you override the generated column names for a property if you need them to differ from the framework's default naming convention.

Step 2: Create the database migration

Add the Id, ClassName, and (optionally) DisplayName columns manually, following the naming convention above:

using FluentMigrator;
using Shesha.FluentMigrator;

[Migration(20260227120000)]
public class M20260227120000 : Migration
{
public override void Up()
{
Create.Table("MyApp_AuditEntries")
.WithIdAsGuid()
.WithFullAuditColumns()
.WithColumn("Action").AsString(100).Nullable()
.WithColumn("Description").AsString(2000).Nullable();

// Adds the RelatedEntityId, RelatedEntityClassName, and RelatedEntityDisplayName columns
Alter.Table("MyApp_AuditEntries")
.AddColumn("RelatedEntityId").AsString(100).Nullable()
.AddColumn("RelatedEntityClassName").AsString(1000).Nullable()
.AddColumn("RelatedEntityDisplayName").AsString(1000).Nullable();
}

public override void Down()
{
throw new NotImplementedException();
}
}

If you don't need the display name column, omit the last AddColumn call:

// Adds only RelatedEntityId and RelatedEntityClassName columns
Alter.Table("MyApp_AuditEntries")
.AddColumn("RelatedEntityId").AsString(100).Nullable()
.AddColumn("RelatedEntityClassName").AsString(1000).Nullable();
AddGenericEntityReferenceColumns produces a mismatched column name

Shesha.FluentMigrator.SheshaFluentMigratorExtensions still exposes an AddGenericEntityReferenceColumns(propertyName, storyDisplayName) helper, but it names the class-name column {propertyName}TypeName, not {propertyName}ClassName. The entity mapping convention for GenericEntityReference properties (Shesha.NHibernate.Maps.Conventions) expects the column suffixed ClassName unless you override it with ClassNameColumnName on [EntityReference]. Using the extension method as-is will create a column NHibernate's default mapping won't find. The framework's own migrations (for example StoredFile.Owner) create the Id/ClassName/DisplayName columns manually, as shown above - do the same for your own properties rather than relying on this helper, unless you also set ClassNameColumnName on the attribute to match.

Working with Generic Entity References in Code

Creating a reference from an entity

When you have an entity instance, pass it to the GenericEntityReference constructor. The ID, class name, and display name are extracted automatically:

var person = await _personRepository.GetAsync(personId);
var auditEntry = new AuditEntry
{
Action = "Reviewed",
Description = "Record reviewed by administrator",
RelatedEntity = new GenericEntityReference(person)
};

Creating a reference manually

If you don't have the entity loaded, you can construct the reference from its ID and type name. The constructor's parameters are id, _className, and an optional _displayName:

var auditEntry = new AuditEntry
{
RelatedEntity = new GenericEntityReference(
id: "550e8400-e29b-41d4-a716-446655440000",
_className: "Shesha.Core.Person",
_displayName: "John Smith"
)
};

Reading reference properties

var entry = await _auditEntryRepository.GetAsync(entryId);

string entityId = entry.RelatedEntity?.Id; // "550e8400-..."
string entityType = entry.RelatedEntity?._className; // "Shesha.Core.Person"
string displayText = entry.RelatedEntity?._displayName; // "John Smith"

API and Front-End Integration

API response format

When returned from an API, a GenericEntityReference is serialized as a JSON object:

{
"id": "550e8400-e29b-41d4-a716-446655440000",
"_className": "Shesha.Core.Person",
"_displayName": "John Smith"
}

Sending a reference in API requests

When creating or updating an entity through the API, send the reference as a JSON object with at least id and _className:

{
"action": "Reviewed",
"description": "Record reviewed by administrator",
"relatedEntity": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"_className": "Shesha.Core.Person",
"_displayName": "John Smith"
}
}

How the Display Name Is Resolved

When a GenericEntityReference is created from an entity instance, the framework automatically determines the display name by looking for (in order of priority):

  1. A property decorated with the [EntityDisplayName] attribute
  2. A property named Name, DisplayName, FullName, Address, or FullAddress (case-insensitive)

If the display name column is enabled ([EntityReference(true)]), this value is persisted in the database so the front-end can display it without additional lookups.

Framework Usage Examples

The Shesha framework itself uses GenericEntityReference in several core entities:

EntityPropertyPurpose
StoredFileOwnerLinks a stored file to any parent entity (Person, Organisation, etc.)
NotificationTriggeringEntityReferences the entity that triggered a notification

See Also