Search code examples
c#nhibernatequery-by-example

NHibernate QueryByExample including only certain properties


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.


Solution

  • 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);
            }