Skip to main content
All docs
V23.2

XPO Best Practices

  • 15 minutes to read

This topic lists best practices that are applicable to XPO on all development platforms.

Connect to a Data Store

Initialize a Data Layer

Initialize your application’s Data Access Layer at startup before any Session or UnitOfWork is created.

To initialize the default Data Access Layer, use the XpoDefault.DataLayer property. When this property is set, all Session and UnitOfWork objects created by their default constructors (without the layer parameter) share the same Data Access Layer.

If your application should use multiple Data Access Layers, leave the XpoDefault.DataLayer property unspecified and use Session and UnitOfWork constructors with the layer parameter. When the XpoDefault.DataLayer property is unspecified, do not use Session and UnitOfWork constructors without parameters, because XPO initializes a new database connection for each Session and UnitOfWork in this case.

For additional information on Data Access Layer initialization, refer to the following topic: Connect to a Data Store.

Use ASP.NET Core Extensions

In ASP.NET Core applications, use XPO extension methods for the IServiceCollection interface to initialize Data Access Layers: XPO Extensions for ASP.NET Core Dependency Injection.

Use dependency injection to get Session and UnitOfWork instances in controllers and components: Create an ASP.NET Core Web API CRUD Service.

To generate service endpoints automatically based on your data model, use Backend Web API Service.

Avoid the Use of the Default Session

XPO uses the default Session if you create objects or XPCollection instances without a Session parameter. This may result in a SessionMixingException causing you to write cumbersome code that reloads data. Do not use the default session to avoid this issue.

Refer to the following topic for additional information: How XPO reloads objects and collections.

A more critical situation can occur if the XpoDefault.DataLayer is not initialized and all Session objects are created with a custom Data Layer. In this case, the default Session targets a fake data store that has no data and is read-only. This is why when persistent objects are loaded or created with a default Session, the result may lead to errors that are hard to find.

To prevent such errors, follow the steps below:

  1. Set XpoDefault.Session to null (Nothing) in the entry point of your application:

    XpoDefault.Session = null; 
    
  2. Remove parameterless constructors from your persistent classes. Doing so will ensure that you cannot use such constructors at the development stage.

Use UnitOfWork to Save Objects

Use UnitOfWork rather than Session when you save data objects.

If you use a Session, and its transaction does not explicitly start, a persistent object is immediately saved in the data store upon the Save method call. Unlike Session, UnitOfWork does not persist changes until its UnitOfWork.CommitChanges() method is called. The UnitOfWork gives you more control over what and when to save. Also, do not work with the same persistent object instance in different threads. Create a separate session for each thread and work with different instances of the same persistent object. For more information, refer to the following topic: XPO transactions.

Create a Separate Application for Database Maintenance and Schema Updates

For security reasons, you may wish to deny access to system tables and disable modifications to the database schema for a database account used in your end-user application. Please use the AutoCreateOption.SchemaAlreadyExists option when you create a DataLayer in your XPO application. This will allow you to grant fewer privileges to a database user account used in your application. To create a database schema, create a separate application that calls the UpdateSchema and CreateObjectTypeRecords methods:

string conn = ...;
IDataLayer dl = XpoDefault.GetDataLayer(conn, DevExpress.Xpo.DB.AutoCreateOption.DatabaseAndSchema);
using(Session session = new Session(dl)) {
    System.Reflection.Assembly[] assemblies = new System.Reflection.Assembly[] {
        typeof(AnyPersistentObjectFromAssemblyA).Assembly,
        typeof(AnyPersistentObjectFromAssemblyB).Assembly
    };
    session.UpdateSchema(assemblies);
    session.CreateObjectTypeRecords(assemblies);
}

In XAF apps, use the DBUpdater tool for that purpose.

Create a Data Model

Business Object Constructor

Always define a constructor with a Session parameter in your persistent objects. This allows you to avoid a SessionCtorAbsentException.

public class OrderDetail : XPObject {
   public OrderDetail(Session session) : base(session) {
   }
   // ...
} 

PersistentBase descendants are always associated with a Session. If you do not pass a Session instance in the constructor, XPO is forced to use a default Session. This is bad practice, because objects created with the default Session can be mixed with objects created with another Session instance. XPO does not allow for this and throws an exception. Refer to the following section for more information: Avoid the Use of the Default Session.

Implement Business Object Property Setters

Use the SetPropertyValue method in persistent property setters.

// Do not define persistent members as fields:
public string Name; // Error


// Do not directly assign value to the value holder without the SetPropertyValue method:
private string name;
public string Name {
     get { return name; }
     set { name = value; }
}

// Do not declare persistent properties as auto implemented properties:
public string Name { get; set; }

