{"id":346,"date":"2023-10-20T13:44:15","date_gmt":"2023-10-20T05:44:15","guid":{"rendered":"https:\/\/miie.net\/?p=346"},"modified":"2023-10-20T13:44:15","modified_gmt":"2023-10-20T05:44:15","slug":"pro-c10-chapter-24-test-driving-autolot","status":"publish","type":"post","link":"https:\/\/diji.net\/?p=346","title":{"rendered":"Pro C#10 CHAPTER 24 Test-Driving AutoLot"},"content":{"rendered":"<p>CHAPTER 24<\/p>\n<p>Test-Driving AutoLot<\/p>\n<p>Now that you have the finished AutoLot data access layer, it\u2019s time to take it for a test-drive. Integration testing in an integral part of software development and is a great way to make sure your data access code behaves as expected. In this chapter, we will be using xUnit, a testing framework for .NET Core.<br \/>\nAfter creating the solution and test project, the AutoLot.Dal and AutoLot.Models projects are added to the solution. Next, xUnit will be explored to show how to run automated tests. Then the rest of the chapter is dedicated to creating and executing integration tests.<\/p>\n<p>Setting Up the Test-Drives<br \/>\nInstead of creating a client application to test-drive the completed AutoLot data access layer, we are going to use automated integration tests. The tests will demonstrate create, read, update, and delete calls to the database. This allows us to examine the code without the overhead of creating another application. Each of the tests in this section will execute a query (either create, read, update, or delete) and then have one or more Assert statements to validate that the result is what was expected.<\/p>\n<p>Create the Project<br \/>\nTo get started, we are going to set up an integration test platform using xUnit, a .NET Core\u2013compatible testing framework. Start by adding a new xUnit test project named AutoLot.Dal.Tests. In Visual Studio, this project type is named xUnit Test Project.<\/p>\n<p>\u25a0Note Unit tests are designed to test a single unit of code. What we will be doing throughout this chapter is technically creating integration tests, since we are testing the C# code and EF Core all the way to the database and back.<\/p>\n<p>From the command-line interface, execute the following commands using the command-line interface (CLI):<\/p>\n<p>dotnet new sln -n Chapter24_AllProjects<br \/>\ndotnet new xunit -lang c# -n AutoLot.Dal.Tests -o .\\AutoLot.Dal.Tests -f net6.0 dotnet sln .\\Chapter23_AllProjects.sln add AutoLot.Dal.Tests<\/p>\n<p>\u00a9 Andrew Troelsen, Phil Japikse 2022<br \/>\nA. Troelsen and P. Japikse, Pro C# 10 with .NET 6, <a href=\"https:\/\/doi.org\/10.1007\/978-1-4842-7869-7_24\"><a href=\"https:\/\/doi.org\/10.1007\/978-1-4842-7869-7_24\"><a href=\"https:\/\/doi.org\/10.1007\/978-1-4842-7869-7_24\">https:\/\/doi.org\/10.1007\/978-1-4842-7869-7_24<\/a><\/a><\/a><\/p>\n<p>1051<\/p>\n<p>Add the Microsoft.EntityFrameworkCore, Microsoft.EntityFrameworkCore.Design, Microsoft. EntityFrameworkCore.SqlServer, and Microsoft.Extensions.Configuration.Json NuGet packages to the AutoLot.Dal.Tests project. If using the CLI, use the following commands:<\/p>\n<p>dotnet add AutoLot.Dal.Tests package Microsoft.EntityFrameworkCore<br \/>\ndotnet add AutoLot.Dal.Tests package Microsoft.EntityFrameworkCore.Design dotnet add AutoLot.Dal.Tests package Microsoft.EntityFrameworkCore.SqlServer dotnet add AutoLot.Dal.Tests package Microsoft.Extensions.Configuration.Json<\/p>\n<p>The tests will use the data initializer code that clears the temporal data, so the same adjustment to the project file must be made regarding the Microsoft.EntityFrameworkCore.Design package. Update the package to remove (or comment out) the IncludeAssets tag:<\/p>\n<p><PackageReference Include=\"Microsoft.EntityFrameworkCore.Design\" Version=\"6.0. 0-rc.1.21452.10\"><br \/>\n<!--<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive\n<\/IncludeAssets>--><br \/>\n<PrivateAssets>all<\/PrivateAssets><br \/>\n<\/PackageReference><\/p>\n<p>The versions of the Microsoft.NET.Test.Sdk and coverlet.collector packages that ship with the xUnit project template typically lag behind the currently available versions. To update them, either use the NuGet Package Manager in Visual Studio to update all NuGet packages or use the CLI. To update them with the CLI, then add them again since adding packages from the command line will always retrieve the latest non-prerelease version. Here are the commands:<\/p>\n<p>dotnet add AutoLot.Dal.Tests package Microsoft.NET.Test.Sdk dotnet add AutoLot.Dal.Tests package coverlet.collector<\/p>\n<p>Next, add project references to AutoLot.Models and AutoLot.Dal. To do this from the command line, execute the following (update the path and directory separator to your projects from Chapter 23):<\/p>\n<p>dotnet add AutoLot.Dal.Tests reference ..\\Chapter_23\\AutoLot.Dal dotnet add AutoLot.Dal.Tests reference ..\\Chapter_23\\AutoLot.Models<\/p>\n<p>Finally, turn off nullable reference types and enable global implicit using statements in the project file:<\/p>\n<p><PropertyGroup><br \/>\n<TargetFramework>net6.0<\/TargetFramework><br \/>\n<IsPackable>false<\/IsPackable><br \/>\n<Nullable>disable<\/Nullable><br \/>\n<ImplicitUsings>enable<\/ImplicitUsings><br \/>\n<\/PropertyGroup><\/p>\n<p>\u25a0Note At the time of this writing, the xUnit project template does add the node for ImplicitUsings into the project file.<\/p>\n<p>Make the AutoLot.Dal Internals Visible to AutoLot.Dal.Tests<br \/>\nTo test (or use) the methods and classes in the AutoLot.Dal project that are marked internal, the internals need to be made visible to the AutoLot.Dal.Tests project. Open the AutoLot.Dal.csproj file and add the following:<\/p>\n<p><ItemGroup><br \/>\n<AssemblyAttribute Include=\"System.Runtime.CompilerServices.InternalsVisibleToAttribute\"><br \/>\n<_Parameter1>AutoLot.Dal.Tests<\/_Parameter1><br \/>\n<\/AssemblyAttribute><br \/>\n<\/ItemGroup><\/p>\n<p>Add the GlobalUsings File<br \/>\nCreate a new file named GlobalUsings.cs in the root of the AutoLot.Dal.Tests project. This will be the central location for all the using statements needed in this project. Update the code to match the following:<\/p>\n<p>global using System.Data;<br \/>\nglobal using System.Linq.Expressions;<\/p>\n<p>global using AutoLot.Dal.EfStructures; global using AutoLot.Dal.Exceptions; global using AutoLot.Dal.Initialization; global using AutoLot.Dal.Repos;<br \/>\nglobal using AutoLot.Dal.Repos.Interfaces;<br \/>\nglobal using AutoLot.Dal.Tests.Base; global using AutoLot.Models.Entities;<br \/>\nglobal using AutoLot.Models.Entities.Owned;<\/p>\n<p>global using Microsoft.EntityFrameworkCore;<br \/>\nglobal using Microsoft.EntityFrameworkCore.ChangeTracking; global using Microsoft.EntityFrameworkCore.Storage;<br \/>\nglobal using Microsoft.EntityFrameworkCore.Query; global using Microsoft.Extensions.Configuration;<br \/>\nglobal using Xunit;<br \/>\nglobal using Xunit.Abstractions;<\/p>\n<p>A First Look at xUnit<br \/>\nThere are two types of tests that will be used in this chapter. Parameterless test methods are referred to as facts (and use the Fact attribute). Tests that take parameters are referred to as theories (and use the Theory attribute). Theory tests run multiple iterations of the test method passing in different values for each run. To demonstrate these test types, create a new class named SampleTests.cs in the AutoLot.Dal.Tests project and update the code to the following:<\/p>\n<p>namespace AutoLot.Dal.Tests;<\/p>\n<p>public class SampleTests<br \/>\n{<br \/>\n\/\/tests go here<br \/>\n}<\/p>\n<p>Fact Test Methods<br \/>\nThe first test to create is a Fact test. With Fact tests, all values are contained in the test method. The following (trivial) example tests 3+2=5:<\/p>\n<p>[Fact]<br \/>\npublic void SimpleFactTest()<br \/>\n{<br \/>\nAssert.Equal(5,3+2);<br \/>\n}<\/p>\n<p>There are a variety of Assert types available. In this example, the test is asserting the actual result (3+2) equals the expected result (5).<\/p>\n<p>Theory Test Methods<br \/>\nWhen using Theory type tests, the values for the tests are passed into the test method. The previous test tested only one case, 3+2. Theories permit testing multiple use cases without having to repeat the test code multiple times. The values can come from the InlineData attribute, methods, or classes. For our purpose, we will only use the InlineData attribute. Create the following test that provided different addends and expected results to the test:<\/p>\n<p>[Theory] [InlineData(3,2,5)]<br \/>\n[InlineData(1,-1,0)]<br \/>\npublic void SimpleTheoryTest(int addend1, int addend2, int expectedResult)<br \/>\n{<br \/>\nAssert.Equal(expectedResult,addend1+addend2);<br \/>\n}<\/p>\n<p>\u25a0Note there will be many examples of Fact and Theory tests throughout this chapter, as well as additional capabilities for the xUnit testing framework. For more information in the xUnit test framework, consult the documentation located at <a href=\"https:\/\/xunit.net\/\"><a href=\"https:\/\/xunit.net\/\"><a href=\"https:\/\/xunit.net\/\">https:\/\/xunit.net\/<\/a><\/a><\/a>.<\/p>\n<p>Executing Tests<br \/>\nWhile the xUnit tests can be executed from the command line (using dotnet test), it is a better developer experience (in my opinion) to use Visual Studio to execute the tests. Launch the Test Explorer from the Test menu to have access to running and debugging all or selected tests.<\/p>\n<p>Configure the Project and DbContext Instances<br \/>\nTo retrieve the connection string at runtime, we are going to use the .NET Core configuration capabilities using a JSON file. Add a JSON file, named appsettings.testing.json, to the project and add your connection string information into the file in the following format (update your connection string from what is listed here as necessary):<\/p>\n<p>{<br \/>\n&quot;ConnectionStrings&quot;: {<br \/>\n&quot;AutoLot&quot;: &quot;server=.,5433;Database=AutoLot;User Id=sa;Password=P@ssw0rd;&quot;<br \/>\n}<br \/>\n}<\/p>\n<p>Update the project file to have the settings file copied to the output folder on every build. Do that by adding the following ItemGroup to the AutoLot.Dal.Tests.csproj file:<\/p>\n<p><ItemGroup><br \/>\n<None Update=\"appsettings.testing.json\"><br \/>\n<CopyToOutputDirectory>Always<\/CopyToOutputDirectory><br \/>\n<\/None><br \/>\n<\/ItemGroup><\/p>\n<p>Create the Integration Test Helper<br \/>\nThe TestHelpers class will handle the application configuration as well as create new instances of<br \/>\nApplicationDbContext. Add a new public static class named TestHelpers.cs in the root of the project:<br \/>\nnamespace AutoLot.Dal.Tests; public static class TestHelpers<br \/>\n{<br \/>\n\/\/helper code goes here<br \/>\n}<\/p>\n<p>Add a public static method to create an instance of the IConfiguration interface using the appsettings. testing.json file. Add the following code to the class:<\/p>\n<p>public static IConfiguration GetConfiguration() =&gt; new ConfigurationBuilder()<br \/>\n.SetBasePath(Directory.GetCurrentDirectory())<br \/>\n.AddJsonFile(&quot;appsettings.testing.json&quot;, true, true)<br \/>\n.Build();<\/p>\n<p>\u25a0Note  Configuration of assemblies is covered in Chapter 16.<\/p>\n<p>Add another public static method to create instances of the ApplicationDbContext class using the<br \/>\nIConfiguration instance. Add the following code to the class:<\/p>\n<p>public static ApplicationDbContext GetContext(IConfiguration configuration)<br \/>\n{<br \/>\nvar optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>(); var connectionString = configuration.GetConnectionString(&quot;AutoLot&quot;); optionsBuilder.UseSqlServer(connectionString);<br \/>\nreturn new ApplicationDbContext(optionsBuilder.Options);<br \/>\n}<\/p>\n<p>Add another static method that will create a new instance of ApplicationDbContext. This demonstrates creating an instance of the ApplicationDbContext class from an existing instance to share the connection and transaction.public static ApplicationDbContext GetSecondContext(<\/p>\n<p>ApplicationDbContext oldContext, IDbContextTransaction trans)<br \/>\n{<br \/>\nvar optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>(); optionsBuilder.UseSqlServer(oldContext.Database.GetDbConnection());<br \/>\nvar context = new ApplicationDbContext(optionsBuilder.Options); context.Database.UseTransaction(trans.GetDbTransaction()); return context;<br \/>\n}<\/p>\n<p>Add the BaseTest Class<br \/>\nThe BaseTest class handles the infrastructure for the tests in this chapter. Add a new folder named Base to the test project and add a new class file named BaseTest.cs to that folder. Make the class abstract and implement IDisposable. Add two protected readonly properties to hold the IConfiguration and ApplicationDbContext instances and dispose of the ApplicationDbContext instance in the virtual Dispose() method.<\/p>\n<p>namespace AutoLot.Dal.Tests.Base;<\/p>\n<p>public abstract class BaseTest : IDisposable<br \/>\n{<br \/>\nprotected readonly IConfiguration Configuration; protected readonly ApplicationDbContext Context;<\/p>\n<p>public virtual void Dispose()<br \/>\n{<br \/>\nContext.Dispose();<br \/>\n}<br \/>\n}<\/p>\n<p>The xUnit test framework provides a mechanism to run code before and after each test is executed. Test classes (called fixtures) that implement the IDisposable interface will execute the code in the class constructor in the inheritance chain before each test is run. This is commonly referred to as the test setup. After each test is executed, the code in the Dispose methods (through the inheritance chain) is executed. This is referred to as test teardown.<\/p>\n<p>Add a protected constructor that creates an instance of IConfiguration and assigns it to the protected class variable. Use the configuration to create an instance of ApplicationDbContext using the TestHelper class and also assign it to the protected class variable.<\/p>\n<p>protected BaseTest()<br \/>\n{<br \/>\nConfiguration = TestHelpers.GetConfiguration(); Context = TestHelpers.GetContext(Configuration);<br \/>\n}<\/p>\n<p>The ITestOutputHelper interface allows for content to be written to the test output window. When using the IDisposable pattern with xUnit test fixtures, the instance for this interface can be injected into the constructor. Add a protected readonly variable to hold the instance and update the constructor to the following:<\/p>\n<p>protected readonly ITestOutputHelper OutputHelper;<\/p>\n<p>protected BaseTest(ITestOutputHelper outputHelper)<br \/>\n{<br \/>\nConfiguration = TestHelpers.GetConfiguration(); Context = TestHelpers.GetContext(Configuration); OutputHelper = outputHelper;<br \/>\n}<\/p>\n<p>Add the Transacted Test Execution Helpers<br \/>\nThe final two methods in the BaseTest class enable running test methods in a transaction. The methods will take an Action delegate as a single parameter, create an explicit transaction (or enlist an existing transaction), execute the Action delegate, and then roll back the transaction. We do this so any create\/ update\/delete tests leave the database in the state it was in before the test was run. The transactions are executed inside an execution strategy in case the ApplicationDbContext is configured to enable retry on transient errors.<br \/>\nThe ExecuteInATransaction() method executes the action delegate using a single instance of the<br \/>\nApplicationDbContext. Add the following code into your BaseTest class:<\/p>\n<p>protected void ExecuteInATransaction(Action actionToExecute)<br \/>\n{<br \/>\nvar strategy = Context.Database.CreateExecutionStrategy(); strategy.Execute(() =&gt;<br \/>\n{<br \/>\nusing var trans = Context.Database.BeginTransaction(); actionToExecute();<br \/>\ntrans.Rollback();<br \/>\n});<br \/>\n}<\/p>\n<p>The ExecuteInASharedTransaction() method allows for multiple ApplicationDbContext instances to share a single transaction. Add the following code into your BaseTest class:<\/p>\n<p>protected void ExecuteInASharedTransaction(Action<IDbContextTransaction> actionToExecute)<br \/>\n{<br \/>\nvar strategy = Context.Database.CreateExecutionStrategy(); strategy.Execute(() =&gt;<br \/>\n{<br \/>\nusing IDbContextTransaction trans = Context.Database.BeginTransaction(IsolationLevel.ReadUncommitted);<br \/>\nactionToExecute(trans); trans.Rollback();<br \/>\n});<br \/>\n}<\/p>\n<p>Add the EnsureAutoLotDatabase Test Fixture Class<br \/>\nThe xUnit test framework provides a mechanism for running code before any of the tests are run (referred to as fixture setup) and after all the tests are run (referred to as fixture teardown). This practice is generally not recommended, but in our case, we want to ensure that the database is created and loaded with data before any tests are run instead of before each test is run. Test classes that implement IClassFixture<T> where T:<br \/>\nTestFixtureClass will have the constructor code of T (the TestFixtureClass) executed before any tests are run, and the Dispose() code will run after all tests are completed.<br \/>\nAdd a new class named EnsureAutoLotDatabaseTestFixture.cs to the Base directory and implement<br \/>\nIDisposable. Make the class public and sealed, and add the following using statements:<\/p>\n<p>namespace AutoLot.Dal.Tests.Base;<\/p>\n<p>public sealed class EnsureAutoLotDatabaseTestFixture : IDisposable<br \/>\n{<br \/>\n\/\/add implementation code here<\/p>\n<p>public void Dispose()<br \/>\n{<br \/>\n}<br \/>\n}<\/p>\n<p>The constructor code uses the TestHelpers class to get the instance of IConfiguration and then gets an instance of the ApplicationDbContext. Next, it calls the ClearAndReseedDatabase() method from the SampleDataInitializer. The final line disposes of the context instance. In our examples, the Dispose() method doesn\u2019t have any code but needs to be implemented to satisfy the IDisposable interface. The following listing shows the updated constructor:<\/p>\n<p>public EnsureAutoLotDatabaseTestFixture()<br \/>\n{<br \/>\nvar configuration = TestHelpers.GetConfiguration(); var context = TestHelpers.GetContext(configuration); SampleDataInitializer.ClearAndReseedDatabase(context); context.Dispose();<br \/>\n}<\/p>\n<p>Add the Integration Test Classes<br \/>\nThe next step is to add the classes that will hold the automated tests. These classes are referred to as test fixtures. Add a new folder named IntegrationTests in the AutoLot.Dal.Tests folder and add five files named CarTests.cs, CustomerOrderViewModelTests.cs, CustomerTests.cs, MakeTests.cs, and OrderTests.cs to this folder.<br \/>\nDepending on the capabilities of the test runner, xUnit tests are run in serial within a test fixture (class), but in parallel across test fixtures. This can be problematic when executing integration tests that interact with a database. Parallel database tests using a single database instance can cause blocks, produce erroneous results, and are generally problematic.<br \/>\nxUnit test execution can be changed to serial across test fixtures by adding them into the same test collection. Test collections are defined by name using the Collection attribute on the class. Add the following Collection attribute to the top of all four classes:<\/p>\n<p>[Collection(&quot;Integration Tests&quot;)]<\/p>\n<p>Next, inherit from BaseTest and implement the IClassFixture interface in both classes. Add a constructor to receive the ITestOutputHelper instance and pass it to the base class. Update each class to match the following:<\/p>\n<p>\/\/CarTests.cs<br \/>\nnamespace AutoLot.Dal.Tests.IntegrationTests;<\/p>\n<p>[Collection(&quot;Integation Tests&quot;)] public class CarTests<br \/>\n: BaseTest, IClassFixture<EnsureAutoLotDatabaseTestFixture><br \/>\n{<br \/>\npublic CarTests(ITestOutputHelper outputHelper) : base(outputHelper)<br \/>\n{<br \/>\n}<br \/>\n}<\/p>\n<p>\/\/CustomerOrderViewModelTests.cs<br \/>\nnamespace AutoLot.Dal.Tests.IntegrationTests;<\/p>\n<p>[Collection(&quot;Integation Tests&quot;)]<br \/>\npublic class CustomerOrderViewModelTests<br \/>\n: BaseTest, IClassFixture<EnsureAutoLotDatabaseTestFixture><br \/>\n{<br \/>\npublic CustomerOrderViewModelTests(ITestOutputHelper outputHelper)<br \/>\n: base(outputHelper)<br \/>\n{<br \/>\n}<br \/>\n}<\/p>\n<p>\/\/CustomerTests.cs<br \/>\nnamespace AutoLot.Dal.Tests.IntegrationTests;<\/p>\n<p>[Collection(&quot;Integation Tests&quot;)] public class CustomerTests<br \/>\n: BaseTest, IClassFixture<EnsureAutoLotDatabaseTestFixture><\/p>\n<p>{<br \/>\npublic CustomerTests(ITestOutputHelper outputHelper) : base(outputHelper)<br \/>\n{<br \/>\n}<br \/>\n}<\/p>\n<p>\/\/MakeTests.cs<br \/>\nnamespace AutoLot.Dal.Tests.IntegrationTests;<\/p>\n<p>[Collection(&quot;Integation Tests&quot;)] public class MakeTests<br \/>\n: BaseTest, IClassFixture<EnsureAutoLotDatabaseTestFixture><br \/>\n{<br \/>\npublic MakeTests(ITestOutputHelper outputHelper) : base(outputHelper)<br \/>\n{<br \/>\n}<br \/>\n}<\/p>\n<p>\/\/OrderTests.cs<br \/>\nnamespace AutoLot.Dal.Tests.IntegrationTests;<\/p>\n<p>[Collection(&quot;Integation Tests&quot;)] public class OrderTests<br \/>\n: BaseTest, IClassFixture<EnsureAutoLotDatabaseTestFixture><br \/>\n{<br \/>\npublic OrderTests(ITestOutputHelper outputHelper):base(outputHelper)<br \/>\n{<br \/>\n}<br \/>\n}<\/p>\n<p>For the CarTests class, update the constructor to create an instance of the CarRepo and assign the instance to a private readonly class-level variable. Override the Dispose() method, and in that method, dispose of the repo.<\/p>\n<p>[Collection(&quot;Integration Tests&quot;)]<br \/>\npublic class CarTests : BaseTest, IClassFixture<EnsureAutoLotDatabaseTestFixture><br \/>\n{<br \/>\nprivate readonly ICarRepo _carRepo;<br \/>\npublic CarTests(ITestOutputHelper outputHelper) : base(outputHelper)<br \/>\n{<br \/>\n_carRepo = new CarRepo(Context);<br \/>\n}<\/p>\n<p>public override void Dispose()<br \/>\n{<br \/>\n_carRepo.Dispose(); base.Dispose();<br \/>\n}<br \/>\n}<\/p>\n<p>Repeat the process for the CustomerOrderViewModelTests class, using the<br \/>\nCustomerOrderViewModelRepo instead:<\/p>\n<p>[Collection(&quot;Integration Tests&quot;)]<br \/>\npublic class CustomerOrderViewModelTests : BaseTest, IClassFixture<EnsureAutoLotDatabase TestFixture><br \/>\n{<br \/>\nprivate readonly ICustomerOrderViewModelRepo _repo;<br \/>\npublic CustomerOrderViewModelTests(ITestOutputHelper outputHelper) : base(outputHelper)<br \/>\n{<br \/>\n_repo = new CustomerOrderViewModelRepo(Context);<br \/>\n}<\/p>\n<p>public override void Dispose()<br \/>\n{<br \/>\n_repo.Dispose(); base.Dispose();<br \/>\n}<br \/>\n}<\/p>\n<p>The setup for the CustomerTests class is simpler, since it does not use the CustomerRepo:<\/p>\n<p>[Collection(&quot;Integration Tests&quot;)]<br \/>\npublic class CustomerTests : BaseTest, IClassFixture<EnsureAutoLotDatabaseTestFixture><br \/>\n{<br \/>\npublic CustomerOrderViewModelTests(ITestOutputHelper outputHelper) : base(outputHelper)<br \/>\n{<br \/>\n}<br \/>\n}<\/p>\n<p>The same process is needed for the MakeTests class, using MakeRepo:<\/p>\n<p>[Collection(&quot;Integration Tests&quot;)]<br \/>\npublic class MakeTests : BaseTest, IClassFixture<EnsureAutoLotDatabaseTestFixture><br \/>\n{<br \/>\nprivate readonly IMakeRepo _repo;<br \/>\npublic MakeTests(ITestOutputHelper outputHelper) : base(outputHelper)<br \/>\n{<br \/>\n_repo = new MakeRepo(Context);<br \/>\n}<\/p>\n<p>public override void Dispose()<br \/>\n{<br \/>\n_repo.Dispose(); base.Dispose();<br \/>\n}<br \/>\n}<\/p>\n<p>The final repository update is in the OrderTests class, using OrderRepo:<\/p>\n<p>[Collection(&quot;Integration Tests&quot;)]<br \/>\npublic class OrderTests : BaseTest, IClassFixture<EnsureAutoLotDatabaseTestFixture><br \/>\n{<br \/>\nprivate readonly IOrderRepo _repo;<br \/>\npublic OrderTests(ITestOutputHelper outputHelper) : base(outputHelper)<br \/>\n{<br \/>\n_repo = new OrderRepo(Context);<br \/>\n}<\/p>\n<p>public override void Dispose()<br \/>\n{<br \/>\n_repo.Dispose(); base.Dispose();<br \/>\n}<br \/>\n}<\/p>\n<p>Querying the Database<br \/>\nRecall that creating entity instances from a database data involves executing a LINQ statement or a SQL statement (using FromSqlRaw()\/FromSqlInterpolated()) against the DbSet<T> properties. When using LINQ, the statements are converted to SQL by the database provider and the LINQ translation engine, and the appropriate data is read from the database. Data can also be loaded using the FromSqlRaw() or<br \/>\nFromSqlInterpolated() method using raw SQL strings and, optionally, additional LINQ statements. Entities loaded into the DbSet<T> collections are added to the ChangeTracker by default but can be added without tracking. Data loaded in keyless DbSet<T> collections is never tracked.<br \/>\nIf related entities are already loaded into the DbSet<T>, EF Core will wire up the new instances along the navigation properties. For example, if the Cars are loaded into the DbSet<Car> collection and then the related Orders are loaded into the DbSet<Order> of the same ApplicationDbContext instance, the Car. Orders navigation property will return the related Order entities without requerying the database.<br \/>\nMany of the methods demonstrated here have async versions available. The syntax of the LINQ queries are structurally the same, so I will only demonstrate the nonasync version.<\/p>\n<p>LINQ Queries<br \/>\nThe DbSet<T> collection type implements (among other interfaces) IQueryable<T>. This allows C# LINQ commands to be used to create queries to get data from the database. While all C# LINQ statements are available for use with the DbSet<T> collection type, some LINQ statements might not be supported by the database provider, and additional LINQ statements are added by EF Core. Unsupported LINQ statements that cannot be translated into the database provider\u2019s query language will throw a runtime exception. Some nontranslatable LINQ statements will execute on the client side if they are the last statement in the LINQ chain; however, others (like the update to the Take() method that works with ranges) will still throw an error unless the query is first executed using ToList() or a similar construct.<\/p>\n<p>LINQ Execution<br \/>\nWhere ToQueryString() is available, the tests in this next section set a variable (qs) to this value and are output to the test results using the ITestOutputHelper so you can examine the query while running the tests.<\/p>\n<p>Get All Records<br \/>\nTo get all the records for a table, simply use the DbSet<T> property directly without any LINQ statements. Add the following Fact to the CustomerTests.cs class:<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetAllOfTheCustomers()<br \/>\n{<br \/>\nvar qs = Context.Customers.ToQueryString(); var customers = Context.Customers.ToList(); Assert.Equal(5, customers.Count);<br \/>\n}<\/p>\n<p>The statement gets translated into the following SQL:<\/p>\n<p>SELECT .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [Dbo].[Customers] AS <\/p>\n<p>The same process is used for Keyless entities, like the CustomerOrderViewModel, which is configured to get its data from the CustomerOrderView. Add the following test to the CustomerOrderViewModelTest.cs class to show getting data from the view:<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetAllViewModels()<br \/>\n{<br \/>\nvar qs = Context.CustomerOrderViewModels.ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;); List<Models.ViewModels.CustomerOrderViewModel> list =<br \/>\nContext.CustomerOrderViewModels.ToList(); Assert.NotEmpty(list);<br \/>\nAssert.Equal(5, list.Count);<br \/>\n}<\/p>\n<p>The statement gets translated into the following SQL:<\/p>\n<p>SELECT .[Color], .[DateBuilt], .[Display], .[FirstName],<br \/>\n.[IsDrivable], .[LastName], .[Make], .[PetName], .[Price] FROM [CustomerOrderView] AS <\/p>\n<p>Filter Records<br \/>\nThe following test in the CustomerTests class shows querying for customers where the last name starts with a W (case insensitive):<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetCustomersWithLastNameW()<br \/>\n{<br \/>\nIQueryable<Customer> query = Context.Customers<br \/>\n.Where(x =&gt; x.PersonInformation.LastName.StartsWith(&quot;W&quot;)); var qs = query.ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;);<\/p>\n<p>List<Customer> customers = query.ToList(); Assert.Equal(2, customers.Count);<br \/>\nforeach (var customer in customers)<br \/>\n{<br \/>\nvar pi = customer.PersonInformation;<br \/>\nAssert.StartsWith(&quot;W&quot;, pi.LastName, StringComparison.OrdinalIgnoreCase);<br \/>\n}<br \/>\n}<\/p>\n<p>The LINQ query gets translated into the following SQL:<\/p>\n<p>SELECT .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [dbo].[Customers] AS <br \/>\nWHERE .[LastName] IS NOT NULL AND (.[LastName] LIKE N'W%')<\/p>\n<p>The next test in the CustomerTests class demonstrates chaining Where() methods in a LINQ query to find the customers where the last name starts with a W and the first name starts with an M. Note that since SQL Server is case insensitive, these queries are also case insensitive:<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetCustomersWithLastNameWAndFirstNameM()<br \/>\n{<br \/>\nIQueryable<Customer> query = Context.Customers<br \/>\n.Where(x =&gt; x.PersonInformation.LastName.StartsWith(&quot;W&quot;))<br \/>\n.Where(x =&gt; x.PersonInformation.FirstName.StartsWith(&quot;M&quot;)); var qs = query.ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;);<br \/>\nList<Customer> customers = query.ToList(); Assert.Single(customers);<br \/>\nforeach (var customer in customers)<br \/>\n{<br \/>\nvar pi = customer.PersonInformation;<br \/>\nAssert.StartsWith(&quot;W&quot;, pi.LastName, StringComparison.OrdinalIgnoreCase); Assert.StartsWith(&quot;M&quot;, pi.FirstName, StringComparison.OrdinalIgnoreCase);<br \/>\n}<br \/>\n}<\/p>\n<p>This test in the CustomerTests class repeats the same filter using a single Where() method instead of two chained methods:<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetCustomersWithLastNameWAndFirstNameM()<br \/>\n{<br \/>\nIQueryable<Customer> query = Context.Customers<br \/>\n.Where(x =&gt; x.PersonInformation.LastName.StartsWith(&quot;W&quot;) &amp;&amp;<br \/>\nx.PersonInformation.FirstName.StartsWith(&quot;M&quot;));<br \/>\nvar qs = query.ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;); List<Customer> customers = query.ToList(); Assert.Single(customers);<br \/>\nforeach (var customer in customers)<\/p>\n<p>{<br \/>\nvar pi = customer.PersonInformation;<br \/>\nAssert.StartsWith(&quot;W&quot;, pi.LastName, StringComparison.OrdinalIgnoreCase); Assert.StartsWith(&quot;M&quot;, pi.FirstName, StringComparison.OrdinalIgnoreCase);<br \/>\n}<br \/>\n}<\/p>\n<p>Both queries get translated into the following SQL:<\/p>\n<p>SELECT .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [dbo].[Customers] AS <br \/>\nWHERE (.[LastName] IS NOT NULL AND (.[LastName] LIKE N'W%')) AND (.[FirstName] IS NOT NULL AND (.[FirstName] LIKE N'M%'))<\/p>\n<p>The next test in the CustomerTests class demonstrates querying for customers where the last name starts with a W (case insensitive) or the last name starts with an H (case insensitive):<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetCustomersWithLastNameWOrH()<br \/>\n{<br \/>\nIQueryable<Customer> query = Context.Customers<br \/>\n.Where(x =&gt; x.PersonInformation.LastName.StartsWith(&quot;W&quot;) ||<br \/>\nx.PersonInformation.LastName.StartsWith(&quot;H&quot;));<br \/>\nvar qs = query.ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;); List<Customer> customers = query.ToList(); Assert.Equal(3, customers.Count);<br \/>\nforeach (var customer in customers)<br \/>\n{<br \/>\nvar pi = customer.PersonInformation; Assert.True(<br \/>\npi.LastName.StartsWith(&quot;W&quot;,StringComparison.OrdinalIgnoreCase) || pi.LastName.StartsWith(&quot;H&quot;,StringComparison.OrdinalIgnoreCase));<br \/>\n}<br \/>\n}<\/p>\n<p>This gets translated into the following SQL:<\/p>\n<p>SELECT .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [dbo].[Customers] AS <br \/>\nWHERE (.[LastName] IS NOT NULL AND (.[LastName] LIKE N'W%')) OR (.[LastName] IS NOT NULL AND (.[LastName] LIKE N'H%'))<\/p>\n<p>The following in the CustomerTests class also queries for customers where the last name starts with a W (case insensitive) or the last name starts with an H (case insensitive). This test demonstrates using the EF.Functions.Like() method. Note that you must include the wildcard (%) yourself.<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetCustomersWithLastNameWOrH()<br \/>\n{<br \/>\nIQueryable<Customer> query = Context.Customers<\/p>\n<p>.Where(x =&gt; EF.Functions.Like(x.PersonInformation.LastName, &quot;W%&quot;) ||<br \/>\nEF.Functions.Like(x.PersonInformation.LastName,  &quot;H%&quot;));<br \/>\nvar qs = query.ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;); List<Customer> customers = query.ToList(); Assert.Equal(3, customers.Count);<br \/>\n}<\/p>\n<p>This gets translated into the following SQL (notice it does not check for null):<\/p>\n<p>SELECT .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [Dbo].[Customers] AS <br \/>\nWHERE (.[LastName] LIKE N'W%') OR (.[LastName] LIKE N'H%')<\/p>\n<p>The following test in the CarTests.cs class uses a Theory to query for the number of Car records (drivable or not) in the Inventory table based on a specified MakeId:<\/p>\n<p>[Theory] [InlineData(1, 2)]<br \/>\n[InlineData(2, 1)]<br \/>\n[InlineData(3, 1)]<br \/>\n[InlineData(4, 2)]<br \/>\n[InlineData(5, 3)]<br \/>\n[InlineData(6, 1)]<br \/>\npublic void ShouldGetTheCarsByMake(int makeId, int expectedCount)<br \/>\n{<br \/>\nIQueryable<Car> query =<br \/>\nContext.Cars.IgnoreQueryFilters().Where(x =&gt; x.MakeId == makeId);<br \/>\nvar qs = query.ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;); var cars = query.ToList(); Assert.Equal(expectedCount, cars.Count);<br \/>\n}<\/p>\n<p>Each InlineData row becomes a unique test in the test runner. For this example, six tests are processed, and six queries are executed against the database. Here is the SQL from one of the tests (the only difference in the queries from the other tests in the Theory is the value for MakeId):<\/p>\n<p>DECLARE @ makeId_0 int = 1;<\/p>\n<p>SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable],<br \/>\n[i].[MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp] FROM [dbo].[Inventory] AS [i]<br \/>\nWHERE [i].[MakeId] = @ makeId_0<\/p>\n<p>The next test uses the Car repository to get the number of records for each Make. Since the GetCarsBy()<br \/>\nmethod leaves the query filters in place, there is one less record when the MakeId is one.<\/p>\n<p>[Theory] [InlineData(1, 1)]<br \/>\n[InlineData(2, 1)]<br \/>\n[InlineData(3, 1)]<\/p>\n<p>[InlineData(4, 2)]<br \/>\n[InlineData(5, 3)]<br \/>\n[InlineData(6, 1)]<br \/>\npublic void ShouldGetTheCarsByMakeUsingTheRepo(int makeId, int expectedCount)<br \/>\n{<br \/>\nvar qs = _carRepo.GetAllBy(makeId).AsQueryable().ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;);<br \/>\nvar cars = _carRepo.GetAllBy(makeId).ToList(); Assert.Equal(expectedCount, cars.Count);<br \/>\n}<\/p>\n<p>When examining the generated query, you can see the query filter that excluded records that aren\u2019t drivable:<\/p>\n<p>DECLARE @ makeId_0 int = 1;<\/p>\n<p>SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable],<br \/>\n[i].[MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp], [m].[Id], [m].[Name], [m].[TimeStamp]<br \/>\nFROM [dbo].[Inventory] AS [i]<br \/>\nINNER JOIN [dbo].[Makes] AS [m] ON [i].[MakeId] = [m].[Id]<br \/>\nWHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND ([i].[MakeId] = @ makeId_0) ORDER BY [i].[PetName]<\/p>\n<p>Sort Records<br \/>\nRecall that sorting is achieved by using OrderBy()\/OrderByDescending(). If more than one story level is required, add ThenBy()\/ThenByDescending() for each subsequent property. Ascending and descending sorts for different properties can be used together, as shown in the following test in the CustomerTests.cs file:<\/p>\n<p>[Fact]<br \/>\npublic void ShouldSortByLastNameThenFirstName()<br \/>\n{<br \/>\n\/\/Sort by Last name then first name var query = Context.Customers<br \/>\n.OrderBy(x =&gt; x.PersonInformation.LastName)<br \/>\n.ThenByDescending(x =&gt; x.PersonInformation.FirstName); var qs = query.ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;);<br \/>\nvar customers = query.ToList();<br \/>\nfor (int x = 0; x &lt; customers.Count - 1; x++)<br \/>\n{<br \/>\nCompare(customers[x].PersonInformation, customers[x + 1].PersonInformation);<br \/>\n}<br \/>\nstatic void Compare(Person p1, Person p2)<br \/>\n{<br \/>\nvar compareValue = string.Compare(p1.LastName, p2.LastName, StringComparison.CurrentCultureIgnoreCase);<br \/>\nAssert.True(compareValue &lt;=0); if (compareValue == 0)<\/p>\n<p>{<br \/>\n\/\/Descending first name sort Assert.True(string.Compare(p1.FirstName,p2.FirstName,<br \/>\nStringComparison.CurrentCultureIgnoreCase) &gt;= 0);<br \/>\n}<br \/>\n}<br \/>\n}<\/p>\n<p>The preceding LINQ query gets translated into the following:<\/p>\n<p>SELECT .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [dbo].[Customers] AS <br \/>\nORDER BY .[LastName], .[FirstName] DESC<\/p>\n<p>Reverse Sort Records<br \/>\nThe Reverse() method reverses the entire sort order, as demonstrated in the next test:<\/p>\n<p>[Fact]<br \/>\npublic void ShouldSortByFirstNameThenLastNameUsingReverse()<br \/>\n{<br \/>\n\/\/Sort by Last name then first name descending then reverse the sort var query = Context.Customers<br \/>\n.OrderBy(x =&gt; x.PersonInformation.LastName)<br \/>\n.ThenByDescending(x =&gt; x.PersonInformation.FirstName)<br \/>\n.Reverse();<br \/>\nvar qs = query.ToQueryString(); var customers = query.ToList();<br \/>\n\/\/if only one customer, nothing to test if (customers.Count &lt;= 1) { return; }<\/p>\n<p>for (int x = 0; x &lt; customers.Count - 1; x++)<br \/>\n{<br \/>\nvar pi1 = customers[x].PersonInformation;<br \/>\nvar pi2 = customers[x + 1].PersonInformation;<br \/>\nvar compareLastName = string.Compare(pi1.LastName, pi2.LastName, StringComparison.CurrentCultureIgnoreCase); Assert.True(compareLastName &gt;= 0);<br \/>\nif (compareLastName != 0) continue;<br \/>\nvar compareFirstName = string.Compare(pi1.FirstName, pi2.FirstName, StringComparison.CurrentCultureIgnoreCase); Assert.True(compareFirstName &lt;= 0);<br \/>\n}<br \/>\n}<\/p>\n<p>The preceding LINQ query gets translated into the following, inverting the original<br \/>\nOrderBy()\/ThenByDescending() query:<\/p>\n<p>SELECT .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [dbo].[Customers] AS <br \/>\nORDER BY .[LastName] DESC, .[FirstName]<\/p>\n<p>Single-Record Queries<br \/>\nBecause of the immediate execution of the single-record LINQ statements, the ToQueryString() method isn\u2019t available. The listed query translations are provided by using SQL Server Profiler. All of the single- record tests are in the CustomerTests.cs file.<\/p>\n<p>Using First<br \/>\nWhen using the parameterless form of First() and FirstOrDefault(), the first record (based on database order or any preceding ordering clauses) will be returned. The following test shows querying for the first record based on database order:<\/p>\n<p>[Fact]<br \/>\npublic void GetFirstMatchingRecordDatabaseOrder()<br \/>\n{<br \/>\n\/\/Gets the first record, database order var customer = Context.Customers.First(); Assert.Equal(1, customer.Id);<br \/>\n}<\/p>\n<p>The preceding LINQ query gets translated into the following:<\/p>\n<p>SELECT TOP(1) .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [Dbo].[Customers] AS <br \/>\nThe following test demonstrates getting the first record based on \u201clast name, first name\u201d order:<\/p>\n<p>[Fact]<br \/>\npublic void GetFirstMatchingRecordNameOrder()<br \/>\n{<br \/>\n\/\/Gets the first record, lastname, first name order var customer = Context.Customers<br \/>\n.OrderBy(x =&gt; x.PersonInformation.LastName)<br \/>\n.ThenBy(x =&gt; x.PersonInformation.FirstName)<br \/>\n.First();<br \/>\nAssert.Equal(1, customer.Id);<br \/>\n}<\/p>\n<p>The preceding LINQ query gets translated into the following:<\/p>\n<p>SELECT TOP(1) .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [Dbo].[Customers] AS <br \/>\nORDER BY .[LastName], .[FirstName]<br \/>\nThe following test asserts that an exception is thrown if there isn\u2019t a match when using First(): [Fact]<br \/>\npublic void FirstShouldThrowExceptionIfNoneMatch()<br \/>\n{<\/p>\n<p>\/\/Filters based on Id. Throws due to no match<br \/>\nAssert.Throws<InvalidOperationException>(() =&gt; Context.Customers.First(x =&gt; x.Id == 10));<br \/>\n}<\/p>\n<p>\u25a0Note  Assert.Throws() is a special type of assert statement. it is expecting an exception to the thrown by the code in the expression. if an exception doesn\u2019t get thrown, the assertion fails.<\/p>\n<p>When using FirstOrDefault(), instead of an exception, the result is a null record when no data is returned. This test shows creating an expression variable:<\/p>\n<p>[Fact]<br \/>\npublic void FirstOrDefaultShouldReturnDefaultIfNoneMatch()<br \/>\n{<br \/>\n\/\/Expression&lt;Func<Customer>&gt; is a lambda expression Expression&lt;Func&lt;Customer, bool&gt;&gt; expression = x =&gt; x.Id == 10;<br \/>\n\/\/Returns null when nothing is found<br \/>\nvar customer = Context.Customers.FirstOrDefault(expression);<br \/>\nAssert.Null(customer);<br \/>\n}<\/p>\n<p>The preceding LINQ query gets translated into the same as the previous:<\/p>\n<p>SELECT TOP(1) .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [Dbo].[Customers] AS <br \/>\nWHERE .[Id] = 10<\/p>\n<p>Using Last<br \/>\nWhen using the parameterless form of Last() and LastOrDefault(), the last record (based on any preceding ordering clauses) will be returned. As a reminder, EF Core will throw an exception if no sort is specified. The following test gets the last record based on \u201clast name, first name\u201d order:<\/p>\n<p>[Fact]<br \/>\npublic void GetLastMatchingRecordNameOrder()<br \/>\n{<br \/>\n\/\/Gets the last record, lastname desc, first name desc order var customer = Context.Customers<br \/>\n.OrderBy(x =&gt; x.PersonInformation.LastName)<br \/>\n.ThenBy(x =&gt; x.PersonInformation.FirstName)<br \/>\n.Last();<br \/>\nAssert.Equal(4, customer.Id);<br \/>\n}<\/p>\n<p>EF Core reverses the order by statements and then takes top(1) to get the result. Here is the executed query:<\/p>\n<p>SELECT TOP(1) .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [Dbo].[Customers] AS <br \/>\nORDER BY .[LastName] DESC, .[FirstName] DESC<\/p>\n<p>This test confirms that EF Core throws an exception when Last() is used without an<br \/>\nOrderBy()\/OrderByDescending():<\/p>\n<p>[Fact]<br \/>\npublic void LastShouldThrowIfNoSortSpecified()<br \/>\n{<br \/>\nAssert.Throws<InvalidOperationException>(() =&gt; Context.Customers.Last());<br \/>\n}<\/p>\n<p>Using Single<br \/>\nConceptually, Single()\/SingleOrDefault() works the same as First()\/FirstOrDefault(). The main difference is that Single()\/SingleOrDefault() returns Top(2) instead of Top(1) and throws an exception if two records are returned from the database. The following test retrieves the single record where Id == 1:<\/p>\n<p>[Fact]<br \/>\npublic void GetOneMatchingRecordWithSingle()<br \/>\n{<br \/>\n\/\/Gets the first record, database order<br \/>\nvar customer = Context.Customers.Single(x =&gt; x.Id == 1);<br \/>\nAssert.Equal(1, customer.Id);<br \/>\n}<\/p>\n<p>The preceding LINQ query gets translated into the following:<\/p>\n<p>SELECT TOP(2) .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [Dbo].[Customers] AS <br \/>\nWHERE .[Id] = 1<\/p>\n<p>Single() throws an exception if no records are returned.[Fact] public void SingleShouldThrowExceptionIfNoneMatch()<br \/>\n{<br \/>\n\/\/Filters based on Id. Throws due to no match<br \/>\nAssert.Throws<InvalidOperationException>(() =&gt; Context.Customers.Single(x =&gt; x.Id == 10));<br \/>\n}<\/p>\n<p>When using Single() or SingleOrDefault() and more than one record is returned, an exception is thrown.<\/p>\n<p>[Fact]<br \/>\npublic void SingleShouldThrowExceptionIfMoreThenOneMatch()<br \/>\n{<br \/>\n\/\/ Throws due to more than one match<br \/>\nAssert.Throws<InvalidOperationException>(() =&gt; Context.Customers.Single());<br \/>\n}<br \/>\n[Fact]<br \/>\npublic void SingleOrDefaultShouldThrowExceptionIfMoreThenOneMatch()<br \/>\n{<br \/>\n\/\/ Throws due to more than one match<br \/>\nAssert.Throws<InvalidOperationException>(() =&gt; Context.Customers.SingleOrDefault());<br \/>\n}<\/p>\n<p>When using SingleOrDefault(), instead of an exception, the result is a null record when no data is returned.<\/p>\n<p>[Fact]<br \/>\npublic void SingleOrDefaultShouldReturnDefaultIfNoneMatch()<br \/>\n{<br \/>\n\/\/Expression&lt;Func<Customer>&gt; is a lambda expression Expression&lt;Func&lt;Customer, bool&gt;&gt; expression = x =&gt; x.Id == 10;<br \/>\n\/\/Returns null when nothing is found<br \/>\nvar customer = Context.Customers.SingleOrDefault(expression);<br \/>\nAssert.Null(customer);<br \/>\n}<\/p>\n<p>The preceding LINQ query gets translated into the following:<br \/>\nSELECT TOP(2) .[Id], .[TimeStamp], .[FirstName], .[FullName], .[LastName] FROM [Dbo].[Customers] AS <br \/>\nWHERE .[Id] = 10<\/p>\n<p>Global Query Filters<br \/>\nRecall that there is a global query filter on the Car entity to filter out any cars where IsDrivable is false. Open the CarTests.cs class and add the following test that gets all the records that pass the query filter:<\/p>\n<p>[Fact]<br \/>\npublic void ShouldReturnDrivableCarsWithQueryFilterSet()<br \/>\n{<br \/>\nIQueryable<Car> query = Context.Cars; var qs = query.ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;); var cars = query.ToList(); Assert.NotEmpty(cars);<br \/>\nAssert.Equal(9, cars.Count);<br \/>\n}<\/p>\n<p>Recall that we create 10 cars in the data initialization process, and one of them is set to be nondrivable.<br \/>\nWhen the query is executed, the global query filter is applied, and the following SQL is executed:<\/p>\n<p>SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable],<br \/>\n[i].[MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp] FROM [dbo].[Inventory] AS [i]<br \/>\nWHERE [i].[IsDrivable] = CAST(1 AS bit)<\/p>\n<p>Disable the Query Filters<br \/>\nTo disable global query filters for the entities in a query, add the IgnoreQueryFilters() method to the LINQ query. If there is more than one entity with a global query filter and some of the entities\u2019 filters are required, they must be added to the LINQ statement\u2019s Where() methods. Add the following test to the CarTests.cs class, which disables the query filter and returns all records:<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetAllOfTheCars()<br \/>\n{<br \/>\nIQueryable<Car> query = Context.Cars.IgnoreQueryFilters(); var qs = query.ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;);<br \/>\nvar cars = query.ToList(); Assert.Equal(10, cars.Count);<br \/>\n}<\/p>\n<p>As one would expect, the where clause eliminating nondrivable cars is no longer on the generated SQL.<\/p>\n<p>SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable], [i]. [MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp]<br \/>\nFROM [dbo].[Inventory] AS [i]<\/p>\n<p>Query Filters on Navigation Properties<br \/>\nIn addition to the global query filter on the Car entity, we added a query filter to the CarNavigation property of the Order entity. To see this in action, add the following test to the OrderTests.cs class:<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetAllOrdersExceptFiltered()<br \/>\n{<br \/>\nvar query = Context.Orders.AsQueryable(); var qs = query.ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;); var orders = query.ToList(); Assert.NotEmpty(orders); Assert.Equal(4,orders.Count);<br \/>\n}<\/p>\n<p>The generated SQL is listed here:<br \/>\nSELECT [o].[Id], [o].[CarId], [o].[CustomerId], [o].[TimeStamp] FROM [dbo].[Orders] AS [o]<br \/>\nINNER JOIN (<br \/>\nSELECT [i].[Id], [i].[IsDrivable] FROM [dbo].[Inventory] AS [i]<br \/>\nWHERE [i].[IsDrivable] = CAST(1 AS bit)<br \/>\n) AS [t] ON [o].[CarId] = [t].[Id]<br \/>\nWHERE [t].[IsDrivable] = CAST(1 AS bit)<\/p>\n<p>Because the CarNavigation navigation property is a required navigation property, the query translation engine uses an INNER JOIN, eliminating the Order records where the Car is nondrivable.<\/p>\n<p>To return all records, add IgnoreQueryFilters() to your LINQ query.[Fact] public void ShouldGetAllOrders()<br \/>\n{<br \/>\nvar query = Context.Orders.IgnoreQueryFilters(); var qs = query.ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;);<\/p>\n<p>var orders = query.ToList(); Assert.NotEmpty(orders); Assert.Equal(5, orders.Count);<br \/>\n}<\/p>\n<p>You can see from the generated SQL that the where clause has been removed, and the query has been simplified:<\/p>\n<p>SELECT [o].[Id], [o].[CarId], [o].[CustomerId], [o].[TimeStamp] FROM [dbo].[Orders] AS [o]<\/p>\n<p>Load Related Data Eagerly<br \/>\nEntities that are linked through navigation properties can be instantiated in one query using eager loading. The Include() method indicates a join to the related entity, and the ThenInclude() method is used for subsequent joins to other entities. Both of these methods will be demonstrated in these tests. When the Include()\/ThenInclude() methods are translated into SQL, required relationships use an inner join, and optional relationships use a left join.<br \/>\nAdd the following test to the CarTests.cs class to show a single Include(): [Fact]<br \/>\npublic void ShouldGetAllOfTheCarsWithMakes()<br \/>\n{<br \/>\nIIncludableQueryable&lt;Car, Make&gt; query = Context.Cars.Include(c =&gt; c.MakeNavigation); var qs = query.ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;);<br \/>\nvar cars = query.ToList(); Assert.Equal(9, cars.Count);<br \/>\n}<\/p>\n<p>The query adds the MakeNavigation property to the results, performing an inner join with the following SQL being executed. The query returns all the columns from both tables, and then EF Core created Car and Make instances from the returned data. Notice the global query filter is in effect.<\/p>\n<p>SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable],<br \/>\n[i].[MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp], [m].[Id], [m].[Name], [m].[TimeStamp]<br \/>\nFROM [dbo].[Inventory] AS [i]<br \/>\nINNER JOIN [dbo].[Makes] AS [m] ON [i].[MakeId] = [m].[Id] WHERE [i].[IsDrivable] = CAST(1 AS bit)<\/p>\n<p>The next test demonstrates using two sets of related data. The first is getting the Make information (same as the previous test), while the second is getting the Orders and then the Customers attached to the Orders. The entire test is also filtering out the Car records that don\u2019t have any orders.<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetCarsOnOrderWithRelatedProperties()<br \/>\n{<\/p>\n<p>IIncludableQueryable&lt;Car, Customer?&gt; query = Context.Cars<br \/>\n.Where(c =&gt; c.Orders.Any())<br \/>\n.Include(c =&gt; c.MakeNavigation)<br \/>\n.Include(c =&gt; c.Orders).ThenInclude(o =&gt; o.CustomerNavigation);<br \/>\nvar qs = query.ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;); var cars = query.ToList(); Assert.Equal(4, cars.Count); cars.ForEach(c =&gt;<br \/>\n{<br \/>\nAssert.NotNull(c.MakeNavigation); Assert.NotNull(c.Orders.ToList()[0].CustomerNavigation);<br \/>\n});<br \/>\n}<\/p>\n<p>The generated query is rather lengthy. Here is the generated query:<\/p>\n<p>SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable],<br \/>\n[i].[MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp], [m].[Id],<br \/>\n[m].[Name], [m].[TimeStamp], [t0].[Id], [t0].[CarId], [t0].[CustomerId],<br \/>\n[t0].[TimeStamp], [t0].[Id0], [t0].[TimeStamp0], [t0].[FirstName],<br \/>\n[t0].[FullName], [t0].[LastName], [t0].[Id1] FROM [dbo].[Inventory] AS [i]<br \/>\nINNER JOIN [dbo].[Makes] AS [m] ON [i].[MakeId] = [m].[Id] LEFT JOIN (<br \/>\nSELECT [o0].[Id], [o0].[CarId], [o0].[CustomerId], [o0].[TimeStamp],<br \/>\n.[Id] AS [Id0], .[TimeStamp] AS [TimeStamp0], .[FirstName],<br \/>\n.[FullName], .[LastName], [t1].[Id] AS [Id1] FROM [dbo].[Orders] AS [o0]<br \/>\nINNER JOIN (<br \/>\nSELECT [i1].[Id], [i1].[IsDrivable] FROM [dbo].[Inventory] AS [i1]<br \/>\nWHERE [i1].[IsDrivable] = CAST(1 AS bit)<br \/>\n) AS [t1] ON [o0].[CarId] = [t1].[Id]<br \/>\nINNER JOIN [dbo].[Customers] AS  ON [o0].[CustomerId] = .[Id] WHERE [t1].[IsDrivable] = CAST(1 AS bit)<br \/>\n) AS [t0] ON [i].[Id] = [t0].[CarId]<br \/>\nWHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND EXISTS ( SELECT 1<br \/>\nFROM [dbo].[Orders] AS [o] INNER JOIN (<br \/>\nSELECT [i0].[Id], [i0].[Color], [i0].[DateBuilt], [i0].[Display],<br \/>\n[i0].[IsDrivable], [i0].[MakeId], [i0].[PetName], [i0].[Price], [i0].[TimeStamp] FROM [dbo].[Inventory] AS [i0]<br \/>\nWHERE [i0].[IsDrivable] = CAST(1 AS bit)<br \/>\n) AS [t] ON [o].[CarId] = [t].[Id]<br \/>\nWHERE ([t].[IsDrivable] = CAST(1 AS bit)) AND ([i].[Id] = [o].[CarId]))<br \/>\nORDER BY [i].[Id], [m].[Id], [t0].[Id], [t0].[Id1], [t0].[Id0]<\/p>\n<p>If you run the same query without the query filters, the query becomes much simpler. Here is the updated test that removes the query filters:<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetCarsOnOrderWithRelatedPropertiesIgnoreFilters()<br \/>\n{<br \/>\nIIncludableQueryable&lt;Car, Customer&gt; query = Context.Cars.IgnoreQueryFilters().Where(c =&gt; c.Orders.Any())<br \/>\n.Include(c =&gt; c.MakeNavigation)<br \/>\n.Include(c =&gt; c.Orders).ThenInclude(o =&gt; o.CustomerNavigati on) ;<br \/>\nvar qs = query.ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;); var cars = query.ToList(); Assert.Equal(5, cars.Count); cars.ForEach(c =&gt;<br \/>\n{<br \/>\nAssert.NotNull(c.MakeNavigation); Assert.NotNull(c.Orders.ToList()[0].CustomerNavigation);<br \/>\n});<br \/>\n}<\/p>\n<p>The generated query is rather lengthy. Here is the generated query:<\/p>\n<p>SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable], [i].<br \/>\n[MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp], [m].[Id], [m].[Name], [m].<br \/>\n[TimeStamp], [t].[Id], [t].[CarId], [t].[CustomerId], [t].[TimeStamp], [t].[Id0], [t]. [TimeStamp0], [t].[FirstName], [t].[FullName], [t].[LastName]<br \/>\nFROM [dbo].[Inventory] AS [i]<br \/>\nINNER JOIN [dbo].[Makes] AS [m] ON [i].[MakeId] = [m].[Id] LEFT JOIN (<br \/>\nSELECT [o0].[Id], [o0].[CarId], [o0].[CustomerId], [o0].[TimeStamp], .[Id] AS<br \/>\n[Id0], .[TimeStamp] AS [TimeStamp0], .[FirstName], .[FullName], .[LastName] FROM [dbo].[Orders] AS [o0]<br \/>\nINNER JOIN [dbo].[Customers] AS  ON [o0].[CustomerId] = .[Id]<br \/>\n) AS [t] ON [i].[Id] = [t].[CarId] WHERE EXISTS (<br \/>\nSELECT 1<br \/>\nFROM [dbo].[Orders] AS [o] WHERE [i].[Id] = [o].[CarId])<br \/>\nORDER BY [i].[Id], [m].[Id], [t].[Id], [t].[Id0]<\/p>\n<p>Splitting Queries on Related Data<br \/>\nThe more joins added into a LINQ query, the more complex the resulting query becomes. As the previous examples demonstrated, query filters can make the queries even more complex. EF Core 5 introduced the ability to run complicated joins as split queries by adding the AsSplitQuery() method into the LINQ query. As discussed in the previous chapters, this can gain efficiency at the risk of data inconsistency. The following test demonstrates the same query just exercised, but as a split query:<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetCarsOnOrderWithRelatedPropertiesAsSplitQuery()<br \/>\n{<br \/>\nIQueryable<Car> query = Context.Cars.Where(c =&gt; c.Orders.Any())<\/p>\n<p>.Include(c =&gt; c.MakeNavigation)<br \/>\n.Include(c =&gt; c.Orders).ThenInclude(o =&gt; o.CustomerNavigation)<br \/>\n.AsSplitQuery();<br \/>\nvar qs = query.ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;); var cars = query.ToList(); Assert.Equal(4, cars.Count); cars.ForEach(c =&gt;<br \/>\n{<br \/>\nAssert.NotNull(c.MakeNavigation); Assert.NotNull(c.Orders.ToList()[0].CustomerNavigation);<br \/>\n});<br \/>\n}<\/p>\n<p>The ToQueryString() method returns only the first query, so the following queries were captured using SQL Server Profiler:<\/p>\n<p>SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable], [i]. [MakeId],<br \/>\n[i].[PetName], [i].[Price], [i].[TimeStamp], [m].[Id], [m].[Name], [m].[TimeStamp] FROM [dbo].[Inventory] AS [i]<br \/>\nINNER JOIN [dbo].[Makes] AS [m] ON [i].[MakeId] = [m].[Id] WHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND EXISTS (<br \/>\nSELECT 1<br \/>\nFROM [dbo].[Orders] AS [o] INNER JOIN (<br \/>\nSELECT [i0].[Id], [i0].[Color], [i0].[DateBuilt], [i0].[Display], [i0].[IsDrivable],<br \/>\n[i0].[MakeId], [i0].[PetName], [i0].[Price], [i0].[TimeStamp] FROM [dbo].[Inventory] AS [i0]<br \/>\nWHERE [i0].[IsDrivable] = CAST(1 AS bit)<br \/>\n) AS [t] ON [o].[CarId] = [t].[Id]<br \/>\nWHERE ([t].[IsDrivable] = CAST(1 AS bit)) AND ([i].[Id] = [o].[CarId])) ORDER BY [i].[Id], [m].[Id]<\/p>\n<p>SELECT [t0].[Id], [t0].[CarId], [t0].[CustomerId], [t0].[TimeStamp], [t0].[Id0],<br \/>\n[t0].[TimeStamp0], [t0].[FirstName], [t0].[FullName], [t0].[LastName], [i].[Id], [m].[Id]<br \/>\nFROM [dbo].[Inventory] AS [i]<br \/>\nINNER JOIN [dbo].[Makes] AS [m] ON [i].[MakeId] = [m].[Id] INNER JOIN (<br \/>\nSELECT [o0].[Id], [o0].[CarId], [o0].[CustomerId], [o0].[TimeStamp],<br \/>\n.[Id] AS [Id0], .[TimeStamp] AS [TimeStamp0], .[FirstName], .[FullName], .[LastName]<br \/>\nFROM [dbo].[Orders] AS [o0] INNER JOIN (<br \/>\nSELECT [i1].[Id], [i1].[IsDrivable] FROM [dbo].[Inventory] AS [i1]<br \/>\nWHERE [i1].[IsDrivable] = CAST(1 AS bit)<br \/>\n) AS [t1] ON [o0].[CarId] = [t1].[Id]<br \/>\nINNER JOIN [dbo].[Customers] AS  ON [o0].[CustomerId] = .[Id] WHERE [t1].[IsDrivable] = CAST(1 AS bit)<br \/>\n) AS [t0] ON [i].[Id] = [t0].[CarId]<\/p>\n<p>WHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND EXISTS ( SELECT 1<br \/>\nFROM [dbo].[Orders] AS [o] INNER JOIN (<br \/>\nSELECT [i0].[Id], [i0].[Color], [i0].[DateBuilt], [i0].[Display], [i0].[IsDrivable],<br \/>\n[i0].[MakeId], [i0].[PetName], [i0].[Price], [i0].[TimeStamp] FROM [dbo].[Inventory] AS [i0]<br \/>\nWHERE [i0].[IsDrivable] = CAST(1 AS bit)<br \/>\n) AS [t] ON [o].[CarId] = [t].[Id]<br \/>\nWHERE ([t].[IsDrivable] = CAST(1 AS bit)) AND ([i].[Id] = [o].[CarId])) ORDER BY [i].[Id], [m].[Id]<\/p>\n<p>Once again, removing the query filters greatly simplifies the queries generated. Here is the updated test:<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetCarsOnOrderWithRelatedPropertiesAsSplitQueryIgnoreQueryFilters()<br \/>\n{<br \/>\nIQueryable<Car> query = Context.Cars.IgnoreQueryFilters()<br \/>\n.Where(c =&gt; c.Orders.Any())<br \/>\n.Include(c =&gt; c.MakeNavigation)<br \/>\n.Include(c =&gt; c.Orders).ThenInclude(o =&gt; o.CustomerNavigation)<br \/>\n.AsSplitQuery(); var qs = query.ToQueryString();<br \/>\nOutputHelper.WriteLine($&quot;Query: {qs}&quot;); var cars = query.ToList(); Assert.Equal(5, cars.Count); cars.ForEach(c =&gt;<br \/>\n{<br \/>\nAssert.NotNull(c.MakeNavigation); Assert.NotNull(c.Orders.ToList()[0].CustomerNavigation);<br \/>\n});<br \/>\n}<\/p>\n<p>Here are the generated queries:<\/p>\n<p>SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable],<br \/>\n[i].[MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp], [m].[Id], [m].[Name], [m]. [TimeStamp]<br \/>\nFROM [dbo].[Inventory] AS [i]<br \/>\nINNER JOIN [dbo].[Makes] AS [m] ON [i].[MakeId] = [m].[Id] WHERE EXISTS (<br \/>\nSELECT 1<br \/>\nFROM [dbo].[Orders] AS [o] WHERE [i].[Id] = [o].[CarId])<br \/>\nORDER BY [i].[Id], [m].[Id]<\/p>\n<p>SELECT [t].[Id], [t].[CarId], [t].[CustomerId], [t].[TimeStamp], [t].[Id0],<br \/>\n[t].[TimeStamp0], [t].[FirstName], [t].[FullName], [t].[LastName], [i].[Id], [m].[Id] FROM [dbo].[Inventory] AS [i]<br \/>\nINNER JOIN [dbo].[Makes] AS [m] ON [i].[MakeId] = [m].[Id]<\/p>\n<p>INNER JOIN (<br \/>\nSELECT [o0].[Id], [o0].[CarId], [o0].[CustomerId], [o0].[TimeStamp],<br \/>\n.[Id] AS [Id0], .[TimeStamp] AS [TimeStamp0], .[FirstName], .[FullName], .[LastName]<br \/>\nFROM [dbo].[Orders] AS [o0]<br \/>\nINNER JOIN [dbo].[Customers] AS  ON [o0].[CustomerId] = .[Id]<br \/>\n) AS [t] ON [i].[Id] = [t].[CarId] WHERE EXISTS (<br \/>\nSELECT 1<br \/>\nFROM [dbo].[Orders] AS [o] WHERE [i].[Id] = [o].[CarId])<br \/>\nORDER BY [i].[Id], [m].[Id]<\/p>\n<p>Filtering Related Data<br \/>\nEF Core 5 introduces the ability to filter when including collection properties. Prior to EF Core 5, the only way to get a filtered list for a collection navigation property was to use explicit loading. Add the following test into the MakeTests.cs class, which demonstrates getting all the Make records and those cars that are yellow:<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetAllMakesAndCarsThatAreYellow()<br \/>\n{<br \/>\nvar query = Context.Makes.IgnoreQueryFilters()<br \/>\n.Include(x =&gt; x.Cars.Where(x =&gt; x.Color == &quot;Yellow&quot;)); var qs = query.ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;);<br \/>\nvar makes = query.ToList(); Assert.NotNull(makes); Assert.NotEmpty(makes);<br \/>\nAssert.NotEmpty(makes.Where(x =&gt; x.Cars.Any())); Assert.Empty(makes.First(m =&gt; m.Id == 1).Cars); Assert.Empty(makes.First(m =&gt; m.Id == 2).Cars); Assert.Empty(makes.First(m =&gt; m.Id == 3).Cars); Assert.Single(makes.First(m =&gt; m.Id == 4).Cars); Assert.Empty(makes.First(m =&gt; m.Id == 5).Cars);<br \/>\n}<\/p>\n<p>The generated SQL is as follows:<\/p>\n<p>SELECT [m].[Id], [m].[Name], [m].[TimeStamp], [t].[Id], [t].[Color],<br \/>\n[t].[DateBuilt], [t].[Display], [t].[IsDrivable], [t].[MakeId],<br \/>\n[t].[PetName], [t].[Price], [t].[TimeStamp] FROM [dbo].[Makes] AS [m]<br \/>\nLEFT JOIN (<br \/>\nSELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable],<br \/>\n[i].[MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp] FROM [dbo].[Inventory] AS [i]<br \/>\nWHERE [i].[Color] = N'Yellow'<br \/>\n) AS [t] ON [m].[Id] = [t].[MakeId]<br \/>\nORDER BY [m].[Id], [t].[Id]<\/p>\n<p>Changing the query to a split query yields this SQL (collection from SQL Server Profiler): SELECT [m].[Id], [m].[Name], [m].[TimeStamp]<br \/>\nFROM [dbo].[Makes] AS [m]<br \/>\nORDER BY [m].[Id]<\/p>\n<p>SELECT [t].[Id], [t].[Color], [t].[DateBuilt], [t].[Display], [t].[IsDrivable],<br \/>\n[t].[MakeId], [t].[PetName], [t].[Price], [t].[TimeStamp], [m].[Id] FROM [dbo].[Makes] AS [m]<br \/>\nINNER JOIN (<br \/>\nSELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable],<br \/>\n[i].[MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp] FROM [dbo].[Inventory] AS [i]<br \/>\nWHERE [i].[Color] = N'Yellow'<br \/>\n) AS [t] ON [m].[Id] = [t].[MakeId] ORDER BY [m].[Id]<\/p>\n<p>Load Related Data Explicitly<br \/>\nIf the related data needs to be loaded after the principal entity was queried into memory, the related entities can be retrieved from the database with subsequent database calls. This is triggered using the Entry() method on the derived DbContext. When loading entities on the many end of a one-to-many relationship, use the Collection() method on the Entry result. To load entities on the one end of a one-to-many (or<br \/>\nin a one-to-one relationship), use the Reference() method. Calling Query() on the Collection() or Reference() method returns an IQueryable<T> that can be used to get the query string (as shown in the following tests) and to manage query filters (as shown in the next section). To execute the query and load the record(s), call the Load() method on the Collection(), Reference(), or Query() method. Query execution happens immediately when Load() is called.<br \/>\nThe following test (back in the CarTests.cs class) shows how to load a reference navigation property on the Car entity:<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetReferenceRelatedInformationExplicitly()<br \/>\n{<br \/>\nvar car = Context.Cars.First(x =&gt; x.Id == 1); Assert.Null(car.MakeNavigation);<br \/>\nvar query = Context.Entry(car).Reference(c =&gt; c.MakeNavigation).Query(); var qs = query.ToQueryString();<br \/>\nOutputHelper.WriteLine($&quot;Query: {qs}&quot;); query.Load(); Assert.NotNull(car.MakeNavigation);<br \/>\n}<\/p>\n<p>The generated SQL to get the Make information is as follows (the Car record was already queried):<\/p>\n<p>DECLARE @ p_0 int = 1;<\/p>\n<p>SELECT [m].[Id], [m].[Name], [m].[TimeStamp] FROM [dbo].[Makes] AS [m]<br \/>\nWHERE [m].[Id] = @ p_0<\/p>\n<p>This test shows how to load a collection navigation property on the Car entity:<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetCollectionRelatedInformationExplicitly()<br \/>\n{<br \/>\nvar car = Context.Cars.First(x =&gt; x.Id == 1); Assert.Empty(car.Orders);<br \/>\nvar query = Context.Entry(car).Collection(c =&gt; c.Orders).Query(); var qs = query.ToQueryString();<br \/>\nOutputHelper.WriteLine($&quot;Query: {qs}&quot;); query.Load(); Assert.Single(car.Orders);<br \/>\n}<\/p>\n<p>The generated SQL is as follows:<\/p>\n<p>DECLARE @ p_0 int = 1;<\/p>\n<p>SELECT [o].[Id], [o].[CarId], [o].[CustomerId], [o].[TimeStamp] FROM [dbo].[Orders] AS [o]<br \/>\nINNER JOIN (<br \/>\nSELECT [i].[Id], [i].[IsDrivable] FROM [dbo].[Inventory] AS [i]<br \/>\nWHERE [i].[IsDrivable] = CAST(1 AS bit)<br \/>\n) AS [t] ON [o].[CarId] = [t].[Id]<br \/>\nWHERE ([t].[IsDrivable] = CAST(1 AS bit)) AND ([o].[CarId] = @ p_0)<\/p>\n<p>Load Related Data Explicitly with Query Filters<br \/>\nIn addition to shaping queries generated when eagerly loading related data, global query filters are active when explicitly loading related data. Take the following test (in the MakeTests.cs class):<\/p>\n<p>[Theory] [InlineData(1,1)] [InlineData(2,1)] [InlineData(3,1)] [InlineData(4,2)] [InlineData(5,3)] [InlineData(6,1)]<br \/>\npublic void ShouldGetAllCarsForAMakeExplicitlyWithQueryFilters(int makeId, int carCount)<br \/>\n{<br \/>\nvar make = Context.Makes.First(x =&gt; x.Id == makeId);<br \/>\nIQueryable<Car> query = Context.Entry(make).Collection(c =&gt; c.Cars).Query(); var qs = query.ToQueryString();<br \/>\nOutputHelper.WriteLine($&quot;Query: {qs}&quot;); query.Load(); Assert.Equal(carCount,make.Cars.Count());<br \/>\n}<\/p>\n<p>This test is similar to ShouldGetTheCarsByMake() from the \u201cFilter Records\u201d section. However, instead of just getting the Car records that have a certain MakeId, the test first gets a Make record and then explicitly loads the Car records for the already retrieved Make record. The generated query is shown here:<\/p>\n<p>DECLARE @ p_0 int = 1;<\/p>\n<p>SELECT [i].[Id], [i].[Color], [i].[DateBuilt], [i].[Display], [i].[IsDrivable],<br \/>\n[i].[MakeId], [i].[PetName], [i].[Price], [i].[TimeStamp] FROM [dbo].[Inventory] AS [i]<br \/>\nWHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND ([i].[MakeId] = @ p_0)<\/p>\n<p>Notice that the query filter is still being used, even though the principal entity in the query is the Make record. To turn off query filters when explicitly loading records, call IgnoreQueryFilters() in conjunction with the Query() method. Here is the test that turns off query filters (again, in the MakeTests.cs class):<\/p>\n<p>[Theory] [InlineData(1, 2)]<br \/>\n[InlineData(2, 1)]<br \/>\n[InlineData(3, 1)]<br \/>\n[InlineData(4, 2)]<br \/>\n[InlineData(5, 3)]<br \/>\n[InlineData(6, 1)]<br \/>\npublic void ShouldGetAllCarsForAMakeExplicitly(int makeId, int carCount)<br \/>\n{<br \/>\nvar make = Context.Makes.First(x =&gt; x.Id == makeId); IQueryable<Car> query =<br \/>\nContext.Entry(make).Collection(c =&gt; c.Cars).Query().IgnoreQueryFilters(); var qs = query.IgnoreQueryFilters().ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;);<br \/>\nquery.Load();<br \/>\nAssert.Equal(carCount, make.Cars.Count());<br \/>\n}<\/p>\n<p>Temporal Queries<br \/>\nThis section exercises EF Core\u2019s ability to retrieve historical data from temporal tables. As a reminder, the repos that derive from the TemporalTableBaseRepo contain the methods to query temporal tables using one of the five temporal query operators. To demonstrate this, open the MakeTests class, and add the following test at the bottom of the file:<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetAllHistoryRows()<br \/>\n{<br \/>\nvar make = new Make { Name = &quot;TestMake&quot; };<br \/>\n_repo.Add(make); Thread.Sleep(2000); make.Name = &quot;Updated Name&quot;;<br \/>\n_repo.Update(make);<br \/>\nThread.Sleep(2000);<br \/>\n_repo.Delete(make);<\/p>\n<p>var list = _repo.GetAllHistory().Where(x =&gt; x.Entity.Id == make.Id).ToList(); Assert.Equal(2, list.Count);<br \/>\nAssert.Equal(&quot;TestMake&quot;, list[0].Entity.Name); Assert.Equal(&quot;Updated Name&quot;, list[1].Entity.Name); Assert.Equal(list[0].ValidTo, list[1].ValidFrom);<br \/>\n}<\/p>\n<p>The test creates a new Make record and adds it to the database. After pausing operation for two seconds, the Name is updated, and the change saved. After another two-second pause, the record is deleted. The test then uses the MakeRepo to get all the history for the Make record, confirms that there are two records in the history, makes sure the records are retrieved in ValidFrom order, and makes sure that the ValidTo of the first record exactly matches the ValidFrom of the second record.<\/p>\n<p>SQL Queries with LINQ<br \/>\nThis section exercises EF Core\u2019s ability to retrieve data using raw SQL queries using the DbSet<T>\u2019s FromSqlRaw() or FromSqlInterpolated() methods. The first test (in the CarTests.cs class) uses a raw SQL query to get all the records from the Inventory table. Notice that the query must use the database names and not the entity names as well as add in the timestamp columns for the temporal functionality:<\/p>\n<p>[Fact]<br \/>\npublic void ShouldNotGetTheLemonsUsingFromSql()<br \/>\n{<br \/>\nvar entity = Context.Model.FindEntityType($&quot;{typeof(Car).FullName}&quot;); var tableName = entity.GetTableName();<br \/>\nvar schemaName = entity.GetSchema(); var query = Context.Cars<br \/>\n.FromSqlRaw($&quot;Select *,ValidFrom,ValidTo from {schemaName}.{tableName}&quot;); var qs = query.ToQueryString();<br \/>\nOutputHelper.WriteLine($&quot;Query: {qs}&quot;); var cars = query.ToList(); Assert.Equal(9, cars.Count);<br \/>\n}<\/p>\n<p>When using raw SQL queries, the query gets wrapped into a larger query by EF Core to support the query filter. If the statement was terminated with a semicolon, the query would not be executable on SQL Server.<\/p>\n<p>SELECT [a].[Id], [a].[Color], [a].[DateBuilt], [a].[Display], [a].[IsDrivable], [a].<br \/>\n[MakeId], [a].[PetName], [a].[Price], [a].[TimeStamp], [a].[ValidFrom], [a].[ValidTo] FROM (<br \/>\nSelect *,ValidFrom,ValidTo from dbo.Inventory<br \/>\n) AS [a]<br \/>\nWHERE [a].[IsDrivable] = CAST(1 AS bit)<\/p>\n<p>When the query filter is removed, the generated SQL becomes the same SQL as the string passed into the FromSqlRaw() method:<\/p>\n<p>Select *,ValidFrom,ValidTo from dbo.Inventory<\/p>\n<p>The following test demonstrates using FromSqlInterpolated() with additional LINQ statements (including the MakeNavigation):<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetOneCarUsingInterpolation()<br \/>\n{<br \/>\nvar carId = 1;<br \/>\nvar query = Context.Cars<br \/>\n.FromSqlInterpolated($&quot;Select *,ValidFrom,ValidTo from dbo.Inventory where Id =<br \/>\n{carId}&quot;)<br \/>\n.Include(x =&gt; x.MakeNavigation);<br \/>\nvar qs = query.ToQueryString(); OutputHelper.WriteLine($&quot;Query: {qs}&quot;); var car = query.First(); Assert.Equal(&quot;Black&quot;, car.Color);<br \/>\nAssert.Equal(&quot;VW&quot;, car.MakeNavigation.Name);<br \/>\n}<\/p>\n<p>Here is the generated SQL:<\/p>\n<p>DECLARE p0 int = 1;<\/p>\n<p>SELECT [a].[Id], [a].[Color], [a].[DateBuilt], [a].[Display], [a].[IsDrivable], [a].[MakeId],<br \/>\n[a].[PetName], [a].[Price], [a].[TimeStamp], [a].[ValidFrom], [a].[ValidTo],<br \/>\n[m].[Id], [m].[Name], [m].[TimeStamp], [m].[ValidFrom], [m].[ValidTo] FROM (<br \/>\nSelect *,ValidFrom,ValidTo from dbo.Inventory where Id = @p0<br \/>\n) AS [a]<br \/>\nINNER JOIN [dbo].[Makes] AS [m] ON [a].[MakeId] = [m].[Id] WHERE [a].[IsDrivable] = CAST(1 AS bit)<\/p>\n<p>Aggregate Methods<br \/>\nThe next set of tests demonstrate the server-side aggregate methods (Max(), Min(), Count(), Average(), etc.). Aggregate methods can be added to the end of a LINQ query with Where() methods, or the filter expression can be contained in the aggregate method itself (just like First() and Single()). The aggregation executes on the server side, and the single value is returned from the query. Global query filters affect aggregate methods as well and can be disabled with IgnoreQueryFilters().<br \/>\nAll the SQL statements shown in this section were collected using SQL Server Profiler.<br \/>\nThis first test (in CarTests.cs) simply counts all the Car records in the database. Since the query filter is still active, the count returns nine cars.<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetTheCountOfCars()<br \/>\n{<br \/>\nvar count = Context.Cars.Count(); Assert.Equal(9, count);<br \/>\n}<\/p>\n<p>The executed SQL is shown here:<\/p>\n<p>SELECT COUNT(*)<br \/>\nFROM [dbo].[Inventory] AS [i]<br \/>\nWHERE [i].[IsDrivable] = CAST(1 AS bit)<\/p>\n<p>By adding IgnoreQueryFilters(), the Count() method returns 10, and the where clause is removed from the SQL query.<\/p>\n<p>[Fact]<br \/>\npublic void ShouldGetTheCountOfCarsIgnoreQueryFilters()<br \/>\n{<br \/>\nvar count = Context.Cars.IgnoreQueryFilters().Count(); Assert.Equal(10, count);<br \/>\n}<\/p>\n<p>--Generated SQL<br \/>\nSELECT COUNT(*) FROM [dbo].[Inventory] AS [i]<\/p>\n<p>The following tests (also in CarTests.cs) demonstrate the Count() method with a where condition. The first test adds the expression directly into the Count() method, and the second adds the Count() method to the end of the LINQ statement.<\/p>\n<p>[Theory] [InlineData(1, 1)]<br \/>\n[InlineData(2, 1)]<br \/>\n[InlineData(3, 1)]<br \/>\n[InlineData(4, 2)]<br \/>\n[InlineData(5, 3)]<br \/>\n[InlineData(6, 1)]<br \/>\npublic void ShouldGetTheCountOfCarsByMakeP1(int makeId, int expectedCount)<br \/>\n{<br \/>\nvar count = Context.Cars.Count(x=&gt;x.MakeId == makeId); Assert.Equal(expectedCount, count);<br \/>\n}<\/p>\n<p>[Theory] [InlineData(1, 1)]<br \/>\n[InlineData(2, 1)]<br \/>\n[InlineData(3, 1)]<br \/>\n[InlineData(4, 2)]<br \/>\n[InlineData(5, 3)]<br \/>\n[InlineData(6, 1)]<br \/>\npublic void ShouldGetTheCountOfCarsByMakeP2(int makeId, int expectedCount)<br \/>\n{<br \/>\nvar count = Context.Cars.Where(x =&gt; x.MakeId == makeId).Count(); Assert.Equal(expectedCount, count);<br \/>\n}<\/p>\n<p>Both tests create the same SQL calls to the server, as shown here (the MakeId changes with each test based on the InlineData):<\/p>\n<p>exec sp_executesql N'SELECT COUNT(*) FROM [dbo].[Inventory] AS [i]<br \/>\nWHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND ([i].[MakeId] = @ makeId_0)'<br \/>\n,N'@ makeId_0 int',@ makeId_0=6<\/p>\n<p>Any() and All()<br \/>\nThe Any() and All() methods check a set of records to see whether any records match the criteria (Any()) or whether all records match the criteria (All()). Global query filters affect Any() and All() methods functions as well and can be disabled with IgnoreQueryFilters(). All the SQL statements shown in this section were collected using SQL Server Profiler.<br \/>\nThis first test (in CarTests.cs) checks if any car records have a specific MakeId:<\/p>\n<p>[Theory] [InlineData(1, true)]<br \/>\n[InlineData(11, false)]<br \/>\npublic void ShouldCheckForAnyCarsWithMake(int makeId, bool expectedResult)<br \/>\n{<br \/>\nvar result = Context.Cars.Any(x =&gt; x.MakeId == makeId); Assert.Equal(expectedResult, result);<br \/>\n}<\/p>\n<p>The executed SQL for the first theory test is shown here:<\/p>\n<p>exec sp_executesql N'SELECT CASE WHEN EXISTS (<br \/>\nSELECT 1<br \/>\nFROM [dbo].[Inventory] AS [i]<br \/>\nWHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND ([i].[MakeId] = @ makeId_0)) THEN CAST(1 AS bit)<br \/>\nELSE CAST(0 AS bit)<br \/>\nEND',N'@ makeId_0 int',@ makeId_0=1<br \/>\nThis second test checks if all car records have a specific MakeId: [Theory]<br \/>\n[InlineData(1, false)] [InlineData(11, false)]<br \/>\npublic void ShouldCheckForAllCarsWithMake(int makeId, bool expectedResult)<br \/>\n{<br \/>\nvar result = Context.Cars.All(x =&gt; x.MakeId == makeId); Assert.Equal(expectedResult, result);<br \/>\n}<\/p>\n<p>The executed SQL for the first theory test is shown here:<br \/>\nexec sp_executesql N'SELECT CASE WHEN NOT EXISTS (<br \/>\nSELECT 1<br \/>\nFROM [dbo].[Inventory] AS [i]<br \/>\nWHERE ([i].[IsDrivable] = CAST(1 AS bit)) AND ([i].[MakeId] &lt;&gt; @ makeId_0)) THEN CAST(1 AS bit)<br \/>\nELSE CAST(0 AS bit)<br \/>\nEND',N'@ makeId_0 int',@ makeId_0=1<\/p>\n<p>Getting Data from Stored Procedures<br \/>\nThe final test is to make sure that the CarRepo can get the PetName from the stored procedure. With this code in place, the test becomes trivial. Add the following test to the CarTests.cs class:<\/p>\n<p>[Theory]<br \/>\n[InlineData(1, &quot;Zippy&quot;)]<br \/>\n[InlineData(2, &quot;Rusty&quot;)]<br \/>\n[InlineData(3, &quot;Mel&quot;)] [InlineData(4, &quot;Clunker&quot;)] [InlineData(5, &quot;Bimmer&quot;)] [InlineData(6, &quot;Hank&quot;)]<br \/>\n[InlineData(7, &quot;Pinky&quot;)]<br \/>\n[InlineData(8, &quot;Pete&quot;)] [InlineData(9, &quot;Brownie&quot;)]<br \/>\npublic void ShouldGetValueFromStoredProc(int id, string expectedName)<br \/>\n{<br \/>\nAssert.Equal(expectedName, _carRepo.GetPetName(id));<br \/>\n}<\/p>\n<p>Creating Records<br \/>\nRecords are added to the database by creating them in code, adding them to their DbSet<T>, and calling S aveChanges()\/SaveChangesAsync() on the context. When SaveChanges() is executed, the ChangeTracker reports all the added entities, and EF Core (along with the database provider) creates the appropriate SQL statement(s) to insert the record(s).<br \/>\nAs a reminder, SaveChanges() executes in an implicit transaction, unless an explicit transaction is used.<br \/>\nIf the save was successful, the server-generated values are then queried to set the values on the entities. These tests will all use an explicit transaction so the changes can be rolled back, leaving the database in the same state as when the test execution began.<br \/>\nAll the SQL statements shown in this section were collected using SQL Server Profiler.<\/p>\n<p>\u25a0Note records can also be added using the derived DbContext as well. these examples will all use the DbSet<T> collection properties to add the records. Both DbSet<T> and DbContext have async versions of Add()\/AddRange(). only the synchronous versions are shown.<\/p>\n<p>Add a Single Record<br \/>\nThe following test demonstrates how to add a single record to the Inventory table:<\/p>\n<p>[Fact]<br \/>\npublic void ShouldAddACar()<br \/>\n{<br \/>\nExecuteInATransaction(RunTheTest);<\/p>\n<p>void RunTheTest()<br \/>\n{<br \/>\nvar car = new Car<br \/>\n{<br \/>\nColor = &quot;Yellow&quot;, MakeId = 1, PetName = &quot;Herbie&quot;<br \/>\n};<br \/>\nvar carCount = Context.Cars.Count(); Context.Cars.Add(car); Context.SaveChanges();<br \/>\nvar newCarCount = Context.Cars.Count(); Assert.Equal(carCount+1,newCarCount);<br \/>\n}<br \/>\n}<\/p>\n<p>The executed SQL statement is shown here. Notice that the recently added entity is queried for the database-generated properties (Id and TimeStamp). When the results of the query come to EF Core, the entity is updated with the server-side values.<\/p>\n<p>exec sp_executesql N'SET NOCOUNT ON;<br \/>\nINSERT INTO [dbo].[Inventory] ([Color], [MakeId], [PetName], [Price]) VALUES (@p0, @p1, @p2, @p3);<br \/>\nSELECT [Id], [DateBuilt], [Display], [IsDrivable], [TimeStamp] FROM [dbo].[Inventory]<br \/>\nWHERE @@ROWCOUNT = 1 AND [Id] = scope_identity(); ',<br \/>\nN'@p0 nvarchar(50),@p1 int,@p2 nvarchar(50),@p3 nvarchar(50)',@p0=N'Yellow',@p1=1, @p2=N'Herbie',@p3=NULL<\/p>\n<p>Add a Single Record Using Attach<br \/>\nThe following test creates a new Car entity with the Id left at the default value of zero. When the entity is attached to the ChangeTracker, the state is set to Added, and calling SaveChanges() adds the entity to the database.<\/p>\n<p>[Fact]<br \/>\npublic void ShouldAddACarWithAttach()<br \/>\n{<br \/>\nExecuteInATransaction(RunTheTest);<\/p>\n<p>void RunTheTest()<br \/>\n{<\/p>\n<p>var car = new Car<br \/>\n{<br \/>\nColor = &quot;Yellow&quot;, MakeId = 1, PetName = &quot;Herbie&quot;<br \/>\n};<br \/>\nvar carCount = Context.Cars.Count(); Context.Cars.Attach(car);<br \/>\nAssert.Equal(EntityState.Added, Context.Entry(car).State); Context.SaveChanges();<br \/>\nvar newCarCount = Context.Cars.Count(); Assert.Equal(carCount + 1, newCarCount);<br \/>\n}<br \/>\n}<\/p>\n<p>Add Multiple Records at Once<br \/>\nTo insert multiple records in a single transaction, use the AddRange() method of DbSet<T>, as shown in this test (note that with SQL Server, for batching to be used when persisting data, there must be at least four actions to execute):<\/p>\n<p>[Fact]<br \/>\npublic void ShouldAddMultipleCars()<br \/>\n{<br \/>\nExecuteInATransaction(RunTheTest);<\/p>\n<p>void RunTheTest()<br \/>\n{<br \/>\n\/\/Have to add 4 to activate batching var cars = new List<Car><br \/>\n{<br \/>\nnew() { Color = &quot;Yellow&quot;, MakeId = 1, PetName = &quot;Herbie&quot; }, new() { Color = &quot;White&quot;, MakeId = 2, PetName = &quot;Mach 5&quot; }, new() { Color = &quot;Pink&quot;, MakeId = 3, PetName = &quot;Avon&quot; }, new() { Color = &quot;Blue&quot;, MakeId = 4, PetName = &quot;Blueberry&quot; },<br \/>\n};<br \/>\nvar carCount = Context.Cars.Count(); Context.Cars.AddRange(cars); Context.SaveChanges();<br \/>\nvar newCarCount = Context.Cars.Count(); Assert.Equal(carCount + 4, newCarCount);<br \/>\n}<br \/>\n}<\/p>\n<p>The add statements are batched into a single call to the database, and all the generated columns are queried. When the results of the query come to EF Core, the entities are updated with the server-side values. The executed SQL statement is shown here:<\/p>\n<p>exec sp_executesql N'SET NOCOUNT ON;<br \/>\nDECLARE @inserted0 TABLE ([Id] int, [_Position] [int]); MERGE [dbo].[Inventory] USING (<\/p>\n<p>VALUES (@p0, @p1, @p2, @p3, 0), (@p4, @p5, @p6, @p7, 1),<br \/>\n(@p8, @p9, @p10, @p11, 2), (@p12, @p13, @p14, @p15, 3))<br \/>\nAS i ([Color], [MakeId], [PetName], [Price], _Position) ON 1=0 WHEN NOT MATCHED THEN<br \/>\nINSERT ([Color], [MakeId], [PetName], [Price])<br \/>\nVALUES (i.[Color], i.[MakeId], i.[PetName], i.[Price]) OUTPUT INSERTED.[Id], i._Position<br \/>\nINTO @inserted0;<\/p>\n<p>SELECT [t].[Id], [t].[DateBuilt], [t].[Display], [t].[IsDrivable], [t].[TimeStamp] FROM [dbo].[Inventory] t<br \/>\nINNER JOIN @inserted0 i ON ([t].[Id] = [i].[Id]) ORDER BY [i].[_Position];<br \/>\n',<br \/>\nN'@p0 nvarchar(50),@p1 int,@p2 nvarchar(50),@p3 nvarchar(50), @p4 nvarchar(50),@p5 int,@p6 nvarchar(50),@p7 nvarchar(50), @p8 nvarchar(50),@p9 int,@p10 nvarchar(50),@p11 nvarchar(50),<br \/>\n@p12 nvarchar(50),@p13 int,@p14 nvarchar(50),@p15 nvarchar(50)', @p0=N'Yellow',@p1=1,@p2=N'Herbie',@p3=NULL,@p4=N'White',@p5=2,<br \/>\n@p6=N'Mach 5',@p7=NULL,@p8=N'Pink',@p9=3,@p10=N'Avon',@p11=NULL,@p12=N'Blue', @p13=4,@p14=N'Blueberry',@p15=NULL<\/p>\n<p>Adding an Object Graph<br \/>\nThe following test demonstrates adding an object graph (related Make, Car, and Radio records):<\/p>\n<p>[Fact]<br \/>\npublic void ShouldAddAnObjectGraph()<br \/>\n{<br \/>\nExecuteInATransaction(RunTheTest);<\/p>\n<p>void RunTheTest()<br \/>\n{<br \/>\nvar make = new Make {Name = &quot;Honda&quot;}; var car = new Car<br \/>\n{<br \/>\nColor = &quot;Yellow&quot;, MakeId = 1,<br \/>\nPetName = &quot;Herbie&quot;, RadioNavigation = new Radio<br \/>\n{<br \/>\nHasSubWoofers = true, HasTweeters = true, RadioId = &quot;Bose 1234&quot;<br \/>\n}<br \/>\n};<br \/>\n\/\/Cast the Cars property to List<Car> from IEnumerable<Car> ((List<Car>)make.Cars).Add(car);<br \/>\nContext.Makes.Add(make);<\/p>\n<p>var carCount = Context.Cars.Count(); var makeCount = Context.Makes.Count(); Context.SaveChanges();<br \/>\nvar newCarCount = Context.Cars. Count(); var newMakeCount = Context.Makes. Count(); Assert.Equal(carCount+1,newCarCount); Assert.Equal(makeCount+1,newMakeCount);<br \/>\n}<br \/>\n}<\/p>\n<p>The executed SQL statements (one for each table) are shown here:<\/p>\n<p>exec sp_executesql N'SET NOCOUNT ON; INSERT INTO [dbo].[Makes] ([Name]) VALUES (@p0);<br \/>\nSELECT [Id], [TimeStamp] FROM [dbo].[Makes]<br \/>\nWHERE @@ROWCOUNT = 1 AND [Id] = scope_identity(); ',<br \/>\nN'@p0 nvarchar(50)',@p0=N'Honda'<\/p>\n<p>exec sp_executesql N'SET NOCOUNT ON;<br \/>\nINSERT INTO [dbo].[Inventory] ([Color], [MakeId], [PetName], [Price]) VALUES (@p1, @p2, @p3, @p4);<br \/>\nSELECT [Id], [DateBuilt], [Display], [IsDrivable], [TimeStamp] FROM [dbo].[Inventory]<br \/>\nWHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();<\/p>\n<p>',N'@p1 nvarchar(50),@p2 int,@p3 nvarchar(50),@p4 nvarchar(50)',@p1=N'Yellow',@p2=7, @p3=N'Herbie',@p4=NULL<\/p>\n<p>exec sp_executesql N'SET NOCOUNT ON;<br \/>\nINSERT INTO [dbo].[Radios] ([InventoryId], [HasSubWoofers], [HasTweeters], [RadioId])<br \/>\nVALUES (@p5, @p6, @p7, @p8); SELECT [Id], [TimeStamp] FROM [dbo].[Radios]<br \/>\nWHERE @@ROWCOUNT = 1 AND [Id] = scope_identity(); ',<br \/>\nN'@p5 int,@p6 bit,@p7 bit,@p8 nvarchar(50)',@p5=11, @p6=1,@p7=1,@p8=N'Bose 1234'<\/p>\n<p>Updating Records<br \/>\nRecords are updated by loading them into DbSet<T> as a tracked entity, changing them through code, and then calling SaveChanges() on the context. When SaveChanges() is executed, the ChangeTracker reports all the modified entities, and EF Core (along with the database provider) creates the appropriate SQL statement(s) to update the record(s).<\/p>\n<p>Update Tracked Entities<br \/>\nThe following test updates a single record, but the process is the same if multiple tracked entities are updated and saved.<\/p>\n<p>[Fact]<br \/>\npublic void ShouldUpdateACar()<br \/>\n{<br \/>\nExecuteInASharedTransaction(RunTheTest);<\/p>\n<p>void RunTheTest(IDbContextTransaction trans)<br \/>\n{<br \/>\nvar car = Context.Cars.First(c =&gt; c.Id == 1); Assert.Equal(&quot;Black&quot;,car.Color);<br \/>\ncar.Color = &quot;White&quot;;<br \/>\n\/\/Calling update is not needed because the entity is tracked<br \/>\n\/\/Context.Cars.Update(car); Context.SaveChanges(); Assert.Equal(&quot;White&quot;, car.Color);<br \/>\nvar context2 = TestHelpers.GetSecondContext(Context, trans); var car2 = context2.Cars.First(c =&gt; c.Id == 1); Assert.Equal(&quot;White&quot;, car2.Color);<br \/>\n}<br \/>\n}<\/p>\n<p>The prior code uses a shared transaction across two instances of ApplicationDbContext. This is to provide isolation between the context executing the test and the context checking the result of the test.<\/p>\n<p>The executed SQL statement is listed here:<\/p>\n<p>exec sp_executesql N'SET NOCOUNT ON; UPDATE [dbo].[Inventory] SET [Color] = @p0 WHERE [Id] = @p1 AND [TimeStamp] = @p2; SELECT [TimeStamp]<br \/>\nFROM [dbo].[Inventory]<br \/>\nWHERE @@ROWCOUNT = 1 AND [Id] = @p1;<\/p>\n<p>',N'@p1 int,@p0 nvarchar(50),@p2 varbinary(8)',@p1=1,@p0=N'White',@p2=0x000000000000862D<\/p>\n<p>Update Nontracked Entities<br \/>\nUntracked entities can also be used to update database records. The process is similar to updating tracked entities except that the entity is created in code (and not queried), and EF Core must be notified that the entity should already exist in the database and needs to be updated.<br \/>\nThe following example reads a record in as nontracked, creates a new instance of the Car class from this record, and changes one property (Color). Then it either sets the state or uses the Update() method on<br \/>\nDbSet<T>, depending on which line of code you uncomment. The Update() method also changes the state to Modified. The test then calls SaveChanges(). All of the extra contexts are there to ensure the test is accurate, and there isn\u2019t any crossover between contexts.<\/p>\n<p>[Fact]<br \/>\npublic void ShouldUpdateACarUsingState()<br \/>\n{<br \/>\nExecuteInASharedTransaction(RunTheTest);<\/p>\n<p>void RunTheTest(IDbContextTransaction trans)<br \/>\n{<br \/>\nvar car = Context.Cars.AsNoTracking().First(c =&gt; c.Id == 1); Assert.Equal(&quot;Black&quot;, car.Color);<br \/>\nvar updatedCar = new Car<br \/>\n{<br \/>\nColor = &quot;White&quot;, \/\/Original is Black Id = car.Id,<br \/>\nMakeId = car.MakeId, PetName = car.PetName, TimeStamp = car.TimeStamp, IsDrivable = car.IsDrivable<br \/>\n};<br \/>\nvar context2 = TestHelpers.GetSecondContext(Context, trans);<br \/>\n\/\/Either call Update or modify the state context2.Entry(updatedCar).State = EntityState.Modified;<br \/>\n\/\/context2.Cars.Update(updatedCar); context2.SaveChanges();<br \/>\nvar context3 = TestHelpers.GetSecondContext(Context, trans);<br \/>\nvar car2 = context3.Cars.First(c =&gt; c.Id == 1); Assert.Equal(&quot;White&quot;, car2.Color);<br \/>\n}<br \/>\n}<\/p>\n<p>Concurrency Checking When Updating Records<br \/>\nThe previous chapters covered concurrency checking in great detail. As a reminder, when an entity has a Timestamp property defined, the value of that property is used in the where clause when changes (updates or deletes) are being persisted to the database. Instead of just searching for the primary key, the TimeStamp value is added to the query, like this example:<\/p>\n<p>UPDATE [dbo].[Inventory] SET [PetName] = @p0 WHERE [Id] = @p1 AND [TimeStamp] = @p2;<\/p>\n<p>The following test shows an example of creating a concurrency exception, catching it, and using the<br \/>\nEntries to get the original values, current values, and values that are currently stored in the database. Getting the current values requires another database call.<\/p>\n<p>[Fact]<br \/>\npublic void ShouldThrowConcurrencyException()<br \/>\n{<br \/>\nExecuteInATransaction(RunTheTest);<\/p>\n<p>void RunTheTest()<br \/>\n{<\/p>\n<p>var car = Context.Cars.First();<br \/>\n\/\/Update the database outside of the context Context.Database.ExecuteSqlInterpolated(<br \/>\n$&quot;Update dbo.Inventory set Color='Pink' where Id = {car.Id}&quot;); car.Color = &quot;Yellow&quot;;<br \/>\nvar ex = Assert.Throws<CustomConcurrencyException>( () =&gt; Context.SaveChanges());<br \/>\nvar entry = ((DbUpdateConcurrencyException) ex.InnerException)?.Entries[0]; PropertyValues originalProps = entry.OriginalValues;<br \/>\nPropertyValues currentProps = entry.CurrentValues;<br \/>\n\/\/This needs another database call<br \/>\nPropertyValues databaseProps = entry.GetDatabaseValues();<br \/>\n}<br \/>\n}<\/p>\n<p>Deleting Records<br \/>\nA single entity is marked for deletion by calling Remove() on DbSet<T> or by setting its state to Deleted. A list of records are marked for deletion by calling RemoveRange() on the DbSet<T>. The removal process will<br \/>\ncause cascade effects on navigation properties based on the rules configured in the Fluent API (or by EF Core conventions). If deletion is prevented due to cascade policy, an exception is thrown.<\/p>\n<p>Delete Tracked Records<br \/>\nThe delete process mirrors the update process. Once an entity is tracked, call Remove() on that instance, and then call SaveChanges() to remove the record from the database.<\/p>\n<p>[Fact]<br \/>\npublic void ShouldRemoveACar()<br \/>\n{<br \/>\nExecuteInATransaction(RunTheTest);<\/p>\n<p>void RunTheTest()<br \/>\n{<br \/>\nvar carCount = Context.Cars. Count();<br \/>\nvar car = Context.Cars.First(c =&gt; c.Id == 9); Context.Cars.Remove(car); Context.SaveChanges();<br \/>\nvar newCarCount = Context.Cars.Count(); Assert.Equal(carCount - 1, newCarCount); Assert.Equal(<br \/>\nEntityState.Detached, Context.Entry(car).State);<br \/>\n}<br \/>\n}<\/p>\n<p>After SaveChanges() is called, the entity instance still exists, but is no longer in the ChangeTracker.<br \/>\nWhen checking the EntityState, the state will be Detached.<\/p>\n<p>The executed SQL call for the delete is listed here:<\/p>\n<p>exec sp_executesql N'SET NOCOUNT ON; DELETE FROM [dbo].[Inventory]<br \/>\nWHERE [Id] = @p0 AND [TimeStamp] = @p1; SELECT @@ROWCOUNT;'<br \/>\n,N'@p0 int,@p1 varbinary(8)',@p0=2,@p1=0x0000000000008680<\/p>\n<p>Delete Nontracked Entities<br \/>\nUntracked entities can delete records the same way untracked entities can update records. The difference is that the entity is tracked by calling Remove()\/RemoveRange() or setting the state to Deleted and then calling SaveChanges().<br \/>\nThe following example reads a record in as nontracked, creates a new instance of the Car class from this record, and changes one property (Color). Then it either sets the state or uses the Remove() method on DbSet<T> (depending on which line you uncomment). The test then calls SaveChanges(). All the extra contexts are there to ensure there isn\u2019t any crossover between contexts.<\/p>\n<p>[Fact]<br \/>\npublic void ShouldRemoveACarUsingState()<br \/>\n{<br \/>\nExecuteInASharedTransaction(RunTheTest);<\/p>\n<p>void RunTheTest(IDbContextTransaction trans)<br \/>\n{<br \/>\nvar carCount = Context.Cars.Count();<br \/>\nvar car = Context.Cars.AsNoTracking().First(c =&gt; c.Id == 1); var context2 = TestHelpers.GetSecondContext(Context, trans);<br \/>\n\/\/Either call Remove or modify the state context2.Entry(car).State = EntityState.Deleted;<br \/>\n\/\/context2.Cars.Remove(car); context2.SaveChanges();<br \/>\nvar newCarCount = Context.Cars.Count(); Assert.Equal(carCount - 1, newCarCount); Assert.Equal(<br \/>\nEntityState.Detached, Context.Entry(car).State);<br \/>\n}<br \/>\n}<\/p>\n<p>Catch Cascade Delete Failures<br \/>\nEF Core will throw a DbUpdateException when an attempt to delete a record fails due to the cascade rules. The following test shows this in action:<\/p>\n<p>[Fact]<br \/>\npublic void ShouldFailToRemoveACar()<br \/>\n{<br \/>\nExecuteInATransaction(RunTheTest);<\/p>\n<p>void RunTheTest()<br \/>\n{<br \/>\nvar car = Context.Cars.First(c =&gt; c.Id == 1); Context.Cars.Remove(car); Assert.Throws<CustomDbUpdateException>(<br \/>\n()=&gt;Context.SaveChanges());<br \/>\n}<br \/>\n}<\/p>\n<p>Concurrency Checking When Deleting Records<br \/>\nDelete also uses concurrency checking if the entity has a TimeStamp property. See the section \u201cConcurrency Checking\u201d in the \u201cUpdating Records\u201d section for more information.<\/p>\n<p>Summary<br \/>\nThis chapter used the knowledge gained in the previous chapter to complete the data access layer for the AutoLot database. You used the EF Core command-line tools to scaffold an existing database, updated the model to its final version, and then created migrations and applied them. Repositories were added for the encapsulation of the data access, and database initialization code with sample data can drop and create the database in a repeatable, reliable manner. The rest of the chapter focused on test-driving the data access layer. This completes our journey through data access and Entity Framework Core.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>CHAPTER 24 Test-Driving AutoLot Now that you have the finished AutoLot data access layer, it\u2019s time to take it for a test-drive. Integration testing in an integral part of software development and is a great way to make sure your data access code behaves as expected. In this chapter, we will be using xUnit, a [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3],"tags":[22],"class_list":["post-346","post","type-post","status-publish","format-standard","hentry","category-csharp","tag-pro-csharp10-with-net6"],"_links":{"self":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/346","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=346"}],"version-history":[{"count":0,"href":"https:\/\/diji.net\/index.php?rest_route=\/wp\/v2\/posts\/346\/revisions"}],"wp:attachment":[{"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=346"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=346"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/diji.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=346"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}