Parte 7, adicione a pesquisa a um aplicativo ASP.NET Core MVC

Observação

Esta não é a versão mais recente deste artigo. Para a versão atual, confira a versão 8.0 do ASP.NET Core deste artigo.

De Rick Anderson

Nesta seção, você adiciona a funcionalidade de pesquisa ao método de ação Index que permite pesquisar filmes por gênero ou nome.

Atualize o método Index encontrado dentro de Controllers/MoviesController.cs com o seguinte código:

public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}

A linha a seguir do método de ação Index cria uma consulta LINQ para selecionar os filmes:

var movies = from m in _context.Movie
             select m;

A consulta é definida apenas neste ponto; ela não foi executada no banco de dados.

Se o parâmetro searchString contiver uma cadeia de caracteres, a consulta de filmes será modificada para filtrar o valor da cadeia de caracteres de pesquisa:

if (!String.IsNullOrEmpty(searchString))
{
    movies = movies.Where(s => s.Title!.Contains(searchString));
}

O código s => s.Title!.Contains(searchString) acima é uma Expressão Lambda. Lambdas são usados em consultas LINQ baseadas em método como argumentos para métodos de operadores de consulta padrão, como o método Where ou Contains (usado no código acima). As consultas LINQ não são executadas quando são definidas ou no momento em que são modificadas com uma chamada a um método, como Where, Contains ou OrderBy. Em vez disso, a execução da consulta é adiada. Isso significa que a avaliação de uma expressão é atrasada até que seu valor realizado seja, de fato, iterado ou o método ToListAsync seja chamado. Para obter mais informações sobre a execução de consulta adiada, consulte Execução da consulta.

O método Contains é executado no banco de dados, não no código C# mostrado acima. A diferenciação de maiúsculas e minúsculas na consulta depende do banco de dados e da ordenação. No SQL Server, Contains é mapeado para SQL LIKE, que não diferencia maiúsculas de minúsculas. No SQLite, com a ordenação padrão, ele diferencia maiúsculas de minúsculas.

Navegue até /Movies/Index. Acrescente uma cadeia de consulta, como ?searchString=Ghost, à URL. Os filmes filtrados são exibidos.

Index view

Se você alterar a assinatura do método Index para que ele tenha um parâmetro chamado id, o parâmetro id corresponderá o espaço reservado {id} opcional com as rotas padrão definidas em Program.cs.

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

Altere o parâmetro para id e todas as ocorrências de searchString para id.

O método Index anterior:

public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}

O método Index atualizado com o parâmetro id:

public async Task<IActionResult> Index(string id)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(id))
    {
        movies = movies.Where(s => s.Title!.Contains(id));
    }

    return View(await movies.ToListAsync());
}

Agora você pode passar o título de pesquisa como dados de rota (um segmento de URL), em vez de como um valor de cadeia de consulta.

Index view with the word ghost added to the Url and a returned movie list of two movies, Ghostbusters and Ghostbusters 2

No entanto, você não pode esperar que os usuários modifiquem a URL sempre que desejarem pesquisar um filme. Agora você adicionará os elementos da interface do usuário para ajudá-los a filtrar filmes. Se você tiver alterado a assinatura do método Index para testar como passar o parâmetro ID associado à rota, altere-o novamente para que ele use um parâmetro chamado searchString:

public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}

Abra o arquivo Views/Movies/Index.cshtml e adicione a marcação <form> destacada abaixo:

@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index">
    <p>
        Title: <input type="text" name="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">

A marcação <form> HTML usa o Auxiliar de Marcação de Formulário. Portanto, quando você enviar o formulário, a cadeia de caracteres de filtro será enviada para a ação Index do controlador de filmes. Salve as alterações e, em seguida, teste o filtro.

Index view with the word ghost typed into the Title filter textbox

Não há nenhuma sobrecarga [HttpPost] do método Index que poderia ser esperada. Isso não é necessário, porque o método não está alterando o estado do aplicativo, apenas filtrando os dados.

Você poderá adicionar o método [HttpPost] Index a seguir.

[HttpPost]
public string Index(string searchString, bool notUsed)
{
    return "From [HttpPost]Index: filter on " + searchString;
}

O parâmetro notUsed é usado para criar uma sobrecarga para o método Index. Falaremos sobre isso mais adiante no tutorial.

Se você adicionar esse método, o chamador de ação fará uma correspondência com o método [HttpPost] Index e o método [HttpPost] Index será executado conforme mostrado na imagem abaixo.

Browser window with application response of From HttpPost Index: filter on ghost

