.NET

18 out, 2017

ASP .NET Core – Iniciando com ASP .NET Core MVC e Entity Framework Core no VS 2017 – Parte 04

Publicidade

Neste artigo iniciaremos a criação de uma aplicação ASP .NET Core MVC usando o Entity Framework Core no Visual Studio 2017.

Continuando a terceira parte do artigo, vamos criar e ajustar os métodos CRUDCreate , Read, Update e Delete gerados pelo Scaffolding.

Customizando os métodos CRUD

Até o momento, temos uma aplicação Web MVC que permite acessar e exibir informações de um banco de dados SQL Server Local DB usando o Entity Framework.

No artigo anterior nós criamos um controlador e as respectivas views para gerenciar as informações sobre estudantes usando o Scaffolding, e dessa forma, agora podemos acessar e exibir os dados dos estudantes, bem como editar, incluir e excluir suas informações.

Vamos rever e customizar o código CRUD gerado pelo Scaffolding começando pela página de detalhes de um estudante: a página Details

Executando a aplicação e clicando no link Detalhes, iremos veremos a exibição da página Details abaixo:

Observe que o código gerado pelo Scaffolding para página não esta exibindo as informações sobre a propriedade Matriculas definida na classe Estudante, pois essa propriedade representa uma coleção. A página, portanto, não esta exibindo as matriculas feitas pelo estudante.

Vamos ajustar isso e exibir a coleção de matriculas em uma tabela HTML. Para começar, temos que primeiro alterar o código do método Action Details do controlador EstudantesController.

No código original gerado pelo Scaffolding o método Action Details utiliza o método SingleOrDefaulAsync para recuperar uma única entidade Estudante.

Vamos alterar o código original (que foi comentado), incluindo o código destacado em azul conforme mostrado abaixo:

 // GET: Estudantes/Details/5
        public async Task<IActionResult> Details(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }
            //var estudante = await _context.Estudantes
            //    .SingleOrDefaultAsync(m => m.EstudanteID == id);
            var estudante = await _context.Estudantes
                .Include(s => s.Matriculas)
                    .ThenInclude(e => e.Curso)
                .AsNoTracking()
                .SingleOrDefaultAsync(m => m.EstudanteID == id);
            if (estudante == null)
            {
                return NotFound();
            }
            return View(estudante);
        }

Alteramos a consulta para agora retornar um estudante e suas matriculas onde os métodos Include e ThenInclude fazem com que o contexto carregue a propriedade de navegação Estudantes.Matriculas e dentro de cada inscrição, a propriedade de navegação Matricula.Curso.

O método AsNoTracking melhora o desempenho em cenários onde as entidades retornadas não serão atualizadas no tempo de vida do contexto atual.

Nota: O método de extensão AsNoTracking() retorna uma nova consulta e as entidades retornadas não serão armazenadas em cache pelo contexto (DbContext ou Object Context). Isso significa que o Entity Framework não executa qualquer processamento ou armazenamento adicional das entidades retornadas pela consulta. Observe que não é possível atualizar essas entidades sem anexar ao contexto.

O valor chave (Id) que é passado para o método Details vem dos dados da rota. Os dados de rota são dados que o model binder encontra em um segmento da URL. Por exemplo, a rota padrão especifica os segmentos controller, action e id conforme vemos abaixo:

App.UseMvc (routes =>
{
    Routes.MapRoute (
    Nome: "default",
    Template: "{controller = Home} / {action = Index} / {id?}");
});

Nota : O model binder da ASP.NET Core MVC mapeia dados de solicitações HTTP para parâmetros dos métodos Actions. Os parâmetros podem ser tipos simples, como strings, inteiros ou floats, ou podem ser tipos complexos.

Na URL a seguir, a rota padrão mapeia o Instrutor como sendo o Controller, Index como a sendo a Action e 1 como o id; Esses são valores de dados de rota.

Http://localhost:1230/Instrutor/Index/1?CursoID=2021

A última parte do URL (“?CursoID=2021”) é um valor de seqüência de caracteres de consulta. O model binder também passará o valor ID para o parâmetro ID do método Details se o passar como um valor de cadeia de consulta:

Http://localhost:1230/Instrutor/Index?Id=1&CursoID=2021

Na página Index, as URLs de hiperlink são criadas por instruções de tag helper na exibição Razor. No código Razor a seguir, o parâmetro id corresponde à rota padrão, dessa forma, id é adicionado aos dados da rota.

<a asp-action=”Edit” asp-route-id=”@item.ID”>Editar</a>

Quando o valor de ID for 6, esse código gera o seguinte HTML:

<a href=”/Estudantes/Edit/6″>Editar</a>

Agora temos que exibir as informações na respectiva View.

Para isso, vamos abrir a view Details.cshtml na pasta Views/Estudantes e vamos incluir o código em azul conforme abaixo:

@model UniversidadeMacoratti.Models.Estudante
@{
    ViewData["Title"] = "Details";
}
<h2>Detalhes</h2>
<div>
    <h4>Estudante</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.SobreNome)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.SobreNome)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Nome)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Nome)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.DataMatricula)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.DataMatricula)
        </dd>
        <!--Exibe as matriculas do estudante -->
        <dt>
            @Html.DisplayNameFor(model => model.Matriculas)
        </dt>
        <dd>
            <table class="table">
                <tr>
                    <th>Curso</th>
                    <th>Nota</th>
                </tr>
                @foreach (var item in Model.Matriculas)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Curso.Titulo)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Nota)
                        </td>
                    </tr>
                }
            </table>
        </dd>
        <!-- fim -->
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.EstudanteID">Editar</a> |
    <a asp-action="Index">Retorna para Lista</a>
</div>

O código inserido cria uma tabela HTML e itera através das entidades na propriedade de navegação Matriculas, onde para cada matricula, ele exibe o nome do curso e a nota.

O nome do curso é retornado a partir da entidade Curso que esta armazenada na propriedade de navegação da entidade Matriculas.

Executando o projeto novamente e clicando no link detalhes para um estudante, agora iremos obter o seguinte resultado:

Atualizando a página Create

Vamos agora ajustar o código do método Create do controlador EstudantesController conforme o código a seguir:

  [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("SobreNome,Nome,DataMatricula")] Estudante estudante)
        {
            try
            {
                if (ModelState.IsValid)
                {
                    _context.Add(estudante);
                    await _context.SaveChangesAsync();
                    return RedirectToAction("Index");
                }
            }
            catch (DbUpdateException /* ex */)
            {
                //Logar o erro (descomente a variável ex e escreva um log
                ModelState.AddModelError("", "Não foi possível salvar. " +
                    "Tente novamente, e se o problema persistir " +
                    "chame o suporte.");
            }
            return View(estudante);
        }

Esse código adiciona a entidade Estudante criada pelo model binder da ASP.NET MVC ao conjunto de entidades Estudantes e salva as alterações no banco de dados.

Nós removemos EstudanteID do atributo Bind porque EstudanteID é o valor da chave primária que o SQL Server definirá automaticamente quando a linha for inserida. A entrada do usuário não define o valor ID.

Diferente do atributo Bind, o bloco try-catch é a única alteração que fizemos no código gerado. Se uma exceção que deriva de DbUpdateException é detectada enquanto as alterações estão sendo salvas, uma mensagem de erro genérica é exibida. As exceções do DbUpdateException às vezes são causadas por algo externo ao aplicativo ao invés de um erro de programação, pelo qual o usuário é aconselhado a tentar novamente. Embora não implementado neste exemplo, um aplicativo de qualidade de produção registraria a exceção usando um log.

Nota: O atributo Bind que o código gerado inclui no método Create é uma maneira de se proteger contra o overposting em cenários de criação de informações. Ele limita o que o model binder usa quando ele cria uma instância de Estudante.

O atributo ValidateAntiForgeryToken ajuda a evitar ataques de falsificação de solicitação entre sites (CSRF). O token é automaticamente injetado na visualização por FormTagHelper e é incluído quando o formulário é enviado pelo usuário. O token é validado pelo atributo ValidateAntiForgeryToken.

