Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should the UI layer be able to pass lambda expressions into the service layer instead of calling a specific method?

The ASP.NET project I am working on has 3 layers; UI, BLL, and DAL. I wanted to know if it was acceptable for the UI to pass a lambda expression to the BLL, or if the UI should pass parameters and the Service method should use those parameters to construct the lambda expression? Here is an example class showing both senarios.

public class JobService 
{
    IRepository<Job> _repository;

    public JobService(IRepository<Job> repository) 
    {
        _repository = repository;
    }

    public Job GetJob(int jobID)
    {
        return _repository.Get(x => x.JobID == jobID).FirstOrDefault();
    }

    public IEnumerable<Job> Get(Expression<Func<Job, bool>> predicate)
    {
        return _repository.Get(predicate);
    }
}

For the above class is it acceptable for the UI to call the following:

JobService jobService = new JobService(new Repository<Job>());
Job job = jobService.Get(x => x.JobID == 1).FirstOrDefault();

or should it only be allowed to call GetJob(int jobID)?

This is a simple example, and my question is in general, should the UI layer be able to pass lambda expressions into the service layer instead of calling a specific method?

like image 960
Jason Avatar asked Jan 24 '12 15:01

Jason


4 Answers

This is a judgement call based on the situation. It's not necessarily wrong to pass in a predicate like this. I think it should be considered a minor bad smell though.

If the passing in of a lambda expression allows you to reduce 6 methods down to 1, then it might be a good move. On the other hand, if you can just as easily pass in a simple type, then lambda syntax is a needless complication.

In the above example, not knowing the context, my preference would be to use a simple integer parameter. There should usually be a basic method that just gets a record by it's ID. And maybe one or two other such methods that are repeatedly used through your application. And then maybe a general purpose method that takes a lambda.

You should also consider what some would suggest should be a rule: that you not have any methods with lambda-specified predicates between your UI and your business layer. (And some believe, with reason, that your repositories shouldn't even have such methods!) I don't believe this should be an iron-clad rule, but there's good reasons for it. Your business and data layers, between them, should keep dangerous queries from happening. If you allow the passing in of lambdas, it's very easy for a junior developer in the UI layer to specify queries that could really hose your database. (For example, they'll do huge queries against non-indexed fields, and/or filter against the resultset using LINQ-to-objects, and not realize how inefficient that is.)

Like many other good practices, this will depend somewhat on scope. In my recent large application, I have no passing of lambda syntax from the UI layer to the business layer. My plan was to invest heavily in the business layer, to make it very smart. It has all the needed methods with simple types. In fact, it typically gives you what you need through simple domain object properties, with no parameters at all. My interface assures that the UI can only cause efficient queries to happen, with perhaps just minor LINQ-to-Objects predicates in the UI layer. (This goes even for "search" pages. My business layer accepts a criteria object with constrained possibilities, and ensures an efficient query.)

Now, you said "layers", rather than "tiers". So these are all in the same assembly? Another disadvantage of lambda is they're (currently) difficult to serialize. So you'd regret them if you had to separate your tiers.

like image 118
Patrick Karcher Avatar answered Nov 19 '22 20:11

Patrick Karcher


This is always a moot point as to which 'layer' that exposing Expression trees (and IQueryables) should be limited to.

So first the obvious - the main benefit of allowing an IQueryable lambda into a layer obviously allows for enormous flexibility in the query, without the need for writing lots of GetXXXByYYY type methods. Client layers can also directly control the joins by using .Include to specify the depth of graph retrieval, and can control ordering (.OrderBy), grouping (.GroupBy), row limits (.Take) in the database, which generally will have performance gains over doing the same thing in memory.

The downsides include:

  • Testability - because the interface is so open, there are an arbitrarily large number of permutations to check.
  • Trust - clients of your layer have free reign to execute arbitrary queries against your database, which could have performance issues (i.e. clients could execute queries which miss all the Indexes) and security issues (retrieving data to which they should not have access).
  • Serialization - Expression Trees can't be directly serialized, so you couldn't expose your BLL across e.g. WCF (although they can be proxied)

If you do allow the Expression tree as a parameter, then I would suggest you go 'all the way' and return IQueryable instead of IEnumerable. This will allow the queries to be chained / aggregated (e.g. you can amend the query later on, before materialising it)

Personally we don't permit expression trees past our BLL, so we wouldn't extend this to our UI - IMO Expression<> predicates are seen as mechanism of implementing Evan's specification pattern.

like image 4
StuartLC Avatar answered Nov 19 '22 21:11

StuartLC


I personally do not like this. I guess it isn't wrong, but it creates a grey area of where you should be writing your queries. I think that when someone else comes into my application, and all of the expressions are in one nice layer, it makes it easier to navigate.

like image 2
Larry Gasik Avatar answered Nov 19 '22 21:11

Larry Gasik


Assuming that the reason why you're thinking it may not be appropriate is that it might violate the encapsulation of the BLL, I'm going to go with it depends.

Delegates and lambda expressions are basic language constructs so the simple act of using them doesn't break any encapsulation per se.

The exception is of course if the signature of the delegate passed as argument to a method exposes a type that should not be accessed directly from within the caller's scope. Let me give you an example:

// Bad
public IEnumerable<Job> Get(Action<SqlCommand> queryCommand);

// Good
public IEnumerable<Job> Get(Func<Job, bool> predicate);

In the first example the method signature breaks encapsulation by exposing an implementation detail to the caller in the form of the SqlCommand type. It also grants the caller too much freedom by allowing it to specify any query as argument, which is plain dangerous.

In the second example, on the other hand, the method signature simple exposes a model class, which is already part of the layer's public interface. It doesn't increase the surface area of the API nor does it expose any details about how the operation is implemented.

like image 2
Enrico Campidoglio Avatar answered Nov 19 '22 21:11

Enrico Campidoglio