No entanto, mesmo se você adicionar esta versão [HttpPost] do método Index, haverá uma limitação na forma de como isso tudo foi implementado. Imagine que você deseja adicionar uma pesquisa específica como Favoritos ou enviar um link para seus amigos para que eles possam clicar para ver a mesma lista filtrada de filmes. Observe que a URL da solicitação HTTP POST é a mesma que a URL da solicitação GET (localhost:{PORT}/Movies/Index) – não há nenhuma informação de pesquisa na URL. As informações de cadeia de caracteres de pesquisa são enviadas ao servidor como um valor de campo de formulário. Verifique isso com as ferramentas do Desenvolvedor do navegador ou a excelente ferramenta Fiddler. A imagem abaixo mostra as ferramentas do Desenvolvedor do navegador Chrome:

Network tab of Microsoft Edge Developer Tools showing a request body with a searchString value of ghost

Veja o parâmetro de pesquisa e o token XSRF no corpo da solicitação. Observe, conforme mencionado no tutorial anterior, que o Auxiliar de Marcação de Formulário gera um token antifalsificação XSRF. Não modificaremos os dados e, portanto, não precisamos validar o token no método do controlador.

Como o parâmetro de pesquisa está no corpo da solicitação e não na URL, não é possível capturar essas informações de pesquisa para adicionar como Favoritos ou compartilhar com outras pessoas. Corrigimos isso especificando que a solicitação deve ser HTTP GET encontrada no arquivo Views/Movies/Index.cshtml.

@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index" method="get">
    <p>
        Title: <input type="text" name="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">

Agora, quando você enviar uma pesquisa, a URL conterá a cadeia de consulta da pesquisa. A pesquisa também irá para o método de ação HttpGet Index, mesmo se você tiver um método HttpPost Index.

Browser window showing the searchString=ghost in the Url and the movies returned, Ghostbusters and Ghostbusters 2, contain the word ghost

A seguinte marcação mostra a alteração para a marcação form:

<form asp-controller="Movies" asp-action="Index" method="get">

Adicionar pesquisa por gênero

Adicione a seguinte classe MovieGenreViewModel à pasta Models:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models;

public class MovieGenreViewModel
{
    public List<Movie>? Movies { get; set; }
    public SelectList? Genres { get; set; }
    public string? MovieGenre { get; set; }
    public string? SearchString { get; set; }
}

O modelo de exibição do gênero de filme conterá:

  • Uma lista de filmes.
  • Uma SelectList que contém a lista de gêneros. Isso permite que o usuário selecione um gênero na lista.
  • MovieGenre, que contém o gênero selecionado.
  • SearchString, que contém o texto que os usuários inserem na caixa de texto de pesquisa.

Substitua o método Index em MoviesController.cs pelo seguinte código:

// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    // Use LINQ to get list of genres.
    IQueryable<string> genreQuery = from m in _context.Movie
                                    orderby m.Genre
                                    select m.Genre;
    var movies = from m in _context.Movie
                 select m;

    if (!string.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.Contains(searchString));
    }

    if (!string.IsNullOrEmpty(movieGenre))
    {
        movies = movies.Where(x => x.Genre == movieGenre);
    }

    var movieGenreVM = new MovieGenreViewModel
    {
        Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
        Movies = await movies.ToListAsync()
    };

    return View(movieGenreVM);
}

O código a seguir é uma consulta LINQ que recupera todos os gêneros do banco de dados.

// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;

A SelectList de gêneros é criada com a projeção dos gêneros distintos (não desejamos que nossa lista de seleção tenha gêneros duplicados).

Quando o usuário pesquisa o item, o valor de pesquisa é mantido na caixa de pesquisa.

Adicionar pesquisa por gênero à exibição Índice

Atualize Index.cshtml, encontrado em Views/Movies/, da seguinte maneira:

@model MvcMovie.Models.MovieGenreViewModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
    <p>

        <select asp-for="MovieGenre" asp-items="Model.Genres">
            <option value="">All</option>
        </select>

        Title: <input type="text" asp-for="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies!)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Examine a expressão lambda usada no auxiliar HTML a seguir:

@Html.DisplayNameFor(model => model.Movies![0].Title)

No código anterior, o Auxiliar de HTML DisplayNameFor inspeciona a propriedade Title referenciada na expressão lambda para determinar o nome de exibição. Como a expressão lambda é inspecionada em vez de avaliada, você não recebe uma violação de acesso quando model, model.Movies ou model.Movies[0] é null ou vazio. Quando a expressão lambda é avaliada (por exemplo, @Html.DisplayFor(modelItem => item.Title)), os valores da propriedade do modelo são avaliados. O ! após model.Movies é o operador que permite valor nulo, que é usado para declarar que Movies não é nulo.

Teste o aplicativo pesquisando por gênero, título do filme e por ambos:

Browser window showing results of https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2

Nesta seção, você adiciona a funcionalidade de pesquisa ao método de ação Index que permite pesquisar filmes por gênero ou nome.

