Generic NoDuplicates rule

Generic NoDuplicates rule

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


tiago posted on Tuesday, July 05, 2011

I have a collection of objects with a Name property. Each object must have a unique Name, and no I don't want to pass this business rule to the database, I want toi hanfle it before it hits the database,

I wrote this lovely rule

/// <summary>
/// Validation rule for checking a name property is unique at the collection level.
/// </summary>
private class NoDuplicates : CommonBusinessRule
{
    /// <summary>
    /// Initializes a new instance of the <see cref="NoDuplicates"/> class.
    /// </summary>
    /// <param name="primaryProperty">Primary property for this rule.</param>
    public NoDuplicates(IPropertyInfo primaryProperty)
        : base(primaryProperty)
    {
        InputProperties = new List<IPropertyInfo> { primaryProperty };
    }
 
    /// <summary>
    /// Initializes a new instance of the <see cref="NoDuplicates"/> class.
    /// </summary>
    /// <param name="primaryProperty">Primary property for this rule.</param>
    /// <param name="errorMessageDelegate">The error message function.</param>
    public NoDuplicates(IPropertyInfo primaryProperty, string errorMessageDelegate)
        : this(primaryProperty)
    {
        ErrorMessageDelegate = () => errorMessageDelegate;
    }
 
    /// <summary>
    /// Gets the error message.
    /// </summary>
    /// <value></value>
    protected override string ErrorMessage
    {
        get
        {
            return HasErrorMessageDelegate ? base.ErrorMessage : "{0} must be unique.";
        }
    }
            
    /// <summary>
    /// Validation rule implementation.
    /// </summary>
    /// <param name="context">Rule context object.</param>
    protected override void Execute(RuleContext context)
    {
        var value = context.InputPropertyValues[PrimaryPropertyas string;
        if (value == null)
            return;
 
        var target = (DocClassEdit)context.Target;
        var parent = (DocClassEditColl)target.Parent;
        if (parent != null)
        {
            foreach (var item in parent)
            {
                if (value.ToUpperInvariant() == ((string)item.ReadProperty(PrimaryProperty)).ToUpperInvariant() &&
                    !(ReferenceEquals(item, target)))
                {
                    context.AddErrorResult(string.Format(ErrorMessageInputProperties[0].FriendlyName));
                    return;
                }
            }
        }
    }
}

and now I want to amke it generic. For that purpose I just need to get rid of the castings to DocClassEdit and to DocClassEditColl. Any help?

RockfordLhotka replied on Tuesday, July 05, 2011

I don't think you can eliminate the cast operations, because RuleContext isn't generic.

But your rule class can be generic, and so you can do the cast operations to generic types.

public class MyGenericRule<X, Y> : BusinessRule
{
  protected override void Execute(RuleContext context)
  {
    var tmp1 = (X)context.Target;
   ...
  }
}

Or something along that line anyway.

tiago replied on Tuesday, July 05, 2011

dynamic and reflection

using System.Collections.Generic;
using Csla.Core;
using Csla.Rules;
 
namespace DocStore.Library.Rules
{
    /// <summary>
    /// Validation rule for checking a name property is unique at the collection level.
    /// </summary>
    public class NoDuplicates : CommonBusinessRule
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="NoDuplicates"/> class.
        /// </summary>
        /// <param name="primaryProperty">Primary property for this rule.</param>
        public NoDuplicates(IPropertyInfo primaryProperty)
            : base(primaryProperty)
        {
            if (ErrorMessageDelegate == null)
                ErrorMessageDelegate = () => "{0} must be unique.";
 
            InputProperties = new List<IPropertyInfo> {primaryProperty};
        }
 
        /// <summary>
        /// Initializes a new instance of the <see cref="NoDuplicates"/> class.
        /// </summary>
        /// <param name="primaryProperty">Primary property for this rule.</param>
        /// <param name="errorMessageDelegate">The error message function.</param>
        public NoDuplicates(IPropertyInfo primaryProperty, string errorMessageDelegate)
            : this(primaryProperty)
        {
            ErrorMessageDelegate = () => errorMessageDelegate;
        }
 
