We started writing an XAF application back in 2019 as a WinForms application to manage work orders for in-house designed and fabricated manufacturing equipment. As the size and complexity of the system grew, the number of active users also grew to the point where supporting WinForms was more difficult than a web-based solution. When DevExpress released Blazor XAF capabilities, the decision was made to move that direction.
As more and more users connected to the system, we started to see a significant increase in memory consumption that was not being released. This was accompanied by low performance of the system as a whole, especially at login. At one point, the system was using 72GB+ of memory on a server before crashing. This occurred daily, sometimes multiple times a day.
The following details describe some of the significant findings that were addressed and resulted in a final outcome of approximately 75-150MB of memory per user on average.
We started writing an XAF application back in 2019 as a WinForms application to manage work orders for in-house designed and fabricated manufacturing equipment. As the size and complexity of the system grew, the number of active users also grew to the point where supporting WinForms was more difficult than a web-based solution. When DevExpress released Blazor XAF capabilities, the decision was made to move that direction.
As more and more users connected to the system, we started to see a significant increase in memory consumption that was not being released. This was accompanied by low performance of the system as a whole, especially at login. At one point, the system was using 72GB+ of memory on a server before crashing. This occurred daily, sometimes multiple times a day.
The following details describe some of the significant findings that were addressed and resulted in a final outcome of approximately 75-150MB of memory per user on average.
Problem: Disconnected Blazor circuits were being retained too long, holding onto memory and event subscriptions.
Solution:
CircuitHandlerProxy with proper cleanup of circuit-specific event subscriptionsKey Code (Startup.cs):
services.AddServerSideBlazor(o => {
o.DisconnectedCircuitMaxRetained = 10;
o.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(1);
});
Problem: Disconnected Blazor circuits were being retained too long, holding onto memory and event subscriptions.
Solution:
CircuitHandlerProxy with proper cleanup of circuit-specific event subscriptionsKey Code (Startup.cs):
services.AddServerSideBlazor(o => {
o.DisconnectedCircuitMaxRetained = 10;
o.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(1);
});
Do this step AFTER you try reducing memory with views in Client mode. Enabling this too early will make issues more difficult to find.
Server mode has limitations and complexities. If you are currently using client-side filtered collections in a property, these do not work and require a custom solution. Contact Us for more details.
Problem: List views were loading entire collections into memory, causing excessive memory usage on large datasets.
Solution:
*_ListView)DataAccessMode = Server for all persistent relevant ListView types (Some restrictions apply)Do this step AFTER you try reducing memory with views in Client mode. Enabling this too early will make issues more difficult to find.
Server mode has limitations and complexities. If you are currently using client-side filtered collections in a property, these do not work and require a custom solution. Contact Us for more details.
Problem: List views were loading entire collections into memory, causing excessive memory usage on large datasets.
Solution:
*_ListView)DataAccessMode = Server for all persistent relevant ListView types (Some restrictions apply)Problem: Properties returning XPCollection objects were creating new collections on every access, causing repeated database queries and object allocations.
This is only an issue on collections where we manually create XPCollections. The default GetCollection<T>() works as expected and does not need optimization.
Solution:
private List<WorkCenter> _availableWorkCenters;
[Browsable(false)]
public List<WorkCenter> AvailableWorkCenters
{
get
{
if (_availableWorkCenters == null)
{
_availableWorkCenters = new XPCollection<WorkCenter>(Session,
CriteriaOperator.Parse("...Your Criteria...")).ToList();
}
return _availableWorkCenters;
}
}
private void OperationCenterChanged()
{
_availableWorkCenters = null; // Invalidate cache when OperationCenter changes
}
Problem: Properties returning XPCollection objects were creating new collections on every access, causing repeated database queries and object allocations.
This is only an issue on collections where we manually create XPCollections. The default GetCollection<T>() works as expected and does not need optimization.
Solution:
private List<WorkCenter> _availableWorkCenters;
[Browsable(false)]
public List<WorkCenter> AvailableWorkCenters
{
get
{
if (_availableWorkCenters == null)
{
_availableWorkCenters = new XPCollection<WorkCenter>(Session,
CriteriaOperator.Parse("...Your Criteria...")).ToList();
}
return _availableWorkCenters;
}
}
private void OperationCenterChanged()
{
_availableWorkCenters = null; // Invalidate cache when OperationCenter changes
}
Problem: Singleton services that hold IObjectSpaceProvider, IObjectSpace, and other scoped references that are never released
Solution:
services.AddSingleton<MyService>();
public class MyService
{
/* Because the IObjectSpaceProvider is scoped, it should not be held in a singleton service
private IObjectSpaceProvider objectSpaceProvider;
public void Initialize(IObjectSpaceProvider provider) {...}
public void TrackDetails()
{
using (IObjectSpace space = objectSpaceProvider.....)
{
}
}
*/
//Pass the IServiceProvider in when needed so you can access the IObjectSpaceProvider and other services
public void TrackDetails(IServiceProvider serviceProvider)
{
//Do what you need here with access to other services through the IServiceProvider
}
}
Problem: Singleton services that hold IObjectSpaceProvider, IObjectSpace, and other scoped references that are never released
Solution:
services.AddSingleton<MyService>();
public class MyService
{
/* Because the IObjectSpaceProvider is scoped, it should not be held in a singleton service
private IObjectSpaceProvider objectSpaceProvider;
public void Initialize(IObjectSpaceProvider provider) {...}
public void TrackDetails()
{
using (IObjectSpace space = objectSpaceProvider.....)
{
}
}
*/
//Pass the IServiceProvider in when needed so you can access the IObjectSpaceProvider and other services
public void TrackDetails(IServiceProvider serviceProvider)
{
//Do what you need here with access to other services through the IServiceProvider
}
}
Problem: Permission checks were hitting the database repeatedly during user sessions
This solution isn't recommended for everyone. If you expect frequent changes to security rules that need to apply immediately without the user logging back in or refreshing the browser, don't use caching.
Solution:
PermissionsReloadMode.CacheOnFirstAccess in security strategyUseXpoPermissionsCaching()Key Code (Startup.cs):
builder.Security
.UseIntegratedMode(options =>
{
options.UseXpoPermissionsCaching();
options.Events.OnSecurityStrategyCreated = securityStrategy => {
((SecurityStrategy)securityStrategy).PermissionsReloadMode =
PermissionsReloadMode.CacheOnFirstAccess;
};
})
Problem: Permission checks were hitting the database repeatedly during user sessions
This solution isn't recommended for everyone. If you expect frequent changes to security rules that need to apply immediately without the user logging back in or refreshing the browser, don't use caching.
Solution:
PermissionsReloadMode.CacheOnFirstAccess in security strategyUseXpoPermissionsCaching()Key Code (Startup.cs):
builder.Security
.UseIntegratedMode(options =>
{
options.UseXpoPermissionsCaching();
options.Events.OnSecurityStrategyCreated = securityStrategy => {
((SecurityStrategy)securityStrategy).PermissionsReloadMode =
PermissionsReloadMode.CacheOnFirstAccess;
};
})
Problem:
Solution:
//[Appearance("PartInfo-MissingThumbnail", "ISNULL([PartThumbnail])", BackColor = "Red", TargetItems = "*")]
[Appearance("PartInfo-MissingThumbnail", "[HasThumbnail] = False", BackColor = "Red", TargetItems = "*")]
public class PartInfo(Session session) : CustomBaseObject(session)
{
[Delayed(true)]
[ImageEditor(ListViewImageEditorCustomHeight = 32)]
public byte[] PartThumbnail
{
get { return GetDelayedPropertyValue<byte[]>(nameof(PartThumbnail)); }
set { SetDelayedPropertyValue<byte[]>(nameof(PartThumbnail), value); }
}
private bool _HasThumbnail;
public bool HasThumbnail
{
get { return _HasThumbnail; }
set { SetPropertyValue<bool>(nameof(HasThumbnail), ref _HasThumbnail, value); }
}
protected override void OnSaving()
{
base.OnSaving();
HasThumbnail = PartThumbnail != null;
}
}
Problem:
Solution:
//[Appearance("PartInfo-MissingThumbnail", "ISNULL([PartThumbnail])", BackColor = "Red", TargetItems = "*")]
[Appearance("PartInfo-MissingThumbnail", "[HasThumbnail] = False", BackColor = "Red", TargetItems = "*")]
public class PartInfo(Session session) : CustomBaseObject(session)
{
[Delayed(true)]
[ImageEditor(ListViewImageEditorCustomHeight = 32)]
public byte[] PartThumbnail
{
get { return GetDelayedPropertyValue<byte[]>(nameof(PartThumbnail)); }
set { SetDelayedPropertyValue<byte[]>(nameof(PartThumbnail), value); }
}
private bool _HasThumbnail;
public bool HasThumbnail
{
get { return _HasThumbnail; }
set { SetPropertyValue<bool>(nameof(HasThumbnail), ref _HasThumbnail, value); }
}
protected override void OnSaving()
{
base.OnSaving();
HasThumbnail = PartThumbnail != null;
}
}
Problem: Business objects contained properties with complex getters that included client-side loading of data
Solution:
We implemented our own calculation service that forces business objects to recalculate persisted values after the Session / IObjectSpace is committed to the database. This requires careful consideration but can be an excellent way to ensure calculations are accurate and consistent.
Simplified Example
//public int ChildCount => this.Children.Where(x=>x.IsActive).Count();
[PersistentAlias("[Children][[IsActive] = True].Count()")]
public int ChildCount => Convert.ToInt32(EvaluateAlias(nameof(ChildCount)));
Problem: Business objects contained properties with complex getters that included client-side loading of data
Solution:
We implemented our own calculation service that forces business objects to recalculate persisted values after the Session / IObjectSpace is committed to the database. This requires careful consideration but can be an excellent way to ensure calculations are accurate and consistent.
Simplified Example
//public int ChildCount => this.Children.Where(x=>x.IsActive).Count();
[PersistentAlias("[Children][[IsActive] = True].Count()")]
public int ChildCount => Convert.ToInt32(EvaluateAlias(nameof(ChildCount)));
Problem: Dashboard data sources were being cached, holding references to large datasets
Caching dashboard data in our case was unnecessary because they are only accessed once before the user navigates away. If your application relies on dashboards being accessed more frequently, the cache may offer better performance and changing cache-specific settings may be better.
Solution:
DataSourceCacheEnabled = false)Key Code (Startup.cs):
.AddDashboards(options =>
{
options.SetupDashboardConfigurator = (c, s) =>
{
c.DataSourceCacheEnabled = false;
c.SetObjectDataSourceCustomFillService(new BlazorDashboardViewDataSourceFillService(s));
};
})
Problem: Dashboard data sources were being cached, holding references to large datasets
Caching dashboard data in our case was unnecessary because they are only accessed once before the user navigates away. If your application relies on dashboards being accessed more frequently, the cache may offer better performance and changing cache-specific settings may be better.
Solution:
DataSourceCacheEnabled = false)Key Code (Startup.cs):
.AddDashboards(options =>
{
options.SetupDashboardConfigurator = (c, s) =>
{
c.DataSourceCacheEnabled = false;
c.SetObjectDataSourceCustomFillService(new BlazorDashboardViewDataSourceFillService(s));
};
})
Problem: Logging in seemed to hold up the entire application, especially when other users were trying to log in
Solution:
Problem: Logging in seemed to hold up the entire application, especially when other users were trying to log in
Solution: