Engenharia reversa (scaffolding)

Engenharia reversa é o processo de scaffolding de classes de tipo de entidade e uma classe DbContext baseada em um esquema de banco de dados. Ele pode ser executado usando o comando Scaffold-DbContext das ferramentas PMC (Console do Gerenciador de Pacotes) do EF Core ou o comando dotnet ef dbcontext scaffold das ferramentas da CLI (Interface de Linha de Comando) do .NET.

Observação

O scaffolding de um DbContext e tipos de entidade documentados aqui é diferente do scaffolding de controladores no ASP.NET Core usando o Visual Studio, que não está documentado aqui.

Dica

Se você usar o Visual Studio, experimente a extensão da comunidade do EF Core Power Tools. Essas ferramentas fornecem uma ferramenta gráfica que se baseia nas ferramentas de linha de comando do EF Core e oferece opções adicionais de fluxo de trabalho e personalização.

Pré-requisitos

  • Antes do scaffolding, você precisará instalar as ferramentas do PMC, que funcionam somente no Visual Studio, ou as ferramentas da CLI do .NET, que têm suporte em todas as plataformas compatíveis com o .NET.
  • Instale o pacote NuGet para Microsoft.EntityFrameworkCore.Design o qual você está scaffolding no projeto.
  • Instale o pacote NuGet para o provedor de banco de dados que tem como destino o esquema de banco de dados do qual você deseja fazer o scaffolding.

Argumentos necessários

O PMC e os comandos da CLI do .NET têm dois argumentos necessários: a cadeia de conexão com o banco de dados e o provedor de banco de dados EF Core a ser usado.

Cadeia de conexão

O primeiro argumento para o comando é uma cadeia de conexão com o banco de dados. As ferramentas usarão essa cadeia de conexão para ler o esquema do banco de dados.

Como você cita e escapa da cadeia de conexão depende de qual shell está usando para executar o comando. Consulte a documentação do shell para obter mais informações. Por exemplo, o PowerShell exige que você escape o caractere $, mas não \.

O exemplo faz scaffolding de tipos de entidade e um DbContext do banco de dados Chinook localizado na instância SQL Server LocalDB da máquina, fazendo uso do provedor de banco de dados Microsoft.EntityFrameworkCore.SqlServer.

dotnet ef dbcontext scaffold "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Chinook" Microsoft.EntityFrameworkCore.SqlServer

Segredos do usuário para cadeias de conexão

Se você tiver um aplicativo .NET que usa o modelo de hospedagem e o sistema de configuração, como um projeto ASP.NET Core, poderá usar a sintaxe Name=<connection-string> para ler a cadeia de conexão da configuração.

Por exemplo, considere um aplicativo ASP.NET Core com o seguinte arquivo de configuração:

{
  "ConnectionStrings": {
    "Chinook": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=Chinook"
  }
}

Essa cadeia de conexão no arquivo de configuração pode ser usada para fazer scaffolding de um banco de dados usando:

dotnet ef dbcontext scaffold "Name=ConnectionStrings:Chinook" Microsoft.EntityFrameworkCore.SqlServer

No entanto, armazenar cadeias de conexão em arquivos de configuração não é uma boa ideia, pois é muito fácil expô-las acidentalmente, por exemplo, enviando por push para o controle do código-fonte. Em vez disso, as cadeias de conexão devem ser armazenadas de maneira segura, como por meio do Azure Key Vault ou, ao trabalhar localmente, da ferramenta Gerenciador de Segredos, também conhecida como "Segredos do Usuário".

Por exemplo, para usar os Segredos do Usuário, primeiro remova a cadeia de conexão do arquivo de configuração do ASP.NET Core. Em seguida, inicialize os Segredos do Usuário executando o seguinte comando no mesmo diretório do projeto ASP.NET Core:

dotnet user-secrets init

Esse comando configura o armazenamento em seu computador separado do código-fonte e adiciona uma chave para esse armazenamento ao projeto.

Em seguida, armazene a cadeia de conexão em segredos do usuário. Por exemplo:

