Desenvolvimento

12 nov, 2018

Construindo uma aplicação Web completa com Blazor – Parte 02

Publicidade

Fala, galera!

Continuando nossa aplicação Web com Blazor, vamos implementar nossas classes de domínio e acesso ao banco de dados de forma rápida para podermos seguir rápido e utilizar o Blazor pra valer.

GitHub

O projeto está disponível no GitHub, com cada parte da série em uma branch, sendo a master atualizada sempre com a última parte publicada. Segue a URL do repositório:

Antes de começar

Antes de começarmos a segunda parte, muitas pessoas me perguntaram sobre o uso do Visual Studio Code, CLI do .NET Core, etc. Vamos ver de forma rápida como criar a mesma estrutura que fizemos e aprendemos na parte 1, mas com o CLI do .NET Core e o Visual Studio Code, ok?

Para começar, precisamos instalar/atualizar os templates do Blazor para o .NET CLI. Para isso, basta executar o comando abaixo:

dotnet new -i Microsoft.AspNetCore.Blazor.Templates

Feito isso, vamos executar o comando a seguir no diretório de trabalho do projeto:

dotnet new blazorhosted — name Kanban

Esse comando criará a mesma estrutura com os três projetos em uma solução, como fizemos na parte 1 com o Visual Studio. O argumento fornecido para o parâmetro —name dará nome para solução e projetos.

Com isso, teremos a seguinte estrutura de diretórios criada, quando abrirmos no Visual Studio Code:

Estrutura da solução criada com o CLI do .NET Core, aberta no Visual Studio Code

Mas ainda tem uma tarefa a ser feita. Removam o arquivo global.json, pois, por padrão, ele “trava” a versão do .NET Core na 2.1.300. Isso já tem uma issue aberta (1386) no projeto oficial do Blazor e deve ser disponibilizado nos próximos dias com a versão 0.6.0.

Outro detalhe importante! O CLI não gera o arquivo .gitignore. No repositório que compartilhei no início do artigo tem um exemplo de arquivo para ser utilizado. Basta fazer o download desse arquivo. Clonem o repositório e comecem esta parte a partir da branch parte 1 como base.

Com isso, podemos prosseguir com o artigo. Lembrando sempre que para executar o projeto “como um todo”, devemos executar o comando dotnet run dentro do diretório Kanban.Server, pois ele fornece tanto as APIs como o client do Blazor, como expliquei na parte 1, certo?

Um último detalhe não obrigatório: o repositório tem um arquivo de exemplo do Azure Pipelines (azure-pipelines.yml). No meu caso, sempre que um PR for aberto para minha branch master ou algo for alterado nela, o Azure Devops executará o Build da aplicação para validar se o código está correto.

Classes de domínio

Começaremos a criar nossas classes de domínio no projeto Shared. Abaixo, temos o código de cada uma delas. Vamos criá-las dentro de um diretório chamado Domain, que criaremos no projeto Shared.

Notem que todas as classes possuem suas propriedades com setters privados e um construtor para “preencher” estas propriedades. Com isso, vamos trabalhar com objetos imutáveis, onde apenas métodos de negócio poderão gerar novos objetos com as modificações que precisamos. Vamos ver estes métodos quando começarmos a implementar nossa lógica de negócios.

Artifact

using System.Collections.Generic;

namespace Kanban.Shared.Domain
{
    public class Artifact
    {
        public Artifact(long id, string name, ArtifactType type, Project project, User assignedUser, ArtifactStatus status, Iteration iteration)
        {
            Id = id;
            Name = name;
            Type = type;
            Project = project;
            AssignedUser = assignedUser;
            Status = status;
            Iteration = iteration;
        }

        //For EF mapping
        protected Artifact()
        {}

        public long Id { get; private set; }
        public string Name { get; private set; }
        public ArtifactType Type { get; private set; }
        public Project Project { get; private set; }
        public User AssignedUser { get; private set; }
        public ArtifactStatus Status { get; private set; }
        public Iteration Iteration { get; private set; }
        public List<ArtifactAttachment> Attachments { get; private set; }
    }
}

ArtifactAttachment

namespace Kanban.Shared.Domain
{
    public class ArtifactAttachment
    {
        public ArtifactAttachment(long id, string name, byte[] content, Artifact artifact)
        {
            Id = id;
            Name = name;
            Content = content;
            Artifact = artifact;
        }

        //For EF mapping
        public ArtifactAttachment()
        {}

