Implementing Global Search with Detailed Views in ASP.NET Core MVC

In this article, we'll walk through the process of implementing a global search feature in an ASP.NET Core MVC application. This feature will include a drop-down filter to search through all categories, products, and specific categories or products, and it will display search results with images. Additionally, we'll implement detailed views for the search results, ensuring a complete end-to-end functionality.

Prerequisites

Before we begin, ensure you have the following prerequisites.

  1. Visual Studio 2019 or later
  2. .NET Core 3.1 or later
  3. Basic knowledge of ASP.NET Core MVC
  4. An existing ASP.NET Core MVC project

Step 1. Setup Database and Models

First, we'll create our database context and models. In this example, we'll have two models: Product and Category.

Models

Create the Product and Category models in the Models folder.

namespace GlobalSearchInAspNetCoreMVC.Models
{
    public class Product
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string? Description { get; set; }
        public string ImageUrl { get; set; }
        public int CategoryId { get; set; }
        public Category Category { get; set; }
    }
}
namespace GlobalSearchInAspNetCoreMVC.Models
{
    public class Category
    {
        public int Id { get; set; }
        public string? Name { get; set; }
    }
}
namespace GlobalSearchInAspNetCoreMVC.Models
{
    public class SearchResultItem
    {
        public int Id { get; set; }
        public string? Name { get; set; }
        public string? Description { get; set; }
        public string? ImageUrl { get; set; }
    }
}

Database Context

Create the ApplicationDbContext in the Data folder.

using GlobalSearchInAspNetCoreMVC.Models;
using Microsoft.EntityFrameworkCore;
namespace GlobalSearchInAspNetCoreMVC.Data
{
    public class ApplicationDbContext : DbContext
    {
        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
            : base(options)
        {
        }
        public DbSet<Product> Products { get; set; }
        public DbSet<Category> Categories { get; set; }
    }

}

Step 2. Create and Seed the Database

Configure the database connection string in appsettings.json.

{
  "ConnectionStrings": {
    "DefaultConnection": "Data Source=SQLLiteDatabase.db"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

Update the Startup. cs or Program. cs to use the ApplicationDbContext.

using GlobalSearchInAspNetCoreMVC.AppService;
using GlobalSearchInAspNetCoreMVC.Data;
using GlobalSearchInAspNetCoreMVC.IAppServices;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));
builder.Services.AddScoped<ISearchService, SearchService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
    endpoints.MapRazorPages();
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
    endpoints.MapRazorPages();
});
app.Run();

Create a migration and update the database.

add-migration GlobalSearch
update-database

Seed the Database

Create a DataSeeder class to seed the database with initial data.

using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
namespace GlobalSearchInAspNetCoreMVC.Data
{
    public static class DataSeeder
    {
        public static void Seed(IServiceProvider serviceProvider)
        {
            using (var context = new ApplicationDbContext(
                serviceProvider.GetRequiredService<DbContextOptions<ApplicationDbContext>>()))
            {
                if (!context.Products.Any())
                {
                    context.Products.AddRange(
                        new Product { Name = "Product 1", Description = "Description 1", ImageUrl = "/images/product1.jpg" },
                        new Product { Name = "Product 2", Description = "Description 2", ImageUrl = "/images/product2.jpg" }
                    );
                    context.SaveChanges();
                }
                if (!context.Categories.Any())
                {
                    context.Categories.AddRange(
                        new Category { Name = "Category 1" },
                        new Category { Name = "Category 2" }
                    );
                    context.SaveChanges();
                }
            }
        }
    }
}

Or save the data using a database query.

-- Insert dummy data into Categories table
INSERT INTO Categories (Name)
VALUES 
    ('Electronics'),
    ('Books'),
    ('Clothing'),
    ('Home & Kitchen'),
    ('Sports & Outdoors');