dotnet user-secrets set ConnectionStrings:Chinook "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=Chinook"

Agora, o mesmo comando usado anteriormente na cadeia de conexão nomeada do arquivo de configuração usará a cadeia de conexão armazenada em Segredos do Usuário. Por exemplo:

dotnet ef dbcontext scaffold "Name=ConnectionStrings:Chinook" Microsoft.EntityFrameworkCore.SqlServer

Cadeias de conexão no código em que foi feito o scaffolding

Por padrão, quem faz o scaffolding incluirá a cadeia de conexão no código onde foi feito o scaffolding, mas com um aviso. Por exemplo:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
#warning To protect potentially sensitive information in your connection string, you should move it out of source code. You can avoid scaffolding the connection string by using the Name= syntax to read it from configuration - see https://go.microsoft.com/fwlink/?linkid=2131148. For more guidance on storing connection strings, see http://go.microsoft.com/fwlink/?LinkId=723263.
    => optionsBuilder.UseSqlServer("Data Source=(LocalDb)\\MSSQLLocalDB;Database=AllTogetherNow");

Isso é feito para que o código gerado não falhe quando usado pela primeira vez, o que seria uma experiência de aprendizado muito ruim. No entanto, como diz o aviso, as cadeias de conexão não devem existir no código de produção. Consulte Tempo de Vida, Configuração e Inicialização do DbContext Lifetime para obter as várias maneiras de gerenciar cadeias de conexão.

Dica

A opção -NoOnConfiguring(PMC do Visual Studio PMC) ou --no-onconfiguring (CLI do .NET) pode ser passada para suprimir a criação do método OnConfiguring que contém a cadeia de conexão.

Nome do provedor

O segundo argumento é o nome do provedor. O nome do provedor normalmente é o mesmo que o nome do pacote NuGet do provedor. Por exemplo, para SQL Server ou SQL do Azure, use Microsoft.EntityFrameworkCore.SqlServer.

Opções de linha de comando

O processo de scaffolding pode ser controlado por várias opções de linha de comando.

Como especificar tabelas e exibições

Por padrão, todas as tabelas e modos de exibição no esquema de banco de dados são estruturadas em tipos de entidade. Você pode limitar quais tabelas e modos de exibição passam por scaffolding especificando esquemas e tabelas.

O argumento -Schemas(PMC do Visual Studio) ou --schema (CLI do .NET) especifica os esquemas de tabelas e exibições para os quais os tipos de entidade serão gerados. Se esse argumento for omitido, todos os esquemas serão incluídos. Se essa opção for usada, todas as tabelas e exibições nos esquemas serão incluídas no modelo, mesmo que não sejam explicitamente incluídas usando -Tables ou --table.

O argumento -Tables (PMC do Visual Studio) ou --table (CLI do .NET) especificou as tabelas e exibições para as quais os tipos de entidade serão gerados. Tabelas ou exibições em um esquema específico podem ser incluídas usando o formato "schema.table" ou "schema.view". Se essa opção for omitida, todas as tabelas e modos d exibição serão incluídas. |

Por exemplo, para estruturar apenas as tabelas Artists e Albums:

dotnet ef dbcontext scaffold ... --table Artist --table Album

Para estruturar todas as tabelas e modos de exibição dos esquemas Customer e Contractor:

dotnet ef dbcontext scaffold ... --schema Customer --schema Contractor

Por exemplo, para estruturar a tabela Purchases do esquema Customer e as tabelas Accounts e Contracts do esquema Contractor:

dotnet ef dbcontext scaffold ... --table Customer.Purchases --table Contractor.Accounts --table Contractor.Contracts

Como preservar nomes de banco de dados

Os nomes de tabela e coluna são corrigidos para corresponder melhor às convenções de nomenclatura do .NET para tipos e propriedades por padrão. Especificar -UseDatabaseNames (PMC do Visual Studio) ou --use-database-names (CLI do .NET) desabilitará esse comportamento preservando os nomes de banco de dados originais o máximo possível. Os identificadores .NET inválidos ainda serão corrigidos e nomes sintetizados, como propriedades de navegação, ainda estarão em conformidade com as convenções de nomenclatura do .NET.

