What Changed
- XafApplication and ValueManager API are unavailable in standalone Web API Service (not hosted inside your XAF Blazor app) and other non-XAF UI apps. That is because XafApplication is in charge for initializing the ValueManager context, and you can no longer rely on a heavy XafApplication instance and its events/methods in these scenarios. ValueManager API may be also unavailable in certain scenarios below. As a result, you may receive InvalidOperationException ("ValueManagerContext.Storage is null" or "IValueManagerStorageContext not active") at runtime in v22.1.3+ when using the following APIs in XAF Blazor UI or Web API Service / Non-XAF UI Apps :
- IFileService, IFileUrlService and other File Attachment Module-related services from the internal IDevExpress.ExpressApp.Blazor.Services namespace;
- SecuritySystem and its static members (Instance, CurrentUserId, CurrentUserName, etc.);
- CaptionHelper, EnumDescriptor and their static members (Instance, GetClassCaption, GetMemberCaption, GetLocalizedText, etc);
- Validator and its static members (RuleSet).
- AuditTrailService and its static members (Instance, SaveAuditTrailData, etc);
- IModelNode, XafApplication.Model and other Application Model-related API;
- ReportDataProvider, ReportsStorage, ReportObjectSpaceProvider and their static members (for instance, ReportsModuleV2.ReportsDataSourceHelper).
- Other built-in XAF and custom-tailored static helpers or singletons based on ValueManager.
- You will receive compilation warnings/errors at design time in v22.2+ when using the following APIs in .NET 6 apps:
-
The
ReportDataProvider.ReportsStorage
static property is obsolete. Use theIReportStorage
service or theReportDataProvider.GetReportStorage(IServiceProvider serviceProvider)
method instead. -
The
ReportObjectSpaceProvider
class is obsolete. Use theIObjectSpaceFactory
service or theReportDataProvider.GetReportObjectSpaceProvider(IServiceProvider serviceProvider)
method instead. -
The
DevExpress.Persistent.Validation.Validator.RuleSet
static property is obsolete. Use theIValidator
service or theValidator.GetService(IServiceProvider serviceProvider)
method instead. -
The
DevExpress.Persistent.AuditTrail.AuditTrailService.Instance
static property is obsolete. Use theIAuditTrailService
service or theAuditTrailService.GetService(IServiceProvider serviceProvider)
method instead. -
The
DevExpress.ExpressApp.Utils.CaptionHelper.Instance
static property is obsolete. Use theICaptionHelperProvider
service or theCaptionHelper.GetService(IServiceProvider serviceProvider)
method instead.You will receive a runtime error if you cast an object returned by the
ReportsModuleV2.ReportsDataSourceHelper
property to theReportDataSourceHelper
type. Use members of the returnedDevExpress.ExpressApp.ReportsV2.IReportDataSourceHelper
interface without casting this interface toReportDataSourceHelper
.
Below are examples of using the replacement API services in XAF Blazor UI:
C#// BEFORE
var ruleSet = Validator.RuleSet;
var auditService = AuditTrailService.Instance;
var captionHelper = CaptionHelper.XXX;
var reportStorage = ReportDataProvider.ReportsStorage;
// ----
// AFTER
IRuleSet ruleSet = Validator.GetService(Application.ServiceProvider);
IAuditTrailService auditService = AuditTrailService.GetService(Application.ServiceProvider);
ICaptionHelper captionHelper = CaptionHelper.GetService(Application.ServiceProvider);
IReportStorage reportStorage = ReportDataProvider.GetReportStorage(Application.ServiceProvider);
// IF YOU DO NOT HAVE ACCESS TO XafApplication, you can pass `null` as a parameter:
// IReportStorage reportStorage = ReportDataProvider.GetReportStorage(null);
Reasons for Change
- ASP.NET Core has a totally different architecture and user "request"/pipeline life cycle as compared to ASP.NET WebForms and WinForms. For performance reasons, we intentionally avoid creating and configuring XafApplication every time users make authentication, authorization and CRUD requests to ASP.NET Core.
- ValueManager has never been designed for XAF Blazor UI, Web API Service, and other non-XAF UI apps - we have always had internal tests for ValueManager API only in WinForms and WebForms. ValueManager might have worked in previous versions by accident (when XAF Blazor and Web API Service were in preview).
- Our ultimate goal is to use more and more lightweight REST API services inside these modern ASP.NET Core XAF and non-XAF apps for the best scalability (for instance, a greater number of concurrent users in XAF Blazor UI or Requests Per Second in our Web API Service).
Impact on Existing Apps
This change mostly affects you if you develop our Web API Service, and other non-XAF UI apps, and want to use some of the APIs above (in the same manner as you did in WinForms or ASP.NET WebForms). If you have a Blazor app only (without our Web API Service), you shouldn't be affected much or at all.
How to Update Existing Apps
Business Classes
If you relied on SecuritySystem.CurrentUserId
in your business class properties, update your implementation as shown in How to implement the CreatedBy, CreatedOn and UpdatedBy, UpdatedOn properties in a business class and online docs. This change is important for both XAF Blazor and Web API Service apps. SecuritySystem.CurrentUserId
will continue to work only in XAF WinForms and ASP.NET WebForms apps.
If you relied on CaptionHelper
and EnumDescriptor
in your business classes, and you reused returned localized values in our Web API Service, move this code out of business classes to the Web API Service endpoint code and use ICaptionHelperProvider: or the CaptionHelper.GetService(IServiceProvider serviceProvider)
method instead: ICaptionHelper helper = captionHelperProvider.GetCaptionHelper();
.
If you need to access XAF's Application Model from within your business class, consider the following services in v23.1+ (for more information, refer to Dependency Injection (DI)):
C#var dataContext = ...; // Your Session or ObjectSpace property depending on whether you are using XPO or EF Core.
ICaptionHelper captionHelper = dataContext.ServiceProvider.GetRequiredService<ICaptionHelperProvider>().GetCaptionHelper();
var applicationModel = captionHelper.GetModelApplication();
API Controllers and custom UI logic
When talking about the XAF Blazor UI, Web API Service, and other non-XAF UI apps, our general recommendation is to avoid using SecuritySystem, ValueManager, and other static properties. Instead, use specialized lightweight services such as IObjectSpaceFactory, ISecurityProvider, etc. We will provide yet more lightweight services for reporting, audit trail, validation, Application Model, and other popular customer scenarios in future versions.
XAF Blazor UI
Web API Service & Non-XAF UI Apps
Store custom values in your app
You can implement custom services and register them in the ConfigureServices method of the Startup.cs file of your ASP.NET Core app (example). Alternatively, you can implement other standard solutions from the Microsoft documentation: Session and state management in ASP.NET Core.
Filtering: Custom Criteria Functions & Operators
In v22.2, custom functions that use the static SecuritySystem.CurrentUserName, SecuritySystem.CurrentUserId, SecuritySystem.UserType, and SecuritySystem.CurrentUser properties stop working. To support this functionality, we introduced the following SecurityOptions.Events API:
- OnCustomizeSecurityCriteriaOperator
- OnCreateCustomSecurityFunctionPatcher
In v22.2, to create a custom criteria function that relies on the currently logged security user, return a required value inside the OnCustomizeSecurityCriteriaOperator delegate. In the following delegate, CurrentOrgId
is the name of a custom criteria function used in criteria expressions (for instance, [DataSourceCriteria("CurrentOrgId()==123")]), and (ApplicationUser)context.Security.User
is a replacement for static properties of the SecuritySystem class.
Codepublic class Startup {
//..
public void ConfigureServices(IServiceCollection services) {
//..
builder.Security
.UseIntegratedMode(options => {
//..
options.Events.OnCustomizeSecurityCriteriaOperator = context => {
if(context.Operator is FunctionOperator functionOperator) {
if(functionOperator.Operands.Count == 1 &&
"CurrentOrgId".Equals((functionOperator.Operands[0] as ConstantValue)?.Value?.ToString(), StringComparison.InvariantCultureIgnoreCase)) {
context.Result = new ConstantValue(((ApplicationUser)context.Security.User)?.Organization?.ID ?? Guid.NewGuid());
}
}
};
})
NOTE: The following additional XPO registration is NOT required in XAF v22.2.5+. In earlier XAF versions, you need to additionally implement and register a custom criteria function for XPO as shown below (EF Core apps do not require this). You can return dummy values in Evaluate and Convert methods of this custom criteria function. Related issues:
- Security.Blazor - "ValueManagerContext.Storage is null" error occurs when a custom function criteria operator is not processed by the SecurityFunctionPatcher logic
- T1156798 - The WebApiMiddleTierClientSecurity constructor got an additional parameter of the IServiceProvider type. (Middle Tier Server)
C#public class CurrentOrganizationCriteriaFunction : ICustomFunctionOperator, ICustomFunctionOperatorConvertibleToExpression {
public const string OperatorName = "CurrentOrgId";
private static readonly Guid mockKey = Guid.NewGuid(); // Just a dummy return value
public static void Register() => CriteriaOperator.RegisterCustomFunction(new CurrentOrganizationCriteriaFunction());
public string Name => OperatorName;
public object Evaluate(params object[] operands) => mockKey;
public Type ResultType(params Type[] operands) => typeof(Guid);
public Expression Convert(ICriteriaToExpressionConverter converter, params Expression[] operands) => Expression.Constant(mockKey);
}
C#public sealed class YourSolutionNameModule : ModuleBase {
public YourSolutionNameModule() {
//..
CurrentOrganizationCriteriaFunction.Register();
//..
The OnCustomizeSecurityCriteriaOperatorContext object has the following members:
C#public ISecurityStrategyBase Security { get; }
public CriteriaOperator Operator { get; }
public CriteriaOperator? Result { get; set; }
public IServiceProvider? ServiceProvider { get; }
The CreateCustomSecurityFunctionPatcherContext object has the following members:
C#public ISecurityStrategyBase Security { get; }
public SecurityFunctionPatcher? CustomSecurityFunctionPatcher { get; set; }
public IServiceProvider? ServiceProvider { get; }
The CreateCustomSecurityFunctionPatcher event helps you create a custom SecurityFunctionPatcher descendant:
Codeoptions.Events.OnCreateCustomSecurityFunctionPatcher = context => {
context.CustomSecurityFunctionPatcher = new CustomSecurityFunctionPatcher(context.Security);
};
Report Module
ReportModule.ReportsDataSourceHelper events can not be used for Web API Service
Web API Service doesn't use an XafApplication instance to load data for a report. If you handled the ReportDataSourceHelperBase.BeforeShowPreview event or other events of ReportDataSourceHelper to customize data before the report preview, add the following code lines to your Startup.cs file if you use the XAF Application builder:
C#// SolutionName.Blazor.Server/Startup.cs
builder.Modules
.AddReports(options => {
//...
options.Events.OnBeforeShowPreview = (context) => { /*...*/ };
})
If you are using direct service registration, add this code:
C#// SolutionName.Blazor.Server/Startup.cs
services.AddXafReporting(options => {
//...
options.Events.OnBeforeShowPreview = (context) => { /*...*/ };
});
Reports that are bound to Non-Persistent Objects no longer use the Application.ObjectSpaceCreated event
The XAF Blazor Reports Module no longer uses an XafApplication instance to create IObjectSpace and then load data. As a result, the XafApplication.ObjectSpaceCreated event is not fired in usage scenarios that involve XAF Blazor reports. In this case, the inner report data sources use the IObjectSpaceProviderFactory service directly instead of creating a new XafApplication instance. If you created XAF Blazor apps between v22.1 and v22.2+ using Application Builders and used How to: Display Non-Persistent Objects in a Report to handle the XafApplication.ObjectSpaceCreated event, follow these steps:
- In the SolutionName.Module project, implement the IObjectSpaceCustomizer service in a custom class as follows:
C#// SolutionName.Module
using System.ComponentModel;
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Core;
//Scoped service
public class NonPersistentObjectSpaceCustomizer : IObjectSpaceCustomizer {
private readonly IObjectSpaceProviderService objectSpaceProvider;
private readonly IObjectSpaceCustomizerService objectSpaceCustomizerService;
public NonPersistentObjectSpaceCustomizer(
IObjectSpaceProviderService objectSpaceProvider,
IObjectSpaceCustomizerService objectSpaceCustomizerService) {
this.objectSpaceProvider = objectSpaceProvider;
this.objectSpaceCustomizerService = objectSpaceCustomizerService;
}
public void OnObjectSpaceCreated(IObjectSpace objectSpace) {
if(objectSpace is NonPersistentObjectSpace nonPersistentObjectSpace) {
nonPersistentObjectSpace.ObjectsGetting += NonPersistentObjectSpace_ObjectsGetting;
nonPersistentObjectSpace.ObjectByKeyGetting += NonPersistentObjectSpace_ObjectByKeyGetting;
nonPersistentObjectSpace.Committing += NonPersistentObjectSpace_Committing;
nonPersistentObjectSpace.PopulateAdditionalObjectSpaces(objectSpaceProvider, objectSpaceCustomizerService);
}
}
private void NonPersistentObjectSpace_ObjectsGetting(object? sender, ObjectsGettingEventArgs e) {
//...
}
private void NonPersistentObjectSpace_ObjectByKeyGetting(object? sender, ObjectByKeyGettingEventArgs e) {
//...
}
private void NonPersistentObjectSpace_Committing(object? sender, CancelEventArgs e) {
//...
}
}
2 Use the TryAddEnumerable method to register your custom IObjectSpaceCustomizer service (NonPersistentObjectSpaceCustomizer):
Blazor
the SolutionName.Blazor.Server\Startup.cs file:
C#public void ConfigureServices(IServiceCollection services) {
//...
services.TryAddEnumerable(ServiceDescriptor.Scoped<IObjectSpaceCustomizer,
NonPersistentObjectSpaceCustomizer>());
//...
}
WinForms
SolutionName.Win\Startup.cs file:
C#public static WinApplication BuildApplication(string connectionString) {
var builder = WinApplication.CreateBuilder();
//...
builder.Services.TryAddEnumerable(ServiceDescriptor.Scoped<IObjectSpaceCustomizer,
NonPersistentObjectSpaceCustomizer>());
//...
}
- Comment out your old implementation for How to: Display Non-Persistent Objects in a Report.
Custom Report Storage
Refer to the How to: Create a Custom Report Storage to Customize UI and Behavior Globally help topic.
Export Report Data in Quartz Jobs
We recommend that you perform a service operation on behalf of a specific user that is designed for this task (a user can have full access or some limitations). You can create a separate scope for your service job and perform the authentication process for that scope. After that, all operations in that scope will be performed on behalf of a specific user. Please don't forget to dispose of the new scope or a memory leak may occur.
C#using System.Security.Claims;
using DevExpress.Data.Filtering;
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Core;
using DevExpress.ExpressApp.ReportsV2;
using DevExpress.ExpressApp.Security;
using DevExpress.ExpressApp.Security.Authentication.Internal;
using DevExpress.Persistent.BaseImpl.EF;
using Newtonsoft.Json;
using Quartz;
public class Class : IDisposable {
IServiceScope reportJobScope;
private readonly IReportExportService service;
private readonly IObjectSpaceFactory _securedObjectSpaceFactory;
public Class(IServiceScopeFactory scopeFactory) {
reportJobScope = scopeFactory.CreateScope();
var jobServiceProvider = reportJobScope.ServiceProvider;
service = jobServiceProvider.GetRequiredService<IReportExportService>();
_securedObjectSpaceFactory = jobServiceProvider.GetRequiredService<IObjectSpaceFactory>();
AuthenticateScope(jobServiceProvider, "ReportsServiceUser");
}
private void AuthenticateScope(IServiceProvider serviceProvider, string userName) {
using var nonSecuredOS = serviceProvider.GetRequiredService<INonSecuredObjectSpaceFactory>().CreateNonSecuredObjectSpace<ApplicationUser>();
var serviceUser = nonSecuredOS.FirstOrDefault<ApplicationUser>(user => user.UserName == userName);
string userKey = nonSecuredOS.GetKeyValueAsString(serviceUser);
var xafIdentityCreator = serviceProvider.GetRequiredService<IStandardAuthenticationIdentityCreator>();
var principal = new ClaimsPrincipal(xafIdentityCreator.CreateIdentity(userKey, ((ISecurityUser)serviceUser).UserName, null));
((IPrincipalProviderInitializer)serviceProvider.GetRequiredService<IPrincipalProvider>()).SetUser(principal);
}
public async System.Threading.Tasks.Task Execute(IJobExecutionContext context) {
//using IObjectSpace objectSpace = _securedObjectSpaceFactory.CreateObjectSpace(typeof(YourJobBusinessObject));
try {
// ...
var report = service.LoadReport<ReportDataV2>(x => x.DisplayName == "YourReportName");
service.SetupReport(report);
var ms = await service.ExportReportAsync(report, DevExpress.XtraPrinting.ExportTarget.Pdf);
// Process your memory stream further, for instance, to add mail attachments.
//objectSpace.CommitChanges();
}
catch(Exception ex) {
Console.WriteLine(ex.ToString());
// ...
//objectSpace.CommitChanges();
}
}
}
public void Dispose() {
reportJobScope.Dispose();
}
}
Audit Trail Module
Custom AuditTrailService for XAF Blazor & Web API
To create a custom IAuditTrailService service, you can use the DevExpress.Persistent.BaseImpl.AuditTrail.Services.AuditTrailServiceBase type as a base type for your custom implementation or implement the IAuditTrailService interface from scratch:
C#using System;
using DevExpress.ExpressApp.AuditTrail;
using DevExpress.ExpressApp.DC;
using DevExpress.ExpressApp.Security;
using DevExpress.Persistent.BaseImpl.AuditTrail.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
public class CustomAuditTrailService : AuditTrailServiceBase {
private const string unknownUserName = "Unknown";
private readonly ISecurityStrategyBase securityStrategy;
private readonly IAuditTrailServiceRoot auditTrailServiceRoot;
private bool isSetupAuditCalled = false;
public CustomAuditTrailService(IServiceProvider serviceProvider, IAuditTrailServiceRoot auditTrailServiceRoot, IOptionsSnapshot<AuditTrailOptions> auditTrailServiceOptions) :
base(serviceProvider, auditTrailServiceRoot, auditTrailServiceOptions.Value) {
this.securityStrategy = serviceProvider.GetService<ISecurityStrategyBase>();
this.auditTrailServiceRoot = auditTrailServiceRoot;
}
protected override void EnsureSetupAudit() {
if(!isSetupAuditCalled) {
isSetupAuditCalled = true;
auditTrailServiceRoot.SetupAudit(serviceProvider.GetRequiredService<ITypesInfo>(), Options.AuditDataItemPersistentType);
}
}
protected override string GetCurrentUserNameCore() {
return securityStrategy != null ? securityStrategy.UserName : unknownUserName;
}
}
Since XAF Blazor and Web API applications use the IAuditTrailService as a service, you need to register your custom 'IAuditTrailService' type as a scoped service. To override default XAF service registration, execute registration logic after registration of audit services.
An XAF Blazor application example:
C#//...
public class Startup {
//...
public void ConfigureServices(IServiceCollection services) {
//...
services.AddXaf(Configuration, builder => {
builder.UseApplication<MyBlazorApplication>();
builder.Modules
//...
.AddAuditTrailXpo()
//...
});
services.AddScoped<IAuditTrailService, CustomAuditTrailService>();
//...
}
//...
}
A standalone Web API application example:
C#//...
public class Startup {
//...
public void ConfigureServices(IServiceCollection services) {
//...
services.AddAuditTrailXpoServices();
services.AddScoped<IAuditTrailService, CustomAuditTrailService>();
//...
}
//...
}