-- Insert dummy data into Products table
INSERT INTO Products (Name, Description, ImageUrl, CategoryId)
VALUES 
    ('Smartphone', 'Latest model smartphone with advanced features', 'https://via.placeholder.com/150/0000FF/808080?text=Smartphone', 1),
    ('Laptop', 'High performance laptop suitable for gaming and work', 'https://via.placeholder.com/150/FF0000/FFFFFF?text=Laptop', 1),
    ('Headphones', 'Noise-cancelling over-ear headphones', 'https://via.placeholder.com/150/FFFF00/000000?text=Headphones', 1),
    ('Science Fiction Book', 'A gripping science fiction novel', 'https://via.placeholder.com/150/00FF00/000000?text=Science+Fiction+Book', 2),
    ('Cookbook', 'Delicious recipes for home cooks', 'https://via.placeholder.com/150/FF69B4/000000?text=Cookbook', 2),
    ('T-shirt', 'Comfortable cotton T-shirt', 'https://via.placeholder.com/150/800080/FFFFFF?text=T-shirt', 3),
    ('Jeans', 'Stylish denim jeans', 'https://via.placeholder.com/150/FFA500/FFFFFF?text=Jeans', 3),
    ('Blender', 'High-speed blender for smoothies and soups', 'https://via.placeholder.com/150/000080/FFFFFF?text=Blender', 4),
    ('Coffee Maker', 'Programmable coffee maker with timer', 'https://via.placeholder.com/150/FFC0CB/000000?text=Coffee+Maker', 4),
    ('Yoga Mat', 'Non-slip yoga mat for all types of exercise', 'https://via.placeholder.com/150/008080/FFFFFF?text=Yoga+Mat', 5),
    ('Dumbbells', 'Set of adjustable dumbbells', 'https://via.placeholder.com/150/000000/FFFFFF?text=Dumbbells', 5);

Call the seeder in the Program. cs.

public static void Main(string[] args)
{
    var host = CreateHostBuilder(args).Build();
    using (var scope = host.Services.CreateScope())
    {
        var services = scope.ServiceProvider;
        var context = services.GetRequiredService<ApplicationDbContext>();
        context.Database.Migrate();
        DataSeeder.Seed(services);
    }
    host.Run();
}

Step 3. Implement the Search Service

Create a search service to handle the search logic.

ISearchService.cs

using GlobalSearchInAspNetCoreMVC.Models;
namespace GlobalSearchInAspNetCoreMVC.IAppServices
{
    public interface ISearchService
    {
        Task<IEnumerable<object>> SearchAsync(string searchTerm,string filter);
        Task<SearchResultItem> GetItemByIdAsync(int id);
    }
}

SearchService.cs

using GlobalSearchInAspNetCoreMVC.Data;
using GlobalSearchInAspNetCoreMVC.IAppServices;
using GlobalSearchInAspNetCoreMVC.Models;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace GlobalSearchInAspNetCoreMVC.AppService
{
    public class SearchService : ISearchService
    {
        private readonly ApplicationDbContext _context;
        public SearchService(ApplicationDbContext context)
        {
            _context = context;
        }
        public async Task<IEnumerable<object>> SearchAsync(string searchTerm, string filter)
        {
            List<object> results = new List<object>();

            if (filter == "all" || filter == "products")
            {
                var products = await _context.Products
                    .Where(p => p.Name.Contains(searchTerm) || p.Description.Contains(searchTerm))
                    .Select(p => new { p.Id, p.Name, p.Description, p.ImageUrl })
                    .ToListAsync();

                results.AddRange(products);
            }
            if (filter == "all" || filter == "categories")
            {
                var categories = await _context.Categories
                    .Where(c => c.Name.Contains(searchTerm))
                    .Select(c => new { c.Id, c.Name })
                    .ToListAsync();

                results.AddRange(categories);
            }
            return results;
        }
        public async Task<SearchResultItem> GetItemByIdAsync(int id)
        {
            var product = await _context.Products.FindAsync(id);
            if (product != null)
            {
                return new SearchResultItem
                {
                    Id = product.Id,
                    Name = product.Name,
                    Description = product.Description,
                    ImageUrl = product.ImageUrl
                };
            }
            var category = await _context.Categories.FindAsync(id);
            if (category != null)
            {
                return new SearchResultItem
                {
                    Id = category.Id,
                    Name = category.Name,
                    Description = "Category"
                };
            }
            return null;
        }
    }
}

Step 4. Create the HomeController

Create the HomeController to handle the search and detail views.

HomeController.cs