XPO relies on proper INotifyPropertyChanged implementation in your persistent objects. If a property value changes, the object must invoke a change notification. Property declarations in the code sample above do not invoke those notifications. As a result, the following limitations apply to your data entities:

  • Most UI controls and components refresh display values once you modify a property value. Without the PropertyChanged event, they display outdated values.
  • The UnitOfWork class cannot function properly. The UnitOfWork class tracks the PropertyChanged event and saves only modified objects to a database when you call the UnitOfWork.CommitChanges() method. Without the property change notification, the UnitOfWork.CommitChanges() method has no effect. The modifications applied to persistent objects are lost, unless you explicitly call the Save method to force the UnitOfWork class to save the object to the database when the UnitOfWork.CommitChanges() method is executed.

Use the following code snippet to load a persistent property:

string fProductName;
public string ProductName {
   get { return fProductName; }
   set { SetPropertyValue("ProductName", ref fProductName, value); }
}

Refer to the following topics for more information: Create a Persistent Object and The Importance of Property Change Notifications for Automatic UI Updates.

Properties Calculated on the Database Server Side

Your business class may contain calculated properties. A calculated property value should be the same on the client and in the database. To turn your property into a calculated property, apply the PersistentAliasAttribute attribute to it. The expression for evaluation can be passed as the constructor’s parameter or specified in the PersistentAliasAttribute. Call the EvaluateAlias(String) to evaluate the expression specified in the attribute.

[PersistentAlias("Quantity*Price")]
public int Total {
  get { return (int)EvaluateAlias("Total"); }
}

If you do not use the EvaluateAlias(String) method, the evaluation result may be incorrect:

[PersistentAlias("Quantity*Price")]
public int Total {
    get { return Quantity*Price; } // Possible evaluation error
} 

See also: PersistentBase.OnChanged.

Base Classes for Business Objects

Do not use an XPBaseObject if you can replace it with an alternative. Use XPCustomObject or XPObject as base classes for WinForms and ASP.NET applications. For WPF and Blazor applications, use PersistentBase as a base class for your business objects. For more information, refer to the following topic: XPO Classes Comparison.

Delayed Loading

When persistent properties are mapped to columns that contain a large quantity of data (images, large text documents, or binary data) and do not need to be loaded with the main persistent object in the UI, use delayed loading to reduce memory consumption and improve form loading performance. However, do not implement all persistent class properties as delayed because it may lead to performance overhead when used excessively (for instance, when a property is initially visible in the UI).

Create-Read-Update-Delete (CRUD)

Custom Logic in Constructors

Do not execute time-consuming operations in a persistent object’s constructor. To add custom logic or initialize business object properties, override the PersistentBase.AfterConstruction() method of your class.

Custom Logic in Property Setters

You can implement custom logic that is executed after a property value changes in the property setter or override the PersistentBase.OnChanged() method.

If you must add custom logic to a setter, do not change or access other persistent properties (directly or indirectly) during the following time intervals:

  • Between OnLoading and OnLoad events (IsLoading is true)
  • Between OnSaving and OnSaved events (IsSaving is true)
private string name;
public string Name {
    get { return name; }
    set {
        bool changed = SetPropertyValue("Name", ref name, value);
        if (!IsLoading && !IsSaving && changed) {
            // ... YourBusinessRule...
        }
    }
} 

Alternatively, use the following syntax:

[Persistent("Name")]
private string PersistentName {
   get { return name; }
   set { SetPropertyValue("PersistentName", ref name, value); }
}
[PersistentAlias("PersistentName")]
public virtual string Name {
   get { return PersistentName; }
   set {
       DoMyBusinessTricksBeforePropertyAssignement();
       PersistentName = value;
       DoMyBusinessTricksAfterPropertyAssignement();
   }
}

Custom Logic in OnSaving and OnDeleting Methods

Use the OnSaving() and OnDeleting() methods of your persistent objects to generate custom keys or to implement custom last-resort data validation (if other validation methods do not suit your scenario). If your application heavily relies on logic implemented in these methods, we recommend that you review and modify your business logic and/or data model.

OnSaving() or OnDeleting() can be called multiple times during a single save or delete operation. Take this behavior into account if you use these methods to execute your business logic. You may consider the following adjustments or alternatives:

Limit the Number of Loaded Objects

If your algorithm requires multiple persistent objects, load them all at once before you use them. Do not load more objects than you need. For more information on data loading, refer to the following topics:

Do not create a class structure that loads a large number of records when it accesses a single object/property. For instance, if you implement an EmployeePosition class, its code should not create a collection of people holding a specific position. Use Server and Instant Feedback data sources in grids and lookups with a large amount of records. If you need to build criteria, use Free Joins.

