Today I will detail about implementing Scheduled Long running tasks in an abstract and reusable way.
The Requirement
Usually, I have a business objects that store time depended data e.g. Exceptions, Logs etc. There is a need to periodically clean up those tables.
The parameters
First we need to identify the problem parameters so we can create a model interface. Parameterizing the problem in a model interface is very useful, because the end user can switch it off if something goes wrong.
Parameters will be: the business object type which we want to clear its records, the criterion to filter the objects and the time to execute this action. The interface along with its registration for this is bellow.
using System;
using System.ComponentModel;
using DevExpress.ExpressApp;
using DevExpress.ExpressApp.Model;
using DevExpress.ExpressApp.Model.Core;
namespace PurgingRule.Module.Controllers{
//rules container
public interface IModelClassPurgingRules:IModelClass{
IModelPurgingRules PurgingRules{ get; }
}
[ModelNodesGenerator(typeof (PurgingRulesModelNodesGenerator))]
public interface IModelPurgingRules : IModelNode, IModelList<IModelPurgingRule>{
}
public interface IModelPurgingRule:IModelNode{
string Criteria{ get; set; }
bool ExecuteOnApplicationStart{ get; set; }
TimeSpan TimeSpan{ get; set; }
[DefaultValue(500)]
int ChunkSize{ get; set; }
}
//will help us generated more rules if needed
public class PurgingRulesModelNodesGenerator:ModelNodesGeneratorBase{
protected override void GenerateNodesCore(ModelNode node){
}
}
public class PurgingController:Controller,IModelExtender{
//model interface registration
public void ExtendModelInterfaces(ModelInterfaceExtenders extenders){
extenders.Add<IModelClass,IModelClassPurgingRules>();
}
}
}
Now, it is possible to use the Model Editor to configure in which object we want to apply our rules.
That's nice!
but I like to use an Editor to write the Criteria and need to add a few more bits for this to happen. For this we need to use the CriteriaOptions and Editor attributes as shown:
public interface IModelPurgingRule:IModelNode{
[Editor("DevExpress.ExpressApp.Win.Core.ModelEditor.CriteriaModelEditorControl, DevExpress.ExpressApp.Win" + XafAssemblyInfo.VersionSuffix + XafAssemblyInfo.AssemblyNamePostfix, typeof(UITypeEditor))]
[CriteriaOptions("TypeInfo")]
string Criteria{ get; set; }
[Browsable(false)]
[ModelValueCalculator("((IModelClass) Parent.Parent).TypeInfo")]
ITypeInfo TypeInfo { get; }
Now it should be straight forward to construct any criterion with the help of this build-in editor.
Making Thread Safe Database calls
Working in a multi threaded environment (long running tasks) is build-in in XAF's XPO ORM so we go for it. Just make sure you enable the switch as bellow.
protected override void CreateDefaultObjectSpaceProvider(CreateCustomObjectSpaceProviderEventArgs args) {
var threadSafe = true //enable threadsafe;
args.ObjectSpaceProviders.Add(new XPObjectSpaceProvider(XPObjectSpaceProvider.GetDataStoreProvider(args.ConnectionString, args.Connection, true), threadSafe));
args.ObjectSpaceProviders.Add(new NonPersistentObjectSpaceProvider(TypesInfo, null));
}
Scheduling the long running tasks
We will use the StartNewPeriodic extension method and will follow the next steps in our code.
- Execute once at application startup .
- Collect the model rules to execute.
- Calculate the next execution time. For this we need to store the last execution time so I used the RuleScheduleStorage BO class found at the end of the next snippet
- Periodically schedule parallel calls to the PurgeObjects method for each rule.
public class PurgingController:Controller,IModelExtender{
protected override void OnFrameAssigned(){
base.OnFrameAssigned();
//1.execute once at application startup .
if (Frame.Context == TemplateContext.ApplicationWindow){
//2.collect the model rules to execute.
var purgingRules = Application.Model.BOModel.Cast<IModelClassPurgingRules>().SelectMany(rules => rules.PurgingRules ).ToArray();
IEnumerable<(IModelPurgingRule rule, DateTime executed)> ruleExecutionTimes;
//create an objectspace to query the last execution time from the database
using (var objectSpace = Application.CreateObjectSpace(typeof(RuleScheduleStorage))){
DeleteObsoleteRules(objectSpace, purgingRules);
var rulesToSchedule = purgingRules.Where(rule =>rule.Interval!=TimeSpan.MinValue);
//get an enumerable of (IModelPurgingRule rule, DateTime executed)
ruleExecutionTimes = CalculateExecutionTimes(rulesToSchedule, objectSpace);
}
foreach (var ruleExecutionTime in ruleExecutionTimes){
var timeSinceLastExecution = DateTime.Now.Subtract(ruleExecutionTime.executed);
//calculate if periodic task should start with a delay based time passed since last execution
int delay=timeSinceLastExecution<ruleExecutionTime.rule.Interval?(int) ruleExecutionTime.rule.Interval.Subtract(timeSinceLastExecution).TotalMilliseconds:0;
//starts the task periodically
Task.Factory.StartNewPeriodic(() => PurgeObjects(ruleExecutionTime.rule),
interval: (int) ruleExecutionTime.rule.Interval.TotalMilliseconds, delay: delay);
}
}
}
private static IEnumerable<(IModelPurgingRule rule, DateTime executed)> CalculateExecutionTimes(IEnumerable<IModelPurgingRule> rulesToSchedule, IObjectSpace objectSpace){
return rulesToSchedule.Select(rule => {
var ruleScheduleStorage = objectSpace.GetObjectsQuery<RuleScheduleStorage>()
.FirstOrDefault(storage =>storage.RuleScheduleType == RuleScheduleType.Purging && storage.RuleId ==((ModelNode) rule).Id);
return (rule:rule,executed:ruleScheduleStorage?.Executed ?? DateTime.MinValue);
});
}
private void DeleteObsoleteRules(IObjectSpace objectSpace, IModelPurgingRule[] purgingRules){
var ids = purgingRules.Cast<ModelNode>().Select(node => node.Id).ToArray();
var rulesToDelete = objectSpace.GetObjectsQuery<RuleScheduleStorage>().Where(storage =>
storage.RuleScheduleType == RuleScheduleType.Purging && !ids.Contains(storage.RuleId)).ToArray();
objectSpace.Delete(rulesToDelete);
objectSpace.CommitChanges();
}
You might wonder why I inherit from a controller and used the OnFrameAssigned method as start signal and not simply write my code at Application.SetupComplete.Event. The reason for this is that I like to keep my implementation in separate files and not pollute the Module.cs.
The Long Running PurgeObjects method:
- Stores last execution time in the database.
- Deletes filtered objects in chunks.
private void PurgeObjects(IModelPurgingRule purgingRule){
try{
Tracing.Tracer.LogVerboseText($"Purging {purgingRule}");
var objectsCount = 0;
using (var objectSpace = Application.CreateObjectSpace(purgingRule.TypeInfo.Type)){
StoreExecutionTime(purgingRule, objectSpace);
var criteriaOperator = objectSpace.ParseCriteria(purgingRule.Criteria);
var objects = objectSpace.GetObjects(purgingRule.TypeInfo.Type, criteriaOperator);
objectSpace.SetTopReturnedObjectsCount(objects, purgingRule.ChunkSize);
while (objects.Count > 0){
objectsCount += objects.Count;
objectSpace.Delete(objects);
objectSpace.CommitChanges();
objectSpace.ReloadCollection(objects);
}
}
Tracing.Tracer.LogVerboseText($"Purged {purgingRule}-{objectsCount}");
}
catch (Exception e){
Tracing.Tracer.LogError(e);
}
}
private static void StoreExecutionTime(IModelPurgingRule purgingRule, IObjectSpace objectSpace){
var ruleId = ((ModelNode) purgingRule).Id;
var ruleScheduleStorage =objectSpace.GetObjectsQuery<RuleScheduleStorage>().FirstOrDefault(storage =>
storage.RuleScheduleType == RuleScheduleType.Purging && storage.RuleId == ruleId) ?? objectSpace.CreateObject<RuleScheduleStorage>();
ruleScheduleStorage.RuleScheduleType = RuleScheduleType.Purging;
ruleScheduleStorage.RuleId = ((ModelNode) purgingRule).Id;
ruleScheduleStorage.Executed = DateTime.Now;
objectSpace.CommitChanges();
}
Last minute feature
I am happy with the PurgingRules, but I would be more happy if I could invalidate them based on a CSharp expression. For example I want to have some rules on my dev machine only.
The PurgingRules already have a criterion so we could create a Custom Function to evaluate CSharp expressions.
I want to use this code in eXpandFramework which there is a <=.NET4 dependency so for evaluating CSharp expressions I chose compile on the fly + caching the from ExpressionEvaluator.Eval method.
public class EvaluateExpressionOperator:ICustomFunctionOperator{
public List<string> Usings=new List<string>();
public const string OperatorName = "EvaluateExpression";
public static EvaluateExpressionOperator Instance{ get; } = new EvaluateExpressionOperator();
public Type ResultType(params Type[] operands){
return typeof(object);
}
public object Evaluate(params object[] operands){
var csCode = string.Join("",operands);
var usings = string.Join(Environment.NewLine,Usings);
var eval = ExpressionEvaluator.Eval(csCode, usings);
return eval;
}
public string Name => OperatorName;
}
Working with such rich libraries like DevExpress XAF suite you make serious stuff in no time!
A few months ago eXpandFramework released the first version of View inheritance(see http://goo.gl/3YhTa9). In the latest version of the framework (13.1.8.12) it is possible filter the nodes that participate in the inheritance strategy.
The MergedDifference Strategy
To inherit a model view configuration from another view you can use the Model Editor and add a new record in the MergedDifferences collection as illustrated below.
Now the great thing is that the Strategy attribute if fully configurable from Application/Options/MergedDifferencesStrategies. By default there are three common strategies Everything, EverythingButLayout, OnlyLayout.
In the above image we see how the Everything strategy is defined. It contains all root nodes of both DetailView and ListView nodes.
As you might have guessed the inheritance scenarios are limited only from your imagination. For example in the next image we see the OnlyLayout strategy however what defines the layout may vary so it is possible to inherit from the GridViewOptions node by simply adding a new record in the OnlyLayout strategy (bold).
Are there any working examples?
Yes in eXpandFramework source code there are many examples and yes eXpandFramework eats its own food.
The LogicOperationPermissionData
From the LogicOperationPermissionData class starts a deep inheritance tree that defines the Permissions used from all Logic Module depended modules as shown in the next image.
Below we see the layout of the base view (LogicRuleOperationPermissionData class).
In the next image the next class in the inheritance tree where we note that everything is the same as the base view and we also added the AdditionalViewControls extra tab.
The ModelDifferenceObject
In the ModelDifference module you can find the next inheritance tree.
The base view.
Next is the Role descendant layout, where everything is the same as the base view and we also added the Roles extra tab.
The XpandLogonParameters
XpandLogonParameters lives in the Xpand Security module and inherits from the AuthenticationStandardLogonParameters which is an XAF class and lives in the XAF Security module.
Bellow we see the layout of the AuthenticationStandardLogonParameters inside the XAF Security module.
Now inside the Xpand Security module we have modified the previous layout of the AuthenticationStandardLogonParameters as in next image.
Next, we inherited that layout from the XpandLogonParameters class and added the extra Remember Me item as illustrated below.
A cross view custom Merged Strategy
In both model DetailView and ListView nodes you can find the HiddenActions node which can be used to hide actions for a view.
Let’s say we want to create a custom merged strategy to distribute the above list of hidden actions (Save, SaveAndClose).
Step 1: Create a View container
We first create a clone of the BaseObject_ListView, name it HiddenActions and add the actions we want to distribute in the HiddenActions collection. I chose the BaseObject_ListView because it can be inherited from all objects.
Step 2: Create a Merged Strategy
For this we have to set the NodePath to the HiddenActions node and the ViewType to Any so we can distribute to both DetailViews and ListViews.
Step 3: Inherit from the HiddenActions view
Go to the view that you want to inherit the HiddenActions view differences and create a MergedDifference record as the one below.
Step 4: Close and open again the Model Editor in order to see the inherited HiddenActions
Big thanks to everybody that helped making this feature so powerful and even bigger thanks to the XAF developers that work hard to create that wonderful Application Model API.
What Locking is all about
Transactional isolation is usually implemented by locking whatever is accessed in a transaction. There are two different approaches to transactional locking: Pessimistic locking and optimistic locking.
The disadvantage of pessimistic locking is that a resource is locked from the time it is first accessed in a transaction until the transaction is finished, making it inaccessible to other transactions during that time. If most transactions simply look at the resource and never change it, an exclusive lock may be overkill as it may cause lock contention, and optimistic locking may be a better approach. With pessimistic locking, locks are applied in a fail-safe way. In the banking application example, an account is locked as soon as it is accessed in a transaction. Attempts to use the account in other transactions while it is locked will either result in the other process being delayed until the account lock is released, or that the process transaction will be rolled back. The lock exists until the transaction has either been committed or rolled back.
With optimistic locking, a resource is not actually locked when it is first is accessed by a transaction. Instead, the state of the resource at the time when it would have been locked with the pessimistic locking approach is saved. Other transactions are able to concurrently access to the resource and the possibility of conflicting changes is possible. At commit time, when the resource is about to be updated in persistent storage, the state of the resource is read from storage again and compared to the state that was saved when the resource was first accessed in the transaction. If the two states differ, a conflicting update was made, and the transaction will be rolled back.
In the banking application example, the amount of an account is saved when the account is first accessed in a transaction. If the transaction changes the account amount, the amount is read from the store again just before the amount is about to be updated. If the amount has changed since the transaction began, the transaction will fail itself, otherwise the new amount is written to persistent storage.
XAF Build in locking mechanism
XAF’s datalayer is based on XPO , which already has an optimistic locking implementation on its core. It is enabled by default for all objects that inherit XPBaseObjet and can be disabled by using the OptimisticLocking attribute .
[OptimisticLocking(false)]
public class Client:XPBaseObject {
public Client(Session session) : base(session) {
}
}
For objects that have that attribute XPO is going to create an extra service field, the OptimisticLockingField to store that state of the object.
To see the locking mechanism in action you can perform the following steps
User 1 | User 2 |
1. Run application | 1. Run application |
2. Go to Client object detail view | 2. Go to Client object detail view |
3. XPO reads the value from the optimistic field value (initial value is zero) and stores it to memory | 3. XPO reads the value from the optimistic field value (initial value is zero) and stores it to memory |
4. User is making a change and tries to save the object - XPO queries the the optimisticfield value and compares it with the one in memory
- 2 values are equal so transaction is commited and optimisticlocking fields is raized by one
| 4. User is making a change and tries to save the object - XPO queries the the optimisticfield value (now its one cause user 1 has save the record) and compares it with the one in memory
- 2 values are not equal so a locking exception is thrown by XPO giving information about the locked object
|
| 5. User reloads the object from the database, new optimisticlocking field value is store in memory (one), user makes changes and is able to save the record |
eXpand Pessimistic locking
XPO has no means to determine any information about users, but we are in XAF context and XAF has a build in security system and we know the current user. Lets try to implement a pessimistic locking feature for our XAF applications.
By carefully looking at “3. XPO reads the value from the optimistic field value (initial value is zero) and stores it to memory “ , we can see that there is a place to minimize the locking conflicts. What if we display the detailview in “ViewMode” and add an “Edit” action? And when the Edit action is executed the object will be reloaded from the db, also when is saved the DetailView will return is View mode.
I think we are going to gain something from that even if we use the default Xpo optimistic locking. Lucky us eXpand has already the ViewEditMode attribute that can do that job for us.
Now from the Pessimistic locking definition (when it is first is accessed by a transaction). We have to decide what that means in our case. A good idea would be to lock the object at the time that is changed by a user. Of course we can enhance that in the future upon your requests. We also need a special field that will store the user that locked the record and a special attribute to mark the object for using our pessimistic locking attribute. Finally its a good idea to disable default OptimisticLocking mechanism.
All the above should be transparent to the user of our feature, we need to spent zero time when using it in future projects
Customizing XAF types is very easy as you know , just some lines of code and can do the trick.
public override void CustomizeTypesInfo(DevExpress.ExpressApp.DC.ITypesInfo typesInfo) {
base.CustomizeTypesInfo(typesInfo);
var typeInfos = typesInfo.PersistentTypes.Where(info => info.FindAttribute<PessimisticLockingAttribute>() != null);
foreach (var typeInfo in typeInfos) {
typeInfo.AddAttribute(new OptimisticLockingAttribute(false));
var memberInfo = typeInfo.FindMember(LockedUser);
if (memberInfo == null) {
memberInfo = typeInfo.CreateMember(LockedUser, SecuritySystem.UserType);
memberInfo.AddAttribute(new BrowsableAttribute(false));
}
}
}
Great! Now we need to define our specifications. Remember we are dealing with data now and with a complex feature that may evolve from fellow developers request. For those cases at eXpand we use a BDD approach, and MSpec as our BDD framework.
Here are some specs
PessimisticLockingViewController, When Object Change
» should lock the object
PessimisticLockingViewController, When objectspace rollback
» should unlock object
PessimisticLockingViewController, When ospace commited
» should unlock object
PessimisticLockingViewController, When View CurrentObject Changing
» should unlock object
PessimisticLockingViewController, When View Is closing
» should unlock object
PessimisticLocker, When object is about to be unlocked
» should not unlock if current user does not match locked user
PessimisticLockingViewController, When a locked object is open by a second user
» should not allowedit on view
PessimisticLockingViewController, When 2 users open the same object and both try to change it
» should mark as readonly last user view
PessimisticLockingViewController, When editing a locked detailview
» should allow edit for the pessimistic locking context
PessimisticLocker, When unlocking new object
» should do nothing
PessimisticLocker, When locking new object
» should do nothing
PessimisticLocker, When new object locking state is queried
» should return unlocked
and in this file you can see the implementation of them
https://github.com/expand/eXpand/blob/master/Xpand/Xpand.Tests/Xpand.Tests/Xpand.ExpressApp/PessimisticLockingSpecs.cs
As you see from the specifications when an object is locked the detailview will be read only for a PessimisticLocking context. But real world is strange we have to cover exceptions as well. What will happen if an object was locked and our application was terminated abnormally? We need an action that will force unlock the object.
Maybe there is a need for some timeout implementation there but I do not have strong ideas on this, better wait for some feedback from out there first before spending any more resources. Anyway we are very close now. What we are missing is a message that will tell which user has locked an object when it is locked.
To display the message I think we can utilize our AdditionalViewControlsProvider module. That module allows to conditionally display (When our LockingUser field is different for the current user) a message. Also allows us to conditionalize/localize the message it self. Lets see how
First step will be to use the AdditionalViewControlsRule to display the message as bellow
[PessimisticLocking]
[Custom("ViewEditMode","View")]
[AdditionalViewControlsRule("teee", "LockedUser!='@CurrentUserID' AND LockedUser Is Not Null", "1=0", "Record is locked by user {0}", Position.Top, MessageProperty = "LockedUserMessage")]
public class Client : BaseObject {
public Client(Session session)
: base(session) {
}
private string _lockedUserMessage;
[NonPersistent][Browsable(false)]
public string LockedUserMessage {
get {
var memberValue = GetMemberValue("LockedUser");
if (_lockedUserMessage != null) {
return memberValue != null ? string.Format(_lockedUserMessage, memberValue) : null;
}
return null;
}
set { _lockedUserMessage = value; }
}
}
that will create a rule at our model like the following.
and will display the message
2nd step is to refactor the attribute to something easier to use like
public class PessimisticLockingMessageAttribute : AdditionalViewControlsRuleAttribute {
public PessimisticLockingMessageAttribute(string id)
: base(id, "LockedUser!='@CurrentUserID' AND LockedUser Is Not Null", "1=0", "Record is locked by user {0}", Position.Top) {
MessageProperty = "LockedUserMessage";
}
}
and 3rd step is to refactor the LockedUserMessage property. We have seen already that dynamically adding a property is a piece of cake, but how can we dynamically add a property that has behavior such as the LockedUserMessage property?
Easy as always :), we just have to create our Custom memberinfo like
public class LockedUserMessageXpMemberInfo : XPCustomMemberInfo {
string _theValue;
public LockedUserMessageXpMemberInfo(XPClassInfo owner)
: base(owner, "LockedUserMessage", typeof(string), null, true, false) {
}
public override object GetValue(object theObject) {
var typeInfo = XafTypesInfo.Instance.FindTypeInfo(theObject.GetType());
var memberValue = typeInfo.FindMember("LockedUser").GetValue(theObject);
if (_theValue != null) {
return memberValue != null ? string.Format(_theValue, memberValue) : null;
}
return null;
}
public override void SetValue(object theObject, object theValue) {
_theValue = theValue as string;
base.SetValue(theObject, theValue);
}
}
and register it on the system.
public override void CustomizeTypesInfo(DevExpress.ExpressApp.DC.ITypesInfo typesInfo) {
base.CustomizeTypesInfo(typesInfo);
var typeInfos = typesInfo.PersistentTypes.Where(info => info.FindAttribute<PessimisticLockingMessageAttribute>() != null);
foreach (var typeInfo in typeInfos) {
var memberInfo = typeInfo.FindMember("LockedUserMessage");
if (memberInfo == null) {
var xpClassInfo = XafTypesInfo.XpoTypeInfoSource.XPDictionary.GetClassInfo(typeInfo.Type);
var lockedUserMessageXpMemberInfo = new LockedUserMessageXpMemberInfo(xpClassInfo);
lockedUserMessageXpMemberInfo.AddAttribute(new BrowsableAttribute(false));
XafTypesInfo.Instance.RefreshInfo(typeInfo);
}
}
}
Conclusion
That was a long post but the result i believe is great. All the above are implemented in eXpand framework. Next time you want to use the pessimistic lock approach presented in this blog you only have to decorate your class with 3 attributes
[PessimisticLocking]
[Custom("ViewEditMode","View")]
[PessimisticLockingMessageAttribute("AnId")]
public class Client : BaseObject {
public Client(Session session)
: base(session) {
}
private string _name;
public string Name {
get {
return _name;
}
set {
SetPropertyValue("Name", ref _name, value);
}
}
}
Download expand from http://expandframework.com/downloads/download.html and sent use your feedback or report any problems you find at our forums http://expandframework.com/forum.html
eXpand FeatureCenter implementation contains a demo of this bolg under the Miscallenous/Pessimistic locking navigation menu