11

I am developing an ASP.NET MVC 3 application using C# and Razor.

I have a search form that looks like this: searchform

The search form works in the following way:

  1. The user selects which property they want to search on.
  2. The user selects how they want to match the search string (e.g. contains, starts with, ends with, equals, etc).
  3. The user enters a search term and clicks Search.

The selections in the first drop down related directly to a property in my ADO.NET Entity Framework model class (and therefore directly to a table column).

Users need the ability to explicitly select which property and which matching method when searching, e.g. a user will explicitly search for all matches of process number that equals '132'.

My first approach was to use dynamic linq to construct a Where clause from the search criteria (see my original question). However I'm starting to think that this isn't the best way to do it.

I'm also hoping for a solution that doesn't require me to hard code the result for each property + matching criteria combination.

Any suggestions on how I should implement this search? It doesn't have to be using my current search form, totally open to any other ideas that fit the requirements.

Community
  • 1
  • 1
dnatoli
  • 6,972
  • 9
  • 57
  • 96
  • 7
    you know, this is soooo old fashion! In our application we will have a quick search as simple as Google Chrome or IE 9 address bar where you can either enter an url or search by text. No more need to specify the column name or conditions, just do a full text search on meaningful columns and combine results with other possible combinations, easier and smooth user experience, our users love this, after a first phase of "feeling lost" :D – Davide Piras Aug 29 '11 at 00:32
  • 3
    I'm not sure if that kind of search would provide the specificity that is needed by our users. They need to be able to explicitly specify whether they want contains, equals, etc., and I want to avoid forcing them to learn some string syntax to accomplish this. Feel free to put your suggestion in as an answer. – dnatoli Aug 29 '11 at 00:40
  • If you model the Property, the Operator and the Term as 3 distinct entities, I don't understand why the 'dynamic linq' option would force you to hardcode anything? You would build the expected Linq query - as a string - from information gathered from the 3 entities. For example, you can create a LinkOperation property to the Operator entity that will help you build this string. – Simon Mourier Sep 16 '11 at 15:53
  • Does your case statement switch on the type or name of the attribute? To my mind its the type of the attribute that is important and should effect both the validation of the clause fields and the contruction of the clause. However, couldn't you group thr types into about 4 cases, Numeric, Textual, Datetime and everything else? – Jodrell Sep 19 '11 at 08:29

7 Answers7

12

Have you looked into using Lucene.NET for this project? given the nature of your searches it would be very simple to build that using Lucene, as it allows you to combine filters on different columns just like your requirements

BlackTigerX
  • 6,006
  • 7
  • 38
  • 48
  • Is Lucene not a document searching/indexing library? My searches aren't searches on pages/content in pages, they are searches on an Entity Framework model. – dnatoli Aug 29 '11 at 03:59
  • Lucene is good for full-text indexing. But SQL Server also has a full text indexing service. I should also point out that a full-text search is only one type of search. – Eric Falsken Sep 15 '11 at 02:37
  • Exactly. And I'm not sure how full text indexing would help me in this case when I'm not just doing a blanket "find me everything in my database that contains this value" kind of search. – dnatoli Sep 15 '11 at 23:57
  • 1
    Could someone explain to me why this is getting so many upvotes even though I can't see how it would work with my style of search and seems like a lot of overkill for one search screen? Obviously I'm missing something... – dnatoli Sep 21 '11 at 07:12
  • This answer is getting so many up votes because Lucene supports the "google-esque" searching style. So many apps now support the single search box that it's what users are beginning to get comfortable with (in general). You've mentioned that Lucene looks like overkill, but Lucene is already doing the heavy lifting, you'd just have to index the data you wanted. – Rikon Sep 21 '11 at 19:30
  • @Rikon - I don't see how in my case, a "google-esque" searching style is appropriate. Also, what I meant by overkill was is it worth the effort of fitting Lucene into my project just for one simple search screen? – dnatoli Sep 21 '11 at 23:45
  • Yeah, I assumed that you didn't want the one text box search, but I was just pointing out why everyone was upvoting it... It is the hotness of the searching feature set these days. Doing a Lucene.net "Hello World" should be about a 2 hour experiment. If you're curious, you can check out the Linq to Lucene project: http://linqtolucene.codeplex.com/. Hopefully within a little "Hello World" time box you can decide if it's overkill... Obviously the vast majority of the people on this answer don't think so. See next comment for one more idea... – Rikon Sep 22 '11 at 00:29
  • @link664: If you went the Lucene route, you could make the whole backend a lucene infrastructure and then "skin" your site with a "Basic" mode and an "Advanced" mode. The advanced mode would simply be an open text box where lucene syntax could be entered directly. The Basic mode would be the series of drop down lists you originally propsed that would piece together the lucene query for a user not comfortable with the lucene syntax. Then you actually get both options... Note that with this option you could create and store complex queries as strings and let the user re-run their search later... – Rikon Sep 22 '11 at 00:34