Por exemplo, considere a seguintes tabelas:

CREATE TABLE [BLOGS] (
    [ID] int NOT NULL IDENTITY,
    [Blog_Name] nvarchar(max) NOT NULL,
    CONSTRAINT [PK_Blogs] PRIMARY KEY ([ID]));

CREATE TABLE [posts] (
    [id] int NOT NULL IDENTITY,
    [postTitle] nvarchar(max) NOT NULL,
    [post content] nvarchar(max) NOT NULL,
    [1 PublishedON] datetime2 NOT NULL,
    [2 DeletedON] datetime2 NULL,
    [BlogID] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogID]) REFERENCES [Blogs] ([ID]) ON DELETE CASCADE);

Por padrão, os seguintes tipos de entidade serão scaffolded dessas tabelas:

public partial class Blog
{
    public int Id { get; set; }
    public string BlogName { get; set; } = null!;
    public virtual ICollection<Post> Posts { get; set; } = new List<Post>();
}

public partial class Post
{
    public int Id { get; set; }
    public string PostTitle { get; set; } = null!;
    public string PostContent { get; set; } = null!;
    public DateTime _1PublishedOn { get; set; }
    public DateTime? _2DeletedOn { get; set; }
    public int BlogId { get; set; }
    public virtual Blog Blog { get; set; } = null!;
    public virtual ICollection<Tag> Tags { get; set; } = new List<Tag>();
}

No entanto, usar -UseDatabaseNames ou --use-database-names resulta nos seguintes tipos de entidade:

public partial class BLOG
{
    public int ID { get; set; }
    public string Blog_Name { get; set; } = null!;
    public virtual ICollection<post> posts { get; set; } = new List<post>();
}

public partial class post
{
    public int id { get; set; }
    public string postTitle { get; set; } = null!;
    public string post_content { get; set; } = null!;
    public DateTime _1_PublishedON { get; set; }
    public DateTime? _2_DeletedON { get; set; }
    public int BlogID { get; set; }
    public virtual BLOG Blog { get; set; } = null!;
}

Usar atributos de mapeamento (também conhecidos como Anotações de Dados)

Os tipos de entidade são configurados usando a API ModelBuilderem OnModelCreating por padrão. Especifique -DataAnnotations (PMC) ou --data-annotations (CLI do .NET Core) para usar atributos de mapeamento quando possível.

Por exemplo, o uso da API Fluente fará o scaffold de:

entity.Property(e => e.Title)
    .IsRequired()
    .HasMaxLength(160);

Usar Anotações de Dados fará o scaffold de:

[Required]
[StringLength(160)]
public string Title { get; set; }

Dica

Alguns aspectos do modelo não podem ser configurados usando atributos de mapeamento. O scaffolder ainda usará a API de criação de modelos para lidar com esses casos.

Nome DbContext

O nome da classe DbContext com scaffold será o nome do banco de dados sufixado com Contextco por padrão. Para especificar outro, use -Context no PMC e --context na CLI do .NET Core.

Diretórios e namespaces de destino

As classes de entidade e uma classe DbContext são agrupadas no diretório raiz do projeto e usam o namespace padrão do projeto.

Você pode especificar o diretório em que as classes são scaffolded usando --output-dir, e --context-dir pode ser usado para executar scaffold da classe DbContext em um diretório separado das classes de tipo de entidade:

dotnet ef dbcontext scaffold ... --context-dir Data --output-dir Models

Por padrão, o namespace será o namespace raiz mais os nomes de quaisquer subdiretórios no diretório raiz do projeto. No entanto, você pode substituir o namespace para todas as classes de saída usando --namespace. Você também pode substituir o namespace apenas para a classe DbContext usando --context-namespace:

dotnet ef dbcontext scaffold ... --namespace Your.Namespace --context-namespace Your.DbContext.Namespace

O código scaffolded