        /// <summary>
        /// Validation rule implementation.
        /// </summary>
        /// <param name="context">Rule context object.</param>
        protected override void Execute(RuleContext context)
        {
            var value = context.InputPropertyValues[PrimaryPropertyas string;
            if (value == null)
                return;
 
            dynamic target = context.Target;
            var parent = target.Parent;
            if (parent != null)
            {
                foreach (var item in parent)
                {
                    System.Reflection.PropertyInfo compare = item.GetType().GetProperty(PrimaryProperty.Name);
                    if (value.ToUpperInvariant() == compare.GetValue(itemnull).ToUpperInvariant() &&
                        !(ReferenceEquals(item, target)))
                    {
                        context.AddErrorResult(string.Format(ErrorMessagePrimaryProperty.FriendlyName));
                        return;
                    }
                }
            }
        }
    }
}

I admit that might be some easier way.

Jonny, this can go to CslaContrib

bwebber replied on Thursday, October 20, 2011

I have written a similar rule, except it can take multiple properties (and its in VB).  I used the generic approach that Rocky suggested.

I have also implemented a unique in DB rule that must be used together with this rule when working with subsets of complete DB lists.

My biggest problem with the non DB rule is that the user can fix the duplication in ways that will not unbreak the rule.

E.G. 

Scenario:

List has 5 items in it.  A new item is added and properties set so that Item 1 and 6 are the duplicate records, but errors only show on item 6.

> Fixing item 1 will not unbreak rule on item 6

  > one could possibly get around this by somehow invoking the rule again on item 6

> Deleting item 1 will not unbreak the rule on item 6

  > I have no idea how to get around this one, except possibly handling the remove in the list and then invoking any Unique rules again on each object

Have any of you clever people found a solution to this problem.  Now that our rules are implemented in classes (I've only recently upgraded from 2.1), would it be a bad idea to have the rule object actually keep track of the duplicate items in its own lists so that it can invoke rules on those objects?

At the moment I'm just hoping my users don't bypass my rules using these methods, but if they do, at least my database will pick it up...

StefanCop replied on Thursday, October 20, 2011

Hi bwebber

Your scenario might be solved using the "Executing Rules in Parent Objects" approach described in the ebook (Creating Business Objects).

If one of the child items changes, you want to re-run the rule for every child.

"But you also need to make sure the rule is executed each time a child object is changed, and to do that you’ll need to override the OnChildChanged method in the parent business class."

protected override void OnChildChanged(Csla.Core.ChildChangedEventArgs e)
{
    base.OnChildChanged(e);
    BusinessRules.CheckRules(SalesOrderEdit.LineItemsProperty);

}

The sample in the ebook added the actual rule in the parent on the Child-List (LineItemsProperty).

I guess, if you want your rule on the child object, the child object has to provide some method to trigger the rule from outside (i.e. CheckRulesForNameProperty() ).

bwebber replied on Friday, October 21, 2011

Thank you for your response.

Yes, I have also implemented a ChildListUnique rule which works perfectly to validate uniqueness in child lists and breaking a rule on the parent when duplicates exist.

I have 3 rule classes that assist me with validating unique rules:

The problem is validating root lists, where I normally use a combination of UniqueInCachedList and UniqueInDBRule (priorities 0 and 1), but the user can trick my rule checking using the methods explained.

As mentioned I am considering adding functionality to the UniqueInCachedList rule class to keep track of the duplicate items, so that it can invoke the rule on these items any time 1 of them are changed (using the method StefanCop explains - or possibly using reflection), but this would complicate the rule class and slow it down.

I am happy to post my VB code if anyone else is interested.

StefanCop replied on Thursday, October 20, 2011

Oh yes, 'dynamic' seems another option to write common rules.
Thanks for your post!

Instead of getting the value through reflection, you could also use ReadProperty()

        foreach (var item in parent)
        {
            string compare = (string) ReadProperty(item, PrimaryProperty);
            if (string.Equals(value, compare, StringComparison.InvariantCultureIgnoreCase) &&
                !(ReferenceEquals(item, target)))
            {
                context.AddErrorResult(string.Format(ErrorMessagePrimaryProperty.FriendlyName));
                return;
            }
         }

Btw, whenever possible I use a constructor like this:
public NoDuplicates(Csla.Core.PropertyInfo<string> primaryProperty)

to express that I expect a property of type string only.

decius replied on Friday, December 16, 2011

Tiago Freitas Leal

I admit that might be some easier way.

 

I realize this post is old, but I was in search of something and saw this. Below might be a simpler approach

 

 

public class UniqueRule<T, C> : Csla.Rules.BusinessRule

where T : Csla.BusinessListBase<T, C>

where C : Csla.BusinessBase<C>

{

public IPropertyInfo KeyProperty { get; set; }

public Func<C, string> UniqueValueFunc { get; set; }

 

public UniqueRule(IPropertyInfo primaryProperty, Func<C, string> uniqueValueFunc)

: base(primaryProperty)

{

UniqueValueFunc = uniqueValueFunc;

}

 

protected override void Execute(RuleContext context)

{

var t = context.Target as C;

if (((T)t.Parent).Count(x => UniqueValueFunc(x) == UniqueValueFunc(t)) > 1)

{

context.AddErrorResult(string.Format("{0} keys must be unique.", typeof(C).Name));

}

}

}

 

EDIT: Actually, I think there might be a serialization issue with the Func<C,string> property. Still, that could be resolved if that was an issue.

 

Copyright (c) Marimer LLC