Você está na página 1de 201

Machine Translated by Google

Capítulo 20 ÿ E/S de arquivo e serialização de objetos

Possíveis problemas de desempenho usando JsonSerializerOption Ao usar


JsonSerializerOption, é melhor criar uma única instância e reutilizá-la em todo o aplicativo.
Com isso em mente, atualize suas instruções de nível superior e métodos JSON para o seguinte:

JsonSerializerOptions opções = new() {

PropertyNameCaseInsensitive = verdadeiro,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
IncludeFields = verdadeiro, WriteIndented = verdadeiro,
NumberHandling = JsonNumberHandling.AllowReadingFromString
| JsonNumberHandling.
WriteAsString
};
SaveAsJsonFormat(opções, jbc, "CarData.json");
Console.WriteLine("=> Carro salvo no formato JSON!");

SaveAsJsonFormat(options, p, "PersonData.json");
Console.WriteLine("=> Pessoa salva no formato JSON!");

static void SaveAsJsonFormat<T>(opções JsonSerializerOptions, T objGraph, string fileName)


=> File.WriteAllText(fileName, System.Text.Json.JsonSerializer.Serialize(objGraph, opções));

Padrões da Web para JsonSerializer


Ao criar aplicativos da Web, você pode usar um construtor especializado para definir as seguintes propriedades:

PropertyNameCaseInsensitive = verdadeiro,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
NumberHandling = JsonNumberHandling.AllowReadingFromString

Você ainda pode definir propriedades adicionais por meio da inicialização do objeto, assim:

JsonSerializerOptions options = new(JsonSerializerDefaults.Web) {

WriteIndented = true };

Serializando Coleções de Objetos


Serializar uma coleção de objetos em JSON é feito da mesma forma que um único objeto. Adicione a seguinte
função local ao final das instruções de nível superior:

static void SaveListOfCarsAsJson(opções JsonSerializerOptions, string fileName) {

//Agora persiste uma List<T> de JamesBondCars.


List<JamesBondCar> meusCarros = new()
{

761
Machine Translated by Google

Capítulo 20 ÿ E/S de arquivo e serialização de objetos

new JamesBondCar { CanFly = true, CanSubmerge = true }, new


JamesBondCar { CanFly = true, CanSubmerge = false }, new
JamesBondCar { CanFly = false, CanSubmerge = true }, new
JamesBondCar { CanFly = false, CanSubmerge = false },
};

File.WriteAllText(fileName, System.Text.Json.JsonSerializer.Serialize(myCars, options)); Console.WriteLine("=>


Lista de carros salva!");
}

Para encerrar tudo, adicione a seguinte linha para exercitar a nova função:

SaveListOfCarsAsJson(options, "CarCollection.json");

Desserializando objetos e coleções de objetos


Assim como a desserialização XML, a desserialização JSON é o oposto da serialização. A função a seguir
desserializará o JSON no tipo especificado usando a versão genérica do método:

static T ReadAsJsonFormat<T>(opções JsonSerializerOptions, string fileName) =>


System.Text.Json.JsonSerializer.Deserialize<T>(File.ReadAllText(fileName), opções);

Adicione o seguinte código às instruções de nível superior para reconstituir seu XML de volta em objetos (ou lista
de objetos):

JamesBondCar saveJsonCar = ReadAsJsonFormat<JamesBondCar>(opções, "CarData.json");


Console.WriteLine("Ler Carro: {0}", saveJsonCar.ToString());

List<JamesBondCar> saveJsonCars = ReadAsJsonFormat<List<JamesBondCar>>(opções,


"CarCollection.json"); Console.WriteLine("Ler Carro: {0}", saveJsonCar.ToString());

Resumo
Você começou este capítulo examinando o uso dos tipos Directory(Info) e File(Info). Como você aprendeu, essas classes
permitem que você manipule um arquivo ou diretório físico em seu disco rígido. Em seguida, você examinou várias
classes derivadas da classe abstrata Stream. Dado que os tipos derivados de Stream operam em um fluxo bruto de bytes,
o namespace System.IO fornece vários tipos de leitor/gravador (por exemplo, StreamWriter, StringWriter e BinaryWriter)
que simplificam o processo. Ao longo do caminho, você também verificou a funcionalidade fornecida pelo DriveType,
aprendeu como monitorar arquivos usando o tipo FileSystemWatcher e viu como interagir com fluxos de maneira
assíncrona.
Este capítulo também apresentou o tópico de serviços de serialização de objetos. Como você viu, o .NET
A plataforma principal usa um gráfico de objeto para contabilizar o conjunto completo de objetos relacionados que você deseja
persistir em um fluxo. Em seguida, você trabalhou com serialização e desserialização de XML e JSON.

762
Machine Translated by Google

CAPÍTULO 21

Acesso a dados com ADO.NET

A plataforma .NET Core define vários namespaces que permitem a interação com sistemas de banco de dados relacionais.
Coletivamente falando, esses namespaces são conhecidos como ADO.NET. Neste capítulo, você aprenderá sobre a função
geral do ADO.NET e os principais tipos e namespaces e, em seguida, passará para o tópico dos provedores de dados
ADO.NET. A plataforma .NET Core oferece suporte a vários provedores de dados (ambos fornecidos como parte do .NET Core
Framework e disponíveis em fontes de terceiros), cada um otimizado para se comunicar com um sistema de gerenciamento de
banco de dados específico (por exemplo, Microsoft SQL Server, Oracle e MySQL).
Depois de entender a funcionalidade comum fornecida por vários provedores de dados, você examinará o padrão de
fábrica do provedor de dados. Como você verá, usando tipos nos namespaces System.Data (incluindo System.Data.Common e
namespaces específicos do provedor, como Microsoft.Data.SqlClient, System.Data.Odbc e somente para Windows
System.Data.Oledb), você pode construir uma única base de código que pode selecionar e escolher dinamicamente o provedor
de dados subjacente sem a necessidade de recompilar ou reimplantar a base de código do aplicativo.

A seguir, você aprenderá a trabalhar diretamente com o provedor de banco de dados SQL Server, criando e abrindo
conexões para recuperar dados e, em seguida, passar para a inserção, atualização e exclusão de dados, seguido pelo
exame do tópico de transações de banco de dados. Por fim, você executará o recurso de cópia em massa do SQL Server usando
ADO.NET para carregar uma lista de registros no banco de dados.

ÿ Observação Este capítulo se concentra no ADO.NET bruto. Começando com o Capítulo 22, abordo o Entity Framework (EF)

Core, a estrutura de mapeamento objeto-relacional (ORM) da Microsoft. Como o Entity Framework Core usa o ADO.NET para

acesso a dados nos bastidores, uma compreensão sólida de como o ADO.NET funciona é vital ao solucionar problemas de acesso

a dados. Também existem cenários que não são resolvidos pelo EF Core (como executar uma cópia em massa do SQL) e você
precisará conhecer o ADO.NET para resolver esses problemas.

ADO.NET vs. ADO


Se você tem experiência no modelo anterior de acesso a dados baseado em COM da Microsoft (Active Data Objects [ADO]) e está
apenas começando a trabalhar com a plataforma .NET Core, precisa entender que ADO.NET tem pouco a ver com ADO além de as
letras A, D e O. Embora seja verdade que existe algum relacionamento entre os dois sistemas (por exemplo, cada um tem o conceito
de conexão e objetos de comando), alguns tipos familiares de ADO (por exemplo, o Recordset) não existem mais. Além disso, você
pode encontrar muitos tipos novos que não têm equivalente direto no ADO clássico (por exemplo, o adaptador de dados).

© Andrew Troelsen, Phillip Japikse 2021 763


A. Troelsen e P. Japikse, Pro C# 9 com .NET 5, https://doi.org/10.1007/978-1-4842-6939-8_21
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Compreendendo os provedores de dados ADO.NET


O ADO.NET não fornece um único conjunto de objetos que se comunicam com vários sistemas de gerenciamento de banco de dados
(DBMSs). Em vez disso, o ADO.NET oferece suporte a vários provedores de dados, cada um otimizado para interagir com um DBMS
específico. O primeiro benefício dessa abordagem é que você pode programar um provedor de dados específico para acessar quaisquer
recursos exclusivos de um DBMS específico. O segundo benefício é que um provedor de dados específico pode se conectar diretamente ao
mecanismo subjacente do DBMS em questão sem uma camada de mapeamento intermediária entre as camadas.

Simplificando, um provedor de dados é um conjunto de tipos definidos em um determinado namespace que entende como se
comunicar com um tipo específico de fonte de dados. Independentemente de qual provedor de dados você usa, cada um define um conjunto
de tipos de classe que fornecem a funcionalidade principal. A Tabela 21-1 documenta algumas das principais classes básicas e as principais
interfaces que elas implementam.

Tabela 21-1. Os objetos principais de um provedor de dados ADO.NET

Classe Base Interfaces relevantes Significado na vida


DbConnection Fornece a capacidade de conectar e desconectar do armazenamento de
IDbConnection
dados. Objetos de conexão também fornecem acesso a um objeto de transação
relacionado.

DbCommand IDbCommand Representa uma consulta SQL ou um procedimento armazenado. Os objetos de


comando também fornecem acesso ao objeto de leitor de dados do provedor.
DbDataReader IDataReader, Fornece acesso somente de leitura e encaminhamento aos dados usando um cursor do
IDataRecord lado do servidor.

DbDataAdapter IDataAdapter, Transfere DataSets entre o chamador e o armazenamento de dados.

IDbDataAdapter Os adaptadores de dados contêm uma conexão e um conjunto de quatro


objetos de comando internos usados para selecionar, inserir, atualizar e excluir
informações do armazenamento de dados.

Parâmetro Db IDataParameter, Representa um parâmetro nomeado em uma consulta parametrizada.


IDbDataParameter

DbTransaction IDbTransaction Encapsula uma transação de banco de dados.

Embora os nomes específicos dessas classes principais sejam diferentes entre os provedores de dados (por exemplo, SqlConnection
versus OdbcConnection), cada classe deriva da mesma classe base (DbConnection, no caso de objetos de conexão) que
implementa interfaces idênticas (por exemplo, IDbConnection). Diante disso, você estaria correto ao supor que, depois de aprender a
trabalhar com um provedor de dados, os provedores restantes se mostram bastante diretos.

ÿ Nota Quando você se refere a um objeto de conexão em ADO.NET, na verdade está se referindo a um tipo derivado de

DbConnection específico; não há nenhuma classe literalmente chamada Connection. A mesma ideia vale para um objeto de

comando, objeto de adaptador de dados e assim por diante. Como uma convenção de nomenclatura, os objetos em um provedor de

dados específico são prefixados com o nome do DBMS relacionado (por exemplo, SqlConnection, SqlCommand e SqlDataReader).

A Figura 21-1 mostra o quadro geral por trás dos provedores de dados ADO.NET. O assembly do cliente pode ser qualquer tipo
do aplicativo .NET Core: programa de console, Windows Forms, Windows Presentation Foundation, ASP.NET Core, biblioteca de
código .NET Core e assim por diante.

764
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Figura 21-1. Provedores de dados ADO.NET fornecem acesso a um determinado DBMS

Um provedor de dados fornecerá a você outros tipos além dos objetos mostrados na Figura 21-1; no entanto, esses objetos
principais definem uma linha de base comum em todos os provedores de dados.

Provedores de Dados ADO.NET


Assim como em todos os .NET Core, os provedores de dados são enviados como pacotes NuGet. Existem vários suportados pela
Microsoft, bem como uma infinidade de provedores terceirizados disponíveis. A Tabela 21-2 documenta alguns dos provedores de
dados suportados pela Microsoft.

Tabela 21-2. Alguns dos provedores de dados com suporte da Microsoft

Provedor de Dados Nome do pacote Namespace/NuGet


Servidor Microsoft SQL Microsoft.Data.SqlClient

ODBC System.Data.Odbc

OLE DB (somente Windows) System.Data.OleDb

O provedor de dados do Microsoft SQL Server oferece acesso direto aos armazenamentos de dados do Microsoft SQL Server
— e somente aos armazenamentos de dados do SQL Server (incluindo o SQL Azure). O namespace Microsoft.Data.SqlClient
contém os tipos usados pelo provedor SQL Server.

765
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

ÿ Observação Embora System.Data.SqlClient ainda tenha suporte, todo o esforço de desenvolvimento para interação com
SQL Server (e SQL Azure) é focado na nova biblioteca do provedor Microsoft.Data.SqlClient .

O provedor ODBC (System.Data.Odbc) fornece acesso a conexões ODBC. Os tipos de ODBC definidos no namespace System.Data.Odbc
normalmente são úteis apenas se você precisar se comunicar com um determinado DBMS para o qual não há um provedor de dados .NET
Core personalizado. Isso é verdade porque o ODBC é um modelo amplamente difundido que fornece acesso a vários armazenamentos de dados.

O provedor de dados OLE DB, que é composto pelos tipos definidos no namespace System.Data.OleDb, permite que você acesse
dados localizados em qualquer armazenamento de dados que ofereça suporte ao protocolo OLE DB clássico baseado em COM. Devido à dependência
do COM, esse provedor funcionará apenas no sistema operacional Windows e deve ser considerado obsoleto no mundo multiplataforma do .NET Core.

Os tipos do namespace System.Data


De todos os namespaces ADO.NET, System.Data é o menor denominador comum. Esse namespace contém tipos que são compartilhados
entre todos os provedores de dados ADO.NET, independentemente do armazenamento de dados subjacente. Além de várias exceções
centradas no banco de dados (por exemplo, NoNullAllowedException, RowNotInTableException e MissingPrimaryKeyException),
System.Data contém tipos que representam várias primitivas de banco de dados (por exemplo, tabelas, linhas, colunas e restrições), bem como as
interfaces comuns implementados por objetos provedores de dados. A Tabela 21-3 lista alguns dos principais tipos dos quais você deve estar ciente.

Tabela 21-3. Membros principais do namespace System.Data

Tipo Significado na vida


Limitação Representa uma restrição para um determinado objeto DataColumn

DataColumn Representa uma única coluna dentro de um objeto DataTable

DataRelation Representa um relacionamento pai-filho entre dois objetos DataTable

DataRow Representa uma única linha dentro de um objeto DataTable

DataSet Representa um cache de dados na memória que consiste em qualquer número de dados inter-relacionados
Objetos DataTable

Tabela de dados Representa um bloco tabular de dados na memória

DataTableReader Permite que você trate um DataTable como um cursor de mangueira de incêndio (acesso a dados somente para frente e
somente leitura)

Exibição de dados Representa uma exibição personalizada de um DataTable para classificação, filtragem, pesquisa, edição e navegação

IDataAdapter Define o comportamento principal de um objeto de adaptador de dados

IDataParameter Define o comportamento principal de um objeto de parâmetro

IDataReader Define o comportamento principal de um objeto de leitor de dados

IDbCommand Define o comportamento principal de um objeto de comando

IDbDataAdapter Estende IDataAdapter para fornecer funcionalidade adicional de um objeto de adaptador de dados

IDbTransaction Define o comportamento principal de um objeto de transação

766
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Sua próxima tarefa é examinar as principais interfaces do System.Data em alto nível; isso pode ajudá-lo a
entender a funcionalidade comum oferecida por qualquer provedor de dados. Você também aprenderá detalhes
específicos ao longo deste capítulo; no entanto, por enquanto é melhor focar no comportamento geral de cada tipo de interface.

O papel da interface IDbConnection


O tipo IDbConnection é implementado por um objeto de conexão do provedor de dados . Essa interface define um
conjunto de membros usados para configurar uma conexão com um armazenamento de dados específico. Também permite
obter o objeto de transação do provedor de dados. Aqui está a definição formal de IDbConnection:

interface pública IDbConnection: IDisposable {

string ConnectionString { get; definir; } int


ConnectionTimeout { get; } string Banco de Dados
{ get; }
ConnectionState Estado { get; }

IDbTransaction BeginTransaction();
IDbTransaction BeginTransaction(IsolationLevel il); void
ChangeDatabase(string databaseName); void Fechar();

IDbCommand CreateCommand();
void Abrir(); void Dispose(); }

O papel da interface IDbTransaction


O método BeginTransaction() sobrecarregado definido por IDbConnection fornece acesso ao objeto de transação do
provedor. Você pode usar os membros definidos por IDbTransaction para interagir programaticamente com uma sessão
transacional e o armazenamento de dados subjacente.

interface pública IDbTransaction: IDisposable {

Conexão IDbConnection { obter; }


IsolationLevel IsolationLevel { get; }

void Commit();
void Rollback(); void
Dispose(); }

O papel da interface IDbCommand


A seguir está a interface IDbCommand, que será implementada por um objeto de comando do provedor de dados. Como outros
modelos de objeto de acesso a dados, os objetos de comando permitem a manipulação programática de instruções SQL,
procedimentos armazenados e consultas parametrizadas. Os objetos de comando também fornecem acesso ao tipo de leitor de
dados do provedor de dados por meio do método ExecuteReader() sobrecarregado.

767
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

public interface IDbCommand: IDisposable {

string CommandText { get; definir; } int


CommandTimeout { get; definir; }
CommandType CommandType { get; definir; }
Conexão IDbConnection { obter; definir; }
Transação IDbTransaction { get; definir; }
Parâmetros IDataParameterCollection { get; }
UpdateRowSource UpdatedRowSource { get; definir; }

void Preparar(); void


Cancelar();
IDbDataParameter CreateParameter(); int
ExecuteNonQuery();
IDataReader ExecuteReader();
IDataReader ExecuteReader(comportamento CommandBehavior);
objeto ExecuteScalar(); void Dispose(); }

O papel das interfaces IDbDataParameter e IDataParameter


Observe que a propriedade Parameters de IDbCommand retorna uma coleção fortemente tipada que implementa
IDataParameterCollection. Essa interface fornece acesso a um conjunto de tipos de classe compatíveis com
IDbDataParameter (por exemplo, objetos de parâmetro).

interface pública IDbDataParameter : IDataParameter {

//Mais membros na interface IDataParameter byte Precision


{ get; definir; } byte Escala { get; definir; } int Tamanho
{ obter; definir; } }

IDbDataParameter estende a interface IDataParameter para obter os seguintes comportamentos adicionais:

public interface IDataParameter {

DbType DbType { obter; definir; }


Parâmetro Direção Direção { get; definir; } bool IsNullable
{ get; } string ParameterName { get; definir; } string
SourceColumn { get; definir; }

DataRowVersion SourceVersion { get; definir; } valor do


objeto { get; definir; } }

Como você verá, a funcionalidade das interfaces IDbDataParameter e IDataParameter permite que você represente
parâmetros em um comando SQL (incluindo procedimentos armazenados) por meio de objetos de parâmetro ADO.NET
específicos, em vez de literais de string codificados permanentemente.

768
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

A função das interfaces IDbDataAdapter e IDataAdapter


Você usa adaptadores de dados para enviar e receber DataSets de e para um determinado armazenamento de dados. A
interface IDbDataAdapter define o seguinte conjunto de propriedades que você pode usar para manter as instruções SQL para
as operações de seleção, inserção, atualização e exclusão relacionadas:

interface pública IDbDataAdapter : IDataAdapter {

// Mais membros de IDataAdapter


IDbCommand SelectCommand { get; definir; }
IDbCommand InserirComando { get; definir; }
IDbCommand UpdateCommand { get; definir; }
IDbCommand DeleteCommand { get; definir; } }

Além dessas quatro propriedades, um adaptador de dados ADO.NET seleciona o comportamento definido na interface
base, IDataAdapter. Essa interface define a função principal de um tipo de adaptador de dados: a capacidade de transferir
DataSets entre o chamador e o armazenamento de dados subjacente usando os métodos Fill() e Update(). A interface IDataAdapter
também permite mapear os nomes das colunas do banco de dados para nomes de exibição mais amigáveis com a propriedade
TableMappings.

interface pública IDataAdapter {

MissingMappingAction MissingMappingAction { get; definir; }


MissingSchemaAction MissingSchemaAction { get; definir; }
ITableMappingCollection TableMappings { get; }

DataTable[] FillSchema(DataSet dataSet, SchemaType schemaType); int Fill(DataSet


dataSet);
IDataParameter[] GetFillParameters(); int
Update(DataSet dataSet); }

A função das interfaces IDataReader e IDataRecord


A próxima interface importante a ser observada é IDataReader, que representa os comportamentos comuns suportados por um
determinado objeto de leitura de dados. Ao obter um tipo compatível com IDataReader de um provedor de dados ADO.NET,
você pode iterar sobre o conjunto de resultados de maneira somente de leitura e encaminhamento.

interface pública IDataReader: IDisposable, IDataRecord {

//Mais membros de IDataRecord


int Profundidade { obter; }
bool IsClosed { obter; } int
RegistrosAfetados { obter; }

void Fechar();
DataTable GetSchemaTable(); bool
PróximoResultado(); bool Ler();

Descarte(); }

769
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Por fim, IDataReader estende IDataRecord, que define muitos membros que permitem extrair um
valor fortemente tipado do fluxo, em vez de converter o System.Object genérico recuperado do método
indexador sobrecarregado do leitor de dados. Aqui está a definição da interface IDataRecord:

interface pública IDataRecord {

int FieldCount { obter; }


objeto this[ int i ] { get; } objeto
this[ nome da string ] { get; } bool
GetBoolean(int i); byte GetByte(int i); long
GetBytes(int i, long fieldOffset, byte[]
buffer,
int bufferoffset, comprimento int);
char GetChar(int i); long GetChars(int
i, long fieldoffset, char[] buffer,
int bufferoffset, comprimento int);
IDataReader GetData(int i);
string GetDataTypeName(int i);
DateTime GetDateTime(int i);
Decimal GetDecimal(int i);
duplo GetDouble(int i);
Digite GetFieldType(int i);
float GetFloat(int i);
Guid GetGuid(int i);
curto GetInt16(int i); int
GetInt32(int i); longo
GetInt64(int i); string
GetName(int i); int
GetOrdinal(string nome); string
GetString(int i); objeto
GetValue(int i); int
GetValues(object[] valores); bool
IsDBNull(int i); }

ÿ Observação Você pode usar o método IDataReader.IsDBNull() para descobrir programaticamente se um campo especificado está

definido como nulo antes de tentar obter um valor do leitor de dados (para evitar o acionamento de uma exceção de tempo de execução).

Lembre-se também de que o C# oferece suporte a tipos de dados anuláveis (consulte o Capítulo 4), que são ideais para interagir com
colunas de dados que podem ser nulas na tabela do banco de dados.

Abstraindo provedores de dados usando interfaces


Neste ponto, você deve ter uma ideia melhor da funcionalidade comum encontrada entre todos os provedores de
dados .NET Core. Lembre-se de que, embora os nomes exatos dos tipos de implementação sejam diferentes entre
os provedores de dados, você pode programar esses tipos de maneira semelhante — essa é a beleza do polimorfismo
baseado em interface. Por exemplo, se você definir um método que receba um parâmetro IDbConnection, poderá passar
qualquer objeto de conexão ADO.NET, assim:

770
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

public static void OpenConnection(IDbConnection cn) {

// Abre a conexão de entrada para o chamador. conexão.Open(); }

ÿ Nota As interfaces não são estritamente necessárias; você pode atingir o mesmo nível de abstração usando classes
base abstratas (como DbConnection) como parâmetros ou valores de retorno. No entanto, usar interfaces em vez de
classes base é a melhor prática geralmente aceita.

O mesmo vale para valores de retorno de membro. Crie um novo aplicativo .NET Core Console chamado MyConnectionFactory.
Adicione os seguintes pacotes NuGet ao projeto (o pacote OleDb só é válido no Windows):

Microsoft.Data.SqlClient

System.Data.Common

System.Data.Odbc

System.Data.OleDb

Em seguida, adicione um novo arquivo chamado DataProviderEnum.cs e atualize o código para o seguinte:

namespace MyConnectionFactory {

//OleDb é apenas para Windows e não é compatível com .NET Core enum
DataProviderEnum {

SqlServer, #if
PC
OleDb,
#endif
ODBC,
Nenhum

}
}

Se você estiver usando um sistema operacional Windows em sua máquina de desenvolvimento, atualize o arquivo de projeto para definir o
símbolo do compilador condicional PC.

<PropertyGroup>
<DefineConstants>PC</DefineConstants>
</PropertyGroup>

Se você estiver usando o Visual Studio, clique com o botão direito do mouse no projeto, selecione Propriedades e vá para a guia Construir para
insira os valores "Símbolos do compilador condicional".
O exemplo de código a seguir permite obter um objeto de conexão específico com base no valor de uma enumeração
personalizada. Para fins de diagnóstico, basta imprimir o objeto de conexão subjacente usando serviços de reflexão.

771
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

usando Sistema;
usando System.Data;
usando System.Data.Odbc;
#if PC usando
System.Data.OleDb; #endif
usando Microsoft.Data.SqlClient;
usando MyConnectionFactory;

Console.WriteLine("**** Fábrica de Conexão Muito Simples *****\n");


Setup(DataProviderEnum.SqlServer); #if PC Setup(DataProviderEnum.OleDb); //
Não compatível com macOS #endif Setup(DataProviderEnum.Odbc);
Setup(DataProviderEnum.None); Console.ReadLine();

void Setup(Provedor DataProviderEnum) {

// Obtém uma conexão específica.


IDbConnection myConnection = GetConnection(provedor);
Console.WriteLine($"Sua conexão é {minhaConexão?.GetType().Name ?? "tipo não
reconhecido"}"); // Abrir, usar e fechar conexão... }

// Este método retorna um objeto de conexão específico // com


base no valor de uma enumeração DataProvider.
IDbConnection GetConnection(DataProviderEnum dataProvider)
=> opção dataProvider {

DataProviderEnum.SqlServer => new SqlConnection(), #if PC //


Não suportado no macOS DataProviderEnum.OleDb => new
OleDbConnection(), #endif DataProviderEnum.Odbc => new
OdbcConnection(),

_ => nulo,
};

O benefício de trabalhar com as interfaces gerais de System.Data (ou, nesse caso, as classes base abstratas de
System.Data.Common) é que você tem uma chance muito maior de criar uma base de código flexível que pode evoluir
com o tempo. Por exemplo, hoje você pode estar criando um aplicativo destinado ao Microsoft SQL Server; no entanto,
é possível que sua empresa mude para um banco de dados diferente. Se você criar uma solução que codifica
permanentemente os tipos de System.Data.SqlClient específicos do Microsoft SQL Server, será necessário editar,
recompilar e reimplantar o código para o novo provedor de banco de dados.
Neste ponto, você criou algum código ADO.NET (bastante simples) que permite criar diferentes tipos de objeto de
conexão específico do provedor. No entanto, obter um objeto de conexão é apenas um aspecto do trabalho com
ADO.NET. Para criar uma biblioteca de fábrica de provedores de dados que valha a pena, você também teria que
contabilizar objetos de comando, leitores de dados, objetos de transação e outros tipos centrados em dados. Construir
tal biblioteca de código não seria necessariamente difícil, mas exigiria uma quantidade considerável de código e tempo.

772
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Desde o lançamento do .NET 2.0, o pessoal gentil de Redmond incorporou exatamente essa funcionalidade diretamente no
as bibliotecas de classe base .NET. Essa funcionalidade foi significativamente atualizada para .NET Core.
Você examinará essa API formal em apenas um momento; no entanto, primeiro você precisa criar um banco de dados personalizado
para usar ao longo deste capítulo (e para muitos capítulos por vir).

Configurando o SQL Server e o Azure Data Studio


Ao trabalhar neste capítulo, você executará consultas em um banco de dados de teste simples do SQL Server chamado AutoLot. De
acordo com o tema automotivo usado ao longo deste livro, esse banco de dados conterá cinco tabelas inter-relacionadas (Inventário,
Marcas, Pedidos, Clientes e Riscos de Crédito) que contêm vários bits de dados que representam informações para uma empresa fictícia
de vendas de automóveis. Antes de entrar nos detalhes do banco de dados, você deve configurar o SQL Server e um SQL Server IDE.

ÿ Observação Se você estiver usando uma máquina de desenvolvimento baseada em Windows e tiver instalado o Visual Studio

2019, também terá uma instância do SQL Server Express (chamada localdb) instalada, que pode ser usada para todos os exemplos

deste livro. Se você quiser usar essa versão, pule para a seção “Instalando um SQL Server IDE”.

Instalando o SQL Server


Para este capítulo e muitos dos capítulos restantes deste livro, você precisará ter acesso a uma instância do SQL Server. Se você estiver
usando uma máquina de desenvolvimento não baseada em Windows e não tiver uma instância externa do SQL Server disponível ou optar
por não usar uma instância externa do SQL Server, poderá executar o SQL Server dentro de um contêiner do Docker em seu Mac ou Linux
estação de trabalho baseada em O Docker também funciona em máquinas Windows, portanto, você pode executar os exemplos deste livro
usando o Docker, independentemente do sistema operacional de sua escolha.

Instalando o SQL Server em um Docker Container


Se você estiver usando uma máquina de desenvolvimento não baseada em Windows e não tiver uma instância do SQL Server
disponível para as amostras, poderá executar o SQL Server dentro de um contêiner do Docker em sua estação de trabalho baseada em
Mac ou Linux. O Docker também funciona em máquinas Windows, portanto, você pode executar os exemplos deste livro usando o Docker,
independentemente do sistema operacional de sua escolha.

ÿ Nota A conteinerização é um tópico extenso e simplesmente não há espaço neste livro para entrar nos detalhes profundos dos

contêineres ou do Docker. Este livro cobrirá apenas o suficiente para que você possa trabalhar com os exemplos.

O Docker Desktop pode ser baixado em www.docker.com/get-started. Baixe e instale o


versão apropriada (Windows, Mac, Linux) para sua estação de trabalho (você precisará de uma conta de usuário gratuita do
DockerHub). Certifique-se de selecionar contêineres do Linux quando solicitado.

ÿ Observação A escolha do contêiner (Windows ou Linux) é o sistema operacional em execução no contêiner, não o sistema

operacional da sua estação de trabalho.

773
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Extraindo a imagem e executando o SQL Server 2019 Os contêineres

são baseados em imagens, e cada imagem é um conjunto de camadas que cria o produto final. Para obter a
imagem necessária para executar o SQL Server 2019 em um contêiner, abra uma janela de comando e digite o
seguinte comando:

docker pull mcr.microsoft.com/mssql/server:2019-latest

Depois de carregar a imagem em sua máquina, você precisa iniciar o SQL Server. Para fazer isso, digite o
seguinte comando (tudo em uma linha):

docker run -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=P@ssw0rd" -p 5433:1433 --name AutoLot -d mcr. microsoft.com/mssql/server:2019-


latest

O comando anterior aceita o contrato de licença do usuário final, define a senha (na vida real, você precisa usar uma senha forte),
define o mapeamento de porta (a porta 5433 em sua máquina é mapeada para a porta padrão do SQL Server no contêiner (1433 ), nomeia
o contêiner (AutoLot) e, finalmente, informa ao Docker para usar a imagem baixada anteriormente.

ÿ Observação Estas não são configurações que você deseja usar para desenvolvimento real. Para obter informações sobre como

alterar a senha SA e ver um tutorial, acesse https://docs.microsoft.com/en-us/sql/linux/quickstart install-connect-docker?view=sql-

server-ver15&pivots=cs1- bash.

Para confirmar que está em execução, digite o comando docker ps -a no prompt de comando. Você verá uma saída como a seguinte
(algumas colunas omitidas por brevidade):

C:\Users\japik>docker ps -a
IMAGEM DE ID DO CONTÊINER STATUS PORTAS

NOMES

347475cfb823 mcr.microsoft.com/mssql/server:2019-latest Até 6 minutos 0.0.0.0:5433->1433/tcp AutoLot

Para parar o container, digite docker stop 34747, onde os números 34747 são os cinco primeiros caracteres do ID do container. Para
reiniciar o contêiner, digite docker start 34747, atualizando novamente o comando com o início do ID do seu contêiner.

ÿ Observação Você também pode usar o nome do contêiner (AutoLot neste exemplo) com os comandos da CLI do Docker, por exemplo,

docker start AutoLot. Esteja ciente de que os comandos do Docker, independentemente do sistema operacional, diferenciam maiúsculas
de minúsculas.

Se você quiser usar o Docker Dashboard, clique com o botão direito do mouse no navio Docker (na bandeja do sistema) e selecione
Dashboard, e você verá a imagem em execução na porta 5433. Passe o mouse sobre o nome da imagem e verá os comandos para
parar, iniciar e excluir (entre outros), conforme mostrado na Figura 21-2.

774
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Figura 21-2. Painel de encaixe

Instalando o SQL Server 2019 Uma

instância especial do SQL Server (chamada (localdb)\mssqllocaldb) é instalada com o Visual Studio 2019.
Se você optar por não usar o SQL Server Express LocalDB (ou Docker) e estiver usando uma máquina
Windows, poderá instalar o SQL Server 2019 Developer Edition. O SQL Server 2019 Developer Edition é
gratuito e pode ser baixado aqui:

https://www.microsoft.com/en-us/sql-server/sql-server-downloads

Se você tiver outra edição, também poderá usar essa instância com este livro; você só vai precisar
altere sua tela de conexão apropriadamente.

Instalando um IDE do SQL Server


O Azure Data Studio é um novo IDE para uso com o SQL Server. É gratuito e multiplataforma, portanto, funcionará no
Windows, Mac ou Linux. Ele pode ser baixado daqui:

https://docs.microsoft.com/en-us/sql/azure-data-studio/download-azure-data-studio

ÿ Observação Se você estiver usando uma máquina Windows e preferir usar o SQL Server Management
Studio (SSMS), poderá baixar a cópia mais recente aqui: https://docs.microsoft.com/en-us/sql/ssms/download-
sql server-management-studio-ssms.

Conectando-se ao SQL Server Depois de


instalar o Azure Data Studio ou o SSMS, é hora de conectar-se à sua instância de banco de dados. As
seções a seguir abordam a conexão com o SQL Server em um contêiner Docker ou LocalDb. Se você
estiver usando outra instância do SQL Server, atualize a cadeia de conexão usada nas seções a seguir
adequadamente.

775
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Conectando-se ao SQL Server em um Docker Container


Para se conectar à sua instância do SQL Server em execução em um contêiner do Docker, primeiro verifique se ela está funcionando.
Em seguida, clique em “Criar uma conexão” no Azure Data Studio, conforme mostrado na Figura 21-3.

Figura 21-3. Criando uma conexão no Azure Data Studio

Na caixa de diálogo Connection Details, insira .,5433" para o valor Server. O ponto indica o host atual e ,5433 é a porta que você
indicou ao criar a instância do SQL Server no contêiner do Docker.
Digite sa para o nome de usuário; e a senha é a mesma que você digitou ao criar a instância do SQL Server. O nome é opcional, mas
permite que você selecione rapidamente essa conexão nas sessões subsequentes do Azure Data Studio. A Figura 21-4 mostra essas
opções de conexão.

776
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Figura 21-4. Configurando as opções de conexão para o Docker SQL Server

777
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Conectando-se ao SQL Server LocalDb


Para se conectar à versão instalada do Visual Studio do SQL Server Express LocalDb, atualize as informações de
conexão para corresponder ao que é mostrado na Figura 21-5.

Figura 21-5. Configurando as opções de conexão para SQL Server LocalDb

Ao conectar-se ao LocalDb, você pode usar a Autenticação do Windows, pois a instância está sendo executada em
a mesma máquina que o Azure Data Studio e o mesmo contexto de segurança do usuário conectado no momento.

778
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Conectando-se a qualquer outra instância do SQL Server

Se você estiver se conectando a qualquer outra instância do SQL Server, atualize as propriedades da conexão adequadamente.

Restaurando o backup do banco de dados do AutoLot


Em vez de criar o banco de dados do zero, você pode usar o SSMS ou o Azure Data Studio para restaurar um dos backups fornecidos
contidos nos arquivos do capítulo no repositório. Há dois backups fornecidos: o chamado AutoLotWindows.ba_ foi projetado para uso
em uma máquina Windows (LocalDb, Windows Server etc.) e o chamado AutoLotDocker.ba_ foi projetado para uso em um contêiner
do Docker.

ÿ Observação Git, por padrão, ignora arquivos com uma extensão bak . Você precisará renomear a extensão de ba_ para
bak antes de restaurar o banco de dados.

Copiando o arquivo de backup para seu contêiner Se você estiver


usando o SQL Server em um contêiner do Docker, primeiro deverá copiar o arquivo de backup para o contêiner.
Felizmente, a CLI do Docker fornece um mecanismo para trabalhar com o sistema de arquivos de um contêiner. Primeiro, crie um
novo diretório para o backup usando o seguinte comando em uma janela de comando em sua máquina host:

docker exec -it AutoLot mkdir var/opt/mssql/backup

A estrutura do caminho deve corresponder ao sistema operacional do contêiner (neste caso, Ubuntu), mesmo que sua
máquina host seja baseada no Windows. Em seguida, copie o backup para seu novo diretório usando o seguinte comando (atualizando
o local de AutoLotDocker.bak para o caminho relativo ou absoluto de sua máquina local):

[Windows]
docker cp .\AutoLotDocker.bak AutoLot:var/opt/mssql/backup

[Não Windows]
docker cp ./AutoLotDocker.bak AutoLot:var/opt/mssql/backup

Observe que a estrutura do diretório de origem corresponde à máquina host (no meu exemplo, Windows), enquanto
o destino é o nome do contêiner e, em seguida, o caminho do diretório (no formato do sistema operacional de destino).

Restaurando o banco de dados com o SSMS Para restaurar o banco de

dados usando o SSMS, clique com o botão direito do mouse no nó Bancos de dados no Pesquisador de objetos. Selecione Restaurar
banco de dados. Selecione Dispositivo e clique nas reticências. Isso abrirá a caixa de diálogo Selecionar dispositivo de backup.

779
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Restaurando o banco de dados para SQL Server (Docker)


Mantenha o “Tipo de mídia de backup” definido como Arquivo e clique em Adicionar, navegue até o arquivo AutoLotDocker.bak
no contêiner e clique em OK. Quando voltar à tela principal de restauração, clique em OK, conforme mostrado na Figura 21-6.

Figura 21-6. Restaurando o banco de dados com SSMS

Restaurando o banco de dados para SQL Server (Windows)


Mantenha o “Tipo de mídia de backup” definido como Arquivo e clique em Adicionar, navegue até AutoLotWindows.bak e
clique em OK. Quando voltar à tela principal de restauração, clique em OK, conforme mostrado na Figura 21-7.

780
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Figura 21-7. Restaurando o banco de dados com SSMS

Restaurando o Banco de Dados com o Azure Data Studio Para restaurar o banco de

dados usando o Azure Data Studio, clique em Exibir, selecione a Paleta de Comandos (ou pressione Ctrl+Shift+P) e
selecione Restaurar. Selecione “Arquivo de backup” como a opção “Restaurar de” e, em seguida, o arquivo que você acabou de
copiar. O banco de dados de destino e os campos relacionados serão preenchidos para você, conforme mostrado na Figura 21-8.

781
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Figura 21-8. Restaurando o banco de dados para o Docker usando o Azure Data Studio

ÿ Observação O processo é o mesmo para restaurar a versão do Windows do backup usando o Azure Data Studio.

Basta ajustar o nome do arquivo e os caminhos.

Criando o banco de dados AutoLot


Esta seção inteira é dedicada à criação do banco de dados AutoLot usando o Azure Data Studio. Se você estiver usando o
SSMS, poderá seguir essas etapas usando os scripts SQL discutidos aqui ou usando as ferramentas da GUI. Se você restaurou
o backup, pode pular para a seção “O modelo de fábrica do provedor de dados ADO.NET”.

ÿ Observação Todos os arquivos de script estão localizados em uma pasta chamada Scripts junto com o código deste capítulo no

repositório Git.

782
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Criando o banco de dados


Para criar o banco de dados AutoLot, conecte-se ao seu servidor de banco de dados usando o Azure Data Studio. Abra uma nova
consulta selecionando File ÿ New Query (ou pressionando Ctrl+N) e inserindo o seguinte texto de comando:

USE [mestre]
IR
/****** Objeto: Banco de Dados [AutoLot50] Data do roteiro: 20/12/2020 01:48:05 ******/
CRIAR BANCO DE DADOS [AutoLot]
IR
ALTER DATABASE [AutoLot50] DEFINIR RECUPERAÇÃO SIMPLES
IR

Além de alterar o modo de recuperação para simples, isso cria o banco de dados AutoLot usando os padrões do SQL
Server. Clique em Executar (ou pressione F5) para criar o banco de dados.

Criando as Tabelas
O banco de dados AutoLot contém cinco tabelas: Inventory, Makes, Customers, Orders e CreditRisks.

Criando a Tabela de Inventário


Com o banco de dados criado, é hora de criar as tabelas. A primeira é a tabela Inventário. Abra uma nova consulta e digite o
seguinte SQL:

USE [Lote Automático]


IR
CREATE TABLE [dbo].[Inventário](
[Id] [int] IDENTIDADE(1,1) NÃO NULO,
[MakeId] [int] NÃO NULO,
[Cor] [nvarchar](50) NÃO NULO,
[PetName] [nvarchar](50) NÃO NULO,
[TimeStamp] [timestamp] NULL,
CONSTRAINT [PK_Inventory] PRIMARY KEY CLUSTERED (

[ID] ASC
) EM [PRIMÁRIO]
) EM [PRIMÁRIO]
IR

Clique em Executar (ou pressione F5) para criar a tabela.

Criando a tabela Marcas A tabela

Inventário armazena uma chave estrangeira para a tabela Marcas (ainda não criada). Crie uma nova consulta
e digite o seguinte SQL para criar a tabela Makes:

USE [Lote Automático]


IR
CREATE TABLE [dbo].[Faz](

783
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

[Id] [int] IDENTIDADE(1,1) NÃO NULO,


[Nome] [nvarchar](50) NÃO NULO,
[TimeStamp] [timestamp] NULL,
CONSTRAINT [PK_Makes] PRIMARY KEY CLUSTERED (

[ID] ASC
) EM [PRIMÁRIO]
) EM [PRIMÁRIO]
IR

Clique em Executar (ou pressione F5) para criar a tabela.

Criando a tabela de clientes


A tabela Clientes (como o nome sugere) conterá uma lista de clientes. Crie uma nova consulta e digite os seguintes comandos SQL:

USE [Lote Automático]


IR
CREATE TABLE [dbo].[Clientes](
[Id] [int] IDENTIDADE(1,1) NÃO NULO,
[Nome] [nvarchar](50) NÃO NULO,
[Sobrenome] [nvarchar](50) NÃO NULO,
[TimeStamp] [timestamp] NULL,
CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED (

[ID] ASC
) EM [PRIMÁRIO]
) EM [PRIMÁRIO]
IR

Clique em Executar (ou pressione F5) para criar a tabela Clientes.

Criando a Tabela de Pedidos


Você usará a próxima tabela, Pedidos, para representar o automóvel que um determinado cliente encomendou. Crie uma nova
consulta, insira o código a seguir e clique em Executar (ou pressione F5):

USE [Lote Automático]


IR
CREATE TABLE [dbo].[Orders]( [Id] [int]
IDENTITY(1,1) NOT NULL, [CustomerId] [int]
NOT NULL, [CarId] [int] NOT NULL, [TimeStamp]
[timestamp] NULL , CONSTRAINT [PK_Orders]
PRIMARY KEY CLUSTERED (

[ID] ASC
) EM [PRIMÁRIO]
) EM [PRIMÁRIO]
IR

784
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Criando a tabela CreditRisks Você usará


sua tabela final, CreditRisks, para representar os clientes que são considerados um risco de crédito.
Crie uma nova consulta, insira o código a seguir e clique em Executar (ou pressione F5):

USE [Lote Automático]


IR
CREATE TABLE [dbo].[CreditRisks](
[Id] [int] IDENTIDADE(1,1) NÃO NULO,
[Nome] [nvarchar](50) NÃO NULO,
[Sobrenome] [nvarchar](50) NÃO NULO,
[CustomerId] [int] NÃO NULO,
[TimeStamp] [timestamp] NULL,
CONSTRAINT [PK_CreditRisks] PRIMARY KEY CLUSTERED (

[ID] ASC
) EM [PRIMÁRIO]
) EM [PRIMÁRIO]
IR

Criando os Relacionamentos de Tabela

Esta próxima seção adicionará os relacionamentos de chave estrangeira entre as tabelas inter-relacionadas.

Criando o inventário para fazer relacionamento

Abra uma nova consulta, digite o seguinte SQL e clique em Executar (ou pressione F5):

USE [Lote Automático]


IR
CREATE NONCLUSTERED INDEX [IX_Inventory_MakeId] ON [dbo].[Inventory] (

[MakeId] ASC
) EM [PRIMÁRIO]
IR
ALTER TABLE [dbo].[Inventory] WITH CHECK ADD CONSTRAINT [FK_Make_Inventory] FOREIGN KEY([MakeId])

REFERÊNCIAS [dbo].[Faz] ([Id])


IR
ALTER TABLE [dbo].[Inventory] CHECK CONSTRAINT [FK_Make_Inventory]
IR

Criando o Relacionamento Estoque para Pedidos

Abra uma nova consulta, digite o seguinte SQL e clique em Executar (ou pressione F5):

USE [Lote Automático]


IR
CREATE NONCLUSTERED INDEX [IX_Orders_CarId] ON [dbo].[Orders]

785
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

(
[CarId] ASC
) EM [PRIMÁRIO]
IR
ALTER TABLE [dbo].[Orders] WITH CHECK ADD CONSTRAINT [FK_Orders_Inventory] FOREIGN KEY([CarId])

REFERÊNCIAS [dbo].[Inventário] ([Id])


IR
ALTER TABLE [dbo].[Orders] CHECK CONSTRAINT [FK_Orders_Inventory]
IR

Criando o Relacionamento Pedidos para Clientes


Abra uma nova consulta, digite o seguinte SQL e clique em Executar (ou pressione F5):

USE [Lote Automático]


IR
CRIAR ÍNDICE NÃO CLUSTERED ÚNICO [IX_Orders_CustomerId_CarId] ON [dbo].[Pedidos] (

[CustomerId] ASC,
[CarId] ASC
) EM [PRIMÁRIO]
IR
ALTER TABLE [dbo].[Orders] WITH CHECK ADD CONSTRAINT [FK_Orders_Customers] FOREIGN KEY([CustomerId])

REFERÊNCIAS [dbo].[Clientes] ([Id])


EM APAGAR EM CASCATA
IR
ALTER TABLE [dbo].[Orders] CHECK CONSTRAINT [FK_Orders_Customers]
IR

Criando o Relacionamento Clientes para Riscos de Crédito


Abra uma nova consulta, digite o seguinte SQL e clique em Executar (ou pressione F5):

USE [Lote Automático]


IR
CREATE NONCLUSTERED INDEX [IX_CreditRisks_CustomerId] ON [dbo].[CreditRisks] (

[CustomerId] ASC
) EM [PRIMÁRIO]
IR
ALTER TABLE [dbo].[CreditRisks] WITH CHECK ADD CONSTRAINT [FK_CreditRisks_Customers]
CHAVE ESTRANGEIRA([CustomerId])
REFERÊNCIAS [dbo].[Clientes] ([Id])
EM APAGAR EM CASCATA
IR
ALTER TABLE [dbo].[CreditRisks] CHECK CONSTRAINT [FK_CreditRisks_Customers]
IR

786
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

ÿ Observação Se você está se perguntando por que existem colunas para FirstName e LastName e um relacionamento com a

tabela customer, é apenas para fins de demonstração. Eu poderia pensar em uma razão criativa para isso, mas no final das contas,

isso configura o capítulo 23 muito bem.

Criando o procedimento armazenado GetPetName()


Mais adiante neste capítulo, você aprenderá como usar o ADO.NET para invocar procedimentos armazenados. Como você já deve
saber, stored procedures são rotinas de código armazenadas em um banco de dados que fazem alguma coisa. Como os métodos
C#, os procedimentos armazenados podem retornar dados ou apenas operar em dados sem retornar nada. Você adicionará um
único procedimento armazenado que retornará o nome do animal de estimação de um automóvel, com base no carId fornecido. Para
fazer isso, crie uma nova janela de consulta e insira o seguinte comando SQL:

USE [Lote Automático]

GO CREATE PROCEDURE [dbo].[GetPetName]


@carID int, @petName nvarchar(50) saída AS
SELECT @petName = PetName de dbo.Inventory
onde Id = @carID GO

Clique em Executar (ou pressione F5) para criar o procedimento armazenado.

Adicionando registros de teste Os

bancos de dados são bastante chatos sem dados, e é uma boa ideia ter scripts que possam carregar rapidamente os registros
de teste no banco de dados.

Faz recordes de mesa


Crie uma nova consulta e execute as seguintes instruções SQL para adicionar registros à tabela Makes:

USE [Lote Automático]

GO SET IDENTITY_INSERT [dbo].[Faz] ON INSERT


INTO [dbo].[Faz] ([Id], [Nome]) VALUES (1, N'VW')
INSERT INTO [dbo].[Faz] ([Id], [Nome]) VALORES (2, N'Ford')
INSERT INTO [dbo].[Faz] ([Id], [Nome]) VALORES (3, N'Saab')
INSERT INTO [dbo].[Faz] ([Id], [Nome]) VALORES (4, N'Yugo')
INSERT INTO [dbo].[Faz] ([Id], [Nome]) VALORES (5, N'BMW')
INSERT INTO [dbo].[Faz] ([Id], [Nome]) VALORES (6, N'Pinto')
SET IDENTITY_INSERT [dbo].[Desativa]

787
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Registros da Tabela de Inventário

Para adicionar registros à sua primeira tabela, crie uma nova consulta e execute as seguintes instruções SQL para
adicionar registros à tabela Inventory:

USE [Lote Automático]


IR
SET IDENTITY_INSERT [dbo].[Inventário] ON GO

INSERT INTO [dbo].[Inventory] ([Id], [MakeId], [Color], [PetName]) VALUES (1, 1, N'Black', N'Zippy')

INSERT INTO [dbo].[Inventory] ([Id], [MakeId], [Color], [PetName]) VALUES (2, 2, N'Rust', N'Rusty')

INSERT INTO [dbo].[Inventory] ([Id], [MakeId], [Color], [PetName]) VALUES (3, 3, N'Black', N'Mel')

INSERT INTO [dbo].[Inventory] ([Id], [MakeId], [Color], [PetName]) VALUES (4, 4, N'Yellow', N'Clunker')

INSERT INTO [dbo].[Inventory] ([Id], [MakeId], [Color], [PetName]) VALUES (5, 5, N'Black', N'Bimmer')

INSERT INTO [dbo].[Inventory] ([Id], [MakeId], [Color], [PetName]) VALUES (6, 5, N'Green', N'Hank')

INSERT INTO [dbo].[Inventory] ([Id], [MakeId], [Color], [PetName]) VALUES (7, 5, N'Pink', N'Pinky')

INSERT INTO [dbo].[Inventory] ([Id], [MakeId], [Color], [PetName]) VALUES (8, 6, N'Black', N'Pete')

INSERT INTO [dbo].[Inventory] ([Id], [MakeId], [Color], [PetName]) VALUES (9, 4, N'Brown', N'Brownie')SET
IDENTITY_INSERT [dbo].[Inventory ] OFF IR

Adicionando registros de teste à tabela de clientes

Para adicionar registros à tabela Clientes, crie uma nova consulta e execute as seguintes instruções SQL:

USE [Lote Automático]


IR
SET IDENTITY_INSERT [dbo].[Clientes] ON INSERT
INTO [dbo].[Clientes] ([Id], [FirstName], [LastName]) VALUES (1, N'Dave', N'Brenner')
INSERT INTO [dbo].[Clientes] ([Id], [FirstName], [LastName]) VALUES (2, N'Matt', N'Walton')
INSERT INTO [dbo].[Clientes] ([Id], [FirstName], [LastName]) VALUES (3, N'Steve', N'Hagen')
INSERT INTO [dbo].[Clientes] ([Id], [FirstName], [LastName]) VALUES (4, N'Pat', N'Walton')
INSERT INTO [dbo].[Clientes] ([Id], [FirstName], [LastName]) VALUES (5, N'Bad', N'Customer')
SET IDENTITY_INSERT [dbo].[Clientes] OFF

Adicionando registros de teste à tabela Orders Agora

adicione dados à sua tabela Orders. Crie uma nova consulta, digite o seguinte SQL e clique em Executar (ou pressione F5):

USE [Lote Automático]


IR
SET IDENTITY_INSERT [dbo].[Pedidos] ON

788
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

INSERT INTO [dbo].[Pedidos] ([Id], [CustomerId], [CarId]) VALORES (1, 1, 5)


INSERT INTO [dbo].[Pedidos] ([Id], [CustomerId], [CarId]) VALORES (2, 2, 1)
INSERT INTO [dbo].[Pedidos] ([Id], [CustomerId], [CarId]) VALORES (3, 3, 4)
INSERT INTO [dbo].[Pedidos] ([Id], [CustomerId], [CarId]) VALORES (4, 4, 7)
SET IDENTITY_INSERT [dbo].[Pedidos] DESLIGADO

Adicionando registros de teste à tabela CreditRisks


A etapa final é adicionar dados à tabela CreditRisks. Crie uma nova consulta, digite o seguinte SQL e clique em Executar (ou pressione
F5):

USE [Lote Automático]


IR
SET IDENTITY_INSERT [dbo].[CreditRisks] ON INSERT INTO
[dbo].[CreditRisks] ([Id], [FirstName], [LastName], [CustomerId]) VALUES (1, N'Bad', N'Customer', 5)

SET IDENTITY_INSERT [dbo].[CreditRisks] OFF

Com isso, o banco de dados do AutoLot está completo! Claro, isso está muito longe de um banco de dados de aplicativo do
mundo real, mas atenderá às suas necessidades para este capítulo e será adicionado nos capítulos do Entity Framework Core.
Agora que você tem um banco de dados para testar, pode mergulhar nos detalhes do modelo de fábrica do provedor de dados
ADO.NET.

O modelo de fábrica do provedor de dados ADO.NET


O padrão de fábrica do provedor de dados .NET Core permite que você crie uma única base de código usando tipos generalizados
de acesso a dados. Para entender a implementação da fábrica do provedor de dados, lembre-se da Tabela 21-1 de que todas as
classes em um provedor de dados derivam das mesmas classes básicas definidas no namespace System.Data.Common.

• DbCommand: A classe base abstrata para todas as classes de comando

• DbConnection: A classe base abstrata para todas as classes de conexão

• DbDataAdapter: A classe base abstrata para todas as classes de adaptadores de dados

• DbDataReader: A classe base abstrata para todas as classes de leitores de dados

• DbParameter: A classe base abstrata para todas as classes de parâmetro

• DbTransaction: A classe base abstrata para todas as classes de transação

Cada um dos provedores de dados compatíveis com .NET Core contém um tipo de classe que deriva de System.Data.
Common.DbProviderFactory. Essa classe base define vários métodos que recuperam objetos de dados específicos do provedor.
Aqui estão os membros do DbProviderFactory:

public abstract class DbProviderFactory { ..public virtual


bool CanCreateDataAdapter { get;}; ..public virtual bool
CanCreateCommandBuilder { get;};

public virtual DbCommand CreateCommand(); public virtual


DbCommandBuilder CreateCommandBuilder();

789
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

public virtual DbConnection CreateConnection(); público virtual


DbConnectionStringBuilder
CreateConnectionStringBuilder(); public virtual
DbDataAdapter CreateDataAdapter(); public virtual DbParameter
CreateParameter(); público virtual DbDataSourceEnumerator

CreateDataSourceEnumerator();
}

Para obter o tipo derivado de DbProviderFactory para seu provedor de dados, cada provedor fornece uma propriedade
estática usada para retornar o tipo correto. Para retornar a versão SQL Server do DbProviderFactory, use o seguinte código:

// Obtenha a fábrica para o provedor de dados SQL.


DbProviderFactory sqlFactory =
Microsoft.Data.SqlClient.SqlClientFactory.Instance;

Para tornar o programa mais versátil, você pode criar uma fábrica DbProviderFactory que retorna um valor específico
tipo de um DbProviderFactory com base em uma configuração no arquivo appsettings.json para o aplicativo. Você aprenderá
como fazer isso em breve; no momento, você pode obter os objetos de dados específicos do provedor associados (por exemplo,
conexões, comandos e leitores de dados) depois de obter a fábrica para o seu provedor de dados.

Um exemplo completo de fábrica de provedores de dados Para obter um exemplo

completo, crie um novo projeto C# Console Application (chamado DataProviderFactory) que imprima o inventário de automóveis
do banco de dados AutoLot. Para este exemplo inicial, você codificará a lógica de acesso a dados diretamente no aplicativo de
console (para manter as coisas simples). À medida que avança neste capítulo, você verá maneiras melhores de fazer isso.

Comece adicionando um novo ItemGroup e o Microsoft.Extensions.Configuration.Json, System.Data.


Common, System.Data.Odbc, System.Data.OleDb e Microsoft.Data.SqlClient para o arquivo de projeto.

dotnet adicionar pacote DataProviderFactory Microsoft.Data.SqlClient dotnet adicionar


pacote DataProviderFactory System.Data.Common dotnet adicionar pacote
DataProviderFactory System.Data.Odbc dotnet adicionar pacote DataProviderFactory
System.Data.OleDb dotnet adicionar pacote DataProviderFactory
Microsoft.Extensions.Configuration.Json

Defina a constante do compilador do PC (se você estiver usando um sistema operacional Windows).

<PropertyGroup>
<DefineConstants>PC</DefineConstants>
</PropertyGroup>

Em seguida, adicione um novo arquivo chamado DataProviderEnum.cs e atualize o código para o seguinte:

namespace DataProviderFactory {

//OleDb é apenas para Windows e não é compatível com .NET Core enum
DataProviderEnum {

Servidor SQL,

790
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

#se PC
OleDb,
#endif
ODBC

}
}

Adicione um novo arquivo JSON chamado appsettings.json ao projeto e atualize seu conteúdo para o seguinte
(atualize as strings de conexão com base em seu ambiente específico):

{
"ProviderName": "SqlServer",
//"ProviderName": "OleDb",
//"ProviderName": "Odbc",
"Servidor SQL": {
// para localdb use @"Data Source=(localdb)\mssqllocaldb;Integrated Security=true;
Catálogo Inicial=Lote Automático"
"ConnectionString": "Data Source=.,5433;User Id=sa;Password=P@ssw0rd;Initial
Catalog=AutoLot" }, "Odbc": { // para localdb use @"Driver={ODBC Driver 17 for SQL
Servidor};Servidor=(localdb)\mssqllocald

b;Database=AutoLot;Trusted_Connection=Sim";
"ConnectionString": "Driver={ODBC Driver 17 para SQL Server};Server=localhost,5433;
Database=AutoLot;UId=sa;Pwd=P@ssw0rd;" },

"OleDb": { //
se localdb use @"Provider=SQLNCLI11;Data Source=(localdb)\mssqllocaldb;Initial
Catálogo=AutoLot;Segurança Integrada=SSPI"),
"ConnectionString": "Provider=SQLNCLI11;Data Source=.,5433;User Id=sa;Password=P@ssw0rd; Initial
Catalog=AutoLot;" }

Informe o MSBuild para copiar o arquivo JSON para o diretório de saída em cada compilação. Atualize o arquivo de
projeto adicionando o seguinte:

<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Sempre</CopyToOutputDirectory> </
None> </ItemGroup>

ÿ Observação O CopyToOutputDirectory é sensível a espaços em branco. Certifique-se de que está tudo em uma linha sem
espaços ao redor da palavra Sempre.

791
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Agora que você tem um appsettings.json adequado, pode ler os valores do provedor e connectionString usando
a configuração do .NET Core. Comece atualizando as instruções using na parte superior de Program.cs para o seguinte:

usando Sistema;
usando System.Data.Common;
usando System.Data.Odbc; #if
PC usando System.Data.OleDb;
#endif usando System.IO;
usando Microsoft.Data.SqlClient;
usando
Microsoft.Extensions.Configuration;

Limpe todo o código do arquivo Program.cs e adicione o seguinte:

usando Sistema;
usando System.Data.Common;
usando System.Data.Odbc; #if
PC usando System.Data.OleDb;
#endif usando System.IO;
usando Microsoft.Data.SqlClient;
usando
Microsoft.Extensions.Configuration;
usando DataProviderFactory;

Console.WriteLine("***** Diversão com Fábricas de Provedores de Dados *****\n"); var


(provedor, connectionString) = GetProviderFromConfiguration(); Fábrica DbProviderFactory
= GetDbProviderFactory(provedor); // Agora pegue o objeto de conexão. usando (conexão
DbConnection = fábrica.CreateConnection()) {

if (conexão == null) {

Console.WriteLine($"Não foi possível criar o objeto de conexão"); retornar;

Console.WriteLine($"Seu objeto de conexão é: {connection.GetType().Name}"); connection.ConnectionString


= connectionString; conexão.Open();

// Torna o objeto de comando.


Comando DbCommand = fábrica.CreateCommand(); if
(comando == nulo) {

Console.WriteLine($"Não foi possível criar o objeto de comando"); retornar;

792
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Console.WriteLine($"Seu objeto de comando é: {command.GetType().Name}");


comando.Connection = conexão; command.CommandText =

"Selecione i.Id, m.Name do inventário i junção interna Faz m em m.Id = i.MakeId";

// Imprime os dados com o leitor de dados.


using (DbDataReader dataReader = command.ExecuteReader()) {

Console.WriteLine($"Seu objeto leitor de dados é: {dataReader.GetType().Name}");


Console.WriteLine("\n***** Inventário atual *****"); while (leitor de dados.Read()) {

Console.WriteLine($"-> Car #{dataReader["Id"]} é um {dataReader["Name"]}."); }

}
}
Console.ReadLine();

Em seguida, adicione o seguinte código ao final do arquivo Program.cs. Esses métodos lêem a configuração,
defina o DataProviderEnum com o valor correto, obtenha a string de conexão e retorne uma instância do
DbProviderFactory:

DbProviderFactory estático GetDbProviderFactory (provedor DataProviderEnum)


=> troca de provedor {

DataProviderEnum.SqlServer => SqlClientFactory.Instance,


DataProviderEnum.Odbc => OdbcFactory.Instance, #if PC
DataProviderEnum.OleDb => OleDbFactory.Instance, #endif => nulo

_
};

estático (DataProviderEnum Provider, string ConnectionString)


GetProviderFromConfiguration() {

IConfiguration config = new ConfigurationBuilder()


.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", verdadeiro, verdadeiro)
.Construir();
var nomedoprovedor = config["Nomedoprovedor"]; if
(Enum.TryParse<DataProviderEnum> (providerName,
fora do provedor DataProviderEnum))
{
return (provider,config[$"{providerName}:ConnectionString"]); }; throw new
Exception("Valor do provedor de dados inválido fornecido."); }

793
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Observe que, para fins de diagnóstico, você usa serviços de reflexão para imprimir o nome da conexão subjacente,
comando e leitor de dados. Se você executar este aplicativo, encontrará os seguintes dados atuais na tabela Inventário do banco
de dados AutoLot impresso no console:

Diversão com fábricas de provedores de dados


***** *****
Seu objeto de conexão é um: SqlConnection
Seu objeto de comando é um: SqlCommand
Seu objeto leitor de dados é um: SqlDataReader

Inventário atual ***** -> O


*****
carro nº 1 é um VW.
-> O carro nº 2 é um Ford.
-> O carro nº 3 é um Saab.
-> O carro nº 4 é um Yugo.
-> O carro nº 9 é um Yugo.
-> O carro nº 5 é um BMW.
-> O carro nº 6 é um BMW.
-> O carro nº 7 é um BMW.
-> O carro nº 8 é um Pinto.

Agora altere o arquivo de configurações para especificar um provedor diferente. O código pegará a string de
conexão relacionada e produzirá a mesma saída de antes, exceto pelas informações específicas do tipo.
Claro, com base em sua experiência com ADO.NET, você pode estar um pouco inseguro sobre qual é exatamente a conexão,
comando e os objetos do leitor de dados realmente fazem. Não se preocupe com os detalhes por enquanto (afinal, ainda restam
algumas páginas neste capítulo!). Neste ponto, basta saber que você pode usar o modelo de fábrica do provedor de dados ADO.NET
para criar uma única base de código que pode consumir vários provedores de dados de maneira declarativa.

Uma desvantagem potencial com o modelo Data Provider Factory Embora este seja um modelo poderoso, você

deve certificar-se de que a base de código use apenas tipos e métodos comuns a todos os provedores por meio dos membros
das classes base abstratas. Portanto, ao criar sua base de código, você está limitado aos membros expostos por DbConnection,
DbCommand e os outros tipos do namespace System.Data.Common.

Diante disso, você pode achar que essa abordagem generalizada o impede de acessar diretamente alguns dos sinos e
assobios de um DBMS específico. Se você precisar invocar membros específicos do provedor subjacente (por exemplo,
SqlConnection), poderá fazê-lo usando uma conversão explícita, como neste exemplo:

if (a conexão é SqlConnection sqlConnection) {

// Imprime qual versão do SQL Server é usada.


WriteLine(sqlConnection.ServerVersion); }

Ao fazer isso, no entanto, sua base de código se torna um pouco mais difícil de manter (e menos flexível) porque você deve
adicionar várias verificações de tempo de execução. No entanto, se você precisar criar bibliotecas de acesso a dados ADO.NET da
maneira mais flexível possível, o modelo de fábrica do provedor de dados fornece um ótimo mecanismo para fazer isso.

ÿ Observe que o Entity Framework Core e seu suporte para injeção de dependência simplificam bastante a criação de

bibliotecas de acesso a dados que precisam acessar fontes de dados diferentes.

Com este primeiro exemplo atrás de você, agora você pode mergulhar nos detalhes do trabalho com ADO.NET. 794
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Mergulhando mais fundo em conexões, comandos


e leitores de dados
Conforme mostrado no exemplo anterior, o ADO.NET permite que você interaja com um banco de dados usando os objetos
de conexão, comando e leitor de dados do seu provedor de dados. Agora você criará um exemplo expandido para obter uma
compreensão mais profunda desses objetos no ADO.NET.
No exemplo anterior demonstrado, você precisa executar os seguintes passos quando quiser se conectar a um
banco de dados e ler os registros usando um objeto leitor de dados:

1. Aloque, configure e abra seu objeto de conexão.

2. Aloque e configure um objeto de comando, especificando o objeto de conexão como um


argumento do construtor ou com a propriedade Connection.

3. Chame ExecuteReader() na classe de comando configurada.

4. Processe cada registro usando o método Read() do leitor de dados.

Para dar o pontapé inicial, crie um novo projeto de aplicativo de console chamado AutoLot.DataReader e adicione o
Pacote Microsoft.Data.SqlClient. Aqui está o código completo dentro de Program.cs (a análise seguirá):

usando Sistema;
usando Microsoft.Data.SqlClient;

Console.WriteLine("***** Diversão com Leitores de Dados *****\n");

// Cria e abre uma conexão. using


(SqlConnection connection = new SqlConnection())
{ connection.ConnectionString = @" Data Source=.,5433;User
Id=sa;Password=P@ssw0rd;Initial Catalog=AutoLot"; conexão.Open(); //
Cria um objeto de comando SQL. string sql = @"Selecione i.id, m.Name como Make, i.Color, i.Petname

FROM Inventory i
INNER JOIN Faz m em m.Id = i.MakeId"; SqlCommand
myCommand = new SqlCommand(sql, connection);

// Obtém um leitor de dados a la ExecuteReader(). using


(SqlDataReader myDataReader = myCommand.ExecuteReader()) { // Percorre os
resultados. while (myDataReader.Read()) { Console.WriteLine($"-> Make:
{myDataReader["Make"]}, PetName: {myDataReader ["PetName"]}, Color:
{myDataReader["Color"]}. ");

}}}

Console.ReadLine();

795
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Trabalhando com objetos de conexão


A primeira etapa a ser executada ao trabalhar com um provedor de dados é estabelecer uma sessão com a fonte de dados usando o objeto
de conexão (que, como você lembra, deriva de DbConnection). Os objetos de conexão .NET Core são fornecidos com uma string de conexão
formatada; esta string contém um número de pares nome-valor, separados por ponto-e-vírgula. Você usa essas informações para identificar
o nome da máquina à qual deseja se conectar, as configurações de segurança necessárias, o nome do banco de dados nessa máquina e
outras informações específicas do provedor de dados.

Como você pode inferir do código anterior, o nome do Catálogo Inicial refere-se ao banco de dados que você deseja
estabelecer uma sessão com. O nome da fonte de dados identifica o nome da máquina que mantém o
base de dados. Estou usando ".,5433", que se refere à máquina host (o ponto, que é o mesmo que usar “localhost”) e a porta 5433, que
é a porta que o contêiner do Docker mapeou para a porta do SQL Server. Se você estiver usando uma instância diferente, defina a
propriedade como machinename,port\instance. Por exemplo, MYSERVER\SQLSERVER2019 significa MYSERVER é o nome do servidor no
qual o SQL Server está sendo executado, a porta padrão está sendo usada e SQLSERVER2019 é o nome da instância. Se a máquina for
local para o desenvolvimento, você pode usar um ponto (.) ou o token (localhost) para o nome do servidor. Se a instância do SQL Server for
a instância padrão, o nome da instância será deixado de fora. Por exemplo, se você criou o AutoLot em uma instalação do Microsoft SQL
Server configurada como a instância padrão em seu computador local, você usaria "Data Source=localhost".

Além disso, você pode fornecer qualquer número de tokens que representam credenciais de segurança. Se a Segurança integrada
for definida como verdadeira, as credenciais atuais da conta do Windows serão usadas para autenticação e autorização.
Depois de estabelecer sua string de conexão, você pode usar uma chamada para Open() para estabelecer uma conexão com
o SGBD. Além dos membros ConnectionString, Open() e Close(), um objeto de conexão fornece vários membros que permitem definir
configurações adicionais relacionadas à sua conexão, como configurações de tempo limite e informações transacionais. A Tabela
21-4 lista alguns (mas não todos) membros da classe base DbConnection.

Tabela 21-4. Membros do tipo DbConnection

Membro Significado na vida


BeginTransaction() Você usa esse método para iniciar uma transação de banco de dados.

ChangeDatabase() Você usa esse método para alterar o banco de dados em uma conexão aberta.

ConnectionTimeout Essa propriedade somente leitura retorna o tempo de espera ao estabelecer uma conexão antes de
terminar e gerar um erro (o valor padrão depende do provedor). Se você quiser alterar o padrão,
especifique um segmento Connect Timeout na string de conexão (por exemplo, Connect Timeout=30).

Base de dados Essa propriedade somente leitura obtém o nome do banco de dados mantido pelo objeto de
conexão.

Fonte de dados Essa propriedade somente leitura obtém a localização do banco de dados mantido pelo objeto de
conexão.

GetSchema() Esse método retorna um objeto DataTable que contém informações de esquema da fonte de dados.

Estado Essa propriedade somente leitura obtém o estado atual da conexão, que é representado pela
enumeração ConnectionState.

796
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

As propriedades do tipo DbConnection são normalmente de natureza somente leitura e são úteis apenas quando você
deseja obter as características de uma conexão em tempo de execução. Quando você precisar substituir as configurações
padrão, deverá alterar a própria string de conexão. Por exemplo, a seguinte string de conexão define a configuração de tempo
limite de conexão do padrão (15 segundos para SQL Server) para 30 segundos:

using(conexão SqlConnection = new SqlConnection()) {

connection.ConnectionString = @" Data


Source=.,5433;User Id=sa;Password=P@ssw0rd;Initial Catalog=AutoLot;Connect Timeout=30"; conexão.Open(); }

O código a seguir mostra detalhes sobre o SqlConnection que passou para ele:

static void ShowConnectionStatus(conexão SqlConnection) {

// Mostra várias estatísticas sobre o objeto de conexão atual.


Console.WriteLine("***** Informações sobre sua conexão *****");
Console.WriteLine($@"Localização do banco de dados: {connection.DataSource}");
Console.WriteLine($"Nome do banco de dados: {connection.Database}");
Console.WriteLine($@"Tempo limite:

{connection.ConnectionTimeout}");
Console.WriteLine($"Estado da conexão:
{connection.State}\n");
}

Embora a maioria dessas propriedades seja auto-explicativa, a propriedade do Estado merece uma menção especial. Você
pode atribuir a essa propriedade qualquer valor da enumeração ConnectionState, conforme mostrado aqui:

public enum ConnectionState {

Quebrado,
Fechado,
Conectando,
Executando,
Buscando,
Abrir }

No entanto, os únicos valores ConnectionState válidos são ConnectionState.Open, ConnectionState.


Connecting e ConnectionState.Closed (os membros restantes deste enum são reservados para uso futuro). Além disso, é
sempre seguro fechar uma conexão, mesmo se o estado da conexão for atualmente ConnectionState.
Fechado.

797
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Trabalhando com objetos ConnectionStringBuilder


Trabalhar com cadeias de conexão de forma programática pode ser complicado porque elas geralmente são representadas como
strings literais, que são difíceis de manter e, na melhor das hipóteses, propensas a erros. Os provedores de dados compatíveis
com .NET Core oferecem suporte a objetos construtores de strings de conexão, que permitem estabelecer os pares nome-valor
usando propriedades fortemente tipadas. Considere a seguinte atualização no código atual:

var connectionStringBuilder = new SqlConnectionStringBuilder { InitialCatalog =


"AutoLot", DataSource = ".,5433", UserID = "sa", Senha = "P@ssw0rd", ConnectTimeout
= 30 }; connection.ConnectionString = connectionStringBuilder.ConnectionString;

Nesta iteração, você cria uma instância de SqlConnectionStringBuilder, define as propriedades de acordo,
e obtenha a string interna usando a propriedade ConnectionString. Observe também que você usa o construtor padrão do
tipo. Se assim o desejar, você também pode criar uma instância do objeto construtor de string de conexão do seu provedor de
dados passando uma string de conexão existente como ponto de partida (isso pode ser útil quando você lê esses valores
dinamicamente de uma fonte externa). Depois de hidratar o objeto com os dados iniciais da string, você pode alterar os pares
nome-valor específicos usando as propriedades relacionadas.

Trabalhando com objetos de comando


Agora que você entende melhor a função do objeto de conexão, a próxima tarefa é verificar como enviar consultas SQL ao
banco de dados em questão. O tipo SqlCommand (que deriva de DbCommand) é uma representação OO de uma consulta
SQL, nome de tabela ou procedimento armazenado. Você especifica o tipo de comando usando a propriedade CommandType,
que pode receber qualquer valor da enumeração CommandType, conforme mostrado aqui:

public enum CommandType


{ StoredProcedure, TableDirect,
Text // Valor padrão.

Ao criar um objeto de comando, você pode estabelecer a consulta SQL como um parâmetro do construtor ou diretamente
usando a propriedade CommandText. Além disso, ao criar um objeto de comando, você precisa especificar a conexão que
deseja usar. Novamente, você pode fazer isso como um parâmetro do construtor ou usando a propriedade Connection. Considere
este trecho de código:

// Cria objeto de comando via ctor args. string sql =


@"Selecione i.id, m.Name como Make, i.Color,
i.Petname FROM Inventory i INNER JOIN Faz m em m.Id = i.MakeId";
SqlCommand meuComando = new SqlCommand(sql, conexão);

798
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

// Cria outro objeto de comando via propriedades.


SqlCommand testCommand = new SqlCommand();
testCommand.Connection = conexão; testeCommand.CommandText
= sql;

Perceba que, neste ponto, você não enviou a consulta SQL ao banco de dados AutoLot, mas
em vez disso, preparou o estado do objeto de comando para uso futuro. A Tabela 21-5 destaca alguns membros adicionais do tipo
DbCommand.

Tabela 21-5. Membros do tipo DbCommand

Membro Significado na vida


CommandTimeout Obtém ou define o tempo de espera durante a execução do comando antes de encerrar a tentativa e gerar
um erro. O padrão é 30 segundos.

Conexão Obtém ou define o DbConnection usado por esta instância do DbCommand.


Parâmetros Obtém a coleção de objetos DbParameter usados para uma consulta parametrizada.

Cancelar() Cancela a execução de um comando.

ExecuteReader() Executa uma consulta SQL e retorna o objeto DbDataReader do provedor de dados, que fornece acesso
somente encaminhamento e somente leitura para o resultado da consulta.

ExecuteNonQuery() Executa uma não consulta SQL (por exemplo, uma inserção, atualização, exclusão ou criação de tabela).

ExecuteScalar() Uma versão leve do método ExecuteReader() que foi projetada especificamente para consultas
singleton (por exemplo, obtenção de uma contagem de registros).

Preparar() Cria uma versão preparada (ou compilada) do comando na fonte de dados. Como você deve saber, uma
consulta preparada é executada um pouco mais rápido e é útil quando você precisa executar a mesma
consulta várias vezes (normalmente com parâmetros diferentes a cada vez).

Trabalhando com leitores de dados Depois de

estabelecer a conexão ativa e o comando SQL, a próxima etapa é enviar a consulta à fonte de dados. Como você pode imaginar, você
tem várias maneiras de fazer isso. O tipo DbDataReader (que implementa IDataReader) é a maneira mais simples e rápida de obter
informações de um armazenamento de dados. Lembre-se de que os leitores de dados representam um fluxo de dados somente leitura e
somente encaminhamento retornado um registro por vez. Diante disso, os leitores de dados são úteis apenas ao enviar instruções de seleção
SQL para o armazenamento de dados subjacente.
Os leitores de dados são úteis quando você precisa iterar rapidamente grandes quantidades de dados e não precisa manter uma
representação na memória. Por exemplo, se você solicitar 20.000 registros de uma tabela para armazenar em um arquivo de texto,
seria um pouco intensivo em memória manter essas informações em um DataSet (porque um DataSet mantém todo o resultado da
consulta na memória ao mesmo tempo).
Uma abordagem melhor é criar um leitor de dados que gire sobre cada registro o mais rápido possível. Estar ciente,
no entanto, os objetos do leitor de dados (ao contrário dos objetos do adaptador de dados, que você examinará posteriormente) mantêm
uma conexão aberta com sua fonte de dados até que você feche explicitamente a conexão.
Você obtém objetos de leitura de dados do objeto de comando usando uma chamada para ExecuteReader(). O leitor de dados
representa o registro atual que leu do banco de dados. O leitor de dados possui um método indexador (por exemplo, sintaxe [] em C#) que
permite acessar uma coluna no registro atual. Você pode acessar a coluna por nome ou por inteiro baseado em zero.

O seguinte uso do leitor de dados aproveita o método Read() para determinar quando você atingiu
o final de seus registros (usando um valor de retorno falso). Para cada registro de entrada que você lê no

799
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

banco de dados, você usa o indexador de tipo para imprimir a marca, o nome do animal de estimação e a cor de cada automóvel.
Observe também que você chama Close() assim que terminar de processar os registros, o que libera o objeto de conexão.

...
// Obtém um leitor de dados via ExecuteReader().
using(SqlDataReader myDataReader = myCommand.ExecuteReader()) {

// Percorre os resultados. while


(myDataReader.Read())
{ WriteLine($"-> Make:
{ myDataReader["Make"]}, PetName: { myDataReader["PetName"]}, Color: { myDataReader["Color"]}.") ; }

}
Leia a linha();

No snippet anterior, você sobrecarrega o indexador de um objeto de leitor de dados para obter uma string
(representando o nome da coluna) ou um int (representando a posição ordinal da coluna). Assim, você pode limpar a lógica
atual do leitor (e evitar nomes de string codificados) com a seguinte atualização (observe o uso da propriedade FieldCount):

while (myDataReader.Read()) { for


(int i = 0; i < myDataReader.FieldCount;
i++) { Console.Write(i != myDataReader.FieldCount - 1

? $"{meuDataReader.GetName(i)} = {meuDataReader.GetValue(i)}, " :


$"{meuDataReader.GetName(i)} = {meuDataReader.GetValue(i)} ");
}
Console.WriteLine(); }

Se você compilar e executar seu projeto neste ponto, deverá ver uma lista de todos os automóveis no
Tabela de inventário do banco de dados AutoLot.

***** Diversão com leitores de dados *****

***** Informações sobre sua conexão *****


Localização do banco de dados: .,5433 Nome do
banco de dados: AutoLot Timeout: 30 Estado da
conexão: Aberto

id = 1, Marca = VW, Cor = Preto, Petname = Zippy id = 2, Marca


= Ford, Cor = Rust, Petname = Rusty id = 3, Marca = Saab, Cor =
Preto, Petname = Mel id = 4, Marca = Yugo, Cor = Amarelo,
Petname = Clunker id = 5, Marca = BMW, Cor = Preto, Petname =
Bimmer id = 6, Marca = BMW, Cor = Verde, Petname = Hank id = 7,
Marca = BMW, Cor = Rosa, Petname = Pinky id = 8, Make = Pinto, Color
= Black, Petname = Pete id = 9, Make = Yugo, Color = Brown, Petname
= Brownie

800
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Obtendo vários conjuntos de resultados usando um leitor de dados


Os objetos do leitor de dados podem obter vários conjuntos de resultados usando um único objeto de comando. Por exemplo, se
você deseja obter todas as linhas da tabela Inventário, bem como todas as linhas da tabela Clientes, pode especificar ambas as
instruções SQL Select usando um delimitador de ponto e vírgula, da seguinte forma:

sql += ";Selecionar * de Clientes;";

ÿ Observação O ponto e vírgula no início não é um erro de digitação. Ao usar várias instruções, elas devem ser
separadas por ponto e vírgula. E como a instrução inicial não continha uma, ela é adicionada aqui no início da segunda
instrução.

Depois de obter o leitor de dados, você pode iterar sobre cada conjunto de resultados usando o método NextResult().
Observe que você sempre retorna o primeiro conjunto de resultados automaticamente. Portanto, se você quiser ler as linhas de cada
tabela, poderá criar a seguinte construção de iteração:

do

{ while (myDataReader.Read()) { for (int i


= 0; i < myDataReader.FieldCount; i+
+) { Console.Write(i != myDataReader.FieldCount - 1

? $"{meuDataReader.GetName(i)} = {meuDataReader.GetValue(i)}, " :


$"{meuDataReader.GetName(i)} = {meuDataReader.GetValue(i)} ");
}
Console.WriteLine(); }

Console.WriteLine(); } while
(myDataReader.NextResult());

Neste ponto, você deve estar mais ciente da funcionalidade que os objetos do leitor de dados trazem para a mesa.
Lembre-se sempre de que um leitor de dados pode processar apenas instruções SQL Select; você não pode usá-los para
modificar uma tabela de banco de dados existente usando solicitações Insert, Update ou Delete. Modificar um banco de dados
existente requer investigação adicional de objetos de comando.

Trabalhando com criar, atualizar e excluir consultas


O método ExecuteReader() extrai um objeto de leitor de dados que permite examinar os resultados de uma instrução SQL Select
usando um fluxo de informações somente de leitura e encaminhamento. No entanto, quando você deseja enviar instruções SQL
que resultam na modificação de uma determinada tabela (ou qualquer outra instrução SQL não consulta, como criar tabelas ou
conceder permissões), chame o método ExecuteNonQuery() de seu objeto de comando.
Esse método único executa inserções, atualizações e exclusões com base no formato do texto do comando.

ÿ Observação Tecnicamente falando, uma não consulta é uma instrução SQL que não retorna um conjunto de resultados.

Portanto, as instruções Select são consultas, enquanto as instruções Insert, Update e Delete não são. Diante disso,
ExecuteNonQuery() retorna um int que representa o número de linhas afetadas, não um novo conjunto de registros.

801
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Todos os exemplos de interação de banco de dados neste capítulo até agora só abriram conexões e as usaram para
recuperar dados. Esta é apenas uma parte do trabalho com um banco de dados; uma estrutura de acesso a dados não seria
muito útil, a menos que também suportasse totalmente a funcionalidade Criar, Ler, Atualizar e Excluir (CRUD). A seguir, você
aprenderá como fazer isso usando chamadas para ExecuteNonQuery().
Comece criando um novo projeto C# Class Library chamado AutoLot.Dal (abreviação de AutoLot data access
camada), exclua o arquivo de classe padrão e adicione o pacote Microsoft.Data.SqlClient ao projeto.
Antes de construir a classe que conduzirá as operações de dados, primeiro criaremos uma classe C# que
representa um registro da tabela Inventário com suas informações de marca relacionadas.

Crie as classes Car e CarViewModel


Bibliotecas modernas de acesso a dados usam classes (comumente chamadas de modelos ou entidades) que são usadas
para representar e transportar os dados do banco de dados. Além disso, as classes podem ser usadas para representar
uma visão dos dados que combina duas ou mais tabelas para tornar os dados mais significativos. As classes de entidade são
usadas para trabalhar com o diretório do banco de dados (para instruções de atualização) e as classes de modelo de exibição
são usadas para exibir os dados de maneira significativa. Você verá no próximo capítulo que esses conceitos são a base dos
mapeadores relacionais de objeto (ORMs) como o Entity Framework Core, mas, por enquanto, você criará apenas um modelo
(para uma linha de inventário bruto) e um modelo de exibição ( combinando uma linha de inventário com os dados relacionados
na tabela Makes). Adicione uma nova pasta ao seu projeto chamada Models e adicione dois novos arquivos, chamados Car.cs
e CarViewModel.cs. Atualize o código para o seguinte:

//espaço
de nomes Car.cs AutoLot.Dal.Models {

classe pública Carro {

public int ID { obter; definir; } public


string Color { get; definir; } public int MakeId
{ get; definir; } public string PetName { get;
definir; } public byte[] TimeStamp {get;set;} }

//Espaço de nome
CarViewModel.cs AutoLot.Dal.Models {

public class CarViewModel: Carro {

string pública Faça { get; definir; } }

ÿ Observação Se você não estiver familiarizado com o tipo de dados TimeStamp do SQL Server (que mapeia para um byte[] em C#),

não se preocupe com isso neste momento. Apenas saiba que ele é usado para verificação de simultaneidade em nível de linha e será

coberto pelo Entity Framework Core.

Essas classes serão usadas em breve.

802
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Adicionando a classe InventoryDal


Em seguida, adicione uma nova pasta chamada DataOperations. Nessa nova pasta, adicione uma nova classe chamada
InventoryDal.cs e altere a classe para public. Essa classe definirá vários membros para interagir com a tabela Inventory
do banco de dados AutoLot. Finalmente, importe os seguintes namespaces:

usando Sistema;
usando System.Collections.Generic; usando
System.Data; usando AutoLot.Dal.Models;
usando Microsoft.Data.SqlClient;

Adicionando construtores Crie

um construtor que receba um parâmetro de string (connectionString) e atribua o valor a uma variável de nível
de classe. Em seguida, crie um construtor sem parâmetros que passe uma string de conexão padrão para o
outro construtor. Isso permite que o código de chamada altere a string de conexão do padrão. O código
correspondente é o seguinte:

namespace AuoLot.Dal.DataOperations {

public class InventoryDal {

string privada somente leitura _connectionString; public


InventoryDal() : this( @"Data Source=.,5433;User
Id=sa;Password=P@ssw0rd;Initial Catalog=AutoLot") { } public InventoryDal(string connectionString)
=> _connectionString = connectionString;

}}

Abrindo e Fechando a Conexão


Em seguida, adicione uma variável de nível de classe para manter uma conexão que será usada pelo código de
acesso a dados. Além disso, adicione dois métodos, um para abrir a conexão (OpenConnection()) e outro para
fechar a conexão (CloseConnection()). No método CloseConnection(), verifique o estado da conexão e, se não estiver
fechada, chame Close() na conexão. A listagem do código segue:

private SqlConnection _sqlConnection = nulo; private void


OpenConnection() {

_sqlConnection = new SqlConnection {

ConnectionString = _connectionString };
_sqlConnection.Open(); }

803
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

private void CloseConnection() {

if (_sqlConnection?.State != ConnectionState.Closed) {

_sqlConnection?.Close(); }

Por uma questão de brevidade, a maioria dos métodos na classe InventoryDal não usará blocos try/catch para lidar com
possíveis exceções, nem lançará exceções personalizadas para relatar vários problemas com a execução (por exemplo, uma
string de conexão malformada). Se você fosse construir uma biblioteca de acesso a dados de força industrial, você certamente
iria querer usar técnicas estruturadas de tratamento de exceções (conforme abordado no Capítulo 7) para contabilizar quaisquer
anomalias de tempo de execução.

Adicionando IDisposable

Adicione a interface IDisposable à definição de classe, assim:

public class InventoryDal : IDisposable {

...
}

Em seguida, implemente o padrão descartável, chamando Dispose no objeto SqlConnection.

bool _disposed = false; void


virtual protegido Dispose(bool disposing) {

if (_disposed) {

retornar;

} if (dispondo) {

_sqlConnection.Dispose(); }
_disposed = verdadeiro; } public
void Dispose() {

Descarte(verdadeiro);
GC.SuppressFinalize(this); }

Adicionando os métodos de seleção

Você começa combinando o que já sabe sobre objetos Command, DataReaders e coleções genéricas para obter os
registros da tabela Inventory. Como você viu anteriormente neste capítulo, o objeto de leitor de dados de um provedor
de dados permite uma seleção de registros usando um mecanismo somente leitura e somente encaminhamento usando
o método Read(). Neste exemplo, a propriedade CommandBehavior no DataReader é definida para fechar
automaticamente a conexão quando o leitor é fechado. O método GetAllInventory() retorna um List<CarViewModel>
para representar todos os dados na tabela Inventory.

804
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

public List<CarViewModel> GetAllInventory() {

OpenConnection(); //
Isso manterá os registros.
List<CarViewModel> inventário = new List<CarViewModel>();

// Prepara o objeto de
comando. string sql =
@"SELECT i.Id, i.Color, i.PetName,m.Name as Make
FROM Inventory i
INNER JOIN Faz m em m.Id = i.MakeId";
usando o comando SqlCommand
= new SqlCommand(sql, _sqlConnection) {

CommandType = CommandType.Text };

command.CommandType = CommandType.Text;
SqlDataReader dataReader =
command.ExecuteReader(CommandBehavior.CloseConnection);
while (leitor de dados.Read()) {

inventário.Add(new CarViewModel {

Id = (int)dataReader["Id"], Color =
(string)dataReader["Color"], Make =
(string)dataReader["Make"], PetName =
(string)dataReader["PetName"] }); }
dataReader.Close(); inventário de devolução; }

O próximo método de seleção obtém um único CarViewModel baseado no CarId.

public CarViewModel GetCar(int id) {

OpenConnection();
CarViewModel carro = null; //
Isso deve usar parâmetros por motivos de segurança string sql
= $@"SELECT i.Id, i.Color, i.PetName,m.Name as Make

FROM Inventory i
INNER JOIN Faz m em m.Id = i.MakeId
WHERE i.Id = {id}"; using SqlCommand
command = new SqlCommand(sql, _sqlConnection) {

CommandType = CommandType.Text };

SqlDataReader dataReader =
command.ExecuteReader(CommandBehavior.CloseConnection);

805
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

while (leitor de dados.Read()) {

carro = novo CarViewModel


{
Id = (int) dataReader["Id"],
Color = (string) dataReader["Color"],
Make = (string) dataReader["Make"],
PetName = (string) dataReader["PetName"] };

} dataReader.Close();
carro de retorno;
}

ÿ Observação Geralmente, é uma prática ruim aceitar a entrada do usuário em instruções SQL brutas, como é feito aqui. Mais

adiante neste capítulo, esse código será atualizado para usar parâmetros.

Inserindo um novo carro Inserir

um novo registro na tabela de inventário é tão simples quanto formatar a instrução SQL Insert (com base na entrada
do usuário), abrir a conexão, chamar o ExecuteNonQuery() usando seu objeto de comando e fechar a conexão. Você
pode ver isso em ação adicionando um método público ao seu tipo InventoryDal chamado InsertAuto() que usa três
parâmetros que mapeiam para as colunas sem identidade da tabela Inventory (Color, Make e PetName). Você usa
esses argumentos para formatar um tipo de string para inserir o novo registro.
Finalmente, use seu objeto SqlConnection para executar a instrução SQL.

public void InsertAuto(string color, int makeId, string petName) { OpenConnection(); //


Formata e executa a instrução SQL. string sql = $"Inserir no inventário (MakeId,
Color, PetName) Valores ('{makeId}',

'{color}', '{petName}')"; // Execute


usando nossa conexão. using
(SqlCommand command = new SqlCommand(sql, _sqlConnection)) {

command.CommandType = CommandType.Text;
comando.ExecuteNonQuery(); }

FecharConexão(); }

Esse método anterior usa três valores para Car e funciona desde que o código de chamada passe os valores na
ordem correta. Um método melhor usa Car para criar um método fortemente tipado, garantindo que todas as
propriedades sejam passadas para o método na ordem correta.

806
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Crie o método InsertCar() de tipo forte


Adicione outro método InsertAuto() que usa Car como parâmetro para sua classe InventoryDal, conforme mostrado aqui:

public void InsertAuto(Carro carro) {

OpenConnection(); //
Formata e executa a instrução SQL. string sql
"
= "Inserir valores no inventário (MakeId, Color, PetName) +
$"('{car.MakeId}', '{car.Color}', '{car.PetName}')";

// Executa usando nossa conexão.


usando (comando SqlCommand = new SqlCommand(sql, _sqlConnection)) {

command.CommandType = CommandType.Text;
comando.ExecuteNonQuery(); }

FecharConexão(); }

Adicionando a lógica de exclusão Excluir um

registro existente é tão simples quanto inserir um novo registro. Ao contrário de quando você criou o código para
InsertAuto(), desta vez você aprenderá sobre um importante escopo try/catch que lida com a possibilidade de
tentar excluir um carro que está atualmente encomendado para um indivíduo na tabela Clientes. As opções padrão
INSERT e UPDATE para chaves estrangeiras impedem a exclusão de registros relacionados em tabelas
vinculadas. Quando isso acontece, um SqlException é lançado. Um programa real lidaria com esse erro de forma
inteligente; no entanto, neste exemplo, você está apenas lançando uma nova exceção. Adicione o seguinte método
ao tipo de classe InventoryDal:

public void DeleteCar(int id) {

OpenConnection(); //
Obtém o ID do carro a ser excluído e, em seguida,
o faz. string sql = $"Excluir do Inventário onde Id = '{id}'"; usando
(comando SqlCommand = new SqlCommand(sql, _sqlConnection)) {

tentar

command.CommandType = CommandType.Text;
comando.ExecuteNonQuery(); } catch (SqlException
ex) {

Exception error = new Exception("Desculpe! Esse carro está no pedido!", ex); lançar erro;

}
}
FecharConexão(); }

807
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Adicionando a lógica de atualização


Quando se trata de atualizar um registro existente na tabela Inventário, a primeira coisa que você deve decidir é o que deseja permitir que o
chamador altere, seja a cor do carro, o nome do animal de estimação, a marca ou todos esses. Uma maneira de dar flexibilidade total ao chamador é
definir um método que usa um tipo de string para representar qualquer tipo de instrução SQL, mas isso é, na melhor das hipóteses, arriscado.

Idealmente, você deseja ter um conjunto de métodos que permitam ao chamador atualizar um registro de várias maneiras.
No entanto, para esta biblioteca simples de acesso a dados, você definirá um único método que permite ao chamador atualizar o nome do animal de
estimação de um determinado automóvel, da seguinte forma:

public void UpdateCarPetName(int id, string newPetName) { OpenConnection(); // Obtém


o ID do carro para modificar o nome do animal de estimação. string sql = $"Atualizar
conjunto de inventário PetName = '{newPetName}' Where Id = '{id}'"; usando (comando
SqlCommand = new SqlCommand(sql, _sqlConnection)) { command.ExecuteNonQuery(); }

FecharConexão(); }

Trabalhando com objetos de comando parametrizados


Atualmente, a lógica de inserção, atualização e exclusão para o tipo InventoryDal usa literais de cadeia de caracteres embutidas em código para
cada consulta SQL. Com consultas parametrizadas, os parâmetros SQL são objetos, em vez de simples bolhas de texto.
Tratar as consultas SQL de maneira mais orientada a objetos ajuda a reduzir o número de erros de digitação (dadas as propriedades fortemente
tipadas); além disso, as consultas parametrizadas geralmente são executadas muito mais rapidamente do que uma string SQL literal porque são
analisadas exatamente uma vez (em vez de cada vez que a string SQL é atribuída à propriedade CommandText). As consultas parametrizadas
também ajudam a proteger contra ataques de injeção de SQL (um conhecido problema de segurança de acesso a dados).

Para oferecer suporte a consultas parametrizadas, os objetos de comando ADO.NET mantêm uma coleção de objetos de parâmetro
individuais. Por padrão, essa coleção está vazia, mas você pode inserir qualquer número de objetos de parâmetro que sejam mapeados para um
parâmetro de espaço reservado na consulta SQL. Quando quiser associar um parâmetro em uma consulta SQL a um membro na coleção de parâmetros
do objeto de comando, você pode prefixar o parâmetro de texto SQL com o símbolo @ (pelo menos ao usar o Microsoft SQL Server; nem todos os
DBMSs suportam essa notação).

Especificando parâmetros usando o tipo DbParameter Antes de criar uma consulta parametrizada,

você precisa se familiarizar com o tipo DbParameter (que é a classe base para o objeto de parâmetro específico de um provedor). Essa classe mantém
várias propriedades que permitem configurar o nome, o tamanho e o tipo de dados do parâmetro, bem como outras características, incluindo a direção
de deslocamento do parâmetro. A Tabela 21-6 descreve algumas propriedades importantes do tipo DbParameter.

808
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Tabela 21-6. Principais membros do tipo DbParameter

Propriedade Significado na vida

DbType Obtém ou define o tipo de dados nativo do parâmetro, representado como um tipo de dados CLR

Direção Obtém ou define se o parâmetro é somente entrada, somente saída, bidirecional ou um parâmetro de
valor de retorno
é anulável Obtém ou define se o parâmetro aceita valores nulos
ParameterName Obtém ou define o nome do DbParameter

Tamanho Obtém ou define o tamanho máximo do parâmetro dos dados em bytes; isso é útil apenas para
dados textuais

Valor Obtém ou define o valor do parâmetro

Agora vamos ver como preencher a coleção de um objeto de comando de objetos compatíveis com DBParameter
retrabalhando os métodos InventoryDal para usar parâmetros.

Atualizar o método GetCar A implementação

original do método GetCar() usava a interpolação de cadeia de caracteres C# ao criar a cadeia de caracteres SQL para
recuperar os dados do carro. Para atualizar este método, crie uma instância de SqlParameter com os valores apropriados,
conforme a seguir:

SqlParameter parâmetro = new SqlParameter {

ParameterName = "@carId", Value


= id, SqlDbType = SqlDbType.Int,
Direção = ParameterDirection.Input };

O valor ParameterName deve corresponder ao nome usado na consulta SQL (você atualizará isso a seguir), o
o tipo deve corresponder ao tipo de coluna do banco de dados e a direção depende se o parâmetro é usado para enviar
dados para a consulta (ParameterDirection.Input) ou se deve retornar dados da consulta (ParameterDirection.Output). Os
parâmetros também podem ser definidos como entrada/saída ou como valores de retorno (por exemplo, de um procedimento
armazenado).
Em seguida, atualize a string SQL para usar o nome do parâmetro ("@carId") em vez da construção de interpolação de string
C# ("{id}").

string sql =
@"SELECT i.Id, i.Color, i.PetName,m.Name as Make
FROM Inventory i
INNER JOIN Faz m em m.Id = i.MakeId WHERE i.Id
= @CarId";

A atualização final é adicionar o novo parâmetro à coleção Parâmetros do objeto de comando.

comando.Parameters.Add(param);

809
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Atualize o método DeleteCar


Da mesma forma, a implementação original do método DeleteCar() usava interpolação de string C#. Para atualizar este
método, crie uma instância de SqlParameter com os valores apropriados, conforme a seguir:

SqlParameter param = new SqlParameter


{ ParameterName = "@carId", Value = id, SqlDbType
= SqlDbType.Int, Direction =
ParameterDirection.Input };

Em seguida, atualize a string SQL para usar o nome do parâmetro ("@carId").

string sql = "Excluir do Inventário onde Id = @carId";

A atualização final é adicionar o novo parâmetro à coleção Parâmetros do objeto de comando.

comando.Parameters.Add(param);

Atualize o método UpdateCarPetName


Este método requer dois parâmetros, um para o ID do carro e outro para o novo PetName. O primeiro parâmetro é
criado exatamente como os dois exemplos anteriores (com exceção de um nome de variável diferente) e o segundo cria um
parâmetro que mapeia para o tipo de banco de dados NVarChar (o tipo de campo PetName da tabela Inventory). Observe
que um valor de tamanho é definido. É importante que este tamanho corresponda ao tamanho do campo do seu banco de
dados para não criar problemas ao executar o comando.

SqlParameter paramId = new SqlParameter


{ ParameterName = "@carId", Value = id, SqlDbType
= SqlDbType.Int, Direction = ParameterDirection.Input };
SqlParameter paramName = novo SqlParameter
{ ParameterName = "@petName", Value =
newPetName, SqlDbType = SqlDbType.NVarChar,
Tamanho = 50, Direção = ParameterDirection.Input };

Em seguida, atualize a string SQL para usar os parâmetros.

string sql = $"Atualizar conjunto de inventário PetName = @petName Where Id = @carId";

810
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

A atualização final é adicionar os novos parâmetros à coleção Parâmetros do objeto de comando.

command.Parameters.Add(paramId);
command.Parameters.Add(paramName);

Atualize o método InsertAuto


Adicione a seguinte versão do método InsertAuto() para aproveitar os objetos de parâmetro:

public void InsertAuto(Carro carro) {

OpenConnection(); //
Observe os "espaços reservados" na consulta SQL. string sql =
"Inserir no inventário" + "Valores (MakeId, Color, PetName)" +
"(@MakeId, @Color, @PetName)";

// Este comando terá parâmetros internos. usando (comando


SqlCommand = new SqlCommand(sql, _sqlConnection)) {

// Preenche a coleção de parâmetros.


Parâmetro SqlParameter = new SqlParameter {

ParameterName = "@MakeId", Value


= car.MakeId, SqlDbType =
SqlDbType.Int, Direction =
ParameterDirection.Input };
comando.Parameters.Add(parâmetro);

parâmetro = novo SqlParameter {

ParameterName = "@Color", Value


= car.Color, SqlDbType = SqlDbType.
NVarChar, Size = 50, Direction =
ParameterDirection.Input };
comando.Parameters.Add(parâmetro);

parâmetro = novo SqlParameter {

ParameterName = "@PetName", Value


= car.PetName, SqlDbType =
SqlDbType. NVarChar, Size = 50, Direction =
ParameterDirection.Input };
comando.Parameters.Add(parâmetro);

811
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

comando.ExecuteNonQuery();
FecharConexão(); }

Embora a construção de uma consulta parametrizada geralmente exija mais código, o resultado final é uma maneira mais
conveniente de ajustar as instruções SQL programaticamente, bem como de obter melhor desempenho geral. Eles também são
extremamente úteis quando você deseja acionar um procedimento armazenado.

Executando um procedimento armazenado Lembre-se de que

um procedimento armazenado é um bloco nomeado de código SQL armazenado no banco de dados. Você pode construir procedimentos
armazenados para que eles retornem um conjunto de linhas ou tipos de dados escalares ou façam qualquer outra coisa que faça sentido (por
exemplo, inserir, atualizar ou excluir registros); você também pode fazer com que eles usem qualquer número de parâmetros opcionais. O resultado
final é uma unidade de trabalho que se comporta como um método típico, exceto pelo fato de estar localizada em um armazenamento de dados
em vez de um objeto de negócios binário. Atualmente, seu banco de dados AutoLot define um único procedimento armazenado chamado GetPetName.
Agora considere o seguinte método final (por enquanto) do tipo InventoryDal, que chama seu procedimento armazenado:

public string LookUpPetName(int carId) {

OpenConnection();
string carPetName;

// Estabelece o nome do proc armazenado.


usando (comando SqlCommand = new SqlCommand("GetPetName", _sqlConnection)) {

command.CommandType = CommandType.StoredProcedure;

// Parâmetro de entrada.
SqlParameter parâmetro = new SqlParameter {

ParameterName = "@carId",
SqlDbType = SqlDbType.Int, Value
= carId, Direção =
ParameterDirection.Input };
comando.Parameters.Add(param);

// Parâmetro de saída.
parâmetro = novo SqlParameter {

ParameterName = "@petName",
SqlDbType = SqlDbType.NVarChar,
Tamanho = 50, Direção =
ParameterDirection.Output };
comando.Parameters.Add(param);

// Executa o proc armazenado.


comando.ExecuteNonQuery();

812
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

// Retorna o parâmetro de saída.


carPetName = (string)command.Parameters["@petName"].Value; FecharConexão(); }
return carPetName;

Um aspecto importante da chamada de um procedimento armazenado é ter em mente que um objeto de comando pode
representar uma instrução SQL (o padrão) ou o nome de um procedimento armazenado. Quando você deseja informar a um objeto
de comando que ele estará invocando um procedimento armazenado, você passa o nome do procedimento (como um argumento
do construtor ou usando a propriedade CommandText) e deve definir a propriedade CommandType com o valor
CommandType.StoredProcedure. (Se você não fizer isso, receberá uma exceção de tempo de execução porque o objeto de comando
está esperando uma instrução SQL por padrão.)
Em seguida, observe que a propriedade Direction do parâmetro @petName está definida como ParameterDirection.
Saída. Como antes, você adiciona cada objeto de parâmetro à coleção de parâmetros do objeto de comando.
Depois que o procedimento armazenado for concluído com uma chamada para ExecuteNonQuery(), você poderá obter o valor do
parâmetro de saída investigando a coleção de parâmetros do objeto de comando e lançando de acordo.

// Retorna o parâmetro de saída.


carPetName = (string)command.Parameters["@petName"].Value;

Neste ponto, você tem uma biblioteca de acesso a dados extremamente simples que pode ser usada para construir um
cliente para exibir e editar seus dados. Você ainda não examinou como criar interfaces gráficas com o usuário, portanto, a seguir, testará
sua biblioteca de dados a partir de um novo aplicativo de console.

Criando um aplicativo cliente baseado em console


Adicione um novo aplicativo de console (chamado AutoLot.Client) à solução AutoLot.Dal e adicione uma referência ao projeto
AutoLot.Dal. Os comandos dotnet CLI para realizar isso são os seguintes (supondo que sua solução seja denominada
Chapter21_AllProjects.sln):

dotnet novo console -lang c# -n AutoLot.Client -o .\AutoLot.Client -f net5.0 dotnet sln .\Chapter21_AllProjects.sln
adicionar .\AutoLot.Client dotnet adicionar pacote AutoLot.Client Microsoft.Data.SqlClient dotnet adicionar
AutoLot .Referência do cliente AutoLot.Dal

Se estiver usando o Visual Studio, clique com o botão direito do mouse em sua solução e selecione Add ÿ New Project. Defina o
novo projeto como o projeto de inicialização (clicando com o botão direito do mouse no projeto no Solution Explorer e selecionando Set
as StartUp Project). Isso executará seu novo projeto ao depurar no Visual Studio. Se você estiver usando o Visual Studio Code, precisará
navegar até o diretório AutoLot.Test e executar o projeto (quando chegar a hora) usando dotnet run.
Limpe o código gerado em Program.cs e adicione as seguintes instruções using ao topo de Program.cs:

usando Sistema;
usando System.Linq;
usando AutoLot.Dal;
usando AutoLot.Dal.Models; usando
AutoLot.Dal.DataOperations; usando
System.Collections.Generic;

813
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Substitua o método Main() pelo seguinte código para exercitar AutoLot.Dal:

InventoryDal dal = new InventoryDal();


List<CarViewModel> list = dal.GetAllInventory();
Console.WriteLine(" ************** Todos
");os Carros **************
Console.WriteLine("Id\tMake\tColor\tPet Name"); foreach (var itm
na lista) {

Console.WriteLine($"{itm.Id}\t{itm.Make}\t{itm.Color}\t{itm.PetName}"); }

Console.WriteLine();
CarViewModel car = dal.GetCar(list.OrderBy(x=>x.Color).Select(x => x.Id).First()); Console.WriteLine("
************** Primeiro carro por cor ************** "); Console.WriteLine("CarId\tMake\tColor\tPet Name");
Console.WriteLine($"{carro.Id}\t{carro.Make}\t{carro.Color}
\t{carro.PetName}");

tentar

{
//Isto falhará devido a dados relacionados na tabela Orders dal.DeleteCar(5);
Console.WriteLine("Carro deletado."); } catch (Exceção ex) {

Console.WriteLine($"Ocorreu uma exceção: {ex.Message}"); }


dal.InsertAuto(new Car { Color = "Blue", MakeId = 5, PetName =
"TowMonster" }); lista = dal.GetAllInventory(); var newCar = list.First(x => x.PetName == "TowMonster");
Console.WriteLine(" ************** ************** "); Console.WriteLine("CarId\tMake\tColor\tPet Name");
Console.WriteLine($"{newCar.Id}\t{newCar.Make}\t{newCar.Color}\t{newCar.PetName}");
dal.DeleteCar(newCar.Id); var petName Carro novo ************** "); New Car
= dal.LookUpPetName(car.Id); Console.WriteLine(" **************
Console.WriteLine($"Nome
do carro: {petName}"); Console.Write("Pressione enter para continuar..."); Console.ReadLine();

Compreendendo as transações do banco de dados


Vamos encerrar este exame do ADO.NET dando uma olhada no conceito de uma transação de banco de dados.
Simplificando, uma transação é um conjunto de operações de banco de dados que são bem-sucedidas ou falham como uma unidade coletiva.
Se uma das operações falhar, todas as outras operações serão revertidas, como se nada tivesse acontecido. Como você pode imaginar, as
transações são muito importantes para garantir que os dados da tabela sejam seguros, válidos e consistentes.
As transações são importantes quando uma operação de banco de dados envolve a interação com várias
tabelas ou vários procedimentos armazenados (ou uma combinação de átomos do banco de dados). O exemplo
clássico de transação envolve o processo de transferência de fundos monetários entre duas contas bancárias. Por
exemplo, se você transferir $ 500 de sua conta poupança para sua conta corrente, as seguintes etapas devem ocorrer
de maneira transacional:

814
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

1. O banco deve retirar $ 500 de sua conta poupança.

2. O banco deve adicionar $ 500 à sua conta corrente.

Seria uma coisa extremamente ruim se o dinheiro fosse retirado da conta poupança, mas não transferido para a
conta corrente (devido a algum erro por parte do banco), porque então você perderia $ 500! No entanto, se essas etapas
forem agrupadas em uma transação de banco de dados, o DBMS garantirá que todas as etapas relacionadas ocorram como
uma única unidade. Se qualquer parte da transação falhar, toda a operação será revertida para o estado original. Por outro
lado, se todas as etapas forem bem-sucedidas, a transação será confirmada.

ÿ Observação Você pode estar familiarizado com o acrônimo ACID por consultar a literatura transacional. Isso representa
as quatro propriedades principais de uma transação primária e adequada: atômica (tudo ou nada), consistente (os dados
permanecem estáveis durante toda a transação), isolada (as transações não interferem em outras operações) e durável
(as transações são salvas e logado).

Acontece que a plataforma .NET Core oferece suporte a transações de várias maneiras. Este capítulo vai olhar
no objeto de transação do seu provedor de dados ADO.NET (SqlTransaction, no caso de Microsoft.Data.
SqlClient).
Além do suporte transacional embutido nas bibliotecas de classe base .NET, é possível usar a linguagem SQL do
seu sistema de gerenciamento de banco de dados. Por exemplo, você pode criar um procedimento armazenado que usa
as instruções BEGIN TRANSACTION, ROLLBACK e COMMIT.

Principais membros de um objeto de transação ADO.NET Todas as transações que

usaremos implementam a interface IDbTransaction. Lembre-se do início deste capítulo que IDbTransaction define um punhado
de membros da seguinte forma:

interface pública IDbTransaction: IDisposable {

Conexão IDbConnection { obter; }


IsolationLevel IsolationLevel { get; }

void Commit();
void Rollback(); }

Observe a propriedade Connection, que retorna uma referência ao objeto de conexão que iniciou a transação atual
(como você verá, você obtém um objeto de transação de um determinado objeto de conexão). Você chama o método
Commit() quando cada uma de suas operações de banco de dados for bem-sucedida. Isso faz com que cada uma das
alterações pendentes seja mantida no armazenamento de dados. Por outro lado, você pode chamar o método Rollback() no
caso de uma exceção de tempo de execução, que informa ao DBMS para desconsiderar quaisquer alterações pendentes,
deixando os dados originais intactos.

ÿ Observação A propriedade IsolationLevel de um objeto de transação permite especificar com que intensidade uma
transação deve ser protegida contra as atividades de outras transações paralelas. Por padrão, as transações são
completamente isoladas até serem confirmadas.

815
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Além dos membros definidos pela interface IDbTransaction, o tipo SqlTransaction define um membro adicional
chamado Save(), que permite definir pontos de salvamento. Esse conceito permite reverter uma transação com falha
até um ponto nomeado, em vez de reverter toda a transação. Essencialmente, quando você chama Save() usando um
objeto SqlTransaction, pode especificar um moniker de string amigável. Ao chamar Rollback(), você pode especificar
esse mesmo moniker como um argumento para executar uma reversão parcial efetiva.
Chamar Rollback() sem argumentos faz com que todas as alterações pendentes sejam revertidas.

Adicionando um método de transação ao InventoryDal


Agora vamos ver como você trabalha com transações ADO.NET programaticamente. Comece abrindo o
projeto de biblioteca de código AutoLot.Dal que você criou anteriormente e adicione um novo método
público chamado ProcessCreditRisk() à classe InventoryDal para lidar com os riscos de crédito percebidos. O método
irá procurar um cliente, adicioná-lo à tabela CreditRisks e, em seguida, atualizar seu sobrenome adicionando “(Risco de
Crédito)” ao final.

public void ProcessCreditRisk(bool throwEx, int customerId) {

OpenConnection(); //
Primeiro, procure o nome atual com base no ID do cliente. string
fNome; string lNome; var cmdSelect = new SqlCommand( "Selecione
* de Clientes onde Id = @customerId", _sqlConnection); SqlParameter
paramId = new SqlParameter {

ParameterName = "@customerId",
SqlDbType = SqlDbType.Int, Value =
customerId, Direction =
ParameterDirection.Input };
cmdSelect.Parameters.Add(paramId); usando
(var dataReader = cmdSelect.ExecuteReader()) {

if (leitor de dados.HasRows) {

dataReader.Read();
fNome = (string) dataReader["Nome"]; lNome =
(string) dataReader["LastName"]; } outro {

FecharConexão();
retornar;
}

} cmdSelect.Parameters.Clear(); //
Cria objetos de comando que representam cada etapa da operação. var cmdUpdate =
new SqlCommand( "Update Customers set LastName = LastName + ' (CreditRisk) '
where Id = @customerId", _sqlConnection);

816
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

cmdUpdate.Parameters.Add(paramId); var
cmdInsert = new SqlCommand( "Inserir em
CreditRisks (CustomerId,FirstName, LastName) Values( @CustomerId, @ FirstName, @LastName)",
_sqlConnection); SqlParameter parameterId2 = new SqlParameter {

ParameterName = "@CustomerId",
SqlDbType = SqlDbType.Int, Value =
customerId, Direction =
ParameterDirection.Input }; SqlParameter
parameterFirstName = new SqlParameter {

ParameterName = "@FirstName",
Valor = fName, SqlDbType =
SqlDbType.NVarChar, Tamanho = 50,
Direção = ParameterDirection.Input };
SqlParameter parameterLastName = new
SqlParameter {

ParameterName = "@LastName",
Valor = lName, SqlDbType =
SqlDbType.NVarChar, Tamanho = 50,
Direção = ParameterDirection.Input };

cmdInsert.Parameters.Add(parameterId2);
cmdInsert.Parameters.Add(parameterFirstName);
cmdInsert.Parameters.Add(parameterLastName); //
Obteremos isso do objeto de conexão.
SqlTransaction tx = nulo; tentar {

tx = _sqlConnection.BeginTransaction(); // Alista os
comandos nesta transação. cmdInsert.Transaction = tx;
cmdUpdate.Transaction = tx; // Executa os comandos.
cmdInsert.ExecuteNonQuery();
cmdUpdate.ExecuteNonQuery(); // Simula erro. if (jogarEx)
{

throw new Exception("Desculpe! Erro no banco de dados! Tx falhou..."); }

// Comprometa-
se! tx.Commit(); }

817
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

catch (Exceção ex) {

Console.WriteLine(ex.Message); //
Qualquer erro reverterá a transação. Usando o novo operador de acesso condicional para
verifique se é nulo.
tx?.Rollback(); }
finalmente {

FecharConexão(); }

Aqui, você usa um parâmetro bool de entrada para representar se lançará uma exceção arbitrária ao tentar
processar o cliente ofensivo. Isso permite simular uma circunstância imprevista que fará com que a transação do
banco de dados falhe. Obviamente, você faz isso aqui apenas para fins ilustrativos; um verdadeiro método de
transação de banco de dados não permitiria que o chamador forçasse a lógica a falhar por capricho!
Observe que você usa dois objetos SqlCommand para representar cada etapa da transação que iniciará.
Depois de obter o nome e o sobrenome do cliente com base no parâmetro customerID de entrada, você pode
obter um objeto SqlTransaction válido do objeto de conexão usando BeginTransaction(). Em seguida, e mais
importante, você deve inscrever cada objeto de comando atribuindo a propriedade Transaction ao objeto de
transação que acabou de obter. Se você não fizer isso, a lógica Inserir/Atualizar não estará em um contexto
transacional.
Depois de chamar ExecuteNonQuery() em cada comando, você lança uma exceção se (e somente se) o valor
do parâmetro bool é verdadeiro. Nesse caso, todas as operações de banco de dados pendentes são revertidas. Se você não
lançar uma exceção, ambas as etapas serão confirmadas nas tabelas do banco de dados assim que você chamar Commit().

Testando sua transação de banco de dados Selecione um


dos clientes que você adicionou à tabela Clientes (por exemplo, Dave Benner, Id = 1). Em seguida, adicione um
novo método a Program.cs no projeto AutoLot.Client chamado FlagCustomer().

void FlagCliente() {

Console.WriteLine("***** Exemplo de Transação Simples *****\n");

// Uma maneira simples de permitir que o tx seja bem-sucedido


ou não. bool lanceEx = verdadeiro; Console.Write("Deseja
lançar uma exceção (S ou N): "); var userAnswer = Console.ReadLine(); if
(string.IsNullOrEmpty(userAnswer) || userAnswer.Equals("N",StringComparison.

OrdinalIgnoreCase)) {

lanceEx = falso;

} var dal = new InventoryDal(); //


Processar cliente 1 – insira o id para o cliente mover.
dal.ProcessCreditRisk(throwEx, 1); Console.WriteLine("Verifique a tabela
CreditRisk para obter os resultados"); Console.ReadLine(); }

818
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Se você executasse seu programa e decidisse lançar uma exceção, descobriria que o último
nome não é alterado na tabela Clientes porque toda a transação foi revertida. No entanto, se você não lançar uma
exceção, descobrirá que o sobrenome do cliente foi atualizado na tabela Clientes e adicionado à tabela CreditRisks.

Executando cópias em massa com ADO.NET


Nos casos em que você precisa carregar muitos registros no banco de dados, os métodos mostrados até agora seriam
bastante ineficientes. SQL Server tem um recurso chamado cópia em massa que é projetado especificamente para este
cenário e é agrupado em ADO.NET com a classe SqlBulkCopy. Esta seção do capítulo mostra como fazer isso com o
ADO.NET.

Explorando a classe SqlBulkCopy A classe SqlBulkCopy

tem um método, WriteToServer() (e a versão assíncrona WriteToServerAsync()), que processa uma lista de registros e
grava os dados no banco de dados com mais eficiência do que escrever uma série de instruções de inserção e executá-
las com um objeto Comando. As sobrecargas de WriteToServer usam um DataTable, um DataReader ou uma matriz de
DataRows. Para manter o tema deste capítulo, você usará a versão DataReader. Para isso, você precisa criar um leitor de
dados personalizado.

Criando um leitor de dados personalizado Você deseja

que seu leitor de dados personalizado seja genérico e mantenha uma lista dos modelos que deseja importar. Comece
criando uma nova pasta no projeto AutoLot.Dal chamada BulkImport; na pasta, crie uma nova classe de interface chamada
IMyDataReader.cs que implementa IDataReader e atualize o código para o seguinte:

usando System.Collections.Generic; usando


System.Data;

namespace AutoLot.Dal.BulkImport {

interface pública IMyDataReader<T> : IDataReader {

List<T> Registros { get; definir; } }

Em seguida, vem a tarefa de implementar o leitor de dados personalizado. Como você já viu, os leitores de dados
tem muitas partes móveis. A boa notícia para você é que, para SqlBulkCopy, você só deve implementar um
punhado deles. Crie uma nova classe chamada MyDataReader.cs e adicione as seguintes instruções using:

usando Sistema;
usando System.Collections.Generic; usando
System.Data; usando System.Linq; usando
System.Reflection;

819
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Em seguida, atualize a classe para pública e selada e implemente IMyDataReader. Adicione um construtor para receber
os registros e definir a propriedade.

classe pública selada MyDataReader<T> : IMyDataReader<T> {

Lista pública<T> Registros { get; definir; } public


MyDataReader(List<T> registros) {

Registros = registros;
}
}

Faça com que o Visual Studio ou o Visual Studio Code implementem todos os métodos para você (ou copie-os do
a seguir) e você terá seu ponto de partida para o leitor de dados personalizado. A Tabela 21-7 detalha os únicos
métodos que precisam ser implementados para este cenário.

Tabela 21-7. Métodos-chave de IDataReader para SqlBulkCopy

Método Significado na vida


Ler Obtém o próximo registro; retorna verdadeiro se houver outro registro ou retorna falso se estiver no
final da lista

FieldCount Obtém o número total de campos na fonte de dados

Obter valor Obtém o valor de um campo com base na posição ordinal

GetSchemaTable Obtém as informações do esquema para a tabela de destino

Começando com o método Read(), retorne false se o leitor estiver no final da lista e retorne true (e incremente um
contador em nível de classe) se o leitor não estiver no final da lista. Adicione uma variável de nível de classe para manter o
índice atual de List<T> e atualize o método Read() assim:

public class MyDataReader<T> : IMyDataReader<T> {

...
private int _currentIndex = -1; public bool
Read() { if (_currentIndex + 1 >=
Records.Count) {

retorna falso;

} _currentIndex++;
retornar verdadeiro;
}
}

Cada um dos métodos get e FieldCount requer um conhecimento íntimo do


modelo a ser carregado. Um exemplo do método GetValue() (usando o CarViewModel) é o seguinte:

820
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

objeto público GetValue(int i) {

Carro currentRecord = Registros[_currentIndex] como Carro; retornar


e trocar {

0 => currentRecord.Id, 1 =>


currentRecord.MakeId, 2 =>
currentRecord.Color, 3 =>
currentRecord.PetName, 4 =>
currentRecord.TimeStamp, =>
_ string.Empty,
};
}

O banco de dados tem apenas quatro tabelas, mas isso significa que você ainda tem quatro variações do leitor de dados.
Imagine se você tivesse um banco de dados de produção real com muito mais tabelas! Você pode fazer melhor do que
isso usando reflexão (abordado no Capítulo 17) e LINQ to Objects (abordado no Capítulo 13).
Adicione variáveis somente leitura para manter os valores PropertyInfo para o modelo, bem como um dicionário que irá
ser usado para manter a posição do campo e o nome da tabela no SQL Server. Atualize o construtor para obter as
propriedades do tipo genérico e inicialize o Dictionary. O código adicionado é o seguinte:

private readonly PropertyInfo[] _propertyInfos; private


readonly Dictionary<int, string> _nameDictionary;

public MyDataReader(List<T> registros) {

Registros = registros;
_propertyInfos = typeof(T).GetProperties();
_nameDictionary = new Dictionary<int,string>(); }

Em seguida, atualize o construtor para obter um SQLConnection, bem como strings para o esquema e os nomes das tabelas
para a tabela na qual os registros serão inseridos e adicione variáveis de nível de classe para os valores.

private readonly SqlConnection _connection; string


privada somente leitura _schema; string privada
somente leitura _tableName; public MyDataReader(List<T>
records, SqlConnection connection, string schema, string tableName) {

Registros = registros;
_propertyInfos = typeof(T).GetProperties(); _nameDictionary
= new Dictionary<int, string>();

_conexão = conexão; _esquema


= esquema; _tableName =
tableName; }

821
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

Implemente o método GetSchemaTable() a seguir. Isso recupera as informações do SQL Server sobre a tabela
de destino.

public DataTable GetSchemaTable() {

using var schemaCommand = new SqlCommand($"SELECT * FROM {_schema}.{_tableName}", _ connection);


using var reader = schemaCommand.ExecuteReader(CommandBehavior.SchemaOnly); return
leitor.GetSchemaTable(); }

Atualize o construtor para usar o SchemaTable para construir o dicionário que contém os campos da tabela de
destino na ordem do banco de dados.

public MyDataReader(List<T> records, SqlConnection connection, string schema, string tableName) {

...
DataTable schemaTable = GetSchemaTable(); for
(int x = 0; x<schemaTable?.Rows.Count;x++) { DataRow
col = schemaTable.Rows[x]; var nomeColuna =
col.Field<string>("NomeColuna");
_nameDictionary.Add(x,columnName); } }

Agora, os seguintes métodos podem ser implementados genericamente, usando as informações refletidas:

public int FieldCount => _propertyInfos.Length; objeto público


GetValue(int i) =>

_propertyInfos .First(x=>x.Name.Equals(_nameDictionary[i],StringComparison.OrdinalIgnoreCase))
.GetValue(Registros[_currentIndex]);

O restante dos métodos que devem estar presentes (mas não implementados) são listados aqui para referência:

public string GetName(int i) => lançar nova NotImplementedException(); public int


GetOrdinal(string name) => lançar nova NotImplementedException(); public string
GetDataTypeName(int i) => lançar novo NotImplementedException(); public Type GetFieldType(int
i) => lançar novo NotImplementedException(); public int GetValues(object[] values) => lançar novo
NotImplementedException(); public bool GetBoolean(int i) => lançar novo NotImplementedException();
public byte GetByte(int i) => lançar novo NotImplementedException(); public long GetBytes(int i, long
fieldOffset, byte[] buffer, int bufferoffset, int length) => lança o novo NotImplementedException();

public char GetChar(int i) => lançar novo NotImplementedException(); public long


GetChars(int i, long fieldoffset, char[] buffer, int bufferoffset, int length) => lança o novo NotImplementedException();
public Guid GetGuid(int i) => lançar novo NotImplementedException(); public short GetInt16(int i) => lançar novo
NotImplementedException(); public int GetInt32(int i) => lançar novo NotImplementedException(); public long
GetInt64(int i) => lançar novo NotImplementedException();

822
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

public float GetFloat(int i) => lançar novo NotImplementedException(); public


double GetDouble(int i) => lançar novo NotImplementedException(); public string
GetString(int i) => lançar nova NotImplementedException(); public decimal
GetDecimal(int i) => lançar novo NotImplementedException(); public DateTime
GetDateTime(int i) => lançar novo NotImplementedException(); public IDataReader
GetData(int i) => lançar novo NotImplementedException(); public bool IsDBNull(int i) =>
lançar novo NotImplementedException(); objeto IDataRecord.this[int i] => lançar novo
NotImplementedException(); object IDataRecord.this[string name] => lançar nova
NotImplementedException(); public void Fechar() => lançar nova NotImplementedException();
public DataTable GetSchemaTable() => lançar nova NotImplementedException(); public
bool NextResult() => lançar novo NotImplementedException(); public int Profundidade
{ obter; } public bool IsClosed { get; } public int RecordsAffected { get; }

Executando a cópia em massa


Adicione uma nova classe estática pública chamada ProcessBulkImport.cs à pasta BulkImport. Adicione as
seguintes instruções using ao topo do arquivo:

usando
Sistema; usando
System.Collections.Generic; usando
System.Data; usando System.Linq;
usando Microsoft.Data.SqlClient;

Adicione o código para manipular conexões de abertura e fechamento (como o código na classe InventoryDal), da seguinte
maneira:

private const string ConnectionString = @"Data


Source=.,5433;User Id=sa;Password=P@ssw0rd;Initial Catalog=AutoLot"; private static
SqlConnection _sqlConnection = null;

private static void OpenConnection() {

_sqlConnection = new SqlConnection {

ConnectionString = ConnectionString };
_sqlConnection.Open(); }

private static void CloseConnection() {

if (_sqlConnection?.State != ConnectionState.Closed) {

_sqlConnection?.Close(); }

823
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

A classe SqlBulkCopy requer o nome (e esquema, se diferente de dbo) para processar o


registros. Após criar uma nova instância de SqlBulkCopy (passando no objeto de conexão), defina a
propriedade DestinationTableName. Em seguida, crie uma nova instância do leitor de dados personalizado que contém
a lista a ser copiada em massa e chame WriteToServer(). O método ExecuteBulkImport é mostrado aqui:

public static void ExecuteBulkImport<T>(registros IEnumerable<T>, string tableName) {

OpenConnection();
usando SqlConnection conn = _sqlConnection;
SqlBulkCopy bc = new SqlBulkCopy(conn) {

DestinationTableName = tableName }; var


dataReader = new
MyDataReader<T>(records.ToList(),_sqlConnection, "dbo",tableName); {
tentar

bc.WriteToServer(leitor de dados); }
catch (Exceção ex) {

//Deveria fazer algo aqui }


finalmente {

FecharConexão(); }

Testando a cópia em massa no projeto

AutoLot.Client, adicione as seguintes instruções using a Program.cs:

usando AutoLot.Dal.BulkImport;
usando SystemCollections.Generic;

Adicione um novo método a Program.cs chamado DoBulkCopy(). Crie uma lista de objetos Car e passe-a (e o nome
da tabela) para o método ExecuteBulkImport(). O restante do código exibe os resultados da cópia em massa.

void DoBulkCopy() {

Console.WriteLine(" ************** Fazer cópia em massa ************** ");


var carros = new Lista<Carro> {

new Car() {Color = "Blue", MakeId = 1, PetName = "MyCar1"}, new Car() {Color
= "Red", MakeId = 2, PetName = "MyCar2"}, new Car() {Color = "White",
MakeId = 3, PetName = "MyCar3"}, new Car() {Color = "Yellow", MakeId = 4,
PetName = "MyCar4"} }; ProcessBulkImport.ExecuteBulkImport(carros,
"Inventário"); InventoryDal dal = new InventoryDal();

824
Machine Translated by Google

Capítulo 21 ÿ Acesso a dados com ADO.NET

List<CarViewModel> list = dal.GetAllInventory(); Console.WriteLine("


************** Todos os Carros ************** ");
Console.WriteLine("CarId\tMake\tColor\tPet Name"); foreach (var itm
na lista) {

Console.WriteLine(
$"{itm.Id}\t{itm.Make}\t{itm.Color}\t{itm.PetName}");
}
Console.WriteLine(); }

Embora adicionar quatro novos carros não mostre os méritos do trabalho envolvido no uso da classe SqlBulkCopy,
imagine tentar carregar milhares de registros. Fiz isso com clientes e o tempo de carregamento foi de apenas alguns segundos,
enquanto o loop de cada registro levava horas! Como tudo no .NET Core, essa é apenas mais uma ferramenta para manter em
sua caixa de ferramentas para usar quando fizer mais sentido.

Resumo
ADO.NET é a tecnologia nativa de acesso a dados da plataforma .NET Core. Neste capítulo, você começou aprendendo a
função dos provedores de dados, que são essencialmente implementações concretas de várias classes base abstratas (no
namespace System.Data.Common) e tipos de interface (no namespace System.Data). Você também viu que é possível criar uma
base de código neutra em relação ao provedor usando o modelo de fábrica de provedores de dados ADO.NET.

Você também aprendeu que usa objetos de conexão, objetos de transação, objetos de comando e objetos de leitor de
dados para selecionar, atualizar, inserir e excluir registros. Além disso, lembre-se de que os objetos de comando suportam
uma coleção de parâmetros internos, que você pode usar para adicionar algum tipo de segurança às suas consultas SQL;
eles também são bastante úteis ao acionar procedimentos armazenados.
Em seguida, você aprendeu como proteger seu código de manipulação de dados com transações e encerrou o capítulo com
uma olhada no uso da classe SqlBulkCopy para carregar grandes quantidades de dados no SQL Server usando ADO.
LÍQUIDO.

825
Machine Translated by Google

PARTE VII

Entity Framework Núcleo


Machine Translated by Google

CAPÍTULO 22

Apresentando o Entity Framework Core

O capítulo anterior examinou os fundamentos do ADO.NET. O ADO.NET permitiu que os programadores .NET trabalhassem com
dados relacionais (de maneira relativamente direta) desde o lançamento inicial da plataforma .NET. Com base no ADO.NET, a
Microsoft introduziu um novo componente da API do ADO.NET chamado Entity Framework (ou simplesmente, EF) no .NET 3.5
Service Pack 1.
O objetivo geral do EF é permitir que você interaja com dados de bancos de dados relacionais usando um objeto
modelo que mapeia diretamente para os objetos de negócios (ou objetos de domínio) em seu aplicativo. Por exemplo, em vez
de tratar um lote de dados como uma coleção de linhas e colunas, você pode operar em uma coleção de objetos fortemente tipados
denominados entidades. Essas entidades são mantidas em classes de coleção especializadas com reconhecimento de LINQ,
permitindo operações de acesso a dados usando código C#. As classes de coleção fornecem consultas no armazenamento de dados
usando a mesma gramática LINQ que você aprendeu no Capítulo 13.
Assim como a estrutura .NET Core, o Entity Framework Core é uma reescrita completa do Entity Framework 6.
Ele é construído sobre a estrutura .NET Core, permitindo que o EF Core seja executado em várias plataformas. Reescrever o EF
Core permitiu que a equipe adicionasse novos recursos e melhorias de desempenho ao EF Core que não poderiam ser razoavelmente
implementados no EF 6.
Recriar uma estrutura inteira do zero requer uma análise detalhada de quais recursos serão suportados
na nova estrutura e quais recursos serão deixados para trás. Uma das características do EF 6 que não está no EF
O núcleo (e provavelmente nunca será adicionado) é o suporte para o Entity Designer. O EF Core oferece suporte apenas ao primeiro
paradigma de desenvolvimento de código. Se você estiver usando o código primeiro, pode ignorar com segurança a frase anterior.

ÿ Observação O EF Core pode ser usado com bancos de dados existentes, bem como bancos de dados em branco e/ou novos.

Ambos os mecanismos são chamados de code first, que provavelmente não é o melhor nome. As classes de entidade e o

DbContext derivado podem ser montados a partir de um banco de dados existente, e os bancos de dados podem ser criados e atualizados a

partir de classes de entidade. Você aprenderá ambas as abordagens nos capítulos do EF Core.

Com cada versão, o EF Core adicionou mais recursos que existiam no EF 6, bem como novos recursos que nunca existiram
no EF 6. A versão 3.1 encurtou significativamente a lista de recursos essenciais que estão faltando no EF Core (em comparação
com o EF 6) , e 5.0 diminuiu ainda mais a diferença. Na verdade, para a maioria dos projetos, o EF Core tem tudo o que você precisa.

Este capítulo e o próximo apresentarão o acesso a dados usando o Entity Framework Core. Você aprenderá sobre como
criar um modelo de domínio, mapear classes e propriedades de entidade para as tabelas e colunas do banco de dados,
implementar o controle de alterações, usar a interface de linha de comando (CLI) do EF Core para scaffolding e migrações e a
função da classe DbContext. Você também aprenderá como relacionar entidades com propriedades de navegação, transações e
verificação de simultaneidade, apenas para citar alguns dos recursos explorados.
Ao concluir estes capítulos, você terá a versão final da camada de acesso a dados para nosso
Banco de dados AutoLot. Antes de entrarmos no EF Core, vamos falar sobre mapeadores relacionais de objeto em geral.

© Andrew Troelsen, Phillip Japikse 2021 829


A. Troelsen e P. Japikse, Pro C# 9 com .NET 5, https://doi.org/10.1007/978-1-4842-6939-8_22
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

ÿ Observação Dois capítulos não são suficientes para cobrir todo o Entity Framework Core, pois livros inteiros (alguns do tamanho

deste) são dedicados apenas ao EF Core. A intenção desses capítulos é fornecer a você um conhecimento prático para começar a usar

o EF Core para seus aplicativos de linha de negócios.

Mapeadores Objeto-Relacionais
O ADO.NET fornece uma estrutura que permite selecionar, inserir, atualizar e excluir dados com conexões, comandos e leitores de dados.
Embora tudo isso seja muito bom, esses aspectos do ADO.NET forçam você a tratar os dados buscados de uma maneira que esteja fortemente
acoplada ao esquema físico do banco de dados. Lembre-se, por exemplo, ao obter registros do banco de dados, você abre uma conexão, cria e
executa um objeto de comando e, em seguida, usa um leitor de dados para iterar cada registro usando nomes de coluna específicos do banco de
dados.
Ao usar o ADO.NET, você deve sempre estar atento à estrutura física do banco de dados de back-end. Você deve conhecer o
esquema de cada tabela de dados, criar consultas SQL potencialmente complexas para interagir com a(s) tabela(s) de dados, rastrear alterações
nos dados recuperados (ou adicionados), etc. em si não fala a linguagem do esquema do banco de dados diretamente.

Para piorar a situação, a maneira como um banco de dados físico é geralmente construído é totalmente focada
em construções de banco de dados, como chaves estrangeiras, visualizações, procedimentos armazenados e normalização de dados, não em
programação orientada a objetos.
Outra preocupação para os desenvolvedores de aplicativos é o controle de alterações. Obter os dados do banco de dados é uma etapa
do processo, mas quaisquer alterações, adições e/ou exclusões devem ser rastreadas pelo desenvolvedor para que possam ser mantidas no
armazenamento de dados.
A disponibilidade de estruturas de mapeamento objeto-relacional (comumente chamadas de ORMs) no .NET aprimorou muito a história
de acesso a dados gerenciando a maior parte das tarefas de acesso a dados Criar, Ler, Atualizar e Excluir (CRUD) para o desenvolvedor. O
desenvolvedor cria um mapeamento entre os objetos .NET e o banco de dados relacional, e o ORM gerencia as conexões, a geração de
consultas, o controle de alterações e a persistência dos dados. Isso deixa o desenvolvedor livre para se concentrar nas necessidades de negócios
do aplicativo.

ÿ Nota É importante lembrar que os ORMs não são unicórnios mágicos montados em arco-íris. Toda decisão envolve trade-offs. Os

ORMs reduzem a quantidade de trabalho para desenvolvedores que criam camadas de acesso a dados, mas também podem

apresentar problemas de desempenho e dimensionamento se usados incorretamente. Use ORMs para operações CRUD e use o poder

de seu banco de dados para operações baseadas em conjunto.

Mesmo que os diferentes ORMs tenham pequenas diferenças em como eles operam e como são usados, eles
todos têm essencialmente as mesmas peças e peças e buscam o mesmo objetivo - facilitar as operações de acesso a dados. Entidades são
classes mapeadas para as tabelas do banco de dados. Um tipo de coleção especializada contém uma ou mais entidades. Um mecanismo de
rastreamento de alterações rastreia o estado das entidades e quaisquer alterações, adições e/ou exclusões feitas a elas, e uma construção central
controla as operações como o líder.

Compreendendo o papel do Entity Framework Core


Nos bastidores, o EF Core usa a infraestrutura ADO.NET que você já examinou no capítulo anterior. Como qualquer interação ADO.NET com
um armazenamento de dados, o EF Core usa um provedor de dados ADO.NET para interações de armazenamento de dados. Antes que um
provedor de dados ADO.NET possa ser usado pelo EF Core, ele deve ser atualizado para se integrar totalmente ao EF Core. Devido a essa
funcionalidade adicionada, você pode ter menos provedores de dados EF Core disponíveis do que provedores de dados ADO.NET.

830
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

O benefício do EF Core usando o padrão de provedor de banco de dados ADO.NET é que ele permite combinar
Paradigmas de acesso a dados EF Core e ADO.NET no mesmo projeto, aumentando seus recursos. Por exemplo, usar o EF Core
para fornecer a conexão, o esquema e o nome da tabela para operações de cópia em massa aproveita os recursos de mapeamento
do EF Core e a funcionalidade BCP incorporada ao ADO.NET. Essa abordagem combinada torna o EF Core apenas mais uma ferramenta
em sua caixa de ferramentas.
Quando você vê quanto do encanamento básico de acesso a dados é tratado para você de uma maneira conveniente e
maneira eficiente, o EF Core provavelmente se tornará seu mecanismo de acesso a dados.

ÿ Observação Muitos bancos de dados de terceiros (por exemplo, Oracle e MySQL) fornecem provedores de dados com reconhecimento de EF. Se

você não estiver usando o SQL Server, consulte seu fornecedor de banco de dados para obter detalhes ou navegue até https://docs.microsoft.com/

en us/ef/core/providers para obter uma lista de provedores de dados EF Core disponíveis.

O EF Core se encaixa melhor no processo de desenvolvimento em situações de formulários sobre dados (ou API sobre dados).
As operações em um pequeno número de entidades usando o padrão de unidade de trabalho para garantir a consistência são o ponto ideal
para o EF Core. Não é muito adequado para operações de dados em grande escala, como aplicativos de data warehouse de extração,
transformação e carregamento (ETL) ou grandes situações de geração de relatórios.

Os blocos de construção do Entity Framework


Os principais componentes do EF Core são DbContext, ChangeTracker, o tipo de coleção especializada DbSet, os provedores de banco
de dados e as entidades do aplicativo. Para trabalhar nesta seção, crie um novo aplicativo de console chamado AutoLot.Samples e adicione
o Microsoft.EntityFrameworkCore, Microsoft.
Pacotes EntityFrameworkCore.Design e Microsoft.EntityFrameworkCore.SqlServer.

dotnet new sln -n Chapter22_AllProjects dotnet new console


-lang c# -n AutoLot.Samples -o .\AutoLot.Samples -f net5.0 dotnet sln .\Chapter22_AllProjects.sln add .\AutoLot.Samples
dotnet add AutoLot.Samples package Microsoft. EntityFrameworkCore dotnet adicionar pacote AutoLot.Samples
Microsoft.EntityFrameworkCore.Design dotnet adicionar pacote AutoLot.Samples Microsoft.EntityFrameworkCore.SqlServer

A Classe DbContext
DbContext é o componente líder do EF Core e fornece acesso ao banco de dados por meio da propriedade Database. DbContext
gerencia a instância ChangeTracker, expõe o método OnModelCreating virtual para acesso à API Fluent, mantém todas as propriedades
DbSet<T> e fornece o método SaveChanges para manter os dados no armazenamento de dados. Não é usado diretamente, mas por meio de
uma classe personalizada que herda DbContext. É nessa classe que as propriedades DbSet<T> são colocadas.

A Tabela 22-1 mostra alguns dos membros de DbContext mais comumente usados.

831
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Tabela 22-1. Membros Comuns de DbContext

Membro do DbContext Significado na Vida


Base de dados Fornece acesso a informações e funcionalidades relacionadas ao banco de dados, incluindo a execução
de instruções SQL.

Modelo Os metadados sobre a forma das entidades, as relações entre elas e como elas são mapeadas para o banco
de dados. Nota: Esta propriedade geralmente não é interagida diretamente.

ChangeTracker Fornece acesso a informações e operações para instâncias de entidade que este DbContext está
rastreando.

DbSet<T> Não é realmente um membro de DbContext, mas propriedades adicionadas à classe DbContext derivada
personalizada. As propriedades são do tipo DbSet<T> e são usadas para consultar e salvar instâncias de
entidades do aplicativo. As consultas LINQ nas propriedades DbSet<T> são convertidas em consultas SQL.

Entrada Fornece acesso a informações e operações de rastreamento de alterações para a entidade, como carregar
explicitamente entidades relacionadas ou alterar o EntityState. Também pode ser chamado em uma entidade
não rastreada para alterar o estado para rastreado.

Definir<TEntidade> Cria uma instância da propriedade DbSet<T> que pode ser usada para consultar e manter dados.

Salvar alterações/ Salva todas as alterações de entidade no banco de dados e retorna o número de registros afetados.
SaveChangesAsync Executa em uma transação (implícita ou explícita).

Adicionar/Adicionar intervalo Métodos para adicionar, atualizar e remover instâncias de entidade. As alterações são mantidas somente
Atualização/Intervalo de atualização quando SaveChanges é executado com sucesso. Versões assíncronas também estão disponíveis.
Remover/RemoverIntervalo Observação: embora estejam disponíveis no DbContext derivado, esses métodos geralmente são chamados
diretamente nas propriedades DbSet<T>.

Encontrar
Localiza uma entidade de um tipo com os valores de chave primária fornecidos. Versões assíncronas
também estão disponíveis. Observação: embora estejam disponíveis no DbContext derivado, esses métodos
geralmente são chamados diretamente nas propriedades DbSet<T>.

Anexar/AnexarIntervalo Começa a rastrear uma entidade (ou lista de entidades). Versões assíncronas também estão disponíveis.
Observação: embora estejam disponíveis no DbContext derivado, esses métodos geralmente são
chamados diretamente nas propriedades DbSet<T>.

Salvando alterações Evento disparado no início de uma chamada para SaveChanges/SaveChangesAsync.

Alterações salvas Evento disparado no final de uma chamada para SaveChanges/SaveChangesAsync.

Falha ao salvar alterações Evento acionado se uma chamada para SaveChanges/SaveChangesAsync falhar.

OnModelCreating Chamado quando um modelo foi inicializado, mas antes de ser finalizado. Métodos da API Fluent são colocados
neste método para finalizar a forma do modelo.

Ao configurar Um construtor usado para criar ou modificar opções para DbContext. Executa cada vez que uma instância
DbContext é criada. Observação: é recomendável não usar isso e, em vez disso, usar DbContextOptions

para configurar a instância DbContext em tempo de execução e usar uma instância de


IDesignTimeDbContextFactory em tempo de design.

832
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Criando um DbContext derivado A primeira

etapa no EF Core é criar uma classe personalizada herdada de DbContext. Em seguida, adicione um construtor que
aceite uma instância fortemente tipada de DbContextOptions (abordada a seguir) e passe a instância para a classe
base.

namespace AutoLot.Samples
{ public class ApplicationDbContext :
DbContext { public
ApplicationDbContext(DbContextOptions<ApplicationDbContext>
opções) : base(opções)

{}}}

Essa é a classe usada para acessar o banco de dados e trabalhar com entidades, o rastreador de alterações e todos os
componentes do EF Core.

Configurando o DbContext A instância

DbContext é configurada usando uma instância da classe DbContextOptions. A instância


DbContextOptions é criada usando DbContextOptionsBuilder, pois a classe DbContextOptions não deve ser
construída diretamente em seu código. Por meio da instância DbContextOptionsBuilder, o provedor de banco de
dados é selecionado (junto com quaisquer configurações específicas do provedor) e as opções gerais do EF Core
DbContext (como log) são definidas. Em seguida, a propriedade Options é injetada no DbContext base em tempo de execução.
Esse recurso de configuração dinâmica permite alterar as configurações em tempo de execução simplesmente
selecionando diferentes opções (por exemplo, MySQL em vez do provedor do SQL Server) e criando uma nova instância
de seu DbContext derivado.

A fábrica DbContext em tempo de design A fábrica

DbContext em tempo de design é uma classe que implementa a interface IDesignTimeDbContextFactory<T>,


em que T é a classe DbContext derivada. A interface tem um método, CreateDbContext(), que você deve
implementar para criar uma instância de seu DbContext derivado.
A classe ApplicationDbContextFactory a seguir usa o método CreateDbContext() para criar um DbContextOptionsBuilder
fortemente tipado para a classe ApplicationDbContext, define o provedor de banco de dados para o provedor SQL Server
(usando a string de conexão da instância do Docker do Capítulo 21) e, em seguida, cria e retorna uma nova instância do
ApplicationDbContext:

usando Sistema;
usando Microsoft.EntityFrameworkCore; usando
Microsoft.EntityFrameworkCore.Design;

namespace AutoLot.Samples
{ public class
ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDb Context>

833
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

public ApplicationDbContext CreateDbContext(string[] args) {

var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>(); var connectionString =


@"server=.,5433;Database=AutoLotSamples;User Id=sa;Password= P@ssw0rd;";
opçõesBuilder.UseSqlServer(connectionString); Console.WriteLine(connectionString); retornar novo
ApplicationDbContext(optionsBuilder.Options); }

}
}

A fábrica de contexto é usada pela interface de linha de comando para criar uma instância da classe DbContext
derivada para executar ações como criação e aplicativo de migração de banco de dados. Como se destina a ser uma construção
de tempo de design e não usada em tempo de execução, a string de conexão para o banco de dados de desenvolvimento
geralmente é codificada.
Novo no EF Core 5, os argumentos podem ser passados para o método CreateDbContext() a partir do comando
linha. Você aprenderá mais sobre isso mais adiante neste capítulo.

OnModelCreating
A classe base DbContext expõe o método OnModelCreating que é usado para moldar suas entidades usando a API
Fluent. Isso será abordado em detalhes posteriormente neste capítulo, mas, por enquanto, adicione o seguinte código à
classe ApplicationDbContext:

substituição protegida void OnModelCreating(ModelBuilder modelBuilder) {

// Chamadas API fluentes vão aqui


OnModelCreatingPartial(modelBuilder); } void
parcial OnModelCreatingPartial(ModelBuilder
modelBuilder);

Salvando alterações
Para acionar DbContext e ChangeTracker para persistir quaisquer alterações nas entidades rastreadas, chame
o método SaveChanges() (ou SaveChangesAsync()) no DbContext derivado.

static void SampleSaveChanges() {

//A fábrica não deve ser usada assim, mas é um código de demonstração :-) var context = new
ApplicationDbContextFactory().CreateDbContext(null); //fazer algumas alterações
context.SaveChanges();

Haverá muitos exemplos de como salvar alterações ao longo deste capítulo (e livro).

834
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Suporte a transações e pontos de economia

O EF Core encapsula cada chamada para SaveChanges/SaveChangesAsync em uma transação implícita usando o nível
de isolamento do banco de dados. Para obter mais controle, você também pode inscrever o DbContext derivado em uma
transação explícita. Para executar em uma transação explícita, crie uma transação usando a propriedade Database do
DbContext derivado. Conduza suas operações normalmente e, em seguida, confirme ou reverta a transação. Aqui está um
trecho de código que demonstra isso:

usando var trans = context.Database.BeginTransaction(); tentar {

//Criar, alterar, excluir itens


context.SaveChanges(); trans.Commit(); }
catch (Exceção ex) {

trans.Rollback(); }

Pontos de salvamento para transações do EF Core foram introduzidos no EF Core 5. Quando SaveChanges()/
SaveChange sAsync() é chamado e uma transação já está em andamento, o EF Core cria um ponto de salvamento nessa transação.
Se a chamada falhar, a transação será revertida para o ponto de salvamento e não para o início da transação. Os pontos de
salvamento também podem ser gerenciados programaticamente chamando CreateSavePoint() e RollbackToSavepoint() na
transação, assim:

usando var trans = context.Database.BeginTransaction(); tentar {

//Criar, alterar, excluir itens


trans.CreateSavepoint("check point 1");
context.SaveChanges(); trans.Commit(); } catch
(Exceção ex) {

trans. RollbackToSavepoint("ponto de verificação 1"); }

Transações e estratégias de execução Quando uma

estratégia de execução está ativa (como ao usar EnableRetryOnFailure()), antes de criar uma transação
explícita, você deve obter uma referência à estratégia de execução atual que o EF Core está usando. Em
seguida, chame o método Execute() na estratégia para criar uma transação explícita.

var estrategia = context.Database.CreateExecutionStrategy(); estrategia.Execute(()


=> {

usando var trans = context.Database.BeginTransaction(); tentar

835
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

{
actionToExecute();
trans.Commit(); } catch
(Exceção ex) {

trans.Rollback(); } });

Eventos de salvamento/alterações salvas


O EF Core 5 introduziu três novos eventos que são acionados pelos métodos SaveChanges()/SaveChangesAsync().
SavingChanges é acionado quando SaveChanges() é chamado (mas antes que as instruções SQL sejam executadas no
armazenamento de dados) e SavedChanges é acionado após a conclusão de SaveChanges(). Os exemplos de código
(triviais) a seguir mostram o evento e seus manipuladores em ação:

public ApplicationDbContext(DbContextOptions<ApplicationDbContext> opções): base(opções)

{
SavingChanges += (remetente, argumentos) =>
{
Console.WriteLine($"Salvando alterações para {((DbContext)remetente).Database.
GetConnectionString()}"); };
SavedChanges += (remetente,
argumentos) => {

Console.WriteLine($"Salva {args.EntitiesSavedCount} entidades"); };


SaveChangesFailed += (remetente, argumentos) => {

Console.WriteLine($"Ocorreu uma exceção! {args.Exception.Message} entidades"); };

A Classe DbSet<T>
Para cada entidade em seu modelo de objeto, você adiciona uma propriedade do tipo DbSet<T>. A classe DbSet<T> é
uma propriedade de coleção especializada usada para interagir com o provedor de banco de dados para obter, adicionar,
atualizar ou excluir registros no banco de dados. Cada DbSet<T> fornece vários serviços principais para cada coleção para as
interações do banco de dados. Quaisquer consultas LINQ executadas em uma classe DbSet<T> são convertidas em consultas
de banco de dados pelo provedor de banco de dados. A Tabela 22-2 descreve alguns dos principais membros da classe DbSet<T>.

836
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Tabela 22-2. Membros comuns e métodos de extensão de DbSet<T>

Membro do DbSet<T> Significado na vida


Adicionar/Adicionar intervalo Começa a rastrear a(s) entidade(s) no estado Adicionado. Os itens serão adicionados quando
SaveChanges for chamado. Versões assíncronas também estão disponíveis.

AsAsyncEnumerable Retorna a coleção como IAsyncEnumerable<T>.

asQueryable Retorna a coleção como IQueryable<T>.


Encontrar
Pesquisa a entidade no ChangeTracker por chave primária. Se não for encontrado no rastreador
de alterações, o armazenamento de dados será consultado para o objeto. Uma versão assíncrona
também está disponível.

Update/UpdateRange Começa a rastrear a(s) entidade(s) no estado Modificado. Os itens serão atualizados quando SaveChanges for
chamado. Versões assíncronas também estão disponíveis.

Remove/RemoveRange Começa a rastrear a(s) entidade(s) no estado Excluído. Os itens serão removidos quando SaveChanges for
chamado. Versões assíncronas também estão disponíveis.

Attach/AttachRange Começa a rastrear a(s) entidade(s). Entidades com chaves primárias numéricas definidas como uma identidade e
valor igual a zero são rastreadas como adicionadas. Todos os outros são rastreados como Inalterados.
Versões assíncronas também estão disponíveis.

FromSqlRaw/ Cria uma consulta LINQ com base em uma string bruta ou interpolada que representa uma
FromSqlInterpolated consulta SQL. Pode ser combinado com instruções LINQ adicionais para execução do lado do servidor.

AsQueryable() Retorna uma instância IQueryable<T> de DbSet<T>.

DbSet<T> implementa IQueryable<T> e normalmente é o destino de consultas LINQ to Entity. Além dos métodos de extensão
adicionados pelo EF Core, DbSet<T> dá suporte aos mesmos métodos de extensão que você aprendeu no Capítulo 13, como ForEach(),
Select() e All().
Você adicionará propriedades DbSet<T> a ApplicationDbContext na seção “Entidades”.

ÿ Observação Muitos dos métodos listados na Tabela 22-2 têm o mesmo nome dos métodos da Tabela
22-1. A principal diferença é que os métodos DbSet<T> já sabem o tipo para operar e possuem a lista de entidades.
Os métodos DbContext devem determinar o que agir usando a reflexão. É muito mais comum usar os métodos de DbSet<T> em vez dos
métodos em DbContext.

Tipos de consulta Os

tipos de consulta são coleções DbSet<T> usadas para representar exibições, uma instrução SQL ou tabelas sem uma chave primária.
Versões anteriores do EF Core usavam DbQuery<T> para isso, mas do EF Core 3.1 em diante, o tipo DbQuery foi retirado. Os tipos de
consulta são adicionados ao DbContext derivado usando propriedades DbSet<T> e configurados como sem chave.

Por exemplo, CustomerOrderViewModel (que você criará ao criar a biblioteca completa de acesso a dados AutoLot) é
configurado com o atributo Keyless.

[Sem chave]
public class CustomerOrderViewModel {

...
}

837
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

O restante da configuração ocorre na API Fluent. O exemplo a seguir define a entidade como sem chave e mapeia o
tipo de consulta para a exibição do banco de dados dbo.CustomerOrderView (observe que HasNoKey()
O método Fluent API não é necessário se a anotação de dados Keyless estiver no modelo e vice-versa, mas é mostrado
neste exemplo para integridade):

modelBuilder.Entity<CustomerOrderViewModel>().HasNoKey().ToView("CustomerOrderView", "dbo");

Os tipos de consulta também podem ser mapeados para uma consulta SQL, conforme mostrado aqui:

modelBuilder.Entity<CustomerOrderViewModel>().HasNoKey().ToSqlQuery(
@"SELECT c.FirstName, c.LastName, i.Color, i.PetName, m.Name AS Make
FROM dbo.Orders o
INNER JOIN dbo.Customers c ON o.CustomerId = c.Id INNER
JOIN dbo.Inventory i ON o.CarId = i.Id INNER JOIN dbo.Makes m
ON m.Id = i.MakeId");

Os mecanismos finais com os quais os tipos de consulta podem ser usados são os métodos
FromSqlRaw() e FromSqlInterpolated(). Aqui está um exemplo da mesma consulta, mas usando FromSqlRaw():

public IEnumerable<CustomerOrderViewModel> GetOrders() { return


CustomerOrderViewModels.FromSqlRaw(

@"SELECT c.FirstName, c.LastName, i.Color, i.PetName, m.Name AS Make


FROM dbo.Orders o
INNER JOIN dbo.Customers c ON o.CustomerId = c.Id INNER
JOIN dbo.Inventory i ON o.CarId = i.Id INNER JOIN dbo.Makes m
ON m.Id = i.MakeId");
}

Consulta flexível/mapeamento de tabela

O EF Core 5 introduziu a capacidade de mapear a mesma classe para mais de um objeto de banco de dados. Esses
objetos podem ser tabelas, exibições ou funções. Por exemplo, CarViewModel do Capítulo 21 pode ser mapeado para uma
exibição que retorna o nome da marca com os dados do carro e a tabela Inventário. O EF Core consultará a exibição e
enviará atualizações para a tabela.

modelBuilder.Entity<CarViewModel>()
.ToTable("Inventário")
.ToView("InventoryWithMakesView");

A instância ChangeTracker A

instância ChangeTracker rastreia o estado dos objetos carregados em DbSet<T> dentro de uma instância DbContext.
A Tabela 22-3 lista os valores possíveis para o estado de um objeto.

838
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Tabela 22-3. Valores de Enumeração de Estado de Entidade

Valor Significado na vida


Adicionado A entidade está sendo rastreada, mas ainda não existe no banco de dados.

Excluído A entidade está sendo rastreada e marcada para exclusão do banco de dados.

independente A entidade não está sendo rastreada pelo rastreador de alterações.

modificado A entrada está sendo rastreada e foi alterada.

Inalterado A entidade está sendo rastreada, existe no banco de dados e não foi modificada.

Se você precisar verificar o estado de um objeto, use o seguinte código:

EntityState estado = context.Entry(entity).State;

Você também pode alterar programaticamente o estado de um objeto usando o mesmo mecanismo. Para mudar o
state para Deleted (por exemplo), use o seguinte código:

context.Entry(entity).State = EntityState.Deleted;

Eventos ChangeTracker
Existem dois eventos que podem ser gerados pelo ChangeTracker. O primeiro é StateChanged e o segundo é Tracked. O evento
StateChanged é acionado quando o estado de uma entidade é alterado. Ele não dispara quando uma entidade é rastreada pela primeira vez.
O evento Tracked é acionado quando uma entidade começa a ser rastreada, sendo adicionada programaticamente a uma instância DbSet<T>
ou quando retornada de uma consulta.

Redefinindo o estado de DbContext Uma novidade

no EF Core 5 é a capacidade de redefinir um DbContext. O método ChangeTracker.Clear() limpa todas as entidades das propriedades
DbSet<T> definindo seu estado como desanexado.

Entidades
As classes fortemente tipadas que mapeiam para tabelas de banco de dados são oficialmente chamadas de entidades. A coleção de entidades
em um aplicativo compreende um modelo conceitual de um banco de dados físico. Falando formalmente, esse modelo é chamado de modelo de
dados de entidade (EDM), geralmente chamado simplesmente de modelo. O modelo é mapeado para o domínio de aplicativo/negócios. As
entidades e suas propriedades são mapeadas para as tabelas e colunas usando convenções Entity Framework Core, configuração e API Fluent
(código). As entidades não precisam ser mapeadas diretamente para o esquema do banco de dados. Você é livre para estruturar suas classes de
entidade para atender às necessidades de seu aplicativo e, em seguida, mapear suas entidades exclusivas para seu esquema de banco de dados.

Esse baixo acoplamento entre o banco de dados e suas entidades significa que você pode moldar as entidades para corresponder
seu domínio de negócios, independentemente do design e da estrutura do banco de dados. Por exemplo, pegue a tabela simples Inventory
no banco de dados AutoLot e a classe de entidade Car do capítulo anterior. Os nomes são diferentes, mas a entidade Carro é mapeada para a
tabela Inventário. O EF Core examina a configuração de suas entidades no modelo para mapear a representação do lado do cliente da tabela
Inventory (em nosso exemplo, a classe Car) para as colunas corretas da tabela Inventory.

As próximas seções detalham como convenções, anotações de dados e código do EF Core (usando a API Fluent) mapeiam entidades,
propriedades e relacionamentos entre entidades no modo para tabelas, colunas e relacionamentos de chave estrangeira em seu banco de dados.

839
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Propriedades de mapeamento para colunas

Ao usar um armazenamento de dados relacional, as convenções do EF Core mapeiam todas as propriedades públicas de leitura/
gravação para colunas na tabela para a qual a entidade é mapeada. Se a propriedade for uma propriedade automática, o EF
Core lê e grava por meio do getter e do setter. Se a propriedade tiver um campo de apoio, o EF Core lerá e gravará no campo
de apoio em vez da propriedade pública, mesmo que o campo de apoio seja privado. Embora o EF Core possa ler e gravar em
campos privados, ainda deve haver uma propriedade pública de leitura/gravação que encapsula o campo de apoio.
Dois cenários em que o suporte de campo de apoio é vantajoso são ao usar o padrão
INotifyPropertyChanged em aplicativos Windows Presentation Foundation (WPF) e quando os valores padrão do banco
de dados entram em conflito com os valores padrão do .NET Core. O uso do EF Core com WPF é abordado no Capítulo
28, e os valores padrão do banco de dados são abordados posteriormente neste capítulo.
Os nomes, tipos de dados e nulidade das colunas são configurados por meio de convenções, dados
anotações e/ou a API Fluent. Cada um desses tópicos é abordado em profundidade mais adiante neste capítulo.

Classes de mapeamento para tabelas Há

dois esquemas de mapeamento de classe para tabela disponíveis no EF Core: tabela por hierarquia (TPH) e tabela por tipo
(TPT). O mapeamento TPH é o padrão e mapeia uma hierarquia de herança para uma única tabela. Novo no EF Core 5, o TPT
mapeia cada classe na hierarquia para sua própria tabela.

ÿ Nota As classes também podem ser mapeadas para exibições e consultas SQL brutas. Eles são referidos como tipos de consulta e são

abordados posteriormente neste capítulo.

Mapeamento de Tabela por Hierarquia (TPH)


Considere o exemplo a seguir, que mostra a classe Car do Capítulo 21 dividida em duas classes: uma classe base para as
propriedades Id e TimeStamp e as demais propriedades deixadas na classe Car. Ambas as classes devem ser criadas no
diretório Models do projeto AutoLot.Samples.

usando System.Collections.Generic;

namespace AutoLot.Samples.Models { public


abstract class BaseEntity { public int Id { get;
definir; } public byte[] TimeStamp { get;
definir; } } }

usando System.Collections.Generic;
namespace AutoLot.Samples.Models { public
class Car : BaseEntity { public string Color
{ get; definir; } public string PetName { get;
definir; } public int MakeId { get; definir; } } }

840
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Para tornar o EF Core ciente de que uma classe de entidade faz parte do modelo de objeto, adicione uma propriedade DbSet<T> para o
entidade. Adicione a seguinte instrução using à classe ApplicationDbContext:

usando AutoLot.Samples.Models;

Adicione o seguinte código à classe ApplicationDbContext entre o construtor e o


Método OnModelCreating():

public DbSet<Car> Carros { get; definir; }

Observe que a classe base não é adicionada como uma instância DbSet<T>. Embora os detalhes da migração
sejam abordados posteriormente neste capítulo, vamos criar o banco de dados e a tabela Cars. Abra um prompt de comando
no mesmo diretório do projeto AutoLot.Samples e execute o seguinte comando (tudo em uma linha):

dotnet tool install --global dotnet-ef --version 5.0.1 dotnet ef migrations add
TPH -o Migrations -c AutoLot.Samples.ApplicationDbContext dotnet ef database update TPH -c
AutoLot.Samples.ApplicationDbContext

O primeiro comando instalou as ferramentas de linha de comando do EF Core como uma ferramenta global. Isto precisa ser feito
apenas uma vez em sua máquina. O segundo comando criou uma migração denominada TPH no diretório Migrations
usando ApplicationDbContext no namespace AutoLot.Samples. O terceiro comando atualizou o banco de dados da migração
TPH.
Quando o EF Core é usado para criar esta tabela no banco de dados, a classe BaseEntity herdada é combinada na
classe Car e uma única tabela é criada, mostrada aqui:

CREATE TABLE [dbo].[Carros]( [Id]


[int] IDENTITY(1,1) NOT NULL, [MakeId] [int]
NOT NULL, [Color] [nvarchar](max) NULL,
[PetName] [nvarchar ](max) NULL, [TimeStamp]
[varbinary](max) NULL, CONSTRAINT
[PK_Cars] PRIMARY KEY CLUSTERED (

[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS
= ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) EM [PRIMARY] TEXTIMAGE_ON [PRIMARY]

O exemplo anterior baseou-se nas convenções do EF Core (abordadas em breve) para criar as propriedades de
tabela e coluna.

Mapeamento de tabela por tipo (TPT)


Para explorar o esquema de mapeamento TPT, as mesmas entidades anteriores podem ser usadas, mesmo com a
classe base marcada como abstrata. Como o TPH é o padrão, o EF Core deve ser instruído a mapear cada classe para uma
tabela. Isso pode ser feito com anotações de dados ou com a API Fluent. Adicione o seguinte código ao ApplicationDbContext:

substituição protegida void OnModelCreating(ModelBuilder modelBuilder) {

modelBuilder.Entity<BaseEntity>().ToTable("BaseEntities");
modelBuilder.Entity<Car>().ToTable("Cars");
OnModelCreatingPartial(modelBuilder); } void parcial
OnModelCreatingPartial(ModelBuilder modelBuilder);

841
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Para “redefinir” o banco de dados e o projeto, exclua a pasta Migrations e o banco de dados. Para forçar a exclusão do
banco de dados usando a CLI, insira o seguinte:

dotnet ef database drop -f -c AutoLot.Samples.ApplicationDbContext

Agora crie e aplique a migração para o padrão TPT.

dotnet ef migrations add TPT -o Migrations -c AutoLot.Samples.ApplicationDbContext dotnet ef database


update TPT -c AutoLot.Samples.ApplicationDbContext

O EF Core criará as tabelas a seguir ao atualizar o banco de dados. Os índices também mostram que o
as tabelas têm um mapeamento um-para-um.

CREATE TABLE [dbo].[BaseEntities](


[Id] [int] IDENTIDADE(1,1) NÃO NULO,
[TimeStamp] [varbinary](max) NULL,
CONSTRAINT [PK_BaseEntities] PRIMARY KEY CLUSTERED (

[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS
= ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) EM [PRIMARY] TEXTIMAGE_ON [PRIMARY]
IR

CREATE TABLE [dbo].[Inventory]( [Id]


[int] NOT NULL, [MakeId] [int] NOT
NULL, [Color] [nvarchar](max) NULL,
[PetName] [nvarchar](max) NULL,
CONSTRAINT [PK_Inventory] PRIMARY
KEY CLUSTERED (

[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS
= ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) EM [PRIMARY] TEXTIMAGE_ON [PRIMARY]
IR

ALTER TABLE [dbo].[Inventory] WITH CHECK ADD CONSTRAINT [FK_Inventory_BaseEntities_Id]


CHAVE ESTRANGEIRA([Id])
REFERÊNCIAS [dbo].[BaseEntities] ([Id])

GO ALTER TABLE [dbo].[Inventory] CHECK CONSTRAINT [FK_Inventory_BaseEntities_Id]


IR

ÿ Nota O mapeamento de tabela por tipo tem implicações de desempenho significativas que devem ser consideradas
antes de usar esse esquema de mapeamento. Para obter mais informações, consulte a documentação: https://
docs.microsoft.com/en-us/ef/core/performance/modeling-for-performance#inheritance-mapping.

842
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Para “redefinir” o banco de dados e o projeto para se preparar para o próximo conjunto de exemplos, comente o código em
o método OnModelCreating() e mais uma vez exclua a pasta Migrations e o banco de dados.

dotnet ef database drop -f -c AutoLot.Samples.ApplicationDbContext

Propriedades de navegação e chaves estrangeiras


As propriedades de navegação representam como as classes de entidade se relacionam entre si e permitem que o código passe de
uma instância de entidade para outra. Por definição, uma propriedade de navegação é qualquer propriedade que mapeia para um tipo não
escalar conforme definido pelo provedor de banco de dados. Na prática, as propriedades de navegação mapeiam para outra entidade
(chamadas propriedades de navegação de referência) ou uma coleção de outra entidade (chamadas propriedades de navegação de coleção).
No lado do banco de dados, as propriedades de navegação são traduzidas em relacionamentos de chave estrangeira entre as tabelas.
Relacionamentos um para um, um para muitos e (novo no EF Core 5) muitos para muitos têm suporte direto no EF Core.
As classes de entidade também podem ter propriedades de navegação de volta para si mesmas, representando tabelas de auto-referência.

ÿ Observação Acho útil considerar objetos com propriedades de navegação como listas vinculadas e, se as propriedades de

navegação forem bidirecionais, os objetos agem como listas duplamente vinculadas.

Antes de abordar os detalhes das propriedades de navegação e padrões de relacionamento de entidades, consulte a Tabela 22-4.
Esses termos são usados em todos os três padrões de relacionamento.

Tabela 22-4. Termos usados para descrever propriedades e relacionamentos de navegação

Prazo Significado na vida

Entidade principal O pai do relacionamento.

Entidade O filho do relacionamento.


dependente

Chave principal A(s) propriedade(s) usada(s) para definir a entidade principal. Pode ser a chave primária ou uma chave alternativa. As
chaves podem ser configuradas usando uma única propriedade ou várias propriedades.

Chave estrangeira A(s) propriedade(s) mantida(s) pela entidade filha para armazenar a chave principal.

Relação Relação em que o valor da chave estrangeira é necessário (não anulável).


necessária

Relação em que o valor da chave estrangeira não é (anulável).


relacionamento opcional

Propriedades de chave estrangeira ausentes

Se uma entidade com uma propriedade de navegação de referência não tiver uma propriedade para o valor da chave estrangeira, o
EF Core criará a(s) propriedade(s) necessária(s) na entidade. Elas são conhecidas como propriedades de chave estrangeira oculta e
são nomeadas no formato <nome da propriedade de navegação><nome da propriedade da chave principal> ou <nome da entidade
principal><nome da propriedade da chave principal>. Isso vale para todos os tipos de relacionamento (um para muitos, um para um, muitos
para muitos). É uma abordagem muito mais limpa criar suas entidades com propriedades/propriedades de chave estrangeira explícitas do que
fazer com que o EF Core as crie para você.

843
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Relacionamentos um-para-muitos Para criar

um relacionamento um-para-muitos, a classe de entidade em um lado (o principal) adiciona uma propriedade de coleção
da classe de entidade que está no lado muitos (o dependente). A entidade dependente também deve ter propriedades para a
chave estrangeira de volta ao principal. Caso contrário, o EF Core criará propriedades de chave estrangeira de sombra,
conforme explicado anteriormente.
Por exemplo, no banco de dados criado no Capítulo 21, a tabela Makes (representada pela classe de entidade
Make) e a tabela Inventory (representada pela classe de entidade Car) têm um relacionamento um-para-muitos. Para
manter as coisas simples para esses exemplos, a entidade Car será mapeada para a tabela Cars. O código a seguir
mostra as propriedades de navegação bidirecional que representam esse relacionamento:

usando System.Collections.Generic;
namespace AutoLot.Samples.Models
{
public class Make: BaseEntity {

string pública Nome {obter; definir; }


public IEnumerable<Car> Carros { get; definir; } = new List<Carro>();
}
}

usando System.Collections.Generic;
namespace AutoLot.Samples.Models
{
classe pública Carro : BaseEntity
{
string pública Cor {obter; definir; }
public string PetName { get; definir; }
public int MakeId { get; definir; } public
MakeNavigation { get; definir; } }

ÿ Observação Ao criar um banco de dados existente, o EF Core nomeia as propriedades de navegação de referência da

mesma forma que o nome do tipo de propriedade (por exemplo, public Make {get; set;}). Isso pode causar problemas com a

navegação e o IntelliSense, além de dificultar o trabalho com o código. Prefiro adicionar o sufixo Navigation para fazer
referência às propriedades de navegação para maior clareza, conforme mostrado no exemplo anterior.

No exemplo Car/Make, a entidade Car é a entidade dependente (o muitos do um-para-muitos), e a


entidade Make é a entidade principal (o um do um-para-muitos).
Adicione a instância DbSet<Make> ao ApplicationDbContext, conforme mostrado aqui:

public DbSet<Car> Carros { get; definir; }


public DbSet<Make> Faz { get; definir; }

Crie a migração e atualize o banco de dados usando os seguintes comandos:

dotnet ef migrations add One2Many -o Migrations -c AutoLot.Samples.ApplicationDbContext dotnet ef


database update One2Many -c AutoLot.Samples.ApplicationDbContext

844
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Quando o banco de dados é atualizado usando migrações do EF Core, as seguintes tabelas são criadas:

CREATE TABLE [dbo].[Makes]( [Id]


[int] IDENTITY(1,1) NOT NULL, [Name]
[nvarchar](max) NULL, [TimeStamp] [varbinary]
(max) NULL, CONSTRAINT [PK_Makes ]
CHAVE PRIMÁRIA CLUSTERED (

[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS
= ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) EM [PRIMARY] TEXTIMAGE_ON [PRIMARY]
IR

CREATE TABLE [dbo].[Cars]( [Id]


[int] IDENTITY(1,1) NOT NULL, [Color]
[nvarchar](max) NULL, [PetName] [nvarchar]
(max) NULL, [TimeStamp] [varbinary](max)
NULL, [MakeId] [int] NOT NULL, CONSTRAINT
[PK_Cars] PRIMARY KEY CLUSTERED (

[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS
= ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) EM [PRIMARY] TEXTIMAGE_ON [PRIMARY]
IR

ALTER TABLE [dbo].[Cars] WITH CHECK ADD CONSTRAINT [FK_Cars_Makes_MakeId] FOREIGN


KEY([MakeId])
REFERÊNCIAS [dbo].[Faz] ([Id])
ON DELETE CASCADE
GO ALTER TABLE
[dbo].[Cars] CHECK CONSTRAINT [FK_Cars_Makes_MakeId]
IR

Observe a chave estrangeira e verifique as restrições criadas na tabela dependente (Carros).

Relacionamentos um-para-um
Em relacionamentos um-para-um, ambas as entidades têm uma propriedade de navegação de referência para a outra
entidade. Embora relacionamentos um-para-muitos indiquem claramente a entidade principal e dependente, ao criar
relacionamentos um-para-um, o EF Core deve ser informado de qual lado é o principal, tendo uma chave estrangeira claramente
definida para a entidade principal ou indicando o principal usando a API Fluent. Se o EF Core não for informado, ele escolherá
um com base em sua capacidade de detectar uma chave estrangeira. Na prática, você deve definir claramente o dependente
adicionando propriedades de chave estrangeira.

namespace AutoLot.Samples.Models {

classe pública Carro : BaseEntity {

845
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

string pública Cor {obter; definir; } public


string PetName { get; definir; } public int MakeId
{ get; definir; } public MakeNavigation { get;
definir; } Public Radio RadioNavigation { get;
definir; } }

namespace AutoLot.Samples.Models {

classe pública Rádio: BaseEntity {

public bool HasTweeters { get; definir; } public bool


HasSubWoofers { get; definir; } public string RadioId
{ get; definir; } public int CarId { get; definir; }
public Car CarNavigation { get; definir; } }

Como Radio tem uma chave estrangeira para a classe Car (com base na convenção, abordada brevemente),
Radio é a entidade dependente e Car é a entidade principal. O EF Core cria implicitamente o índice exclusivo necessário
na propriedade de chave estrangeira na entidade dependente. Se você quiser alterar o nome do índice, isso pode ser feito
usando anotações de dados ou a API Fluent.
Adicione DbSet<Radio> a ApplicationDbContext.

public virtual DbSet<Car> Carros { get; definir; } public


virtual DbSet<Make> Faz { get; definir; } public virtual
DbSet<Rádio> Rádios { get; definir; }

Crie a migração e atualize o banco de dados usando os seguintes comandos:

dotnet ef migrations add One2One -o Migrations -c AutoLot.Samples.ApplicationDbContext dotnet ef database update


One2One -c AutoLot.Samples.ApplicationDbContext

Quando o banco de dados é atualizado usando migrações do EF Core, a tabela Cars permanece inalterada e o seguinte
Tabela de rádios é criada:

CREATE TABLE [dbo].[Radios]( [Id]


[int] IDENTITY(1,1) NOT NULL, [HasTweeters]
[bit] NOT NULL, [HasSubWoofers] [bit] NOT
NULL, [RadioId] [nvarchar]( max) NULL,
[TimeStamp] [varbinary](max) NULL, [CarId]
[int] NOT NULL, CONSTRAINT [PK_Radios]
PRIMARY KEY CLUSTERED (

[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS
= ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) EM [PRIMARY] TEXTIMAGE_ON [PRIMARY]
IR

846
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

ALTER TABLE [dbo].[Radios] WITH CHECK ADD CONSTRAINT [FK_Radios_Cars_CarId] FOREIGN


KEY([CarId])
REFERÊNCIAS [dbo].[Carros] ([Id])
EM APAGAR EM CASCATA
IR
ALTER TABLE [dbo].[Radios] CHECK CONSTRAINT [FK_Radios_Cars_CarId]
IR

Observe a chave estrangeira e verifique as restrições criadas na tabela dependente (Radios).

Relacionamentos muitos-para-muitos (Novo EF Core 5)


Em relacionamentos muitos-para-muitos, ambas as entidades têm uma propriedade de navegação de coleção para a outra entidade.
Isso é implementado no armazenamento de dados com uma tabela de junção entre as duas tabelas de entidade. Esta tabela de
junção é nomeada após as duas tabelas usando <Entity1Entity2>. O nome pode ser alterado programaticamente por meio da API
Fluent. A entidade de junção tem relacionamentos um-para-muitos com cada uma das tabelas de entidades.

namespace AutoLot.Samples.Models
{ public class Car : BaseEntity {

string pública Cor {obter; definir; } public


string PetName { get; definir; } public int
MakeId { get; definir; } public MakeNavigation
{ get; definir; } Public Radio RadioNavigation { get;
definir; } public IEnumerable<Driver> Drivers { get;
definir; } = new List<Driver>(); }

namespace AutoLot.Samples.Models {

driver de classe pública: BaseEntity {

public string FirstName { get; definir; } public


string Sobrenome { get; definir; } public
IEnumerable<Car> Carros { get; definir; } = new List<Carro>(); }

O equivalente pode ser feito criando as três tabelas explicitamente e é assim que deve ser feito em
Versões do EF Core anteriores ao EF Core 5. Aqui está um exemplo abreviado:

driver de classe pública {

...
public IEnumerable<CarDriver> CarDrivers { get; definir; } }

847
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

classe pública Carro


{
...
public IEnumerable<CarDriver> CarDrivers { get; definir; } } public
class Motorista {

public int CarId {get;set;} public


Car CarNavigation {get;set;} public int
DriverId {get;set;} public Driver
DriverNavigation {get;set;} }

Adicione DbSet<Driver> a ApplicationDbContext.

public virtual DbSet<Car> Carros { get; definir; } public


virtual DbSet<Make> Faz { get; definir; } public virtual
DbSet<Rádio> Rádios { get; definir; } drivers virtuais
DbSet<Driver> públicos { get; definir; }

Crie a migração e atualize o banco de dados usando os seguintes comandos:

dotnet ef migrations add Many2Many -o Migrations -c AutoLot.Samples.ApplicationDbContext dotnet ef database


update many2Many -c AutoLot.Samples.ApplicationDbContext

Quando o banco de dados é atualizado usando migrações do EF Core, a tabela Cars permanece inalterada e a tabela Drivers
e as tabelas CarDriver são criadas.

CREATE TABLE [dbo].[Drivers](


[Id] [INT] IDENTIDADE(1,1) NÃO NULO,
[Nome] [NVARCHAR](MAX) NULL,
[Sobrenome] [NVARCHAR](MAX) NULL,
[TimeStamp] [VARBINARY](MAX) NULL,
CONSTRAINT [PK_Drivers] PRIMARY KEY CLUSTERED (

[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS
= ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) EM [PRIMARY] TEXTIMAGE_ON [PRIMARY]
IR

CREATE TABLE [dbo].[CarDriver]


( [CarsId] [int] NOT NULL, [DriversId]
[int] NOT NULL,
CONSTRAINT [PK_CarDriver] PRIMARY KEY CLUSTERED
(
[CarsId] ASC,
[DriversId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS
= ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) EM [PRIMÁRIO]

848
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

IR
ALTER TABLE [dbo].[CarDriver] WITH CHECK ADD CONSTRAINT [FK_CarDriver_Cars_CarsId] FOREIGN KEY([CarsId])

REFERÊNCIAS [dbo].[Carros] ([Id])


EM APAGAR EM CASCATA
IR
ALTER TABLE [dbo].[CarDriver] CHECK CONSTRAINT [FK_CarDriver_Cars_CarsId]
IR
ALTER TABLE [dbo].[CarDriver] WITH CHECK ADD CONSTRAINT [FK_CarDriver_Drivers_DriversId]
CHAVE ESTRANGEIRA([DriversId])
REFERÊNCIAS [dbo].[Drivers] ([Id])
EM APAGAR EM CASCATA
IR
ALTER TABLE [dbo].[CarDriver] CHECK CONSTRAINT [FK_CarDriver_Drivers_DriversId]
IR

Observe que a chave primária composta, as restrições de verificação (chaves estrangeiras) e o comportamento em cascata são todos
criado pelo EF Core para garantir que a tabela CarDriver esteja configurada como uma tabela de junção adequada.

ÿ Observação No momento em que este livro foi escrito, relacionamentos muitos-para-muitos scaffolding ainda não eram
suportados. Os relacionamentos muitos para muitos são estruturados com base na estrutura da tabela, como no segundo

exemplo com a entidade CarDriver . O problema está sendo rastreado aqui: https://github.com/dotnet/efcore/issues/22475.

Comportamento em cascata

A maioria dos armazenamentos de dados (como o SQL Server) possui regras que controlam o comportamento quando uma linha é
excluída. Se os registros relacionados (dependentes) também devem ser excluídos, isso é chamado de exclusão em cascata. No EF
Core, há três ações que podem ocorrer quando uma entidade principal (com entidades dependentes carregadas na memória) é excluída.

• Os registros dependentes são excluídos.

• As chaves estrangeiras dependentes são definidas como nulas.

• A entidade dependente permanece inalterada.

O comportamento padrão é diferente entre relacionamentos opcionais e obrigatórios. O comportamento também pode
ser configurado para um dos sete valores, embora apenas cinco sejam recomendados para uso. O comportamento é
configurado com a enumeração DeleteBehavior usando a API Fluent. As opções disponíveis na enumeração estão listadas
aqui:

• Cascata

• ClientCascade

• ClientNoAction (não recomendado para uso)

• ClienteSetNull

• NoAction (não recomendado para uso)

• SetNull

• Restringir

849
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

No EF Core, o comportamento especificado é acionado somente depois que uma entidade é excluída e SaveChanges() é chamado no DbContext
derivado. Consulte a seção "Execução de consulta" para obter mais detalhes sobre quando o EF Core interage com o armazenamento de dados.

Relacionamentos Opcionais

Lembre-se da Tabela 22-4 que os relacionamentos opcionais são onde a entidade dependente pode definir o(s) valor(es) da chave estrangeira
como nulo. Para relacionamentos opcionais, o comportamento padrão é ClientSetNull. A Tabela 22-5 mostra o comportamento em cascata com
entidades dependentes e o efeito nos registros do banco de dados ao usar o SQL Server.

Tabela 22-5. Comportamento em cascata com relacionamentos opcionais

Excluir comportamento Efeito sobre dependentes (na memória) Efeito sobre dependentes (no banco de dados)

Cascata As entidades são excluídas pelo banco de dados.


As entidades são excluídas.

ClientCascade As entidades são excluídas. Para bancos de dados que não dão suporte à exclusão em
cascata, o EF Core exclui as entidades.

ClientSetNull (padrão) Propriedades/propriedades de chave estrangeira Nenhum.


definidas como nulas.

SetNull Propriedades/propriedades de chave estrangeira Propriedades/propriedades de chave estrangeira definidas como nulas.
definidas como nulas.

Restringir Nenhum. Nenhum.

Relacionamentos obrigatórios Os

relacionamentos obrigatórios são onde a entidade dependente não pode definir o(s) valor(es) da chave estrangeira como nulos. Para
relacionamentos obrigatórios, o comportamento padrão é Cascata. A Tabela 22-6 mostra o comportamento em cascata com entidades
dependentes e o efeito nos registros do banco de dados ao usar o SQL Server.

Tabela 22-6. Comportamento em Cascata com Relacionamentos Necessários

Excluir comportamento Efeito nos dependentes (na memória) Efeito nos dependentes (no banco de dados)

Cascata (padrão) As entidades são excluídas.


As entidades são excluídas.

ClientCascade As entidades são excluídas. Para bancos de dados que não dão suporte à exclusão em
cascata, o EF Core exclui as entidades.

ClientSetNull SaveChanges lança exceção. Nenhum.

SetNull SaveChanges lança exceção. SaveChanges lança exceção.

Restringir Nenhum. Nenhum.

850
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Convenções da Entidade

Existem muitas convenções que o EF Core usa para definir uma entidade e como ela se relaciona com o armazenamento de dados. As
convenções estão sempre habilitadas, a menos que sejam anuladas por anotações de dados ou código na API Fluent. A Tabela 22-7 lista algumas
das convenções EF Core mais importantes.

Tabela 22-7. Algumas das principais convenções da EF

Convenção Significado na vida incluída


Todas as classes comDbSet
uma propriedade
e todas as classes que podem ser acessadas (através das propriedades das tabelas de navegação) por uma
classe DbSet são criadas no banco de dados.

Incluído Todas as propriedades públicas com um getter e um setter (incluindo propriedades automáticas) são mapeadas para colunas.
colunas

Nome da tabela Mapeia para o nome da propriedade DbSet no DbContext derivado. Se nenhum DbSet existir, o nome da classe será usado.

Esquema As tabelas são criadas no esquema padrão do armazenamento de dados (dbo no SQL Server).

Nome da coluna Os nomes das colunas são mapeados para os nomes de propriedade da classe.

Dados da coluna Os tipos de dados são selecionados com base no tipo de dados .NET Core e traduzidos pelo provedor de
tipo banco de dados (SQL Server). DateTime mapeia para datetime2(7) e string mapeia para nvarchar(max). Strings como
parte de um mapa de chave primária para nvarchar(450).

Coluna Os tipos de dados não anuláveis são criados como colunas de persistência Not Null. O EF Core respeita a nulidade do C# 8.

Chave primária As propriedades denominadas Id ou <EntityTypeName>Id serão configuradas como a chave primária. Chaves
do tipo short, int, long ou Guid têm valores controlados pelo armazenamento de dados. Valores numéricos são criados como
colunas de identidade (SQL Server).

Relacionamentos Os relacionamentos entre tabelas são criados quando há propriedades de navegação entre duas classes de entidade.

Propriedades de chave estrangeira denominadas <OtherClassName>Id são chaves estrangeiras para propriedades de navegação do tipo
<OtherClassName>.

Todos os exemplos de propriedades de navegação anteriores aproveitam as convenções do EF Core para criar as relações entre as tabelas.

Propriedades de mapeamento para colunas


Por convenção, as propriedades públicas de leitura/gravação são mapeadas para colunas com o mesmo nome. O tipo de dados corresponde ao
equivalente do armazenamento de dados do tipo de dados CLR da propriedade. As propriedades não anuláveis são definidas como não nulas no
armazenamento de dados e as propriedades anuláveis são definidas para permitir nulo. O EF Core oferece suporte a tipos de referência anuláveis
que foram introduzidos no C# 8.

Para campos de apoio, o EF Core espera que o campo de apoio seja nomeado usando uma das seguintes convenções (em ordem
de precedência):

• _<nome da propriedade com caixa de camelo>

• _<nome da propriedade>

• m_<nome da propriedade com caixa de camelo>

• m_<nome da propriedade>

851
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Se a propriedade Color da classe Car for atualizada para usar um campo de apoio, ela precisaria (por convenção) ser nomeada como
_color, _Color, m_color ou m_Color, assim:

string privada _color = "Ouro"; string pública Cor


{

obter => _cor; set =>


_color = valor; }

Anotações de dados do Entity Framework


Anotações de dados são atributos C# usados para moldar ainda mais suas entidades. A Tabela 22-8 lista algumas das anotações de dados
usadas com mais frequência para definir como suas classes de entidade e propriedades são mapeadas para tabelas e campos do banco de
dados. As anotações de dados substituem quaisquer convenções conflitantes. Há muito mais anotações que você pode usar para refinar as
entidades no modelo, como você verá ao longo deste capítulo e livro.

Tabela 22-8. Algumas anotações de dados suportadas pelo Entity Framework Core (*Novos atributos no EF Core 5)

Anotação de Dados Significado na vida


Mesa Define o esquema e o nome da tabela para a entidade.

Sem chave* Indica que uma entidade não possui uma chave (por exemplo, representando uma exibição de banco de dados).

Coluna Define o nome da coluna para a propriedade da entidade.

BackingField* Especifica o campo de apoio C# para uma propriedade.

Chave Define a chave primária da entidade. Os campos-chave também são implicitamente [Obrigatório].

Índice* Colocado em uma classe para especificar uma única coluna ou índice de várias colunas. Permite
especificar que o índice é único.

Controlado Declara que a classe será propriedade de outra classe de entidade.

Obrigatório Declara a propriedade como não anulável no banco de dados.

Chave Estrangeira Declara uma propriedade que é usada como chave estrangeira para uma propriedade de navegação.

InverseProperty Declara a propriedade de navegação na outra extremidade de um relacionamento.

StringLength Especifica o comprimento máximo para uma propriedade de string.

carimbo de hora Declara um tipo como uma versão de linha no SQL Server e adiciona verificações de simultaneidade às
operações de banco de dados que envolvem a entidade.

Campo ConcurrencyCheck Flags a ser usado na verificação de simultaneidade ao executar atualizações e exclusões.

DatabaseGenerated Especifica se o campo é gerado pelo banco de dados ou não. Recebe uma DatabaseGeneratedOption
valor de Computed, Identity ou None.

Tipo de dados Fornece uma definição mais específica de um campo do que o tipo de dados intrínseco.

Não mapeado Exclui a propriedade ou classe em relação aos campos e tabelas do banco de dados.

O código a seguir mostra a classe BaseEntity com anotações que declaram o campo Id como primário
chave. A segunda anotação de dados na propriedade Id indica que é uma coluna Identity no SQL Server.
A propriedade TimeStamp será uma propriedade timestamp/rowversion do SQL Server (para verificação de simultaneidade, abordada
posteriormente neste capítulo).

852
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

usando System.ComponentModel.DataAnnotations;
usando System.ComponentModel.DataAnnotations.Schema;
classe abstrata pública BaseEntity {

[Chave, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public


int Id { get; definir; }
[Timestamp]
public byte[] TimeStamp { get; definir; } }

Aqui está a classe Car e as anotações de dados que a moldam no banco de dados:

usando System.Collections.Generic;
usando System.ComponentModel.DataAnnotations;
usando System.ComponentModel.DataAnnotations.Schema;
usando Microsoft.EntityFrameworkCore;

[Table("Inventário", Schema="dbo")]
[Index(nameof(MakeId), Name = "IX_Inventory_MakeId")] public
class Car : BaseEntity {

[Obrigatório, StringLength(50)]
public string Color { get; definir; }
[Obrigatório, StringLength(50)]
public string PetName { get; definir; } public
int MakeId { get; definir; }
[ForeignKey(nameof(MakeId))]
public Make MakeNavigation { get; definir; }
[InverseProperty(nameof(Driver.Cars))] public
IEnumerable<Driver> Drivers { get; definir; } }

O atributo Table mapeia a classe Car para a tabela Inventory no esquema dbo (o atributo Column é usado para
alterar um nome de coluna ou tipo de dados). O atributo Index cria um índice na chave estrangeira MakeId.
Os dois campos de texto são definidos como obrigatórios e um StringLength máximo de 50 caracteres. Os atributos
InverseProperty e ForeignKey são explicados na próxima seção.
As alterações das convenções do EF Core são as seguintes:

• Renomeando a tabela de Carros para Inventário

• Alterar a coluna TimeStamp de varbinary(max) para o timestamp do SQL Server


tipo de dados

• Configurando o tipo de dados e nulidade para as colunas Color e PetName de


nvarchar(max)/null para nvarchar(50)/not null

• Renomear o índice no MakeId

O restante das anotações usadas corresponde à configuração definida pelas convenções do EF Core.
Se você criar uma migração e tentar aplicá-la, a migração falhará. O SQL Server não permite que uma coluna
existente seja alterada para um tipo de dados de carimbo de data/hora de outro tipo de dados. A coluna deve ser
eliminada e recriada. Infelizmente, a infraestrutura de migração não é descartada e recriada. Ele tenta alterar a coluna.

853
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

A maneira mais fácil de resolver isso é comentar a propriedade TimeStamp na entidade base, criar e
aplique uma migração e, em seguida, remova o comentário do TimeStamp e crie e aplique outra migração.
Comente a propriedade TimeStamp e a anotação de dados e execute estes comandos:

dotnet ef migrations add RemoveTimeStamp -o Migrations -c AutoLot.Samples.


Atualização do banco de
dados ApplicationDbContext dotnet ef RemoveTimeStamp -c AutoLot.Samples.ApplicationDbContext

Descomente a propriedade TimeStamp e a anotação de dados e execute estes comandos para adicionar a propriedade
para cada tabela como uma coluna de timestamp:

dotnet ef migrations add ReplaceTimeStamp -o Migrations -c AutoLot.Samples.


Atualização do banco de
dados ApplicationDbContext dotnet ef ReplaceTimeStamp -c AutoLot.Samples.ApplicationDbContext

Agora seu banco de dados corresponde ao seu modelo.

Anotações e propriedades de navegação

A anotação ForeignKey permite que o EF Core saiba qual propriedade é o campo de apoio para a propriedade de
navegação. Por convenção, <TypeName>Id seria definido automaticamente como propriedade de chave estrangeira, mas
no exemplo anterior ele foi definido explicitamente. Isso suporta diferentes estilos de nomenclatura, além de ter mais de
uma chave estrangeira para a mesma tabela. Também (na minha opinião honesta) aumenta a legibilidade do código.
InverseProperty informa ao EF Core como as tabelas se relacionam, indicando qual é a propriedade de navegação nas
outras entidades que navegam de volta para essa entidade. InverseProperty é necessário quando uma entidade se relaciona
com outra entidade mais de uma vez e também (novamente, na minha opinião sincera) torna o código mais legível.

A API Fluente
A API Fluent configura as entidades do aplicativo por meio do código C#. Os métodos são expostos pela instância
ModelBuilder disponível no método DbContext OnModelCreating(). A Fluent API é o mais poderoso dos métodos de
configuração e substitui quaisquer convenções ou anotações de dados que estejam em conflito. Algumas opções de
configuração estão disponíveis apenas usando a API Fluent, como definir valores padrão e comportamento em cascata para
propriedades de navegação.

Mapeamento de classes e propriedades

O código a seguir mostra o exemplo Car anterior com a API Fluent equivalente às anotações de dados usadas (omitindo as
propriedades de navegação, que serão abordadas a seguir).

modelBuilder.Entity<Car>(entity =>
{ entity.ToTable("Inventory","dbo");
entity.HasKey(e=>e.Id); entity.HasIndex(e =>
e.MakeId, "IX_Inventory_MakeId" );
entidade.Propriedade(e => e.Cor)

.É necessário()
.HasMaxLength(50);
entidade.Propriedade(e => e.PetName)
.É necessário()
.HasMaxLength(50);
854
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

entidade.Propriedade(e => e.TimeStamp)


.IsRowVersion()
.IsConcurrencyToken();
});

Se você criasse e executasse uma migração agora, descobriria que nada mudou desde o
os comandos na Fluent API correspondem à configuração atual definida pelas convenções e anotações de
dados.

Valores padrão

A Fluent API fornece métodos para definir valores padrão para colunas. O valor padrão pode ser um tipo de valor ou
uma string SQL. Por exemplo, para definir a cor padrão de um carro novo como preto, use o seguinte:

modelBuilder.Entity<Car>(entity => {

...
entidade.Propriedade(e => e.Cor)
.HasColumnName("CarColor")
.É necessário()
.HasMaxLength(50)
.HasDefaultValue("Preto"); });

Para definir o valor para uma função de banco de dados (como getdate()), use o método HasDefaultValueSql().
Presuma que uma propriedade DateTime chamada DateBuilt foi adicionada à classe Car e o valor padrão deve ser a
data atual usando o método getdate() do SQL Server. As colunas são configuradas assim:

modelBuilder.Entity<Car>(entity => {

...
entity.Property(e => e.DateBuilt)
.HasDefaultValueSql("getdate()"); });

Assim como usar SQL para inserir um registro, se uma propriedade que mapeia para uma coluna com um valor padrão tiver um valor
quando o EF Core insere o registro, o valor da propriedade é usado em vez do padrão. Se o valor da propriedade for
nulo, o valor padrão da coluna será usado.
Existe um problema quando o tipo de dados da propriedade tem um valor padrão. Lembre-se de que o padrão dos números é
zero e o padrão dos booleanos é falso. Se você definir o valor das propriedades numéricas como zero ou das propriedades booleanas
como false e inserir essa entidade, o EF Core tratará essa propriedade como não tendo um valor definido. Se essa propriedade for
mapeada para uma coluna com um valor padrão, o valor padrão na definição da coluna será usado.
Por exemplo, adicione uma propriedade booliana chamada IsDrivable à classe Car. Defina o padrão para o mapeamento de
coluna da propriedade como true.

//Car.cs
public class Car : BaseEntity {

...
public bool IsDrivable { get; definir; } }

855
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

//Substituição protegida
por ApplicationDbContext void OnModelCreating(ModelBuilder modelBuilder) {

modelBuilder.Entity<Car>(entity => {

...
entity.Property(e => e.IsDrivable).HasDefaultValue(true); });

Ao salvar um novo registro com IsDrivable = false, o valor será ignorado (já que é o padrão
valor para booleanos) e o padrão do banco de dados será usado. Isso significa que o valor de IsDrivable sempre
será verdadeiro! Uma solução para isso é tornar sua propriedade pública (e, portanto, a coluna) anulável, mas isso pode
não atender às necessidades do negócio.
Outra solução é fornecida pelo EF Core e seu suporte para campos de apoio. Lembre-se de que, se existir um
campo de apoio (e for identificado como o campo de fundo para a propriedade por meio de convenção, anotação de
dados ou API do Fluent), o EF Core usará o campo de apoio para ações de leitura e gravação e não a propriedade pública.
Se você atualizar IsDrivable para usar um campo de apoio anulável (mas manter a propriedade não anulável), o ER Core
fará a leitura/gravação do campo de apoio e não da propriedade. O valor padrão para um booleano anulável é nulo e não falso.
Essa alteração agora faz com que a propriedade funcione conforme o esperado.

classe pública Carro


{
...
bool privado? _é Dirigível;
public bool IsDrivable {

get => _isDrivable ?? verdadeiro;


set => _isDrivable = valor; }

A Fluent API é usada para informar o EF Core do campo de apoio.

modelBuilder.Entity<Car>(entity => {

entidade.Propriedade(p => p.IsDrivable)


.HasField("_isDrivable")
.HasDefaultValue(true);
});

ÿ Observação O método HasField() não é necessário neste exemplo, pois o nome do campo de apoio segue as
convenções de nomenclatura. Incluí-o para mostrar como usar a API Fluent para configurá-lo.

O EF Core converte o campo para a seguinte definição SQL:

CREATE TABLE [dbo].[Inventário](


...
[IsDrivable] [BIT] NÃO NULO,
...
IR

856
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

ALTER TABLE [dbo].[Inventory] ADD DEFAULT (CONVERT([BIT],(1))) FOR [IsDrivable]


IR

Colunas computadas
As colunas também podem ser definidas como computadas com base nos recursos do armazenamento de dados.
Para o SQL Server, duas das opções são calcular o valor com base no valor de outros campos no mesmo registro
ou usar uma função escalar. Por exemplo, para criar uma coluna computada na tabela Inventory que combina os
valores PetName e Color para criar um DisplayName, use a função HasComputedColumnSql().

modelBuilder.Entity<Car>(entity =>
{ entity.Property(p => p.FullName)

.HasComputedColumnSql("[PetName] + ' (' + [Color] + ')'");


});

Novidade no EF Core 5, os valores calculados podem ser mantidos, portanto, o valor é calculado apenas na criação da linha
ou atualização. Embora o SQL Server ofereça suporte a isso, nem todos os armazenamentos de dados o fazem, portanto, verifique a documentação do seu
provedor de banco de dados.

modelBuilder.Entity<Car>(entity => {

entidade.Propriedade(p => p.FullName)


.HasComputedColumnSql("[PetName] + ' (' + [Color] + ')'", stored:true);

});

Relacionamentos um-para-muitos Para usar a

API do Fluent para definir relacionamentos um-para-muitos, escolha uma das entidades a serem atualizadas. Ambos os lados da
cadeia de navegação são definidos em um bloco de código.

modelBuilder.Entity<Car>(entity => {

...
entidade.HasOne(d => d.MakeNavigation)
.ComMuitos(p => p.Carros)
.HasForeignKey(d => d.MakeId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_Inventory_Makes_MakeId");
});

Se você selecionar a entidade principal como base para a configuração da propriedade de navegação, o código
ficará assim:

modelBuilder.Entity<Make>(entity => {

...
entidade.HasMany(e=>e.Cars)
.WithOne(c=>c.MakeNavigation)
.HasForeignKey(c=>c.MakeId)

857
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_Inventory_Makes_MakeId"); });

Relacionamentos um-para-um
As relações um-para-um são configuradas da mesma maneira, exceto que o método WithOne() Fluent API é usado
em vez de WithMany(). Um índice exclusivo é adicionado à entidade dependente. Aqui está o código para o
relacionamento entre as entidades Carro e Rádio usando a entidade dependente (Rádio):

modelBuilder.Entity<Radio>(entidade => {

entidade.HasIndex(e => e.CarId, "IX_Radios_CarId")


.É único();

entidade.HasOne(d => d.CarNavigation)


.WithOne(p => p.RadioNavigation)
.HasForeignKey<Radio>(d => d.CarId);
});

Se o relacionamento for definido em uma entidade principal, um índice exclusivo ainda será adicionado à entidade dependente.
Aqui está o código para o relacionamento entre as entidades Car e Radio usando a entidade principal para o
relacionamento:

modelBuilder.Entity<Radio>(entidade => {

entidade.HasIndex(e => e.CarId, "IX_Radios_CarId")


.É único();
});

modelBuilder.Entity<Car>(entity => {

entidade.HasOne(d => d.RadioNavigation)


.WithOne(p => p.CarNavigation)
.HasForeignKey<Radio>(d => d.CarId);
});

Relacionamentos muitos-para-muitos
As relações muitos-para-muitos são muito mais personalizáveis com a API Fluent. Os nomes de campo de chave estrangeira,
nomes de índice e comportamento em cascata podem ser definidos nas instruções que definem o relacionamento. Aqui
está o exemplo de relacionamento muitos-para-muitos replicado anteriormente usando a API Fluent (os nomes das chaves
e colunas são alterados para torná-los mais legíveis):

modelBuilder.Entity<Car>()
.HasMany(p => p.Drivers)
.ComMuitos(p => p.Carros)
.UsingEntity<Dicionário<string, objeto>>( "CarDriver",
j => j

858
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

.HasOne<Driver>()
.Com muitos()
.HasForeignKey("DriverId")
.HasConstraintName("FK_CarDriver_Drivers_DriverId")
.OnDelete(DeleteBehavior.Cascade), j =>
j .HasOne<Car>()

.Com muitos()
.HasForeignKey("CarId")
.HasConstraintName("FK_CarDriver_Cars_CarId")
.OnDelete(DeleteBehavior.ClientCascade));

Convenções, anotações e a API Fluent, Oh My!


Neste ponto do capítulo, você pode estar se perguntando qual das três opções usar para moldar suas entidades e seu relacionamento
entre si e com o armazenamento de dados. A resposta é todas as três. As convenções estão sempre ativas (a menos que você as
substitua por anotações de dados ou pela API do Fluent). As anotações de dados podem fazer quase tudo que os métodos Fluent API
podem fazer e manter as informações na própria classe de entidade, o que pode aumentar a legibilidade e o suporte do código. A API
Fluent é a mais poderosa das três, mas o código está escondido na classe DbContext. Quer você use anotações de dados ou a Fluent
API, saiba que as anotações de dados anulam as convenções internas e os métodos da Fluent API anulam tudo.

Execução da consulta
As consultas de recuperação de dados são criadas com consultas LINQ gravadas nas propriedades DbSet<T>. A consulta LINQ
é alterada para a linguagem específica do banco de dados (por exemplo, T-SQL) pelo mecanismo de tradução LINQ do provedor de
banco de dados e executada no lado do servidor. Consultas LINQ multiregistro (ou multiregistro potencial) não são executadas até que
a consulta seja iterada (por exemplo, usando um foreach) ou vinculada a um controle para exibição (como uma grade de dados). Essa
execução adiada permite criar consultas no código sem sofrer problemas de desempenho devido à conversa com o banco de dados.

Por exemplo, para obter todos os registros de carros amarelos do banco de dados, execute a seguinte consulta:

var carros = Context.Cars.Where(x=>x.Color == "Amarelo");

Com a execução adiada, esse banco de dados não é realmente consultado até que os resultados sejam iterados. Ter
a consulta é executada imediatamente, use ToList().

var carros = Context.Cars.Where(x=>x.Color == "Yellow").ToList();

Como as consultas não são executadas até serem acionadas, elas podem ser construídas em várias linhas de código. O
seguinte exemplo de código executa o mesmo que o exemplo anterior:

var query = Context.Cars.AsQueryable(); query =


query.Where(x=>x.Color == "Yellow"); var carros = query.ToList();

Consultas de registro único (como ao usar First()/FirstOrDefault()) são executadas imediatamente ao chamar a ação (como
FirstOrDefault()) e as instruções de criação, atualização e exclusão são executadas imediatamente quando o método
DbContext.SaveChanges() É executado.

859
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Avaliação mista cliente-servidor


As versões anteriores do EF Core introduziram a capacidade de combinar execução do lado do servidor e do lado do cliente. Isso
significava que uma função C# poderia ser usada no meio de uma instrução LINQ e essencialmente negar o que descrevi no parágrafo
anterior. A parte até a função C# seria executada no lado do servidor, mas todos os resultados (nesse ponto da consulta) são trazidos
de volta no lado do cliente e, em seguida, o restante da consulta seria executado como LINQ to Objects. Isso acabou causando mais
problemas do que resolvendo, e com o lançamento do EF Core 3.1, essa funcionalidade foi alterada. Agora, apenas o nó final de uma
instrução LINQ pode ser executado no lado do cliente.

Consultas de rastreamento x NoTracking


Quando os dados são lidos do banco de dados em uma instância DbSet<T>, as entidades (por padrão) são rastreadas pelo
rastreador de alterações. Normalmente, isso é o que você deseja em seu aplicativo. Depois que uma instância é rastreada pelo
rastreador de alterações, qualquer outra chamada ao banco de dados para esse mesmo item (com base na chave primária) resultará
em uma atualização do item e não em uma duplicação.
No entanto, pode haver momentos em que você precise obter alguns dados do banco de dados, mas não deseja que eles sejam
rastreados pelo rastreador de alterações. O motivo pode ser o desempenho (rastrear valores originais e atuais para um grande
conjunto de registros pode adicionar pressão de memória) ou talvez você saiba que esses registros nunca serão alterados pela parte
do aplicativo que precisa dos dados.
Para carregar dados em uma instância DbSet<T> sem adicionar os dados ao ChangeTracker, adicione
AsNoTracking() à instrução LINQ. Isso sinaliza ao EF Core para recuperar os dados sem adicioná-los ao ChangeTracker. Por
exemplo, para carregar um registro de carro sem adicioná-lo ao ChangeTracker, execute o seguinte:

carro virtual público? FindAsNoTracking(int id)


=> Table.AsNoTracking().FirstOrDefault(x => x.Id == id);

Isso fornece o benefício de não adicionar a pressão de memória potencial com uma desvantagem potencial:
chamadas adicionais para recuperar o mesmo carro criarão cópias adicionais do registro. À custa de usar mais memória e
ter um tempo de execução um pouco mais lento, a consulta pode ser modificada para garantir que haja apenas uma instância
do carro não mapeado.

carro virtual público? FindAsNoTracking(int id)


=> Table.AsNoTrackingWithIdentityResolution().FirstOrDefault(x => x.Id == id);

Recursos Notáveis do EF Core


Muitos recursos do EF 6 foram replicados no EF Core, com mais sendo adicionados em cada versão. Muitos desses recursos
tiveram grandes melhorias no EF Core, tanto em funcionalidade quanto em desempenho. Além de replicar recursos do EF 6, o EF
Core tem muitos novos recursos que não estão na versão anterior. A seguir estão alguns dos recursos mais notáveis no EF Core
(em nenhuma ordem específica).

ÿ Observação Os exemplos de código nesta seção vêm diretamente da biblioteca de acesso a dados AutoLot concluída
que você criará no próximo capítulo.

860
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Manipulando valores gerados pelo banco de dados

Além do controle de alterações e da geração de consultas SQL a partir do LINQ, uma vantagem significativa de usar o EF Core
sobre o ADO.NET bruto é a manipulação perfeita dos valores gerados pelo banco de dados. Depois de adicionar ou atualizar uma
entidade, o EF Core consulta quaisquer dados gerados pelo banco de dados e atualiza automaticamente a entidade com os
valores corretos. No ADO.NET bruto, você mesmo precisaria fazer isso.
Por exemplo, a tabela Inventory tem uma chave primária inteira definida no SQL Server como uma identidade
coluna. As colunas de identidade são preenchidas pelo SQL Server com um número exclusivo (de uma sequência) quando
um registro é adicionado e não pode ser atualizado durante as atualizações normais (excluindo o caso especial de habilitar
a inserção de identidade). Além disso, a tabela Inventory possui uma coluna Timestamp usada para verificação de
simultaneidade. A verificação de simultaneidade é abordada a seguir, mas, por enquanto, saiba apenas que a coluna
Timestamp é mantida pelo SQL Server e atualizada em qualquer ação de adição ou edição.
Considere, por exemplo, adicionar um novo carro à tabela Inventário. O código a seguir cria uma nova instância Car,
adiciona-a à instância DbSet<Car> no DbContext derivado e chama SaveChanges() para manter os dados:

var carro = carro novo


{
Cor = "Amarelo",
MakeId = 1,
PetName = "Herbie"
};
Context.Carros.Add(carro);
Context.SaveChanges();

Quando SaveChanges é executado, o novo registro é inserido na tabela e, em seguida, o Id e o Timestamp


os valores são retornados da tabela para o EF Core, onde as propriedades da entidade são atualizadas de acordo.

INSERT INTO [Dbo].[Inventory] ([Color], [MakeId], [PetName])


VALORES (N'Amarelo', 1, N'Herbie');
SELECIONE [Id], [TimeStamp]
DE [Dbo].[Inventário]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

ÿ Observe que o EF Core realmente executa consultas parametrizadas, mas simplifiquei todos os exemplos para facilitar a leitura.

Isso também funciona ao adicionar vários itens ao banco de dados. O EF Core sabe como conectar os valores às
entidades corretas. Ao atualizar registros, os valores da chave primária já são conhecidos, portanto, em nosso exemplo Car,
apenas o valor Timestamp atualizado é consultado e retornado.

Verificação de simultaneidade Os

problemas de simultaneidade surgem quando dois processos separados (usuários ou sistemas) tentam atualizar o mesmo
registro aproximadamente ao mesmo tempo. Por exemplo, o Usuário 1 e o Usuário 2 obtêm os dados do Cliente A. O
Usuário 1 atualiza o endereço e salva a alteração. O usuário 2 atualiza a classificação de crédito e tenta salvar o mesmo registro.
Se o salvamento para o Usuário 2 funcionar, as alterações do Usuário 1 serão revertidas, pois o endereço foi alterado após
o Usuário 2 recuperar o registro. Outra opção é falhar ao salvar para o Usuário 2, caso em que as alterações do Usuário 1 são
mantidas, mas as alterações do Usuário 2 não.

861
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

A maneira como essa situação é tratada depende dos requisitos do aplicativo. As soluções vão desde não fazer nada (a
segunda atualização substitui a primeira) até o uso de simultaneidade otimista (a segunda atualização falha) até soluções mais
complicadas, como verificar campos individuais. Exceto pela escolha de não fazer nada (considerado universalmente uma má
ideia de programação), os desenvolvedores precisam saber quando surgem problemas de simultaneidade para que possam ser
tratados adequadamente.
Felizmente, muitos bancos de dados modernos têm ferramentas para ajudar a equipe de desenvolvimento a lidar com a simultaneidade
problemas. O SQL Server tem um tipo de dados interno chamado timestamp, um sinônimo para rowversion. Se uma
coluna for definida com um tipo de dados timestamp, quando um registro for adicionado ao banco de dados, o valor da coluna
será criado pelo SQL Server e, quando um registro for atualizado, o valor da coluna também será atualizado. O valor é
virtualmente garantido como exclusivo e controlado pelo SQL Server.
O EF Core pode aproveitar o tipo de dados de carimbo de data/hora do SQL Server implementando uma propriedade
de carimbo de data/hora em uma entidade (representada como byte[] em C#). As propriedades de entidade definidas com o
atributo Timestamp ou a designação Fluent API são adicionadas à cláusula where ao atualizar ou excluir registros. Em vez de
apenas usar o(s) valor(es) da chave primária, o SQL gerado adiciona o valor da propriedade timestamp à cláusula where. Isso
limita os resultados aos registros em que a chave primária e os valores de registro de data e hora correspondem. Se outro
usuário (ou o sistema) tiver atualizado o registro, os valores do carimbo de data/hora não corresponderão e a instrução de
atualização ou exclusão não atualizará o registro. Aqui está um exemplo de uma consulta de atualização usando a coluna Timestamp:

ATUALIZAÇÃO [Dbo].[Inventário] SET [Cor] = N'Amarelo'


ONDE [Id] = 1 E [TimeStamp] = 0x000000000000081F;

Quando o armazenamento de dados relata um número de registros afetados diferente do número de registros que o
ChangeTracker esperava que fosse alterado, o EF Core lança um DbUpdateConcurrencyException e reverte toda a transação.
DbUpdateConcurrencyException contém informações de todos os registros que não persistiram, incluindo os valores originais
(quando a entidade foi carregada do banco de dados) e os valores atuais (conforme o usuário/sistema os atualizou). Há
também um método para obter os valores atuais do banco de dados (isso requer outra chamada para o servidor). Com essa
riqueza de informações, o desenvolvedor pode lidar com o erro de simultaneidade conforme exigido pelo aplicativo. O código a
seguir mostra isso em ação:

try
{ //
Obtém um registro de carro (não importa qual) var car =
Context.Cars.First(); //Atualiza o banco de dados fora do
contexto Context.Database.ExecuteSqlInterpolated($"Update
dbo.Inventory set Color='Pink' where Id = {car.Id}"); //atualiza o registro do carro no change tracker e tenta salvar as alterações
car.Color = "Yellow"; Context.SaveChanges(); } catch (DbUpdateConcurrencyException ex) {

//Obter a entidade que falhou ao atualizar var entry =


ex.Entries[0]; //Obtém os valores originais (quando a
entidade foi carregada)
PropertyValues originalProps = entrada.OriginalValues; //Obtém os
valores atuais (atualizados por este caminho de código)
PropertyValues currentProps = entrada.CurrentValues; //obtém os
valores atuais do armazenamento de dados – //Observação: isso
requer outra chamada de banco de dados //PropertyValues
databaseProps = entry.GetDatabaseValues(); }

862
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Resiliência de conexão
Erros transitórios são difíceis de depurar e mais difíceis de replicar. Felizmente, muitos provedores de banco de dados têm um mecanismo
de repetição interno para falhas no sistema de banco de dados (problemas de tempdb, limites de usuário etc.) que podem ser aproveitados
pelo EF Core. Para SQL Server, SqlServerRetryingExecutionStrategy detecta erros transitórios (conforme definido pela equipe do SQL
Server) e, se habilitado no DbContext derivado por meio de DbContextOptions, o EF Core repete automaticamente a operação até que o
limite máximo de novas tentativas seja atingido.
Para o SQL Server, existe um método de atalho que pode ser usado para habilitar
SqlServerRetryingExecutionStrategy com todos os padrões. O método usado com SqlServerOptions é EnableRetryOnFailure() e é
demonstrado aqui:

public ApplicationDbContext CreateDbContext(string[] args) { var optionsBuilder = new


DbContextOptionsBuilder<ApplicationDbContext>(); var connectionString =
@"server=.,5433;Database=AutoLot50;User Id=sa;Password=P@ssw0rd;";
optionsBuilder.UseSqlServer(connectionString, opções => opções.EnableRetryOnFailure()); retornar novo
ApplicationDbContext(optionsBuilder.Options); }

O número máximo de novas tentativas e o limite de tempo entre novas tentativas podem ser configurados de acordo com
os requisitos do aplicativo. Se o limite de repetição for atingido sem a conclusão da operação, o EF Core notificará o aplicativo sobre
os problemas de conexão lançando um RetryLimitExceededException. Essa exceção, quando tratada pelo desenvolvedor, pode
repassar as informações pertinentes ao usuário, proporcionando uma melhor experiência.

tente { Context.SaveChanges(); }
catch (RetryLimitExceededException
ex) {

//Ocorreu um erro de limite de repetição //


Deve tratar de forma inteligente
Console.WriteLine($"Limite de repetição excedido! {ex.Message}"); }

Para provedores de banco de dados que não fornecem uma estratégia de execução integrada, estratégias de execução personalizadas
também podem ser criadas. Para obter mais informações, consulte a documentação do EF Core: https://docs.microsoft.com/en-us/ef/core/
miscellaneous/connection-resiliency.

Dados Relacionados

As propriedades de navegação da entidade são usadas para carregar os dados relacionados a uma entidade. Os dados relacionados
podem ser carregados rapidamente (uma instrução LINQ, uma consulta SQL), rapidamente com consultas divididas (uma instrução LINQ,
várias consultas SQL), explicitamente (várias chamadas LINQ, várias consultas SQL) ou preguiçosamente (uma instrução LINQ, várias
consultas SQL sob demanda).
Além da capacidade de carregar dados relacionados usando as propriedades de navegação, o EF Core corrigirá automaticamente as
entidades à medida que forem carregadas no rastreador de alterações. Por exemplo, suponha que todos os registros Make sejam carregados
em DbSet<Make>. Em seguida, todos os registros Car são carregados em DbSet<Car>. Mesmo que os registros tenham sido carregados
separadamente, eles estarão acessíveis entre si por meio das propriedades de navegação.

863
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Carregando ansioso

Carregamento rápido é o termo para carregar registros relacionados de várias tabelas em uma chamada de banco de dados.
Isso é análogo à criação de uma consulta em T-SQL vinculando duas ou mais tabelas com junções. Quando as entidades têm
propriedades de navegação e essas propriedades são usadas nas consultas LINQ, o mecanismo de tradução usa junções para obter
dados das tabelas relacionadas e carrega as entidades correspondentes. Isso geralmente é muito mais eficiente do que executar uma
consulta para obter os dados de uma tabela e, em seguida, executar consultas adicionais para cada uma das tabelas relacionadas. Para
aqueles momentos em que é menos eficiente usar uma consulta, o EF Core 5 introduziu a divisão de consulta, abordada a seguir.
Os métodos Include() e ThenInclude() (para propriedades de navegação subsequentes) são usados para percorrer
as propriedades de navegação em consultas LINQ. Se a relação for necessária, o mecanismo de tradução LINQ
criará uma junção interna. Se a relação for opcional, o mecanismo de tradução criará uma junção à esquerda.
Por exemplo, para carregar todos os registros do carro com suas informações de marca relacionadas, execute o seguinte LINQ
consulta:

var queryable = Context.Cars.IgnoreQueryFilters().Include(c => c.MakeNavigation).ToList();

O LINQ anterior executa a seguinte consulta no banco de dados:

SELECIONE [i].[Id], [i].[Cor], [i].[MakeId], [i].[PetName], [i].[TimeStamp],


[m].[Id], [m].[Nome], [m].[TimeStamp]
DE [Dbo].[Inventário] AS [i]
INNER JOIN [dbo].[Faz] AS [m] ON [i].[MakeId] = [m].[Id]

Várias instruções Include() podem ser usadas na mesma consulta para unir mais de uma entidade ao
original. Para trabalhar na árvore de propriedades de navegação, use ThenInclude() após um Include(). Por exemplo,
para obter todos os registros de Carros com suas informações de Marca e Pedido relacionadas e as informações do
Cliente relacionadas ao Pedido, use a seguinte instrução:

var carros = Context.Cars.Where(c => c.Orders.Any())


.Include(c => c.MakeNavigation)
.Include(c => c.Orders).ThenInclude(o => o.CustomerNavigation).ToList();

Inclusão filtrada
Novo no EF Core 5, os dados incluídos podem ser filtrados e classificados. As operações permitidas na navegação da
coleção são Where(), OrderBy(), OrderByDescending(), ThenBy(), ThenByDescending(), Skip() e Take(). Por exemplo,
se você deseja obter todos os registros Make, mas apenas os registros relacionados Car onde a cor é amarela, você
filtra a propriedade de navegação na expressão lambda, assim:

var query = Context.Makes .Include(x


=> x.Cars.Where(x=>x.Color == "Yellow")).ToList();

A query que é executada é a seguinte:

SELECIONE [m].[Id], [m].[Nome], [m].[TimeStamp], [t].[Id], [t].[Cor],


[t].[MakeId], [t].[PetName], [t].[TimeStamp]
FROM [dbo].[Faz] AS [m]
LEFT JOIN
( SELECT [i].[Id], [i].[Color], [i].[MakeId], [i].[PetName], [i].[TimeStamp]
DE [Dbo].[Inventário] AS [i]
WHERE [i].[Cor] = N'Amarelo') AS [t] ON [m].[Id] = [t].[MakeId]
ORDER POR [m].[Id], [t].[Id]

864
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Carregamento antecipado com consultas divididas

Quando uma consulta LINQ contém muitas inclusões, pode haver um impacto negativo no desempenho. Para
resolver essa situação, o EF Core 5 introduziu consultas divididas. Em vez de executar uma única consulta, o EF
Core dividirá a consulta LINQ em várias consultas SQL e conectará todos os dados relacionados. Por exemplo, a
consulta anterior pode ser esperada como várias consultas SQL adicionando AsSplitQuery() à consulta LINQ, assim:

var query = Context.Makes.AsSplitQuery()


.Include(x => x.Cars.Where(x=>x.Color == "Yellow")).ToList();

As consultas que são executadas são mostradas aqui:

SELECIONE [m].[Id], [m].[Nome], [m].[TimeStamp]


FROM [dbo].[Faz] AS [m]
ORDEM POR [m].[Id]

SELECIONE [t].[Id], [t].[Cor], [t].[MakeId], [t].[PetName], [t].[TimeStamp], [m].[Id]


FROM [dbo].[Faz] AS [m]
INNER JOIN
( SELECT [i].[Id], [i].[Color], [i].[MakeId], [i].[PetName], [i].[TimeStamp]
DE [Dbo].[Inventário] AS [i]
WHERE [i].[Cor] = N'Amarelo'
) AS [t] ON [m].[Id] = [t].[MakeId]
ORDEM POR [m].[Id]

Há uma desvantagem em usar consultas divididas: se os dados mudarem entre a execução das consultas, o
os dados retornados serão inconsistentes.

Carregamento explícito

O carregamento explícito está carregando dados ao longo de uma propriedade de navegação depois que o objeto principal já foi
carregado. Esse processo envolve a execução de uma chamada de banco de dados adicional para obter os dados relacionados. Isso
pode ser útil se seu aplicativo precisar obter seletivamente os registros relacionados e não obter todos os registros relacionados, talvez com
base em alguma ação do usuário.

O processo começa com uma entidade que já está carregada e usando o método Entry() no derivado
DbContext. Ao consultar uma propriedade de navegação de referência (por exemplo, obtendo as informações de marca
de um carro), use o método Reference(). Ao consultar uma propriedade de navegação de coleção, use o método
Collection(). A consulta é adiada até que Load(), ToList() ou uma função agregada (por exemplo, Count(), Max()) seja executada.

Os exemplos a seguir mostram como obter os dados de Marca relacionados, bem como quaisquer registros de Pedidos para um Carro:

//Pega o registro do carro


var car = Context.Cars.First(x => x.Id == 1); //Obtém as
informações do Make Context.Entry(car).Reference(c =>
c.MakeNavigation).Load(); //Obter qualquer pedido ao qual o carro esteja
relacionado Context.Entry(carro).Collection(c =>
c.Orders).Query().IgnoreQueryFilters().Load();

865
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Carregamento lento

O carregamento lento está carregando um registro sob demanda quando uma propriedade de navegação é usada para acessar
um registro relacionado que ainda não foi carregado na memória. O carregamento lento é um recurso do EF 6 que foi adicionado
novamente ao EF Core com a versão 2.1. Embora possa parecer uma boa ideia ativá-lo, habilitar o carregamento lento pode
causar problemas de desempenho em seu aplicativo, fazendo idas e vindas potencialmente desnecessárias ao seu banco de dados.
Por esse motivo, o carregamento lento está desativado por padrão no EF Core (foi habilitado por padrão no EF 6).
O carregamento lento pode ser útil em aplicativos smart client (WPF, WinForms), mas não é recomendável usá-lo
em aplicativos da Web ou de serviço. Por esse motivo, não abordo carregamento lento neste texto. Se quiser saber mais
sobre carregamento lento e como usá-lo com o EF Core, consulte a documentação aqui: https://docs.microsoft.com/en-
us/ef/core/querying/related-data/lazy.

Filtros de consulta global Os filtros de

consulta global permitem que uma cláusula where seja adicionada a todas as consultas LINQ para uma entidade específica. Por
exemplo, um padrão de design de banco de dados comum é usar exclusões reversíveis em vez de exclusões definitivas. Um campo
é adicionado à tabela para indicar o status excluído do registro. Se o registro for “excluído”, o valor é definido como verdadeiro (ou 1),
mas não removido do banco de dados. Isso é chamado de exclusão reversível. Para filtrar os registros excluídos temporariamente das
operações normais, cada cláusula where deve verificar o valor desse campo. Lembrar de incluir esse filtro em todas as consultas pode
ser demorado, se não problemático.
O EF Core permite adicionar um filtro de consulta global a uma entidade que é aplicado a todas as consultas que envolvem
essa entidade. Para o exemplo de exclusão temporária descrito anteriormente, você define um filtro na classe de entidade para
excluir os registros excluídos temporariamente. Quaisquer consultas criadas pelo EF Core envolvendo entidades com filtros de
consulta globais terão seu filtro aplicado. Você não precisa mais se lembrar de incluir a cláusula where em cada consulta.
Mantendo o tema Carro deste livro, presumimos que todos os registros de Carros que não são dirigíveis devem ser
filtrado das consultas normais. Usando a Fluent API, você pode adicionar um filtro de consulta global como este:

modelBuilder.Entity<Car>(entity =>
{ entity.HasQueryFilter(c => c.IsDrivable ==
true) ; });

Com o filtro de consulta global ativado, as consultas envolvendo a entidade Carro filtrarão automaticamente os
carros não dirigíveis. Por exemplo, executando esta consulta LINQ:

var carros = Context.Cars.ToList();

executa o seguinte SQL:

SELECIONE [i].[Id], [i].[Cor], [i].[IsDrivable], [i].[MakeId], [i].[PetName], [i].[TimeStamp]


DE [Dbo].[Inventário] AS [i]
WHERE [i].[IsDrivable] = CAST(1 AS bit)

Se você precisar ver os registros filtrados, adicione IgnoreQueryFilters() ao LINQ da consulta, que
desabilita os filtros de consulta global para cada entidade na consulta LINQ. Executando esta consulta LINQ:

var carros = Context.Cars.IgnoreQueryFilters().ToList();

executa o seguinte SQL:

SELECIONE [i].[Id], [i].[Cor], [i].[IsDrivable], [i].[MakeId], [i].[PetName], [i].[TimeStamp]


DE [Dbo].[Inventário] AS [i]

866
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

É importante observar que chamar IgnoreQueryFilters() remove o filtro de consulta para cada entidade no
Consulta LINQ, incluindo qualquer uma que esteja envolvida nas instruções Include() ou ThenInclude().

Filtros de consulta global nas propriedades de navegação


Os filtros de consulta globais também podem ser definidos nas propriedades de navegação. Suponha que você queira filtrar quaisquer
pedidos que contenham um carro que não pode ser dirigido. O filtro de consulta é criado na propriedade de navegação CarNavigation
da entidade Order, assim:

modelBuilder.Entity<Order>().HasQueryFilter(e => e.CarNavigation.IsDrivable);

Ao executar uma consulta LINQ padrão, quaisquer pedidos que contenham um carro não dirigível serão
excluídos do resultado. Aqui está a instrução LINQ e a instrução SQL gerada:

//C# Code
var orders = Context.Orders.ToList();

/* Consulta SQL gerada */


SELECT [o].[Id], [o].[CarId], [o].[CustomerId], [o].[TimeStamp]
DE [Dbo].[Pedidos] AS [o]
INNER JOIN (SELECT [i].[Id], [i].[IsDrivable]
DE [Dbo].[Inventário] AS [i]
WHERE [i].[IsDrivable] = CAST(1 AS bit)) AS [t]
ON [o].[CarId] = [t].[Id]
WHERE [t].[IsDrivable] = CAST(1 AS bit)

Para remover o filtro de consulta, use IgnoreQueryFilters(). A seguir estão as instruções LINQ atualizadas
e o SQL gerado subsequente:

//C# Code
var orders = Context.Orders.IgnoreQueryFilters().ToList();

/* Consulta SQL gerada */


SELECT [o].[Id], [o].[CarId], [o].[CustomerId], [o].[TimeStamp]
DE [Dbo].[Pedidos] AS [o]

Uma palavra de cautela aqui: o EF Core não detecta filtros de consulta globais cíclicos, portanto, tenha cuidado ao adicionar
filtros de consulta às propriedades de navegação.

Carregamento explícito com filtros de consulta global


Os filtros de consulta globais também estão em vigor ao carregar dados relacionados explicitamente. Por exemplo, se
você quiser carregar os registros de carro para uma marca, o filtro IsDrivable impedirá que carros não dirigíveis sejam
carregados na memória. Tome o trecho de código a seguir como exemplo:

var make = Context.Makes.First(x => x.Id == makeId);


Context.Entry(make).Collection(c=>c.Cars).Load();

867
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Até agora não deveria ser nenhuma surpresa que a consulta SQL gerada inclui o filtro para carros não dirigíveis.

SELECIONE [i].[Id], [i].[Cor], [i].[IsDrivable], [i].[MakeId], [i].


[PetName], [i].[TimeStamp]
DE [Dbo].[Inventário] AS [i]
ONDE ([i].[IsDrivable] = CAST(1 bit AS)) AND ([i].[MakeId] = 1

Há um pequeno problema em ignorar filtros de consulta ao carregar dados explicitamente. O tipo retornado
pelo método Collection() é CollectionEntry<Make,Car> e não implementa explicitamente a interface IQueryable<T>.
Para chamar IgnoreQueryFilters(), você deve primeiro chamar Query(), que retorna um IQueryable<Car>.

var make = Context.Makes.First(x => x.Id == makeId);


Context.Entry(make).Collection(c=>c.Cars).Query().IgnoreQueryFilters().Load();

O mesmo processo se aplica ao usar o método Reference() para recuperar dados de uma propriedade de
navegação de referência.

Consultas SQL brutas com LINQ


Às vezes, obter a instrução LINQ correta para uma consulta complicada pode ser mais difícil do que apenas escrever o
SQL diretamente. Felizmente, o EF Core tem um mecanismo para permitir que instruções SQL brutas sejam executadas em
um DbSet<T>. Os métodos FromSqlRaw() e FromSqlRawInterpolated() usam uma string que se torna a base da consulta
LINQ. Esta consulta é executada no lado do servidor.
Se a instrução SQL bruta não terminar (por exemplo, nem um procedimento armazenado nem uma função definida pelo usuário
nem uma instrução que usa uma expressão de tabela comum ou termina com um ponto e vírgula), instruções LINQ
adicionais podem ser adicionadas à consulta. As instruções LINQ adicionais, como as cláusulas Include(), OrderBy() ou
Where(), serão combinadas com a chamada SQL bruta original e qualquer filtro de consulta global, e toda a consulta será
executada no lado do servidor.
Ao usar uma das variantes FromSql, a consulta deve ser escrita usando o esquema de armazenamento de dados
e nome da tabela, e não os nomes da entidade. FromSqlRaw() enviará a string exatamente como está escrita.
FromSqlInterpolated() usa interpolação de cadeia de caracteres C# e cada cadeia de caracteres interpolada é traduzida
no parâmetro SQL. Você deve usar a versão interpolada sempre que estiver usando variáveis para a proteção adicional
inerente às consultas parametrizadas.
Presumindo que o filtro de consulta global esteja definido na entidade Car, a instrução LINQ a seguir obterá o primeiro
registro de inventário onde o Id é um, inclua os dados de marca relacionados e filtre os carros não dirigíveis:

var carro = Context.Carros


.FromSqlInterpolated($"Selecione * de dbo.Inventory where Id = {carId}")
.Include(x => x.MakeNavigation)
.Primeiro();

O mecanismo de tradução LINQ to SQL combina a instrução SQL bruta com o resto do LINQ
declarações e executa a seguinte consulta:

SELECT TOP(1) [c].[Id], [c].[Color], [c].[IsDrivable], [c].[MakeId], [c].[PetName], [c].[ TimeStamp],


[m].[Id], [m].[Nome], [m].[TimeStamp]

FROM (Selecione * de dbo.Inventory onde Id = 1) AS [c]


INNER JOIN [dbo].[Faz] AS [m] ON [c].[MakeId] = [m].[Id]
WHERE [c].[IsDrivable] = CAST(1 AS bit)

868
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Saiba que existem algumas regras que devem ser observadas ao usar SQL bruto com LINQ.

• A consulta SQL deve retornar dados para todas as propriedades do tipo entidade.

• Os nomes das colunas devem corresponder às propriedades às quais são mapeados (uma melhoria em relação
ao EF 6, em que os mapeamentos eram ignorados).

• A consulta SQL não pode conter dados relacionados.

Lote de instruções O EF Core melhorou

significativamente o desempenho ao salvar alterações no banco de dados executando as instruções em um ou mais lotes. Isso
diminui as viagens entre o aplicativo e o banco de dados, aumentando o desempenho e potencialmente reduzindo o custo (por
exemplo, para bancos de dados em nuvem onde as transações são cobradas).

O EF Core agrupa as instruções de criação, atualização e exclusão usando parâmetros com valor de tabela. O número
de instruções que os lotes do EF dependem do provedor de banco de dados. Por exemplo, para o SQL Server, o lote é ineficiente
abaixo de 4 instruções e acima de 40. Independentemente do número de lotes, todas as instruções ainda são executadas em uma
transação. O tamanho do lote também pode ser configurado por meio de DbContextOptions, mas a recomendação é permitir que o EF
Core calcule o tamanho do lote para a maioria (se não todas) das situações.
Se você inserir quatro carros em uma transação como esta:

var carros = new Lista<Carro> {

new Car { Color = "Yellow", MakeId = 1, PetName = "Herbie" }, new Car { Color = "White",
MakeId = 2, PetName = "Mach 5" }, new Car { Color = "Pink", MakeId = 3, PetName =
"Avon" }, new Car { Color = "Blue", MakeId = 4, PetName = "Blueberry" }, };
Context.Cars.AddRange(carros); Context.SaveChanges();

O EF Core agruparia as instruções em uma única chamada. A consulta gerada é mostrada aqui:

exec sp_executesql N'SET NOCOUNT ON; DECLARE


@inserted0 TABLE ([Id] int, [_Position] [int]); MERGE [Dbo].[Inventário] USANDO
( VALUES (@p0, @p1, @p2, 0), (@p3, @p4, @p5, 1), (@p6, @p7, @p8, 2),
(@p9, @p10, @p11, 3)) AS i ([Color], [MakeId], [PetName], _Position) ON 1=0
QUANDO NÃO CORRESPONDER ENTÃO INSERIR ([Color], [MakeId], [PetName ])

VALUES (i.[Color], i.[MakeId], i.[PetName])


OUTPUT INSERTED.[Id], i._Position INTO
@inserted0;

SELECT [t].[Id], [t].[IsDrivable], [t].[TimeStamp] FROM [Dbo].[Inventory] t INNER JOIN @inserted0 i ON ([t].[Id] =
[i ].[Eu ia])
ORDEM POR [i].[_Posição];

869
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

',N'@p0 nvarchar(50),@p1 int,@p2 nvarchar(50),@p3 nvarchar(50),@p4 int,@p5 nvarchar(50),@p6 nvarchar(50),@p7


int ,@p8 nvarchar(50),@p9 nvarchar(50),@p10 int,@p11 nvarchar(50)',@ p0=N'Amarelo',@p1=1,@p2=N'Herbie',@p3
=N'Branco',@p4=2,@p5=N'Mach 5',@p6=N'Rosa',@p7=3,@ p8=N'Avon',@p9=N'Azul',@ p10=4,@p11=N'Amora'

Tipos de entidade de propriedade O

uso de uma classe C# como uma propriedade em uma entidade para definir uma coleção de propriedades para outra entidade
foi introduzido pela primeira vez na versão 2.0 e continuamente atualizado. Quando os tipos marcados com o atributo [Owned]
(ou configurados com a Fluent API) são adicionados como uma propriedade de uma entidade, o EF Core adicionará todas as
propriedades da classe de entidade [Owned] à entidade proprietária. Isso aumenta a possibilidade de reutilização do código C#.
Nos bastidores, o EF Core considera isso uma relação de um para um. A classe proprietária é a entidade
dependente e a classe proprietária é a entidade principal. A classe proprietária, embora seja considerada uma entidade,
não pode existir sem a entidade proprietária. Os nomes de coluna padrão do tipo de propriedade serão formatados como
NavigationPropertyName_OwnedEntityPropertyName (por exemplo, PersonalNavigation_FirstName). Os nomes padrão
podem ser alterados usando a API Fluent.
Pegue esta classe Person (observe o atributo Owned):

[Propriedade] public
class Pessoa {
[Obrigatório, StringLength(50)] public
string FirstName { get; definir; } = "Novo";
[Obrigatório, StringLength(50)] public
string LastName { get; definir; } = "Cliente"; }

Isso é usado pela classe Cliente:

[Table("Clientes", Schema = "Dbo")] classe


parcial pública Cliente: BaseEntity {

public Person PersonalInformation { get; definir; } = new Pessoa(); [JsonIgnore]

[InverseProperty(nameof(CreditRisk.CustomerNavigation))] public
IEnumerable<CreditRisk> CreditRisks { get; definir; } = new List<CreditRisk>(); [JsonIgnore]

[InverseProperty(nameof(Order.CustomerNavigation))] public
IEnumerable<Order> Orders { get; definir; } = new List<Ordem>(); }

Por padrão, as duas propriedades Person são mapeadas para colunas denominadas PersonalInformation_
FirstName e PersonalInformation_LastName. Para alterar isso, adicione o seguinte código Fluent API ao método
OnConfiguring():

modelBuilder.Entity<Cliente>(entidade => {

entidade.OwnsOne(o => o.PersonalInformation,


pd =>
{

870
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

pd.Property<string>(nameof(Person.FirstName))
.HasColumnName(nome da(Pessoa.Nome))
.HasColumnType("nvarchar(50)");
pd.Property<string>(nameof(Person.LastName))
.HasColumnName(nameof(Pessoa.LastName))
.HasColumnType("nvarchar(50)");
});
});

A tabela resultante é criada assim (observe que a nulidade das colunas FirstName e LastName
não corresponde às anotações de dados na entidade de propriedade da Pessoa):

CREATE TABLE [dbo].[Clientes](


[Id] [int] IDENTIDADE(1,1) NÃO NULO,
[FirstName] [nvarchar](50) NULL,
[Sobrenome] [nvarchar](50) NULL,
[TimeStamp] [timestamp] NULL,
[Nome Completo] AS (([Sobrenome]+', ')+[Nome]),
CONSTRAINT [PK_Customers] PRIMARY KEY CLUSTERED
(
[ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS
= ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) EM [PRIMÁRIO]
IR

O EF Core 5 corrige um problema com entidades de propriedade que podem não aparecer para você, mas podem ser um
problema significativo. Observe que a classe Person tem a anotação de dados Required em ambas as propriedades, mas as colunas
do SQL Server são definidas como NULL. Isso ocorre devido a um problema com a forma como o sistema de migração traduz as
entidades de propriedade quando elas são usadas com um relacionamento opcional. A correção é tornar o relacionamento necessário.

Para corrigir isso, existem algumas opções. A primeira é habilitar a nulidade do C# (no nível do projeto ou nas classes).
Isso torna a propriedade de navegação PersonalInformation não anulável, que o EF Core honra e, por sua vez, o EF Core
configura apropriadamente as colunas na entidade de propriedade. A outra opção é adicionar uma instrução Fluent API
para tornar a propriedade de navegação obrigatória.

modelBuilder.Entity<Cliente>(entidade => {

entidade.OwnsOne(o => o.PersonalInformation,


pd =>
{
pd.Property<string>(nameof(Person.FirstName))
.HasColumnName(nome da(Pessoa.Nome))
.HasColumnType("nvarchar(50)");
pd.Property<string>(nameof(Person.LastName))
.HasColumnName(nameof(Pessoa.LastName))
.HasColumnType("nvarchar(50)");
});
entity.Navigation(c => c.PersonalInformation).IsRequired(true); });

871
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Existem opções adicionais para explorar com entidades de propriedade, incluindo coleções, divisão de tabela e
nidificação. Tudo isso está além do escopo deste livro. Para obter mais informações, consulte a documentação do EF Core
sobre entidades de propriedade aqui: https://docs.microsoft.com/en-us/ef/core/modeling/owned entitys.

Mapeamento de função de banco de dados As

funções do SQL Server podem ser mapeadas para métodos C# e incluídas em instruções LINQ. O método C# é apenas um
espaço reservado, pois a função do servidor é dobrada no SQL gerado para a consulta. O suporte para mapeamento de função
com valor de tabela foi adicionado no EF Core ao suporte já existente para mapeamento de função escalar. Para obter mais
informações sobre mapeamento de função de banco de dados, consulte a documentação: https://docs. microsoft.com/en-us/ef/
core/querying/user-defined-function-mapping.

Os comandos da CLI da ferramenta global do EF Core


A ferramenta CLI global dotnet-ef EF Core contém os comandos necessários para estruturar bancos de dados existentes em
código, para criar/remover migrações de banco de dados e para operar em um banco de dados (atualizar, descartar, etc.). Antes
de poder usar as ferramentas globais dotnet-ef, elas devem ser instaladas com o seguinte comando (se você estava acompanhando
anteriormente neste capítulo, você já fez isso):

instalação da ferramenta dotnet --global dotnet-ef --versão 5.0.1

ÿ Observação Como o EF Core 5 não é uma versão com suporte de longo prazo, para usar as ferramentas globais do EF Core 5, você deve

especificar uma versão.

Para testar a instalação, abra um prompt de comando e digite o seguinte comando:

dotnet ef

Se o tooling for instalado com sucesso, você obterá o EF Core Unicorn (o mascote do time) e a lista de comandos
disponíveis, assim (o unicórnio fica melhor na tela): _/\__ ---==/ \\ |. \|\ |__||__| | ) \\\ |_||_| \_/ | //|\\ |__ ||_| \\\/\\

___ ___

Ferramentas de linha de comando do Entity Framework Core .NET 5.0.1

Uso: dotnet ef [opções] [comando]

Opções: --
version -h|-- Mostrar informações de versão
help -v|-- Mostrar informações de ajuda
verbose --no- Mostrar saída detalhada.
color --prefix- Não colorize a saída.
output Saída do prefixo com nível.

872
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Comandos:
database Comandos para gerenciar o banco de dados.
dbcontext Comandos para gerenciar tipos de DbContext. migrations
Comandos para gerenciar migrações.

Use "dotnet ef [command] --help" para obter mais informações sobre um comando.

A Tabela 22-9 descreve os três comandos principais na ferramenta global EF Core. Cada comando principal possui
subcomandos adicionais. Assim como todos os comandos do .NET Core, cada comando tem um sistema de ajuda avançado que
pode ser acessado digitando -h junto com o comando.

Tabela 22-9. Comandos de ferramentas principais do EF

Comando Significado na Vida

Banco de Dados Comandos para gerenciar o banco de dados. Os subcomandos incluem drop e update.

DbContext Comandos para gerenciar os tipos de DbContext. Os subcomandos incluem scaffold, list e info.

Migrações Comandos para gerenciar migrações. Os subcomandos incluem adicionar, listar, remover e script.

Os comandos do EF Core são executados em arquivos de projeto .NET Core (e não em arquivos de solução). o projeto alvo
precisa fazer referência ao pacote NuGet de ferramentas do EF Core Microsoft.EntityFrameworkCore.Design. Os comandos
operam no arquivo de projeto localizado no mesmo diretório em que os comandos são executados ou em um arquivo de
projeto em outro diretório, se referenciado por meio das opções de linha de comando.
Para os comandos EF Core CLI que precisam de uma instância de uma classe DbContext derivada (banco de dados e
Migrations), se houver apenas um no projeto, esse será usado. Se houver mais de um, o DbContext precisará ser
especificado nas opções de linha de comando. A classe DbContext derivada será instanciada usando uma instância de uma
classe que implementa a interface IDesignTimeDbContextFactory<TContext> se uma puder ser localizada. Se o conjunto de
ferramentas não conseguir encontrar um, o DbContext derivado será instanciado usando o construtor sem parâmetros. Se
nenhum deles existir, o comando falhará. Observe que a opção de construtor sem parâmetros requer a existência da substituição
OnConfiguring, que não é considerada uma boa prática. A melhor (e realmente única) opção é sempre criar um
IDesignTimeDbContextFactory<TCo ntext> para cada DbContext derivado que você possui em seu aplicativo.

Existem opções comuns disponíveis para os comandos do EF Core, mostradas na Tabela 22-10. Muitos dos
comandos têm opções ou argumentos adicionais.

Tabela 22-10. Opções de comando do EF Core

Opção (taquigrafia || Longhand) --c || Significado na

--contexto <DBCONTEXT> vida A classe DbContext derivada totalmente qualificada a ser usada. Se
existir mais de um DbContext derivado no projeto, esta é uma opção
obrigatória.

-p || --projeto <PROJETO> O projeto a ser usado (onde colocar os arquivos). O padrão é o diretório
de trabalho atual.

-s || --startup-project <PROJECT> O projeto de inicialização a ser usado (contém o DbContext derivado).


O padrão é o diretório de trabalho atual.

-h || --ajuda -v || Exibe a ajuda e todas as opções.

--verbose Mostra a saída detalhada.

873
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Para listar todos os argumentos e opções de um comando, digite dotnet ef <command> -h em uma janela de comando, como esta:

dotnet ef migrations add -h

ÿ Observação É importante observar que os comandos CLI não são comandos C#, portanto, as regras de escape de barras e
aspas não se aplicam.

Os comandos de migração
Os comandos migrations são usados para adicionar, remover, listar e migrações de script. À medida que as migrações são aplicadas a uma
base, um registro é criado na tabela __EFMigrationsHistory. A Tabela 22-11 descreve os comandos. As seções a seguir explicam os
comandos em detalhes.

Tabela 22-11. Comandos de migração do EF Core

Comando Significado na vida


Adicionar
Cria uma nova migração com base nas alterações da migração anterior

Remover Verifica se a última migração no projeto foi aplicada ao banco de dados e, se não, exclui o arquivo de migração
(e seu designer) e, em seguida, reverte a classe de instantâneo para a migração anterior

Lista Lista todas as migrações para um DbContext derivado e seu status (aplicado ou pendente)

Roteiro Cria um script SQL para todas, uma ou várias migrações

O Comando Adicionar

O comando add cria uma nova migração de banco de dados com base no modelo de objeto atual. O processo examina cada
entidade com uma propriedade DbSet<T> no DbContext derivado (e cada entidade que pode ser acessada dessas entidades usando
propriedades de navegação) e determina se há alguma alteração que precise ser aplicada ao banco de dados. Se houver alterações, o código
adequado é gerado para atualizar o banco de dados.
Você aprenderá mais sobre isso em breve.
O comando Adicionar requer um argumento de nome, que é usado para nomear a classe de criação e os arquivos para o
migração. Além das opções comuns, a opção -o <PATH> ou –output-dir <PATH> indica para onde os arquivos de migração devem ir.
O diretório padrão é denominado Migrations em relação ao caminho atual.
Cada migração adicionada cria dois arquivos parciais da mesma classe. Ambos os arquivos iniciam seus nomes com um registro
de data e hora e o nome da migração é usado como argumento para o comando add. O primeiro arquivo é denominado
<YYYYMMDDHHMMSS>_<MigrationName>.cs e o segundo é denominado <YYYYMMDDHHMMSS>_<MigrationName>.
Designer.cs. O carimbo de data/hora é baseado em quando o arquivo foi criado e corresponderá exatamente a ambos os arquivos.
O primeiro arquivo representa o código gerado para as alterações do banco de dados nesta migração , e o arquivo de designer representa
o código para criar e atualizar o banco de dados com base em todas as migrações até e incluindo esta.
O arquivo principal contém dois métodos, Up() e Down(). O método Up() contém o código a ser atualizado
o banco de dados com as alterações dessa migração e o método Down() contém o código para reverter as alterações dessa
migração. Uma lista parcial da migração inicial do início deste capítulo (a migração One2Many) está listada aqui:

874
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

classe parcial pública One2Many : Migração {

substituição protegida void Up(MigrationBuilder migrationBuilder) {

migrationBuilder.CreateTable( nome:
"Criar", colunas: tabela => novo

{
Id = table.Column<int>(type: "int", anulável: false)
.Annotation("SqlServer:Identity", "1, 1"), Name =
table.Column<string>(type: "nvarchar(max)", anulável: true), TimeStamp =
table.Column<byte[]>( type: "varbinary(max)", nullable: true) }, constraints: table => {

table.PrimaryKey("PK_Make", x => x.Id); });

...
migrationBuilder.CreateIndex( nome:
"IX_Cars_MakeId", tabela:
"Carros", coluna: "MakeId");

substituição protegida void Down(MigrationBuilder migrationBuilder) {

migrationBuilder.DropTable(nome: "Carros");
migrationBuilder.DropTable(nome: "Make"); }

Como você pode ver, o método Up() está criando tabelas, colunas, índices, etc. O método Down() está
descartando os itens criados. O mecanismo de migração emitirá instruções alter, add e drop conforme necessário para
garantir que o banco de dados corresponda ao seu modelo.
O arquivo de designer contém dois atributos que vinculam esses parciais ao nome do arquivo e ao derivado
DbContext. Os atributos são mostrados aqui com uma lista parcial da classe de design:

[DbContext(typeof(ApplicationDbContext))]
[Migration("20201230020509_One2Many")]
classe parcial One2Many {

substituição protegida void BuildTargetModel(ModelBuilder modelBuilder) {

...
}
}

A primeira migração cria um arquivo adicional no diretório de destino nomeado para o DbContext derivado no
formato <DerivedDbContextName>ModelSnapshot.cs. O formato deste arquivo é o mesmo da parcial do designer e
contém o código que é a soma de todas as migrations. Quando as migrações são adicionadas ou removidas, esse
arquivo é atualizado automaticamente para corresponder às alterações.

875
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

ÿ Observação É extremamente importante que você não exclua os arquivos de migração manualmente. Isso fará com
que <Deri vedDbContext>ModelSnapshot.cs fique fora de sincronia com suas migrações, essencialmente quebrando-
as. Se você for excluí-los manualmente, exclua todos e comece de novo. Para remover uma migração, use o comando
remove , abordado brevemente.

Excluindo Tabelas das Migrações


Se uma entidade for compartilhada entre vários DbContexts, cada DbContext criará código nos arquivos de migração para
qualquer alteração nessa entidade. Isso causa um problema, pois o segundo script de migração falhará se as alterações já
estiverem presentes no banco de dados. Antes do EF Core 5, a única solução era editar manualmente um dos arquivos de
migração para remover essas alterações.
No EF Core 5, um DbContext pode marcar uma entidade como excluída das migrações, permitindo que o outro
DbContext se torne o sistema de registro dessa entidade. O código a seguir mostra uma entidade sendo excluída das migrações:

substituição protegida void OnModelCreating(ModelBuilder modelBuilder)


{ modelBuilder.Entity<LogEntry>().ToTable("Logs", t => t.ExcludeFromMigrations()); }

O Comando Remover
O comando remove é usado para remover migrações do projeto e sempre opera na última migração (com base nos
timestamps das migrações). Ao remover uma migração, o EF Core garantirá que ela não tenha sido aplicada verificando a
tabela __EFMigrationsHistory no banco de dados. Se a migração tiver sido aplicada, o processo falhará. Se a migração ainda
não foi aplicada ou foi revertida, a migração é removida e o arquivo de instantâneo do modelo é atualizado.

O comando remove não aceita nenhum argumento (já que sempre funciona na última migração) e
usa as mesmas opções do comando add. Há uma opção adicional, a opção force (-f || --force). Isso reverterá a última
migração e a removerá em uma etapa.

O Comando Listar
O comando list é usado para mostrar todas as migrações para um DbContext derivado. Por padrão, ele listará todas as
migrações e consultará o banco de dados para determinar se elas foram aplicadas. Caso não tenham sido aplicadas, serão
listadas como pendentes. Existe uma opção para passar uma cadeia de conexão específica e outra opção para não se conectar
ao banco de dados e, em vez disso, apenas listar as migrações. A Tabela 22-12 mostra essas opções.

Tabela 22-12. Opções adicionais para o comando de lista de migrações do EF Core

Opção (taquigrafia || Longhand) Significado na vida --


connection <CONNECTION> String de conexão com
banco de o
dados. O padrão é aquele especificado na instância de
IDesignTimeDbContextFactory ou no método OnConfiguring de DbContext.

--no-connect Instrui o comando a ignorar a verificação do banco de dados.

876
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

O comando do script
O comando script cria um script SQL com base em uma ou mais migrações. O comando usa dois argumentos opcionais que
representam a migração inicial e a migração final. Se nenhum for inserido, todas as migrações serão com script. A Tabela 22-13
descreve os argumentos.

Tabela 22-13. Argumentos para o comando de script de migrações do EF Core

Argumento Significado na
<DE> vida A migração inicial. O padrão é 0 (zero), a migração inicial.

<TO> A migração de destino. O padrão é a última migração.

Se nenhuma migração for nomeada, o script criado será o total cumulativo de todas as migrações. Se nomeado
forem fornecidas migrações, o script conterá as alterações entre as duas migrações (inclusive). Cada migração é agrupada em
uma transação. Caso a tabela __EFMigrationsHistory não exista no banco de dados onde o script é executado, ela será criada. A
tabela também será atualizada para corresponder às migrações que foram executadas. Alguns exemplos são mostrados aqui:

//Script de todas as migrações dotnet ef


migrations script //script do início até as
migrações Many2Many dotnet ef migrations script 0 Many2Many

Existem algumas opções adicionais disponíveis, conforme mostrado na Tabela 22-14. A opção -o permite especificar um
arquivo para o script (o diretório é relativo ao local onde o comando é executado) e -i cria um script idempotente. Isso significa que
ele contém verificações para ver se uma migração já foi aplicada e pula essa migração se tiver. A opção –no-transaction desativa as
transações normais que são incluídas no script.

Tabela 22-14. Opções adicionais para o comando de script de migrações do EF Core

Opção (taquigrafia || Longhand) Significado na vida -o ||

-saída <ARQUIVO> O arquivo para gravar o script resultante para

-i || --idempotente Gera um script que verifica se uma migração já foi aplicada antes de aplicá-la

--sem transações Não envolve cada migração em uma transação

Os comandos do banco de dados


Existem dois comandos de banco de dados, drop e update. O comando drop exclui o banco de dados se ele existir.
O comando update atualiza o banco de dados usando migrations.

O Comando Soltar
O comando drop descarta o banco de dados especificado pela string de conexão na fábrica de contexto do método
OnConfiguring de DbContext. Usar a opção forçar não solicita confirmação e forçar fecha todas as conexões. Consulte a Tabela 22-15.

877
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Tabela 22-15. Opções de eliminação do banco de dados do EF Core

Opção (taquigrafia || Longhand) Significado na vida -f || --força

Não confirme a queda. Forçar o fechamento de todas as conexões.

--funcionamento a seco Mostre qual banco de dados será descartado, mas não o descarte.

O comando de atualização do banco de dados


O comando update usa um argumento (o nome da migração) e as opções usuais. O comando tem uma opção adicional, --connection
<CONNECTION>. Isso permite o uso de uma cadeia de conexão que não está configurada na fábrica de tempo de design ou DbContext.

Se o comando for executado sem um nome de migração, o comando atualizará o banco de dados para a migração mais recente,
criando o banco de dados se necessário. Se uma migração for nomeada, o banco de dados será atualizado para essa migração. Todas as
migrações anteriores que ainda não foram aplicadas também serão aplicadas. À medida que as migrações são aplicadas, seus nomes são
armazenados na tabela __EFMigrationsHistory.
Se a migração nomeada tiver um registro de data e hora anterior a outras migrações aplicadas, todas as migrações
posteriores serão revertidas. Se um 0 (zero) for passado como a migração nomeada, todas as migrações serão revertidas, deixando
um banco de dados vazio (exceto para a tabela __EFMigrationsHistory).

Os Comandos DbContext
Existem quatro comandos DbContext. Três deles (lista, informações, script) operam em classes DbContext derivadas em seu projeto. O
comando scaffold cria um DbContext derivado e entidades de um banco de dados existente. A Tabela 22-16 mostra os quatro comandos.

Tabela 22-16. Os Comandos DbContext

Comando Significado na vida


Informações
Obtém informações sobre um tipo DbContext

Lista Lista os tipos de DbContext disponíveis


Andaime Scaffolds um DbContext e tipos de entidade para um banco de dados

Roteiro Gera script SQL a partir do DbContext com base no modelo de objeto, ignorando quaisquer migrações

Os comandos list e info têm as opções usuais disponíveis. O comando list lista os derivados
Classes DbContext no projeto de destino. O comando info fornece detalhes sobre a classe DbContext derivada especificada, incluindo
a string de conexão, nome do provedor, nome do banco de dados e fonte de dados. O comando script cria um script SQL que cria seu
banco de dados com base no modelo de objeto, ignorando quaisquer migrações que possam estar presentes. O comando scaffold é
usado para fazer engenharia reversa de um banco de dados existente e é abordado na próxima seção.

O comando DbContext Scaffold


O comando scaffold cria as classes C# (DbContext e entidades derivadas) completas com anotações de dados (se solicitado)
e comandos Fluent API de um banco de dados existente. Existem dois argumentos obrigatórios, a string de conexão do banco de
dados e o provedor totalmente qualificado (por exemplo, Microsoft.
EntityFrameworkCore.SqlServer). A Tabela 22-17 descreve os argumentos.

878
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Tabela 22-17. Os argumentos do andaime DbContext

Argumento Significado na vida


Conexão A string de conexão com o banco de dados

Fornecedor O provedor de banco de dados EF Core a ser usado (por exemplo, Microsoft.EntityFrameworkCore.SqlServer)

As opções disponíveis incluem a seleção de esquemas e tabelas específicos, o nome e o namespace da classe de
contexto criada, o diretório de saída e o namespace das classes de entidade geradas e muito mais. As opções padrão também
estão disponíveis. As opções estendidas estão listadas na Tabela 22-18, com discussão a seguir.

Tabela 22-18. As opções do andaime DbContext

Opção (taquigrafia || Longhand) -d || Significado na

--data-annotations vida Use atributos para configurar o modelo (quando possível). Se omitido,
apenas a API Fluent é usada.

-c || --contexto <NOME> O nome do DbContext derivado a ser criado.

--context-dir <CAMINHO> O diretório para colocar o DbContext derivado, relativo ao diretório do


projeto. O padrão é o nome do banco de dados.

-f || --force -o || -- Substitui quaisquer arquivos existentes no diretório de destino.

output-dir <CAMINHO> O diretório para colocar as classes de entidade geradas. Relativo ao diretório
do projeto.

--schema <SCHEMA_NAME>... -t Os esquemas das tabelas para as quais gerar tipos de entidade.

|| --tabela <TABLE_NAME>... As tabelas para as quais gerar tipos de entidade.


--use-database-names Use os nomes de tabela e coluna diretamente do banco de dados.

-n | --namespaces <NAMESPACE> O namespace para as classes de entidade geradas. Corresponde ao


diretório por padrão. --context-namespace <NAMESPACE> O namespace

para a classe DbContext derivada gerada.


Corresponde ao diretório por padrão.

--no-onconfiguring --no- Não gera método OnConfiguring.

pluralize Não usa o pluralizador.

O comando scaffold ficou muito mais robusto com o EF Core 5.0. Como você pode ver, existem
muitas opções para escolher. Se a opção de anotações de dados (-d) for selecionada, o EF Core usará anotações de dados
onde puder e preencherá as diferenças com a API do Fluent. Se essa opção não for selecionada, toda a configuração (quando
diferente das convenções) é codificada na API do Fluent. Você pode especificar o namespace, o esquema e o local para as
entidades geradas e os arquivos DbContext derivados. Se você não deseja fazer scaffold em todo o banco de dados, pode
selecionar determinados esquemas e tabelas. A opção --no-onconfiguring elimina o método OnConfiguring() da classe scaffolded,
e a opção –no-pluralize desliga o pluralizador, que transforma entidades singulares (Car) em tabelas plurais (Cars) ao criar
migrações e transforma tabelas plurais em entidades únicas ao montar andaimes.

879
Machine Translated by Google

Capítulo 22 ÿ Apresentando o Entity Framework Core

Resumo
Este capítulo iniciou a jornada para o Entity Framework Core. Este capítulo examinou os fundamentos do EF Core, como
as consultas são executadas e o controle de alterações. Você aprendeu sobre como moldar seu modelo, as convenções
do EF Core, anotações de dados e a API Fluent e como usá-los afeta o design do seu banco de dados. A seção final
abordou o poder da interface de linha de comando do EF Core e das ferramentas globais.
Embora este capítulo aborde muita teoria e algum código, o próximo capítulo é quase todo código com um pouco
pouco de teoria. Ao terminar o Capítulo 23, você terá a camada de acesso aos dados do AutoLot concluída.

880
Machine Translated by Google

CAPÍTULO 23

Crie uma camada de acesso a


dados com o Entity Framework Core

O capítulo anterior abordou os detalhes do EF Core e seus recursos. Este capítulo se concentra em aplicar o que você aprendeu sobre
o EF Core para criar a camada de acesso a dados AutoLot. Você começa o capítulo montando as entidades e o DbContext derivado do
banco de dados do capítulo anterior. Em seguida, o projeto é alterado do banco de dados primeiro para o código primeiro, e as entidades
são atualizadas para sua versão final e aplicadas ao banco de dados usando migrações do EF Core. A alteração final no banco de
dados é recriar o procedimento armazenado GetPetName e criar uma nova exibição de banco de dados (completa com um modelo de
exibição correspondente), tudo usando migrações.
A próxima etapa é criar repositórios que forneçam acesso isolado Criar, Ler, Atualizar e Excluir (CRUD) ao banco de dados. O
código de inicialização de dados, completo com dados de amostra, é adicionado ao projeto para uso em testes. O restante do capítulo
é gasto testando a camada de acesso a dados do AutoLot por meio de testes de integração automatizados.

Code First ou Database First


Antes de começarmos a criar a camada de acesso a dados, vamos discutir as duas maneiras diferentes de trabalhar com o EF Core e
seu banco de dados: código primeiro e banco de dados primeiro. Ambas são formas válidas de trabalhar com o EF Core, e a escolha
depende muito da sua equipe de desenvolvimento sobre qual abordagem usar.
Código primeiro significa que você cria e configura suas classes de entidade e o DbContext derivado no código e, em
seguida, usa migrações para atualizar o banco de dados. É assim que a maioria dos projetos greenfield, ou novos, são
desenvolvidos. A vantagem é que conforme você cria seu aplicativo, suas entidades evoluem com base nas necessidades de seu
aplicativo. As migrações mantêm o banco de dados sincronizado, de modo que o design do banco de dados evolui junto com seu
aplicativo. Esse processo de design emergente é popular entre as equipes de desenvolvimento ágil, pois você constrói as peças certas
no momento certo.
Se você já possui um banco de dados ou prefere que o design do banco de dados conduza seu aplicativo, isso é chamado
de banco de dados primeiro. Em vez de criar o DbContext derivado e todas as entidades manualmente, você monta as classes do
banco de dados. Quando o banco de dados muda, você precisa reformular suas classes para manter seu código sincronizado com o
banco de dados. Qualquer código personalizado nas entidades ou no DbContext derivado deve ser colocado em classes parciais para
que não seja substituído quando as classes forem reformuladas. Felizmente, o processo de scaffolding cria classes parciais apenas por
esse motivo.
Seja qual for o método escolhido, código primeiro ou banco de dados primeiro, saiba que é um compromisso. Se você
estiver usando o código primeiro, todas as alterações serão feitas nas classes de entidade e contexto e o banco de dados será
atualizado usando migrações. Se você estiver trabalhando primeiro com o banco de dados, todas as alterações deverão ser feitas no
banco de dados e, em seguida, as classes serão reformuladas. Com algum esforço e planejamento, você pode alternar do banco de
dados primeiro para o código primeiro (e vice-versa), mas não deve fazer alterações manuais no código e no banco de dados ao mesmo tempo.

© Andrew Troelsen, Phillip Japikse 2021 881


A. Troelsen e P. Japikse, Pro C# 9 com .NET 5, https://doi.org/10.1007/978-1-4842-6939-8_23
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Crie os projetos AutoLot.Dal e AutoLot.Models


A camada de acesso a dados AutoLot consiste em dois projetos, um para manter o código específico do EF Core
(o DbContext derivado, fábrica de contexto, repositórios, migrações, etc.) e outro para manter as entidades e
modelos de exibição. Crie uma nova solução chamada Chapter23_AllProjects e adicione uma biblioteca de
classes .NET Core chamada AutoLot.Models à solução. Exclua a classe padrão criada com o modelo e adicione
os seguintes pacotes NuGet ao projeto:

Microsoft.EntityFrameworkCore.Abstractions

System.Text.Json

O pacote Microsoft.EntityFrameworkCore.Abstractions fornece acesso a muitas construções do EF


Core (como anotações de dados) e é mais leve que o pacote Microsoft.EntityFrameworkCore.
Adicione outro projeto de biblioteca de classes .NET Core chamado AutoLot.Dal à solução. Exclua a classe
padrão criada com o modelo, adicione uma referência ao projeto AutoLot.Models e adicione os seguintes pacotes
NuGet ao projeto:

Microsoft.EntityFrameworkCore

Microsoft.EntityFrameworkCore.SqlServer

Microsoft.EntityFrameworkCore.Design

O pacote Microsoft.EntityFrameworkCore fornece a funcionalidade comum para EF Core. O


O pacote Microsoft.EntityFrameworkCore.SqlServer fornece o provedor de dados do SQL Server, e o pacote
Microsoft.EntityFrameworkCore.Design é necessário para as ferramentas de linha de comando do EF Core.
Para concluir todas essas etapas usando a linha de comando, use o seguinte (no diretório onde você deseja
a solução a ser criada):

dotnet new sln -n Chapter23_AllProjects

dotnet new classlib -lang c# -n AutoLot.Models -o .\AutoLot.Models -f net5.0 dotnet sln .


\Chapter23_AllProjects.sln add .\AutoLot.Models dotnet add pacote AutoLot.Models
Microsoft.EntityFrameworkCore.Abstractions dotnet add AutoLot .Modelos pacote System.Text.Json

dotnet new classlib -lang c# -n AutoLot.Dal -o .\AutoLot.Dal -f net5.0 dotnet sln .


\Chapter23_AllProjects.sln add .\AutoLot.Dal dotnet add referência AutoLot.Dal
AutoLot.Models dotnet add AutoLot.Dal pacote Microsoft.EntityFrameworkCore dotnet
adicionar AutoLot.Dal pacote Microsoft.EntityFrameworkCore.Design dotnet adicionar
pacote AutoLot.Dal Microsoft.EntityFrameworkCore.SqlServer dotnet adicionar pacote
AutoLot.Dal Microsoft.EntityFrameworkCore.Tools

ÿ Observação Se você não estiver usando uma máquina baseada em Windows, ajuste o caractere separador de diretório
para seu sistema operacional. Isso precisa ser feito para todos os comandos da CLI neste capítulo.

Após a criação dos projetos, atualize cada arquivo *.csproj para habilitar os tipos de referência anuláveis do C# 8. A
atualização é mostrada aqui em negrito:

882
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<Nullable>ativar</Nullable> </PropertyGroup>

Estruture o DbContext e as Entidades

A próxima etapa é estruturar o banco de dados AutoLot do Capítulo 21 usando as ferramentas de linha de comando do EF Core.
Navegue até o diretório do projeto AutoLot.Dal em um prompt de comando ou no console do gerenciador de pacotes do Visual
Studio.

ÿ Observação Na pasta do repositório do Capítulo 21 estão os backups de banco de dados para Windows e Docker. Se
precisar restaurar o banco de dados, consulte as instruções no Capítulo 21.

Use as ferramentas EF Core CLI para estruturar o banco de dados AutoLot nas entidades e o banco de dados derivado de DbContext
class com o seguinte comando (tudo em uma linha):

dotnet ef dbcontext scaffold "server=.,5433;Database=AutoLot;User Id=sa;Password=P@ssw0rd;"


Microsoft.EntityFrameworkCore.SqlServer -d -c ApplicationDbContext --context-namespace AutoLot.Dal.EfStructures --context-dir
EfStructures --no-onconfiguring -n AutoLot.Models.
Entidades -o ..\AutoLot.Models\Entidades

O comando anterior estrutura o banco de dados localizado na string de conexão fornecida (essa é a string de conexão para o
contêiner Docker usado no Capítulo 21) usando o provedor de banco de dados do SQL Server.
O sinalizador -d é para priorizar as anotações de dados sempre que possível (na API Fluent). O -c nomeia o contexto, --context-namespaces
especifica o namespace para o contexto, --context-dir indica o diretório (relativo ao projeto atual) para o contexto, --no-onconfiguring
impede que o método OnConfiguring seja scaffoldado, o -o é o diretório de saída para as entidades (relativo ao diretório do projeto) e o -n
especifica o namespace para as entidades. Este comando coloca todas as entidades no projeto AutoLot.Models na pasta de entidades e
coloca ApplicationDbContext na pasta EfStructures do projeto AutoLot.Dal.

Se você tem acompanhado este capítulo, notará que o procedimento armazenado não foi
andaime. Se houvesse visualizações no banco de dados, elas teriam sido agrupadas em entidades sem chave.
Como não há uma construção do EF Core que mapeia diretamente para um procedimento armazenado, não há nada para o scaffold
do procedimento armazenado. Procedimentos armazenados e outros objetos SQL podem ser criados usando o EF Core, mas, no
momento, apenas tabelas e exibições são scaffolded.

Alternar para o código primeiro

Agora que você tem o banco de dados organizado em entidades, é hora de mudar primeiro do banco de dados para o código primeiro.
Para alternar, uma fábrica de contexto deve ser criada e uma migração é criada a partir do estado atual do projeto.
Em seguida, a migração é aplicada descartando e recriando o banco de dados ou falsa aplicada por “enganação”
Núcleo EF.

883
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Criar a fábrica de tempo de design DbContext


Como você se lembra do Capítulo 22, IDesignTimeDbContextFactory é usado pelas ferramentas de linha
de comando do EF Core para criar uma instância da classe DbContext derivada. Crie um novo arquivo
de classe chamado ApplicationDbContextFactory.cs no projeto AutoLot.Dal no diretório EfStructures. Adicione
os seguintes namespaces à classe:

usando Sistema;
usando Microsoft.EntityFrameworkCore;
usando Microsoft.EntityFrameworkCore.Design;

Os detalhes da fábrica foram abordados no capítulo anterior, então vou apenas listar o código aqui.
A chamada adicional para Console.WriteLine() gera a string de conexão para o console. Isso é usado apenas para
fins informativos. Certifique-se de atualizar sua string de conexão para corresponder ao seu ambiente.

namespace AutoLot.Dal.EfStructures {

classe pública ApplicationDbContextFactory: IDesignTimeDbContextFactory<ApplicationDb


Contexto>

{ public ApplicationDbContext CreateDbContext(string[] args) { var


optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
var connectionString = @"servidor=.,5433;Database=AutoLot;ID do usuário=sa;Senha=P@
ssw0rd;"; opçõesBuilder.UseSqlServer(connectionString); Console.WriteLine(connectionString);
retornar novo ApplicationDbContext(optionsBuilder.Options); } } }

Criar a migração inicial


Lembre-se de que a primeira migração criará três arquivos: os dois arquivos para a classe parcial de migração
e o terceiro arquivo é o instantâneo completo do modelo. Digite o seguinte em um prompt de comando no diretório
AutoLot.Dal para criar uma nova migração chamada Initial (usando a instância ApplicationDbContext que acabou
de ser montada) e colocando os arquivos de migração na pasta EfStructures\Migrations do projeto AutoLot.Dal:

dotnet ef migrations add Initial -o EfStructures\Migrations -c AutoLot.Dal.EfStructures.


ApplicationDbContext

ÿ Observação É importante garantir que nenhuma alteração seja aplicada aos arquivos gerados ou ao banco de dados até que essa

primeira migração seja criada e aplicada. Alterações em qualquer um dos lados farão com que o código e o banco de dados fiquem fora de

sincronia. Depois de aplicadas, todas as alterações no banco de dados precisam ser concluídas por meio de migrações do EF Core.

884
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Para confirmar que a migração foi criada e está aguardando para ser aplicada, execute o comando list.

lista de migrações dotnet ef -c AutoLot.Dal.EfStructures.ApplicationDbContext

O resultado mostrará a migração inicial pendente (seu registro de data e hora será diferente). A conexão
string é mostrada na saída devido a Console.Writeline() no método CreateDbContext().

Construção iniciada...
Compilação bem-sucedida.

server=.,5433;Database=AutoLot;User Id=sa;Password=P@ssw0rd;
20201231203939_Inicial (Pendente)

Aplicando a Migração
O método mais fácil de aplicar a migração ao banco de dados é descartar o banco de dados e recriá-lo. Se essa for
uma opção, você pode inserir os seguintes comandos e passar para a próxima seção:

dotnet ef database drop -f dotnet


ef database update Inicial -c AutoLot.Dal.EfStructures.ApplicationDbContext

Se descartar e recriar o banco de dados não for uma opção (por exemplo, é um banco de dados SQL do
Azure), o EF Core precisa acreditar que a migração foi aplicada. Felizmente, isso é direto com o EF Core fazendo
todo o trabalho. Comece criando um script SQL da migração usando o seguinte comando:

script de migrações dotnet ef --idempotent -o FirstMigration.sql

As partes relevantes desse script são as partes que criam a tabela __EFMigrationsHistory e, em seguida,
adicionam o registro de migração à tabela para indicar que foi aplicado. Copie essas partes para uma nova
consulta no Azure Data Studio ou no SQL Server Manager Studio. Aqui está o código SQL que você precisa (seu
timestamp será diferente):

SE OBJECT_ID(N'[__EFMigrationsHistory]') É NULO
COMEÇAR

CREATE TABLE [__EFMigrationsHistory] (


[MigrationId] nvarchar(150) NÃO NULO,
[ProductVersion] nvarchar(32) NÃO NULO,
CONSTRAINT [PK___EFMigrationsHistory] CHAVE PRIMÁRIA ([MigrationId])
);
FIM;
IR

INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])


VALORES (N'20201231203939_Inicial', N'5.0.1');

Agora, se você executar o comando list, ele não mostrará mais a migração inicial como pendente. Com a
migração inicial aplicada, o projeto e o banco de dados estão sincronizados e o desenvolvimento continuará o código primeiro.

885
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Atualizar o modelo
Esta seção atualiza todas as entidades atuais para sua versão final e adiciona uma entidade de registro. Observe que seus
projetos não serão compilados até que esta seção seja concluída.

As Entidades
No diretório Entities do projeto AutoLot.Models, você encontrará cinco arquivos, um para cada tabela do banco de dados.
Observe que os nomes são singulares e não plurais (como estão no banco de dados). Esta é uma alteração no EF Core 5 em que
o pluralizador está ativado por padrão ao criar entidades do banco de dados.
As mudanças que você fará nas entidades incluem adicionar uma classe base, criar uma entidade Person própria,
corrigindo nomes de propriedades de navegação e adicionando algumas propriedades adicionais. Você também adicionará
uma nova entidade para log (que será usada pelos capítulos do ASP.NET Core). O capítulo anterior abordou as convenções
do EF Core, anotações de dados e a API Fluent em profundidade, portanto, a maior parte desta seção será listagens de código
com breves descrições.

A Classe BaseEntity
A classe BaseEntity conterá as colunas Id e TimeStamp que estão em cada entidade. Crie um novo diretório chamado
Base no diretório Entities do projeto AutoLot.Models. Nesse diretório, crie um novo arquivo chamado BaseEntity.cs. Atualize
o código para corresponder ao seguinte:

usando System.ComponentModel.DataAnnotations; usando


System.ComponentModel.DataAnnotations.Schema;

namespace AutoLot.Models.Entities.Base { public


abstract class BaseEntity {

[Chave, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id


{ get; definir; }
[Timestamp]
byte público[]? TimeStamp { get; definir; } } }

Todas as entidades scaffolded do banco de dados AutoLot serão atualizadas para usar esta classe base.

A Entidade Pessoa Própria


As entidades Customer e CreditRisk possuem propriedades FirstName e LastName. Entidades que possuem exatamente as
mesmas propriedades em cada uma podem se beneficiar ao mover essas propriedades para classes próprias. Embora duas
propriedades sejam um exemplo trivial, entidades de propriedade ajudam a reduzir a duplicação de código e aumentar a
consistência. Além das duas propriedades nas classes, é adicionada uma nova propriedade que será mapeada para uma coluna
computada do SQL Server.
Crie um novo diretório chamado Owned no diretório Entities do projeto AutoLot.Models. Nisso
novo diretório, crie um novo arquivo chamado Person.cs. Atualize o código para corresponder ao seguinte:

886
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

usando System.ComponentModel.DataAnnotations;
usando System.ComponentModel.DataAnnotations.Schema; usando
Microsoft.EntityFrameworkCore;

namespace AutoLot.Models.Entities.Owned {

[Propriedade] public class


Pessoa {
[Obrigatório, StringLength(50)] public
string FirstName { get; definir; } = "Novo";

[Obrigatório, StringLength(50)] public


string LastName { get; definir; } = "Cliente";

[DatabaseGenerated(DatabaseGeneratedOption.Computed)] string
pública? Nome Completo { get; definir; } }

A propriedade FullName é anulável, pois novas entidades não terão o valor definido até que sejam salvas no
base de dados. A configuração final da propriedade Fullname será adicionada usando a API Fluent.

A Entidade Carro (Inventário)


A tabela Inventário foi montada em uma classe de entidade chamada Inventário. Preferimos usar o nome Carro. Isso é fácil
de corrigir: altere o nome do arquivo para Car.cs e o nome da classe para Car. O atributo Table já está aplicado
corretamente, então basta adicionar o esquema dbo. Observe que o parâmetro schema é opcional porque o padrão do
SQL Server é dbo, mas eu o incluo para completar.

[Table("Inventário", Schema = "dbo")]


[Index(nameof(MakeId), Name = "IX_Inventory_MakeId")] classe
parcial pública Car : BaseEntity {

...
}

Atualize as instruções using para corresponder ao seguinte:

usando Sistema;
usando System.Collections.Generic; usando
System.ComponentModel; usando
System.ComponentModel.DataAnnotations; usando
System.ComponentModel.DataAnnotations.Schema; usando
System.Text.Json.Serialization; usando AutoLot.Models.Entities.Base;
usando Microsoft.EntityFrameworkCore;

887
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Em seguida, herde de BaseEntity e remova as propriedades Id e TimeStamp, o construtor e o


pragma #anulável desabilitar. Este é o código para a classe após essas alterações:

namespace AutoLot.Models.Entities {

[Table("Inventário", Schema = "dbo")]


[Index(nameof(MakeId), Name = "IX_Inventory_MakeId")] classe
parcial pública Car : BaseEntity {

public int MakeId { obter; definir; }


[Obrigatório]
[StringLength(50)]
public string Color { get; definir; }
[Obrigatório]
[StringLength(50)]
public string PetName { get; definir; }
[ForeignKey(nameof(MakeId))]
[InverseProperty("Inventários")] public
virtual Make Make { get; definir; }
[InverseProperty(nameof(Order.Car))] public
virtual ICollection<Order> Orders { get; definir; } }

Ainda há alguns problemas com esse código que precisam ser corrigidos e há novas propriedades a serem adicionadas.
As propriedades Color e PetName são definidas como não anuláveis, mas os valores não estão sendo definidos no construtor
ou inicializados com a definição da propriedade. Isso é resolvido atribuindo a cada propriedade um inicializador. Adicione o
atributo DisplayName à propriedade PetName para obter um nome melhor e legível por humanos. Atualize as propriedades para
corresponder ao seguinte (alterações em negrito):

[Obrigatório]
[StringLength(50)]
public string Color { get; definir; } = "Ouro";

[Obrigatório]
[StringLength(50)]
[DisplayName("Pet Name")]
public string PetName { get; definir; } = "Meu Precioso";

ÿ Observação O atributo DisplayName é usado pelo ASP.NET Core e será abordado na Parte 8.

A propriedade de navegação Make precisa ser renomeada para MakeNavigation e tornada anulável, e a
propriedade inverse está usando uma string mágica em vez do método C# nameof(). A mudança final é remover o
modificador virtual. Aqui está a propriedade atualizada:

[ForeignKey(nameof(MakeId))]
[InverseProperty(nameof(Make.Cars))]
public Make? MakeNavigation { get; definir; }

888
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

ÿ Observação O modificador virtual é necessário para carregamento lento. Como nenhum dos exemplos deste livro usa

carregamento lento, o modificador virtual será removido de todas as propriedades na camada de acesso a dados.

A propriedade de navegação Orders precisa do atributo JsonIgnore para evitar referências JSON circulares
ao serializar o modelo de objeto. O código scaffolded usa o método nameof() na propriedade inverse, mas precisa de
uma atualização, pois todas as propriedades de navegação de referência terão o sufixo Navigation adicionado aos seus
nomes. A alteração final é ter o tipo da propriedade digitada como IEnumerable<Order> em vez de ICollection<Order> e
inicializada com uma nova List<Order>. Esta não é uma alteração necessária, pois ICollection<Order> também funcionará.
Prefiro usar o IEnumerable<T> de nível inferior nas propriedades de navegação da coleção (já que IQueryable<T> e
ICollection<T> derivam de IEnumerable<T>). Atualize o código para corresponder ao seguinte:

[JsonIgnore]
[InverseProperty(nameof(Order.CarNavigation))] public
IEnumerable<Order> Orders { get; definir; } = new List<Ordem>();

Em seguida, adicione uma propriedade NotMapped que exibirá o valor Make do carro. Isso elimina a necessidade
do CarViewModel no Capítulo 21. Se as informações relacionadas à marca foram recuperadas do banco de dados com o
registro do carro, o nome da marca será exibido. Se os dados relacionados não foram recuperados, a propriedade exibe
“Desconhecido”. Como lembrete, as propriedades NotMapped não fazem parte do banco de dados e existem apenas na entidade.
Adicione o seguinte:

[NotMapped]
public string MakeName => MakeNavigation?.Name ?? "Desconhecido";

Substitua ToString() para exibir as informações do veículo.

public override string ToString() { // Como a


coluna PetName pode estar vazia, forneça //
o nome padrão de **No Name**. return $"{PetName ?? "**No
Name**"} é um {Color} {MakeNavigation?.Name} com ID {Id}."; }

Adicione os atributos Required e DisplayName ao MakeId. Mesmo que a propriedade MakeId seja considerada
pelo EF Core como necessária por ser não anulável, o mecanismo de validação do ASP.NET Core precisa do atributo Required.
Atualize o código para corresponder ao seguinte:

[Obrigatório]
[DisplayName("Make")]
public int MakeId { get; definir; }

A alteração final é adicionar a propriedade bool não anulável IsDrivable com um campo de apoio anulável e um nome de
exibição.

bool privado? _é Dirigível;

[DisplayName("É Dirigível")] public


bool É Dirigível

889
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

{
get => _isDrivable ?? falso; set =>
_isDrivable = valor; }

Isso completa a entidade Car atualizada.

A Entidade Cliente
A tabela Clientes foi estruturada para uma classe de entidade chamada Cliente. Atualize as instruções using para
corresponder ao seguinte:

usando Sistema;
usando System.Collections.Generic; usando
System.ComponentModel.DataAnnotations.Schema; usando
System.Text.Json.Serialization; usando AutoLot.Models.Entities.Base;
usando AutoLot.Models.Entities.Owned;

Em seguida, herde de BaseEntity e remova as propriedades Id e TimeStamp. Exclua o construtor e o pragma


#nullable disable e adicione o atributo Table com schema. Remova as propriedades FirstName e LastName, pois elas
serão substituídas pela entidade de propriedade da Pessoa. É aqui que o código da classe está neste momento:

namespace AutoLot.Models.Entities {

[Table("Clientes", Schema = "dbo")] classe


parcial pública Cliente : BaseEntity {

[InverseProperty(nameof(CreditRisk.Customer))] public virtual


ICollection<CreditRisk> CreditRisks { get; definir; }
[InverseProperty(nameof(Order.Customer))] public
virtual ICollection<Order> Orders { get; definir; } }

Assim como a entidade Carro, ainda há alguns problemas com esse código que precisam ser corrigidos, e a entidade proprietária
deve ser adicionado. As propriedades de navegação precisam do atributo JsonIgnore, os atributos de propriedade
inversa precisam ser atualizados com o sufixo Navigation, os tipos alterados para um IEnumerable<T> inicializado e o
modificador virtual removido. Atualize o código para corresponder ao seguinte:

[JsonIgnore]
[InverseProperty(nameof(CreditRisk.CustomerNavigation))] public
IEnumerable<CreditRisk> CreditRisks { get; definir; } = new List<CreditRisk>();

[JsonIgnore]
[InverseProperty(nameof(Order.CustomerNavigation))] public
IEnumerable<Order> Orders { get; definir; } = new List<Ordem>();

890
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

A alteração final é adicionar a propriedade de propriedade. A relação será configurada posteriormente na API Fluent.

public Person PersonalInformation { get; definir; } = new Pessoa();

Isso completa a entidade Customer atualizada.

A Entidade Marca
A tabela Makes foi montada em uma classe de entidade chamada Make. Atualize as instruções using para corresponder
ao seguinte:

usando Sistema;
usando System.Collections.Generic; usando
System.ComponentModel; usando
System.ComponentModel.DataAnnotations; usando
System.ComponentModel.DataAnnotations.Schema; usando
System.Text.Json.Serialization; usando AutoLot.Models.Entities.Base;
usando Microsoft.EntityFrameworkCore;

Herde de BaseEntity e remova as propriedades Id e TimeStamp. Exclua o construtor e o pragma #nullable disable e
adicione o atributo Table com schema. Aqui está o estado atual da entidade:

namespace AutoLot.Models.Entities {

[Table("Makes", Schema = "dbo")] classe


parcial pública Make: BaseEntity {

[Obrigatório]
[StringLength(50)] nome
público da string { get; definir; }
[InverseProperty(nameof(Inventory.Make))] public virtual
ICollection<Inventory> Inventários { get; definir; } }

O código a seguir mostra a propriedade Name não anulável inicializada e a propriedade de navegação Cars corrigida
(observe a alteração de Inventory para Car no método nameof):

[Obrigatório]
[StringLength(50)] nome
público da string { get; definir; } = "Ford";

[JsonIgnore]
[InverseProperty(nameof(Car.MakeNavigation))] public
IEnumerable<Car> Cars { get; definir; } = new List<Carro>();

Isso completa a entidade Make.

891
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

A Entidade CreditRisk A tabela

CreditRisks foi estruturada para uma classe de entidade chamada CreditRisk. Atualize as instruções using para
corresponder ao seguinte:

usando System.ComponentModel.DataAnnotations.Schema;
usando AutoLot.Models.Entities.Base; usando
AutoLot.Models.Entities.Owned;

Herde de BaseEntity e remova as propriedades Id e TimeStamp. Exclua o construtor e o pragma #nullable


disable e adicione o atributo Table com schema. Remova as propriedades FirstName e LastName, pois elas serão
substituídas pela entidade de propriedade da Pessoa. Aqui está o código da classe atualizado:

namespace AutoLot.Models.Entities {

[Table("CreditRisks", Schema = "dbo")] public


private class CreditRisk : BaseEntity { public Person
PersonalInformation { get; definir; } = new Pessoa();
public int CustomerId { get; definir; }

[ForeignKey(nameof(CustomerId))]
[InverseProperty("CreditRisks")] public
virtual Cliente Cliente { get; definir; } } }

Corrija a propriedade de navegação removendo o modificador virtual, use o método nameof() no


atributo InverseProperty e adicione o sufixo de navegação ao nome da propriedade.

[ForeignKey(nameof(CustomerId))]
[InverseProperty(nameof(Customer.CreditRisks))]
público Cliente? Navegação do cliente { get; definir; }

A alteração final é adicionar a propriedade de propriedade. A relação será configurada posteriormente na API
Fluent.

public Person PersonalInformation { get; definir; } = new Pessoa();

Isso completa a entidade CreditRisk.

A Entidade Order A

tabela Orders foi estruturada para uma classe de entidade chamada Order. Atualize as instruções using para
corresponder ao seguinte:

usando Sistema;
usando System.ComponentModel.DataAnnotations.Schema;
usando AutoLot.Models.Entities.Base; usando
Microsoft.EntityFrameworkCore;

892
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Herde de BaseEntity e remova as propriedades Id e TimeStamp. Exclua o construtor e o


pragma #nullable desabilite e adicione o atributo Table com schema. Aqui está o código atual:

namespace AutoLot.Models.Entities {

[Tabela("Pedidos", Esquema = "dbo")]


[Index(nameof(CarId), Name = "IX_Orders_CarId")]
[Index(nameof(CustomerId), nameof(CarId), Name = "IX_Orders_CustomerId_CarId",
IsUnique = true)]
ordem de classe parcial pública: BaseEntity {

public int CustomerId { get; definir; } public int


CarId { get; definir; }
[ForeignKey(nameof(CarId))]
[InverseProperty(nameof(Inventory.Orders))] public
virtual Inventory Car { get; definir; }
[ForeignKey(nameof(CustomerId))]
[InverseProperty("Pedidos")] cliente
virtual público { get; definir; } }

As propriedades de navegação Car e Customer precisam do sufixo Navigation adicionado aos seus nomes de propriedade.
A propriedade de navegação Car precisa do tipo corrigido para Car from Inventory. A propriedade inversa precisa do
método nameof() para usar Car.Orders em vez de Inventory.Orders. A propriedade de navegação Customer precisa usar
o método nameof() para InverseProperty. Ambas as propriedades precisam ser anuláveis e o modificador virtual removido.

[ForeignKey(nameof(CarId))]
[InverseProperty(nameof(Car.Orders))] carro
público ? Navegação do carro { get; definir; }

[ForeignKey(nameof(CustomerId))]
[InverseProperty(nameof(Customer.Orders))]
público Cliente? Navegação do cliente { get; definir; }

Isso completa a entidade Order.

ÿ Observação Neste momento, o projeto AutoLot.Models deve ser construído corretamente. O projeto AutoLot.Dal não será

compilado até que a classe ApplicationDbContext seja atualizada.

A entidade SeriLogEntry
O banco de dados precisa de uma tabela adicional para manter os registros de log. Os projetos ASP.NET Core na Parte
8 usarão a estrutura de log SeriLog e uma das opções é gravar registros de log em uma tabela do SQL Server. Vamos
adicionar a tabela agora, sabendo que ela será usada daqui a alguns capítulos.

893
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

A tabela não está relacionada a nenhuma outra tabela e não usa a classe BaseEntity. Adicionar um novo arquivo de classe
denominado SeriLogEntry.cs na pasta Entidades. O código está listado na íntegra aqui:

usando
Sistema; usando System.ComponentModel.DataAnnotations;
usando System.ComponentModel.DataAnnotations.Schema;
usando System.Xml.Linq;

namespace AutoLot.Models.Entities {

[Table("SeriLogs", Schema = "Logging")] public


class SeriLogEntry {

[Chave, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; definir; } string pública? Mensagem { obter; definir; }
string pública? MessageTemplate { get; definir; }

[MaxLength(128)]
string pública? Nível { obter; definir; }
[DataType(DataType.DateTime)]
DateTime público? TimeStamp { get; definir; }
string pública? Exceção { obter; definir; } string
pública? Propriedades { obter; definir; } string
pública? LogEvent { obter; definir; } string
pública? SourceContext { obter; definir; } string
pública? RequestPath { obter; definir; } string
pública? ActionName { obter; definir; } string
pública? ApplicationName { get; definir; } string
pública? MachineName { get; definir; } string pública?
FilePath { obter; definir; } string pública? Nome do
membro { get; definir; } public int? NúmeroLinha { get;
definir; }
[NotMapped]
XElement público? PropertiesXml => (Propriedades != null)? XElement.Parse(Propriedades):null; }

Isso completa a entidade SeriLogEntry.

ÿ Observação A propriedade TimeStamp nesta entidade não é igual à propriedade TimeStamp na classe
BaseEntity . Os nomes são os mesmos, mas nesta tabela contém a data e hora de quando a entrada foi registrada
(isso será configurado como padrão do SQL Server) e não a versão da linha nas outras entidades.

894
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

O ApplicationDbContext
É hora de atualizar ApplicationDbContext.cs. Comece atualizando as instruções using para corresponder ao seguinte:

usando Sistema;
usando System.Collections;
usando System.Collections.Generic; usando
AutoLot.Models.Entities; usando
AutoLot.Models.Entities.Owned; usando
Microsoft.EntityFrameworkCore; usando
Microsoft.EntityFrameworkCore.Storage; usando
Microsoft.EntityFrameworkCore.ChangeTracking; usando
AutoLot.Dal.Exceptions;

O arquivo começa com um construtor sem parâmetros. Exclua isso, pois não precisaremos dele. O próximo construtor
pega uma instância do objeto DbContextOptions e está bom por enquanto. Os ganchos de evento para DbContext e
ChangeTracker serão adicionados posteriormente neste capítulo.
As propriedades DbSet<T> precisam ser atualizadas para serem anuláveis, os nomes corrigidos e os modificadores
virtuais removidos. A nova entidade de criação de log precisa ser adicionada. Navegue até as propriedades DbSet<T> e
atualize-as para o seguinte:

public DbSet<SeriLogEntry>? LogEntries { obter; definir; } public


DbSet<CreditRisk>? Riscos de Crédito { get; definir; } public
DbSet<Cliente>? Clientes { obtêm; definir; } public DbSet<Make>? Faz
{ obter; definir; } public DbSet<Carro>? Carros { obter; definir; } public
DbSet<Pedido>? Pedidos { obter; definir; }

Atualize o código da Fluent API


A substituição OnModelCreating é onde o código Fluent API pertence e usa uma instância da classe ModelBuilder
para atualizar o modelo.

A Entidade SeriLog
A primeira alteração nesse método é adicionar o código Fluent API para a configuração da entidade SeriLogEntry.
A propriedade Properties é uma coluna XML do SQL Server e a propriedade TimeStamp é mapeada para uma coluna
datetime2 no SQL Server com o valor padrão definido para a função getdate() do SQL Server. No método OnModelCreating,
adicione o seguinte código:

modelBuilder.Entity<SeriLogEntry>(entity =>
{ entity.Property(e => e.Properties).HasColumnType("Xml");
entity.Property(e => e.TimeStamp).HasDefaultValueSql("GetDate()") ; });

895
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

A Entidade CreditRisk O próximo

código a ser atualizado é para a entidade CreditRisk. O bloco de configuração para a coluna TimeStamp é
removido, pois é configurado no BaseEntity. A configuração de navegação deve ser atualizada com os novos nomes.
Também afirmamos que a propriedade de navegação não é nula. A outra alteração é configurar a propriedade da
entidade de propriedade para mapeamentos de nome de coluna para FirstName e LastName e adicionar o valor
computado para a propriedade FullName. Segue o bloco atualizado para a entidade CreditRisk, com as alterações
destacadas em negrito:

modelBuilder.Entity<CreditRisk>(entity => {

entity.HasOne(d => d.CustomerNavigation)


.ComMuitos(p => p!.Riscos de Crédito)
.HasForeignKey(d => d.CustomerId)
.HasConstraintName("FK_CreditRisks_Customers");

entidade.OwnsOne(o => o.PersonalInformation,


pd =>

{ pd.Property<string>(nameof(Person.FirstName))
.HasColumnName(nome da(Pessoa.Nome))
.HasColumnType("nvarchar(50)");
pd.Property<string>(nameof(Person.LastName))
.HasColumnName(nameof(Pessoa.LastName))
.HasColumnType("nvarchar(50)");
pd.Property(p => p.FullName)
.HasColumnName(nomeda(Pessoa.NomeCompleto))
.HasComputedColumnSql("[Sobrenome] + ', ' + [Nome]");
});
});

A Entidade Cliente O próximo

código a ser atualizado é para a entidade Cliente. O código TimeStamp é removido e as propriedades da entidade de
propriedade são configuradas.

modelBuilder.Entity<Cliente>(entidade => {

entidade.OwnsOne(o => o.PersonalInformation,


pd =>
{
pd.Property(p => p.FirstName).HasColumnName(nameof(Pessoa.
Primeiro
nome)); pd.Property(p => p.LastName).HasColumnName(nameof(Pessoa.LastName));
pd.Property(p => p.FullName)
.HasColumnName(nomeda(Pessoa.NomeCompleto))
.HasComputedColumnSql("[Sobrenome] + ', ' + [Nome]");
});
});

896
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

A entidade Make Para a

entidade Make, atualize o bloco de configuração para remover o TimeStamp e adicione o código que restringe a exclusão
de uma entidade que possui entidades dependentes.

modelBuilder.Entity<Make>(entity => {

entidade.HasMany(e => e.Cars)


.WithOne(c => c.MakeNavigation!)
.HasForeignKey(k => k.MakeId)
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("FK_Make_Inventory");
});

A entidade Order Para a

entidade Order, atualize os nomes das propriedades de navegação e assegure que as propriedades inversas não sejam
nulas. Em vez de restringir exclusões, o relacionamento Cliente para Pedidos é definido como exclusão em cascata.

modelBuilder.Entity<Pedido>(entidade => {

entidade.HasOne(d => d.CarNavigation)


.ComMuitos(p => p!.Pedidos)
.HasForeignKey(d => d.CarId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_Orders_Inventory");

entity.HasOne(d => d.CustomerNavigation)


.ComMuitos(p => p!.Pedidos)
.HasForeignKey(d => d.CustomerId)
.OnDelete(DeleteBehavior.Cascade)
.HasConstraintName("FK_Orders_Customers");
});

Defina um filtro de consulta na propriedade CarNavigation da tabela Order para filtrar carros não dirigíveis.
Observe que esse código não está no mesmo bloco do código anterior. Não há razão técnica para separá-lo; é
uma sintaxe alternativa para definir a configuração em blocos separados.

modelBuilder.Entity<Order>().HasQueryFilter(e => e.CarNavigation!.IsDrivable);

A entidade Car A classe

scaffolded continha a configuração para a classe Inventory. Ele precisa ser alterado para a classe Carro. O TimeStamp
pode ser removido, e a configuração da propriedade de navegação fica com a atualização dos nomes das propriedades
MakeNavigation e Cars. A entidade obtém um filtro de consulta definido para mostrar apenas carros dirigíveis por padrão e
define o valor padrão da propriedade IsDrivable como true. Atualize o código para corresponder ao seguinte:

modelBuilder.Entity<Car>(entity => {

entidade.HasQueryFilter(c => c.IsDrivable);


entity.Property(p => p.IsDrivable).HasField("_isDrivable").HasDefaultValue(true);

897
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

entidade.HasOne(d => d.MakeNavigation)


.ComMuitos(p => p.Carros)
.HasForeignKey(d => d.MakeId)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_Make_Inventory");
});

Exceções personalizadas

Um padrão comum no tratamento de exceções é capturar exceções do sistema (e/ou exceções do EF Core, como
neste exemplo), registrar a exceção e lançar uma exceção personalizada. Se uma exceção personalizada for
capturada em um método upstream, o desenvolvedor saberá que a exceção já foi registrada e só precisa reagir à
exceção adequadamente em seu código.
Crie um novo diretório chamado Exceptions no projeto AutoLot.Dal. Nesse diretório, crie quatro novos
arquivos de classe: CustomException.cs, CustomConcurrencyException.cs, CustomDbUpdateException.cs e
CustomRetryLimitExceededException.cs. Todos os quatro arquivos são mostrados na listagem a seguir:

//CustomException.cs
using System; namespace
AutoLot.Dal.Exceptions {

public class CustomException: Exceção {

public CustomException() {} public


CustomException(string message) : base(message) { } public
CustomException(string message, Exception innerException) : base(message,
innerException) { }
}
}

//CustomConcurrencyException.cs
usando Microsoft.EntityFrameworkCore;
namespace AutoLot.Dal.Exceptions {

public class CustomConcurrencyException : CustomException {

public CustomConcurrencyException() { } public


CustomConcurrencyException(string message) : base(message) { } public
CustomConcurrencyException(string message, DbUpdateConcurrencyException
innerException) : base(message, innerException) { }

}
}

//CustomDbUpdateException.cs
usando Microsoft.EntityFrameworkCore;
namespace AutoLot.Dal.Exceptions {

public class CustomDbUpdateException : CustomException {

898
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

public CustomDbUpdateException() { } public


CustomDbUpdateException(string message) : base(message) { } public
CustomDbUpdateException(string message, DbUpdateException innerException) :
base(message, innerException) { }

}
}

//CustomRetryLimitExceededException.cs using
System; usando
Microsoft.EntityFrameworkCore.Storage;

namespace AutoLot.Dal.Exceptions {

public class CustomRetryLimitExceededException : CustomException {

public CustomRetryLimitExceededException() { } public


CustomRetryLimitExceededException(string message): base(message)
{ } public CustomRetryLimitExceededException(string message,
RetryLimitExceededException innerException) : base(message,
innerException) { }

}
}

ÿ Observação A manipulação de exceção personalizada foi abordada em detalhes no Capítulo 7.

Substituir o método SaveChanges Conforme

discutido no capítulo anterior, o método SaveChanges() na classe base DbContext persiste as alterações,
adições e exclusões de dados no banco de dados. Substituir esse método permite que a manipulação de
exceções seja encapsulada em um só lugar. Com as exceções personalizadas em vigor, adicione a instrução
AutoLot.Dal.Exceptions using à parte superior da classe ApplicationDbContext. Em seguida, adicione a seguinte
substituição ao método SaveChanges():

substituição pública int SaveChanges() {

tente { return base.SaveChanges(); }


catch (DbUpdateConcurrencyException
ex) {

//Ocorreu um erro de simultaneidade //


Deve logar e manipular de forma inteligente
throw new CustomConcurrencyException("Aconteceu um erro de simultaneidade.", ex); }
catch (RetryLimitExceededException ex) {

899
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

//Limite de repetição de DbResiliency


excedido //Deve registrar e manipular de
forma inteligente throw new CustomRetryLimitExceededException("Há um problema com o SQl Server.",
ex); } catch (DbUpdateException ex) {

//Deve registrar e manipular de forma


inteligente throw new CustomDbUpdateException("Ocorreu um erro ao atualizar o banco de
dados", ex); } catch (Exceção ex) {

//Deve registrar e manipular de forma


inteligente throw new CustomException("Ocorreu um erro ao atualizar o banco de
dados", ex); }
}

Manipulando eventos DbContext e ChangeTracker


Navegue até o construtor de ApplicationDbContext e adicione os três eventos DbContext discutidos no
capítulo anterior.

public ApplicationDbContext(DbContextOptions<ApplicationDbContext> opções)


: base(opções)
{
base.SavingChanges += (sender, args) =>
{ Console.WriteLine($"Salvando alterações
para {((ApplicationDbContext)sender)!.Database!.
GetConnectionString()}"); };
base.SavedChanges +=
(sender, args) => { Console.WriteLine($"Saved
{args!.EntitiesSavedCount} alterações para
{((ApplicationDbContext)sender)!.Database!.GetConnectionString ()}"); };
base.SaveChangesFailed += (remetente, args) => { Console.WriteLine($"Ocorreu
uma exceção! {args.Exception.Message} entidades"); }; }

Em seguida, adicione manipuladores para os eventos ChangeTracker StateChanged e Tracked.

public ApplicationDbContext(DbContextOptions<ApplicationDbContext> opções)


: base(opções)
{
...
ChangeTracker.Tracked += ChangeTracker_Tracked;
ChangeTracker.StateChanged += ChangeTracker_StateChanged; }

900
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Os argumentos do evento rastreado contêm uma referência à entidade que acionou o evento e se ele veio
uma consulta (carregada do banco de dados) ou foi adicionada programaticamente. Adicione o seguinte manipulador de
eventos em ApplicationDbContext:

private void ChangeTracker_Tracked(object? sender, EntityTrackedEventArgs e) { var source =


(e.FromQuery) ? "Banco de Dados" : "Código"; if (e.Entry.Entity is Car c) { Console.WriteLine($"A
entrada do carro {c.PetName} foi adicionada de {source}"); }

O evento StateChanged é acionado quando o estado de uma entidade rastreada muda. Um uso para este evento é a auditoria.
No manipulador de eventos a seguir, se o NewState da entidade for Unchanged, o OldState será examinado para
ver se a entidade foi adicionada ou modificada. Adicione o seguinte manipulador de eventos em ApplicationDbContext:

private void ChangeTracker_StateChanged(object? sender, EntityStateChangedEventArgs e) { if (e.Entry.Entity is


not Car c) {

retornar;

} var ação = string.Empty;


Console.WriteLine($"Carro {c.PetName} era {e.OldState} antes de mudar de estado para {e.NewState}");
switch (e.NewState) {

caso EntityState. Inalterado:


ação = opção e.OldState {

EntityState.Added => "Adicionado",


EntityState.Modified => "Editado", => ação
_
};
Console.WriteLine($"O objeto era {action}"); quebrar;

}
}

Criar a migração e atualizar o banco de dados Neste ponto do capítulo, ambos os

projetos são compilados e estamos prontos para criar outra migração para atualizar o banco de dados. Insira os
seguintes comandos no diretório do projeto AutoLot.Dal (cada comando deve ser inserido em uma linha):

dotnet ef migrations add UpdatedEntities -o EfStructures\Migrations -c AutoLot.Dal.


EfStructures.ApplicationDbContext

atualização do banco de dados dotnet ef UpdatedEntities -c AutoLot.Dal.EfStructures.ApplicationDbContext

901
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Adicione a exibição do banco de dados e o procedimento armazenado

Há duas alterações restantes para o banco de dados. A primeira é adicionar o procedimento armazenado GetPetName do
Capítulo 21, e a segunda é adicionar uma visualização de banco de dados que combine a tabela Orders com os detalhes
Customer, Car e Make.

Adicione a classe MigrationHelpers


Criamos o procedimento armazenado e exibimos usando uma migração, que requer a codificação manual da migração.
O motivo para fazer isso (em vez de apenas abrir o Azure Data Studio e executar o código T-SQL) é colocar toda a configuração
do banco de dados em um único processo. Quando tudo está contido nas migrações, uma única chamada para atualização do
banco de dados dotnet ef garante que o banco de dados esteja atualizado, incluindo configuração do EF Core e SQL
personalizado.
Chamar o comando dotnet migrations add quando não houver nenhuma alteração de modelo ainda criará os arquivos de
migração com carimbo de data/hora apropriado com os métodos Up() e Down() vazios. Execute o seguinte para criar a migração
vazia (mas não aplique a migração):

dotnet ef migrations add SQL -o EfStructures\Migrations -c AutoLot.Dal.EfStructures.


ApplicationDbContext

Agora, adicione um novo arquivo chamado MigrationHelpers.cs na pasta EfStructures do projeto AutoLot.Dal.
Adicione uma instrução using para Microsoft.EntityFrameworkCore.Migrations, torne a classe pública e estática e adicione os
seguintes métodos, que usam o MigrationBuilder para executar instruções SQL no banco de dados:

namespace AutoLot.Dal.EfStructures { public


static class MigrationHelpers { public static void
CreateSproc(MigrationBuilder migrationBuilder)
{ migrationBuilder.Sql($@" exec (N'

CREATE PROCEDURE [dbo].[GetPetName]


@carID int, @petName nvarchar(50) output

COMO

SELECT @petName = PetName de dbo.Inventory onde Id = @carID ')");

} public static void DropSproc(MigrationBuilder migrationBuilder)


{ migrationBuilder.Sql("DROP PROCEDURE [dbo].[GetPetName]"); }

public static void CreateCustomerOrderView(MigrationBuilder migrationBuilder) { migrationBuilder.Sql($@"


exec (N'

CREATE VIEW [dbo].[CustomerOrderView]


COMO

902
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

SELECT dbo.Customers.FirstName, dbo.Customers.LastName,


dbo.Inventory.Color, dbo.Inventory.PetName, dbo.Inventory.IsDrivable,
dbo.Makes.Name AS Make FROM dbo.Orders INNER JOIN dbo.Customers ON
dbo. Orders.CustomerId = dbo.Customers.Id INNER JOIN dbo.Inventory ON
dbo.Orders.CarId = dbo.Inventory.Id INNER JOIN dbo.Makes ON dbo.Makes.Id =
dbo.Inventory.MakeId ')");

} public static void DropCustomerOrderView(MigrationBuilder migrationBuilder) {

migrationBuilder.Sql("EXEC (N' DROP VIEW [dbo].[CustomerOrderView] ')"); }

}
}

Atualizar e aplicar a migração A classe


MigrationHelpers tem dois métodos para cada objeto do SQL Server: um que cria o objeto e outro que
descarta o objeto. Lembre-se que quando uma migração é aplicada, o método Up() é executado, e quando
uma migração é revertida, o método Down() é executado. Os métodos create static vão para o método Up() da
migration, e os métodos drop vão para o método Down() da migration. Quando essa migração é aplicada, os
dois objetos do SQL Server são criados e, quando a migração é revertida, os dois objetos do SQL Server são
descartados. Aqui está a listagem atualizada do código de migração:

namespace AutoLot.Dal.EfStructures.Migrations {

classe parcial pública SQL: Migração {

substituição protegida void Up(MigrationBuilder migrationBuilder) {

MigrationHelpers.CreateSproc(migrationBuilder);
MigrationHelpers.CreateCustomerOrderView(migrationBuilder); }

substituição protegida void Down(MigrationBuilder migrationBuilder) {

MigrationHelpers.DropSproc(migrationBuilder);
MigrationHelpers.DropCustomerOrderView(migrationBuilder); }

}
}

Se você descartou seu banco de dados para executar a migração inicial, pode aplicar essa migração e seguir em frente.
Aplique a migração executando o seguinte comando:

atualização do banco de dados dotnet ef -c AutoLot.Dal.EfStructures.ApplicationDbContext

903
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Se você não descartou seu banco de dados para a primeira migração, o procedimento já existe e não pode ser criado.
A correção simples é comentar a chamada para criar o procedimento armazenado no método Up(), assim:

substituição protegida void Up(MigrationBuilder migrationBuilder) { //


MigrationHelpers.CreateSproc(migrationBuilder);

MigrationHelpers.CreateCustomerOrderView(migrationBuilder); }

Depois de aplicar essa migração pela primeira vez, descomente essa linha e tudo será processado normalmente.
Obviamente, outra opção é excluir o procedimento armazenado do banco de dados e aplicar a migração.
Isso quebra o paradigma de “um lugar para atualizações”, mas faz parte da transição do banco de dados primeiro para o código
primeiro.

ÿ Observação Você também pode escrever um código que verifique primeiro a existência de um objeto e o descarte se ele já existir,

mas acho que é um exagero para um problema que pode nunca acontecer.

Adicione o ViewModel
Agora que a visão do SQL Server está pronta, é hora de criar o ViewModel que será usado para exibir os dados da visão. O
modelo de exibição será adicionado como um DbSet<T> sem chave. A vantagem disso é que os dados podem ser consultados
usando o processo LINQ normal comum a todas as coleções DbSet<T>.

Adicione o ViewModel
Adicione uma nova pasta chamada ViewModels no projeto AutoLot.Models. Nesta pasta, adicione uma classe chamada
CustomerOrderViewModel.cs e adicione as seguintes instruções using ao arquivo:

usando System.ComponentModel.DataAnnotations.Schema; usando


Microsoft.EntityFrameworkCore;

Em seguida, atualize o código para o seguinte:

namespace AutoLot.Models.ViewModels {

[Sem chave]
public class CustomerOrderViewModel { public
string? PrimeiroNome { get; definir; } string pública?
Sobrenome { get; definir; } string pública? Cor { obter;
definir; } string pública? PetName { get; definir; } string
pública? Faça { obter; definir; } public bool? É Dirigível
{ get;set; }

[NotMapped]
public string FullDetail => $"{FirstName}
{LastName} pediu um {Color} {Make} chamado {PetName}";

904
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

string de substituição pública ToString() => FullDetail; }

A anotação de dados KeyLess indica que esta é uma entidade que trabalha com dados que não possuem uma chave
primária e pode ser otimizada como dados somente leitura (de uma perspectiva de banco de dados). As cinco primeiras
propriedades representam os dados provenientes da exibição. A propriedade FullDetail é decorada com a anotação de dados
NotMapped. Isso informa ao EF Core que essa propriedade não deve ser incluída no banco de dados nem é proveniente do banco
de dados devido a operações de consulta. A substituição ToString() também é ignorada pelo EF Core.

Adicionar o ViewModel ao ApplicationDbContext A etapa final é registrar e configurar o

CustomerOrderViewModel no ApplicationDbContext. Adicione uma instrução using para AutoLot.Models.ViewModels ao


ApplicationDbContext e adicione a propriedade DbSet<T>.

público virtual DbSet<CustomerOrderViewModel>? CustomerOrderViewModels { get; definir; }

Além de adicionar a instância DbSet<T>, a Fluent API mapeia o modelo de exibição para a exibição do SQL Server. O
método HasNoKey() Fluent API e a anotação de dados Keyless realizam a mesma coisa, com o método Fluent API substituindo a
anotação de dados. Eu prefiro manter a anotação de dados no lugar para maior clareza. Adicione o seguinte ao método
OnModelCreating():

modelBuilder.Entity<CustomerOrderViewModel>(entity => {

entidade.HasNoKey().ToView("CustomerOrderView","dbo"); });

Adicionando repositórios
Um padrão de design de acesso a dados comum é o padrão de repositório. Conforme descrito por Martin Fowler
(www.martinfowler.com/eaaCatalog/repository.html), o núcleo desse padrão é mediar entre o domínio e as camadas de
mapeamento de dados. Ter um repositório básico genérico que contém o código comum de acesso a dados ajuda a eliminar a
duplicação de código. Ter repositórios e interfaces específicos derivados de um repositório base também funciona bem com a
estrutura de injeção de dependência no ASP.NET Core.
Cada uma das entidades de domínio na camada de acesso a dados AutoLot terá um repositório fortemente tipado
para encapsular todo o trabalho de acesso a dados. Para começar, crie uma pasta chamada Repos no projeto AutoLot.Dal para
armazenar todas as aulas.

ÿ Nota Esta próxima seção não pretende ser (nem pretende ser) uma interpretação literal do padrão de projeto
do Sr. Fowler. Se você estiver interessado no padrão original que motivou esta versão, poderá encontrar mais
informações sobre o padrão de repositório em www.martinfowler.com/eaaCatalog/repository.html.

905
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Adicionando a Interface Base IRepo


A interface base IRepo expõe muitos dos métodos comuns usados no acesso a dados. Adicione uma nova pasta no projeto
AutoLot.Dal chamada Repos e, nessa pasta, crie uma nova pasta chamada Base. Adicione uma nova interface à pasta Repos\Base
chamada IRepo. Atualize as instruções using para o seguinte:

usando Sistema;
usando System.Collections.Generic;

A interface completa está listada aqui:

namespace AutoLot.Dal.Repos.Base {

interface pública IRepo<T>: IDisposable { int Add(T


entity, bool persist = true); int AddRange(entidades
IEnumerable<T>, bool persist = true); int Update(T
entity, bool persist = true); int UpdateRange(IEnumerable<T> entidades, bool
persist = true); int Delete(int id, byte[] timeStamp, bool persist = true); int Delete(T
entidade, bool persist = true); int DeleteRange(IEnumerable<T> entidades, bool persist
= true); T? Find(int? id); T? FindAsNoTracking(int id); T? FindIgnoreQueryFilters(int id);
IEnumerable<T> GetAll(); IEnumerable<T> GetAllIgnoreQueryFilters(); void
ExecuteQuery(string sql, object[] sqlParametersObjects); int SalvarAlterações(); } }

Adicionando o BaseRepo
Em seguida, adicione a classe denominada BaseRepo ao diretório Repos\Base. Essa classe implementará a interface IRepo e
fornecerá a funcionalidade principal para repositórios específicos de tipo (a seguir). Atualize as instruções using para o seguinte:

usando Sistema;
usando System.Collections.Generic; usando
System.Linq; usando AutoLot.Dal.EfStructures;
usando AutoLot.Dal.Exceptions; usando
AutoLot.Models.Entities.Base; usando
Microsoft.EntityFrameworkCore;

Torne a classe genérica com o tipo T e restrinja o tipo a BaseEntity e new(), o que limita o
tipos para classes que possuem um construtor sem parâmetros. Implemente a interface IRepo<T> da seguinte maneira:

classe abstrata pública BaseRepo<T> : IRepo<T> onde T : BaseEntity, new()

906
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

O repositório precisa de uma instância do ApplicationDbContext injetada em um construtor. Quando usado com o
contêiner ASP.NET Core DI, o contêiner manipulará o tempo de vida do contexto. Um segundo construtor aceitará
DbContextOptions e precisará criar uma instância do ApplicationDbContext. Esse contexto precisará ser descartado. Como
essa classe é abstrata, ambos os construtores são protegidos. Adicione o seguinte código para o ApplicationDbContext
público, os dois construtores e o padrão Dispose:

private readonly bool _disposeContext; public


ApplicationDbContext Contexto { obter; }

BaseRepo protegido (contexto ApplicationDbContext) {

Contexto = contexto;
_disposeContext = falso; }

BaseRepo protegido(DbContextOptions<ApplicationDbContext> opções): this(novo


ApplicationDbContext(opções)) {

_disposeContext = verdadeiro; }

public void Dispose()


{ Dispose(true);
GC.SuppressFinalize(this); }
private bool _isDisposed; void
virtual protegido Dispose(bool
disposing) { if (_isDisposed) {

retornar;
}

if (disposing) { if
(_disposeContext)
{ Context.Dispose(); }

} _isDisposed = verdadeiro; }

~BaseRepo() {

Descarte(falso); }

907
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

As propriedades DbSet<T> de ApplicationDbContext podem ser referenciadas usando o método


Context.Set<T>(). Crie uma propriedade pública chamada Table do tipo DbSet<T> e defina o valor no construtor
inicial, assim:

public Tabela DbSet<T> { get; }


baseRepo protegida (contexto ApplicationDbContext) {

Contexto = contexto;
Tabela = Context.Set<T>();
_disposeContext = falso; }

Implemente o método SaveChanges O BaseRepo tem

um SaveChanges() que chama o método SaveChanges() substituído que demonstra o padrão de exceção
personalizado. Adicione o seguinte código à classe BaseRepo:

public int SalvarAlterações() {

tente { return Context.SaveChanges(); }


catch (CustomException ex) {

//Deve lidar de forma inteligente - lançamento já registrado;

} catch (Exceção ex) {

//Deve registrar e manipular de forma


inteligente throw new CustomException("Ocorreu um erro ao atualizar o banco de dados",
ex); }
}

Implemente os métodos de leitura comuns A próxima

série de métodos retorna registros usando instruções LINQ. O método Find() pega o(s) valor(es) da chave
primária e pesquisa o ChangeTracker primeiro. Se a entidade já estiver sendo rastreada, a instância rastreada
será retornada. Caso contrário, o registro é recuperado do banco de dados.

público virtual T? Find(int?id) => Table.Find(id);

Os dois métodos Find() adicionais estendem o método base Find(). O próximo método demonstra como
recuperar um registro, mas não adicioná-lo ao ChangeTracker usando AsNoTrackingWithIdentityResolution().
Adicione o seguinte código à classe:

público virtual T? FindAsNoTracking(int id) =>


Table.AsNoTrackingWithIdentityResolution().FirstOrDefault(x => x.Id == id);

908
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

A próxima variação remove os filtros de consulta da entidade e usa a versão abreviada


(pulando o método Where()) para obter FirstOrDefault(). Adicione o seguinte à classe:

público T? FindIgnoreQueryFilters(int id) =>


Table.IgnoreQueryFilters().FirstOrDefault(x => x.Id == id);

Os métodos GetAll() retornam todos os registros da tabela. O primeiro os recupera na ordem do banco
de dados e o segundo reveza os filtros de consulta.

public virtual IEnumerable<T> GetAll() => Tabela; public


virtual IEnumerable<T> GetAllIgnoreQueryFilters()
=> Table.IgnoreQueryFilters();

O método ExecuteQuery() existe para executar stored procedures:

public void ExecuteQuery(string sql, object[] sqlParametersObjects)


=> Context.Database.ExecuteSqlRaw(sql, sqlParametersObjects);

Os métodos Add, Update e Delete O próximo bloco de

código a ser adicionado agrupa os métodos Add(), Update() e Remove() correspondentes na propriedade
DbSet<T> específica. O parâmetro persist determina se o repositório executa SaveChanges() imediatamente
quando os métodos de repositório Add()/Update()/Remove() são chamados. Todos os métodos são marcados
como virtuais para permitir a substituição de downstream. Adicione o seguinte código à sua classe:

public virtual int Add(T entity, bool persist = true) { Table.Add(entity);


retorno persiste? SalvarAlterações() : 0; } public virtual int
AddRange(IEnumerable<T> entidades, bool persist = true)
{ Table.AddRange(entities); retorno persiste? SalvarAlterações() :
0; } public virtual int Update(T entity, bool persist = true)
{ Table.Update(entity); retorno persiste? SalvarAlterações() : 0; } public virtual int
UpdateRange(IEnumerable<T> entidades, bool persist = true) { Table.UpdateRange(entities);
retorno persiste? SalvarAlterações() : 0; } public virtual int Delete(T entity, bool persist =
true) { Table.Remove(entity); retorno persiste? SalvarAlterações() : 0; } public virtual int
DeleteRange(IEnumerable<T> entidades, bool persist = true)

909
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

{
Table.RemoveRange(entidades);
retorno persiste? SalvarAlterações() : 0; }

Existe mais um método Delete() que não segue o mesmo padrão. Esse método usa EntityState para
conduzir a operação de exclusão, que é usada com bastante frequência em operações ASP.NET Core para reduzir o
tráfego de rede. Está listado aqui:

public int Delete(int id, byte[] timeStamp, bool persist = true) {

var entidade = new T {Id = id, TimeStamp = timeStamp};


Context.Entry(entity).State = EntityState.Deleted; retorno persiste?
SalvarAlterações() : 0; }

Isso conclui a classe BaseRepo e agora é hora de construir os repositórios específicos da entidade.

Interfaces de repositório específicas da entidade Cada

entidade terá um repositório fortemente tipado derivado de BaseRepo<T> e uma interface que implementa IRepo<T>. Adicione
uma nova pasta chamada Interfaces no diretório Repos no projeto AutoLot.Dal. Nesse novo diretório, adicione cinco interfaces.

ICarRepo.cs

ICreditRiskRepo.cs

ICustomerRepo.cs

IMakelRepo.cs

IOrderRepo.cs

As próximas seções completam as interfaces.

A interface do repositório de carros


Abra a interface ICarRepo.cs. Adicione as seguintes instruções using ao topo do arquivo:

usando System.Collections.Generic; usando


AutoLot.Models.Entities; usando
AutoLot.Dal.Repos.Base;

Altere a interface para public e implemente IRepo<Category> da seguinte forma:

namespace AutoLot.Dal.Repos.Interfaces {

interface pública ICarRepo : IRepo<Car> {

IEnumerable<Car> GetAllBy(int makeId); string


GetPetName(int id); }

}
910
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

A interface de risco de crédito

Abra a interface ICreditRiskRepo.cs. Essa interface não adiciona nenhuma funcionalidade além do que
é fornecido no BaseRepo. Atualize o código para o seguinte:

usando AutoLot.Models.Entities;
usando AutoLot.Dal.Repos.Base;
namespace AutoLot.Dal.Repos.Interfaces
{ public interface ICreditRiskRepo :
IRepo<CreditRisk> { } }

A interface do repositório do cliente Abra a


interface ICustomerRepo.cs. Essa interface não adiciona nenhuma funcionalidade além do que é
fornecido no BaseRepo. Atualize o código para o seguinte:

usando AutoLot.Models.Entities;
usando AutoLot.Dal.Repos.Base;
namespace AutoLot.Dal.Repos.Interfaces
{ public interface ICustomerRepo :
IRepo<Customer> { } }

A interface Make Repository Abra a


interface IMakeRepo.cs. Essa interface não adiciona nenhuma funcionalidade além do que é fornecido
no BaseRepo. Atualize o código para o seguinte:

usando AutoLot.Models.Entities;
usando AutoLot.Dal.Repos.Base;
namespace AutoLot.Dal.Repos.Interfaces
{ public interface IMakeRepo : IRepo<Make>
{}}

A interface do repositório de pedidos


Abra a interface IOrderRepo.cs. Adicione as seguintes instruções using ao topo do arquivo:

usando System.Collections.Generic;
usando System.Linq; usando
AutoLot.Models.Entities; usando
AutoLot.Dal.Repos.Base; usando
AutoLot.Models.ViewModels;

911
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Altere a interface para public e implemente IRepo<Order> da seguinte forma:

namespace AutoLot.Dal.Repos.Interfaces {

interface pública IOrderRepo : IRepo<Ordem> {

IQueryable<CustomerOrderViewModel> GetOrdersViewModel(); }

Isso completa a interface, pois todos os terminais de API necessários são cobertos na classe base.

Implemente os repositórios específicos da entidade Os repositórios


implementados obtêm a maior parte de sua funcionalidade da classe base. Esta seção cobre a funcionalidade
adicionada ou substituída do repositório base. No diretório Repos do projeto AutoLot.Dal, adicione as cinco
classes repo.

CarRepo.cs

CreditRiskRepo.cs

CustomerRepo.cs

MakeRepo.cs

OrderRepo.cs

As próximas seções completam os repositórios.

O repositório de carros
Abra a classe CarRepo.cs e adicione as seguintes instruções using ao topo do arquivo:

usando System.Collections.Generic;
usando System.Data; usando System.Linq;
usando AutoLot.Dal.EfStructures; usando
AutoLot.Models.Entities; usando
AutoLot.Dal.Repos.Base; usando
AutoLot.Dal.Repos.Interfaces; usando
Microsoft.Data.SqlClient; usando
Microsoft.EntityFrameworkCore;

Altere a classe para public, herde de BaseRepo<Car> e implemente ICarRepo.

namespace AutoLot.Dal.Repos {

public class CarRepo : BaseRepo<Car>, ICarRepo { }

912
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Cada um dos repositórios deve implementar os dois construtores do BaseRepo.

public CarRepo(ApplicationDbContext context): base(context) { } internal


CarRepo(DbContextOptions<ApplicationDbContext> options) : base(options) { }

Adicione substituições para GetAll() e GetAllIgnoreQueryFilters() para incluir a propriedade MakeNavigation


e ordene pelos valores PetName.

substituição pública IEnumerable<Car> GetAll()


=> Mesa
.Include(c => c.MakeNavigation)
.OrderBy(o => o.PetName);

substituição pública IEnumerable<Car> GetAllIgnoreQueryFilters()


=> Mesa
.Include(c => c.MakeNavigation)
.OrderBy(o => o.PetName)
.IgnoreQueryFilters();

Implemente o método GetAllBy(). Este método deve definir o filtro de consulta no contexto antes
executando. Inclua a propriedade de navegação Make e classifique por valor PetName.

public IEnumerable<Car> GetAllBy(int makeId) {

return
Table .Where(x => x.MakeId == makeId)
.Include(c => c.MakeNavigation)
.OrderBy(c => c.PetName);
}

Adicione uma substituição para Find() para incluir a propriedade MakeNavigation e ignorar filtros de consulta.

substituição pública Carro? Localizar(int? id)


=>
Tabela .IgnoreQueryFilters()
.Where(x => x.Id == id)
.Include(m => m.MakeNavigation)
.FirstOrDefault();

Adicione o método para obter o valor PetName de um carro usando o procedimento armazenado.

string pública GetPetName(int id) {

var parâmetroId = new SqlParameter {

ParameterName = "@carId",
SqlDbType = SqlDbType.Int, Value
= id, };

913
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

var parameterName = new SqlParameter {

ParameterName = "@petName",
SqlDbType = SqlDbType.NVarChar,
Tamanho = 50, Direção =
ParameterDirection.Output };

_ =
Context.Database .ExecuteSqlRaw("EXEC [dbo].[GetPetName] @carId, @petName
OUTPUT",parameterId, parameterName); return (string)parameterName.Value; }

O Repositório CreditRisk
Abra a classe CreditRiskRepo.cs e adicione as seguintes instruções using ao topo do arquivo:

usando AutoLot.Dal.EfStructures;
usando AutoLot.Dal.Models.Entities;
usando AutoLot.Dal.Repos.Base; usando
AutoLot.Dal.Repos.Interfaces; usando
Microsoft.EntityFrameworkCore;

Altere a classe para public, herde de BaseRepo<CreditRisk>, implemente ICreditRiskRepo e adicione os dois
construtores necessários.

namespace AutoLot.Dal.Repos {

public class CreditRiskRepo : BaseRepo<CreditRisk>, ICreditRiskRepo {

public CreditRiskRepo(ApplicationDbContext context) : base(context) { } internal


CreditRiskRepo(

DbContextOptions<ApplicationDbContext> opções):
base(opções) { }

}
}

O Repositório do Cliente
Abra a classe CustomerRepo.cs e adicione as seguintes instruções using à parte superior do arquivo:

usando System.Collections.Generic;
usando System.Linq; usando
AutoLot.Dal.EfStructures; usando
AutoLot.Dal.Models.Entities; usando
AutoLot.Dal.Repos.Base;

914
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

usando AutoLot.Dal.Repos.Interfaces; usando


Microsoft.EntityFrameworkCore;

Altere a classe para public, herde de BaseRepo<Customer>, implemente ICustomerRepo e adicione o


dois construtores necessários.

namespace AutoLot.Dal.Repos {

public class CustomerRepo : BaseRepo<Cliente>, ICustomerRepo {

public CustomerRepo(ApplicationDbContext context): base(contexto)

{ } interno CustomerRepo(
DbContextOptions<ApplicationDbContext> opções): base(opções)

{}
}
}

A etapa final é adicionar o método que retorna todos os registros de Clientes com seus pedidos classificados por
Sobrenome. Adicione o seguinte método à classe:

substituição pública IEnumerable<Customer> GetAll()


=>
Tabela .Include(c => c.Pedidos)
.OrderBy(o => o.PersonalInformation.LastName);

O Repositório Make
Abra a classe MakeRepo.cs e adicione as seguintes instruções using ao topo do arquivo:

usando System.Collections.Generic; usando


System.Linq; usando AutoLot.Dal.EfStructures;
usando AutoLot.Dal.Models.Entities; usando
AutoLot.Dal.Repos.Base; usando
AutoLot.Dal.Repos.Interfaces; usando
Microsoft.EntityFrameworkCore;

Altere a classe para public, herde de BaseRepo<Make>, implemente IMakeRepo e adicione os dois construtores
necessários.

namespace AutoLot.Dal.Repos {

public class MakeRepo : BaseRepo<Make>, IMakeRepo {

public MakeRepo(ApplicationDbContext context)


: base(contexto)

915
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

{}

interno MakeRepo(
DbContextOptions<ApplicationDbContext> opções): base(opções)

{}
}
}

Os métodos finais a serem substituídos são os métodos GetAll(), classificando os valores Make por nome.

substituição pública IEnumerable<Make> GetAll()


=> Tabela.OrderBy(m => m.Nome);
substituição pública IEnumerable<Make> GetAllIgnoreQueryFilters()
=> Table.IgnoreQueryFilters().OrderBy(m => m.Name);

O Repositório de Pedidos

Abra a classe OrderRepo.cs e adicione as seguintes instruções using à parte superior do arquivo: using
AutoLot.Dal.EfStructures; usando AutoLot.Dal.Models.Entities; usando AutoLot.Dal.Repos.Base; usando
AutoLot.Dal.Repos.Interfaces; usando Microsoft.EntityFrameworkCore;

Altere a classe para public, herde de BaseRepo<Order> e implemente IOrderRepo.

namespace AutoLot.Dal.Repos {

public class OrderRepo : BaseRepo<Pedido>, IOrderRepo {

public OrderRepo(ApplicationDbContext context)


: base(contexto)

{}

pedidoRepo interno(
DbContextOptions<ApplicationDbContext> opções): base(opções)

{}
}
}

O método final a ser implementado é o método GetOrderViewModel(), que retorna um IQueryable<Cus


tomOrderViewModel> na exibição do banco de dados.

916
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

public IQueryable<CustomerOrderViewModel> GetOrdersViewModel() {

return Context.CustomerOrderViewModels!.AsQueryable(); }

Isso completa todos os repositórios. A próxima seção criará o código para descartar, criar e propagar o banco de dados.

Banco de dados programático e tratamento de migração


A propriedade Database de DbContext fornece métodos programáticos para descartar e criar o banco de dados, bem como
executar todas as migrações. A Tabela 23-1 descreve os métodos relacionados a essas operações.

Tabela 23-1. Trabalhando programaticamente com o banco de dados

Membro do Banco de Dados Significado na Vida

Assegurar Excluído Descarta o banco de dados se ele existir. Não faz nada se não existir.

Garantido-criado Cria o banco de dados se ele não existir. Não faz nada se isso acontecer. Cria as tabelas e
colunas com base nas classes acessíveis das propriedades DbSet<T>. Não aplica nenhuma
migração. Observação: isso não deve ser usado em conjunto com migrações.

Migrar Cria o banco de dados se ele não existir. Aplica todas as migrações ao banco de dados.

Conforme mencionado na tabela, o método VerifyCreated() criará o banco de dados se ele não existir e criará as tabelas,
colunas e índices com base no modelo de entidade. Não aplica nenhuma migração. Se você estiver usando migrações (como
nós), isso apresentará erros ao trabalhar com o banco de dados e você terá que enganar o EF Core (como fizemos anteriormente)
para acreditar que as migrações foram aplicadas. Você também terá que aplicar quaisquer objetos SQL personalizados ao banco
de dados manualmente. Ao trabalhar com migrações, sempre use o método Migrate() para criar o banco de dados
programaticamente e não o método VerifyCreated().

Solte, crie e limpe o banco de dados


Durante o desenvolvimento, pode ser benéfico descartar e recriar o banco de dados de desenvolvimento e, em seguida,
semeá-lo com dados de amostra. Isso cria um ambiente onde o teste (manual ou automatizado) pode ser executado sem
medo de arruinar outros testes devido à alteração dos dados. Crie uma nova pasta chamada Initialization no projeto AutoLot.Dal.
Nesta pasta, crie uma nova classe chamada SampleDataInitializer.cs. Na parte superior do arquivo, atualize as instruções
using para o seguinte:

usando Sistema;
usando System.Collections.Generic; usando
System.Linq; usando AutoLot.Dal.EfStructures;
usando AutoLot.Models.Entities; usando
AutoLot.Models.Entities.Base; usando
Microsoft.EntityFrameworkCore; usando
Microsoft.EntityFrameworkCore.Storage;

917
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Torne a classe pública e estática conforme mostrado aqui:

namespace AutoLot.Dal.Initialization {

classe estática pública SampleDataInitializer { }

Crie um método chamado DropAndCreateDatabase que usa uma instância de ApplicationDbContext como o
único parâmetro. Esse método usa a propriedade Database de ApplicationDbContext para primeiro excluir o banco de
dados (usando o método VerifyDeleted()) e, em seguida, cria o banco de dados (usando o método Migrate()).

public static void DropAndCreateDatabase(ApplicationDbContext context) {

context.Database.EnsureDeleted();
context.Database.Migrate(); }

Crie outro método chamado ClearData() que exclua todos os dados do banco de dados e redefina os valores
de identidade para a chave primária de cada tabela. O método percorre uma lista de entidades de domínio e usa a
propriedade DbContext Model para obter o esquema e o nome da tabela para os quais cada entidade é mapeada. Em
seguida, ele executa uma instrução delete e redefine a identidade de cada tabela usando o método ExecuteSqlRaw() na
propriedade DbContext Database.

interno static void ClearData(ApplicationDbContext context) {

var entidades = novo[] {

typeof(Pedido).FullName,
typeof(Cliente).FullName,
typeof(Carro).FullName,
typeof(Marca).FullName,
typeof(CreditRisk).FullName };
foreach (var entityName em entidades)
{

var entidade = context.Model.FindEntityType(entityName); var tableName


= entity.GetTableName(); var schemaName = entity.GetSchema();
context.Database.ExecuteSqlRaw($"DELETE FROM {schemaName}.
{tableName}"); context.Database.ExecuteSqlRaw($"DBCC CHECKIDENT (\"{schemaName}.
{tableName}\", RESEED, 1);"); }

ÿ Observação O método ExecuteSqlRaw() da fachada do banco de dados deve ser usado com cuidado para evitar possíveis
ataques de injeção de SQL.

918
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Agora que você pode descartar e criar o banco de dados e limpar os dados, é hora de criar os métodos que
adicionarão os dados de amostra.

Inicialização de Dados
Vamos construir nosso próprio sistema de propagação de dados que pode ser executado sob demanda. A primeira etapa é criar
os dados de amostra e, em seguida, adicionar os métodos no SampleDataInitializer usado para carregar os dados de amostra no
banco de dados.

Criar os dados de amostra Adicione um

novo arquivo chamado SampleData.cs à pasta Initialization. Torne a classe pública e estática e atualize as instruções
using para o seguinte:

usando System.Collections.Generic; usando


AutoLot.Dal.Entities; usando
AutoLot.Dal.Entities.Owned;

namespace AutoLot.Dal.Initialization {

classe estática pública SampleData { }

O arquivo consiste em cinco métodos estáticos que criam os dados de amostra.

public static List<Customer> Customers => new() { new() {Id


= 1, PersonalInformation = new() {FirstName = "Dave",
LastName = "Brenner"}}, new() {Id = 2, PersonalInformation = new() {FirstName = "Matt", LastName = "Walton"}},
new() {Id = 3, PersonalInformation = new() {FirstName = "Steve", LastName = "Hagen"}}, new() {Id = 4,
PersonalInformation = new() {FirstName = "Pat", LastName = "Walton"}}, new() {Id = 5, PersonalInformation = new()
{FirstName = "Bad", LastName = "Cliente" }}, };

public static List<Make> Makes => new() { new()


{Id = 1, Name = "VW"}, new() {Id = 2, Name =
"Ford"}, new() {Id = 3 , Nome = "Saab"}, new()
{Id = 4, Nome = "Yugo"}, new() {Id = 5, Nome =
"BMW"}, new() {Id = 6, Nome = "Pinto "}, };

public static List<Car> Inventory => new() { new() {Id =


1, MakeId = 1, Color = "Black", PetName = "Zippy"},
new() {Id = 2, MakeId = 2, Color = "Rust", PetName = "Rusty"},

919
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

new() {Id = 3, MakeId = 3, Color = "Black", PetName = "Mel"}, new() {Id


= 4, MakeId = 4, Color = "Yellow", PetName = "Clunker"}, new() {Id = 5,
MakeId = 5, Color = "Black", PetName = "Bimmer"}, new() {Id = 6, MakeId =
5, Color = "Green", PetName = "Hank"}, new() {Id = 7, MakeId = 5, Color =
"Pink", PetName = "Pinky"}, new() {Id = 8, MakeId = 6, Color = "Black",
PetName = "Pete"}, new() {Id = 9, MakeId = 4, Color = "Brown", PetName =
"Brownie"}, new() {Id = 10, MakeId = 1, Color = "Rust", PetName = "Lemon",
IsDrivable = falso}, };

public static List<Pedido> Pedidos => new() {

new() {Id = 1, CustomerId = 1, CarId = 5}, new()


{Id = 2, CustomerId = 2, CarId = 1}, new() {Id =
3, CustomerId = 3, CarId = 4} , new() {Id = 4,
CustomerId = 4, CarId = 7}, new() {Id = 5,
CustomerId = 5, CarId = 10}, };

public static List<CreditRisk> CreditRisks => new() {

novo()
{
Id = 1,
CustomerId = Clientes[4].Id,
PersonalInformation = new() {

FirstName = Clientes[4].PersonalInformation.FirstName,
LastName = Clientes[4].PersonalInformation.LastName
}

} };

Carregar os dados de amostra O


método interno SeedData() na classe SampleDataInitializer adiciona os dados dos métodos SampleData
em uma instância do ApplicationDbContext e, em seguida, persiste os dados no banco de dados.

interno static void SeedData(ApplicationDbContext context) {

tentar

ProcessInsert(contexto, contexto.Clientes!, SampleData.Clientes);


ProcessInsert(contexto, contexto.Makes!, SampleData.Makes);
ProcessInsert(contexto, contexto.Carros!, SampleData.Inventory);
ProcessInsert(contexto, contexto.Pedidos!, SampleData.Pedidos);
ProcessInsert(contexto, contexto.CreditRisks!, SampleData.CreditRisks); } catch
(Exceção ex) {

Console.WriteLine(ex);

920
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

//Defina um ponto de interrupção aqui para determinar quais são os problemas


lançados;

} static void ProcessInsert<TEntity>(


Contexto ApplicationDbContext,
tabela DbSet<TEntity>,
List<TEntity> registros) onde TEntity : BaseEntity {

if (table.Any()) {

retornar;
}
estratégia IExecutionStrategy = context.Database.CreateExecutionStrategy();
estrategia.Execute(() => {

usando var transaction = context.Database.BeginTransaction(); tentar {

var metaData = context.Model.FindEntityType(typeof(TEntity).FullName);


context.Database.ExecuteSqlRaw( $"SET IDENTITY_INSERT {metaData.GetSchema()}.
{metaData.GetTableName()} ON"); tabela.AddRange(registros); context.SaveChanges();
context.Database.ExecuteSqlRaw( $"SET IDENTITY_INSERT {metaData.GetSchema()}.
{metaData.GetTableName()} OFF"); transação.Commit(); } catch (Exceção) {

transação.Rollback(); } });

}
}

O método SeedData() usa uma função local para processar os dados. Ele primeiro verifica se a tabela possui
algum registro e, caso contrário, processa os dados de amostra. Uma ExecutionStrategy é criada a partir da fachada
do banco de dados e é usada para criar uma transação explícita, necessária para ativar e desativar a inserção de
identidade. Os registros são adicionados e, se tudo for bem-sucedido, a transação é confirmada; caso contrário, é
revertido.
Os dois métodos são públicos e usados para redefinir o banco de dados. InitializeData() descarta e recria o
banco de dados antes de semeá-lo, e o método ClearDatabase() apenas exclui todos os registros, redefine a
identidade e, em seguida, semeia os dados.

public static void InitializeData(ApplicationDbContext context) {

DropAndCreateDatabase(contexto);
SeedData(contexto); }

921
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

public static void ClearAndReseedDatabase(ApplicationDbContext context) {

ClearData(contexto);
SeedData(contexto); }

Configurando os Test-Drives
Em vez de criar um aplicativo cliente para testar a camada de acesso a dados AutoLot concluída, usaremos testes de integração
automatizados. Os testes demonstrarão criar, ler, atualizar e excluir chamadas para o banco de dados. Isso nos permite
examinar o código sem a sobrecarga de criar outro aplicativo. Cada um dos testes nesta seção executará uma consulta (criar,
ler, atualizar ou excluir) e, em seguida, terá uma ou mais instruções Assert para validar se o resultado é o esperado.

Criar o projeto Para começar,

vamos configurar uma plataforma de teste de integração usando xUnit, uma estrutura de teste compatível com .NET Core.
Comece adicionando um novo projeto de teste xUnit chamado AutoLot.Dal.Tests. No Visual Studio, esse tipo de projeto é
denominado xUnit Test Project (.NET Core).

ÿ Nota Os testes de unidade são projetados para testar uma única unidade de código. O que faremos ao longo deste
capítulo é criar tecnicamente testes de integração, já que estamos testando o código C# e o EF Core até o banco de
dados e vice-versa.

Na interface de linha de comando, execute o seguinte comando:

dotnet new xunit -lang c# -n AutoLot.Dal.Tests -o .\AutoLot.Dal.Tests -f net5.0 dotnet sln .\Chapter23_AllProjects.sln
add AutoLot.Dal.Tests

Adicione os seguintes pacotes NuGet ao projeto AutoLot.Dal.Tests:

• Microsoft.EntityFrameworkCore

• Microsoft.EntityFrameworkCore.SqlServer

• Microsoft.Extensions.Configuration.Json

Como a versão do pacote Microsoft.NET.Test.Sdk que vem com o modelo de projeto xUnit geralmente é inferior à versão
disponível atualmente, use o Gerenciador de Pacotes NuGet para atualizar todos os pacotes NuGet.
Em seguida, adicione referências de projeto a AutoLot.Models e AutoLot.Dal.
Se você estiver usando a CLI, execute os seguintes comandos (observe que os comandos removem e adicionam novamente
Microsoft.NET.Test.Sdk para garantir que a versão mais recente seja referenciada):

dotnet adicionar pacote AutoLot.Dal.Tests Microsoft.EntityFrameworkCore dotnet adicionar


pacote AutoLot.Dal.Tests Microsoft.EntityFrameworkCore.SqlServer dotnet adicionar pacote AutoLot.Dal.Tests
Microsoft.Extensions.Configuration.Json dotnet remover pacote AutoLot.Dal.Tests Microsoft.NET .Test.Sdk
dotnet adicionar pacote AutoLot.Dal.Tests Microsoft.NET.Test.Sdk dotnet adicionar referência
AutoLot.Dal.Tests AutoLot.Dal dotnet adicionar referência AutoLot.Dal.Tests AutoLot.Models

922
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Configurar o Projeto
Para recuperar a cadeia de conexão em tempo de execução, usaremos os recursos de configuração do .NET Core usando
um arquivo JSON. Adicione um arquivo JSON, chamado appsettings.json, ao projeto e adicione suas informações de string de
conexão no arquivo no seguinte formato (atualize sua string de conexão do que está listado aqui conforme necessário):

{
"ConnectionStrings":
{ "AutoLot": "server=.,5433;Database=AutoLotFinal;User Id=sa;Password=P@ssw0rd;"

}}

Atualize o arquivo de projeto para que o arquivo de configurações seja copiado para a pasta de saída em cada
compilação. Faça isso adicionando o seguinte ItemGroup ao arquivo AutoLot.Dal.Tests.csproj:

<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Sempre</CopyToOutputDirectory> </None>

</ItemGroup>

Criar o auxiliar de teste


A classe TestHelper manipulará a configuração do aplicativo, bem como criará uma nova instância de
ApplicationDbContext. Adicione uma nova classe estática pública chamada TestHelpers.cs na raiz do projeto.
Atualize as instruções using para o seguinte:

usando System.IO;
usando AutoLot.Dal.EfStructures; usando
Microsoft.EntityFrameworkCore; usando
Microsoft.EntityFrameworkCore.Storage; usando
Microsoft.Extensions.Configuration;

namespace AutoLot.Dal.Tests {

classe estática pública TestHelpers { } }

Adicione dois métodos estáticos públicos para criar instâncias de IConfiguration e ApplicationDbContext
Aulas. Adicione o seguinte código à classe:

public static IConfiguration GetConfiguration() =>


novo Construtor de Configuração()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", verdadeiro, verdadeiro)
.Construir();

923
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

public static ApplicationDbContext GetContext(configuração IConfiguration) {

var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>(); var connectionString =


configuration.GetConnectionString("AutoLot"); optionsBuilder.UseSqlServer(connectionString,
sqlOptions => sqlOptions.EnableRetryOn Failure()); retornar novo ApplicationDbContext(optionsBuilder.Options); }

Observe a chamada para EnableRetryOnFailure() (em negrito). Como lembrete, isso opta pela estratégia de nova
tentativa de execução do SQL Server, que repetirá automaticamente as operações que falharam devido a erros transitórios.
Adicione outro método estático que criará uma nova instância do ApplicationDbContext usando o mesmo
conexão e transação como o original transmitido. Este método mostra como criar uma instância do ApplicationDbContext de
uma instância existente para compartilhar a conexão e a transação.

public static ApplicationDbContext GetSecondContext(


ApplicationDbContextoldContext,
IDbContextTransaction trans) {

var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();


optionsBuilder.UseSqlServer(oldContext.Database.GetDbConnection(), sqlServerOptions =>
sqlServerOptions.EnableRetryOnFailure()); var contexto = new
ApplicationDbContext(optionsBuilder.Options); context.Database.UseTransaction(trans.GetDbTransaction());
contexto de retorno;

Adicione a classe BaseTest


Agora adicione uma nova pasta chamada Base ao projeto e adicione um novo arquivo de classe chamado BaseTest.cs a essa pasta.
Atualize as instruções using para o seguinte:

usando Sistema;
usando System.Data;
usando AutoLot.Dal.EfStructures; usando
Microsoft.EntityFrameworkCore; usando
Microsoft.EntityFrameworkCore.Storage; usando
Microsoft.Extensions.Configuration;

Torne a classe abstrata e implemente IDisposable. Adicione duas propriedades somente leitura protegidas para manter
as instâncias IConfiguration e ApplicationDbContext e descarte a instância ApplicationDbContext no método virtual
Dispose().

namespace AutoLot.Dal.Tests.Base {

classe abstrata pública BaseTest : IDisposable {

protegido somente leitura IConfiguration Configuração; protegido


somente leitura ApplicationDbContext Contexto;

924
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

public virtual void Dispose() {

Context.Dispose(); }

}
}

A estrutura de teste xUnit fornece um mecanismo para executar o código antes e depois de cada teste ser executado.
As classes de teste (chamadas de fixtures) que implementam a interface IDisposable executarão o código no construtor de
classe (neste caso, o construtor de classe base e o construtor de classe derivada) antes de cada teste ser executado (também chamado
de configuração de teste) e o código em o método Dispose (tanto na classe derivada quanto na classe base) é executado após a
execução de cada teste (também chamado de desmontagem de teste ).
Adicione um construtor protegido que cria uma instância de IConfiguration e a atribui à variável de classe
protegida. Use a configuração para criar uma instância de ApplicationDbContext usando a classe TestHelper e
também atribua-a à variável de classe protegida.

BaseTest() protegido {

Configuração = TestHelpers.GetConfiguration(); Contexto =


TestHelpers.GetContext(Configuração); }

Adicionar os auxiliares de execução de teste transacionados

Os dois métodos finais na classe BaseTest permitem a execução de métodos de teste em uma transação. Os
métodos usarão um delegado Action como um único parâmetro, criarão uma transação explícita (ou inscreverão uma
transação existente), executarão o delegado Action e, em seguida, reverterão a transação. Fazemos isso para que
qualquer teste de criação/atualização/exclusão deixe o banco de dados no estado em que estava antes da execução
do teste. Como ApplicationDbContext está configurado para permitir a repetição de erros transitórios, todo o processo deve ser
executado a partir da estratégia de execução de ApplicationDbContext.
ExecuteInATransaction() é executado usando uma única instância de ApplicationDbContext. O
método ExecuteInASharedTransaction() permite que várias instâncias ApplicationDbContext compartilhem uma
transação. Você aprenderá mais sobre esses métodos posteriormente neste capítulo. Por enquanto, adicione o
seguinte código à sua classe BaseTest:

protected void ExecuteInATransaction(Ação actionToExecute) {

var estrategia = Context.Database.CreateExecutionStrategy();


estrategia.Execute(() => {

usando var trans = Context.Database.BeginTransaction();


actionToExecute(); trans.Rollback(); }); }

protected void ExecuteInASharedTransaction(Action<IDbContextTransaction> actionToExecute) {

var estrategia = Context.Database.CreateExecutionStrategy();


estrategia.Execute(() =>

925
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

{
usando IDbContextTransaction trans =
Context.Database.BeginTransaction(IsolationLevel.ReadUncommitted);
actionToExecute(trans);
trans.Rollback(); }); }

Adicione a classe de dispositivo de teste do VerifyAutoLotDatabase


A estrutura de teste xUnit fornece um mecanismo para executar o código antes que qualquer um dos testes seja executado (referido
como configuração de fixação ) e depois que todos os testes forem executados (referido como desmontagem de fixação ). Essa
prática geralmente não é recomendada, mas, em nosso caso, queremos garantir que o banco de dados seja criado e carregado
com dados antes da execução de qualquer teste, e não antes da execução de cada teste. Classes de teste que implementam
IClassFixture<T> onde T: TestFixtureClass terá o código do construtor de T (o TestFixtureClass) executado antes da execução de
qualquer teste e o código Dispose() será executado após a conclusão de todos os testes.
Adicione uma nova classe chamada VerifyAutoLotDatabaseTestFixture.cs ao diretório Base e implemente
IDisposable. Torne a classe pública e selada e adicione as seguintes instruções using:

usando
Sistema; usando AutoLot.Dal.Initialization;

namespace AutoLot.Dal.Tests.Base {

classe pública selada GarantirAutoLotDatabaseTestFixture: IDisposable { }

O código do construtor cria uma instância de IConfiguration e, em seguida, cria uma instância de
ApplicationDbContext usando a instância de IConfiguration. Em seguida, ele chama o método
ClearAndReseedDatabase() do SampleDataInitializer. A linha final dispõe da instância de contexto. Em nossos
exemplos, o método Dispose() não tem nenhum trabalho a fazer (mas precisa estar lá para satisfazer a
interface IDisposable). A listagem a seguir mostra o construtor e o método Dispose():

public GuaranteeAutoLotDatabaseTestFixture() {

var configuração = TestHelpers.GetConfiguration(); var contexto


= TestHelpers.GetContext(configuração);
SampleDataInitializer.ClearAndReseedDatabase(contexto);
context.Dispose(); }

public void Dispose() { }

926
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Adicionar as classes de teste de integração


O próximo passo é adicionar as classes que irão realizar os testes automatizados. Essas classes são chamadas de
equipamentos de teste. Adicione uma nova pasta chamada IntegrationTests na pasta AutoLot.Dal.Tests e adicione quatro
arquivos chamados CarTests.cs, CustomerTests.cs, MakeTests.cs e OrderTests.cs a essa pasta.
Dependendo dos recursos do executor de teste, os testes xUnit são executados em série dentro de um dispositivo de teste
(classe), mas em paralelo em todos os dispositivos de teste (classes). Isso pode ser problemático ao executar testes de integração
que interagem com um banco de dados, pois os testes estão interagindo com um único banco de dados. A execução pode ser
alterada para serial entre dispositivos de teste adicionando-os à mesma coleção de teste. As coleções de teste são definidas pelo
nome usando o atributo Collection na classe. Adicione o seguinte atributo Collection ao topo de todas as quatro classes:

[Collection("Testes de Integração")]

Em seguida, herde de BaseTest e implemente a interface IClassFixture em ambas as classes. Atualize as instruções using
para cada classe para corresponder ao seguinte:

//CarTests.cs
usando System.Collections.Generic; usando
System.Linq; usando AutoLot.Dal.Exceptions;
usando AutoLot.Dal.Repos; usando
AutoLot.Dal.Tests.Base; usando
AutoLot.Models.Entities; usando
Microsoft.EntityFrameworkCore; usando
Microsoft.EntityFrameworkCore.ChangeTracking;
usando Microsoft.EntityFrameworkCore.Query; usando
Microsoft.EntityFrameworkCore.Storage; usando Xunit; namespace
AutoLot.Dal.Tests.IntegrationTests {

[Collection("Integation Tests")] public class


CarTests : BaseTest, IClassFixture<EnsureAutoLotDatabaseTestFixture> { } }

//CustomerTests.cs
usando System.Collections.Generic; usando
Sistema; usando System.Linq; usando
System.Linq.Expressions; usando
AutoLot.Dal.Tests.Base; usando
AutoLot.Models.Entities; usando
Microsoft.EntityFrameworkCore; usando Xunit;
namespace AutoLot.Dal.Tests.IntegrationTests {

[Collection("Integation Tests")] public class


CustomerTests : BaseTest, IClassFixture<EnsureAutoLotDatabaseTestFixture> { } }

927
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

//MakeTests.cs
usando System.Linq;
usando AutoLot.Dal.Repos;
usando AutoLot.Dal.Repos.Interfaces;
usando AutoLot.Dal.Tests.Base; usando
AutoLot.Models.Entities; usando
Microsoft.EntityFrameworkCore; usando
Microsoft.EntityFrameworkCore.ChangeTracking; usando
Xunit; namespace AutoLot.Dal.Tests.IntegrationTests {

[Collection("Integation Tests")]
public class MakeTests : BaseTest, IClassFixture<EnsureAutoLotDatabaseTestFixture> { }

//OrderTests.cs
using System.Linq;
usando AutoLot.Dal.Repos;
usando AutoLot.Dal.Repos.Interfaces;
usando AutoLot.Dal.Tests.Base; usando
Microsoft.EntityFrameworkCore; usando
Xunit; namespace
AutoLot.Dal.Tests.IntegrationTests {

[Collection("Integation Tests")]
public class OrderTests : BaseTest, IClassFixture<EnsureAutoLotDatabaseTestFixture> { }

Para a classe MakeTests, adicione um construtor que crie uma instância de MakeRepo e atribua a instância a
uma variável de nível de classe somente leitura privada. Substitua o método Dispose() e, nesse método, descarte o
repositório.

[Collection("Integration Tests")] public


class MakeTests : BaseTest, IClassFixture<EnsureAutoLotDatabaseTestFixture> {

private readonly IMakeRepo _repo;


public MakeTests() {

_repo = new MakeRepo(Contexto); }


public override void Dispose() {

_repo.Dispose(); }

...
}

928
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Repita para a classe OrderTests, usando OrderRepo em vez de MakeRepo.

[Collection("Integration Tests")] public


class OrderTests : BaseTest, IClassFixture<EnsureAutoLotDatabaseTestFixture> {

private readonly IOrderRepo _repo; public


OrderTests() {

_repo = new PedidoRepo(Contexto); }


public override void Dispose() {

_repo.Dispose(); }

...
}

Métodos de teste de fato e teoria


Métodos de teste sem parâmetros são referidos como fatos (e usam o atributo Fact). Os testes que usam
parâmetros são referidos como teorias (e usam o atributo Theory) e podem executar várias iterações com
diferentes valores passados para o método de teste como parâmetros. Para demonstrar esses tipos de teste, crie
uma nova classe chamada SampleTests.cs no projeto AutoLot.Dal.Tests. Atualize as instruções using para o seguinte:

usando Xunit;

namespace AutoLot.Dal.Tests {

public class SampleTests { }

O primeiro teste a ser criado é um teste de fato. Com testes de fato, todos os valores estão contidos no método de teste. O
seguinte exemplo (trivial) testa o 3+2=5:

[Fato]
public void SimpleFactTest() {

Assert.Equal(5,3+2); }

Ao usar testes do tipo Teoria, os valores dos testes são passados para o método de teste. Os valores podem
vir do atributo InlineData, métodos ou classes. Para nosso propósito, usaremos apenas o atributo InlineData. Crie o
seguinte teste que forneceu diferentes adendos e o resultado esperado para o teste:

[Teoria]
[InlineData(3,2,5)]
[InlineData(1,-1,0)]

929
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

public void SimpleTheoryTest(int addend1, int addend2, int esperadoResult) {

Assert.Equal(expectedResult,addend1+addend2); }

ÿ Observação Para obter mais informações sobre a estrutura de teste xUnit, consulte a documentação localizada em https://
xunit.net/.

Executando os testes Embora os

testes do xUnit possam ser executados a partir da linha de comando (usando dotnet test), é uma melhor experiência do
desenvolvedor (na minha opinião) usar o Visual Studio para executar os testes. Inicie o Test Explorer no menu Test para ter
acesso à execução e depuração de todos os testes selecionados.

Consultando o banco de dados


Lembre-se de que a criação de instâncias de entidade a partir de dados de banco de dados geralmente envolve a execução
de uma instrução LINQ nas propriedades DbSet<T>. As instruções LINQ são convertidas em SQL pelo provedor de banco de
dados e o mecanismo de tradução LINQ, e os dados apropriados são lidos do banco de dados. Os dados também podem ser
carregados usando o método FromSqlRaw() ou FromSqlInterpolated() usando strings SQL brutas. As entidades carregadas nas
coleções DbSet<T> são adicionadas ao ChangeTracker por padrão, mas podem ser adicionadas sem rastreamento. Os dados
carregados em coleções DbSet<T> sem chave nunca são rastreados.
Se as entidades relacionadas já estiverem carregadas no DbSet<T>, o EF Core conectará as novas instâncias junto com
as propriedades de navegação. Por exemplo, se os carros forem carregados na coleção DbSet<Car> e os pedidos relacionados
forem carregados no DbSet<Order> da mesma instância ApplicationDbContext, o método Car.
A propriedade de navegação Orders retornará as entidades Order relacionadas sem requerer o banco de dados.
Muitos dos métodos demonstrados aqui têm versões assíncronas disponíveis. A sintaxe das consultas LINQ são
estruturalmente as mesmas, portanto, demonstrarei apenas a versão nonasync.

Estado da Entidade

Quando uma entidade é criada lendo dados do banco de dados, o valor EntityState é definido como Inalterado.

Consultas LINQ O tipo de

coleção DbSet<T> implementa (entre outras interfaces) IQueryable<T>. Isso permite que comandos C# LINQ sejam usados
para criar consultas para obter dados do banco de dados. Embora todas as instruções C# LINQ estejam disponíveis para uso
com o tipo de coleção DbSet<T>, algumas instruções LINQ podem não ser suportadas pelo provedor de banco de dados e
instruções LINQ adicionais são adicionadas pelo EF Core. As instruções LINQ sem suporte que não podem ser traduzidas para
a linguagem de consulta do provedor de banco de dados lançarão uma exceção de tempo de execução, a menos que a
instrução seja a última instrução da cadeia LINQ. Se uma instrução LINQ sem suporte for a instrução final na cadeia LINQ, ela
será executada no lado do cliente (em C#).

930
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

ÿ Nota Este livro não é uma referência LINQ completa, mas mostra apenas alguns exemplos. Para obter mais
exemplos de consultas LINQ, a Microsoft publicou 101 exemplos de LINQ em https://code.msdn.microsoft.com/101-
LINQ Samples-3fb9811b.

Execução LINQ
Como lembrete, ao usar LINQ para consultar o banco de dados para uma lista de entidades, a consulta não é executada até que
a consulta seja iterada, convertida em List<T> (ou uma matriz) ou vinculada a um controle de lista ( como uma grade de dados).
Para consultas de registro único, a instrução é executada imediatamente quando a chamada de registro único (First(), Single(),
etc.) é usada.
Novidade no EF Core 5, você pode chamar o método ToQueryString() na maioria das consultas LINQ para examinar a
consulta executada no banco de dados. Para consultas divididas, o método ToQueryString() retorna apenas a primeira consulta que
será executada. Quando disponível, os testes nesta próxima seção definem uma variável (qs) para esse valor para que você possa
examinar a consulta durante a depuração dos testes.
O primeiro conjunto de testes (a menos que especificamente mencionado de outra forma) está na classe CustomerTests.cs.

Obter todos os registros

Para obter todos os registros de uma tabela, basta usar a propriedade DbSet<T> diretamente sem nenhuma instrução LINQ.
Adicione o seguinte fato:

[Fato]
public void ShouldGetAllOfTheCustomers() { var qs =
Context.Customers.ToQueryString(); var clientes =
Context.Customers.ToList(); Assert.Equal(5,
clientes.Contagem); }

A instrução é traduzida no seguinte SQL:

SELECT [c].[Id], [c].[TimeStamp], [c].[FirstName], [c].[FullName], [c].[LastName]


DE [Dbo].[Clientes] AS [c]

O mesmo processo é usado para entidades Keyless, como o CustomerOrderViewModel, que é configurado para obter seus dados
do CustomerOrderView.

modelBuilder.Entity<CustomerOrderViewModel>().HasNoKey().ToView("CustomerOrderView", "dbo");

A instância DbSet<T> para modelos de exibição fornece todo o poder de consulta de DbSet<T> para uma entidade com chave.
A diferença está nos recursos de atualização. As alterações do modelo de exibição não podem ser mantidas no banco de dados,
enquanto as entidades com chave podem. Adicione o seguinte teste à classe OrderTest.cs para mostrar a obtenção de dados da exibição:

public void ShouldGetAllViewModels() { var qs =


Context.Orders.ToQueryString(); var pedidos =
Context.Orders.ToList();

931
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Assert.NotEmpty(pedidos);
Assert.Equal(5,pedidos.Contagem); }

A instrução é traduzida no seguinte SQL:

SELECIONE [c].[Cor], [c].[Nome], [c].[É Dirigível], [c].[Sobrenome], [c].[Marca], [c].


[Nome do animal de estimação]

FROM [dbo].[CustomerOrderView] AS [c]

Filtrar Registros
O método Where() é usado para filtrar registros do DbSet<T>. Múltiplos métodos Where() podem ser encadeados fluentemente
para construir dinamicamente a consulta. Os métodos Where() encadeados são sempre combinados como cláusulas e. Para
criar uma instrução or, use a mesma cláusula Where().
O teste a seguir retorna o cliente cujo sobrenome começa com W (não diferencia maiúsculas de minúsculas):

[Fato]
public void DeveGetCustomersWithLastNameW() {

IQueryable<Customer> query = Context.Customers .Where(x


=> x.PersonalInformation.LastName.StartsWith("W")); var qs =
query.ToQueryString(); List<Cliente> clientes = query.ToList(); Assert.Equal(2,
clientes.Contagem); }

A consulta LINQ é traduzida no seguinte SQL:

SELECT [c].[Id], [c].[TimeStamp], [c].[FirstName], [c].[FullName], [c].[LastName]


DE [Dbo].[Clientes] AS [c]
ONDE [c].[LastName] NÃO É NULO E ([c].[LastName] LIKE N'W%')

O teste a seguir retorna o cliente onde o sobrenome começa com um W (não diferencia maiúsculas de minúsculas) e o primeiro
nome começa com um M (não diferencia maiúsculas de minúsculas) e demonstra o encadeamento de métodos Where() em uma consulta LINQ:

[Fato]
public void ShouldGetCustomersWithLastNameWAndFirstNameM() {

IQueryable<Customer> query = Context.Customers .Where(x


=> x.PersonalInformation.LastName.StartsWith("W"))
.Where(x => x.PersonalInformation.FirstName.StartsWith("M"));
var qs = query.ToQueryString();
List<Cliente> clientes = query.ToList();
Assert.Single(clientes); }

O teste a seguir retorna o cliente onde o sobrenome começa com um W (não diferencia maiúsculas de minúsculas) e o
o primeiro nome começa com um M (não diferencia maiúsculas de minúsculas) usando um único método Where():

932
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

[Fato]
public void ShouldGetCustomersWithLastNameWAndFirstNameM() {

Consulta IQueryable<Cliente> = Context.Clientes


.Where(x => x.PersonalInformation.LastName.StartsWith("W") &&
x.PersonalInformation.FirstName.StartsWith("M"));
var qs = query.ToQueryString();
List<Cliente> clientes = query.ToList();
Assert.Single(clientes); }

Ambas as consultas são traduzidas para o seguinte SQL:

SELECT [c].[Id], [c].[TimeStamp], [c].[FirstName], [c].[FullName], [c].[LastName]


DE [Dbo].[Clientes] AS [c]
ONDE ([c].[LastName] NÃO É NULO E ([c].[LastName] LIKE N'W%'))
E ([c].[Nome] NÃO É NULO E ([c].[Nome] LIKE N'M%'))

O teste a seguir retorna o cliente onde o sobrenome começa com W (não diferencia maiúsculas de minúsculas) ou o sobrenome
começa com H (não diferencia maiúsculas de minúsculas):

[Fato]
public void DeveGetCustomersWithLastNameWOrH() {

Consulta IQueryable<Cliente> = Context.Clientes


.Where(x => x.PersonalInformation.LastName.StartsWith("W") ||
x.PersonalInformation.LastName.StartsWith("H"));
var qs = query.ToQueryString();
List<Cliente> clientes = query.ToList(); Assert.Equal(3,
clientes.Contagem); }

Isso é traduzido no seguinte SQL:SELECT [c].[Id], [c].[TimeStamp], [c].


[FirstName], [c].[FullName], [c].[LastName]
DE [Dbo].[Clientes] AS [c]
ONDE ([c].[LastName] NÃO É NULO E ([c].[LastName] LIKE N'W%'))
OU ([c].[LastName] NÃO É NULO E ([c].[LastName] LIKE N'H%'))

O teste a seguir retorna o cliente onde o sobrenome começa com um W (não diferencia maiúsculas de minúsculas) ou o último
o nome começa com um H (não diferencia maiúsculas de minúsculas). Este teste demonstra o uso do método EF.Functions.Like().
Observe que você mesmo deve incluir o curinga (%).

[Fato]
public void DeveGetCustomersWithLastNameWOrH() {

Consulta IQueryable<Cliente> = Context.Clientes


.Where(x => EF.Functions.Like(x.PersonalInformation.LastName, "W%") ||
EF.Functions.Like(x.PersonalInformation.LastName, "H%"));
var qs = query.ToQueryString();
List<Cliente> clientes = query.ToList(); Assert.Equal(3,
clientes.Contagem); }

933
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Isso é traduzido no seguinte SQL (observe que não verifica nulo):

SELECT [c].[Id], [c].[TimeStamp], [c].[FirstName], [c].[FullName], [c].[LastName]


DE [Dbo].[Clientes] AS [c]
WHERE ([c].[LastName] LIKE N'W%') OU ([c].[LastName] LIKE N'H%')

O teste a seguir na classe CarTests.cs usa uma teoria para testar o número de registros de carro na
tabela de inventário com base no MakeId (o método IgnoreQueryFilters() é abordado na seção "Filtros de
consulta global"):

[Teoria]
[InlineData(1, 2)]
[InlineData(2, 1)]
[InlineData(3, 1)]
[InlineData(4, 2)]
[InlineData(5, 3)]
[InlineData(6, 1)]
public void ShouldGetTheCarsByMake(int makeId, int esperadoCount) {

IQueryable<Car> query =
Context.Cars.IgnoreQueryFilters().Where(x => x.MakeId == makeId);
var qs = query.ToQueryString(); var
carros = query.ToList();
Assert.Equal(expectedCount, cars.Count); }

Cada linha InlineData torna-se um teste exclusivo no executor de teste. Para este exemplo, seis testes são processados,
e seis consultas são executadas no banco de dados. Aqui está o SQL de um dos testes (a única diferença nas
consultas dos outros testes na Teoria é o valor para MakeId):

DECLARE @__makeId_0 int = 1;


SELECIONE [i].[Id], [i].[Cor], [i].[IsDrivable], [i].[MakeId], [i].[PetName], [i].[TimeStamp]
FROM [dbo].[Inventário] AS [i]
WHERE [i].[MakeId] = @__makeId_0

O seguinte teste de teoria mostra uma consulta filtrada com CustomerOrderViewModel (coloque o teste
na classe OrderTests.cs):

[Teoria]
[InlineData("Preto",2)]
[InlineData("Rust",1)]
[InlineData("Amarelo",1)]
[InlineData("Verde",0)]
[InlineData("Rosa",1)]
[InlineData("Brown",0)]
public void ShouldGetAllViewModelsByColor(string color, int esperadoCount) {

var query = _repo.GetOrdersViewModel().Where(x=>x.Color == cor); var qs =


query.ToQueryString(); var pedidos = query.ToList();
Assert.Equal(expectedCount,orders.Count);

934
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

A consulta gerada para o primeiro teste InlineData está listada aqui:

DECLARE @__color_0 nvarchar(4000) = N'Preto';


SELECIONE [c].[Cor], [c].[Nome], [c].[É Dirigível], [c].[Sobrenome], [c].[Marca], [c].
[Nome do animal de estimação]

FROM [dbo].[CustomerOrderView] AS [c]


WHERE [c].[Cor] = @__color_0

Classificar registros

Os métodos OrderBy() e OrderByDescending() definem a(s) classificação(ões) da consulta, crescente e decrescente,


respectivamente. Se forem necessárias classificações subsequentes, use os métodos ThenBy() e ThenByDescending(). A
classificação é mostrada no seguinte teste:

[Fato]
public void DeveSortByLastNameThenFirstName() {

//Ordenar por sobrenome e depois primeiro


nome var query = Context.Customers .OrderBy(x
=> x.PersonalInformation.LastName)
.ThenBy(x => x.PersonalInformation.FirstName);
var qs = query.ToQueryString(); var
clientes = query.ToList(); //se apenas um
cliente, nada para testar if (customers.Count <= 1)
{ return; } for (int x = 0; x < clientes.Contagem - 1; x+
+) {

var pi = clientes[x].PersonalInformation; var pi2 =


clientes[x + 1].PersonalInformation; var compareLastName =
string.Compare(pi.LastName,
pi2.LastName, StringComparison.CurrentCultureIgnoreCase);
Assert.True(compareLastName <= 0); if (compareLastName != 0) continue; var
compareFirstName = string.Compare(pi.FirstName,

pi2.FirstName, StringComparison.CurrentCultureIgnoreCase);
Assert.True(compareFirstName <= 0); }

A consulta LINQ anterior é traduzida para o seguinte:

SELECT [c].[Id], [c].[TimeStamp], [c].[FirstName], [c].[FullName], [c].[LastName]


DE [Dbo].[Clientes] AS [c]
ORDER POR [c].[Sobrenome], [c].[Nome]

935
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Registros de Ordenação Reversa

O método Reverse() inverte toda a ordem de classificação, conforme demonstrado no próximo teste:

[Fato]
public void ShouldSortByFirstNameThenLastNameUsingReverse() {

//Ordenar por sobrenome, depois primeiro nome e inverter a classificação


var query = Context.Customers .OrderBy(x =>
x.PersonalInformation.LastName)
.ThenBy(x => x.PersonalInformation.FirstName)
.Reverter();
var qs = query.ToQueryString(); var
clientes = query.ToList(); //se apenas
um cliente, nada para testar if (customers.Count
<= 1) { return; }

for (int x = 0; x < clientes.Contagem - 1; x++) {

var pi1 = clientes[x].PersonalInformation; var pi2 =


clientes[x + 1].PersonalInformation; var compareLastName
= string.Compare(pi1.LastName, pi2.LastName,
StringComparison.CurrentCultureIgnoreCase); Assert.True(compareLastName
>= 0); if (compareLastName != 0) continue; var compareFirstName =
string.Compare(pi1.FirstName, pi2.FirstName,
StringComparison.CurrentCultureIgnoreCase); Assert.True(compareFirstName
>= 0); }

A consulta LINQ anterior é traduzida para o seguinte:

SELECT [c].[Id], [c].[TimeStamp], [c].[FirstName], [c].[FullName], [c].[LastName]


DE [Dbo].[Clientes] AS [c]
ORDER BY [c].[LastName] DESC, [c].[FirstName] DESC

Recuperar um Único Registro


Existem três métodos principais para retornar um único registro com uma consulta: First()/FirstOrDefault(),
Last()/LastOrDefault() e Single()/SingleOrDefault(). Embora todos os três retornem um único registro, suas
abordagens diferem. Os três métodos e suas variantes são detalhados aqui:

• First() retorna o primeiro registro que corresponde à condição da consulta e a qualquer cláusula de
ordenação. Se nenhuma ordem for especificada, o registro retornado será baseado na ordem do banco
de dados. Se nenhum registro for retornado, uma exceção será lançada.

• O comportamento FirstOrDefault() corresponde a First(), exceto que, se nenhum registro


corresponder à consulta, o método retorna o valor padrão para o tipo (nulo).

936
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

• Single() retorna o primeiro registro que corresponde à condição da consulta e a qualquer cláusula de
ordenação. Se nenhuma ordem for especificada, o registro retornado será baseado na ordem do banco
de dados. Se nenhum registro ou mais de um registro corresponder à consulta, uma exceção será
lançada.

• O comportamento SingleOrDefault() corresponde a Single() exceto que, se nenhum registro corresponder à


consulta, o método retorna o valor padrão para o tipo (nulo).

• Last() retorna o último registro que corresponde à condição de consulta e quaisquer cláusulas de
ordenação. Se nenhuma ordem for especificada, uma exceção será lançada. Se nenhum registro for
retornado, uma exceção será lançada.

• O comportamento LastOrDefault() corresponde a Last(), exceto que, se nenhum registro corresponder à


consulta, o método retorna o valor padrão para o tipo (nulo).

Todos os métodos também podem usar um Expression<Func<T, bool>> (um lambda) para filtrar o conjunto de resultados. Esse
significa que você pode colocar a expressão Where() dentro da chamada para os métodos First()/Single(). As seguintes
declarações são equivalentes:

Context.Customers.Where(c=>c.Id < 5).First();


Context.Customers.First(c=>c.Id < 5);

Devido à execução imediata das instruções LINQ de registro único, o método ToQueryString()
método não está disponível. As traduções de consulta listadas são fornecidas usando o SQL Server Profiler.

Usando primeiro

Ao usar a forma sem parâmetros de First() e FirstOrDefault(), o primeiro registro (com base na ordem do banco de dados ou em
qualquer cláusula de ordenação anterior) será retornado.
O teste a seguir obtém o primeiro registro com base na ordem do banco de dados:

[Fato]
public void GetFirstMatchingRecordDatabaseOrder() {

//Obtém o primeiro registro, ordem do banco de dados


var customer = Context.Customers.First(); Assert.Equal(1,
customer.Id); }

A consulta LINQ anterior é traduzida para o seguinte:

SELECT TOP(1) [c].[Id], [c].[TimeStamp], [c].[FirstName], [c].[FullName], [c].[LastName]


DE [Dbo].[Clientes] AS [c]

O teste a seguir obtém o primeiro registro com base na ordem “sobrenome, nome”:

[Fato]
public void GetFirstMatchingRecordNameOrder() {

//Obtém o primeiro registro, sobrenome, nome ordem var customer =


Context.Customers
.OrderBy(x => x.PersonalInformation.LastName)
.ThenBy(x => x.PersonalInformation.FirstName)

937
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

.Primeiro();
Assert.Equal(1, customer.Id); }

A consulta LINQ anterior é traduzida para o seguinte:

SELECT TOP(1) [c].[Id], [c].[TimeStamp], [c].[FirstName], [c].[FullName], [c].[LastName]


DE [Dbo].[Clientes] AS [c]
ORDER POR [c].[Sobrenome], [c].[Nome]

O teste a seguir afirma que uma exceção é lançada se não houver correspondência ao usar First():

[Fato]
public void FirstShouldThrowExceptionIfNoneMatch() {

//Filtros baseados em Id. Lança devido a nenhuma


correspondência Assert.Throws<InvalidOperationException>(() => Context.Customers.First(x => x.Id ==
10)); }

A consulta LINQ anterior é traduzida para o seguinte:

SELECT TOP(1) [c].[Id], [c].[TimeStamp], [c].[FirstName], [c].[FullName], [c].[LastName]


DE [Dbo].[Clientes] AS [c]
ONDE [c].[Id] = 10

ÿ Observação Assert.Throws() é um tipo especial de instrução assert. Ele está esperando uma exceção ao
lançado pelo código na expressão. Se uma exceção não for lançada, a asserção falhará.

Ao usar FirstOrDefault(), em vez de uma exceção, o resultado é um registro nulo quando nenhum dado é
retornado.

[Fact]
public void FirstOrDefaultShouldReturnDefaultIfNoneMatch() { //
Expression<Func<Customer>> é uma expressão lambda
Expression<Func<Customer, bool>> expression = x => x.Id == 10; //
Retorna null quando nada é encontrado var customer =
Context.Customers.FirstOrDefault(expression); Assert.Null(cliente); }

A consulta LINQ anterior é traduzida da mesma forma que a anterior:

SELECT TOP(1) [c].[Id], [c].[TimeStamp], [c].[FirstName], [c].[FullName], [c].[LastName]


DE [Dbo].[Clientes] AS [c]
ONDE [c].[Id] = 10

938
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Usando Last

Ao usar a forma sem parâmetros de Last() e LastOrDefault(), o último registro (baseado em quaisquer
cláusulas de ordenação anteriores) será retornado.
O teste a seguir obtém o último registro com base na ordem “sobrenome, nome”:

[Fato]
public void GetLastMatchingRecordNameOrder() {

//Obtém o último registro, lastname desc, primeiro nome desc order var customer
= Context.Customers
.OrderBy(x => x.PersonalInformation.LastName)
.ThenBy(x => x.PersonalInformation.FirstName)
.Durar();
Assert.Equal(4, customer.Id); }

O EF Core inverte a ordem por instruções e, em seguida, pega o top(1) para obter o resultado. Aqui está a
consulta executada:

SELECT TOP(1) [c].[Id], [c].[TimeStamp], [c].[FirstName], [c].[FullName], [c].[LastName]


DE [Dbo].[Clientes] AS [c]
ORDER BY [c].[LastName] DESC, [c].[FirstName] DESC

Usando Single

conceitualmente, Single()/SingleOrDefault() funciona da mesma forma que First()/FirstOrDefault(). A principal


diferença é que Single()/SingleOrDefault() retorna Top(2) em vez de Top(1) e gera uma exceção se dois registros
forem retornados do banco de dados.

Os testes a seguir recuperam o registro único em que Id == 1:[Fact] public void


GetOneMatchingRecordWithSingle() {

//Obtém o primeiro registro, ordem do banco de


dados var customer = Context.Customers.Single(x => x.Id == 1);
Assert.Equal(1, customer.Id); }

A consulta LINQ anterior é traduzida para o seguinte:

SELECT TOP(2) [c].[Id], [c].[TimeStamp], [c].[FirstName], [c].[FullName], [c].[LastName]


DE [Dbo].[Clientes] AS [c]
ONDE [c].[Id] = 1

Single() lançará uma exceção se nenhum registro for retornado.

[Fato]
public void SingleShouldThrowExceptionIfNoneMatch() {

//Filtros baseados em Id. Lança devido a nenhuma


correspondência Assert.Throws<InvalidOperationException>(() => Context.Customers.Single(x => x.Id == 10)); }

939
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

A consulta LINQ anterior é traduzida para o seguinte:

SELECT TOP(2) [c].[Id], [c].[TimeStamp], [c].[FirstName], [c].[FullName], [c].[LastName]


DE [Dbo].[Clientes] AS [c]
ONDE [c].[Id] = 10

Ao usar Single() ou SingleOrDefault() e mais de um registro é retornado, uma exceção é lançada.

[Fato]
public void SingleShouldThrowExceptionIfMoreThenOneMatch() {

// Lança devido a mais de uma


correspondência Assert.Throws<InvalidOperationException>(() =>
Context.Customers.Single()); }
[Fato]
public void SingleOrDefaultShouldThrowExceptionIfMoreThenOneMatch() {

// Lança devido a mais de uma


correspondência Assert.Throws<InvalidOperationException>(() =>
Context.Customers.SingleOrDefault()); }

As consultas LINQ anteriores são traduzidas para o seguinte:

SELECT TOP(2) [c].[Id], [c].[TimeStamp], [c].[FirstName], [c].[FullName], [c].[LastName]


DE [Dbo].[Clientes] AS [c]

Ao usar SingleOrDefault(), em vez de uma exceção, o resultado é um registro nulo quando nenhum dado é
retornado.

[Fato]
public void SingleOrDefaultShouldReturnDefaultIfNoneMatch() {

//Expression<Func<Customer>> é uma expressão lambda


Expression<Func<Customer, bool>> expression = x => x.Id == 10; //
Retorna null quando nada é encontrado var customer =
Context.Customers.SingleOrDefault(expression); Assert.Null(cliente);

A consulta LINQ anterior é traduzida para o seguinte:

SELECT TOP(1) [c].[Id], [c].[TimeStamp], [c].[FirstName], [c].[FullName], [c].[LastName]


DE [Dbo].[Clientes] AS [c]
ONDE [c].[Id] = 10

940
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Filtros de consulta global

Lembre-se de que há um filtro de consulta global na entidade Car para filtrar todos os carros em que IsDrivable é falso.

modelBuilder.Entity<Car>(entity =>
{ entity.HasQueryFilter(c => c.IsDrivable);

...
});

Abra a classe CarTests.cs e adicione o seguinte teste (todos os testes nas próximas seções estão na classe
CarTests.cs, a menos que especificamente mencionado de outra forma):

[Fato]
public void ShouldReturnDrivableCarsWithQueryFilterSet()
{ IQueryable<Car> query = Context.Cars; var qs = query.ToQueryString();
var carros = query.ToList(); Assert.NotEmpty(carros); Assert.Equal(9,
carros.Contagem); }

Além disso, lembre-se de que criamos 10 carros no processo de inicialização de dados e um deles é
definido como não dirigível. Quando a consulta é executada, o filtro de consulta global é aplicado e o seguinte
SQL é executado:

SELECIONE [i].[Id], [i].[Cor], [i].[IsDrivable], [i].[MakeId], [i].[PetName], [i].[TimeStamp]


FROM [dbo].[Inventário] AS [i]
WHERE [i].[IsDrivable] = CAST(1 AS bit)

ÿ Observação Filtros de consulta global também são aplicados ao carregar entidades relacionadas e ao usar
FromSqlRaw() e FromSqlInterpolated(). Estes serão abordados em breve.

Desabilitar os filtros de consulta Para

desabilitar os filtros de consulta globais para as entidades em uma consulta, adicione o método IgnoreQueryFilters() à consulta
LINQ. Isso desativa todos os filtros em todas as entidades na consulta. Se houver mais de uma entidade com um filtro de
consulta global e alguns dos filtros de entidades forem necessários, eles deverão ser adicionados aos métodos Where() da
instrução LINQ.
Adicione o seguinte teste à classe CarTests.cs, que desativa o filtro de consulta e retorna todos os registros:

[Fato]
public void ShouldGetAllOfTheCars()
{ IQueryable<Car> query =
Context.Cars.IgnoreQueryFilters(); var qs = query.ToQueryString();
var carros = query.ToList(); Assert.Equal(10, carros.Contagem); }

941
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Como seria de esperar, a cláusula where que elimina carros não dirigíveis não está mais no SQL gerado.

SELECIONE [i].[Id], [i].[Cor], [i].[IsDrivable], [i].[MakeId], [i].[PetName], [i].[TimeStamp]


FROM [dbo].[Inventário] AS [i]

Filtros de consulta nas propriedades de navegação


Além do filtro de consulta global na entidade Car, adicionamos um filtro de consulta à propriedade CarNavigation da entidade
Order.

modelBuilder.Entity<Order>().HasQueryFilter(e => e.CarNavigation!.IsDrivable);

Para ver isso em ação, adicione o seguinte teste à classe OrderTests.cs:

[Fato]
public void ShouldGetAllOrdersExceptFiltered() {

var query = Context.Orders.AsQueryable(); var qs =


query.ToQueryString(); var pedidos = query.ToList();
Assert.NotEmpty(pedidos); Assert.Equal(4,pedidos.Contagem);

O SQL gerado está listado aqui:

SELECT [o].[Id], [o].[CarId], [o].[CustomerId], [o].[TimeStamp]


DE [Dbo].[Pedidos] AS [o]
INNER JOIN
( SELECT [i].[Id], [i].[IsDrivable]
FROM [dbo].[Inventário] AS [i]
WHERE [i].[IsDrivable] = CAST(1 AS bit)\r\n) AS [t] ON [o].[CarId] = [t].[Id]
WHERE [t].[IsDrivable] = CAST(1 AS bit)

Como a propriedade de navegação CarNavigation é uma propriedade de navegação obrigatória , a tradução da consulta
motor usa um INNER JOIN, eliminando os registros de Ordem onde o Carro não pode ser dirigido.
Para retornar todos os registros, adicione IgnoreQueryFilters() à sua consulta LINQ.

Carregue Dados Relacionados Antecipadamente

Conforme discutido no capítulo anterior, as entidades vinculadas por meio de propriedades de navegação podem
ser instanciadas em uma consulta usando o carregamento antecipado. O método Include() indica uma junção para a entidade
relacionada e o método ThenInclude() é usado para junções subsequentes. Ambos os métodos serão demonstrados nesses
testes. Conforme mencionado anteriormente, quando os métodos Include()/ThenInclude() são convertidos em SQL, os
relacionamentos obrigatórios usam uma junção interna e os relacionamentos opcionais usam uma junção esquerda.
Adicione o seguinte teste à classe CarTests.cs para mostrar um único Include():

[Fato]
public void ShouldGetAllOfTheCarsWithMakes() {

942
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

IIncludableQueryable<Car, Make?> query =


Context.Cars.Include(c => c.MakeNavigation); var
queryString = query.ToQueryString(); var carros =
query.ToList(); Assert.Equal(9, carros.Contagem); }

O teste adiciona a propriedade MakeNavigation aos resultados, realizando uma junção interna com o seguinte SQL
sendo executado. Observe que o filtro de consulta global está em vigor.

SELECIONE [i].[Id], [i].[Cor], [i].[IsDrivable], [i].[MakeId], [i].[PetName], [i].


[TimeStamp],
[m].[Id], [m].[Nome], [m].[TimeStamp]
FROM [dbo].[Inventário] AS [i]
INNER JOIN [dbo].[Faz] AS [m] ON [i].[MakeId] = [m].[Id]
WHERE [i].[IsDrivable] = CAST(1 AS bit)

O segundo teste usa dois conjuntos de dados relacionados. A primeira é obter as informações do Make (igual ao anterior
teste), enquanto o segundo está recebendo os Pedidos e depois os Clientes anexados aos Pedidos. Todo o teste também
está filtrando os registros do Carro que possuem algum pedido. As relações opcionais geram junções à esquerda.

[Fato]
public void ShouldGetCarsOnOrderWithRelatedProperties() {

IIncludableQueryable<Car, Customer?> query = Context.Cars .Where(c


=> c.Orders.Any())
.Include(c => c.MakeNavigation)
.Include(c => c.Orders).ThenInclude(o => o.CustomerNavigation);
var queryString = query.ToQueryString(); var
carros = query.ToList(); Assert.Equal(4,
carros.Contagem); carros.ParaCada(c => {

Assert.NotNull(c.MakeNavigation);
Assert.NotNull(c.Orders.ToList()[0].CustomerNavigation); }); }

Aqui está a consulta gerada:

SELECIONE [i].[Id], [i].[Cor], [i].[IsDrivable], [i].[MakeId], [i].[PetName], [i].


[TimeStamp],
[m].[Id], [m].[Nome], [m].[TimeStamp], [t0].[Id], [t0].[CarId], [t0].[CustomerId ], [t0].[TimeStamp], [t0].
[Id0], [t0].[TimeStamp0], [t0].[FirstName], [t0].[FullName], [t0].[LastName], [t0].[Id1]

FROM [dbo].[Inventário] AS [i]


INNER JOIN [dbo].[Faz] AS [m] ON [i].[MakeId]=[m].[Id]
LEFT JOIN(SELECT [o].[Id], [o].[CarId], [o].[CustomerId], [o].[TimeStamp],
[c].[Id] AS [Id0], [c].[TimeStamp] AS [TimeStamp0], [c].[FirstName], [c].[FullName], [c].[LastName],
[t ].[Id] AS [Id1]
FROM [dbo].[Pedidos] AS [o]
INNER JOIN(SELECT [i0].[Id], [i0].[IsDrivable]

943
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

FROM [dbo].[Inventário] AS [i0]


WHERE [i0].[IsDrivable]=CAST(1 AS BIT)) AS [t] ON [o].
[CarId]=[t].[Id]
INNER JOIN [dbo].[Clientes] AS [c] ON [o].[CustomerId]=[c].[Id]
WHERE [t].[IsDrivable]=CAST(1 AS BIT)) AS [t0] ON [i].[Id]=[t0].[CarId]
WHERE([i].[IsDrivable]=CAST(1 AS BIT))AND EXISTS (SELECT 1
FROM [dbo].[Orders] AS [o0]
INNER JOIN(SELECT [i1].[Id], [i1].
[Cor], [i1].[IsDrivable], [i1].

[MakeId], [i1].[PetName], [i1].[TimeStamp]


FROM [dbo].[Inventário] AS
[i1]
ONDE [i1].
[IsDrivable]=CAST(1 AS BIT)) AS [t1] ON [o0].[CarId]=[t1].[Id]
WHERE([t1].[IsDrivable]=CAST(1 AS BIT))
AND([i].[Id]=[o0].[CarId]))
ORDER BY [i].[Id], [m].[Id], [t0].[Id], [t0].[Id1], [t0].[Id0];

Dividindo consultas em dados relacionados


Quanto mais junções forem adicionadas a uma consulta LINQ, mais complexa se tornará a consulta resultante. Uma novidade no
EF Core 5 é a capacidade de executar junções complicadas como consultas divididas. Consulte o capítulo anterior para obter a
discussão completa, mas para resumir, adicionar o método AsSplitQuery() à consulta LINQ instrui o EF Core a dividir a chamada
para o banco de dados em várias chamadas. Isso pode ganhar eficiência com o risco de inconsistência de dados. Adicione o
seguinte teste ao seu dispositivo de teste:

[Fato]
public void ShouldGetCarsOnOrderWithRelatedPropertiesAsSplitQuery() {

Consulta IQueryable<Car> = Context.Cars.Where(c => c.Orders.Any())


.Include(c => c.MakeNavigation)
.Include(c => c.Orders).ThenInclude(o => o.CustomerNavigation)
.AsSplitQuery();
var carros = query.ToList();
Assert.Equal(4, carros.Contagem);
carros.ParaCada(c => {

Assert.NotNull(c.MakeNavigation);
Assert.NotNull(c.Orders.ToList()[0].CustomerNavigation); }); }

O método ToQueryString() retorna apenas a primeira consulta, então as seguintes consultas foram capturadas usando
Perfilador do SQL Server:

SELECIONE [i].[Id], [i].[Cor], [i].[IsDrivable], [i].[MakeId], [i].[PetName], [i].


[TimeStamp], [m].[Id], [m].[Nome], [m].[TimeStamp]
FROM [dbo].[Inventário] AS [i]
INNER JOIN [dbo].[Faz] AS [m] ON [i].[MakeId] = [m].[Id]
WHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND EXISTS (

944
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

SELECIONE 1 DE [Dbo].[Pedidos] AS [o]


INNER JOIN
( SELECT [i0].[Id], [i0].[Color], [i0].[IsDrivable], [i0].[MakeId], [i0].[PetName], [i0].[TimeStamp ]

FROM [dbo].[Inventário] AS [i0]


WHERE [i0].[IsDrivable] = CAST(1 AS bit)
) AS [t] ON [o].[CarId] = [t].[Id]
WHERE ([t].[IsDrivable] = CAST(1 AS bit)) AND ([i].[Id] = [o].[CarId]))
ORDEM POR [i].[Id], [m].[Id]

SELECIONE [t0].[Id], [t0].[CarId], [t0].[CustomerId], [t0].[TimeStamp], [t0].[Id1], [t0].


[TimeStamp1], [t0].[FirstName], [t0].[FullName], [t0].[LastName], [i].[Id], [m].[Id]
FROM [dbo].[Inventário] AS [i]
INNER JOIN [dbo].[Faz] AS [m] ON [i].[MakeId] = [m].[Id]
INNER JOIN
( SELECT [o].[Id], [o].[CarId], [o].[CustomerId], [o].[TimeStamp], [c].[Id] AS [Id1], [c ].
[TimeStamp] AS [TimeStamp1], [c].[FirstName], [c].[FullName], [c].[LastName]
DE [Dbo].[Pedidos] AS [o]
INNER JOIN
( SELECT [i0].[Id], [i0].[IsDrivable]
FROM [dbo].[Inventário] AS [i0]
WHERE [i0].[IsDrivable] = CAST(1 AS bit)
) AS [t] ON [o].[CarId] = [t].[Id]
INNER JOIN [Dbo].[Clientes] AS [c] ON [o].[CustomerId] = [c].[Id]
WHERE [t].[IsDrivable] = CAST(1 AS bit)
) AS [t0] ON [i].[Id] = [t0].[CarId]
WHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND EXISTS (

SELECIONE 1 DE [Dbo].[Pedidos] AS [o0]


INNER JOIN
( SELECT [i1].[Id], [i1].[Color], [i1].[IsDrivable], [i1].[MakeId], [i1].[PetName], [i1].[TimeStamp ]

FROM [dbo].[Inventário] AS [i1]


WHERE [i1].[IsDrivable] = CAST(1 bit AS)
) AS [t1] ON [o0].[CarId] = [t1].[Id]
WHERE ([t1].[IsDrivable] = CAST(1 AS bit)) AND ([i].[Id] = [o0].[CarId]))
ORDEM POR [i].[Id], [m].[Id]

Se você divide suas consultas ou não, depende das necessidades de sua empresa.

Filtrando Dados Relacionados


O EF Core 5 apresenta a capacidade de filtrar ao incluir propriedades de coleção. Antes do EF Core 5, a única maneira de
obter uma lista filtrada para uma propriedade de navegação de coleção era usar o carregamento explícito. Adicione o
seguinte teste à classe MakeTests.cs, que demonstra a obtenção de todos os registros de marca e os carros são tão amarelos:

[Fato]
public void ShouldGetAllMakesAndCarsThatAreYellow() {

var query = Context.Makes.IgnoreQueryFilters()


.Include(x => x.Cars.Where(x => x.Color == "Yellow"));
945
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

var qs = query.ToQueryString(); var faz


= query.ToList(); Assert.NotNull(faz);
Assert.NotEmpty(faz);
Assert.NotEmpty(makes.Where(x =>
x.Cars.Any())); Assert.Empty(makes.First(m => m.Id ==
1).Carros); Assert.Empty(makes.First(m => m.Id == 2).Carros);
Assert.Empty(makes.First(m => m.Id == 3).Cars);
Assert.Single(makes.First(m => m.Id == 4).Cars);
Assert.Empty(makes.First(m => m.Id == 5).Carros); }

O SQL gerado é o seguinte:

SELECIONE [m].[Id], [m].[Nome], [m].[TimeStamp], [t].[Id], [t].[Cor], [t].[IsDrivable],


[t].[MakeId], [t].[PetName], [t].[TimeStamp]
FROM [dbo].[Faz] AS [m]
LEFT JOIN
( SELECT [i].[Id], [i].[Color], [i].[IsDrivable], [i].[MakeId], [i].[PetName], [i].[TimeStamp ]

FROM [dbo].[Inventário] AS [i]


WHERE [i].[Cor] = N'Amarelo') AS [t] ON [m].[Id] = [t].[MakeId]
ORDER POR [m].[Id], [t].[Id]

Alterar a consulta para uma consulta dividida produz este SQL (coleção do SQL Server Profiler):

SELECIONE [m].[Id], [m].[Nome], [m].[TimeStamp]


FROM [dbo].[Faz] AS [m]
ORDEM POR [m].[Id]

SELECT [t].[Id], [t].[Cor], [t].[IsDrivable], [t].[MakeId], [t].[PetName], [t].


[TimeStamp], [m].[Id]
FROM [dbo].[Faz] AS [m]
INNER JOIN
( SELECT [i].[Id], [i].[Cor], [i].[IsDrivable], [i].[MakeId], [i].[PetName], [i].
[TimeStamp]
FROM [dbo].[Inventário] AS [i]
WHERE [i].[Cor] = N'Amarelo'
) AS [t] ON [m].[Id] = [t].[MakeId]
ORDEM POR [m].[Id]

Carregar dados relacionados explicitamente

Se os dados relacionados precisarem ser carregados depois que a entidade principal foi consultada na memória, as entidades
relacionadas podem ser recuperadas do banco de dados com chamadas de banco de dados subsequentes. Isso é acionado
usando o método Entry() no DbContext derivado. Ao carregar entidades nas extremidades de um relacionamento um-para-
muitos, use o método Collection() no resultado Entry. Para carregar entidades em uma extremidade de um relacionamento um-
para-muitos (ou em um relacionamento um-para-um), use o método Reference(). Chamar Query() no método Collection() ou
Reference() retorna um IQueryable<T> que pode ser usado para obter a string de consulta (conforme mostrado nos testes a
seguir) e para gerenciar filtros de consulta (conforme mostrado na próxima seção) . Para executar a consulta e carregar o(s)
registro(s), chame o método Load() no método Collection(), Reference() ou Query(). A execução da consulta ocorre
imediatamente quando Load() é chamado. 946
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

O teste a seguir (de volta à classe CarTests.cs) mostra como carregar uma propriedade de navegação de referência
na entidade Car:

[Fato]
public void ShouldGetReferenceRelatedInformationExplicitly() {

var carro = Context.Cars.First(x => x.Id == 1);


Assert.Null(carro.MakeNavigation); var query =
Context.Entry(carro).Reference(c => c.MakeNavigation).Query(); var qs =
query.ToQueryString(); query.Load(); Assert.NotNull(carro.MakeNavigation); }

O SQL gerado é o seguinte:

DECLARE @__p_0 int =


1; SELECIONE [m].[Id], [m].[Nome], [m].[TimeStamp]
FROM [dbo].[Faz] AS [m]
ONDE [m].[Id] = @__p_0

Este teste mostra como carregar uma propriedade de navegação de coleção na entidade Car:

[Fato]
public void ShouldGetCollectionRelatedInformationExplicitly() {

var carro = Context.Cars.First(x => x.Id == 1);


Assert.Empty(carro.Pedidos); var query =
Context.Entry(carro).Collection(c => c.Orders).Query(); var qs =
query.ToQueryString(); query.Load(); Assert.Single(carro.Pedidos); }

O SQL gerado é o seguinte:

DECLARE @__p_0 int =


1; SELECT [o].[Id], [o].[CarId], [o].[CustomerId], [o].[TimeStamp]
DE [Dbo].[Pedidos] AS [o]
INNER JOIN
( SELECT [i].[Id], [i].[IsDrivable]
FROM [dbo].[Inventário] AS [i]
WHERE [i].[IsDrivable] = CAST(1 AS bit)
) AS [t] ON [o].[CarId] = [t].[Id]
WHERE ([t].[IsDrivable] = CAST(1 AS bit)) AND ([o].[CarId] = @__p_0)

947
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Carregar dados relacionados explicitamente com filtros de consulta

Além de moldar as consultas geradas ao carregar dados relacionados, os filtros de consulta globais estão ativos
ao carregar dados relacionados explicitamente. Faça o seguinte teste (na classe MakeTests.cs):

[Teoria]
[InlineData(1,1)]
[InlineData(2,1)]
[InlineData(3,1)]
[InlineData(4,2)]
[InlineData(5,3)]
[InlineData(6,1)]
public void ShouldGetAllCarsForAMakeExplicitlyWithQueryFilters(int makeId, int carCount) {

var make = Context.Makes.First(x => x.Id == makeId);


IQueryable<Car> query = Context.Entry(make).Collection(c => c.Cars).Query(); var
qs = query.ToQueryString(); query.Load(); Assert.Equal(carCount,make.Cars.Count()); }

Este teste é semelhante a ShouldGetTheCarsByMake() da seção “Filter Records”. No entanto, em vez de obter apenas os
registros de carro que possuem um determinado MakeId, o teste primeiro obtém um registro de marca e, em seguida, carrega
explicitamente os registros de carro para o registro de marca na memória. A consulta gerada é mostrada aqui:

DECLARE @__p_0 int =


5; SELECIONE [i].[Id], [i].[Cor], [i].[IsDrivable], [i].[MakeId], [i].[PetName], [i].[TimeStamp]
FROM [dbo].[Inventário] AS [i]
WHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND ([i].[MakeId] = @__p_0)

Observe que o filtro de consulta ainda está sendo usado, mesmo que a entidade principal na consulta seja o
registro Make. Para desativar os filtros de consulta ao carregar registros explicitamente, chame IgnoreQueryFilters() em
conjunto com o método Query(). Aqui está o teste que desativa os filtros de consulta (novamente, na classe MakeTests.cs):

[Teoria]
[InlineData(1, 2)]
[InlineData(2, 1)]
[InlineData(3, 1)]
[InlineData(4, 2)]
[InlineData(5, 3)]
[InlineData(6, 1)]
public void ShouldGetAllCarsForAMakeExplicitly(int makeId, int carCount) {

var make = Context.Makes.First(x => x.Id == makeId);


Consulta IQueryable<Car> =
Context.Entry(make).Collection(c => c.Cars).Query().IgnoreQueryFilters(); var
qs = query.IgnoreQueryFilters().ToQueryString(); query.Load(); Assert.Equal(carCount,
make.Cars.Count()); }

948
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Consultas SQL com LINQ


Se a instrução LINQ para uma consulta específica for muito complicada ou o teste revelar que o desempenho é
inferior ao desejado, os dados poderão ser recuperados usando uma instrução SQL bruta usando o método
FromSqlRaw() ou FromSqlInterpolated() de DbSet<T>. A instrução SQL pode ser uma instrução select T-SQL
embutida, um procedimento armazenado ou uma função com valor de tabela. Se a consulta for uma consulta aberta
(por exemplo, instrução T-SQL sem um ponto e vírgula de terminação), as instruções LINQ poderão ser adicionadas ao
método FromSqlRaw()/FromSqlInterpolated( ) para definir melhor a consulta gerada. Toda a consulta é executada no lado
do servidor, combinando a instrução SQL com o SQL gerado a partir das instruções LINQ.
Se a instrução for encerrada ou contiver SQL que não pode ser construído (por exemplo, usa expressões de tabela
comuns), essa consulta ainda será executada no lado do servidor, mas qualquer filtragem e processamento adicionais deverão
ser feitos no lado do cliente como LINQ to Objects.
FromSqlRaw() executa a consulta exatamente como ela é digitada. FromSqlInterpolated() usa interpolação
de cadeia de caracteres C# e, em seguida, transforma os valores interpolados em parâmetros. Os testes a seguir (na
classe CarTests. cs) mostram exemplos de uso desses dois métodos, com e sem os filtros de consulta globais:

[Fato]
public void ShouldNotGetTheLemonsUsingFromSql() {

var entity = Context.Model.FindEntityType($"{typeof(Carro).FullName}"); var tableName =


entity.GetTableName(); var schemaName = entity.GetSchema(); var cars =
Context.Cars.FromSqlRaw($"Selecione * de {schemaName}.{tableName}").ToList();
Assert.Equal(9, carros.Contagem);

[Fato]
public void ShouldGetTheCarsUsingFromSqlWithIgnoreQueryFilters() {

var entity = Context.Model.FindEntityType($"{typeof(Carro).FullName}"); var tableName =


entity.GetTableName(); var schemaName = entity.GetSchema(); var cars =
Context.Cars.FromSqlRaw($"Selecione * de {schemaName}.{tableName}")

.IgnoreQueryFilters().ToList();
Assert.Equal(10, carros.Contagem);
}

[Fato]
public void ShouldGetOneCarUsingInterpolation() {

onde carId = 1;
var carro = Context.Carros
.FromSqlInterpolated($"Selecione * de dbo.Inventory where Id = {carId}")
.Include(x => x.MakeNavigation)
.Primeiro();
Assert.Equal("Preto", carro.Cor);
Assert.Equal("VW", carro.MakeNavigation.Name);
}

[Teoria]
[InlineData(1, 1)]
[InlineData(2, 1)]

949
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

[InlineData(3, 1)]
[InlineData(4, 2)]
[InlineData(5, 3)]
[InlineData(6, 1)] public
void ShouldGetTheCarsByMakeUsingFromSql(int makeId, int esperadoCount) {

var entity = Context.Model.FindEntityType($"{typeof(Carro).FullName}"); var tableName


= entity.GetTableName(); var schemaName = entity.GetSchema(); var cars =
Context.Cars.FromSqlRaw($"Selecione * de {schemaName}.{tableName}")

.Where(x => x.MakeId == makeId).ToList();


Assert.Equal(expectedCount, cars.Count); }

Existem algumas regras ao usar os métodos FromSqlRaw()/FromSqlInterpolated(): as colunas retornadas da


instrução SQL devem corresponder às colunas mapeadas no modelo, todas as colunas do modelo devem ser
retornadas e os dados relacionados não podem ser retornados.

Métodos agregados O EF Core

também oferece suporte a métodos agregados do lado do servidor (Max(), Min(), Count(), Average(), etc.). Métodos
agregados podem ser adicionados ao final de uma consulta LINQ com métodos Where(), ou a expressão de filtro
pode estar contida no próprio método agregado (assim como First() e Single()). A agregação é executada no lado do
servidor e o valor único é retornado da consulta. Os filtros de consulta globais também afetam os métodos agregados
e podem ser desativados com IgnoreQueryFilters().
Todas as instruções SQL mostradas nesta seção foram coletadas usando o SQL Server Profiler.
Este primeiro teste (em CarTests.cs) simplesmente conta todos os registros do carro no banco de dados. Desde a consulta
filtro ainda estiver ativo, a contagem retornará nove carros.

[Fato]
public void ShouldGetTheCountOfCars() {

var contagem = Context.Cars.Count();


Assert.Equal(9, contagem); }

O SQL executado é mostrado aqui:SELECT COUNT(*)


FROM [dbo].[Inventário] AS [i]
WHERE [i].[IsDrivable] = CAST(1 AS bit)

Ao adicionar IgnoreQueryFilters(), o método Count() retorna 10 e a cláusula where é removida da consulta


SQL.

[Fato]
public void ShouldGetTheCountOfCarsIgnoreQueryFilters() {

var contagem = Context.Cars.IgnoreQueryFilters().Count();


Assert.Equal(10, contagem); }

--SQL gerado
SELECT COUNT(*) FROM [dbo].[Inventário] AS [i] 950
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Os testes a seguir (também em CarTests.cs) demonstram o método Count() com uma condição where. O primeiro
teste adiciona a expressão diretamente ao método Count() e o segundo adiciona o método Count() ao final da instrução
LINQ.

[Teoria]
[InlineData(1, 1)]
[InlineData(2, 1)]
[InlineData(3, 1)]
[InlineData(4, 2)]
[InlineData(5, 3)]
[InlineData(6, 1)] public
void ShouldGetTheCountOfCarsByMakeP1(int makeId, int esperadoCount) {

var count = Context.Cars.Count(x=>x.MakeId == makeId);


Assert.Equal(expectedCount, count);
}

[Teoria]
[InlineData(1, 1)]
[InlineData(2, 1)]
[InlineData(3, 1)]
[InlineData(4, 2)]
[InlineData(5, 3)]
[InlineData(6, 1)] public
void ShouldGetTheCountOfCarsByMakeP2(int makeId, int esperadoCount) {

var count = Context.Cars.Where(x => x.MakeId == makeId).Count();


Assert.Equal(expectedCount, count);
}

Ambos os testes criam as mesmas chamadas SQL para o servidor, conforme mostrado aqui (o MakeId muda a cada teste
com base no InlineData):

exec sp_executesql N'SELECT COUNT(*)


FROM [dbo].[Inventário] AS [i]
WHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND ([i].[MakeId] = @__makeId_0)'
,N'@__makeId_0 int',@__makeId_0=6

Qualquer e todos()
Os métodos Any() e All() verificam um conjunto de registros para ver se algum registro corresponde aos
critérios (Any()) ou se todos os registros correspondem aos critérios (All()). Assim como os métodos de agregação,
eles podem ser adicionados ao final de uma consulta LINQ com métodos Where() ou a expressão de filtro pode estar
contida no próprio método. Os métodos Any() e All() são executados no lado do servidor e um booleano é retornado
da consulta. Os filtros de consulta globais também afetam as funções dos métodos Any() e All() e podem ser
desabilitados com IgnoreQueryFilters().
Todas as instruções SQL mostradas nesta seção foram coletadas usando o SQL Server Profiler.
Este primeiro teste (em CarTests.cs) verifica se algum registro de carro possui um MakeId específico.

[Teoria]
[InlineData(1, verdadeiro)]

951
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

[InlineData(11, false)] public


void ShouldCheckForAnyCarsWithMake(int makeId, bool esperadoResult) {

var result = Context.Cars.Any(x => x.MakeId == makeId);


Assert.Equal(resultado esperado, resultado); }

O SQL executado para o primeiro teste teórico é mostrado aqui:

exec sp_executesql N'SELECT CASE


QUANDO EXISTE (
SELECIONE 1

FROM [dbo].[Inventário] AS [i]


WHERE ([i].[IsDrivable] = CAST(1 bit AS)) AND ([i].[MakeId] = @__makeId_0)) THEN CAST(1 bit AS)

ELSE CAST(0 bit AS)


END',N'@__makeId_0 int',@__makeId_0=1

Este segundo teste verifica se todos os registros de carros possuem um MakeId específico.

[Teoria]
[InlineData(1, false)]
[InlineData(11, false)] public
void ShouldCheckForAllCarsWithMake(int makeId, bool esperadoResult) {

var result = Context.Cars.All(x => x.MakeId == makeId);


Assert.Equal(resultado esperado, resultado); }

O SQL executado para o primeiro teste teórico é mostrado aqui:

exec sp_executesql N'SELECT CASE


WHEN NOT EXISTS ( SELECT 1
FROM [dbo].[Inventory] AS [i]

WHERE ([i].[IsDrivable] = CAST(1 bit AS)) AND ([i].[MakeId] <> @__makeId_0)) THEN CAST(1 bit AS)

ELSE CAST(0 bit AS)


END',N'@__makeId_0 int',@__makeId_0=1

Obtendo dados de procedimentos armazenados O padrão final

de recuperação de dados a ser examinado é obter dados de procedimentos armazenados. Embora existam algumas
lacunas no EF Core em relação aos procedimentos armazenados (em comparação com o EF 6), lembre-se de que o
EF Core é construído sobre o ADO.NET. Só precisamos abrir uma camada e lembrar como chamamos os procedimentos
armazenados pré ORM. O método a seguir no CarRepo cria os parâmetros necessários (entrada e saída), aproveita a
propriedade ApplicationDbContext Database e chama ExecuteSqlRaw():

string pública GetPetName(int id) {

var parâmetroId = novo SqlParameter

952
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

{
ParameterName = "@carId",
SqlDbType = System.Data.SqlDbType.Int, Value
= id, };

var parameterName = new SqlParameter {

ParameterName = "@petName",
SqlDbType = System.Data.SqlDbType.NVarChar,
Tamanho = 50, Direção = ParameterDirection.Output };

var resultado = Context.Database


.ExecuteSqlRaw("EXEC [dbo].[GetPetName] @carId, @petName OUTPUT",parameterId,
parameterName); return (string)parameterName.Value; }

Com esse código instalado, o teste se torna trivial. Adicione o seguinte teste à classe CarTests.cs:

[Teoria]
[InlineData(1, "Zippy")]
[InlineData(2, "Enferrujado")]
[InlineData(3, "Mel")]
[InlineData(4, "Clunker")]
[InlineData(5, "Bimmer")]
[InlineData(6, "Hank")]
[InlineData(7, "Pinky")]
[InlineData(8, "Pete")]
[InlineData(9, "Brownie")] public
void ShouldGetValueFromStoredProc(int id, string esperadoNome) {

Assert.Equal(expectedName, new CarRepo(Context).GetPetName(id));


}

Criando Registros
Os registros são adicionados ao banco de dados criando-os no código, adicionando-os ao DbSet<T> e chamando
S aveChanges()/SaveChangesAsync() no contexto. Quando SaveChanges() é executado, o ChangeTracker relata
todas as entidades adicionadas e o EF Core (junto com o provedor de banco de dados) cria a(s) instrução(ões) SQL
apropriada(s) para inserir o(s) registro(s).
Como lembrete, SaveChanges() é executado em uma transação implícita, a menos que uma transação explícita seja usada.
Se o salvamento for bem-sucedido, os valores gerados pelo servidor serão consultados para definir os valores nas entidades.
Todos esses testes usarão uma transação explícita para que as alterações possam ser revertidas, deixando o banco de dados no
mesmo estado de quando a execução do teste começou.
Todas as instruções SQL mostradas nesta seção foram coletadas usando o SQL Server Profiler.

953
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

ÿ Nota Os registros também podem ser adicionados usando o DbContext derivado . Todos esses exemplos usarão as
propriedades da coleção DbSet<T> para adicionar os registros. Ambos DbSet<T> e DbContext têm versões assíncronas
de Add()/AddRange(). Somente as versões síncronas são mostradas.

Entity State Quando

uma entidade é criada por meio de código, mas ainda não foi adicionada a um DbSet<T>, o EntityState é Detached.
Depois que uma nova entidade é adicionada a um DbSet<T>, o EntityState é definido como Added. Após a execução bem-sucedida
de SaveChanges(), o EntityState é definido como Inalterado.

Adicionar um único registro


O teste a seguir demonstra como adicionar um único registro à tabela Inventário:

[Fato]
public void ShouldAddACar() {

ExecuteInATransaction(RunTheTest);

void RunTheTest() {

var carro = carro novo {

Color = "Amarelo",
MakeId = 1, PetName
= "Herbie" }; var carCount
= Context.Cars.Count();
Context.Carros.Add(carro); Context.SaveChanges();
var newCarCount = Context.Cars.Count();
Assert.Equal(carCount+1,newCarCount); }

A instrução SQL executada é mostrada aqui. Observe que a entidade adicionada recentemente é consultada quanto
às propriedades geradas pelo banco de dados (Id e TimeStamp). Quando os resultados da consulta chegam ao EF Core, a
entidade é atualizada com os valores do lado do servidor.

exec sp_executesql N'SET NOCOUNT ON; INSERT


INTO [dbo].[Inventory] ([Color], [MakeId], [PetName])
VALORES (@p0, @p1, @p2);

SELECIONE [Id], [IsDrivable], [TimeStamp]


DE [dbo].[Inventário]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

',N'@p0 nvarchar(50),@p1 int,@p2 nvarchar(50)',@p0=N'Amarelo',@p1=1,@p2=N'Herbie'

954
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Adicionar um único registro usando anexar


Quando a chave primária de uma entidade é mapeada para uma coluna de identidade no SQL Server, o EF Core tratará essa
instância da entidade como adicionada se o valor da propriedade da chave primária for zero. O teste a seguir cria uma nova
entidade Car com o Id deixado no valor padrão de zero. Quando a entidade é anexada ao ChangeTracker, o estado é definido
como Added e chamar SaveChanges() adiciona a entidade ao banco de dados.

[Fato]
public void ShouldAddACarWithAttach() {

ExecuteInATransaction(RunTheTest);

void RunTheTest() {

var carro = carro novo


{
Cor = "Amarelo",
MakeId = 1,
PetName = "Herbie"
};
var carCount = Context.Cars.Count();
Context.Cars.Attach(carro);
Assert.Equal(EntityState.Added, Context.Entry(car).State);
Context.SaveChanges(); var newCarCount = Context.Cars.Count();
Assert.Equal(carCount + 1, newCarCount); } }

Adicionar vários registros de uma só vez


Para inserir vários registros em uma única transação, use o método AddRange() de DbSet<T>, conforme mostrado neste
teste (observe que com o SQL Server, para que o lote seja usado ao persistir dados, deve haver pelo menos quatro
ações para executar):

[Fato]
public void ShouldAddMultipleCars() {

ExecuteInATransaction(RunTheTest);

void RunTheTest() {

//Tem que adicionar 4 para ativar o lote var cars


= new List<Car>

{ new() { Color = "Yellow", MakeId = 1, PetName = "Herbie" }, new() { Color =


"White", MakeId = 2, PetName = "Mach 5" }, new() { Color = "Pink", MakeId =
3, PetName = "Avon" }, new() { Color = "Blue", MakeId = 4, PetName =
"Blueberry" }, };

955
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

var carCount = Context.Cars.Count();


Context.Cars.AddRange(carros);
Context.SaveChanges(); var newCarCount =
Context.Cars.Count(); Assert.Equal(carCount + 4,
newCarCount); }

As instruções add são agrupadas em uma única chamada ao banco de dados e todas as colunas geradas são
consultadas. Quando os resultados da consulta chegam ao EF Core, as entidades são atualizadas com os valores do lado do servidor.
A instrução SQL executada é mostrada aqui:

exec sp_executesql N'SET NOCOUNT ON;


DECLARE @inserted0 TABLE ([Id] int, [_Position] [int]); MERGE [dbo].
[Inventário] USING ( VALUES (@p0, @p1, @p2, 0), (@p3, @p4, @p5, 1),
(@p6, @p7, @p8, 2), (@p9, @p10, @p11, 3)) AS i ([Color], [MakeId],
[PetName], _Position) ON 1=0 QUANDO NÃO CORRESPONDER ENTÃO
INSERIR ([Color], [MakeId], [PetName ])

VALUES (i.[Color], i.[MakeId], i.[PetName])


OUTPUT INSERTED.[Id], i._Position INTO
@inserted0;

SELECT [t].[Id], [t].[IsDrivable], [t].[TimeStamp] FROM [dbo].[Inventory] t INNER JOIN @inserted0 i ON


([t].[Id] = [i ].[Eu ia])
ORDER BY [i].[_Position];', N'@p0
nvarchar(50),@p1 int,@p2 nvarchar(50),@p3 nvarchar(50),@p4 int,@p5 nvarchar(50), @p6 nvarchar(50),@p7 int,@p8
nvarchar(50),@p9 nvarchar(50),@p10 int,@p11 nvarchar(50)', @p0=N'Amarelo',@p1=1,
@p2=N'Herbie',@p3=N'Branco',@p4=2,@p5=N'Mach 5',@p6=N'Rosa',@p7=3,
@p8=N'Avon' ,@p9=N'Azul',@p10=4,@p11=N'Azul'

Considerações sobre a coluna de identidade ao adicionar registros Quando uma entidade tem uma

propriedade numérica definida como a chave primária, essa propriedade (por padrão) é mapeada para uma coluna de
identidade no SQL Server. O EF Core considera qualquer entidade com o valor padrão (zero) para a propriedade de
chave como nova e qualquer entidade com um valor não padrão como já existente no banco de dados. Se você criar uma
nova entidade e definir a propriedade da chave primária como um número diferente de zero e tentar adicioná-la ao banco
de dados, o EF Core falhará ao adicionar o registro porque a inserção de identidade não está habilitada. O código de dados
Initialize demonstra como habilitar a inserção de identidade.

Adicionando um gráfico de objeto Ao

adicionar uma entidade ao banco de dados, os registros filho podem ser adicionados na mesma chamada sem adicioná-
los especificamente em seu próprio DbSet<T>, desde que sejam adicionados à propriedade de coleção do registro pai. Por
exemplo, uma nova entidade Make é criada e um registro filho Car é adicionado à propriedade Cars na marca. Quando a
entidade Make é adicionada à propriedade DbSet<Make>, o EF Core também começa a rastrear automaticamente o registro
Car filho, sem precisar adicioná-lo explicitamente à propriedade DbSet<Car>. A execução de SaveChanges() salva o Make
e o Car juntos. O seguinte teste demonstra isso:

956
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

[Fato]
public void ShouldAddAnObjectGraph() {

ExecuteInATransaction(RunTheTest);

void RunTheTest() {

var marca = nova Marca {Nome = "Honda"}; var


car = new Car { Color = "Yellow", MakeId = 1, PetName = "Herbie" }; //Cast a propriedade Cars
para List<Car> de IEnumerable<Car> ((List<Car>)make.Cars).Add(car); Context.Makes.Add(make);
var carCount = Context.Cars.Count(); var makeCount = Context.Makes.Count();
Context.SaveChanges(); var newCarCount = Context.Cars. Contar(); var newMakeCount =
Context.Makes. Contar(); Assert.Equal(carCount+1,newCarCount);
Assert.Equal(makeCount+1,newMakeCount); }

As instruções add não são colocadas em lote porque há menos de duas instruções e, com o SQL Server, o lote começa
em quatro instruções. As instruções SQL executadas são mostradas aqui:

exec sp_executesql N'SET NOCOUNT ON;


INSERT INTO [dbo].[Faz] ([Nome])
VALORES (@p0);
SELECIONE [Id], [TimeStamp]
DE [dbo].[Faz]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

',N'@p0 nvarchar(50)',@p0=N'Honda'

exec sp_executesql N'SET NOCOUNT ON;


INSERT INTO [dbo].[Inventory] ([Color], [MakeId], [PetName])
VALORES (@p1, @p2, @p3);
SELECIONE [Id], [IsDrivable], [TimeStamp]
DE [dbo].[Inventário]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

',N'@p1 nvarchar(50),@p2 int,@p3 nvarchar(50)',@p1=N'Amarelo',@p2=7,@p3=N'Herbie'

Atualizando Registros
Os registros são atualizados carregando-os em DbSet<T> como uma entidade rastreada, alterando-os por meio do código
e, em seguida, chamando SaveChanges() no contexto. Quando SaveChanges() é executado, o ChangeTracker relata
todas as entidades modificadas e o EF Core (juntamente com o provedor de banco de dados) cria a(s) instrução(ões) SQL
apropriada(s) para atualizar o(s) registro(s).

957
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Estado da Entidade

Quando uma entidade rastreada é editada, EntityState é definido como Modificado. Após as alterações serem salvas com sucesso,
o estado é retornado para Inalterado.

Atualizar entidades rastreadas

Atualizar um único registro é muito parecido com adicionar um único registro. Carregue o registro do banco de dados em
uma entidade rastreada, faça algumas alterações e chame SaveChanges(). Observe que você não precisa chamar os
métodos Update()/UpdateRange() no DbSet<T>, pois as entidades são rastreadas. O teste a seguir atualiza apenas um
registro, mas o processo é o mesmo se várias entidades rastreadas forem atualizadas e salvas.

[Fato]
public void ShouldUpdateACar() {

ExecuteInASharedTransaction(RunTheTest);

void RunTheTest(IDbContextTransaction trans) { var car =


Context.Cars.First(c => c.Id == 1); Assert.Equal("Preto",carro.Cor);
car.Color = "Branco"; //A chamada de atualização não é
necessária porque a entidade é rastreada //
Context.Cars.Update(car); Context.SaveChanges();
Assert.Equal("Branco", carro.Cor); var context2 = TestHelpers.GetSecondContext(Contexto,
trans); var car2 = context2.Cars.First(c => c.Id == 1); Assert.Equal("Branco",
carro2.Cor); } }

O código anterior usa uma transação compartilhada em duas instâncias de ApplicationDbContext. Isso é para fornecer
isolamento entre o contexto que executa o teste e o contexto que verifica o resultado do teste.
A instrução SQL executada está listada aqui:

exec sp_executesql N'SET NOCOUNT ON;


UPDATE [dbo].[Inventário] SET [Cor] = @p0 WHERE [Id] =
@p1 AND [TimeStamp] = @p2; SELECIONE [TimeStamp]

DE [dbo].[Inventário]
ONDE @@ROWCOUNT = 1 AND [Id] = @p1;

',N'@p1 int,@p0 nvarchar(50),@p2 varbinary(8)',@p1=1,@p0=N'Branco',@p2=0x000000000000862D

ÿ Observação A cláusula where anterior verificava não apenas a coluna Id , mas também a coluna TimeStamp .
A verificação de simultaneidade será abordada em breve.

958
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Atualizar entidades não rastreadas


As entidades não rastreadas também podem ser usadas para atualizar os registros do banco de dados. O processo é semelhante
à atualização de entidades rastreadas, exceto que a entidade é criada no código (e não consultada) e o EF Core deve ser
notificado de que a entidade já deve existir no banco de dados e precisa ser atualizada.
Depois de criar uma instância da entidade, há duas maneiras de notificar o EF Core de que essa entidade precisa ser
processada como uma atualização. A primeira é chamar o método Update() no DbSet<T>, que define o estado como Modified,
assim:

context2.Cars.Update(updatedCar);

A segunda é usar a instância de contexto e o método Entry() para definir o estado como Modificado, assim:

context2.Entry(updatedCar).State = EntityState.Modified;

De qualquer forma, SaveChanges() ainda deve ser chamado para que os valores persistam.
O exemplo a seguir lê um registro como não rastreado, cria uma nova instância da classe Car a partir desse registro e
altera uma propriedade (Color). Em seguida, ele define o estado ou usa o método Update() em DbSet<T>, dependendo de qual
linha de código você descomenta. O método Update() também altera o estado para Modificado. O teste então chama SaveChanges().
Todos os contextos extras estão lá para garantir que o teste seja preciso e não haja nenhum cruzamento entre os contextos.

[Fato]
public void ShouldUpdateACarUsingState() {

ExecuteInASharedTransaction(RunTheTest);

void RunTheTest(IDbContextTransaction trans) { var car =


Context.Cars.AsNoTracking().First(c => c.Id == 1);
Assert.Equal("Preto", carro.Cor); var updatedCar = new Car { Color = "White", //Original
is Black Id = car.Id, MakeId = car.MakeId, PetName = car.PetName, TimeStamp =
car.TimeStamp IsDrivable = car.IsDrivable

};
var context2 = TestHelpers.GetSecondContext(Contexto, trans); //Chame Update ou
modifique o estado context2.Entry(updatedCar).State = EntityState.Modified; //
context2.Cars.Update(updatedCar); context2.SaveChanges(); var context3 =
TestHelpers.GetSecondContext(Contexto, trans); var car2 = context3.Cars.First(c =>
c.Id == 1); Assert.Equal("Branco", carro2.Cor); } }

959
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

A instrução SQL executada está listada aqui:

exec sp_executesql N'SET NOCOUNT ON;


UPDATE [dbo].[Inventário] SET [Cor] = @p0 WHERE [Id]
= @p1 AND [TimeStamp] = @p2; SELECIONE
[TimeStamp]
DE [dbo].[Inventário]
ONDE @@ROWCOUNT = 1 AND [Id] = @p1;

',N'@p1 int,@p0 nvarchar(50),@p2 varbinary(8)',@p1=1,@p0=N'Branco',@p2=0x000000000000862D

Verificação de simultaneidade O capítulo

anterior abordou a verificação de simultaneidade em grande detalhe. Como lembrete, quando uma entidade possui
uma propriedade Timestamp definida, o valor dessa propriedade é usado na cláusula where quando as alterações
(atualizações ou exclusões) estão sendo mantidas no banco de dados. Em vez de apenas procurar a chave primária, o
valor TimeStamp é adicionado à consulta, como neste exemplo:

UPDATE [dbo].[Inventário] SET [PetName] = @p0 WHERE


[Id] = @p1 AND [TimeStamp] = @p2;

O teste a seguir mostra um exemplo de como criar uma exceção de simultaneidade, capturá-la e usar as Entradas
para obter os valores originais, os valores atuais e os valores atualmente armazenados no banco de dados.
Obter os valores atuais requer outra chamada de banco de dados.

[Fato]
public void ShouldThrowConcurrencyException() {

ExecuteInATransaction(RunTheTest);

void RunTheTest()
{ var carro =
Context.Cars.First(); //Atualiza o banco
de dados fora do contexto
Context.Database.ExecuteSqlInterpolated(
$"Update dbo.Inventory set Color='Pink' where Id = {car.Id}");
car.Color = "Amarelo"; var
ex = Assert.Throws<CustomConcurrencyException>(
() => Context.SaveChanges());
var entrada = ((DbUpdateConcurrencyException) ex.InnerException)?.Entries[0]; PropertyValues
originalProps = entrada.OriginalValues; PropertyValues currentProps = entrada.CurrentValues; //Isso
precisa de outra chamada de banco de dados PropertyValues databaseProps =
entry.GetDatabaseValues(); }

As chamadas SQL executadas são listadas aqui. A primeira é a declaração de atualização e a segunda é para a chamada para obter
os valores do banco de dados.

exec sp_executesql N'SET NOCOUNT ON;


UPDATE [dbo].[Inventário] SET [Cor] = @p0 WHERE [Id]
= @p1 AND [TimeStamp] = @p2;

960
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

SELECIONE [TimeStamp]
DE [dbo].[Inventário]
ONDE @@ROWCOUNT = 1 AND [Id] = @p1;'
,N'@p1 int,@p0 nvarchar(50),@p2 varbinary(8)',@p1=1,@p0=N'Amarelo',@p2=0x0000000000008665

exec sp_executesql N'SELECT TOP(1) [i].[Id], [i].[Cor], [i].[IsDrivable], [i].[MakeId], [i].[PetName], [ i].[TimeStamp]

FROM [dbo].[Inventário] AS [i]


WHERE [i].[Id] = @__p_0',N'@__p_0 int',@__p_0=1

Excluindo registros
Uma única entidade é marcada para exclusão chamando Remove() em DbSet<T> ou definindo seu estado como Deleted.
Uma lista de registros é marcada para exclusão chamando RemoveRange() no DbSet<T>. O processo de remoção causará
efeitos em cascata nas propriedades de navegação com base nas regras configuradas em OnModelCreating() (ou pelas
convenções do EF Core). Se a exclusão for impedida devido à política em cascata, uma exceção será lançada.

Entity State Quando

o método Remove() é chamado em uma entidade que está sendo rastreada, seu EntityState é definido como Deleted.
Após a execução bem-sucedida da instrução delete, a entidade é removida do ChangeTracker e seu estado é alterado
para Detached. Observe que a entidade ainda existe em seu aplicativo, a menos que tenha saído do escopo e tenha sido
coletada como lixo.

Excluir registros rastreados


O processo de exclusão espelha o processo de atualização. Depois que uma entidade é rastreada, chame Remove() nessa instância
e, em seguida, chame SaveChanges() para remover o registro do banco de dados.

[Fato]
public void ShouldRemoveACar() {

ExecuteInATransaction(RunTheTest);

void RunTheTest() {

var carCount = Context.Cars. Contar(); var carro =


Context.Cars.First(c => c.Id == 2); Context.Cars.Remove(carro);
Context.SaveChanges(); var newCarCount =
Context.Cars.Count(); Assert.Equal(carCount - 1,
newCarCount); Assert.Equal( EntityState.Detached,
Context.Entry(carro).State);

}
}

961
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

Depois que SaveChanges() é chamado, a instância da entidade ainda existe, mas não está mais no ChangeTracker.
Ao verificar o EntityState, o estado será Detached.

A chamada SQL executada para a exclusão está listada aqui:exec sp_executesql N'SET NOCOUNT ON; EXCLUIR DE
[dbo].[Inventário]
ONDE [Id] = @p0 E [TimeStamp] = @p1; SELECT
@@ROWCOUNT;'
,N'@p0 int,@p1 varbinary(8)',@p0=2,@p1=0x0000000000008680

Excluir entidades não rastreadas


Entidades não rastreadas podem excluir registros da mesma forma que entidades não rastreadas podem atualizar registros. A
diferença é que a entidade é rastreada chamando Remove()/RemoveRange() ou definindo o estado como Deleted e então
chamando SaveChanges().
O exemplo a seguir lê um registro como não rastreado, cria uma nova instância da classe Car a partir desse registro
e altera uma propriedade (Color). Em seguida, ele define o estado ou usa o método Remove() em DbSet<T> (dependendo de
qual linha você descomenta). O teste então chama SaveChanges(). Todos os contextos extras estão lá para garantir que não
haja nenhum cruzamento entre os contextos.

[Fato]
public void ShouldRemoveACarUsingState() {

ExecuteInASharedTransaction(RunTheTest);

void RunTheTest(IDbContextTransaction trans) { var carCount


= Context.Cars.Count(); var carro =
Context.Cars.AsNoTracking().First(c => c.Id == 2); var
context2 = TestHelpers.GetSecondContext(Contexto, trans); //Chame Remova ou
modifique o estado context2.Entry(car).State = EntityState.Deleted; //
contexto2.Carros.Remover(carro); context2.SaveChanges(); var newCarCount =
Context.Cars.Count(); Assert.Equal(carCount - 1, newCarCount);
Assert.Equal( EntityState.Detached, Context.Entry(carro).State);

}
}

Capturar falhas de exclusão em cascata


O EF Core lançará um DbUpdateException quando uma tentativa de excluir um registro falhar devido às regras em cascata.
O teste a seguir mostra isso em ação:

[Fato]
public void ShouldFailToRemoveACar() {

ExecuteInATransaction(RunTheTest);

962
Machine Translated by Google

Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core

void RunTheTest()
{
var carro = Context.Cars.First(c => c.Id == 1);
Context.Cars.Remove(carro);

Assert.Throws<CustomDbUpdateException>( ()=>Context.SaveChanges());
}
}

Exclusão de verificação de simultaneidade

também usa verificação de simultaneidade se a entidade tiver uma propriedade TimeStamp. Consulte a seção “Verificação de
simultaneidade” na seção “Atualizando registros” para obter mais informações.

Resumo
Este capítulo usou o conhecimento adquirido no capítulo anterior para completar a camada de acesso a dados para o
banco de dados AutoLot. Você usou as ferramentas de linha de comando do EF Core para estruturar um banco de dados
existente, atualizou o modelo para sua versão final e, em seguida, criou migrações e as aplicou. Os repositórios foram
adicionados para o encapsulamento do acesso aos dados, e o código de inicialização do banco de dados com dados de
amostra pode descartar e criar o banco de dados de maneira confiável e repetível. O restante do capítulo concentrou-se em
testar a camada de acesso a dados. Isso conclui nossa jornada pelo acesso a dados e Entity Framework Core.

963

Você também pode gostar