O resultado do scaffolding de um banco de dados existente é:

  • Um arquivo que contém uma classe que herda de DbContext
  • Um arquivo para cada tipo de entidade

Dica

A partir do EF7, você também pode usar modelos de texto T4 para personalizar o código gerado. Confira Modelos de engenharia reversa personalizados para obter mais detalhes.

Tipos de referências anuláveis em C#

O scaffolder pode criar tipos de entidade e modelo EF que usam Tipos de referência anuláveis (NRTs) em C#. O uso de NRT passa por scaffolding automaticamente quando o suporte do NRT é habilitado no projeto C# no qual o código está sofrendo scaffolding..

Por exemplo, a seguinte tabela Tags contém colunas de cadeia de caracteres anuláveis e não anuláveis:

CREATE TABLE [Tags] (
  [Id] int NOT NULL IDENTITY,
  [Name] nvarchar(max) NOT NULL,
  [Description] nvarchar(max) NULL,
  CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));

O resultado disso são propriedades de cadeia de caracteres anuláveis e não anuláveis correspondentes na classe gerada:

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

Da mesma forma, as seguintes tabelas Posts contêm uma relação obrigatória com a tabela Blogs:

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    [BlogId] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id]));

O resultado disso é o scaffolding da relação não anulável (obrigatória) entre os blogs:

public partial class Blog
{
    public Blog()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;

    public virtual ICollection<Post> Posts { get; set; }
}

E posta:

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

    public virtual ICollection<Tag> Tags { get; set; }
}

Relacionamento muitos para muitos

O processo de scaffolding detecta tabelas de junção simples e gera automaticamente um mapeamento muitos para muitos para elas. Por exemplo, considere tabelas para Posts e Tags, e uma tabela de junção PostTag as conectando:

CREATE TABLE [Tags] (
  [Id] int NOT NULL IDENTITY,
  [Name] nvarchar(max) NOT NULL,
  [Description] nvarchar(max) NULL,
  CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]));

CREATE TABLE [PostTag] (
    [PostsId] int NOT NULL,
    [TagsId] int NOT NULL,
    CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostsId], [TagsId]),
    CONSTRAINT [FK_PostTag_Posts_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_PostTag_Tags_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Posts] ([Id]) ON DELETE CASCADE);

Quando o scaffolding é feito, isso resulta em uma classe para Post:

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

    public virtual ICollection<Tag> Tags { get; set; }
}

E uma classe para Tag:

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

Mas nenhuma classe para a tabela PostTag. Em vez disso, é feito scaffolding na configuração para uma relação muitos para muitos:

entity.HasMany(d => d.Tags)
    .WithMany(p => p.Posts)
    .UsingEntity<Dictionary<string, object>>(
        "PostTag",
        l => l.HasOne<Tag>().WithMany().HasForeignKey("PostsId"),
        r => r.HasOne<Post>().WithMany().HasForeignKey("TagsId"),
        j =>
            {
                j.HasKey("PostsId", "TagsId");
                j.ToTable("PostTag");
                j.HasIndex(new[] { "TagsId" }, "IX_PostTag_TagsId");
            });

Outras linguagens de programação

Os pacotes do EF Core publicados pelo código C# do scaffold da Microsoft. No entanto, o sistema de scaffolding subjacente é compatível com um modelo de plug-in para scaffolding para outras linguagens. Esse modelo de plug-in é usado por vários projetos de execução da comunidade, por exemplo:

Como personalizar o código

A partir do EF7, uma das melhores maneiras de personalizar o código gerado é personalizando os modelos T4 usados para gerá-lo.

O código também pode ser alterado depois de gerado, mas a melhor maneira de fazer isso depende se você pretende executar novamente o processo de scaffolding quando o modelo de banco de dados é alterado.

Fazer scaffolding apenas uma vez

Com essa abordagem, o código que sofreu engenharia reversa fornece um ponto de partida para o mapeamento baseado em código daqui para frente. Todas as alterações no código gerado podem ser feitas conforme desejado. Ela se torna um código normal, assim como qualquer outro código em seu projeto.

