3

I'm trying to implement method Find that searches the database.

I forgot to mention that I'm using Postgresql, so I can't use built in LINQ to SQL.

I want it to be like that:

var user = User.Find(a => a.LastName == "Brown");

Like it's done in List class. But when I go to List's source code (thanks, Reflector), I see this:

public T Find(Predicate<T> match)
{
    if (match == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
    }
    for (int i = 0; i < this._size; i++)
    {
        if (match(this._items[i]))
        {
            return this._items[i];
        }
    }
    return default(T);
}

How can I implement this thing? I need to get those parameters to make the search.

Solution

Okay, I understood now that I need to do LINQ to SQL to do all this good expressions stuff, otherwise I'd have to spend a lot of time reimplementeing the wheel.

Since I can't use LINQ to SQL, I implemented this easy method:

public static User Find(User match, string orderBy = "")
    {
        string query = "";
        if (!String.IsNullOrEmpty(match.FirstName)) query += "first_name='" + match.FirstName + "'";
        if (!String.IsNullOrEmpty(match.LastName)) query += "last_name='" + match.LastName+ "'";
        return Find(query + (!String.IsNullOrEmpty(orderBy) ? orderBy : ""));
    }

This is how to use it:

var user = User.Find(new User { FirstName = "Bob", LastName = "Brown" });
Alex
  • 34,581
  • 26
  • 91
  • 135

7 Answers7

6

Your method should accept Expression<Func<User>>.

This will give you expression tree instead of delegate which you can analyze and serialize to SQL or convert to any other API call your database have.

If you want everything to be generic, you may wish to go on with implementing IQueryable interface. Useful information can be found here: LINQ Tips: Implementing IQueryable Provider

Although for a simple scenario I would suggest not to complicate everything and stick with using Expression Trees and returning plain IEnumerable<T> or even List<T>.

For your case first version of code could look like this:

public IEnumerable<T> Get(Expression<Func<T, bool>> condition)
{
    if (condition.Body.NodeType == ExpressionType.Equal)
    {
        var equalityExpression = ((BinaryExpression)condition.Body);

        var column = ((MemberExpression)equalityExpression.Left).Member.Name;

        var value = ((ConstantExpression)equalityExpression.Right).Value;

        var table = typeof(T).Name;

        var sql = string.Format("select * from {0} where {1} = '{2}'", table, column, value);

        return ExecuteSelect(sql);
    }

    return Enumerable.Empty<T>();
}

And it's complexity grows fast when you want to handle new and new scenarios so make sure you have reliable unit tests for each scenario.

C# Samples for Visual Studio 2008 contain ExpressionTreeVisualizer that will help you to dig into Expression Trees more easily to understand how to extract information you need from it.

And of course, if you can stick with using existing implementation of LINQ, I would suggest to do it. There are Linq to SQL for SQL Server databases, Linq to Entities for many different databases, Linq to NHibernate for NHbernate projects.

Many other LINQ providers can be found here: Link to Everything: A List of LINQ Providers. Amount of work to implement LINQ provider is not trivial so it's a good idea to reuse tested and supported solution.

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
Konstantin Spirin
  • 20,609
  • 15
  • 72
  • 90
  • +1 This is really the only answer that answers what the poster is looking for. You can't really use something like Enumerable.Where on an object just to search the database for a single item. However, be aware that this method isn't very versatile (it relies on very specific conventions). – Joseph Yaduvanshi Mar 08 '10 at 04:05
  • That's a great example, the only problem is that I'll need several parameters to use for search. Something like "last_name='{1}' AND first_name='{2}' AND age={3}" – Alex Mar 08 '10 at 15:24
  • @Alex, the number of parameters doesn't matter. That's the beauty of passing an expression tree. You still only need to take in an Expression> and LINQ will take care of turning whatever expression you pass to it into a SQL query. This of course assumes you have a Postgresql LINQ provider. Implementing that yourself would be a huge pain. – Josh Mar 08 '10 at 15:52
2

Exactly the same way. Just replace this._items with your users collection.

Also replace the type parameter T with the type User.