Vamos testar a criação de um novo Estudante. Execute a aplicação novamente e clique no link Estudantes e a seguir, em Criar Novo;

Informe os dados para o novo estudante e clique no botão Criar;

Vemos o resultado exibido nas páginas abaixo:

Ajustando a página para Editar dados: O método Edit (HttpGet e HttpPost)

Vamos agora alterar o código dos métodos Edit do controlador EstudantesController. Note que temos dois métodos Edit. Um que não usa nenhum atributo, e neste caso é o método Edit HttpGet e o outro método que usa o atributo, [HttPost].

O método HttpGet não precisa ser alterado. Vamos alterar o método Edit HttpPost conforme mostrado abaixo:

 [HttpPost, ActionName("Edit")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> EditPost(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }
            var atualizarEstudante = await _context.Estudantes.SingleOrDefaultAsync(s => s.EstudanteID == id);
            if (await TryUpdateModelAsync<Estudante>(
                atualizarEstudante,
                "",
                s => s.Nome, s => s.SobreNome, s => s.DataMatricula))
            {
                try
                {
                    await _context.SaveChangesAsync();
                    return RedirectToAction("Index");
                }
                catch (DbUpdateException /* ex */)
                {
                    //Logar o erro (descomente a variável ex e escreva um log
                    ModelState.AddModelError("", "Não foi possível salvar. " +
                        "Tente novamente, e se o problema persistir " +
                        "chame o suporte.");
                }
            }
            return View(atualizarEstudante);
        }

As mudanças feitas no código implementam uma prática recomendada de segurança para evitar a sobreposição de post. O código gerado pelo Scaffolding usava o atributo Bind e adicionava a entidade criada pelo model binder ao conjunto de entidades com um sinalizador Modified. Esse código não é recomendado para muitos cenários porque o atributo Bind limpa quaisquer dados pré-existentes em campos não listados no parâmetro Include.

O novo código lê a entidade existente e chama TryUpdateModel para atualizar os campos na entidade recuperada, com base na entrada do usuário nos dados do formulário postados.

O controle de alterações automático do Entity Framework define o sinalizador Modified nos campos que são alterados pela entrada de formulário. Quando o método SaveChanges é chamado, o Entity Framework cria instruções SQL para atualizar o registro do banco de dados. Os conflitos de simultaneidade são ignorados, e somente as colunas da tabela atualizadas pelo usuário são atualizadas no banco de dados.

Como uma prática mais recomendada para evitar o overposting, os campos que você deseja que sejam atualizáveis pela página Editar estão na lista vazia nos parâmetros TryUpdateModel. (A string vazia que precede a lista de campos na lista de parâmetros é para um prefixo a ser usado com os nomes dos campos de formulário.) Atualmente, não há campos extras que você está protegendo, mas listando os campos que você deseja que o model binder vincule, garante quem caso você adicione campos ao modelo de dados no futuro, eles serão protegidos automaticamente até que você os adicione explicitamente aqui.

Como resultado dessas alterações, a assinatura de método do método HttpPost Edit é a mesma que o método HttpGet Edit; por isso alteramos o nome do método para EditPost.

Uma palavra sobre os estados das entidades

O contexto do banco de dados faz o controle se as entidades na memória estão sincronizadas com suas linhas correspondentes no banco de dados e essas informações determinam o que acontece quando você chama o método SaveChanges. Por exemplo, quando você passa uma nova entidade para o método Add, o estado dessa entidade é definido como Added e a seguir, quando você chama o método SaveChanges, o contexto do banco de dados emite um comando SQL INSERT.

O estado da entidade é uma enumeração do tipo System.Data.EntityState que declara os seguintes valores:

  • Added – A entidade é marcada como adicionada
  • Deleted – A entidade é marcada como deletada
  • Modified – A entidade foi modificada
  • Unchanged – A entidade não foi modificada
  • Detached – A entidade não esta sendo tratada no contexto

Em um aplicativo desktop, as alterações de estado são normalmente definidas automaticamente. Você lê uma entidade e faz alterações em alguns de seus valores de propriedade. Isso faz com que seu estado seja alterado automaticamente para Modified. Em seguida, quando você chamar SaveChanges, o Entity Framework gera uma instrução SQL UPDATE, que atualiza somente as propriedades reais que você alterou.

