1

I have created a custom Property Selector to accept an array in the constructor to say which properties should be included in the search. The approach works well as long as there are no component types, but how do I deal with those? Here is an example:

public class Customer
{
    public virtual int Id { get; private set; }
    public virtual Name Name { get; set; }
    public virtual bool isPreferred { get; set; }


    //...etc
}

public class Name
{
        public string Title { get; set; }
        public string Firstname { get; set; }
        public string Lastname { get; set; }
        public string Fullname { get; }
}


public class CustomerPropertySelector : Example.IPropertySelector
    {
        private string[] _propertiesToInclude = { };

        public CustomerPropertySelector(string[] propertiesToInclude)
        {
            this._propertiesToInclude = propertiesToInclude;
        }

        public bool Include(object propertyValue, String propertyName, NHibernate.Type.IType type)
        {
            //...Checking for null and zeros etc, excluded for brevity

            if (!_propertiesToInclude.Contains(propertyName))
                return false;

            return true;
        }
   }

I would like to be able to search by first name, but not necessarily last. The property name is Name however, so both first and last names seem to be part of the same property, and something like Name.Firstname, which would normally work as a criterion, doesn't seem to work here. What would be the best way around that?

EXAMPLE:

Customer exampleCust = new Customer(FirstName: "Owen");
IList<Customer> matchedCustomers = _custRepo.GetByExample(exampleCust, new string[] { "Name.FirstName" });

Given that there are 2 customers in db, only one named "Owen", but both have isPreferred = false, I would like my query to only return the first one. Standard QBE will return both based on isPreferred property.

SOLUTION:

Thank you for the answers, the solution is mostly based on answer by therealmitchconnors, however I couldn't have done it without Mark Perry's answer either.

The trick was to realise that rather than including Name.FirstName property, I actually want to exclude Name.LastName, since QBE only allows us to exclude properties. I used a method adapted from therealmitchconnors's answer to help me determine fully qualified names of properties. Here is the working code:

public IList<T> GetByExample(T exampleInstance, params string[] propertiesToInclude)
{
    ICriteria criteria = _session.CreateCriteria(typeof(T));
    Example example = Example.Create(exampleInstance);

    var props = typeof(T).GetProperties();
    foreach (var prop in props)
    {
        var childProperties = GetChildProperties(prop);
        foreach (var c in childProperties)
        {
            if (!propertiesToInclude.Contains(c))
                example.ExcludeProperty(c);
        }
    }
    criteria.Add(example);

    return criteria.List<T>();
}

private IEnumerable<string> GetChildProperties(System.Reflection.PropertyInfo property)
{
    var builtInTypes = new List<Type> { typeof(bool), typeof(byte), typeof(sbyte), typeof(char), 
        typeof(decimal), typeof(double), typeof(float), typeof(int), typeof(uint), typeof(long), 
        typeof(ulong), typeof(object), typeof(short), typeof(ushort), typeof(string), typeof(DateTime) };

    List<string> propertyNames = new List<string>();
    if (!builtInTypes.Contains(property.PropertyType) && !property.PropertyType.IsGenericType)
    {
        foreach (var subprop in property.PropertyType.GetProperties())
        {
            var childNames = GetChildProperties(subprop);
            propertyNames = propertyNames.Union(childNames.Select(r => property.Name + "." + r)).ToList();
        }
    }
    else
        propertyNames.Add(property.Name);

    return propertyNames;
}

I wasn't sure of the best way to determine whether a property was a component class or not, any suggestions on how to improve the code are very welcome.

Shagglez
  • 1,522
  • 3
  • 21
  • 38

2 Answers2

1

The following code would replace the logic you are using to populate propertiesToInclude. I changed it from an array to a list so I could use the Add method because I am lazy, but I think you get the picture. This does only work for one sub-level of properties. For n levels you would need to recurse.

        List<string> _propertiesToInclude = new List<string>();

        Type t;
        var props = t.GetProperties();
        foreach (var prop in props)
        {
            if (prop.PropertyType.IsClass)
                foreach (var subprop in prop.PropertyType.GetProperties())
                    _propertiesToInclude.Add(string.Format("{0}.{1}", prop.Name, subprop.Name));
            else
                _propertiesToInclude.Add(prop.Name);
        }
