XAF - How to generate a sequential number for a persistent object within a database transaction with XPO
This example illustrates how to implement a business object with an identifier field with autogenerated sequential values.
This is a variation of the How to generate a sequential number for a business object within a database transaction XPO example, which was specially adapted for XAF applications.
In particular, for better reusability and smoother integration with standard XAF CRUD Controllers, all required operations to generate sequences are managed within the base persistent class automatically when a persistent object is being saved. This solution consists of several key parts:
- Sequence and SequenceGenerator are auxiliary classes that are primarily responsible for generating user-friendly identifiers.
- UserFriendlyIdPersistentObject is a base persistent class that subscribes to XPO's Session events and delegates calls to the core classes above. To get the described functionality in your project, inherit your own business classes from this base class.
Check the original example description first for more information on the demonstrated scenarios and functionality.
-
Copy the SequenceGenerator and UserFriendlyIdPersistentObject files to your project.
-
Register the
SequenceGeneratorProvider
scoped service, configureSequenceGeneratorOptions
, and specify the method that will be used to retrieve the Connection String from the database:For applications without multi-tenancy:
-
ASP.NET Core Blazor (
YourSolutionName\YourSolutionName.Blazor.Server\Startup.cs
)public class Startup { //... public void ConfigureServices(IServiceCollection services) { //... services.AddXaf(Configuration, builder => { //... builder.Services.AddScoped<SequenceGeneratorProvider>(); builder.Services.Configure<SequenceGeneratorOptions>(opt => { opt.GetConnectionString = (serviceProvider) => { return Configuration.GetConnectionString("ConnectionString"); }; });
-
WinForms (
YourSolutionName\YourSolutionName.Win\Startup.cs
)//... public class ApplicationBuilder : IDesignTimeApplicationFactory { public static WinApplication BuildApplication(string connectionString) { var builder = WinApplication.CreateBuilder(); builder.UseApplication<SequenceGeneratorWindowsFormsApplication>(); //... builder.Services.AddScoped<SequenceGeneratorProvider>(); builder.Services.Configure<SequenceGeneratorOptions>(opt => { opt.GetConnectionString = (serviceProvider) => { return connectionString; }; });
For multi-tenant applications:
-
ASP.NET Core Blazor (
YourSolutionName\YourSolutionName.Blazor.Server\Startup.cs
)public class Startup { //... public void ConfigureServices(IServiceCollection services) { //... services.AddXaf(Configuration, builder => { //... builder.Services.AddScoped<SequenceGeneratorProvider>(); builder.Services.Configure<SequenceGeneratorOptions>(opt => { opt.GetConnectionString = (serviceProvider) => { return serviceProvider.GetRequiredService<IConnectionStringProvider>().GetConnectionString(); }; });
-
WinForms (
YourSolutionName\YourSolutionName.Win\Startup.cs
)//... public class ApplicationBuilder : IDesignTimeApplicationFactory { public static WinApplication BuildApplication(string connectionString) { var builder = WinApplication.CreateBuilder(); builder.UseApplication<SequenceGeneratorWindowsFormsApplication>(); //... builder.Services.AddScoped<SequenceGeneratorProvider>(); builder.Services.Configure<SequenceGeneratorOptions>(opt => { opt.GetConnectionString = (serviceProvider) => { return serviceProvider.GetRequiredService<IConnectionStringProvider>().GetConnectionString(); }; });
For applications with Middle Tier Security:
-
Middle Tier project (
YourSolutionName.MiddleTier\Startup.cs
):public class Startup //... public void ConfigureServices(IServiceCollection services) { services.AddScoped<SequenceGeneratorProvider>(); services.Configure<SequenceGeneratorOptions>(opt => { opt.GetConnectionString = (serviceProvider) => { var options = serviceProvider.GetRequiredService<IOptions<DataServerSecurityOptions>>(); return options.Value.ConnectionString; }; });
-
-
Inherit your business classes to which you want to add sequential numbers from the module's
UserFriendlyIdPersistentObject
class. Declare a calculated property that uses theSequenceNumber
property of the base class to produce a string identifier according to the required format:public class Contact : GenerateUserFriendlyId.Module.BusinessObjects.UserFriendlyIdPersistentObject { [PersistentAlias("Concat('C',PadLeft(ToStr(SequentialNumber),6,'0'))")] public string ContactId { get { return Convert.ToString(EvaluateAlias("ContactId")); } }
-
Separate sequences are generated for each business object type. If you need to create multiple sequences for the same type, based on values of other object properties, override the
GetSequenceName
method and return the constructed sequence name. TheAddress
class in this example uses separate sequences for eachProvince
as follows:protected override string GetSequenceName() { return string.Concat(ClassInfo.FullName, "-", Province.Replace(" ", "_")); }
-
As an alternative, you can implement much simpler solutions at the database level or by using the built-in
DistributedIdGeneratorHelper.Generate
method. Refer to the following article for more details: Auto-Generate Unique Number Sequence. -
In application with the Security System, the newly generated sequence number will appear in the Detail View only after a manual refresh (in other words, it will be empty right after saving a new record), because the sequence is generated on the server side only and is not passed to the client. See the following section of the Auto-Generate Unique Number Sequence KB article: Refresh the Identifier field value in UI.
-
You can specify the initial sequence value manually. For this purpose, either edit the Sequence table in the database or use the standard XPO/XAF techniques to manipulate the
Sequence
objects. For example, you can use the following code:using(IObjectSpace os = Application.CreateObjectSpace(typeof(Sequence))) { Sequence sequence = os.FindObject<Sequence>(CriteriaOperator.Parse("TypeName=?", typeof(Contact).FullName)); sequence.NextSequence = 5; os.CommitChanges(); }