42

I am using ASP.NET MVC with EF 6.

I have a stock page which shows all the information on stock items. Now I want to filter records too.

In picture below I have 3 options. I might filter by each option, one at a time or by combination of two or with all three.

I was thinking of writing linq query for each and every options selected. But this wouldn't be possible if filter option increases.Is there is any better way to this.

Thanks!

enter image description here

This is what I did in my controller.(currently dropdown has two options, excluding : " -- select one -- ")

public ActionResult StockLevel(string option, string batch, string name)
{
    if (option != "0" && batch == "" && name == "")
    {
        if(option == "BelowMin")
        {
            List<Stock> stk = (from s in db.Stocks
                               where s.Qty < s.Item.AlertQty
                               select s).ToList();
            return View(stk);
        }
        else
        {
            List<Stock> stk = (from s in db.Stocks
                               where s.Qty == s.InitialQty
                               select s).ToList();
            return View(stk);
        }
    }
    if (option == "0" && batch != "" && name == "")
    {
        List<Stock> stk = (from s in db.Stocks
                           where s.BatchNo == batch
                           select s).ToList();
        return View(stk);
    }
    if (option == "0" && batch == "" && name != "")
    {
        List<Stock> stk = (from s in db.Stocks
                           where s.Item.Name.StartsWith(""+name+"")
                           select s).ToList();
        return View(stk);
    }
    return View(db.Stocks.ToList());
}
Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
Avishekh Bharati
  • 1,858
  • 4
  • 27
  • 44

4 Answers4

91

I recommend you separate concerns and use an approach that the code in your controller be like this, simple, beautiful and extensible:

public ActionResult Index(ProductSearchModel searchModel)
{
    var business = new ProductBusinessLogic();
    var model = business.GetProducts(searchModel);
    return View(model);
}

Benefits:

  • You can put anything you need in your ProductSearchModel based on your requirements.
  • You can write any logic in GetProducts based on requirements. There is no limitation.
  • If you add a new field or option to search, your action and controller will remain untouched.
  • If the logic of your search changes, your action and controller will remain untouched.
  • You can reuse logic of search wherever you need to search on products, in controllers or even in other business logic.
  • Having such ProductSearchModel, you can use it as model of ProductSearch partial view and you can apply DataAnnotations to it to enhance the model validation and help UI to render it using Display or other attributes.
  • You can add other business logic related to your product in that business logic class.
  • Following this way you can have a more organized application.

Sample Implementation:

Suppose you have a Product class:

public class Product
{
    public int Id { get; set; }
    public int Price { get; set; }
    public string Name { get; set; }
}

You can create a ProductSearchModel class and put some fields you want to search based on them:

public class ProductSearchModel
{
    public int? Id { get; set; }
    public int? PriceFrom { get; set; }
    public int? PriceTo { get; set; }
    public string Name { get; set; }
}

Then you can put your search logic in ProductBusinessLogic class this way:

public class ProductBusinessLogic
{
    private YourDbContext Context;
    public ProductBusinessLogic()
    {
        Context = new YourDbContext();
    }

    public IQueryable<Product> GetProducts(ProductSearchModel searchModel)
    {
        var result = Context.Products.AsQueryable();
        if (searchModel != null)
        {
            if (searchModel.Id.HasValue)
                result = result.Where(x => x.Id == searchModel.Id);
            if (!string.IsNullOrEmpty(searchModel.Name))
                result = result.Where(x => x.Name.Contains(searchModel.Name));
            if (searchModel.PriceFrom.HasValue)
                result = result.Where(x => x.Price >= searchModel.PriceFrom);
            if (searchModel.PriceTo.HasValue)
                result = result.Where(x => x.Price <= searchModel.PriceTo);
        }
        return result;     
    }
}

Then in your ProductController you can use this way:

public ActionResult Index(ProductSearchModel searchModel)
{
    var business = new ProductBusinessLogic();
    var model = business.GetProducts(searchModel);
    return View(model);
}

Important Note:

In a real world implementation, please consider implementing a suitable Dispose pattern for your business class to dispose db context when needed. For more information take a look at Implementing a Dispose method or Dispose Pattern.

Reza Aghaei
  • 120,393
  • 18
  • 203
  • 398
  • I am using your idea and it works fine when i search, but when i try to sort my data the search criteria is lost. my action looks like this `public ActionResult Index(string sortOrder, SearchTransacModel searchModel, SearchTransacModel currentFilter, int? page) {}` I am passing the search model to the view like this in order to keep my search parameters ` ViewBag.currentFilter = searchModel;` – KayGee Aug 29 '16 at 17:04
  • 1
    The result of `GetProducts` method is `IQueryable` and you can simply apply sorting and paging to the result. It's enough to pass suitable sort column name, sort order and page number to the action and use them. – Reza Aghaei Aug 29 '16 at 17:08
  • When rendering your paging links and your column headers which are responsible for sorting, you should add suitable query string to links to act correctly. In my projects, it's the responsibility of my `Grid` helper/component. – Reza Aghaei Aug 29 '16 at 17:11
  • But when I click sort: `@Html.ActionLink("Reference No", "Index", new { sortOrder = ViewBag.RefNoSortParm, currentFilter = ViewBag.CurrentFilter })` currentFilter is null in the controller. I have checked and ViewBag.currentFilter does have the data in the view but for some reason it is not going back to the Controller – KayGee Aug 29 '16 at 17:11
  • @KayGee Because it probably generates a link like `/?sortorder=acs&curretFilter=SomeNameSpace.SearchTransacModel`. You cannot mix search model values and sortorder this way. – Reza Aghaei Aug 29 '16 at 17:16
  • 3
    @KayGee It's better to ask a new question inspired by this post and let the community help you. Also if you notify me, surely I also will take a look at your question :) – Reza Aghaei Aug 29 '16 at 17:17
  • thanx I'll do that :) – KayGee Aug 29 '16 at 17:24
  • @RezaAghaei i have a quick question on this. On the controller, all the search fields(on the view) are mapped to SearchModel. Suppose i need to return search results thats not coming from one table(in the above example its coming from one mode Products) but multiple tables/models, is it better for me to create a view in the db and then add that to the entity model and then use that as a view model ? – cableload Oct 13 '16 at 15:00
  • I guess i can create a new ViewModel thats combination of multiple models and return that? – cableload Oct 13 '16 at 15:07
  • @cableload You don't need to create a View in db when you use Entity Framework. Just use EF and shape the result of query to your custom ViewModel. It's an example and I kept it simple. You can return a custom view model, also you can include paging and sorting [like this](http://stackoverflow.com/a/39215048/3110834). – Reza Aghaei Oct 13 '16 at 18:36
  • Yes, thats what i am leaning on. I do have another quick question. I have a top screen where i have all the search fields (mapped to searchmodel) and on the bottom i want to have a search results which is coming from a different view model. I cant define search model and view model in my main view. I am confused on how to structure this. – cableload Oct 13 '16 at 18:40
  • @cableload You can create a single ViewModel including a property for SearchModel and a property of List as the search result. The approach which I used myself was having using a partial view for search panel and a partial view for search result and using ajax. So I didn't need to mix those models in a single view model, but you can mix them with no problem. – Reza Aghaei Oct 13 '16 at 18:44
  • @RezaAghaei on your second approach, how would you pass the search fields (searchmodel) to the partial view in DataModel? – cableload Oct 13 '16 at 18:46
  • @cableload I used the ViewBag. For next times when you click on submit button of search partial view, I send an ajax request and the result (Index partial view) will be returned and replaced previous results. My search partial view is at top of page and I didn't need to render it again. But I use the search model which I need it in Index Partial view. For example take a look at [this post](http://stackoverflow.com/a/39215048/3110834). – Reza Aghaei Oct 13 '16 at 18:53
  • @cableload Using ViewBag is somehow ugly, but it's simple to use. You can use ViewBag, also you can go with first option which is creating a Custom ViewModel containing search model and search results. In our solution because of some reasons we tried to use data models as much as possible instead of view models but there is no need to avoid view models, indeed it's recommended to use view models. – Reza Aghaei Oct 13 '16 at 18:57
  • Thanks @RezaAghaei for answering my questions and giving good feedback. I will try the viewmodel method first and see how it goes. – cableload Oct 13 '16 at 18:58
  • @cableload By the way, if you find this post or the linked post useful, it would be great if you vote for question and answers to make them more popular and useful for future readers. It's not compulsory at all :) – Reza Aghaei Oct 13 '16 at 18:59
  • The Logic works perfectly fine untill some one refreshes the page and the moment you refresh it, all the data vanishes away. – Abdul Hannan Jul 25 '17 at 04:51
  • @AbdulHannan persisting the search/sort state is your job. For example you can simply pass searchModel to View using ViewData, then you can use it in Html.ActionLink or Html.BeginForm methods to include it in QueryString for links or forms. This way, when you navigate, refresh or re-post the form you will see the search model will be passed to your action. For example take a look at [this post](https://stackoverflow.com/questions/39212140/how-can-i-pass-some-objects-in-viewbag-to-the-action-preserve-search-sort-an/39215048#39215048). – Reza Aghaei Jul 25 '17 at 05:48
  • Hi, in the view binding/typing **should I have the SearchModel as the strong type or the ProdctViewModel?**. More importantly, when the results from the query are sent back they're a list of product and how do you iterate them to insert them in a table _if you dont have a strongly typed_ `ProductViewModel` inside the view. **can you please add the view code here so we know how to populate the view** – Transformer Jan 06 '18 at 06:46
  • Hi @transformer In the above example, the model used for the view is `Product` and the `ProductSearchModel` is passed to view (to be added to sort columns, page numbers and show in search fields to persist the search model between requests) using `ViewData`. – Reza Aghaei Jan 06 '18 at 15:22
  • @Reza Aghaei - this is nice but how is your controller taking in the object searchModel how are you constructing it? In addition how does your controller pass this object in as when the page loads you simply are on the ui level no object created yet??? – JonH Feb 06 '19 at 04:44
  • @JonH This is ASP.NET MVC. When a request like `/product/index?name=x&pricefrom=100` comes in, `Index` action of the `Product` controller will handle the request and the model binder will create an instance of `ProductSearchModel` having `Name=x` and `PriceFrom=10` will be created and passed as parameter to the `Index` action. – Reza Aghaei Feb 06 '19 at 06:26
  • Do the parameter names have to match the class property names. How or when does it know to use the for instance ProductSearchModel and when not to use this object? – JonH Feb 06 '19 at 14:51
  • @JonH It's the way that ASP.NET MVC model binding works. It maps the url parameters or form parameters to action parameters. So if the url is `/product/index` the instance of `ProductSearchModel` which will be created contains null for all fields and you see the search logic ignores null values. – Reza Aghaei Feb 06 '19 at 14:55
  • But how does it know because the name ProductSearchModel? – JonH Feb 06 '19 at 14:58
  • What i am asking is how does mvc know that just because you have a link called product/index does it know to bind to the productsearchmodel object? – JonH Feb 06 '19 at 14:59
  • @JonH So I suggest you read [Model Binding in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-2.2#how-model-binding-works) :) – Reza Aghaei Feb 06 '19 at 14:59
  • Reza i understand the route from controller to action but not binding to an object. That link doesnt give info on thstt – JonH Feb 06 '19 at 15:00
  • @JonH When an action has a parameter of type `Class1`, model binder will *always* create an instance of `Class1` and if there are post/querystring parameters with the same name as properties of `Class1`, those properties will be initialized with values of those parameters. – Reza Aghaei Feb 06 '19 at 15:04
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/188010/discussion-between-jonh-and-reza-aghaei). – JonH Feb 06 '19 at 16:36
  • But where does class1 come into play. For instance your URL is: products/index?name=x&priceFrom=100 correct? How does mvc know that this translates to an object of type ProductSearchModel? Is it just ecause the parameter names are the exact same as the properties of this object ??? If so does it matter that the property name is case different ? Meaning the url parameter say is name, but your class 's property is public string Name {get; set; } does that matter ? – JonH Feb 06 '19 at 16:43
  • 1
    `ModelBinder` looks to parameter of action and since it sees the action has a parameter of type `ProductSearchModel`, creates an instance of it. Then looks into querystring or form parameters, check if there is a value posted for `Id` property (case-insensitive) then initializes the `Id` property of the object, the same for `Name` property and the other properties. For more information, search about *Model binding in ASP.NET MVC* and read a few articles like [this one](https://www.tutorialsteacher.com/mvc/model-binding-in-asp.net-mvc). – Reza Aghaei Feb 06 '19 at 16:51
  • Exactly what i was looking for and you explained it well now. Thanks! – JonH Feb 11 '19 at 04:20
  • 1
    An elegant solution: https://codereview.stackexchange.com/a/5506/167333 – MD TAREQ HASSAN Aug 22 '19 at 15:03
18

Conditional filtering

.ToList(), .First(), .Count() and a few other methods execute the final LINQ query. But before it is executed you can apply filters just like that:

var stocks = context.Stocks.AsQueryable();
if (batchNumber != null) stocks = stocks.Where(s => s.Number = batchNumber);
if (name != null)        stocks = stocks.Where(s => s.Name.StartsWith(name));
var result = stocks.ToList(); // execute query

WhereIf LINQ Extension

Simple WhereIf can significantly simplify code:

var result = db.Stocks
    .WhereIf(batchNumber != null, s => s.Number == batchNumber)
    .WhereIf(name != null,        s => s.Name.StartsWith(name))       
    .ToList();

WhereIf implementation. It's a simple extension method for IQueryable:

public static class CollectionExtensions
{
    public static IQueryable<TSource> WhereIf<TSource>(
        this IQueryable<TSource> source,
        bool condition,
        Expression<Func<TSource, bool>> predicate)
    {
        if (condition)
            return source.Where(predicate);
        else
            return source;
    }
}

Non-WhereIf LINQ way (Recommended)

WhereIf provides more declarative way, if you don't want to use extensions you can just filter like that:

var result = context.Stocks
    .Where(batchNumber == null || stock.Number == batchNumber)
    .Where(name == null || s => s.Name.StartsWith(name))
    .ToList();

It gives an exact same effect as WhereIf and it will work faster as runtime will need to build just one ExpressionTree instead of building multiple trees and merging them.

Andrei
  • 42,814
  • 35
  • 154
  • 218
1

I've written some extensions to make this easier. https://www.nuget.org/packages/LinqConditionalExtensions/

It's not reinventing the wheel. Some of the extensions have already been recommended. You could rewrite your logic as follows.

var results = db.Stocks
                .If(option != "0", stocks => stocks
                    .IfChain(option == "BelowMin", optionStocks => optionStocks
                        .Where(stock => stock.Qty < stock.Item.AlertQty))
                    .Else(optionStocks => optionStocks
                        .Where(stock => stock.Qty == stock.InitialQty)))
                .WhereIf(!string.IsNullOrWhiteSpace(batch), stock => stock.BatchNo == batch)
                .WhereIf(!string.IsNullOrWhiteSpace(name), stock => stock.Item.Name.StartsWith("" + name + ""))
                .ToList();

return results;

Basically, the initial If() method will apply the passed if-chain if the condition is true. The IfChain() is your nested if-else statement. IfChain() allows you to chain multiple IfElse() and end with an Else().

The WhereIf() will just conditionally apply your where clause if the condition is true.

If you are interested in the library, https://github.com/xKloc/LinqConditionalExtensions has a readme.

Todd Skelton
  • 6,839
  • 3
  • 36
  • 48
0
public ActionResult Index(string searchid)
{ 
var personTables = db.PersonTables.Where(o => o.Name.StartsWith(searchid) )||  o.CombanyTable.ComName.StartsWith(searchid) ).Include(k => k.CombanyTable);
return View(personTables.ToList());
}
ana
  • 1
  • 1