        public long Id { get; private set; }
        public string Name { get; private set; }
        public byte[] Content { get; private set; }
        public Artifact Artifact { get; private set; }
    }
}

ArtifactStatus

namespace Kanban.Shared.Domain
{
    public class ArtifactStatus
    {
        public ArtifactStatus(long id, string name, bool active)
        {
            Id = id;
            Name = name;
            Active = active;
        }

        //For EF mapping
        protected ArtifactStatus()
        {}

        public long Id { get; private set; }
        public string Name { get; private set; }
        public bool Active { get; private set; }
        public ArtifactType ArtifactType { get; private set; }
    }
}

ArtifactType

namespace Kanban.Shared.Domain
{
    public class ArtifactStatus
    {
        public ArtifactStatus(long id, string name, bool active, ArtifactType type)
        {
            Id = id;
            Name = name;
            Active = active;
            Type = type;
        }

        //For EF mapping
        protected ArtifactStatus()
        {}

        public long Id { get; private set; }
        public string Name { get; private set; }
        public bool Active { get; private set; }
        public ArtifactType Type { get; private set; }
    }
}

Iteration

using System.Collections.Generic;

namespace Kanban.Shared.Domain
{
    public class Team
    {
        public Team(long id, string name, bool active)
        {
            Id = id;
            Name = name;
            Active = active;
        }

        //For EF mapping
        protected Team()
        {}

        public long Id { get; private set; }
        public string Name { get; private set; }
        public bool Active { get; private set; }
        public List<Iteration> Iterations { get; private set; }
    }
}

Project

namespace Kanban.Shared.Domain
{
    public class Project
    {
        public Project(long id, string name, bool active, User owner)
        {
            Id = id;
            Name = name;
            Active = active;
            Owner = owner;
        }

        //For EF mapping
        protected Project()
        {}

        public long Id { get; private set; }
        public string Name { get; private set; }
        public bool Active { get; private set; }
        public User Owner { get; private set; }
    }
}

Team

using System.Collections.Generic;

namespace Kanban.Shared.Domain
{
    public class Team
    {
        public Team(long id, string name, bool active)
        {
            Id = id;
            Name = name;
            Active = active;
        }

        //For EF mapping
        protected Team()
        { }

        public long Id { get; private set; }
        public string Name { get; private set; }
        public bool Active { get; private set; }
        public List<Iteration> Iterations { get; private set; }
        public List<User> Members { get; private set; }
    }
}

User

namespace Kanban.Shared.Domain
{
    public class User
    {
        public User(long id, string email, string password, string firstName, string lastName, bool active, Team team)
        {
            Id = id;
            Email = email;
            Password = password;
            FirstName = firstName;
            LastName = lastName;
            Active = active;
            Team = team;
        }

        //For EF mapping
        protected User()
        {}

        public long Id { get; private set; }
        public string Email { get; private set; }
        public string Password { get; private set; }
        public string FirstName { get; private set; }
        public string LastName { get; private set; }
        public bool Active { get; private set; }
        public Team Team { get; private set; }
    }
}

Para finalizar esta parte sem estender muito o artigo, vamos configurar agora uma classe de contexto do Entity Framework Core para podermos trabalhar com nossos objetos.

Criando o contexto do Entity Framework Core

Notem que não utilizamos nenhuma anotação do EF Core nas classes, de modo que elas são puras, sem referências a frameworks, e podem ser utilizadas sem problemas no projeto client do Blazor. Vamos configurar nosso mapeamento com o banco de dados na classe de contexto do EF, utilizando a API fluente dele.

Primeiramente vamos criar um novo projeto, chamado Kanban.Infra.Database, onde vamos configurar o Entity Framework e repositórios.

Para isso, no diretório onde encontra-se o arquivo Kanban.sln, execute o comando abaixo:

dotnet new classlib — name Kanban.Infra.Database

Podemos remover o arquivo “Class1.cs” que já vem no projeto.

Em seguida, acesse o diretório deste novo projeto criado e use o comando abaixo para referenciar o projeto Kanban.Shared:

cd Kanban.Infra.Database
dotnet add Kanban.Infra.Database.csproj reference ..\Kanban.Shared\Kanban.Shared.csproj

E finalmente, voltando para o diretório do arquivo de solução, o comando abaixo para incluir o projeto Kanban.Infra.Database na solução:

dotnet sln add Kanban.Infra.Database\Kanban.Infra.Database.csproj

