Setting up a database and projects for this book
We need a database and some projects that we can use throughout this book. To make it reasonably realistic, we need multiple projects that use common features like a SQL Server database, class libraries, unit tests, and so on.
We will define an entity data model as a pair of reusable class libraries. One part of the pair will define the entities like Product
and Customer
. The second part of the pair will define the tables in the database, the default configuration for how to connect to the database, and use the Fluent API to configure additional options for the model.
We will create three projects:
- A class library for entity models like
Category
andProduct
namedNorthwind.EntityModels
- A class library for an EF Core data context named
Northwind.DataContext
- An xUnit project for unit and integration tests named
Northwind.Tests
Using a sample relational database
It would be useful to have a sample database that has a medium complexity and a decent number of sample records. Microsoft offers several sample databases, most of which are too complex for our needs, so instead, we will use a database that was first created in the early 1990s known as Northwind.
Let’s take a minute to look at a diagram of the Northwind database and its eight most important tables. You can use the diagram in Figure 1.2 to refer to as we write code and queries throughout this book:
Figure 1.2: The Northwind database tables and relationships
Note that:
- Each category has a unique identifier, name, description, and picture. The picture is stored as a byte array in JPEG format.
- Each product has a unique identifier, name, unit price, number of units in stock, and other columns.
- Each product is associated with a category by storing the category’s unique identifier.
- The relationship between
Categories
andProducts
is one-to-many, meaning each category can have zero, one, or more products. - Each product is supplied by a supplier company indicated by storing the supplier’s unique identifier.
- The quantity and unit price of a product is stored for each detail of an order.
- Each order is made by a customer, taken by an employee, and shipped by a shipping company.
- Each employee has a name, address, contact details, birth, and hire dates, a reference to their manager (except for the boss, whose
ReportsTo
field isnull
), and a photo stored as a byte array in JPEG format. The table has a one-to-many relationship to itself because one employee can manage many other employees.
Setting up SQL Server and the Northwind database
Microsoft offers various editions of its popular and capable SQL Server product for Windows, Linux, and Docker containers.
If you have Windows, then you can use a free version that runs standalone, known as SQL Server Developer Edition. You can also use the Express edition or the free SQL Server LocalDB edition that can be installed with Visual Studio. To install SQL Server locally on Windows, please see the online instructions at the following link: https://github.com/markjprice/tools-skills-net8/blob/main/docs/sql-server/README.md. If you prefer to install SQL Server locally on Linux, then you will find instructions at the following link: https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-setup.
If you do not have a Windows computer or if you want to use a cross-platform database system, then please see the online-only section, Installing Azure SQL Edge in Docker, found at the following link: https://github.com/markjprice/tools-skills-net8/blob/main/docs/sql-server/edge.md.
You’ll need to have set up SQL Server, run the SQL script to create the Northwind database and confirm that you can connect to the database and view the rows in its tables like Products
and Categories
before continuing with the project. The following two subsections provide detailed steps to help you do so using either a local SQL Server or SQL Edge in Docker. You can skip this if you already have this set up.
Creating the Northwind database for a local SQL Server
To run a SQL script to create the Northwind sample database for a local SQL Server:
- If you have not previously downloaded or cloned the GitHub repository for this book, then do so now using the following link: https://github.com/markjprice/tools-skills-net8/.
- Copy the script to create the Northwind database for SQL Server from the following path in your local Git repository:
/scripts/sql-scripts/Northwind4SQLServer.sql
into a working folder. - Start SQL Server Management Studio.
- In the Connect to Server dialog, for Server name, enter . (a dot), meaning the local computer name, and then click the Connect button. If you had to create a named instance, like tools-skills-net8, then enter
.\tools-skills-net8
. - Navigate to File | Open | File....
- Browse to select the
Northwind4SQLServer.sql
file and then click the Open button. - In the toolbar, click Execute, and note the Command(s) completed successfully message.
- In Object Explorer, expand the Northwind database, and then expand Tables.
- Right-click Products, click Select Top 1000 Rows, and note the returned results.
- Exit SQL Server Management Studio.
Creating the Northwind database for SQL Edge in Docker
To run a database script to create the Northwind sample database for SQL Edge in Docker:
- In your preferred code editor, open the
Northwind4AzureSQLedge.sql
file. - Connect to SQL Edge in Docker using the following connection information:
- Data Source aka server:
tcp:127.0.0.1,1433
- You must use SQL Server Authentication aka SQL Login i.e. you must supply a user name and password. Azure SQL Edge image has the
sa
user already created and you had to give it a strong password when you ran the container. We chose the passwords3cret-Ninja
. - Database:
master
or leave blank. We will create the Northwind database using a SQL script.
- Data Source aka server:
- Execute the SQL script:
- If you are using Visual Studio, right-click in the script, select Execute, and then wait to see the Command completed successfully message.
- If you are using Code, right-click in the script, select Execute Query, select the Azure SQL Edge in Docker connection profile, and then wait to see the Commands completed successfully messages.
- Refresh the data connection:
- If you are using Visual Studio, then in Server Explorer, right-click Tables and select Refresh.
- If you are using Code, then right-click the Azure SQL Edge in Docker connection profile and choose Refresh.
- Expand Databases, expand Northwind, and then expand Tables.
- Note that 13 tables have been created, for example, Categories, Customers, and Products. Also note that dozens of views and stored procedures have also been created.
- Next, we will define an entity data model for the Northwind database as a pair of reusable class libraries.
Good Practice
You should create a separate class library project for your entity data models. This allows easier sharing between backend web servers and frontend desktop, mobile, and Blazor clients.
Creating a class library for entity models using SQL Server
You will now create the entity models using the dotnet-ef
tool:
- Using your preferred code editor, create a new project, as defined in the following list:
- Project template: Class Library /
classlib
- Project file and folder:
Northwind.EntityModels
- Solution file and folder:
Chapter01
- Project template: Class Library /
- In the
Northwind.EntityModels
project, treat warnings as errors, and add package references for the SQL Server database provider and EF Core design-time support, as shown highlighted in the following markup:<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net8.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> <ItemGroup> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.6"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> </ItemGroup> </Project>
You can check the most recent package versions at the following links: https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.SqlServer and https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Design.
If you are unfamiliar with how packages like Microsoft.EntityFrameworkCore.Design
can manage their assets, then you can learn more at the following link: https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#controlling-dependency-assets.
Good Practice
By default, compiler warnings may appear if there are potential problems with your code when you first build a project, but they do not prevent compilation and they are hidden if you rebuild. Warnings are given for a reason, so ignoring warnings encourages poor development practices. I recommend that you force yourself to fix warnings by enabling the option to treat warnings as errors.
- Delete the
Class1.cs
file. - Build the
Northwind.EntityModels
project. - Open a command prompt or terminal for the
Northwind.EntityModels
folder. - If you do not already have the
dotnet-ef
tool, then install the latest version, as shown in the following command:dotnet tool install --global dotnet-ef
- Instead of installing, you can update using the following command:
dotnet tool update --global dotnet-ef
The next step assumes a database connection string for a local SQL Server authenticated with Windows Integrated Security. Modify it for Azure SQL Edge with a user ID and password if necessary.
- At the command line, generate entity class models for all tables, as shown in the following commands:
dotnet ef dbcontext scaffold "Data Source=.;Initial atalog=Northwind;Integrated Security=true;TrustServerCertificate=True;" Microsoft.EntityFrameworkCore.SqlServer --namespace Northwind.EntityModels --data-annotations
Note the following:
- The command to perform:
dbcontext scaffold
. - The connection string:
"Data Source=.;Initial Catalog=Northwind;Integrated Security=true;TrustServerCertificate=True;"
- The database provider:
Microsoft.EntityFrameworkCore.SqlServer
- The namespace for the generated classes:
--namespace Northwind.EntityModels
- To use data annotations as well as the Fluent API:
--data-annotations
- Note that 28 classes were generated, from
AlphabeticalListOfProduct.cs
toTerritory.cs
. - At the top of the
NorthwindContext.cs
file, import the namespace for working with ADO.NET types, as shown in the following code:using Microsoft.Data.SqlClient; // To use SqlConnectionStringBuilder.
- Modify the
OnConfiguring
method to dynamically set the connection string and set any sensitive parameters using environment variables, as shown in the following code:protected override void OnConfiguring( DbContextOptionsBuilder optionsBuilder) { // If not already configured by a client project. For example, // a client project could use AddNorthwindContext to override // the database connection string. if (!optionsBuilder.IsConfigured) { SqlConnectionStringBuilder builder = new(); builder.DataSource = "."; builder.InitialCatalog = "Northwind"; builder.TrustServerCertificate = true; builder.MultipleActiveResultSets = true; // If using Azure SQL Edge. // builder.DataSource = "tcp:127.0.0.1,1433"; // Because we want to fail faster. Default is 15 seconds. builder.ConnectTimeout = 3; // If using Windows Integrated authentication. builder.IntegratedSecurity = true; // If using SQL Server authentication. // builder.UserID = Environment.GetEnvironmentVariable("MY_SQL_USR"); // builder.Password = Environment.GetEnvironmentVariable("MY_SQL_PWD"); optionsBuilder.UseSqlServer(builder.ConnectionString); } }
- In
Customer.cs
, thedotnet-ef
tool correctly identified that theCustomerId
column is the primary key and it is limited to a maximum of five characters, but we also want the values to always be uppercase. So, add a regular expression to validate its primary key value to only allow uppercase Western characters, as shown highlighted in the following code:[Key] [StringLength(5)] [RegularExpression("[A-Z]{5}")] public string CustomerId { get; set; } = null!;
Creating a class library for the data context using SQL Server
Next, you will move the context model that represents the database to a separate class library:
- Add a new project, as defined in the following list:
- Project template: Class Library /
classlib
- Project file and folder:
Northwind.DataContext
- Solution file and folder:
Chapter01
- Project template: Class Library /
- In the
DataContext
project, add a project reference to theEntityModels
project, and add a package reference to the EF Core data provider for SQL Server, as shown in the following markup:<ItemGroup> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.6" /> </ItemGroup> <ItemGroup> <ProjectReference Include= "..\Northwind.EntityModels\Northwind.EntityModels.csproj" /> </ItemGroup>
You can try out previews of EF Core 9 by specifying version 9.0-*
. The target framework for your project should continue to use net8.0
. By using a wildcard, you will automatically download the latest monthly preview when you restore the packages for the project. Once the EF Core 9 GA version is released in November 2024, change the package version to 9.0.0
or later. After February 2025, you will be able to do similar with EF Core 10 (use a package version of 10.0-*
) but that will likely require a project targeting net10.0
so you will have to install a preview version of .NET 10 SDK as well.
- In the
Northwind.DataContext
project, delete theClass1.cs
file. - Build the
Northwind.DataContext
project. - Move the
NorthwindContext.cs
file from theNorthwind.EntityModels
project/folder to theNorthwind.DataContext
project/folder. - In the
Northwind.DataContext
project, add a class namedNorthwindContextExtensions.cs
, and modify its contents to define an extension method that adds the Northwind database context to a collection of dependency services, as shown in the following code:using Microsoft.Data.SqlClient; // SqlConnectionStringBuilder using Microsoft.EntityFrameworkCore; // UseSqlServer using Microsoft.Extensions.DependencyInjection; // IServiceCollection namespace Northwind.EntityModels; public static class NorthwindContextExtensions { /// <summary> /// Adds NorthwindContext to the specified IServiceCollection. Uses the SqlServer database provider. /// </summary> /// <param name="services">The service collection.</param> /// <param name="connectionString">Set to override the default.</param> /// <returns>An IServiceCollection that can be used to add more services.</returns> public static IServiceCollection AddNorthwindContext( this IServiceCollection services, string? connectionString = null) { if (connectionString == null) { SqlConnectionStringBuilder builder = new(); builder.DataSource = "."; builder.InitialCatalog = "Northwind"; builder.TrustServerCertificate = true; builder.MultipleActiveResultSets = true; // If using Azure SQL Edge. // builder.DataSource = "tcp:127.0.0.1,1433"; // Because we want to fail fast. Default is 15 seconds. builder.ConnectTimeout = 3; // If using Windows Integrated authentication. builder.IntegratedSecurity = true; // If using SQL Server authentication. // builder.UserID = Environment.GetEnvironmentVariable("MY_SQL_USR"); // builder.Password = Environment.GetEnvironmentVariable("MY_SQL_PWD"); connectionString = builder.ConnectionString; } services.AddDbContext<NorthwindContext>(options => { options.UseSqlServer(connectionString); // Log to console when executing EF Core commands. options.LogTo(Console.WriteLine, new[] { Microsoft.EntityFrameworkCore .Diagnostics.RelationalEventId.CommandExecuting }); }, // Register with a transient lifetime to avoid concurrency // issues with Blazor Server projects. contextLifetime: ServiceLifetime.Transient, optionsLifetime: ServiceLifetime.Transient); return services; } }
- Build the two class libraries and fix any compiler errors.
Good Practice
We have provided an optional argument for the AddNorthwindContext
method so that we can override the SQL Server database connection string. This will allow us more flexibility, for example, to load these values from a configuration file.
Creating a test project to check the integration of the class libraries
Since we will not be creating a client project in this chapter that uses the EF Core model, we should create a test project to make sure the database context and entity models integrate correctly:
- Use your preferred code editor to add a new xUnit Test Project [C#] /
xunit
project namedNorthwind.Tests
to theChapter01
solution. - In
Northwind.Tests.csproj
, modify the configuration to treat warnings as errors and add an item group with a project reference to theNorthwind.DataContext
project, as shown in the following markup:<ItemGroup> <ProjectReference Include= "..\Northwind.DataContext\Northwind.DataContext.csproj" /> </ItemGroup>
Warning!
The path to the project reference should not have a line break in your project file.
- Build the
Northwind.Tests
project to build and restore project dependencies. - Rename the file
UnitTest1.cs
toNorthwindEntityModelsTests.cs
(Visual Studio prompts you to rename the class when you rename the file). - In
NorthwindEntityModelsTests.cs
, if you are using Code, then manually rename the class toNorthwindEntityModelsTests
. - In
NorthwindEntityModelsTests.cs
, modify the class to import theNorthwind.EntityModels
namespace and have some test methods for ensuring that the context class can connect, the provider is SQL Server, and the first product is namedChai
, as shown in the following code:using Northwind.EntityModels; // To use NorthwindContext and Product. namespace Northwind.Tests; public class NorthwindEntityModelsTests { [Fact] public void CanConnectIsTrue() { using (NorthwindContext db = new()) // arrange { bool canConnect = db.Database.CanConnect(); // act Assert.True(canConnect); // assert } } [Fact] public void ProviderIsSqlServer() { using (NorthwindContext db = new()) { string? provider = db.Database.ProviderName; Assert.Equal("Microsoft.EntityFrameworkCore.SqlServer", provider); } } [Fact] public void ProductId1IsChai() { using (NorthwindContext db = new()) { Product? product1 = db?.Products?.Single(p => p.ProductId == 1); Assert.Equal("Chai", product1?.ProductName); } } }
Running tests
Now we are ready to run the tests and see the results using either Visual Studio or Code.
Using Visual Studio:
- In Visual Studio, in Solution Explorer, right-click the
Northwind.Tests
project, and then select Run Tests. - In Test Explorer, note that the results indicate that three tests ran, and all passed, as shown in Figure 1.3:
Figure 1.3: All the tests passed
Using Code:
- In Code, in the
Northwind.Tests
project’s TERMINAL window, run the tests, as shown in the following command:dotnet test
If you are using C# Dev Kit, then you can also build the test project and then run the tests from the Testing section in the Primary Side Bar.
- In the output, note that the results indicate that three tests ran, and all passed.