therealmitchconnors
  • 2,732
  • 1
  • 18
  • 36
  • I think you misunderstood the question slightly - the problem isn't deciding how to populate `_propertiesToInclude`, it's given that array how to tell QBE that I want to exclude `Name.LastName` from comparison, but **not** `Name.FirstName`. I think it might not be possible, because as far as QBE is concerned `Name` is a whole, I can either include it or not, but I can't partially include it. – Shagglez Jun 22 '11 at 18:19
  • So, you want `CustomerPropertySelector.Include(null, "Name.FirstName", IType.Something);` to return `true` and `CustomerPropertySelector.Include(null, "Name.LastName", IType.Something);` to return `false`? Based on what criteria? Couldn't you just hard code the method to exclude Name.LastName? I think I must be missing the point. – therealmitchconnors Jun 22 '11 at 18:46
  • I think so too, maybe I haven't phrased the question correctly. I want to create an example customer `new Customer(FirstName: "Owen")`, then pass this to my method with `propertiesToInclude: new string[] { "Name.FirstName" }`, which should then only compare objects based on that one field. – Shagglez Jun 23 '11 at 08:23
  • `typeof(string).IsClass` returns true, so this wouldn't work on classes with string properties. I can't think of a better way than to loop through and check all primitive types – Shagglez Jun 28 '11 at 13:34
1

I thought I had something but reading your question again you want to know why the QBE NHibernate code doesn't work with component properties.

I think you need to create a sub-criteria query for the Name part.

Perhaps something like this:

public IList<Customer> GetByExample(Customer customer, string[] propertiesToExclude){
    Example customerQuery = Example.Create(customer);
    Criteria nameCriteria = customerQuery.CreateCriteria<Name>();
    nameCriteria.Add(Example.create(customer.Name));
    propertiesToExclude.ForEach(x=> customerQuery.ExcludeProperty(x));
    propertiesToExclude.ForEach(x=> nameCriteria.ExcludeProperty(x));
    return customerQuery.list();
}

This is an example from the NHibernate Test Project, it shows how to exclude Component properties.

[Test]
public void TestExcludingQBE()
{
        using (ISession s = OpenSession())
        using (ITransaction t = s.BeginTransaction())
        {
            Componentizable master = GetMaster("hibernate", null, "ope%");
            ICriteria crit = s.CreateCriteria(typeof(Componentizable));
            Example ex = Example.Create(master).EnableLike()
                .ExcludeProperty("Component.SubComponent");
            crit.Add(ex);
            IList result = crit.List();
            Assert.IsNotNull(result);
            Assert.AreEqual(3, result.Count);

            master = GetMaster("hibernate", "ORM tool", "fake stuff");
            crit = s.CreateCriteria(typeof(Componentizable));
            ex = Example.Create(master).EnableLike()
                .ExcludeProperty("Component.SubComponent.SubName1");
            crit.Add(ex);
            result = crit.List();
            Assert.IsNotNull(result);
            Assert.AreEqual(1, result.Count);
            t.Commit();
        }
    }

Source code link

Mark Perry
  • 1,705
  • 10
  • 12
  • I think you hit the nail on the head with "QBE NHibernate code doesn't work with component properties". Is there a reference that could confirm this? – Shagglez Jun 28 '11 at 08:56
  • The issue is that QBE doesn't know to include/exclude the component part of the object, unless you specifically add the component criteria to the query. I have strayed away from using QBE in my application completely in favour of using the following API's (in preference order). Linq, QueryOver, Criteria, HQL. – Mark Perry Jun 28 '11 at 08:59
  • Further to my last comment I have found some code in the NHibernate Test Project which appears to show how to exclude Component Properties from the QBE queries. – Mark Perry Jun 28 '11 at 09:09
  • Aha! Thanks very much from the follow-up, this made me realise the answer. QBE does support component properties, I was just getting confused by how to do it. Unfortunately therealmitchconnors's answer is closer to truth, although without NHTest example I would have never figured it out. – Shagglez Jun 28 '11 at 13:36