Sort Records Explicitly if Their Order Is Important

Do not make your code dependent on the order of records returned by the XPCollection, XPView, and XPCursor objects, unless you explicitly sorted them. If records are not sorted, XPO loads them in an arbitrary order. This mimics the behavior of the SQL SELECT statement in the same circumstances.

Create Strongly-Typed Criteria Expressions

You can specify the same criteria in multiple ways. For example, if you want to filter only objects that have a value equal to or greater than 20 in their UnitPrice field, you can use the following criteria:

// Variant 1
CriteriaOperator criteria = CriteriaOperator.FromLambda<YourClass>(p => p.UnitPrice > 20);

// Variant 2
CriteriaOperator criteria = new BinaryOperator(nameof(YourClass.UnitPrice), 20, BinaryOperatorType.GreaterOrEqual);

// Variant 3
CriteriaOperator criteria = new OperandProperty(nameof(YourClass.UnitPrice)) >= new OperandValue(20);

// Variant 4
CriteriaOperator criteria = CriteriaOperator.Parse($"{nameof(YourClass.UnitPrice)} >= ?", 20);

// Variant 5
CriteriaOperator criteria = CriteriaOperator.Parse("UnitPrice >= 20");

// Variant 6
CriteriaOperator criteria = CriteriaOperator.Parse(string.Format("UnitPrice >= {0}", 20));

We recommend that you use variants 1, 2, and 3, when you can, because they are strongly-typed and lead to fewer errors. Variant 4 is also a safe option and uses positional parameters (the question mark character identifies these parameters).

Variants 5-6 are not error-proof and should be used only by experienced developers who know the criteria language syntax.

Refer to the following topics for more information:

Exceptions in Property Implementation Code

Do not throw exceptions from persistent property code.

If you don’t see a way around an exception, do not throw it during the following time intervals:

  • Between OnLoading and OnLoaded events (IsLoading is true)
  • Between OnSaving and OnSaved events (IsSaving is true)

Control CRUD Operations

Use a new UnitOfWork instance rather than Session to fully control data load, modification, and storage operations.

A Session caches objects upon the Save method call if the Session transaction does not explicitly start.

When you create a new UnitOfWork instance, it does not persist changes until you call the UnitOfWork.CommitChanges() method. This is why we recommend that you use a separate UnitOfWork instance in all visual modules (Forms and UserControls) of your application. Refer to the following article for examples:

Save Persistent Objects

Separate your business logic (including data validation) from the code that saves/persists objects.

Validate data before you save objects. Execute validation right after you apply changes (for example, in property setters). You can also use context-aware validator classes.

Delete Persistent Objects

Do not set the XPCollection.DeleteObjectOnRemove property to true for the collection property values used in associations.

When XPCollection is bound to a grid, a row deletion removes a persistent object from the collection but does not delete it from the database. To enable this deletion, set the DeleteObjectOnRemove property to true.

A common mistake is to create a base class for all persistent classes in an application and override the CreateCollection method of this base class to set the DeleteObjectOnRemove property to true.

protected override XPCollection<T> CreateCollection<T>(DevExpress.Xpo.Metadata.XPMemberInfo property) {
XPCollection<T> result = base.CreateCollection<T>(property);
result.DeleteObjectOnRemove = true;
return result;
}

This approach is not recommended because XPO internally removes objects from an associated collection when you modify a reference property from the opposite side of the association. Therefore, an instance previously referenced by the owner of the collection is deleted from the database, even though this is not supposed to happen. The following code illustrates the problem:

Customer customerA = new Customer(session);
Customer customerB = new Customer(session);
Order order = new Order(session) { Customer = customerA };
order.Customer = customerB;

The expected behavior includes adding the order to the Orders collection of customerB. Since the order is removed from the Orders collection of customerA, however, it is deleted from the database (because the DeleteObjectOnRemove property of the collection is set to true). We recommend that you handle the deletion operation at the UI level and use the Session.Delete method to delete an object. Refer to the following article for an example: Why are objects not deleted when I delete them in XtraGrid?.

Do not Share Persistent Object Instances When Building Remote or Distributed Applications

To access data remotely (for instance, to build mobile, desktop, or other distributed apps connected to a secure data service built with XPO), use IDataStore and IObjectLayer interfaces or build custom web services based on HTTP, OData, and other protocols.

For more information on IDataStore, refer to the following articles:

If you do not want your customers to change anything in remote IDataStore, create an IDataStore wrapper that throws InvalidOperationException on each ModifyData(ModificationStatement[]) call.