In particular, for better reusability and more smooth integration with the standard XAF CRUD Controllers, all the required operations to generate sequences are managed within the base persistent class automatically when a persistent object is being saved. For more developer convenience, this solution is organized as a reusable XAF module - GenerateUserFriendlyId.Module. This module consists of several key parts:
Check the original example description first for more information on the demonstrated scenarios and functionality.
Note that the sequential number functionality shown in this example does not work with DC shared parts , because it requires a custom base class, which is not allowed for shared parts.
4. By default, separate sequences are generated for each busienss 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 costructed sequence name. The Address class in this example uses separate sequences for each Province as follows:
Question Comments
Added By:
Mr. Murat YILMAZ at:
10/7/2012 9:15:18 AM Thanks for the example. I developed ISequentialNumber domain component which based on this article and code. Now this sequential number functonatility shon in this example work with domain components and shared parts.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using DevExpress.ExpressApp.DC;
using DevExpress.Persistent.Base;
using DevExpress.Xpo;
using DevExpress.Xpo.Metadata;
using DevExpress.ExpressApp;
using DevExpress.Persistent.Base;
using DevExpress.Persistent.BaseImpl;
using DevExpress.Persistent.Validation;
using MyWay.Xaf.Module;
using MyWay.Xaf.Utils.Sequence;
namespace MyWay.Xaf.Module
{
[DomainComponent]
[NavigationItem]
public interface ISequentialNumber
{
#region PROPERTIES
#region SequentialNo
[Persistent]
string SequentialNo
{
get;
}
#endregion SequentialNo
#region SequentialNumber
[System.ComponentModel.Browsable(false)]
[Indexed(Unique = false)]
long SequentialNumber
{
get ;
set;
}
#endregion SequentialNumber
#endregion PROPERTIES
}
[DomainLogic(typeof(ISequentialNumber))]
public class ISequentialNumberLogic
{
public static string Get_SequentialNo(ISequentialNumber instance)
{
return "asd" + instance.SequentialNumber.ToString();
}
private static object syncRoot = new object();
private static SequenceGenerator sequenceGenerator;
#region METHODS
#region PUBLIC METHODS
#region GenerateSequence
public static void GenerateSequence(ISequentialNumber instance)
{
lock (syncRoot)
{
Dictionary<string, bool> typeToExistsMap = new Dictionary<string, bool>();
foreach (object item in ((XPBaseObject)instance).Session.GetObjectsToSave())
{
typeToExistsMap[((XPBaseObject)instance).Session.GetClassInfo(item).FullName] = true;
}
if (sequenceGenerator == null)
{
sequenceGenerator = new SequenceGenerator(typeToExistsMap);
}
SubscribeToEvents(((XPBaseObject)instance).Session);
OnSequenceGenerated(sequenceGenerator.GetNextSequence(((XPBaseObject)instance).ClassInfo), instance);
}
}
#endregion GenerateSequence
#endregion PUBLIC METHODS
#region PROTECTED METHODS
#region OnSaving
public static void OnSaving(ISequentialNumber instance)
{
try
{
//base.OnSaving();
if (((XPBaseObject)instance).Session.IsNewObject(instance) && !typeof(NestedUnitOfWork).IsInstanceOfType(((XPBaseObject)instance).Session))
{
GenerateSequence(instance);
}
}
catch
{
CancelSequence(((XPBaseObject)instance).Session);
throw;
}
}
#endregion OnSaving
#endregion PROTECTED METHODS
#region INTERNAL METHODS
#endregion INTERNAL METHODS
#region PRIVATE METHODS
#region AcceptSequence
private static void AcceptSequence(Session session)
{
lock (syncRoot)
{
if (sequenceGenerator != null)
{
try
{
sequenceGenerator.Accept();
}
finally
{
CancelSequence(session);
}
}
}
}
#endregion AcceptSequence
#region CancelSequence
private static void CancelSequence(Session session)
{
lock (syncRoot)
{
UnSubscribeFromEvents(session);
if (sequenceGenerator != null)
{
sequenceGenerator.Close();
sequenceGenerator = null;
}
}
}
#endregion CancelSequence
#region Session_AfterCommitTransaction
private static void Session_AfterCommitTransaction(object sender, SessionManipulationEventArgs e)
{
AcceptSequence(e.Session);
}
#endregion Session_AfterCommitTransaction
#region Session_AfterRollBack
private static void Session_AfterRollBack(object sender, SessionManipulationEventArgs e)
{
CancelSequence(e.Session);
}
#endregion Session_AfterRollBack
#region Session_FailedCommitTransaction
private static void Session_FailedCommitTransaction(object sender, SessionOperationFailEventArgs e)
{
CancelSequence((Session)sender);
}
#endregion Session_FailedCommitTransaction
#region SubscribeToEvents
private static void SubscribeToEvents(Session session)
{
if (!(session is NestedUnitOfWork))
{
session.AfterCommitTransaction += Session_AfterCommitTransaction;
session.AfterRollbackTransaction += Session_AfterRollBack;
session.FailedCommitTransaction += Session_FailedCommitTransaction;
}
}
#endregion SubscribeToEvents
#region UnSubscribeFromEvents
private static void UnSubscribeFromEvents(Session session)
{
if (!(session is NestedUnitOfWork))
{
session.AfterCommitTransaction -= Session_AfterCommitTransaction;
session.AfterRollbackTransaction -= Session_AfterRollBack;
session.FailedCommitTransaction -= Session_FailedCommitTransaction;
}
}
#endregion UnSubscribeFromEvents
#region OnSequenceGenerated
private static void OnSequenceGenerated(long newId, ISequentialNumber instance)
{
instance.SequentialNumber = newId;
}
#endregion OnSequenceGenerated
#endregion PRIVATE METHODS
#endregion METHODS
}
}
Added By:
Luis Alberto Santiago at:
2/25/2013 8:06:36 PM Hi Mr. Murat YILMAZ, Can yo provide some example to use the class. I am looking it for shared parts
Added By:
Andrew Bingham 2 at:
7/8/2013 3:38:51 AM I implemented this OK in a Solution
When I eimplmented it in a different solution I got an Exception
"Message: Value cannot be null.
Parameter name: Application"
thrown at:
public static void Initialize() {
Guard.ArgumentNotNull(Application, "Application");
Added By:
Dennis (DevExpress Support) at:
7/11/2013 3:11:06 AM @Andrew: You forgot to add the following code:
public override void Setup(XafApplication application) {
base.Setup(application);
SequenceGeneratorInitializer.Register(application);
}
You will not experience this and similar difficulties if you simply add the reusable module into your solution as per instructions above.
Added By:
Carlitos at:
8/23/2014 6:46:05 PM Hi,
Is this now supported in Middle-Tier XAF applications?
Thanks,
Carlitos
Added By:
Dennis (DevExpress Support) at:
8/25/2014 4:38:12 AM We have not performed additional R&D and testing of this particular example in a middle-tier scenario. Moreover, I see technical problems using this particular solution in this configuration, because here we use ExplicitUnitOfWork, which implies that it will be a sole owner of the database connection. Probably, for this configuration, it is better to stick to a standard solution, e.g. using database triggers.
Added By:
Carlitos at:
8/25/2014 9:34:23 AM Hi Dennis,
But how does the Middle-tier handle database connections? I thought it would do some sort of connection pooling and manage all client connections?
Why do you think that Triggers are a good alternative?
Carlos
Added By:
Dennis (DevExpress Support) at:
8/26/2014 6:14:27 AM By default, the middle-tier app server uses ThreadSafeDataLayer to effectively handle requests from multiple clients at the same time. Connection pooling may be used here, but this is not managed by us by default and is rather controller by ADO.NET transparently for a consumer. My main concern here was that in such a multi-user environment it will not be possible to exclusively lock the connection by one user.
A solution at the database level looks more universal as it works at the database level and is irrelevant to whether the app server is used or not.
Added By:
Vishwas Mokashi at:
4/14/2015 11:21:58 AM In XAF application I have about 25 various business objects that need to generate unique user friendly sequence numbers in when saved. For example Customer Number, Invoice Number, Order Number, etc. Many users are going to work concurrently and everything should work in transaction.
Which approach should I use, the one in E2620 or E2829?.
I felt E2620 a bit better as I need not to inherit my classes (25 of them) from UserFriendlyIdPersistentObject as shown in E2829.
But approach in E2829 is said to be primarily for XAF applications
Added By:
Dennis (DevExpress Support) at:
4/15/2015 4:47:25 AM @Vishwas: Both E2620 and E2829 demonstrate the same solution using ExplicitUnitOfWork. E2829 is just a bit better integrated with the XAF infrastructure.
If you do not want to inherit from a common base class, I suggest you consider using How to generate a sequential and user-friendly identifier field within a business class, which is simpler to implement and provides similar capabilities without using ExplicitUnitOfWork.
Added By:
Vishwas Mokashi at:
4/17/2015 6:54:45 AM OK thanks Dennis.
Will simpler approach in "How to generate a sequential and user-friendly identifier field within a business class" handle concurrent users creating same business objects properly?. We can assume about 25 Concurrent users for same business object, with SQL Server DB.
Some users will be accessing system over low banddwidth VPN internet connection. (for Sale Invoice object which needs to generate sequencial Invoice Numbers)
Added By:
Dennis (DevExpress Support) at:
4/20/2015 7:58:16 AM @Vishwas: Yes, it will. The DistributedIdGeneratorHelper class tries to save a record and repeats its attempts several times until it reaches the maximum number or saves data completely. Check out the corresponding source code for more details on how this works.
public static class DistributedIdGeneratorHelper {
public const int MaxIdGenerationAttemptsCounter = 7;
public static int Generate(IDataLayer idGeneratorDataLayer, string seqType, string serverPrefix) {
for(int attempt = 1; ; ++attempt) {
try {
using(Session generatorSession = new Session(idGeneratorDataLayer)) {
CriteriaOperator serverPrefixCriteria;
if(serverPrefix == null) {
serverPrefixCriteria = new NullOperator("Prefix");
}
else {
serverPrefixCriteria = new BinaryOperator("Prefix", serverPrefix);
}
OidGenerator generator = generatorSession.FindObject<OidGenerator>(
new GroupOperator(new BinaryOperator("Type", seqType), serverPrefixCriteria));
if(generator == null) {
generator = new OidGenerator(generatorSession);
generator.Type = seqType;
generator.Prefix = serverPrefix;
}
generator.Oid++;
generator.Save();
return generator.Oid;
}
}
catch(LockingException) {
if(attempt >= MaxIdGenerationAttemptsCounter)
throw;
}
}
}Added By:
Vishwas Mokashi at:
4/21/2015 6:03:28 AM Thanks Dennis
Added By:
Konstantin B (DevExpress) at:
12/4/2017 1:20:01 AM Update: This example illustrates one of the possible approaches to implementing an identifier field with sequential values. Alternate approaches are listed in the
An overview of approaches to implementing a user-friendly sequential number for use with an XPO business class KB article.Added By:
Cecil Carter at:
3/3/2018 7:30:11 AM I've tried running the example scenario against a MySQL database and I'm receiving the following error. I assume this is related to the SQL statement generated by the ORM for MySQL. Is there a possible work around to this error?
DevExpress.ExpressApp.Updating.CompatibilityException
HResult=0x80131509
Message=Unable to create 'PrimaryKey' 'PK_Sequence'. Parent: 'Sequence'. Error: Executing Sql 'alter table `Sequence` add constraint `PK_Sequence` primary key (`TypeName`)' with parameters '' exception 'MySql.Data.MySqlClient.MySqlException (0x80004005): BLOB/TEXT column 'TypeName' used in key specification without a key length
at MySql.Data.MySqlClient.MySqlStream.ReadPacket()
at MySql.Data.MySqlClient.NativeDriver.GetResult(Int32& affectedRow, Int64& insertedId)
at MySql.Data.MySqlClient.Driver.NextResult(Int32 statementId, Boolean force)
at MySql.Data.MySqlClient.MySqlDataReader.NextResult()
at MySql.Data.MySqlClient.MySqlCommand.ExecuteReader(CommandBehavior behavior)
at MySql.Data.MySqlClient.MySqlCommand.ExecuteNonQuery()
at DevExpress.Xpo.Logger.LogManager.Log[T](String category, LogHandler`1 handler, MessageHandler`1 createMessageHandler)
at DevExpress.Xpo.DB.ConnectionProviderSql.ExecSql(Query query)'
Source=DevExpress.ExpressApp.v17.2
StackTrace:
at DevExpress.ExpressApp.Updating.DatabaseUpdater.Update()
at GenerateUserFriendlyId.Win.GenerateUserFriendlyIdWindowsFormsApplication.GenerateUserFriendlyIdWindowsFormsApplication_DatabaseVersionMismatch(Object sender, DatabaseVersionMismatchEventArgs e) in C:\Users\Cecil Carter\Downloads\eXpressApp Framework\17.2.5\E2829\Demo.Win\WinApplication.cs:line 26
at DevExpress.ExpressApp.XafApplication.OnDatabaseVersionMismatch(DatabaseVersionMismatchEventArgs args)
at DevExpress.ExpressApp.XafApplication.CheckCompatibilityCore()
at DevExpress.ExpressApp.XafApplication.CheckCompatibility()
at DevExpress.ExpressApp.XafApplication.Logon(PopupWindowShowActionExecuteEventArgs logonWindowArgs)
Inner Exception 1:
UnableToCreateDBObjectException: Unable to create 'PrimaryKey' 'PK_Sequence'. Parent: 'Sequence'. Error: Executing Sql 'alter table `Sequence` add constraint `PK_Sequence` primary key (`TypeName`)' with parameters '' exception 'MySql.Data.MySqlClient.MySqlException (0x80004005): BLOB/TEXT column 'TypeName' used in key specification without a key length
at MySql.Data.MySqlClient.MySqlStream.ReadPacket()
at MySql.Data.MySqlClient.NativeDriver.GetResult(Int32& affectedRow, Int64& insertedId)
at MySql.Data.MySqlClient.Driver.NextResult(Int32 statementId, Boolean force)
at MySql.Data.MySqlClient.MySqlDataReader.NextResult()
at MySql.Data.MySqlClient.MySqlCommand.ExecuteReader(CommandBehavior behavior)
at MySql.Data.MySqlClient.MySqlCommand.ExecuteNonQuery()
at DevExpress.Xpo.Logger.LogManager.Log[T](String category, LogHandler`1 handler, MessageHandler`1 createMessageHandler)
at DevExpress.Xpo.DB.ConnectionProviderSql.ExecSql(Query query)'
Inner Exception 2:
SqlExecutionErrorException: Executing Sql 'alter table `Sequence` add constraint `PK_Sequence` primary key (`TypeName`)' with parameters '' exception 'MySql.Data.MySqlClient.MySqlException (0x80004005): BLOB/TEXT column 'TypeName' used in key specification without a key length
at MySql.Data.MySqlClient.MySqlStream.ReadPacket()
at MySql.Data.MySqlClient.NativeDriver.GetResult(Int32& affectedRow, Int64& insertedId)
at MySql.Data.MySqlClient.Driver.NextResult(Int32 statementId, Boolean force)
at MySql.Data.MySqlClient.MySqlDataReader.NextResult()
at MySql.Data.MySqlClient.MySqlCommand.ExecuteReader(CommandBehavior behavior)
at MySql.Data.MySqlClient.MySqlCommand.ExecuteNonQuery()
at DevExpress.Xpo.Logger.LogManager.Log[T](String category, LogHandler`1 handler, MessageHandler`1 createMessageHandler)
at DevExpress.Xpo.DB.ConnectionProviderSql.ExecSql(Query query)'
Inner Exception 3:
MySqlException: BLOB/TEXT column 'TypeName' used in key specification without a key length
Added By:
Dennis (DevExpress Support) at:
3/5/2018 12:35:24 AM @Cecil Carter: I've created a separate ticket on your behalf (T612198: MySql - The "BLOB/TEXT column 'XXX' used in key specification without a key length" error may occur when using a string key property). It has been placed in our processing queue and will be answered shortly.