Atualize o método Index encontrado dentro de Controllers/MoviesController.cs com o seguinte código:

public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}

A linha a seguir do método de ação Index cria uma consulta LINQ para selecionar os filmes:

var movies = from m in _context.Movie
             select m;

A consulta é definida apenas neste ponto; ela não foi executada no banco de dados.

Se o parâmetro searchString contiver uma cadeia de caracteres, a consulta de filmes será modificada para filtrar o valor da cadeia de caracteres de pesquisa:

if (!String.IsNullOrEmpty(searchString))
{
    movies = movies.Where(s => s.Title!.Contains(searchString));
}

O código s => s.Title!.Contains(searchString) acima é uma Expressão Lambda. Lambdas são usados em consultas LINQ baseadas em método como argumentos para métodos de operadores de consulta padrão, como o método Where ou Contains (usado no código acima). As consultas LINQ não são executadas quando são definidas ou no momento em que são modificadas com uma chamada a um método, como Where, Contains ou OrderBy. Em vez disso, a execução da consulta é adiada. Isso significa que a avaliação de uma expressão é atrasada até que seu valor realizado seja, de fato, iterado ou o método ToListAsync seja chamado. Para obter mais informações sobre a execução de consulta adiada, consulte Execução da consulta.

O método Contains é executado no banco de dados, não no código C# mostrado acima. A diferenciação de maiúsculas e minúsculas na consulta depende do banco de dados e da ordenação. No SQL Server, Contains é mapeado para SQL LIKE, que não diferencia maiúsculas de minúsculas. No SQLite, com a ordenação padrão, ele diferencia maiúsculas de minúsculas.

Navegue até /Movies/Index. Acrescente uma cadeia de consulta, como ?searchString=Ghost, à URL. Os filmes filtrados são exibidos.

Index view

Se você alterar a assinatura do método Index para que ele tenha um parâmetro chamado id, o parâmetro id corresponderá o espaço reservado {id} opcional com as rotas padrão definidas em Program.cs.

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

Altere o parâmetro para id e todas as ocorrências de searchString para id.

O método Index anterior:

public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}

O método Index atualizado com o parâmetro id:

public async Task<IActionResult> Index(string id)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(id))
    {
        movies = movies.Where(s => s.Title!.Contains(id));
    }

    return View(await movies.ToListAsync());
}

Agora você pode passar o título de pesquisa como dados de rota (um segmento de URL), em vez de como um valor de cadeia de consulta.

Index view with the word ghost added to the Url and a returned movie list of two movies, Ghostbusters and Ghostbusters 2

No entanto, você não pode esperar que os usuários modifiquem a URL sempre que desejarem pesquisar um filme. Agora você adicionará os elementos da interface do usuário para ajudá-los a filtrar filmes. Se você tiver alterado a assinatura do método Index para testar como passar o parâmetro ID associado à rota, altere-o novamente para que ele use um parâmetro chamado searchString:

public async Task<IActionResult> Index(string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    var movies = from m in _context.Movie
                select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}

Abra o arquivo Views/Movies/Index.cshtml e adicione a marcação <form> destacada abaixo:

@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index">
    <p>
        Title: <input type="text" name="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">

A marcação <form> HTML usa o Auxiliar de Marcação de Formulário. Portanto, quando você enviar o formulário, a cadeia de caracteres de filtro será enviada para a ação Index do controlador de filmes. Salve as alterações e, em seguida, teste o filtro.

Index view with the word ghost typed into the Title filter textbox

Não há nenhuma sobrecarga [HttpPost] do método Index que poderia ser esperada. Isso não é necessário, porque o método não está alterando o estado do aplicativo, apenas filtrando os dados.

Você poderá adicionar o método [HttpPost] Index a seguir.

[HttpPost]
public string Index(string searchString, bool notUsed)
{
    return "From [HttpPost]Index: filter on " + searchString;
}

O parâmetro notUsed é usado para criar uma sobrecarga para o método Index. Falaremos sobre isso mais adiante no tutorial.

Se você adicionar esse método, o chamador de ação fará uma correspondência com o método [HttpPost] Index e o método [HttpPost] Index será executado conforme mostrado na imagem abaixo.

Browser window with application response of From HttpPost Index: filter on ghost

No entanto, mesmo se você adicionar esta versão [HttpPost] do método Index, haverá uma limitação na forma de como isso tudo foi implementado. Imagine que você deseja adicionar uma pesquisa específica como Favoritos ou enviar um link para seus amigos para que eles possam clicar para ver a mesma lista filtrada de filmes. Observe que a URL da solicitação HTTP POST é a mesma que a URL da solicitação GET (localhost:{PORT}/Movies/Index) – não há nenhuma informação de pesquisa na URL. As informações de cadeia de caracteres de pesquisa são enviadas ao servidor como um valor de campo de formulário. Verifique isso com as ferramentas do Desenvolvedor do navegador ou a excelente ferramenta Fiddler. A imagem abaixo mostra as ferramentas do Desenvolvedor do navegador Chrome:

Network tab of Microsoft Edge Developer Tools showing a request body with a searchString value of ghost

Veja o parâmetro de pesquisa e o token XSRF no corpo da solicitação. Observe, conforme mencionado no tutorial anterior, que o Auxiliar de Marcação de Formulário gera um token antifalsificação XSRF. Não modificaremos os dados e, portanto, não precisamos validar o token no método do controlador.

Como o parâmetro de pesquisa está no corpo da solicitação e não na URL, não é possível capturar essas informações de pesquisa para adicionar como Favoritos ou compartilhar com outras pessoas. Corrigimos isso especificando que a solicitação deve ser HTTP GET encontrada no arquivo Views/Movies/Index.cshtml.

@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index" method="get">
    <p>
        Title: <input type="text" name="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">

Agora, quando você enviar uma pesquisa, a URL conterá a cadeia de consulta da pesquisa. A pesquisa também irá para o método de ação HttpGet Index, mesmo se você tiver um método HttpPost Index.

Browser window showing the searchString=ghost in the Url and the movies returned, Ghostbusters and Ghostbusters 2, contain the word ghost

A seguinte marcação mostra a alteração para a marcação form:

<form asp-controller="Movies" asp-action="Index" method="get">

Adicionar pesquisa por gênero

Adicione a seguinte classe MovieGenreViewModel à pasta Models:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models;

public class MovieGenreViewModel
{
    public List<Movie>? Movies { get; set; }
    public SelectList? Genres { get; set; }
    public string? MovieGenre { get; set; }
    public string? SearchString { get; set; }
}

O modelo de exibição do gênero de filme conterá:

  • Uma lista de filmes.
  • Uma SelectList que contém a lista de gêneros. Isso permite que o usuário selecione um gênero na lista.
  • MovieGenre, que contém o gênero selecionado.
  • SearchString, que contém o texto que os usuários inserem na caixa de texto de pesquisa.

Substitua o método Index em MoviesController.cs pelo seguinte código:

// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
    if (_context.Movie == null)
    {
        return Problem("Entity set 'MvcMovieContext.Movie'  is null.");
    }

    // Use LINQ to get list of genres.
    IQueryable<string> genreQuery = from m in _context.Movie
                                    orderby m.Genre
                                    select m.Genre;
    var movies = from m in _context.Movie
                 select m;

    if (!string.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.Contains(searchString));
    }

    if (!string.IsNullOrEmpty(movieGenre))
    {
        movies = movies.Where(x => x.Genre == movieGenre);
    }

    var movieGenreVM = new MovieGenreViewModel
    {
        Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
        Movies = await movies.ToListAsync()
    };

    return View(movieGenreVM);
}

O código a seguir é uma consulta LINQ que recupera todos os gêneros do banco de dados.

// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;

A SelectList de gêneros é criada com a projeção dos gêneros distintos (não desejamos que nossa lista de seleção tenha gêneros duplicados).

Quando o usuário pesquisa o item, o valor de pesquisa é mantido na caixa de pesquisa.

Adicionar pesquisa por gênero à exibição Índice

Atualize Index.cshtml, encontrado em Views/Movies/, da seguinte maneira:

@model MvcMovie.Models.MovieGenreViewModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
    <p>

        <select asp-for="MovieGenre" asp-items="Model.Genres">
            <option value="">All</option>
        </select>

        Title: <input type="text" asp-for="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies!)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Examine a expressão lambda usada no auxiliar HTML a seguir:

@Html.DisplayNameFor(model => model.Movies![0].Title)

No código anterior, o Auxiliar de HTML DisplayNameFor inspeciona a propriedade Title referenciada na expressão lambda para determinar o nome de exibição. Como a expressão lambda é inspecionada em vez de avaliada, você não recebe uma violação de acesso quando model, model.Movies ou model.Movies[0] é null ou vazio. Quando a expressão lambda é avaliada (por exemplo, @Html.DisplayFor(modelItem => item.Title)), os valores da propriedade do modelo são avaliados. O ! após model.Movies é o operador que permite valor nulo, que é usado para declarar que Movies não é nulo.

Teste o aplicativo pesquisando por gênero, título do filme e por ambos:

Browser window showing results of https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2

Nesta seção, você adiciona a funcionalidade de pesquisa ao método de ação Index que permite pesquisar filmes por gênero ou nome.

Atualize o método Index encontrado dentro de Controllers/MoviesController.cs com o seguinte código:

public async Task<IActionResult> Index(string searchString)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}

A primeira linha do método de ação Index cria uma consulta LINQ para selecionar os filmes:

var movies = from m in _context.Movie
             select m;

A consulta é somente definida neste ponto; ela não foi executada no banco de dados.

Se o parâmetro searchString contiver uma cadeia de caracteres, a consulta de filmes será modificada para filtrar o valor da cadeia de caracteres de pesquisa:

if (!String.IsNullOrEmpty(searchString))
{
    movies = movies.Where(s => s.Title!.Contains(searchString));
}

O código s => s.Title!.Contains(searchString) acima é uma Expressão Lambda. Lambdas são usados em consultas LINQ baseadas em método como argumentos para métodos de operadores de consulta padrão, como o método Where ou Contains (usado no código acima). As consultas LINQ não são executadas quando são definidas ou no momento em que são modificadas com uma chamada a um método, como Where, Contains ou OrderBy. Em vez disso, a execução da consulta é adiada. Isso significa que a avaliação de uma expressão é atrasada até que seu valor realizado seja, de fato, iterado ou o método ToListAsync seja chamado. Para obter mais informações sobre a execução de consulta adiada, consulte Execução da consulta.

O método Contains é executado no banco de dados, não no código C# mostrado acima. A diferenciação de maiúsculas e minúsculas na consulta depende do banco de dados e da ordenação. No SQL Server, Contains é mapeado para SQL LIKE, que não diferencia maiúsculas de minúsculas. No SQLite, com a ordenação padrão, ele diferencia maiúsculas de minúsculas.

Navegue até /Movies/Index. Acrescente uma cadeia de consulta, como ?searchString=Ghost, à URL. Os filmes filtrados são exibidos.

Index view

Se você alterar a assinatura do método Index para que ele tenha um parâmetro chamado id, o parâmetro id corresponderá o espaço reservado {id} opcional com as rotas padrão definidas em Program.cs.

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

Altere o parâmetro para id e todas as ocorrências de searchString para id.

O método Index anterior:

public async Task<IActionResult> Index(string searchString)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}

O método Index atualizado com o parâmetro id:

public async Task<IActionResult> Index(string id)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(id))
    {
        movies = movies.Where(s => s.Title!.Contains(id));
    }

    return View(await movies.ToListAsync());
}

Agora você pode passar o título de pesquisa como dados de rota (um segmento de URL), em vez de como um valor de cadeia de consulta.

Index view with the word ghost added to the Url and a returned movie list of two movies, Ghostbusters and Ghostbusters 2

No entanto, você não pode esperar que os usuários modifiquem a URL sempre que desejarem pesquisar um filme. Agora você adicionará os elementos da interface do usuário para ajudá-los a filtrar filmes. Se você tiver alterado a assinatura do método Index para testar como passar o parâmetro ID associado à rota, altere-o novamente para que ele use um parâmetro chamado searchString:

public async Task<IActionResult> Index(string searchString)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}

Abra o arquivo Views/Movies/Index.cshtml e adicione a marcação <form> destacada abaixo:

@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index">
    <p>
        Title: <input type="text" name="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>

A marcação <form> HTML usa o Auxiliar de Marcação de Formulário. Portanto, quando você enviar o formulário, a cadeia de caracteres de filtro será enviada para a ação Index do controlador de filmes. Salve as alterações e, em seguida, teste o filtro.

Index view with the word ghost typed into the Title filter textbox

Não há nenhuma sobrecarga [HttpPost] do método Index que poderia ser esperada. Isso não é necessário, porque o método não está alterando o estado do aplicativo, apenas filtrando os dados.

Você poderá adicionar o método [HttpPost] Index a seguir.

[HttpPost]
public string Index(string searchString, bool notUsed)
{
    return "From [HttpPost]Index: filter on " + searchString;
}

O parâmetro notUsed é usado para criar uma sobrecarga para o método Index. Falaremos sobre isso mais adiante no tutorial.

Se você adicionar esse método, o chamador de ação fará uma correspondência com o método [HttpPost] Index e o método [HttpPost] Index será executado conforme mostrado na imagem abaixo.

Browser window with application response of From HttpPost Index: filter on ghost

No entanto, mesmo se você adicionar esta versão [HttpPost] do método Index, haverá uma limitação na forma de como isso tudo foi implementado. Imagine que você deseja adicionar uma pesquisa específica como Favoritos ou enviar um link para seus amigos para que eles possam clicar para ver a mesma lista filtrada de filmes. Observe que a URL da solicitação HTTP POST é a mesma que a URL da solicitação GET (localhost:{PORT}/Movies/Index) – não há nenhuma informação de pesquisa na URL. As informações de cadeia de caracteres de pesquisa são enviadas ao servidor como um valor de campo de formulário. Verifique isso com as ferramentas do Desenvolvedor do navegador ou a excelente ferramenta Fiddler. A imagem abaixo mostra as ferramentas do Desenvolvedor do navegador Chrome:

