Skip to content

Latest commit

 

History

History
150 lines (115 loc) · 8.36 KB

File metadata and controls

150 lines (115 loc) · 8.36 KB

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.

image

Scenario

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.

Implementation Details

  1. Copy the SequenceGenerator and UserFriendlyIdPersistentObject files to your project.

  2. Register the SequenceGeneratorProvider scoped service, configure SequenceGeneratorOptions, 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;
               };
           });
      
  3. Inherit your business classes to which you want to add sequential numbers from the module's UserFriendlyIdPersistentObject class. Declare a calculated property that uses the SequenceNumber 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")); }
    }
       
  4. 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. The Address class in this example uses separate sequences for each Province as follows:

    protected override string GetSequenceName() {
    	return string.Concat(ClassInfo.FullName, "-", Province.Replace(" ", "_"));
    }

Additional Information

  1. 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.

  2. 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.

  3. 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();
    }

Documentation