Feito tudo isso, vamos criar no projeto Kanban.Infra.Database, um diretório chamado Contexts, e dentro dele uma classe chamada KanbanContext. Antes de implementarmos nossa classe de contexto, precisamos adicionar o Entity Framework ao projeto. Para isso, dentro do diretório do projeto Kanban.Infra.Database vamos executar o comando abaixo:

dotnet add package Microsoft.EntityFrameworkCore.SqlServer

Notem que não é mais necessário adicionar o CLI do EF Core, nem o pacote Design, pois hoje eles já fazem parte do CLI do .NET Core – lembrando que a partir da versão 2.1.

Agora vamos configurar nossa classe de Contexto. Para começar, vamos fazer ela estender a classe DbContext, e criar propriedades do tipo DbSet<T> para cada classe de domínio que criamos no projeto Shared, sendo T cada uma destas classes. Vamos codificar o construtor para receber as configurações de banco de dados através de um parâmetro do tipo DbContextOptions.

Teremos o seguinte resultado após esta primeira parte de codificação:

using Kanban.Shared.Domain;
using Microsoft.EntityFrameworkCore;

namespace Kanban.Infra.Database.Contexts
{
    public class KanbanContext : DbContext
    {
        public KanbanContext(DbContextOptions options) : base(options)
        {

        }

        public DbSet<Artifact> Artifacts { get; set; }
        public DbSet<ArtifactAttachment> ArtifactAttachments { get; set; }
        public DbSet<ArtifactStatus> ArtifactStatuses { get; set; }
        public DbSet<ArtifactType> ArtifactTypes { get; set; }
        public DbSet<Iteration> Iterations { get; set; }
        public DbSet<Project> Projects { get; set; }
        public DbSet<Team> Teams { get; set; }
        public DbSet<User> Users { get; set; }
    }
}

Agora precisamos mapear nossas classes através da API fluente do EF Core. Não vou entrar em detalhes dela, pois não é o foco do artigo. Para quem tiver dúvidas ou quiser aprender um pouco mais sobre isso, segue o link da documentação oficial do EF Core. No menu lateral, temos as demais tarefas que podem ser feitas com a API fluente, como chave primária, relacionamentos, índices, etc.

Para configurar o mapeamento de cada classe de domínio, vamos fazer isso sobrescrevendo o método OnModelCreating, que fornece um parâmetro do tipo ModelBuilder, que vamos utilizar para configurar cada classe de domínio. A assinatura do método é:

protected override void OnModelCreating(ModelBuilder modelBuilder)

Fazendo o mapeamento conforme a documentação da API fluente, temos o resultado abaixo. Notem que nem todas as classes tiveram o mapeamento de navegação, aquela propriedade do tipo List<EntidadeComMuitos>, pois só fiz nas classes que achei necessário por funcionalidade do sistema.

using Kanban.Shared.Domain;
using Microsoft.EntityFrameworkCore;

namespace Kanban.Infra.Database.Contexts
{
    public class KanbanContext : DbContext
    {
        public KanbanContext(DbContextOptions options) : base(options)
        {

        }

        public DbSet<Artifact> Artifacts { get; set; }
        public DbSet<ArtifactAttachment> ArtifactAttachments { get; set; }
        public DbSet<ArtifactStatus> ArtifactStatuses { get; set; }
        public DbSet<ArtifactType> ArtifactTypes { get; set; }
        public DbSet<Iteration> Iterations { get; set; }
        public DbSet<Project> Projects { get; set; }
        public DbSet<Team> Teams { get; set; }
        public DbSet<User> Users { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Artifact>()
                .HasKey(a => a.Id);

            modelBuilder.Entity<Artifact>()
                .HasOne(a => a.Type);

            modelBuilder.Entity<Artifact>()
                .HasOne(a => a.Project);

            modelBuilder.Entity<Artifact>()
                .HasOne(a => a.AssignedUser);

            modelBuilder.Entity<Artifact>()
                .HasOne(a => a.Status);

            modelBuilder.Entity<Artifact>()
                .HasOne(a => a.Iteration)
                .WithMany(i => i.Artifacts);

            modelBuilder.Entity<ArtifactAttachment>()
                .HasKey(a => a.Id);

            modelBuilder.Entity<ArtifactAttachment>()
                .HasOne(aa => aa.Artifact)
                .WithMany(a => a.Attachments);
                
            modelBuilder.Entity<ArtifactStatus>()
                .HasKey(a => a.Id);

            modelBuilder.Entity<ArtifactStatus>()
                .HasOne(s => s.Type)
                .WithMany(at => at.Statuses);

            modelBuilder.Entity<ArtifactType>()
                .HasKey(a => a.Id);

            modelBuilder.Entity<Iteration>()
                .HasKey(a => a.Id);

            modelBuilder.Entity<Iteration>()
                .HasOne(i => i.Team)
                .WithMany(t => t.Iterations);

            modelBuilder.Entity<Project>()
                .HasKey(a => a.Id);

            modelBuilder.Entity<Project>()
                .HasOne(p => p.Owner);

            modelBuilder.Entity<Team>()
                .HasKey(a => a.Id);

            modelBuilder.Entity<User>()
                .HasKey(a => a.Id);

            modelBuilder.Entity<User>()
                .HasOne(u => u.Team)
                .WithMany(t => t.Members);
        }
    }
}

