A simple pattern for complex filter criteria

A simple pattern for complex filter criteria

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


RobKraft posted on Friday, August 24, 2007

I am hoping to create a pattern that I can use in all my business objects for passing criteria through the data portal.  Many of my classes need to allow data to be retrieved using different where clauses.  I am submitting the idea I came up with because I hope someone can point out any major flaws with it before I implement it in hundreds of classes.

First, I am creating 2 common criteria classes that can be used by all my business classes.  Doing so means that I don't have to define any criteria classes in any of my business objects.  The first class inherits from HashTable, and the second looks similar to many example CSLA criteria classes.

using System;
using System.Collections.Generic;
using System.Text;

namespace MyCompany
{
    [Serializable()]
    public class CriteriaHash : System.Collections.Hashtable
    {
        public CriteriaHash()
        {
        }
        public CriteriaHash(CriteriaItems[] filters)
        {
            foreach (CriteriaItems f in filters)
            {
                this.Add(f.ParmName, f.value);
            }
        }
    }
    [Serializable()]
    public class CriteriaItems
    {
        private object _value = null;
        private string _parmName = null;
        public object value { get { return _value; } }
        public string ParmName { get { return _parmName; } }
        public CriteriaItems(string parmName, object value)
        {
            _parmName = parmName;
            _value = value;
        }
    }
}
And here is an example of the usage:
using System;
using System.Data;
using System.Data.SqlClient;
using Csla;
using Csla.Data;

namespace MyNamespace
{
    [Serializable()]
    public class ProductLookupList : ReadOnlyListBase<ProductLookupList, ProductLookup>
    {
        public static ProductLookupList GetProductsAll()
        {
            return DataPortal.Fetch<SupervisorLookupList>(new CriteriaHash());
        }
        public static ProductLookupList GetProductsByDept(string Dept)
        {
            return DataPortal.Fetch<ProductLookupList>(new CriteriaHash(new CriteriaItems[1] { new CriteriaItems("DEPT", Dept) }));
        }
        public static ProductLookupList GetProductsByVendor(string Vendor)
        {
            return DataPortal.Fetch<ProductLookupList>(new CriteriaHash(new CriteriaItems[1] { new CriteriaItems("VENDOR", Vendor) }));
        }
        public static ProductLookupList GetProductsByVendorAndDept(string Vendor, string Dept)
        {
            return DataPortal.Fetch<ProductLookupList>(new CriteriaHash(new CriteriaItems[2] { new CriteriaItems("VENDOR", Vendor), new CriteriaItems("DEPT", Dept) }));
        }

        internal ProductLookupList(){}

        private void DataPortal_Fetch(CriteriaHash criteria)
        {
            string spName = "sp_GetAll";
            if (criteria["DEPT"] != null)
            {
                spName = "sp_GetByDept";
            }
            if (criteria["VENDOR"] != null)
            {
                spName = "sp_GetByVend";
                if (criteria["DEPT"] != null)
                {
                    spName = "sp_GetByVendAndDept";
                }
            }
            db.CreateCommand(spName);
            ...
        }
    }
}

The benefits of this approach is that I don't need to write
the Criteria or FilteredCriteria classes in each business class
(and it is a simpler pattern for code generation).
The call to DataPortal.Fetch is a little bit uglier than most CSLA examples,
and the size of the serialized object is about 150 bytes larger;
but I think the tradeoff looks worth it to me in order to have the simplified pattern.
What do you think?
Thanks in advance for your feedback!

RockfordLhotka replied on Friday, August 24, 2007

Conceptually that sort of thing should be fine - but you can't inherit from Hashtable. You need to inherit from CriteriaBase to make a reusable criteria.

Remember that the criteria object must provide two types of information to the server-side data portal:

  1. The type of the business object you are looking for
  2. The criteria data needed for that object to load itself with data

The first bit of information comes either because the criteria class is directly nested within the business class (as shown in the book), or because it inherits from CriteriaBase and you explicitly provide the Type object in the constructor - which is what you need to do to create a reusable criteria class.

