Async Rule causes Can not change a read-only list or collection error

Async Rule causes Can not change a read-only list or collection error

Old forum URL: forums.lhotka.net/forums/t/10685.aspx


Russ posted on Wednesday, September 14, 2011

I am getting the “Can not change a read-only list or collection” error on the base.DataPortal_Create(); line of the following method.

[RunLocal]

protected void DataPortal_Create(int userApplicationFK)

{

  using (BypassPropertyChecks)

  {

    UserApplicationPK = userApplicationFK;

    PasswordRules = BuildPasswordRulesText();

    AcceptablePasswordCharacters = BuildAcceptablePwdCharsText();

  }

  base.DataPortal_Create();

}

 

Here is the stack trace:

   at Csla.Core.ReadOnlyObservableBindingList`1.<.ctor>b__0(Object o, NotifyCollectionChangedEventArgs e)

   at System.Collections.ObjectModel.ObservableCollection`1.OnCollectionChanged(NotifyCollectionChangedEventArgs e)

   at Csla.Core.ObservableBindingList`1.OnCollectionChanged(NotifyCollectionChangedEventArgs e)

   at System.Collections.ObjectModel.ObservableCollection`1.RemoveItem(Int32 index)

   at Csla.Core.ObservableBindingList`1.RemoveItem(Int32 index)

   at System.Collections.ObjectModel.Collection`1.Remove(T item)

   at Csla.Rules.BrokenRulesCollection.ClearRules(IPropertyInfo property)

   at Csla.Rules.BusinessRules.CheckRulesForProperty(IPropertyInfo property, Boolean cascade)

   at Csla.Rules.BusinessRules.CheckRulesForProperty(IPropertyInfo property, Boolean cascade)

   at Csla.Rules.BusinessRules.CheckRules(IPropertyInfo property)

   at Csla.Rules.BusinessRules.CheckRules()

   at Csla.Core.BusinessBase.DataPortal_Create()

   at Library.Security.ChangePasswordER.DataPortal_Create(Int32 userApplicationFK) in C:\Code\Test\Test\Admin\Library.Net\Security\ChangePasswordER.cs:line 137

   at lambda_method(Closure , Object , Object[] )

   at Csla.Reflection.MethodCaller.CallMethod(Object obj, DynamicMethodHandle methodHandle, Object[] parameters)

 

I have determined that this is caused by the use of an async rule (MinPwdLength) on the NewPasswordProperty in combination with a sync rule (NotEqual) and a dependency against the OldPasswordProperty that forces the rules for the NewPasswordProperty to be run.  Here is the code:

// new must not equal old

BusinessRules.AddRule(new Carus.CSLA.CommonRules.NotEqual(NewPasswordProperty, OldPasswordProperty));

BusinessRules.AddRule(new Csla.Rules.CommonRules.Dependency(OldPasswordProperty, NewPasswordProperty));

 

// new must be >= min length

BusinessRules.AddRule(new Carus.CSLA.CommonRules.MinPwdLength(NewPasswordProperty));

 

The issue is the MinPwdLength rule Execute() method is called twice in rapid succession because of the dependency. I suspect the execute method is not thread safe. If I comment the MinPwdLength addrule line then the error goes away. The error also goes away if I comment the Dependency addrule line.

The MinPwdRule rule is very simple. Here is the code:

/// <summary>

/// Initializes a new instance of the <see cref="MinPwdLength"/> class.

/// </summary>

/// <param name="primaryProperty">The primary property.</param>

public MinPwdLength(IPropertyInfo primaryProperty)

  : base(primaryProperty)

{

  IsAsync = true;

  PrimaryProperty = primaryProperty;

  InputProperties = new List<IPropertyInfo> { primaryProperty };

}

 

/// <summary>

/// Executes the rule in specified context.

/// </summary>

/// <param name="context">The context.</param>

protected override void Execute(RuleContext context)

{

  System.Diagnostics.Debug.WriteLine("MinPwdLength.Execute");

  var bw = new System.ComponentModel.BackgroundWorker();

  bw.DoWork += (o, e) =>

  {

    string newPassword = context.InputPropertyValues[PrimaryProperty].ToString();

    int minPwdLength = 4;// int.Parse(GetSystemDefaultValue("MinPwdLength"));

    if (newPassword.Length < minPwdLength)

      context.AddErrorResult(string.Format("New password must be at least {0} characters long.", minPwdLength));

  };

  bw.RunWorkerCompleted += (o, e) =>

  {

    if (e.Error != null)

      context.AddErrorResult(e.Error.Message);

    context.Complete();

  };

  bw.RunWorkerAsync();

}

 

I suspect the first call to the execute method is locking the business object’s BrokenRules collection when it completes causing the second execution of the execute method to fail on the context.Complete(); line when it tries to write to the business object’s BrokenRulesCollection.

Does anyone know how to make an async rule thread safe?

JonnyBee replied on Wednesday, September 14, 2011

Hi Russ,

In general :

  1. Only use async rules for rules that must do a database lookup.  All other rules should just be synchronous rules.
  2. Always add async rules with a higher Priority than the syncronous rules
    (in general - if validion fails with sync rules do not execute async rules)

So for your new password field id probalby have:

  1. Required rule with default Priority (= 0)
  2. Dependency rule with default Priority (= 0)
  3. NotEqualwith Priority = 1
  4. MinPwdLength with Priority = 10

The result being that

- Required is executed first
   - If Required is not broke run NotEqual
      - If NotEqual is broken run the async rule as last rule

By default - all rules at Priority = 0 (actually rules at priority less or equal BusinessRules,ProcessThroughPriority that is default 0) is run unless a rule explicitly set StopProcessing to shortcut the rule engine.

Which version of Csla are you using?

Which UI technology do you use?

What is likely happening is that context.Complete methos is NOT thread safe and may ba called at almost the exact same time and when the forst one completes and sets the BrokenRuleCollection.IsReadOnly to false tha second rule is still trying to update the list (or clear the list for that property). However this should not happen as the BW.RunWorkerCompleted event is a callback to be executed on the UI Thread (in typical conditions) or are you using an async data portal call (factory method) as well for Create?

So, the execute method itself is OK but the callback on the BW is causing the problem.

For CSLA 4.2 the rule engine is updated so that you can restrict when the rule is allowed to run like:

Russ replied on Thursday, September 15, 2011

Thanks Jonny!  Using the rule priorities to stagger the rule execution worked!

I’m using CSLA 4.1 with Silverlight 4, bxf, MVVM.  I have modeled this application from the Rocky’s UsingCSLA4 Data Access EncapsultedInvokeDto sample.

I’m looking forward to the CSLA 4.2 and hoping it will not be too hard to upgrade my current project from CSLA 4.1.

Thanks again for the help.

Russ.

Copyright (c) Marimer LLC