Em um aplicação web, o DbContext que inicialmente lê uma entidade e exibe seus dados a serem editados, é descartado após uma página ser processada. Quando o método Action HttpPost Edit é chamado, uma nova solicitação é feita e você tem uma nova instância do DbContext. Se você reler a entidade nesse novo contexto, você simula o processamento da área de trabalho.

Agora vamos testar a edição de dados.

Execute a aplicação e clique no link Editar para um estudante e altere uma informação clicando a seguir no botão Salvar.

Abaixo vemos o resultado da alteração feita.

Ajustando o método Delete e sua View

Agora é a vez de alterarmos o método HttpGet Delete do controlador EstudantesController. Esse método usa o SingleOrDefaultAsync para recuperar a entidade estudante selecionada, como você viu nos métodos Details e Edit. No entanto, para implementar uma mensagem de erro personalizada, quando a chamada para SaveChanges falhar, vamos adicionar algumas funcionalidades à esse método e à sua view correspondente.

Como vimos nas operações para atualizar e criar um estudante, a operação para excluir um estudante também exige dois métodos Action.

  1. O método que é chamado em resposta a uma solicitação GET exibe uma view que dá ao usuário a chance de aprovar ou cancelar a operação de exclusão.
  2. Se o usuário aprovar, um pedido POST é criado. Quando isso acontece, o método HttpPost Delete é chamado e, em seguida, esse método realmente executa a operação de exclusão.

Vamos adicionar um bloco try-catch ao método HttpPost Delete para lidar com quaisquer erros que possam ocorrer quando o banco de dados for atualizado. Se ocorrer um erro, o método HttpPost Delete chama o método HttpGet Delete, passando um parâmetro que indica que ocorreu um erro. O método HttpGet Delete então exibirá novamente a página de confirmação juntamente com a mensagem de erro, dando ao usuário a oportunidade de cancelar ou tentar novamente.

Substitua o método Action HttpGet Delete com o código a seguir, que gerencia o relatório de erros.

  public async Task<IActionResult> Delete(int? id, bool? saveChangesError = false)
        {
            if (id == null)
            {
                return NotFound();
            }
            var estudante = await _context.Estudantes
                .AsNoTracking()
                .SingleOrDefaultAsync(m => m.EstudanteID == id);

            if (estudante == null)
            {
                return NotFound();
            }
            if (saveChangesError.GetValueOrDefault())
            {
                ViewData["ErrorMessage"] =
                    "A exclusão falhou. Tente novamente e se o problema persistir " +
                    "contate o suporte.";
            }
            return View(estudante);
        }

Este código aceita um parâmetro opcional que indica se o método foi chamado após uma falha para salvar as alterações. Esse parâmetro é false quando o método HttpGet Delete é chamado sem uma falha anterior. Quando ele é chamado pelo método HttpPost Delete em resposta a um erro de atualização de banco de dados, o parâmetro é true e uma mensagem de erro é passada para a exibição.

