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:
Set XpoDefault.Session to
null
(Nothing
) in the entry point of your application: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 thePropertyChanged
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 theSave
method to force theUnitOfWork
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
andOnLoad
events (IsLoading
istrue
) - Between
OnSaving
andOnSaved
events (IsSaving
istrue
)
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:
- Check additional conditions. For example, make sure a value was not already assigned to a property. Check to ensure that the session is a NestedUnitOfWork or ObjectLayer is a SimpleObjectLayer. Refer to the following article for an example: How to generate a sequential and user-friendly identifier field within an XPO business class.
- Move your logic directly into dependent property setters. Refer to the following topic for an example: How to: Calculate a Property Value Based on Values from a Detail Collection.
- Move your logic from a business class to the
ViewController
(for example, handle the IObjectSpace.Committing or IObjectSpace.ObjectChanged event) or another suitable UI-related entity. Refer to the following topic for examples: Execute Business Logic When a Property is Changed. - Do not mix operations that save/persist objects and business logic (validation rules included). Examples of defensive validation described in the previous point are exceptions, rather than rules. We do not recommend that you use this code in your applications often. The best practice is to validate your objects before you save them. Do this right after you apply changes (for example, in property setters), or later, with the help of a specific validator class that is context-aware.
#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:
- XPQuery<T>
- XPView.TopReturnedRecords
- XPObjectSource
- XPBaseCollection.TopReturnedObjects
- How to: Use Pageable Collections
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
andOnLoaded
events (IsLoading
istrue
) - Between
OnSaving
andOnSaved
events (IsSaving
istrue
)
#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:
- Transfer Data via ASP.NET Core Web API / REST API
- Transfer Data via WCF Services
- Cached Data Store
- Backend Web API Service / REST API (for XAF and non-XAF applications)
- Middle Tier Security (XPO Only) (for XAF applications only)
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.