The second bit of information could be strongly typed (as shown in the book), or could be a Dictionary<string, object> or Hashtable or any other data structure that is serializable. Remember that some data structures serialize to be larger or smaller than others, and complex collections can be on the larger side (more metadata). Typically that's not an issue, but it is something of which you should be aware.

RobKraft replied on Friday, August 24, 2007

Thanks for the quick feedback and for steering me in the right direction!

Based on that, I will inherit from CriteriaBase like this:

    [Serializable()]
    public class CriteriaHash : Csla.CriteriaBase
    {
        private System.Collections.Hashtable _hashTable = new System.Collections.Hashtable();
        public System.Collections.Hashtable HashTable
        {
            get { return _hashTable; } 
        }
        public CriteriaHash(Type type): base(type)
        {
        }
        public CriteriaHash(Type type, CriteriaItems[] filters) : base(type)
        {
            foreach (CriteriaItems f in filters)
            {
                _hashTable.Add(f.ParmName, f.value);
            }
        }
    }

And then I will do the following in the businesss classes:

        public static ProductLookupList GetProductsAll()
        {
            return DataPortal.Fetch<ProductLookupList>(new CriteriaHash(Type.GetType("MyCompany.ProductLookupList")));
        }
        public static ProductLookupList GetProductsByDept(string Dept)
        {
            return DataPortal.Fetch<ProductLookupList>(new CriteriaHash(Type.GetType("MyCompany.ProductLookupList"), new CriteriaItems[1] { new CriteriaItems("DEPT", Dept) }));
        }

This appears to function as I desire; but I have yet to test it with .Net Remoting.

 

 

 

juddaman replied on Friday, August 24, 2007

Just a thought on helping to tidy up the IF statement section. Maybe you could just pass the SP name as part of the CriteriaHash object i.e. a property in the CriteriaHash. Would change this section of code to something like:
	
public static ProductLookupList GetProductsAll()
{
return DataPortal.Fetch<ProductLookupList>(new CriteriaHash(Type.GetType("MyCompany.ProductLookupList")), "sp_GetAll");
}

internal ProductLookupList(){}

private void DataPortal_Fetch(CriteriaHash criteria)
{
db.CreateCommand(criteria.SPName);
....
}

OK it's a small change :-) but if you had loads of different filter types it might be worth it?

Regards

George

RockfordLhotka replied on Friday, August 24, 2007

Putting on my “architectural purity” hat now :)

 

The problem with passing in the sproc name is that this forces the UI, or at least the business layer, to know about sprocs and their names.

 

The knowledge of the very _existence_ of sprocs belongs at the data access layer and below. Business layer and UI layer code should, at most, hear about these “sproc things” as scary myths. As the boogeyman that should be avoided.

 

OK, been reading too much Harry Potter and got overly dramatic, sorry…

 

But from a purity standpoint, you really don’t want to pass the sproc name.

 

However, you are OK if you abstract the concept. In other words, it is OK for the business layer to pass in a “query name” parameter that is used to index into the right sproc or DAL component.

 

And the pragmatist in me says that _usually_ the query name might actually _be_ the sproc name. But it might _not_ too – and that’s the important thing to remember from an architectural perspective.

 

Rocky

juddaman replied on Friday, August 24, 2007

Hehe I thought this may be raised, and I totally agree from a purity point it is a bit naff. However, it would make the code more readable and maintainble (less IF statements!!). You wouldn't need to change the code in the dataportal method if you added a new filter requirement, which is pretty neat. (Here I'm thinking a 'for loop' cycling through the keys in the hash and adding an in param to the db command.) Suppose you could just call them 'query name' and 'param name' and wack a "@" before all the param names (in the DAL) and keep both your hats happy hehe. I guess it depends if more filters will be needed and how many. Maybe the bogeyman will come out to play?

George

Copyright (c) Marimer LLC