Agora vamos ajustar o método HttPost Delete substitindo o método Action HttpPost Delete pelo código abaixo onde alteramos o nome do método para DeleteConfirmed.

 [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(int id)
        {
            var student = await _context.Estudantes
                .AsNoTracking()
                .SingleOrDefaultAsync(m => m.EstudanteID == id);
            if (estudante == null)
            {
                return RedirectToAction("Index");
            }
            try
            {
                _context.Estudantes.Remove(estudante);
                await _context.SaveChangesAsync();
                return RedirectToAction("Index");
            }
            catch (DbUpdateException /* ex */)
            {
                //Logar o erro
                return RedirectToAction("Delete", new { id = id, saveChangesError = true });
            }
        }

Esse código retorna uma entidade selecionada, e então chama o método Remove para definir o status da entidade como Deleted. Quando o método SaveChanges for invocado, o comando SQL DELETE é gerado para excluir o registro da tabela.

Agora para concluir vamos alterar a view Delete.cshtml na pasta /Views/Estudantes incluindo uma mensagem de erro na página conforme o código abaixo:

@model UniversidadeMacoratti.Models.Estudante
@{
    ViewData["Title"] = "Delete";
}
<h2>Deletar</h2>
<p class="text-danger">@ViewData["ErrorMessage"]</p>
<h3>Tem certeza que deseja deletar este registro?</h3>
<div>
    <h4>Estudante</h4>
    <hr />
    <dl class="dl-horizontal">
        <input type="hidden" asp-for="EstudanteID" />
        <dt>
            @Html.DisplayNameFor(model => model.SobreNome)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.SobreNome)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Nome)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Nome)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.DataMatricula)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.DataMatricula)
        </dd>
    </dl>
    <form asp-action="Delete">
        <div class="form-actions no-color">
            <input type="submit" value="Deletar" class="btn btn-default" /> |
            <a asp-action="Index">Retornar para Lista</a>
        </div>
    </form>
</div>

Agora vamos testar a exclusão de dados executando novamente a aplicação, selecionado um estudante e clicando no botão Deletar:

A página Index irá exibir a relação de estudantes sem o estudante excluído.

E assim concluímos os ajustes nas operações CRUD para a nossa entidade Estudante.

Fechando conexões de banco de dados

Para liberar os recursos que uma conexão de banco de dados utiliza, a instância de contexto deve ser descartada assim que você terminar de usá-la. A injeção de dependência incorporada do ASP.NET Core cuida dessa tarefa para você.

No arquivo Startup.cs chamamos o método de extensão AddDbContext para provisionar a classe DbContext no recipiente ASP.NET DI. Esse método define a vida útil do serviço como Scoped por padrão. Scoped significa que o tempo de vida do objeto de contexto coincide com o tempo de vida da requisição web e o método Dispose será chamado automaticamente no final da solicitação web.

Gerenciando Transações

Por padrão, o Entity Framework implicitamente implementa transações. Nos cenários em que você faz alterações em várias linhas ou tabelas e em seguida chama SaveChanges, o Entity Framework automaticamente garante que todas as alterações sejam bem-sucedidas ou falhem. Se algumas alterações são feitas primeiro e em seguida ocorre um erro, essas alterações são automaticamente revertidas.

Consultas de não acompanhamento (No-Tracking)

Quando um contexto de banco de dados recupera linhas de uma tabela e cria objetos de entidade que os representam, por padrão, ele mantém um registro se as entidades na memória estão sincronizadas com o que está no banco de dados. Os dados na memória atuam como um cache e são usados ​​quando você atualiza uma entidade. Esse armazenamento em cache é muitas vezes desnecessário em um aplicativo Web, pois as instâncias de contexto são normalmente de curta duração (uma nova é criada e disposta para cada solicitação) e o contexto que lê uma entidade normalmente é descartado antes que essa entidade seja usada novamente.

Você pode desabilitar o rastreamento de objetos de entidade na memória chamando o método AsNoTracking. Isso melhora o desempenho.

A seguir algumas situações onde podemos desabilitar o rastreamento :

  1. Durante a vida do contexto, você não precisa atualizar quaisquer entidades e não precisa que o EF carregue automaticamente propriedades de navegação com entidades recuperadas por consultas separadas. Frequentemente, estas condições são satisfeitas nos métodos Action HttpGet do controlador.
  2. Você está executando uma consulta que recupera um grande volume de dados e somente uma pequena parte dos dados retornados serão atualizados. Pode ser mais eficiente desativar o rastreamento para a consulta e executar uma consulta mais tarde para as poucas entidades que precisam ser atualizadas.
  3. Você deseja anexar uma entidade para atualizá-la, mas antes você recuperou a mesma entidade para uma finalidade diferente. Como a entidade já está sendo rastreada pelo contexto do banco de dados, não é possível anexar a entidade que você deseja alterar. Uma maneira de lidar com essa situação é chamar AsNoTracking na consulta anterior.

Na próxima parte do artigo vamos continuar realizando operações de filtragem, ordenação, paginação e agrupamento.