-
Book Overview & Buying
-
Table Of Contents
Apps and Services with .NET 7
By :
Practical applications usually need to work with data in a relational database or another data store. Earlier in this chapter, we defined EF Core models in the same console app project that we used them in. Now, we will define an entity data model for the Northwind database 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, default configuration for how to connect to the database, and use fluent API to configure additional options for the model. This pair of class libraries will be used in many of the apps and services that you create in subsequent chapters.
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 WebAssembly clients.
You will now create the entity models using the dotnet-ef tool:
classlibNorthwind.Common.EntityModels.SqlServerChapter02Northwind.Common.EntityModels.SqlServer 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>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference
Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.0" />
<PackageReference
Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
Class1.cs file.Northwind.Common.EntityModels.SqlServer project.Northwind.Common.EntityModels.SqlServer folder.
The next step assumes a database connection string for a local SQL Server authenticated with Windows Integrated security. Modify it for Azure SQL Database or Azure SQL Edge with a user ID and password if necessary.
dotnet ef dbcontext scaffold "Data Source=.;Initial Catalog=Northwind;Integrated Security=true;TrustServerCertificate=True;" Microsoft.EntityFrameworkCore.SqlServer --namespace Packt.Shared --data-annotations
Note the following:
dbcontext scaffold"Data Source=.;Initial Catalog=Northwind;Integrated Security=true;TrustServerCertificate=True;"Microsoft.EntityFrameworkCore.SqlServer--namespace Packt.Shared--data-annotationsAlphabeticalListOfProduct.cs to Territory.cs.Customer.cs, the dotnet-ef tool correctly identified that the CustomerId 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!;
Next, you will move the context model that represents the database to a separate class library:
classlibNorthwind.Common.DataContext.SqlServerChapter02Northwind.Common.DataContext.SqlServer as the active OmniSharp project.DataContext project, treat warnings as errors, add a project reference to the EntityModels project, and add a package reference to the EF Core data provider for SQL Server, as shown highlighted in the following markup:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference
Include="Microsoft.EntityFrameworkCore.SqlServer" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Northwind.Common.EntityModels
.SqlServer\Northwind.Common.EntityModels.SqlServer.csproj" />
</ItemGroup>
</Project>
Warning! The path to the project reference should not have a line break in your project file.
Northwind.Common.DataContext.SqlServer project, delete the Class1.cs file.Northwind.Common.DataContext.SqlServer project.NorthwindContext.cs file from the Northwind.Common.EntityModels.SqlServer project/folder to the Northwind.Common.DataContext.SqlServer project/folder.Northwind.Common.DataContext.SqlServer project, in NorthwindContext.cs, remove the compiler warning about the connection string.Northwind.Common.DataContext.SqlServer project, add a class named NorthwindContextExtensions.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.EntityFrameworkCore; // UseSqlServer
using Microsoft.Extensions.DependencyInjection; // IServiceCollection
namespace Packt.Shared;
public static class NorthwindContextExtensions
{
/// <summary>
/// Adds NorthwindContext to the specified IServiceCollection. Uses the SqlServer database provider.
/// </summary>
/// <param name="services"></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 = "Data Source=.;Initial Catalog=Northwind;" +
"Integrated Security=true;MultipleActiveResultsets=true;Encrypt=false")
{
services.AddDbContext<NorthwindContext>(options =>
{
options.UseSqlServer(connectionString);
options.LogTo(Console.WriteLine,
new[] { Microsoft.EntityFrameworkCore
.Diagnostics.RelationalEventId.CommandExecuting });
});
return services;
}
}
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.
EF Core 7 adds an IMaterializationInterceptor interface that allows interception before and after an entity is created, and when properties are initialized. This is useful for calculated values.
For example, when a service or client app requests entities to show to the user, it might want to cache a copy of the entity for a period of time. To do this, it needs to know when the entity was last refreshed. It would be useful if this information was automatically generated and stored with each entity.
To achieve this goal, we must complete four steps:
InitializedInstance that will execute on any entity, and if that entity implements the custom interface with the extra property, then it will set its value.Now let’s implement this for Northwind Employee entities:
Northwind.Common.EntityModels.SqlServer project, add a new file named IHasLastRefreshed.cs, and modify its contents to define the interface, as shown in the following code:
namespace Packt.Shared;
public interface IHasLastRefreshed
{
DateTimeOffset LastRefreshed { get; set; }
}
Northwind.Common.EntityModels.SqlServer project, in Employee.cs, implement the interface, as shown highlighted in the following code:
public partial class Employee : IHasLastRefreshed
{
...
[NotMapped]
public DateTimeOffset LastRefreshed { get; set; }
}
Northwind.Common.DataContext.SqlServer project, add a new file named SetLastRefreshedInterceptor.cs, and modify its contents to define the interceptor, as shown in the following code:
// IMaterializationInterceptor, MaterializationInterceptionData
using Microsoft.EntityFrameworkCore.Diagnostics;
namespace Packt.Shared;
public class SetLastRefreshedInterceptor : IMaterializationInterceptor
{
public object InitializedInstance(
MaterializationInterceptionData materializationData,
object entity)
{
if (entity is IHasLastRefreshed entityWithLastRefreshed)
{
entityWithLastRefreshed.LastRefreshed = DateTimeOffset.UtcNow;
}
return entity;
}
}
Northwind.Common.DataContext.SqlServer project, in NorthwindContext.cs, register the interceptor, as shown highlighted in the following code:
public partial class NorthwindContext : DbContext
{
private static readonly SetLastRefreshedInterceptor
setLastRefreshedInterceptor = new();
...
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
optionsBuilder.UseSqlServer("...");
}
optionsBuilder.AddInterceptors(setLastRefreshedInterceptor);
}
...
}
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:
xunit project named Northwind.Common.EntityModels.Tests to the Chapter02 workspace/solution.Northwind.Common.EntityModels.Tests.csproj, modify the configuration to treat warnings as errors and to add an item group with a project reference to the Northwind.Common.DataContext.SqlServer project, as shown in the following markup:
<ItemGroup>
<ProjectReference Include="..\Northwind.Common.DataContext
.SqlServer\Northwind.Common.DataContext.SqlServer.csproj" />
</ItemGroup>
Warning! The path to the project reference should not have a line break in your project file.
Northwind.Common.EntityModels.Tests project.A well-written unit test will have three parts:
Now, we will write some unit tests for the NorthwindContext and entity model classes:
UnitTest1.cs to NorthwindEntityModelsTests.cs and then open it.NorthwindEntityModelsTests. (Visual Studio prompts you to rename the class when you rename the file.)NorthwindEntityModelsTests class to import the Packt.Shared namespace and have some test methods for ensuring the context class can connect, ensuring the provider is SQL Server, and ensuring the first product is named Chai, as shown in the following code:
using Packt.Shared;
namespace Northwind.Common.EntityModels.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);
}
}
[Fact]
public void EmployeeHasLastRefreshedIn10sWindow()
{
using (NorthwindContext db = new())
{
Employee employee1 = db.Employees.Single(p => p.EmployeeId == 1);
DateTimeOffset now = DateTimeOffset.UtcNow;
Assert.InRange(actual: employee1.LastRefreshed,
low: now.Subtract(TimeSpan.FromSeconds(5)),
high: now.AddSeconds(5));
}
}
}
}
Now we are ready to run the unit tests and see the results:
Now we are ready to run the unit tests and see the results:
Northwind.Common.EntityModels.Tests project’s TERMINAL window, run the tests, as shown in the following command:
dotnet test
As an optional task, can you think of other tests you could write to make sure the database context and entity models are correct?
Change the font size
Change margin width
Change background colour