tags:

views:

291

answers:

7

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" });
+2  A: 

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

Also replace the type parameter T with the type User.

Josh Einstein
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
If you are querying LINQ in that method you will need to use Konstantin's suggestion to use Expression<Func<User>> instead of Predicate<User> then you can just pass that directly to the Where extension method of IQueryable.
Josh Einstein
A: 

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
That verbosity is precisely what lambdas were designed to avoid.
Josh Einstein
+1  A: 

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.

http://stackoverflow.com/questions/623413/expression-trees-for-dummies

http://stackoverflow.com/questions/1588963/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.

Sam
+1 for the difference between a Function and an Expression, which I think is the key to the quesiton.
kyoryu
+5  A: 

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.

Konstantin Spirin
+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).
Jim Schubert
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
@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<Func<User>> 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 Einstein
+1  A: 

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
I forgot to mention that I'm using Postgresql, so I can't use built in LINQ to SQL.
Alex
In that case, you definitely want to use @Konstantin's answer; don't attempt to iterate through the elements in C# like the `List<T>` does, you need to use the expression tree so you can generate a SQL `WHERE` clause.
Aaronaught
A: 

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
+1  A: 

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
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