This example illustrates how to implement a business object with an identifier field with autogenerated sequential values.
This Readme focuses on Entity Framework Core. For information on how to achieve the same functionality with XPO, see the Readme.md file in the XPO solution's folder.
Implementation Details
Entity Framework Core allows you to set up generation of sequential values for non-key data fields as described in the following article in the Entity Framework Core documentation: Generated Values - Explicitly configuring value generation.
Use the steps below to generate sequential values for a business object's property so that the property is assigned a value once the object has been saved to the database:
- Add the
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
attribute to the required business object property:C#public class Address : BaseObject { // ... [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public virtual long SequentialNumber { get; set; } // ... }
- Add the following code to the DbContext's
OnModelCreating
method implementation so that the generated values are always displayed in the UI immediately after the object has been saved:C#public class GenerateUserFriendlyIdEFCoreDbContext : DbContext { // ... protected override void OnModelCreating(ModelBuilder modelBuilder) { // ... modelBuilder.Entity<Address>().UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction); } }
As an alternative technique, you can use a database-specific sequence object to generate sequential values as described in the following article: Sequences.
Files to Review
Documentation
Does this example address your development requirements/objectives?
(you will be redirected to DevExpress.com to submit your response)
Example Code
Code# 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.

## Scenario
This is a variation of the [How to generate a sequential number for a business object within a database transaction](https://www.devexpress.com/Support/Center/p/E2620) 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](SequenceGenerator/SequenceGenerator.Module/SequenceClasses/SequenceGenerator.cs) and [SequenceGenerator](SequenceGenerator/SequenceGenerator.Module/SequenceClasses/SequenceGenerator.cs) are auxiliary classes that are primarily responsible for generating user-friendly identifiers.
* [UserFriendlyIdPersistentObject](SequenceGenerator/SequenceGenerator.Module/SequenceClasses/UserFriendlyIdPersistentObject.cs) 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](SequenceGenerator/SequenceGenerator.Module/SequenceClasses/SequenceGenerator.cs) and [UserFriendlyIdPersistentObject](SequenceGenerator/SequenceGenerator.Module/SequenceClasses/UserFriendlyIdPersistentObject.cs) 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`)
```cs{7-12}
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`)
```cs{7-12}
//...
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`)
```cs{7-12}
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`)
```cs{7-12}
//...
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`):
```cs{4-10}
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:
```cs
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:
```cs
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](https://www.devexpress.com/Support/Center/p/T567184").
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](https://docs.devexpress.com/eXpressAppFramework/403605/business-model-design-orm/unique-auto-increment-number-generation#refresh-the-identifier-field-value-in-the-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](https://docs.devexpress.com/eXpressAppFramework/113711/data-manipulation-and-business-logic/create-read-update-and-delete-data) techniques to manipulate the `Sequence` objects. For example, you can use the following code:
```cs
using(IObjectSpace os = Application.CreateObjectSpace(typeof(Sequence))) {
Sequence sequence = os.FindObject<Sequence>(CriteriaOperator.Parse("TypeName=?", typeof(Contact).FullName));
sequence.NextSequence = 5;
os.CommitChanges();
}
```
## Documentation
* [Auto-Generate Unique Number Sequence](https://www.devexpress.com/Support/Center/p/T567184)
C#using System;
using System.ComponentModel;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.BaseImpl.EF;
using System.Collections.ObjectModel;
using System.ComponentModel.DataAnnotations.Schema;
namespace Demo.Module.BusinessObjects {
[DefaultClassOptions]
[DefaultProperty("FullAddress")]
[ImageName("BO_Address")]
public class Address : BaseObject {
public string AddressId {
get {
return $"A{SequentialNumber.ToString("D6")}";
}
}
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public virtual long SequentialNumber { get; set; }
public virtual string Province { get; set; }
public virtual string ZipCode { get; set; }
public virtual string Country { get; set; }
public virtual string City { get; set; }
public virtual string Address1 { get; set; }
public virtual string Address2 { get; set; }
public virtual ICollection<Contact> Persons { get; set; } = new ObservableCollection<Contact>();
public string FullAddress {
get { return $"{Country}, {Province}, {City}, {ZipCode}"; }
}
}
}
C#using System;
using System.ComponentModel;
using System.Collections.Generic;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.BaseImpl.EF;
using System.ComponentModel.DataAnnotations.Schema;
namespace Demo.Module.BusinessObjects {
[DefaultClassOptions]
[DefaultProperty("FullName")]
[ImageName("BO_Person")]
public class Contact : BaseObject {
public string ContactId {
get { return $"A{SequentialNumber.ToString("D6")}"; }
}
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public virtual long SequentialNumber { get; set; }
public virtual string FirstName { get; set; }
public virtual string LastName { get; set; }
public virtual int Age { get; set; }
public virtual Sex Sex { get; set; }
public virtual Address Address { get; set; }
public string FullName {
get { return $"{FirstName} {LastName}"; }
}
}
public enum Sex {
Male,
Female
}
}
C#using DevExpress.ExpressApp.DC;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.BaseImpl.EF;
using DevExpress.Persistent.Validation;
using System.ComponentModel.DataAnnotations.Schema;
namespace Demo.Module.BusinessObjects {
[DefaultClassOptions]
[XafDefaultProperty("Title")]
[ImageName("BO_Note")]
public class Document : BaseObject {
public virtual string DocumentId {
get { return $"D{SequentialNumber.ToString("D6")}"; }
}
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public virtual long SequentialNumber { get; set; }
[RuleRequiredField("IDocument.Title.RuleRequiredField", DefaultContexts.Save)]
[FieldSize(255)]
public virtual string Title { get; set; }
[FieldSize(8192)]
public virtual string Text { get; set; }
}
}
C#using DevExpress.ExpressApp.EFCore.Updating;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using DevExpress.Persistent.BaseImpl.EF.PermissionPolicy;
using DevExpress.Persistent.BaseImpl.EF;
using DevExpress.ExpressApp.Design;
using DevExpress.ExpressApp.EFCore.DesignTime;
using Demo.Module.BusinessObjects;
namespace GenerateUserFriendlyId.Module.BusinessObjects;
// This code allows our Model Editor to get relevant EF Core metadata at design time.
// For details, please refer to https://supportcenter.devexpress.com/ticket/details/t933891.
public class GenerateUserFriendlyIdContextInitializer : DbContextTypesInfoInitializerBase {
protected override DbContext CreateDbContext() {
var optionsBuilder = new DbContextOptionsBuilder<GenerateUserFriendlyIdEFCoreDbContext>()
.UseSqlServer(";")
.UseChangeTrackingProxies()
.UseObjectSpaceLinkProxies();
return new GenerateUserFriendlyIdEFCoreDbContext(optionsBuilder.Options);
}
}
//This factory creates DbContext for design-time services. For example, it is required for database migration.
public class GenerateUserFriendlyIdDesignTimeDbContextFactory : IDesignTimeDbContextFactory<GenerateUserFriendlyIdEFCoreDbContext> {
public GenerateUserFriendlyIdEFCoreDbContext CreateDbContext(string[] args) {
throw new InvalidOperationException("Make sure that the database connection string and connection provider are correct. After that, uncomment the code below and remove this exception.");
//var optionsBuilder = new DbContextOptionsBuilder<GenerateUserFriendlyIdEFCoreDbContext>();
//optionsBuilder.UseSqlServer("Integrated Security=SSPI;Pooling=false;Data Source=(localdb)\\mssqllocaldb;Initial Catalog=E2829EFCore");
//optionsBuilder.UseChangeTrackingProxies();
//optionsBuilder.UseObjectSpaceLinkProxies();
//return new GenerateUserFriendlyIdEFCoreDbContext(optionsBuilder.Options);
}
}
[TypesInfoInitializer(typeof(GenerateUserFriendlyIdContextInitializer))]
public class GenerateUserFriendlyIdEFCoreDbContext : DbContext {
public GenerateUserFriendlyIdEFCoreDbContext(DbContextOptions<GenerateUserFriendlyIdEFCoreDbContext> options) : base(options) {
}
public DbSet<Address> Addresses { get; set; }
public DbSet<Contact> Contacts { get; set; }
public DbSet<Document> Documents { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder) {
base.OnModelCreating(modelBuilder);
modelBuilder.HasChangeTrackingStrategy(ChangeTrackingStrategy.ChangingAndChangedNotificationsWithOriginalValues);
modelBuilder.Entity<Address>().UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction);
modelBuilder.Entity<Contact>().UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction);
modelBuilder.Entity<Document>().UsePropertyAccessMode(PropertyAccessMode.FieldDuringConstruction);
}
}