Network tab of Microsoft Edge Developer Tools showing a request body with a searchString value of ghost

Veja o parâmetro de pesquisa e o token XSRF no corpo da solicitação. Observe, conforme mencionado no tutorial anterior, que o Auxiliar de Marcação de Formulário gera um token antifalsificação XSRF. Não modificaremos os dados e, portanto, não precisamos validar o token no método do controlador.

Como o parâmetro de pesquisa está no corpo da solicitação e não na URL, não é possível capturar essas informações de pesquisa para adicionar como Favoritos ou compartilhar com outras pessoas. Corrigimos isso especificando que a solicitação deve ser HTTP GET encontrada no arquivo Views/Movies/Index.cshtml.

@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index" method="get">
    <p>
        Title: <input type="text" name="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">

Agora, quando você enviar uma pesquisa, a URL conterá a cadeia de consulta da pesquisa. A pesquisa também irá para o método de ação HttpGet Index, mesmo se você tiver um método HttpPost Index.

Browser window showing the searchString=ghost in the Url and the movies returned, Ghostbusters and Ghostbusters 2, contain the word ghost

A seguinte marcação mostra a alteração para a marcação form:

<form asp-controller="Movies" asp-action="Index" method="get">

Adicionar pesquisa por gênero

Adicione a seguinte classe MovieGenreViewModel à pasta Models:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models
{
    public class MovieGenreViewModel
    {
        public List<Movie>? Movies { get; set; }
        public SelectList? Genres { get; set; }
        public string? MovieGenre { get; set; }
        public string? SearchString { get; set; }
    }
}

O modelo de exibição do gênero de filme conterá:

  • Uma lista de filmes.
  • Uma SelectList que contém a lista de gêneros. Isso permite que o usuário selecione um gênero na lista.
  • MovieGenre, que contém o gênero selecionado.
  • SearchString, que contém o texto que os usuários inserem na caixa de texto de pesquisa.

Substitua o método Index em MoviesController.cs pelo seguinte código:

// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
    // Use LINQ to get list of genres.
    IQueryable<string> genreQuery = from m in _context.Movie
                                    orderby m.Genre
                                    select m.Genre;
    var movies = from m in _context.Movie
                 select m;

    if (!string.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.Contains(searchString));
    }

    if (!string.IsNullOrEmpty(movieGenre))
    {
        movies = movies.Where(x => x.Genre == movieGenre);
    }

    var movieGenreVM = new MovieGenreViewModel
    {
        Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
        Movies = await movies.ToListAsync()
    };

    return View(movieGenreVM);
}

O código a seguir é uma consulta LINQ que recupera todos os gêneros do banco de dados.

// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;

A SelectList de gêneros é criada com a projeção dos gêneros distintos (não desejamos que nossa lista de seleção tenha gêneros duplicados).

Quando o usuário pesquisa o item, o valor de pesquisa é mantido na caixa de pesquisa.

Adicionar pesquisa por gênero à exibição Índice

Atualize Index.cshtml, encontrado em Views/Movies/, da seguinte maneira:

@model MvcMovie.Models.MovieGenreViewModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
    <p>

        <select asp-for="MovieGenre" asp-items="Model.Genres">
            <option value="">All</option>
        </select>

        Title: <input type="text" asp-for="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Examine a expressão lambda usada no auxiliar HTML a seguir:

@Html.DisplayNameFor(model => model.Movies[0].Title)

No código anterior, o Auxiliar de HTML DisplayNameFor inspeciona a propriedade Title referenciada na expressão lambda para determinar o nome de exibição. Como a expressão lambda é inspecionada em vez de avaliada, você não recebe uma violação de acesso quando model, model.Movies ou model.Movies[0] é null ou vazio. Quando a expressão lambda é avaliada (por exemplo, @Html.DisplayFor(modelItem => item.Title)), os valores da propriedade do modelo são avaliados.

Teste o aplicativo pesquisando por gênero, título do filme e por ambos:

Browser window showing results of https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2

Nesta seção, você adiciona a funcionalidade de pesquisa ao método de ação Index que permite pesquisar filmes por gênero ou nome.

Atualize o método Index encontrado dentro de Controllers/MoviesController.cs com o seguinte código:

public async Task<IActionResult> Index(string searchString)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}

A primeira linha do método de ação Index cria uma consulta LINQ para selecionar os filmes:

var movies = from m in _context.Movie
             select m;

A consulta é somente definida neste ponto; ela não foi executada no banco de dados.

Se o parâmetro searchString contiver uma cadeia de caracteres, a consulta de filmes será modificada para filtrar o valor da cadeia de caracteres de pesquisa:

if (!String.IsNullOrEmpty(searchString))
{
    movies = movies.Where(s => s.Title.Contains(searchString));
}

O código s => s.Title.Contains() acima é uma Expressão Lambda. Lambdas são usados em consultas LINQ baseadas em método como argumentos para métodos de operadores de consulta padrão, como o método Where ou Contains (usado no código acima). As consultas LINQ não são executadas quando são definidas ou no momento em que são modificadas com uma chamada a um método, como Where, Contains ou OrderBy. Em vez disso, a execução da consulta é adiada. Isso significa que a avaliação de uma expressão é atrasada até que seu valor realizado seja, de fato, iterado ou o método ToListAsync seja chamado. Para obter mais informações sobre a execução de consulta adiada, consulte Execução da consulta.

O método Contains é executado no banco de dados, não no código C# mostrado acima. A diferenciação de maiúsculas e minúsculas na consulta depende do banco de dados e da ordenação. No SQL Server, Contains é mapeado para SQL LIKE, que não diferencia maiúsculas de minúsculas. No SQLite, com a ordenação padrão, ele diferencia maiúsculas de minúsculas.

Navegue até /Movies/Index. Acrescente uma cadeia de consulta, como ?searchString=Ghost, à URL. Os filmes filtrados são exibidos.

Index view

Se você alterar a assinatura do método Index para que ele tenha um parâmetro chamado id, o parâmetro id corresponderá o espaço reservado {id} opcional com as rotas padrão definidas em Startup.cs.

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllerRoute(
        name: "default",
        pattern: "{controller=Home}/{action=Index}/{id?}");
});

Altere o parâmetro para id e todas as ocorrências de searchString altere para id.

O método Index anterior:

public async Task<IActionResult> Index(string searchString)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}

O método Index atualizado com o parâmetro id:

public async Task<IActionResult> Index(string id)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(id))
    {
        movies = movies.Where(s => s.Title.Contains(id));
    }

    return View(await movies.ToListAsync());
}

Agora você pode passar o título de pesquisa como dados de rota (um segmento de URL), em vez de como um valor de cadeia de consulta.

Index view with the word ghost added to the Url and a returned movie list of two movies, Ghostbusters and Ghostbusters 2

No entanto, você não pode esperar que os usuários modifiquem a URL sempre que desejarem pesquisar um filme. Agora você adicionará os elementos da interface do usuário para ajudá-los a filtrar filmes. Se você tiver alterado a assinatura do método Index para testar como passar o parâmetro ID associado à rota, altere-o novamente para que ele use um parâmetro chamado searchString:

public async Task<IActionResult> Index(string searchString)
{
    var movies = from m in _context.Movie
                 select m;

    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title.Contains(searchString));
    }

    return View(await movies.ToListAsync());
}

Abra o arquivo Views/Movies/Index.cshtml e adicione a marcação <form> destacada abaixo:

    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>

<form asp-controller="Movies" asp-action="Index">
    <p>
        Title: <input type="text" name="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>

A marcação <form> HTML usa o Auxiliar de Marcação de Formulário. Portanto, quando você enviar o formulário, a cadeia de caracteres de filtro será enviada para a ação Index do controlador de filmes. Salve as alterações e, em seguida, teste o filtro.

Index view with the word ghost typed into the Title filter textbox

Não há nenhuma sobrecarga [HttpPost] do método Index que poderia ser esperada. Isso não é necessário, porque o método não está alterando o estado do aplicativo, apenas filtrando os dados.

Você poderá adicionar o método [HttpPost] Index a seguir.

[HttpPost]
public string Index(string searchString, bool notUsed)
{
    return "From [HttpPost]Index: filter on " + searchString;
}

O parâmetro notUsed é usado para criar uma sobrecarga para o método Index. Falaremos sobre isso mais adiante no tutorial.

Se você adicionar esse método, o chamador de ação fará uma correspondência com o método [HttpPost] Index e o método [HttpPost] Index será executado conforme mostrado na imagem abaixo.

Browser window with application response of From HttpPost Index: filter on ghost

No entanto, mesmo se você adicionar esta versão [HttpPost] do método Index, haverá uma limitação na forma de como isso tudo foi implementado. Imagine que você deseja adicionar uma pesquisa específica como Favoritos ou enviar um link para seus amigos para que eles possam clicar para ver a mesma lista filtrada de filmes. Observe que a URL da solicitação HTTP POST é a mesma que a URL da solicitação GET (localhost:{PORT}/Movies/Index) – não há nenhuma informação de pesquisa na URL. As informações de cadeia de caracteres de pesquisa são enviadas ao servidor como um valor de campo de formulário. Verifique isso com as ferramentas do Desenvolvedor do navegador ou a excelente ferramenta Fiddler. A imagem abaixo mostra as ferramentas do Desenvolvedor do navegador Chrome:

Network tab of Microsoft Edge Developer Tools showing a request body with a searchString value of ghost

Veja o parâmetro de pesquisa e o token XSRF no corpo da solicitação. Observe, conforme mencionado no tutorial anterior, que o Auxiliar de Marcação de Formulário gera um token antifalsificação XSRF. Não modificaremos os dados e, portanto, não precisamos validar o token no método do controlador.

Como o parâmetro de pesquisa está no corpo da solicitação e não na URL, não é possível capturar essas informações de pesquisa para adicionar como Favoritos ou compartilhar com outras pessoas. Corrigimos isso especificando que a solicitação deve ser HTTP GET encontrada no arquivo Views/Movies/Index.cshtml.

@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
    <p>
        Title: <input type="text" name="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Title)

Agora, quando você enviar uma pesquisa, a URL conterá a cadeia de consulta da pesquisa. A pesquisa também irá para o método de ação HttpGet Index, mesmo se você tiver um método HttpPost Index.

Browser window showing the searchString=ghost in the Url and the movies returned, Ghostbusters and Ghostbusters 2, contain the word ghost

A seguinte marcação mostra a alteração para a marcação form:

<form asp-controller="Movies" asp-action="Index" method="get">

Adicionar pesquisa por gênero

Adicione a seguinte classe MovieGenreViewModel à pasta Models:

using Microsoft.AspNetCore.Mvc.Rendering;
using System.Collections.Generic;

namespace MvcMovie.Models
{
    public class MovieGenreViewModel
    {
        public List<Movie> Movies { get; set; }
        public SelectList Genres { get; set; }
        public string MovieGenre { get; set; }
        public string SearchString { get; set; }
    }
}

O modelo de exibição do gênero de filme conterá:

  • Uma lista de filmes.
  • Uma SelectList que contém a lista de gêneros. Isso permite que o usuário selecione um gênero na lista.
  • MovieGenre, que contém o gênero selecionado.
  • SearchString, que contém o texto que os usuários inserem na caixa de texto de pesquisa.

Substitua o método Index em MoviesController.cs pelo seguinte código:

// GET: Movies
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
    // Use LINQ to get list of genres.
    IQueryable<string> genreQuery = from m in _context.Movie
                                    orderby m.Genre
                                    select m.Genre;

    var movies = from m in _context.Movie
                 select m;

    if (!string.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title.Contains(searchString));
    }

    if (!string.IsNullOrEmpty(movieGenre))
    {
        movies = movies.Where(x => x.Genre == movieGenre);
    }

    var movieGenreVM = new MovieGenreViewModel
    {
        Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
        Movies = await movies.ToListAsync()
    };

    return View(movieGenreVM);
}

O código a seguir é uma consulta LINQ que recupera todos os gêneros do banco de dados.

// Use LINQ to get list of genres.
IQueryable<string> genreQuery = from m in _context.Movie
                                orderby m.Genre
                                select m.Genre;

A SelectList de gêneros é criada com a projeção dos gêneros distintos (não desejamos que nossa lista de seleção tenha gêneros duplicados).

Quando o usuário pesquisa o item, o valor de pesquisa é mantido na caixa de pesquisa.

Adicionar pesquisa por gênero à exibição Índice

Atualize Index.cshtml, encontrado em Views/Movies/, da seguinte maneira:

@model MvcMovie.Models.MovieGenreViewModel

@{
    ViewData["Title"] = "Index";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<form asp-controller="Movies" asp-action="Index" method="get">
    <p>

        <select asp-for="MovieGenre" asp-items="Model.Genres">
            <option value="">All</option>
        </select>

        Title: <input type="text" asp-for="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies[0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.Title)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReleaseDate)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Genre)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Price)
                </td>
                <td>
                    <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> |
                    <a asp-action="Details" asp-route-id="@item.Id">Details</a> |
                    <a asp-action="Delete" asp-route-id="@item.Id">Delete</a>
                </td>
            </tr>
        }
    </tbody>
</table>

Examine a expressão lambda usada no auxiliar HTML a seguir:

@Html.DisplayNameFor(model => model.Movies[0].Title)

No código anterior, o Auxiliar de HTML DisplayNameFor inspeciona a propriedade Title referenciada na expressão lambda para determinar o nome de exibição. Como a expressão lambda é inspecionada em vez de avaliada, você não recebe uma violação de acesso quando model, model.Movies ou model.Movies[0] é null ou vazio. Quando a expressão lambda é avaliada (por exemplo, @Html.DisplayFor(modelItem => item.Title)), os valores da propriedade do modelo são avaliados.

Teste o aplicativo pesquisando por gênero, título do filme e por ambos:

Browser window showing results of https://localhost:5001/Movies?MovieGenre=Comedy&SearchString=2