Josh
  • 68,005
  • 14
  • 144
  • 156
  • Yes, but I need to do the "SELECT * FROM TABLE where last_name='" + LastName", I don't have any collection of Users to find it in. – Alex Mar 08 '10 at 15:11
  • If you are querying LINQ in that method you will need to use Konstantin's suggestion to use Expression> instead of Predicate then you can just pass that directly to the Where extension method of IQueryable. – Josh Mar 08 '10 at 15:51
1

A lambda expression in source code can be converted to either a compiled executable delegate or an expression tree upon compilation. Usually we associate lambda's with delegates but in your case since you say you want access to the parameters (in this case I assume you mean LastName and "Brown" then you want an expression tree.

Once you have an expression tree, you can parse it to see exactly what it is an translate it to whatever you actually need to do.

Here are a few questions about expression trees.

Expression trees for dummies?

Bit Curious to understand Expression Tree in .NET

Sounds like you're definitely reinventing a very complicated wheel though. I'm sure it'll be a useful learning experience, but you should look into LINQ to Entities or LINQ to SQL for real-world programming.

Community
  • 1
  • 1
Samuel Neff
  • 73,278
  • 17
  • 138
  • 182
  • +1 for the difference between a Function and an Expression, which I think is the key to the quesiton. – kyoryu Mar 08 '10 at 03:57
1

Maybe I just haven't understood the question, but there's already a method for doing what you want: Enumerable.Where.

If you need to find a single element then use SingleOrDefault or FirstOrDefault instead.

Aaronaught
  • 120,909
  • 25
  • 266
  • 342
  • I forgot to mention that I'm using Postgresql, so I can't use built in LINQ to SQL. – Alex Mar 08 '10 at 15:18
  • In that case, you definitely want to use @Konstantin's answer; don't attempt to iterate through the elements in C# like the `List` does, you need to use the expression tree so you can generate a SQL `WHERE` clause. – Aaronaught Mar 08 '10 at 15:25
1

You could do it something like this:

public static IEnumerable<User> Find(Predicate<User> match)
{
    //I'm not sure of the name
    using (var cn = new NpgsqlConnection("..your connection string..") )
    using (var cmd = new NpgsqlCommand("SELECT * FROM Users", cn))
    using (var rdr = cmd.ExecuteReader())
    {
        while (rdr.Read())
        {
           var user = BuildUserObjectFromIDataRecord(rdr);
           if (match(user)) yield return user;
        }
    }
}

And then you can call it like this

var users = User.Find(a => a.LastName == "Brown");

Note that this returns any number of users, you still have to implement the BuildUserObjectFromIDataRecord() function, and that it will always want to iterate over the entire users table. But it gives you the exact semantics you want.

Joel Coehoorn
  • 399,467
  • 113
  • 570
  • 794
  • Yep, I tried to do that. I do have the BuildUserObjectFromIDataRecord implemented. But the only problem is that instead of looking for 1 certain user, the script will have to get ALL records, and then look for necessary in all of them. – Alex Mar 09 '10 at 17:02
0

Okay, I understood now that I need to do LINQ to SQL to do all this good expressions stuff, otherwise I'd have to spend a lot of time reimplementeing the wheel.

Since I can't use LINQ to SQL, I implemented this easy method:

public static User Find(User match, string orderBy = "")
    {
        string query = "";
        if (!String.IsNullOrEmpty(match.FirstName)) query += "first_name='" + match.FirstName + "'";
        if (!String.IsNullOrEmpty(match.LastName)) query += "last_name='" + match.LastName+ "'";
        return Find(query + (!String.IsNullOrEmpty(orderBy) ? orderBy : ""));
    }

This is how to use it:

var user = User.Find(new User { FirstName = "Bob", LastName = "Brown" });
Alex
  • 34,581
  • 26
  • 91
  • 135
-1

One way would be to create an anonymous delegate, like so:

Predicate<User> Finder = delegate(User user)
{
    return user.LastName == "Brown";
}

var User = User.Find(Finder);
rwhit
  • 89
  • 4