Scenario
This is a variation of the How to generate and assign a sequential number for a business object within a database transaction, while being a part of a successful saving process XPO example, which was specially adapted for XAF applications.
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:
- Sequence and SequenceGenerator are auxiliary classes that take the main part in generating user-friendly identifiers. Take special note that the SequenceGenerator.Initialize method must be called during your XAF application startup for the correct operation.
- UserFriendlyIdPersistentObject is a base persistent class that subscribes to XPO's Session events and delegates calls to the core classes above. Normally, you must inherit your own business classes from this base class to get the described functionality in your project.
- IUserFriendlyIdDomainComponent is a base domain component that should be implemented by all domain components that require the described functionality.
Check the original example description first for more information on the demonstrated scenarios and functionality.
Steps to implement
1. Copy and include the GenerateUserFriendlyId.Module project into your solution and make sure it is built successfully. Invoke the Module or Application Designer for the YourSolutionName.Module/Module.xx or YourSolutionName.Wxx/WxxApplication.xx files by double-clicking it in Solution Explorer. Invoke the Toolbox (Alt+X+T) and then drag & drop the GenerateUserFriendlyIdModule component into the modules list on the left.
2. In the YourSolutionName.Wxx/WxxApplication.xx files, modify the CreateDefaultObjectSpaceProvider method to call the SequenceGenerator.Initialize method as shown in the Demo.Wxx\WxxApplication.xx files:
[C#]...protectedoverridevoidCreateDefaultObjectSpaceProvider(CreateCustomObjectSpaceProviderEventArgsargs){GenerateUserFriendlyId.Module.Utils.SequenceGenerator.Initialize(this,args.ConnectionString);//!!!...}
3. If you are using pure XPO classes, then inherit your required business classes from the module's UserFriendlyIdPersistentObject one and use the derived SequenceNumber property as required:
[C#]publicclassContact:GenerateUserFriendlyId.Module.BusinessObjects.UserFriendlyIdPersistentObject{[PersistentAlias("concat('C', ToStr(SequentialNumber))")]publicstringContactId{get{returnConvert.ToString(EvaluateAlias("ContactId"));}}
If you are using DC, then implement the IUserFriendlyIdDomainComponent interface by your custom domain component:
[C#]publicinterfaceIDocument:GenerateUserFriendlyId.Module.BusinessObjects.IUserFriendlyIdDomainComponent{[Calculated("concat('D', ToStr(SequentialNumber))")]stringDocumentId{get;}
Additionally for DC, use the UserFriendlyIdPersistentObject as a base class during your custom domain component registration, e.g.:
[C#]XafTypesInfo.Instance.RegisterEntity("Document",typeof(IDocument),typeof(GenerateUserFriendlyId.Module.BusinessObjects.UserFriendlyIdPersistentObject));
For more information, download and review the Address, Contact, and IDocument types within the Demo.Module project. These are test business objects that demonstrate the use of the described functionality for XPO and DC respectively. A required format for the user-friendly identifier property in these end classes is defined within an aliased property (ContactId in the example above) by concatenation of a required constant or dynamic value with the SequentialNumber property provided by the base UserFriendlyIdPersistentObject class. So, if you need to have a different format, modify the PersistentAliasAttribute expression as your business needs dictate.
IMPORTANT NOTES
1. As an alternative, you can use a more simple solution that is using the DistributedIdGeneratorHelper.Generate method as shown in the FeatureCenter demo ("%Public%\Documents\DXperience 13.X Demos\eXpressApp Framework\FeatureCenter\CS\FeatureCenter.Module\KeyProperty\GuidKeyPropertyObject.cs" ) or at How to generate a sequential and user-friendly identifier field within a business class
2. This solution is not yet tested in the middle-tier and SecuredObjectSpaceProvider scenario and most likely, it will have to be modified to support its specifics.
3. The sequential number functionality shown in this example does not work with shared parts (a part of the Domain Components (DC) technology) in the current version, because it requires a custom base class, which is not allowed for shared parts.
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
}
}
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 AMI 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");
@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 PMHi,
Is this now supported in Middle-Tier XAF applications?
Thanks,
Carlitos
Added By: Dennis (DevExpress Support) at: 8/25/2014 4:38:12 AMWe 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 AMHi 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 AMBy 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.
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.
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