Escolar Documentos
Profissional Documentos
Cultura Documentos
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!");
PropertyNameCaseInsensitive = verdadeiro,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
NumberHandling = JsonNumberHandling.AllowReadingFromString
Você ainda pode definir propriedades adicionais por meio da inicialização do objeto, assim:
WriteIndented = true };
761
Machine Translated by Google
Para encerrar tudo, adicione a seguinte linha para exercitar a nova função:
SaveListOfCarsAsJson(options, "CarCollection.json");
Adicione o seguinte código às instruções de nível superior para reconstituir seu XML de volta em objetos (ou lista
de objetos):
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
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.
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.
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
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.
ODBC System.Data.Odbc
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
ÿ 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.
DataSet Representa um cache de dados na memória que consiste em qualquer número de dados inter-relacionados
Objetos DataTable
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
IDbDataAdapter Estende IDataAdapter para fornecer funcionalidade adicional de um objeto de adaptador de dados
766
Machine Translated by Google
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.
IDbTransaction BeginTransaction();
IDbTransaction BeginTransaction(IsolationLevel il); void
ChangeDatabase(string databaseName); void Fechar();
IDbCommand CreateCommand();
void Abrir(); void Dispose(); }
void Commit();
void Rollback(); void
Dispose(); }
767
Machine Translated by Google
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
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.
void Fechar();
DataTable GetSchemaTable(); bool
PróximoResultado(); bool Ler();
Descarte(); }
769
Machine Translated by Google
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:
ÿ 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.
770
Machine Translated by Google
ÿ 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
usando Sistema;
usando System.Data;
usando System.Data.Odbc;
#if PC usando
System.Data.OleDb; #endif
usando Microsoft.Data.SqlClient;
usando MyConnectionFactory;
_ => 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
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).
ÿ 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”.
ÿ 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.
ÿ Observação A escolha do contêiner (Windows ou Linux) é o sistema operacional em execução no contêiner, não o sistema
773
Machine Translated by Google
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:
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):
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
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
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
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.
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.
775
Machine Translated by Google
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
777
Machine Translated by Google
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
Se você estiver se conectando a qualquer outra instância do SQL Server, atualize as propriedades da conexão adequadamente.
ÿ 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.
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).
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
780
Machine Translated by Google
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
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.
ÿ 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
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.
[ID] ASC
) EM [PRIMÁRIO]
) EM [PRIMÁRIO]
IR
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:
783
Machine Translated by Google
[ID] ASC
) EM [PRIMÁRIO]
) EM [PRIMÁRIO]
IR
[ID] ASC
) EM [PRIMÁRIO]
) EM [PRIMÁRIO]
IR
[ID] ASC
) EM [PRIMÁRIO]
) EM [PRIMÁRIO]
IR
784
Machine Translated by Google
[ID] ASC
) EM [PRIMÁRIO]
) EM [PRIMÁRIO]
IR
Esta próxima seção adicionará os relacionamentos de chave estrangeira entre as tabelas inter-relacionadas.
Abra uma nova consulta, digite o seguinte SQL e clique em Executar (ou pressione F5):
[MakeId] ASC
) EM [PRIMÁRIO]
IR
ALTER TABLE [dbo].[Inventory] WITH CHECK ADD CONSTRAINT [FK_Make_Inventory] FOREIGN KEY([MakeId])
Abra uma nova consulta, digite o seguinte SQL e clique em Executar (ou pressione F5):
785
Machine Translated by Google
(
[CarId] ASC
) EM [PRIMÁRIO]
IR
ALTER TABLE [dbo].[Orders] WITH CHECK ADD CONSTRAINT [FK_Orders_Inventory] FOREIGN KEY([CarId])
[CustomerId] ASC,
[CarId] ASC
) EM [PRIMÁRIO]
IR
ALTER TABLE [dbo].[Orders] WITH CHECK ADD CONSTRAINT [FK_Orders_Customers] FOREIGN KEY([CustomerId])
[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
ÿ 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,
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.
787
Machine Translated by Google
Para adicionar registros à sua primeira tabela, crie uma nova consulta e execute as seguintes instruções SQL para
adicionar registros à tabela Inventory:
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
Para adicionar registros à tabela Clientes, crie uma nova consulta e execute as seguintes instruções SQL:
adicione dados à sua tabela Orders. Crie uma nova consulta, digite o seguinte SQL e clique em Executar (ou pressione F5):
788
Machine Translated by Google
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.
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:
789
Machine Translated by Google
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:
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.
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.
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
#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
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;
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;
if (conexão == null) {
792
Machine Translated by Google
}
}
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:
_
};
793
Machine Translated by Google
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:
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:
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
Com este primeiro exemplo atrás de você, agora você pode mergulhar nos detalhes do trabalho com ADO.NET. 794
Machine Translated by Google
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;
FROM Inventory i
INNER JOIN Faz m em m.Id = i.MakeId"; SqlCommand
myCommand = new SqlCommand(sql, connection);
}}}
Console.ReadLine();
795
Machine Translated by Google
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.
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
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:
O código a seguir mostra detalhes sobre o SqlConnection que passou para ele:
{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:
Quebrado,
Fechado,
Conectando,
Executando,
Buscando,
Abrir }
797
Machine Translated by Google
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.
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:
798
Machine Translated by Google
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.
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).
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
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()) {
}
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):
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.
800
Machine Translated by Google
ÿ 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
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.
ÿ 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
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.
//espaço
de nomes Car.cs AutoLot.Dal.Models {
//Espaço de nome
CarViewModel.cs AutoLot.Dal.Models {
ÿ 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á
802
Machine Translated by Google
usando Sistema;
usando System.Collections.Generic; usando
System.Data; usando AutoLot.Dal.Models;
usando Microsoft.Data.SqlClient;
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 {
}}
ConnectionString = _connectionString };
_sqlConnection.Open(); }
803
Machine Translated by Google
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
...
}
if (_disposed) {
retornar;
} if (dispondo) {
_sqlConnection.Dispose(); }
_disposed = verdadeiro; } public
void Dispose() {
Descarte(verdadeiro);
GC.SuppressFinalize(this); }
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
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; }
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
} 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.
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.
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
OpenConnection(); //
Formata e executa a instrução SQL. string sql
"
= "Inserir valores no inventário (MakeId, Color, PetName) +
$"('{car.MakeId}', '{car.Color}', '{car.PetName}')";
command.CommandType = CommandType.Text;
comando.ExecuteNonQuery(); }
FecharConexão(); }
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:
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
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:
FecharConexão(); }
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
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
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.
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:
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";
comando.Parameters.Add(param);
809
Machine Translated by Google
comando.Parameters.Add(param);
810
Machine Translated by Google
command.Parameters.Add(paramId);
command.Parameters.Add(paramName);
OpenConnection(); //
Observe os "espaços reservados" na consulta SQL. string sql =
"Inserir no inventário" + "Valores (MakeId, Color, PetName)" +
"(@MakeId, @Color, @PetName)";
811
Machine Translated by Google
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.
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:
OpenConnection();
string carPetName;
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);
812
Machine Translated by Google
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.
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.
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
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) {
814
Machine Translated by Google
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.
usaremos implementam a interface IDbTransaction. Lembre-se do início deste capítulo que IDbTransaction define um punhado
de membros da seguinte forma:
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
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.
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
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)
{
// Comprometa-
se! tx.Commit(); }
817
Machine Translated by Google
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().
void FlagCliente() {
OrdinalIgnoreCase)) {
lanceEx = falso;
818
Machine Translated by Google
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.
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.
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:
namespace AutoLot.Dal.BulkImport {
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
Em seguida, atualize a classe para pública e selada e implemente IMyDataReader. Adicione um construtor para receber
os registros e definir a propriedade.
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.
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:
...
private int _currentIndex = -1; public bool
Read() { if (_currentIndex + 1 >=
Records.Count) {
retorna falso;
} _currentIndex++;
retornar verdadeiro;
}
}
820
Machine Translated by Google
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:
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.
Registros = registros;
_propertyInfos = typeof(T).GetProperties(); _nameDictionary
= new Dictionary<int, string>();
821
Machine Translated by Google
Implemente o método GetSchemaTable() a seguir. Isso recupera as informações do SQL Server sobre a tabela
de destino.
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.
...
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:
_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:
822
Machine Translated by Google
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:
ConnectionString = ConnectionString };
_sqlConnection.Open(); }
if (_sqlConnection?.State != ConnectionState.Closed) {
_sqlConnection?.Close(); }
823
Machine Translated by Google
OpenConnection();
usando SqlConnection conn = _sqlConnection;
SqlBulkCopy bc = new SqlBulkCopy(conn) {
bc.WriteToServer(leitor de dados); }
catch (Exceção ex) {
FecharConexão(); }
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() {
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
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
CAPÍTULO 22
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.
ÿ 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
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
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.
830
Machine Translated by Google
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/
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.
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
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>.
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
832
Machine Translated by Google
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.
usando Sistema;
usando Microsoft.EntityFrameworkCore; usando
Microsoft.EntityFrameworkCore.Design;
namespace AutoLot.Samples
{ public class
ApplicationDbContextFactory : IDesignTimeDbContextFactory<ApplicationDb Context>
833
Machine Translated by Google
}
}
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:
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.
//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
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:
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:
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.
835
Machine Translated by Google
{
actionToExecute();
trans.Commit(); } catch
(Exceção ex) {
trans.Rollback(); } });
{
SavingChanges += (remetente, argumentos) =>
{
Console.WriteLine($"Salvando alterações para {((DbContext)remetente).Database.
GetConnectionString()}"); };
SavedChanges += (remetente,
argumentos) => {
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
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.
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
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():
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
Excluído A entidade está sendo rastreada e marcada para exclusão do banco de dados.
Inalterado A entidade está sendo rastreada, existe no banco de dados e não foi modificada.
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.
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
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.
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
usando System.Collections.Generic;
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
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;
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:
[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.
modelBuilder.Entity<BaseEntity>().ToTable("BaseEntities");
modelBuilder.Entity<Car>().ToTable("Cars");
OnModelCreatingPartial(modelBuilder); } void parcial
OnModelCreatingPartial(ModelBuilder modelBuilder);
841
Machine Translated by Google
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:
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.
[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
[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
ÿ 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
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.
ÿ Observação Acho útil considerar objetos com propriedades de navegação como listas vinculadas e, se as propriedades de
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.
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.
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
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 {
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.
844
Machine Translated by Google
Quando o banco de dados é atualizado usando migrações do EF Core, as seguintes tabelas são criadas:
[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
[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
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 {
845
Machine Translated by Google
namespace AutoLot.Samples.Models {
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.
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:
[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
namespace AutoLot.Samples.Models
{ public class Car : BaseEntity {
namespace AutoLot.Samples.Models {
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:
...
public IEnumerable<CarDriver> CarDrivers { get; definir; } }
847
Machine Translated by Google
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.
[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
848
Machine Translated by Google
IR
ALTER TABLE [dbo].[CarDriver] WITH CHECK ADD CONSTRAINT [FK_CarDriver_Cars_CarsId] FOREIGN KEY([CarsId])
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.
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
• ClienteSetNull
• SetNull
• Restringir
849
Machine Translated by Google
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.
Excluir comportamento Efeito sobre dependentes (na memória) Efeito sobre dependentes (no banco de dados)
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.
SetNull Propriedades/propriedades de chave estrangeira Propriedades/propriedades de chave estrangeira definidas como nulas.
definidas como nulas.
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.
Excluir comportamento Efeito nos dependentes (na memória) Efeito nos dependentes (no banco de dados)
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.
850
Machine Translated by Google
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.
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.
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>
• m_<nome da propriedade>
851
Machine Translated by Google
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:
Tabela 22-8. Algumas anotações de dados suportadas pelo Entity Framework Core (*Novos atributos no EF Core 5)
Sem chave* Indica que uma entidade não possui uma chave (por exemplo, representando uma exibição de banco de dados).
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.
Chave Estrangeira Declara uma propriedade que é usada como chave estrangeira para uma propriedade de navegação.
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
usando System.ComponentModel.DataAnnotations;
usando System.ComponentModel.DataAnnotations.Schema;
classe abstrata pública BaseEntity {
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:
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
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:
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:
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.
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
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
//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.
modelBuilder.Entity<Car>(entity => {
ÿ 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.
856
Machine Translated by Google
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)
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 => {
});
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
.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 => {
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 => {
modelBuilder.Entity<Car>(entity => {
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
.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));
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:
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().
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:
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
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.
ÿ 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
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:
ÿ 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
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:
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) {
862
Machine Translated by Google
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:
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) {
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
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:
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:
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:
864
Machine Translated by Google
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:
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:
865
Machine Translated by Google
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.
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:
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:
866
Machine Translated by Google
É 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().
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();
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();
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.
867
Machine Translated by Google
Até agora não deveria ser nenhuma surpresa que a consulta SQL gerada inclui o filtro para carros não dirigíveis.
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>.
O mesmo processo se aplica ao usar o método Reference() para recuperar dados de uma propriedade de
navegação de referência.
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:
868
Machine Translated by Google
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).
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:
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:
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
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"; }
[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 => {
870
Machine Translated by Google
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):
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 => {
871
Machine Translated by Google
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.
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.
ÿ 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
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): _/\__ ---==/ \\ |. \|\ |__||__| | ) \\\ |_||_| \_/ | //|\\ |__ ||_| \\\/\\
___ ___
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
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.
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.
--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.
873
Machine Translated by Google
Para listar todos os argumentos e opções de um comando, digite dotnet ef <command> -h em uma janela de comando, como esta:
ÿ 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.
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)
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
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 => {
...
migrationBuilder.CreateIndex( nome:
"IX_Cars_MakeId", tabela:
"Carros", coluna: "MakeId");
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 {
...
}
}
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
ÿ 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.
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.
876
Machine Translated by Google
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.
Argumento Significado na
<DE> vida A migração inicial. O padrão é 0 (zero), a migração inicial.
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:
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.
-i || --idempotente Gera um script que verifica se uma migração já foi aplicada antes de aplicá-la
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
--funcionamento a seco Mostre qual banco de dados será descartado, mas não o descarte.
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.
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.
878
Machine Translated by Google
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.
--data-annotations vida Use atributos para configurar o modelo (quando possível). Se omitido,
apenas a API Fluent é usada.
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.
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
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
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.
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
Microsoft.EntityFrameworkCore.Abstractions
System.Text.Json
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.EntityFrameworkCore.Design
ÿ 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>
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):
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.
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
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 {
ÿ 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.
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:
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:
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
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:
Todas as entidades scaffolded do banco de dados AutoLot serão atualizadas para usar esta classe base.
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 {
[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.
...
}
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
namespace AutoLot.Models.Entities {
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";
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.
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; }
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;
namespace AutoLot.Models.Entities {
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.
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 {
[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>();
891
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
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;
namespace AutoLot.Models.Entities {
[ForeignKey(nameof(CustomerId))]
[InverseProperty("CreditRisks")] public
virtual Cliente Cliente { get; definir; } } }
[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.
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
namespace AutoLot.Models.Entities {
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; }
ÿ Observação Neste momento, o projeto AutoLot.Models deve ser construído corretamente. O projeto AutoLot.Dal não será
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 {
[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; }
ÿ 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:
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
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 => {
{ 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]");
});
});
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 => {
896
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
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 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 => {
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.
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 => {
897
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
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 {
//CustomConcurrencyException.cs
usando Microsoft.EntityFrameworkCore;
namespace AutoLot.Dal.Exceptions {
}
}
//CustomDbUpdateException.cs
usando Microsoft.EntityFrameworkCore;
namespace AutoLot.Dal.Exceptions {
898
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
}
}
//CustomRetryLimitExceededException.cs using
System; usando
Microsoft.EntityFrameworkCore.Storage;
namespace AutoLot.Dal.Exceptions {
}
}
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():
899
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
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:
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:
retornar;
}
}
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):
901
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
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.
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:
COMO
902
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
}
}
namespace AutoLot.Dal.EfStructures.Migrations {
MigrationHelpers.CreateSproc(migrationBuilder);
MigrationHelpers.CreateCustomerOrderView(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:
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:
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:
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
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.
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
usando Sistema;
usando System.Collections.Generic;
namespace AutoLot.Dal.Repos.Base {
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:
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:
Contexto = contexto;
_disposeContext = falso; }
_disposeContext = verdadeiro; }
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
Contexto = contexto;
Tabela = Context.Set<T>();
_disposeContext = falso; }
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:
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.
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:
908
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
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.
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:
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:
Isso conclui a classe BaseRepo e agora é hora de construir os repositórios específicos da entidade.
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
namespace AutoLot.Dal.Repos.Interfaces {
}
910
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
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> { } }
usando AutoLot.Models.Entities;
usando AutoLot.Dal.Repos.Base;
namespace AutoLot.Dal.Repos.Interfaces
{ public interface ICustomerRepo :
IRepo<Customer> { } }
usando AutoLot.Models.Entities;
usando AutoLot.Dal.Repos.Base;
namespace AutoLot.Dal.Repos.Interfaces
{ public interface IMakeRepo : IRepo<Make>
{}}
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
namespace AutoLot.Dal.Repos.Interfaces {
IQueryable<CustomerOrderViewModel> GetOrdersViewModel(); }
Isso completa a interface, pois todos os terminais de API necessários são cobertos na classe base.
CarRepo.cs
CreditRiskRepo.cs
CustomerRepo.cs
MakeRepo.cs
OrderRepo.cs
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;
namespace AutoLot.Dal.Repos {
912
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
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.
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.
Adicione o método para obter o valor PetName de um carro usando o procedimento armazenado.
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
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 {
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
namespace AutoLot.Dal.Repos {
{ } 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:
O Repositório Make
Abra a classe MakeRepo.cs e adicione as seguintes instruções using ao topo do arquivo:
Altere a classe para public, herde de BaseRepo<Make>, implemente IMakeRepo e adicione os dois construtores
necessários.
namespace AutoLot.Dal.Repos {
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.
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;
namespace AutoLot.Dal.Repos {
{}
pedidoRepo interno(
DbContextOptions<ApplicationDbContext> opções): base(opções)
{}
}
}
916
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
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.
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().
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
namespace AutoLot.Dal.Initialization {
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()).
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.
typeof(Pedido).FullName,
typeof(Cliente).FullName,
typeof(Carro).FullName,
typeof(Marca).FullName,
typeof(CreditRisk).FullName };
foreach (var entityName em entidades)
{
ÿ 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.
novo arquivo chamado SampleData.cs à pasta Initialization. Torne a classe pública e estática e atualize as instruções
using para o seguinte:
namespace AutoLot.Dal.Initialization {
919
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
novo()
{
Id = 1,
CustomerId = Clientes[4].Id,
PersonalInformation = new() {
FirstName = Clientes[4].PersonalInformation.FirstName,
LastName = Clientes[4].PersonalInformation.LastName
}
} };
tentar
Console.WriteLine(ex);
920
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
if (table.Any()) {
retornar;
}
estratégia IExecutionStrategy = context.Database.CreateExecutionStrategy();
estrategia.Execute(() => {
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.
DropAndCreateDatabase(contexto);
SeedData(contexto); }
921
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
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.
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.
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
• 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):
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>
usando System.IO;
usando AutoLot.Dal.EfStructures; usando
Microsoft.EntityFrameworkCore; usando
Microsoft.EntityFrameworkCore.Storage; usando
Microsoft.Extensions.Configuration;
namespace AutoLot.Dal.Tests {
Adicione dois métodos estáticos públicos para criar instâncias de IConfiguration e ApplicationDbContext
Aulas. Adicione o seguinte código à classe:
923
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
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.
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 {
924
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
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 {
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:
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(); }); }
usando
Sistema; usando AutoLot.Dal.Initialization;
namespace AutoLot.Dal.Tests.Base {
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() {
926
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
[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 {
//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 {
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.
_repo.Dispose(); }
...
}
928
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
_repo.Dispose(); }
...
}
usando Xunit;
namespace AutoLot.Dal.Tests {
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
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/.
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.
Estado da Entidade
Quando uma entidade é criada lendo dados do banco de dados, o valor EntityState é definido como Inalterado.
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.
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); }
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:
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); }
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() {
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() {
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() {
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() {
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() {
933
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
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):
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) {
934
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
Classificar registros
[Fato]
public void DeveSortByLastNameThenFirstName() {
pi2.FirstName, StringComparison.CurrentCultureIgnoreCase);
Assert.True(compareFirstName <= 0); }
935
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
O método Reverse() inverte toda a ordem de classificação, conforme demonstrado no próximo teste:
[Fato]
public void ShouldSortByFirstNameThenLastNameUsingReverse() {
• 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.
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.
• 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.
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:
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() {
O teste a seguir obtém o primeiro registro com base na ordem “sobrenome, nome”:
[Fato]
public void GetFirstMatchingRecordNameOrder() {
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); }
O teste a seguir afirma que uma exceção é lançada se não houver correspondência ao usar First():
[Fato]
public void FirstShouldThrowExceptionIfNoneMatch() {
ÿ 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); }
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:
Usando Single
[Fato]
public void SingleShouldThrowExceptionIfNoneMatch() {
939
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
[Fato]
public void SingleShouldThrowExceptionIfMoreThenOneMatch() {
Ao usar SingleOrDefault(), em vez de uma exceção, o resultado é um registro nulo quando nenhum dado é
retornado.
[Fato]
public void SingleOrDefaultShouldReturnDefaultIfNoneMatch() {
940
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
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:
ÿ 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 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.
[Fato]
public void ShouldGetAllOrdersExceptFiltered() {
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.
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
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.
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() {
Assert.NotNull(c.MakeNavigation);
Assert.NotNull(c.Orders.ToList()[0].CustomerNavigation); }); }
943
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
[Fato]
public void ShouldGetCarsOnOrderWithRelatedPropertiesAsSplitQuery() {
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:
944
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
Se você divide suas consultas ou não, depende das necessidades de sua empresa.
[Fato]
public void ShouldGetAllMakesAndCarsThatAreYellow() {
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
Alterar a consulta para uma consulta dividida produz este SQL (coleção do SQL Server Profiler):
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() {
Este teste mostra como carregar uma propriedade de navegação de coleção na entidade Car:
[Fato]
public void ShouldGetCollectionRelatedInformationExplicitly() {
947
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
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) {
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:
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) {
948
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
[Fato]
public void ShouldNotGetTheLemonsUsingFromSql() {
[Fato]
public void ShouldGetTheCarsUsingFromSqlWithIgnoreQueryFilters() {
.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) {
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() {
[Fato]
public void ShouldGetTheCountOfCarsIgnoreQueryFilters() {
--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) {
[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) {
Ambos os testes criam as mesmas chamadas SQL para o servidor, conforme mostrado aqui (o MakeId muda a cada teste
com base no InlineData):
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
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) {
WHERE ([i].[IsDrivable] = CAST(1 bit AS)) AND ([i].[MakeId] <> @__makeId_0)) THEN CAST(1 bit AS)
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():
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, };
ParameterName = "@petName",
SqlDbType = System.Data.SqlDbType.NVarChar,
Tamanho = 50, Direção = ParameterDirection.Output };
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) {
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.
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.
[Fato]
public void ShouldAddACar() {
ExecuteInATransaction(RunTheTest);
void RunTheTest() {
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.
954
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
[Fato]
public void ShouldAddACarWithAttach() {
ExecuteInATransaction(RunTheTest);
void RunTheTest() {
[Fato]
public void ShouldAddMultipleCars() {
ExecuteInATransaction(RunTheTest);
void RunTheTest() {
955
Machine Translated by Google
Capítulo 23 ÿ Construir uma camada de acesso a dados com o Entity Framework Core
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:
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.
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() {
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:
',N'@p0 nvarchar(50)',@p0=N'Honda'
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 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);
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:
DE [dbo].[Inventário]
ONDE @@ROWCOUNT = 1 AND [Id] = @p1;
ÿ 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
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);
};
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
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:
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.
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]
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.
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.
[Fato]
public void ShouldRemoveACar() {
ExecuteInATransaction(RunTheTest);
void RunTheTest() {
}
}
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
[Fato]
public void ShouldRemoveACarUsingState() {
ExecuteInASharedTransaction(RunTheTest);
}
}
[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());
}
}
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