Camada de repositórios

Vamos agora criar nossos repositórios. Por agora, criaremos apenas a classe base, sendo um repositório genérico e uma classe para cada domínio, que apenas estenderão a classe base.

Conforme avançarmos com as funcionalidades, vamos implementar métodos mais específicos em cada repositório, assim também não estendemos muito o artigo. Antes criaremos um diretório Repositories para colocar todos os repositórios.

A classe base dos repositórios, servindo de repositório genérico é mostrada abaixo. Note que é uma classe abstrata, forçando que cada Domínio precise ter sua própria classe de repositório implementada, que estende o repositório genérico, facilitando a identificação e injeção de dependência depois.

Nomearemos nossa classe como GenericRepository, e esta será definida conforme uma interface, a IGenericRepository.

using System;
using System.Linq;
using System.Linq.Expressions;

namespace Kanban.Infra.Database.Repositories
{
    public interface IGenericRepository<T> where T : class
    {
        IQueryable<T> GetAll();
        IQueryable<T> FindBy(Expression<Func<T, bool>> predicate);
        void Add(T entity);
        void Delete(T entity);
        void Edit(T entity);
        void Save();
    }
}
using System;
using System.Linq;
using System.Linq.Expressions;
using Kanban.Infra.Database.Contexts;
using Microsoft.EntityFrameworkCore;

namespace Kanban.Infra.Database.Repositories
{
    public abstract class GenericRepository<T> : IGenericRepository<T> where T : class
    {
        protected KanbanContext Context { get; private set; }

        public GenericRepository(KanbanContext context)
        {
            Context = context;
        }

        public void Add(T entity)
        {
            Context.Set<T>().Add(entity);
        }

        public void Delete(T entity)
        {
            Context.Set<T>().Remove(entity);
        }

        public void Edit(T entity)
        {
            Context.Entry(entity).State = EntityState.Modified;
        }

        public IQueryable<T> FindBy(Expression<Func<T, bool>> predicate)
        {
            IQueryable<T> query = Context.Set<T>().Where(predicate);
            return query;
        }

        public IQueryable<T> GetAll()
        {
            IQueryable<T> query = Context.Set<T>();
            return query;
        }

        public void Save()
        {
            Context.SaveChanges();
        }
    }
}

Nossa classe possui um parâmetro genérico T, que é representado por uma classe. Ao implementar cada um dos repositórios concretos informaremos qual domínio será utilizado. Abaixo temos uma interface e uma implementação de repositório como exemplo. Os demais encontram-se na branch parte2 do repositório.

using Kanban.Shared.Domain;

namespace Kanban.Infra.Database.Repositories
{
    public interface IUserRepository : IGenericRepository<User>
    {
         
    }
}
using Kanban.Infra.Database.Contexts;
using Kanban.Shared.Domain;

namespace Kanban.Infra.Database.Repositories
{
    public class UserRepository : GenericRepository<User>
    {
        public UserRepository(KanbanContext context) : base(context)
        {
        }
    }
}

Concluindo a Parte 02

Por agora, vamos encerrar por aqui para não estender demais o artigo. O conteúdo que fizemos até aqui está disponível no repositório do GitHub, branch parte2.

Na próxima parte, vamos finalizar a configuração estrutural do projeto, criando nosso projeto para a camada de aplicação e as migrações do projeto de banco de dados, onde precisaremos fazer referência pelo projeto da API com as configurações de injeção de dependência e conexão ao banco de dados para que o EF Core possa configurar as migrações. Também criaremos alguns dos controllers da API.

Lançarei a Parte 03 em breve aqui no portal, para finalizarmos a estrutura e começarmos a colocar a mão na massa com o Blazor.

Novamente, agradeço a todos que me acompanham aqui no iMasters e aguardo o feedback de vocês.

Um abraço!