Manter o banco de dados e o modelo EF em sincronia pode ser feito de duas maneiras:

  • Alterne para usar migrações de banco de dados do EF Core e use os tipos de entidade e a configuração do modelo EF como a fonte da verdade, usando migrações para conduzir o esquema.
  • Atualize manualmente os tipos de entidade e a configuração do EF quando o banco de dados for alterado. Por exemplo, se uma nova coluna for adicionada a uma tabela, adicione uma propriedade para a coluna ao tipo de entidade mapeada e adicione as configurações necessárias usando atributos de mapeamento e/ou código em OnModelCreating. Isso é relativamente fácil, e o único desafio real é garantir que as alterações no banco de dados sejam registradas ou detectadas de alguma forma para que os desenvolvedores responsáveis pelo código possam reagir.

Engenharia reversa repetida

Uma abordagem alternativa para fazer engenharia reversa uma vez é fazê-la novamente sempre que o banco de dados for alterado. Isso substituirá qualquer código que passou por engenharia reversa anteriormente, o que significa que quaisquer alterações feitas em tipos de entidade ou configuração de EF nesse código serão perdidas.

[DICA] Por padrão, os comandos EF não substituirão nenhum código existente para proteger contra perda acidental de código. O argumento -Force (PMC do Visual Studio) ou --force (CLI do .NET) pode ser usado para forçar a substituição de arquivos existentes.

Como o código de engenharia reversa será substituído, é melhor não modificá-lo diretamente, mas sim contar com classes e métodos parciais e com os mecanismos no EF Core que permitem que a configuração seja substituída. Especificamente:

  • As classes DbContext e as classes de entidade são geradas como parciais. Isso permite a introdução de membros adicionais e código em um arquivo separado que não será substituído quando a engenharia reversa for executada.
  • A classe DbContext contém um método parcial chamado OnModelCreatingPartial. Uma implementação desse método pode ser adicionada à classe parcial para o DbContext. Em seguida, ele será chamado depois que OnModelCreating for chamado.
  • A configuração de modelo feita usando as APIs do ModelBuilder substitui qualquer configuração feita por convenções ou atributos de mapeamento, bem como a configuração anterior feita no construtor de modelos. Isso significa que o código OnModelCreatingPartial pode ser usado para substituir a configuração gerada pelo processo de engenharia reversa, sem a necessidade de remover essa configuração.

Por fim, lembre-se de que, a partir do EF7, os modelos T4 usados para gerar código podem ser personalizados. Geralmente, essa é uma abordagem mais eficaz do que a engenharia reversa com os padrões e, em seguida, a modificação com classes parciais e/ou métodos.

Como ele funciona

A engenharia reversa começa lendo o esquema de banco de dados. Ela lê informações sobre tabelas, colunas, restrições e índices.

Em seguida, usa as informações de esquema para criar um modelo EF Core. As tabelas são usadas para criar tipos de entidade, colunas são usadas para criar propriedades e chaves estrangeiras são usadas para criar relações.

Por fim, o modelo é usado para gerar código. As classes de tipo de entidade correspondentes, a API fluente e as anotações de dados são scaffolded para recriar o mesmo modelo de seu aplicativo.

Limitações

  • Nem tudo sobre um modelo pode ser representado usando um esquema de banco de dados. Por exemplo, informações sobre hierarquias de herança, tipos de propriedade e divisão de tabelas não estão presentes no esquema de banco de dados. Por isso, essas construções nunca passarão por engenharia reversa.
  • Além disso, alguns tipos de coluna podem não ter suporte do provedor EF Core. Essas colunas não serão incluídas no modelo.
  • Você pode definir tokens de simultaneidade em um modelo EF Core para impedir que dois usuários atualizem a mesma entidade ao mesmo tempo. Alguns bancos de dados têm um tipo especial para representar esse tipo de coluna (por exemplo, rowversion no SQL Server). Nesse caso, podemos fazer engenharia reversa dessas informações. No entanto, outros tokens de simultaneidade não serão passarão por engenharia reversa.