using GlobalSearchInAspNetCoreMVC.IAppServices;
using Microsoft.AspNetCore.Mvc;
namespace GlobalSearchInAspNetCoreMVC.Controllers
{
    public class HomeController : Controller
    {
        private readonly ISearchService _searchService;

        public HomeController(ISearchService searchService)
        {
            _searchService = searchService;
        }
        public IActionResult Index()
        {
            return View();
        }
        public async Task<IActionResult> Details(int id)
        {
            var item = await _searchService.GetItemByIdAsync(id);
            if (item == null)
            {
                return NotFound();
            }
            return View(item);
        }
    }
}

Step 5. Create the Views

Views/Home/Index.cshtml

<div class="text-center">
    <h1 class="display-4">Search</h1>
    <div class="form-group">
        <select id="search-filter" class="form-control">
            <option value="all">All</option>
            <option value="products">Products</option>
            <option value="categories">Categories</option>
        </select>
    </div>
    <input type="text" id="search-input" placeholder="Search..." class="form-control" />
    <div id="search-results" class="dropdown-menu" style="display: block; position: absolute; width: 100%;"></div>
</div>
@section Scripts {
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
    <script>
        $(document).ready(function () {
            function performSearch() {
                let query = $('#search-input').val();
                let filter = $('#search-filter').val();
                if (query.length >= 3) {
                    $.ajax({
                        url: '/api/search',
                        type: 'GET',
                        data: { query: query, filter: filter },
                        success: function (data) {
                            let results = $('#search-results');
                            results.empty();
                            if (data.length > 0) {
                                data.forEach(item => {
                                    if (item.imageUrl) { // Check if item has an image
                                        results.append(`
                                                        <a class="dropdown-item" href="/Home/Details/${item.id}">
                                                    <img src="${item.imageUrl}" alt="${item.name}" style="width:50px; height:50px;"/>
                                                    ${item.name || item.description}
                                                </a>
                                               `);
                                    } else {
                                        results.append(`<a class="dropdown-item" href="/Home/Details/${item.id}">${item.name || item.description}</a>`);
                                    }
                                });
                            } else {
                                results.append('<a class="dropdown-item">No results found</a>');
                            }
                        }
                    });
                } else {
                    $('#search-results').empty();
                }
            }
            $('#search-input').on('input', performSearch);
            $('#search-filter').on('change', performSearch);
        });
    </script>
}

Views/Home/Details.cshtml

@model SearchResultItem 
@{
    ViewData["Title"] = "Details";
}
<div class="text-center">
    <h1 class="display-4">Details</h1>
    @if (Model != null)
    {
        <div class="card" style="width: 18rem;">
            @if (!string.IsNullOrEmpty(Model.ImageUrl))
            {
                <img class="card-img-top" src="@Model.ImageUrl" alt="Card image cap">
            }
            <div class="card-body">
                <h5 class="card-title">@Model.Name</h5>
                <p class="card-text">@Model.Description</p>
            </div>
        </div>
    }
    else
    {
        <p>Item not found.</p>
    }
</div>

Step 6. Create the Search Controller

using GlobalSearchInAspNetCoreMVC.IAppServices;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace GlobalSearchInAspNetCoreMVC.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class SearchController : ControllerBase
    {
        private readonly ISearchService _searchService;

        public SearchController(ISearchService searchService)
        {
            _searchService = searchService;
        }
        [HttpGet]
        public async Task<IActionResult> Get(string query, string filter)
        {
            if (string.IsNullOrEmpty(query))
            {
                return BadRequest("Query cannot be empty");
            }
            var results = await _searchService.SearchAsync(query, filter);
            return Ok(results);
        }
    }
}

Step 7. Run and Test

Now that everything is set up, run your application. You should be able to search for products and categories using the dropdown filter, view the search results with images, and click on each result to see detailed information.

Step 8. Output

Output

Conclusion

In this article, we have implemented a global search feature in an ASP.NET Core MVC application. We covered creating models, setting up the database context, seeding the database with initial data, implementing the search service, and creating the necessary views and controllers. This end-to-end implementation should provide a comprehensive understanding of how to build a search functionality in ASP.NET Core MVC applications.