4

You can build expression tree for where predicate using code. For example,

public static IQueryable<T> DynamicWhere<T>(this IQueryable<T> src, string propertyName, string value)
{
    var pe = Expression.Parameter(typeof(T), "t");
    var left = Expression.Property(pe, typeof(T).GetProperty(propertyName));
    var right = Expression.Constant(value);
    // Illustrated a equality condition but you can put a switch based on some parameter
    // to have different operators
    var condition = Expression.Equal(left, right);

    var predicate = Expression.Lambda<Func<T, bool>>(condition, pe);
    return src.Where(predicate);
}

Use it as Orders.DynamicWhere(searchBy, searchValue). You can add one more parameter to accept the operator such as Equals, Greater Than etc to complete the function.

See these links for more info:

http://msdn.microsoft.com/en-us/library/bb882637.aspx

http://msdn.microsoft.com/en-us/library/bb397951.aspx

Also check list of methods on the Expression class to get an idea.

VinayC
  • 47,395
  • 5
  • 59
  • 72
  • This seems like a great approach. Thanks. Unfortunately, in my case I need to actually compare a property in a related table to `T`. (The other table has an FK to my `T` table but I'm only interested in one row if there are accidentally more.) After spending a bunch of time on this, I cannot see how I can modify this to support a property in a related table. – Jonathan Wood Apr 18 '14 at 21:17
1

You can use Dynamic Linq and you can create the Where clausole with a utility class like this:

public class Criteria 
{
   StringBuilder sb = new StringBuilder();
   bool first = true;

   public void And(string property, string dbOperator, string value) {
       if (first)
       {
           sb.Append(" ").Append(property).Append(" ");
           sb.Append(" ").Append(dbOperator).Append(" ");
           sb.Append(" ").Append(value).Append(" ");
           first = false;
       }
       else
       {
           sb.Append(" && ").Append(property).Append(" ");
           sb.Append(" ").Append(dbOperator).Append(" ");
           sb.Append(" ").Append(value).Append(" ");
       }
   }

   public void Or(string property, string dbOperator, string value)
   {
       if (first)
       {
           sb.Append(" ").Append(property).Append(" ");
           sb.Append(" ").Append(dbOperator).Append(" ");
           sb.Append(" ").Append(value).Append(" ");
           first = false;
       }
       else
       {
           sb.Append(" || ").Append(property).Append(" ");
           sb.Append(" ").Append(dbOperator).Append(" ");
           sb.Append(" ").Append(value).Append(" ");
       }
   }

   public string ToString() 
   {
       return sb.ToString();
   }

}

So you can build a Criteria with many properties using Or or And methods and put it in the Where operator of Dynamic Linq.

Massimo Zerbini
  • 3,125
  • 22
  • 22
0

create method and call it on button click demo below

public List gettaskssdata(int c, int userid, string a, string StartDate, string EndDate, string ProjectID, string statusid) {

        List<tbltask> tbtask = new List<tbltask>();


        var selectproject = entity.tbluserprojects.Where(x => x.user_id == userid).Select(x => x.Projectid);

        if (statusid != "" && ProjectID != "" && a != "" && StartDate != "" && EndDate != "")
        {
            int pid = Convert.ToInt32(ProjectID);
            int sid = Convert.ToInt32(statusid);
            DateTime sdate = Convert.ToDateTime(StartDate).Date;
            DateTime edate = Convert.ToDateTime(EndDate).Date;
            tbtask = entity.tbltasks.Include(x => x.tblproject).Include(x => x.tbUser).Where(x => selectproject.Contains(x.ProjectId) && (x.tblproject.company_id == c) && (x.tblproject.ProjectId == pid) && (x.tblstatu.StatusId == sid) && (x.TaskName.Contains(a) || x.tbUser.User_name.Contains(a)) && (x.StartDate >= sdate && x.EndDate <= edate)).OrderByDescending(x => x.ProjectId).ToList();
        }
        else if (statusid == "" && ProjectID != "" && a != "" && StartDate != "" && EndDate != "")
        {
            int pid = Convert.ToInt32(ProjectID);
            DateTime sdate = Convert.ToDateTime(StartDate).Date;
            DateTime edate = Convert.ToDateTime(EndDate).Date;
            tbtask = entity.tbltasks.Include(x => x.tblproject).Include(x => x.tbUser).Where(x => selectproject.Contains(x.ProjectId) && (x.tblproject.company_id == c) && (x.tblproject.ProjectId == pid) && (x.TaskName.Contains(a) || x.tbUser.User_name.Contains(a)) && (x.StartDate >= sdate && x.EndDate <= edate)).OrderByDescending(x => x.ProjectId).ToList();
        }
        else if (ProjectID == "" && statusid != "" && a != "" && StartDate != "" && EndDate != "")
        {
            int sid = Convert.ToInt32(statusid);
            DateTime sdate = Convert.ToDateTime(StartDate).Date;
            DateTime edate = Convert.ToDateTime(EndDate).Date;
            tbtask = entity.tbltasks.Include(x => x.tblproject).Include(x => x.tbUser).Where(x => selectproject.Contains(x.ProjectId) && (x.tblproject.company_id == c) && (x.tblstatu.StatusId == sid) && (x.TaskName.Contains(a) || x.tbUser.User_name.Contains(a)) && (x.StartDate >= sdate && x.EndDate <= edate)).OrderByDescending(x => x.ProjectId).ToList();
        }
        else if(ProjectID!="" && StartDate == "" && EndDate == "" && statusid == ""  && a == "")
        {
            int pid = Convert.ToInt32(ProjectID);
            tbtask = entity.tbltasks.Include(x => x.tblproject).Include(x => x.tbUser).Where(x => selectproject.Contains(x.ProjectId) && (x.tblproject.company_id == c) && (x.tblproject.ProjectId == pid)).OrderByDescending(x => x.ProjectId).ToList();

        }
        else if(statusid!="" && ProjectID=="" && StartDate == "" && EndDate == ""  && a == "")
        {
            int sid = Convert.ToInt32(statusid);
            tbtask = entity.tbltasks.Include(x => x.tblproject).Include(x => x.tbUser).Where(x => selectproject.Contains(x.ProjectId) && (x.tblproject.company_id == c) && (x.tblstatu.StatusId == sid) ).OrderByDescending(x => x.ProjectId).ToList();
        }
        else if (a == "" && StartDate != "" && EndDate != "" && ProjectID != "")
        {
            int pid = Convert.ToInt32(ProjectID);
            DateTime sdate = Convert.ToDateTime(StartDate).Date;
            DateTime edate = Convert.ToDateTime(EndDate).Date;
            tbtask = entity.tbltasks.Include(x => x.tblproject).Include(x => x.tbUser).Where(x => selectproject.Contains(x.ProjectId) && (x.tblproject.ProjectId == pid) && (x.StartDate >= sdate && x.EndDate <= edate)).OrderByDescending(x => x.ProjectId).ToList();

        }
        else if (StartDate == "" && EndDate == "" && statusid != "" && ProjectID != "" && a != "")
        {
            int pid = Convert.ToInt32(ProjectID);
            int sid = Convert.ToInt32(statusid);
            tbtask = entity.tbltasks.Include(x => x.tblproject).Include(x => x.tbUser).Where(x => selectproject.Contains(x.ProjectId) && (x.tblproject.company_id == c) && (x.tblproject.ProjectId == pid) && (x.tblstatu.StatusId == sid) && (x.TaskName.Contains(a) || x.tbUser.User_name.Contains(a))).OrderByDescending(x => x.ProjectId).ToList();
        }
        else if (a == "" && StartDate == "" && EndDate == "" && ProjectID != "" && statusid != "")
        {
            int pid = Convert.ToInt32(ProjectID);
            int sid = Convert.ToInt32(statusid);
            tbtask = entity.tbltasks.Include(x => x.tblproject).Include(x => x.tbUser).Include(x => x.tblstatu).Where(x => selectproject.Contains(x.ProjectId) && x.tblproject.company_id == c && x.tblproject.ProjectId == pid && x.tblstatu.StatusId == sid).OrderByDescending(x => x.ProjectId).ToList();
        }
        else if (a != "" && StartDate == "" && EndDate == "" && ProjectID == "" && statusid == "")
        {
            tbtask = entity.tbltasks.Include(x => x.tblproject).Include(x => x.tbUser).Where(x => selectproject.Contains(x.ProjectId) && (x.tblproject.company_id == c) && (x.TaskName.Contains(a) || x.tbUser.User_name.Contains(a))).OrderByDescending(x => x.ProjectId).ToList();

        }
        else if (a != "" && ProjectID != "" && StartDate == "" && EndDate == "" && statusid == "")
        {
            int pid = Convert.ToInt32(ProjectID);
            tbtask = entity.tbltasks.Include(x => x.tblproject).Include(x => x.tbUser).Where(x => selectproject.Contains(x.ProjectId) && (x.tblproject.company_id == c) && (x.tblproject.ProjectId == pid) && (x.TaskName.Contains(a) || x.tbUser.User_name.Contains(a))).OrderByDescending(x => x.ProjectId).ToList();
        }
        else if (a != "" && StartDate != "" && EndDate != "" && ProjectID == "" && statusid == "")
        {
            DateTime sdate = Convert.ToDateTime(StartDate).Date;
            DateTime edate = Convert.ToDateTime(EndDate).Date;
            tbtask = entity.tbltasks.Include(x => x.tblproject).Include(x => x.tbUser).Where(x => selectproject.Contains(x.ProjectId) && (x.tblproject.company_id == c) && (x.TaskName.Contains(a) || x.tbUser.User_name.Contains(a)) && (x.StartDate >= sdate && x.EndDate <= edate)).OrderByDescending(x => x.ProjectId).ToList();
        }
        else
        {
            tbtask = entity.tbltasks.Include(x => x.tblproject).Include(x => x.tbUser).Where(x => selectproject.Contains(x.ProjectId) && x.tblproject.company_id == c).OrderByDescending(x => x.ProjectId).ToList();
        }
        return tbtask;

    }
Muhafil Saiyed
  • 230
  • 1
  • 20
0

We started out resolving similar queries against our Entity Framework model using dynamic linq queries. However, our attempts to generalize query generation resulted in bad performance due to EF being confused by the resulting complex expressions, so in the end horrible SQL was produced.

We resorted to Entity SQL.

0

Not sure if you are using MS SQL. Seems SQL could do most of the work for you, and you can build dynamic queries. Obviously the select/from statement needs work, but you can get the idea from the where clause.

DECLARE @SEARCHTYPE VARCHAR(20)
DECLARE @SEARCHTERM VARCHAR(100)

SELECT
    [FIELDS]
FROM
    [TABLE]
WHERE
    (@SEARCHTYPE = 'BEGINSWITH' AND [FIELD] LIKE @SEARCHTERM + '%') OR
    (@SEARCHTYPE = 'ENDSWITH' AND [FIELD] LIKE '%' + @SEARCHTERM) OR
    (@SEARCHTYPE = 'EQUALS' AND [FIELD] = @SEARCHTERM)
Michael C. Gates
  • 982
  • 1
  • 6
  • 18
0

You could have the first combo data source set to myEntityObject.GetType().GetProperties(), the second to a list of displayable Funcs<string, string, bool>, like this:

public class ComboPredicate
{
    public Func<string, string, bool> Func {get; set;}
    public string Name {get; set; }
}

Later, when you load the form:

comboProperty.Datasource = myEntityObject.GetType().GetProperties()
comboOperation.Datasource = new List<Predicate>
    {
        {
            Name = "Contains",
            Predicate = (s1, s2) => s1 != null && s1.Contains(s2),
        },
        {
            Name = "Equals",
            Predicate = (s1, s2) => string.Compare(s1, s2) == 0,
        },
        //...
    }

And later, when you want to select your entities:

var propertyInfo = (PropertyInfo)comboProperty.SelectedValue;
var predicate = ((ComboPredicate)comboOperation.SelectedValue).Predicate;
var filteredObjects = objects.Where(o => predicate(propertyInfo.GetValue(o, null).ToString(), textBoxValue.Text));
Evren Kuzucuoglu
  • 3,781
  • 28
  • 51