本文内容为转载,重新排版以供学习研究。如有侵权,请联系作者删除。
转载请注明本文出处:Professional C# 6 and .NET Core 1.0 - Chapter 38 Entity Framework Core
-----------------------------------------------------------------------
What’s In This Chapter?
- Introducing Entity Framework Core 1.0
- Using Dependency Injection with Entity Framework
- Creating a Model with Relations
- Using Migrations with the .NET CLI Tools and MSBuild
- Object Tracking
- Updating Objects and Object Trees
- Conflict Handling with Updates
- Using Transactions
Wrox.Com Code Downloads For This Chapter
The wrox.com code downloads for this chapter are found atwww.wrox.com/go/professionalcsharp6on the Download Code tab. The code for this chapter is divided into the following major examples:
- Books Sample
- Books Sample with DI
- Menus Sample
- Menus with Data Annotations
- Conflict Handling Sample
- Transactions Sample
History of Entity Framework
Entity Framework is a framework offering mapping of entities to relationships.With this, you can create types that map to database tables, create database queries using LINQ, create and update objects, and write them to the database.
After many years of few changes to Entity Framework, the newest version is a complete rewrite. Let’s have a look at the history of Entity Framework to see the reasons for the rewrite.
- Entity Framework 1—The first version of Entity Framework was not ready with .NET 3.5, but it was soon available with .NET 3.5 SP1. Another product offering somewhat similar functionality that was already available with .NET 3.5 was LINQ to SQL. Both LINQ to SQL and Entity Framework offered similar features from a wide view. However, LINQ to SQL was simpler to use but was only available for accessing SQL Server. Entity Framework was provider-based and offered access to several different relational databases. It included more features, such as many-to-many mapping without the need for mapping objects, and n-to-n mapping was possible. One disadvantage of Entity Framework was that it required model types to derive from the EntityObject base class. Mapping the objects to relations was done using an EDMX file that contains XML. The XML contained is defined by three schemas: the Conceptual Schema Definition (CSD) defines the object types with their properties and associations; the Storage Schema Definition (SSD) defines the database tables, columns, and relations; and the Mapping Schema Language (MSL) defines how the CSD and SSD map to each other.
- Entity Framework 4—Entity Framework 4 was available with .NET 4 and received major improvements, many coming from LINQ to SQL ideas. Because of the big changes, versions 2 and 3 have been skipped. With this edition, lazy loading was added to fetch relations on accessing a property. Creating a database was possible after designing a model using SQL Data Definition Language (DDL). The two models using Entity Framework were now Database First or Model First. Possibly the most important feature added was the support for Plain Old CLR Objects (POCO), so it was no longer necessary to derive from the base class EntityObject.
With later updates (such as Entity Framework 4.1, 4.2), additional features have been added with NuGet packages. This allowed adding features faster. Entity Framework 4.1 offers the Code First model where the EDMX file to define the mappings is no longer used. Instead, all the mapping is defined using C# code—either using attributes or with a fluent API to define the mapping using code. Entity Framework 4.3 added support for Migrations. With this, it is possible to define updates to the database schemas using C# code. The database update can be automatically applied from the application using the database.
- Entity Framework 5—The NuGet package for Entity Framework 5 supported both .NET 4.5 and .NET 4 applications. However, many of the features of Entity Framework 5 have been available with .NET 4.5. Entity Framework was still based on types that are installed on the system with .NET 4.5. New with this release were performance improvements as well as supporting new SQL Server features, such as spatial data types.
- Entity Framework 6—Entity Framework 6 solved some issues with Entity Framework 5, which was partly a part of the framework installed on the system and partly available via NuGet extensions. Now the complete code of Entity Framework has moved to NuGet packages. For not creating conflicts, a new namespace was used. When porting apps to the new version, the namespace had to be changed.
- This book covers the newest version of Entity Framework, Entity Framework Core 1.0. This version is a complete rewrite and removes old behaviors. This version no longer supports the XML file mapping with CSDL, SSDL, and MSL. Only Code First is supported now—the model that was added with Entity Framework 4.1. Code First doesn’t mean that the database can’t exist first. You can either create the database first or define the database purely from code; both options are possible.
NOTE The name Code First is somewhat misleading. With Code First, either the code or the database can be created first. Originally with the beta version of Code First, the name was Code Only. Because the other model options had First in their names, the name Code Only was changed as well.
The complete rewrite of Entity Framework was also done to not only support relational databases but to also support NoSql databases as well—you just need a provider. Currently, at the time of this writing, provider support is limited, but offers will increase over time.
The new version of Entity Framework is based on .NET Core; thus it is possible to use this framework on Linux and Mac systems as well.
Entity Framework Core 1.0 does not support all the features that were offered by Entity Framework 6. More features will be available over time with newer releases of Entity Framework. You just need to pay attention to what version of Entity Framework you are using. There are many valid reasons to stay with Entity
Framework 6, but using ASP.NET Core 1.0 on non-Windows platforms, using Entity Framework with the Universal Windows Platform, and using nonrelational data stores all require the use of Entity Framework Core 1.0.
This chapter introduces you to Entity Framework Core 1.0. It starts with a simple model reading and writing information from SQL Server. Later on, relations are added, and you will be introduced to the change tracker and conflict handling when writing to the database. Creating and modifying database schemas using migrations is another important part of this chapter.
NOTE This chapter uses the Books database. This database is included with the download of the code samples at www.wrox.com/go/professionalcsharp6.
Introducing Entity Framework
The first example uses a single Book type and maps this type to the Books table in a SQL Server database. You write records to the database and then read, update, and delete them.
With the first example, you create the database first. You can do this with the SQL Server Object Explorer that is part of Visual Studio 2015. Select the database instance (localdb)MSSQLLocalDB is installed with Visual Studio), click the Databases node in the tree view, and select Add New Database. The sample database has only a single table named Books.
You can create the table Books by selecting the Tables node within the Books database and then selecting Add New Table. Using the designer shown in Figure 38.1, or by entering the SQL DDL statement in the T-SQL editor, you can create the table Books. The following code snippet shows the T-SQL code for creating the table. When you click the Update button, you can submit the changes to the database.
CREATE TABLE [dbo].[Books] ( [BookId] INT NOT NULL PRIMARY KEY IDENTITY, [Title] NVARCHAR(50) NOT NULL, [Publisher] NVARCHAR(25) NOT NULL )
Creating a Model
The sample application BookSample for accessing the Books database is a Console Application (Package). This sample makes use of the following dependencies and namespaces:
Dependencies
NETStandard.Library
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
Namespaces
Microsoft.EntityFrameworkCore System.ComponentModel.DataAnnotations.Schema System System.Linq System.Threading.Tasks static System.Console
Figure 38.1
The class Book is a simple entity type that defines three properties. The BookId property maps to the primary key of the table, the Title property to the Title column, and the Publisher property to the Publisher column. To map the type to the Books table, the Table attribute is applied to the type (code file BooksSample/Book.cs):
[Table("Books")] public class Book { public int BookId { get; set; } public string Title { get; set; } public string Publisher { get; set; } }
Creating a Context
The association of the Book table with the database is done creating the
BooksContext class. This class derives from the base class DbContext. The BooksContext class defines the Books property that is of type DbSet<Book>. This type allows creating queries and adding Book instances for storing it in the database. To define the connection string, the OnConfiguring method of the DbContext can be overridden. Here, the UseSqlServer extension method maps the context to a SQL Server database (code file BooksSample/BooksContext.cs):
public class BooksContext: DbContext { private const string ConnectionString = @"server= (localdb)MSSQLLocalDb;database=Books;trusted_connection=true"; public DbSet<Book> Books { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); optionsBuilder.UseSqlServer(ConnectionString); } }
Another option to define the connection string is by using dependency injection, which is shown later in this chapter.
Writing to the Database
The database with the Books table is created; the model and context classes is defined and now you can fill the table with data. The AddBookAsync method is created to add a Book object to the database. First, the BooksContext object is instantiated. With the using statement it is ensured that the database connection is closed. After adding the object to the context using the Add method, the entity is written to the database calling SaveChangesAsync (code file BooksSample/Program.cs):
private async Task AddBookAsync(string title, string publisher) { using (var context = new BooksContext()) { var book = new Book { Title = title, Publisher = publisher }; context.Add(book); int records = await context.SaveChangesAsync();
WriteLine($"{records} record added"); } WriteLine(); }
For adding a list of books, you can use the AddRange method (code file BooksSample/Program.cs):
private async Task AddBooksAsync() { using (var context = new BooksContext()) { var b1 = new Book { Title ="Professional C# 5 and .NET 4.5.1", Publisher ="Wrox Press" }; var b2 = new Book { Title ="Professional C# 2012 and .NET 4.5", Publisher ="Wrox Press" }; var b3 = new Book { Title ="JavaScript for Kids", Publisher ="Wrox Press" }; var b4 = new Book { Title ="Web Design with HTML and CSS", Publisher ="For Dummies" }; context.AddRange(b1, b2, b3, b4); int records = await context.SaveChangesAsync(); WriteLine($"{records} records added"); } WriteLine(); }
When you run the application and invoke these methods, you can see the data written to the database using the SQL Server Object Explorer.
Reading from the Database
To read the data from C# code, you just need to invoke the BooksContext and access the Books property. Accessing this property creates a SQL statement to retrieve all books from the database (code file BooksSample/Program.cs):
private void ReadBooks() { using (var context = new BooksContext()) { var books = context.Books; foreach (var b in books) { WriteLine($"{b.Title} {b.Publisher}"); } } WriteLine(); }
When you open the IntelliTrace Events window during debugging, you can see the SQL statement that is sent to the database (this requires Visual Studio Enterprise edition):
SELECT [b].[BookId], [b].[Publisher], [b].[Title] FROM [Books] AS [b]
Entity Framework offers a LINQ provider. With that, you can create LINQ queries to access the database. You can either use the method syntax as shown here:
private void QueryBooks() { using (var context = new BooksContext()) { var wroxBooks = context.Books.Where(b => b.Publisher =="Wrox Press"); foreach (var b in wroxBooks) { WriteLine($"{b.Title} {b.Publisher}"); } } WriteLine(); }
or use the declarative LINQ query syntax:
var wroxBooks = from b in context.Books where b.Publisher =="Wrox Press" select b;
With both syntax variants, this SQL statement is sent to the database:
SELECT [b].[BookId], [b].[Publisher], [b].[Title] FROM [Books] AS [b] WHERE [b].[Publisher] = 'Wrox Press'
NOTE LINQ is discussed in detail in Chapter 13, “Language Integrated Query.”
Updating Records
Updating records can be easily achieved just by changing objects that have been loaded with the context, and invoking SaveChangesAsync (code file BooksSample/Program.cs):
private async Task UpdateBookAsync() { using (var context = new BooksContext()) { int records = 0; var book = context.Books.Where(b => b.Title =="Professional C# 6") .FirstOrDefault(); if (book != null) { book.Title ="Professional C# 6 and .NET Core 5"; records = await context.SaveChangesAsync(); } WriteLine($"{records} record updated"); } WriteLine(); }
Deleting Records
Finally, let’s clean up the database and delete all records. You do this by retrieving all records and invoking the Remove or RemoveRange method to set the state of the objects in the context to deleted. Invoking the SaveChangesAsync method now deletes the records from the database and invokes SQL Delete statements for every object (code file BooksSample/Program.cs):
private async Task DeleteBooksAsync() { using (var context = new BooksContext()) { var books = context.Books; context.Books.RemoveRange(books); int records = await context.SaveChangesAsync(); WriteLine($"{records} records deleted"); } WriteLine(); }
NOTE An object-relational mapping tool such as Entity Framework is not useful with all scenarios. Deleting all objects was not done efficiently with the sample code. You can delete all records using a single SQL statement instead of one for every record. How this can be done is explained in Chapter 37, “ADO.NET.”
Now that you’ve seen how to add, query, update, and delete records, this chapter steps into features behind the scenes and gets into advanced scenarios using Entity Framework.
Using Dependency Injection
Entity Framework Core 1.0 has built-in support for dependency injection. Instead of defining the connection and the use of SQL Server with the DbContext derived class, the connection and SQL Server selection can be injected by using a dependency injection framework.
To see this in action, the previous sample has been modified with the BooksSampleWithDI sample project.
This sample makes use of the following dependencies and namespaces:
Dependencies
NETStandard.Library
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.Framework.DependencyInjection
Namespaces
Microsoft.EntityFrameworkCore System.Linq System.Threading.Tasks static System.Console
The BooksContext class now looks a lot simpler in just defining the Books property (code file BooksSampleWithDI/BooksContext.cs):
public class BooksContext: DbContext { public DbSet<Book> Books { get; set; } }
The BooksService is the new class that makes use of the BooksContext. Here, the BooksContext is injected via constructor injection. The methods AddBooksAsync and ReadBooks are very similar to these methods from the previous sample, but they use the context member of the BooksService class instead of creating a new one (code file BooksSampleWithDI/BooksService.cs):
public class BooksService { private readonly BooksContext _booksContext; public BooksService(BooksContext context) { _booksContext = context; } public async Task AddBooksAsync() { var b1 = new Book { Title ="Professional C# 5 and .NET 4.5.1", Publisher ="Wrox Press" }; var b2 = new Book { Title ="Professional C# 2012 and .NET 4.5", Publisher ="Wrox Press" }; var b3 = new Book { Title ="JavaScript for Kids", Publisher ="Wrox Press" }; var b4 = new Book { Title ="Web Design with HTML and CSS", Publisher ="For Dummies" }; _booksContext.AddRange(b1, b2, b3, b4); int records = await _booksContext.SaveChangesAsync(); WriteLine($"{records} records added"); } public void ReadBooks() { var books = _booksContext.Books; foreach (var b in books) { WriteLine($"{b.Title} {b.Publisher}"); } WriteLine(); } }
The container of the dependency injection framework is initialized in the InitializeServices method. Here, a ServiceCollection instance is created, and the BooksService class is added to this collection with a transient lifetime management. With this, the ServiceCollection is instantiated every time this service is requested. For registering Entity Framework and SQL Server, the extension methods AddEntityFramework, AddSqlServer, and AddDbContext are available. The AddDbContext method requires an Action delegate as parameter where a DbContextOptionsBuilder parameter is received. With this options parameter, the context can be configured using the UseSqlServer extension method. This is the similar functionality to register SQL Server with Entity Framework in the previous sample (code file BooksSampleWithDI/Program.cs):
private void InitializeServices() { const string ConnectionString =@"server= (localdb)MSSQLLocalDb;database=Books;trusted_connection=true"; var services = new ServiceCollection(); services.AddTransient<BooksService>(); services.AddEntityFramework() .AddSqlServer() .AddDbContext<BooksContext>(options => options.UseSqlServer(ConnectionString)); Container = services.BuildServiceProvider(); } public IServiceProvider Container { get; private set; }
The initialization of the services as well as the use of the BooksService is done from the Main method. The BooksService is retrieved invoking the GetService method of the IServiceProvider (code file BooksSampleWithDI/Program.cs):
static void Main() { var p = new Program(); p.InitializeServices(); var service = p.Container.GetService<BooksService>(); service.AddBooksAsync().Wait(); service.ReadBooks(); }
When you run the application, you can see that records are added and read from the Books database.
NOTE You can read more information about dependency injection and the Microsoft.Framework.DependencyInjection package in Chapter 31, “Patterns with XAML Apps,” and also see it in action in Chapter 40, “ASP.NET Core,” and Chapter 41, “ASP.NET MVC.”
Creating a Model
The first example of this chapter mapped a single table. The second example shows creating a relation between tables. Instead of creating the database with a SQL DDL statement (or by using the designer), in this section C# code is used to create the database.
The sample application MenusSample makes use of the following dependencies and namespaces:
Dependencies
NETStandard.Library
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
Namespaces
Microsoft.EntityFrameworkCore Microsoft.EntityFrameworkCore.ChangeTracking System System.Collections.Generic System.ComponentModel.DataAnnotations System.ComponentModel.DataAnnotations.Schema System.Linq System.Threading System.Threading.Tasks static System.Console
Creating a Relation
Let’s start creating a model. The sample project defines a one-to-many relation using the MenuCard and Menu types. The MenuCard contains a list of Menu objects. This relation is simply defined by the Menu property of type List<Menu> (code file MenusSample/MenuCard.cs):
public class MenuCard { public int MenuCardId { get; set; } public string Title { get; set; } public List<Menu> Menus { get; } = new List<Menu>(); public override string ToString() => Title; }
The relation can also be accessed in the other direction; a Menu can access the MenuCard using the MenuCard property. The MenuCardId property is specified to define a foreign key relationship (code file MenusSample/Menu.cs):
public class Menu { public int MenuId { get; set; } public string Text { get; set; } public decimal Price { get; set; } public int MenuCardId { get; set; } public MenuCard MenuCard { get; set; } public override string ToString() => Text; }
The mapping to the database is done by the MenusContext class. This class is defined similarly to the previous context type; it just contains two properties to map the two object types: the properties Menus and MenuCards (code file MenusSamples/MenusContext.cs):
public class MenusContext: DbContext { private const string ConnectionString = @"server=(localdb)MSSQLLocalDb;" + "Database=MenuCards;Trusted_Connection=True"; public DbSet<Menu> Menus { get; set; } public DbSet<MenuCard> MenuCards { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); optionsBuilder.UseSqlServer(ConnectionString); } }
Migrations with .NET CLI
To automatically create the database using C# code, the .NET CLI tools can be extended with the ef tools using the package dotnet-ef. This package contains commands to create C# code for the migration. The commands are made available by installing the dotnet-ef NuGet package. You install it by referencing this package from the tools section in the project configuration file (code file MenusSample/project.json):
"tools": { "dotnet-ef":"1.0.0-*" }
With the ef command in place, it offers the commands database, dbcontext, and migrations. The database command is used to upgrade the database to a specific migration state. The dbcontext command lists all DbContext derived types from the project (dbcontext list), and it creates context and entity from the database (dbcontext scaffold). The migrations command allows creating and removing migrations, as well as creating a SQL script to create the database with all the migrations. In case the production database should only be created and modified from the SQL administrator using SQL code, you can hand the generated script over to the SQL administrator.
To create an initial migration to create the database from code, the following command can be invoked from the developer command prompt. This command creates a migration named InitMenuCards:
>dotnet ef migrations add InitMenuCards
The command migrations add accesses the DbContext derived classes using
reflection and in turn the referenced model types. With this information, it creates two classes to create and update the database. With the Menu, MenuCard, and MenusContext classes, two classes are created, the MenusContextModelSnapshot and InitMenuCards. You can find both types in the Migrations folder after the command succeeds.
The MenusContextModelSnapshot class contains the current state of the model to build the database:
[DbContext(typeof(MenusContext))] partial class MenusContextModelSnapshot: ModelSnapshot { protected override void BuildModel(ModelBuilder modelBuilder) { modelBuilder .HasAnnotation("ProductVersion","7.0.0-rc1-16348") .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); modelBuilder.Entity("MenusSample.Menu", b => { b.Property<int>("MenuId") .ValueGeneratedOnAdd(); b.Property<int>("MenuCardId"); b.Property<decimal>("Price"); b.Property<string>("Text"); b.HasKey("MenuId"); }); modelBuilder.Entity("MenusSample.MenuCard", b => { b.Property<int>("MenuCardId") .ValueGeneratedOnAdd(); b.Property<string>("Title"); b.HasKey("MenuCardId"); }); modelBuilder.Entity("MenusSample.Menu", b => { b.HasOne("MenusSample.MenuCard") .WithMany() .HasForeignKey("MenuCardId"); }); } }
The InitMenuCards class defines Up and Down methods. The Up method lists all the actions that are needed to create the MenuCard and Menu tables including the primary keys, columns, and the relation. The Down method drops the two tables:
public partial class InitMenuCards: Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.CreateTable( name:"MenuCard", columns: table => new { MenuCardId = table.Column<int>(nullable: false) .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), Title = table.Column<string>(nullable: true) }, constraints: table => { table.PrimaryKey("PK_MenuCard", x => x.MenuCardId); }); migrationBuilder.CreateTable( name:"Menu", columns: table => new { MenuId = table.Column<int>(nullable: false) .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), MenuCardId = table.Column<int>(nullable: false), Price = table.Column<decimal>(nullable: false), Text = table.Column<string>(nullable: true) }, constraints: table => { table.PrimaryKey("PK_Menu", x => x.MenuId); table.ForeignKey( name:"FK_Menu_MenuCard_MenuCardId", column: x => x.MenuCardId, principalTable:"MenuCard", principalColumn:"MenuCardId", onDelete: ReferentialAction.Cascade); }); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropTable("Menu"); migrationBuilder.DropTable("MenuCard"); } }
NOTE With every change you’re doing, you can create another migration. The new migration only defines the changes needed to get from the previous version to the new version. In case a customer’s database needs to be updated from any earlier version, the necessary migrations are invoked when migrating the database.
During the development process, you don’t need all the migrations that you might create with the project, as no database with such interim states might exist. In that case you can remove a migration and create a new, larger one.
Migrations with MSBuild
In case you are using the Entity Framework migrations with MSBuild-based projects instead of DNX, the commands for migration are different. With full framework Console applications, WPF applications, or ASP.NET 4.6 project types, you need to specify migration commands in the NuGet Package Manager Console instead of the Developer Command Prompt. You start the Package Manager Console from Visual Studio via Tools ➪ Library Package Manager ➪ Package Manager Console.
With the Package Manager Console, you can use PowerShell scripts to add and remove migrations. The command
> Add-Migration InitMenuCards
creates a Migrations folder including the migration classes as shown before.
Creating the Database
Now, with the migrations types in place, the database can be created. The DbContext derived class MenusContext contains a Database property that returns a DatabaseFacade object. Using the DatabaseFacade, you can create and delete databases. The method EnsureCreated creates a database if it doesn’t exist. If the database already exists, nothing is done. The method EnsureDeletedAsync deletes the database. The following code snippet creates the database if it doesn’t exist (code file MenusSample/Program.cs):
private static async Task CreateDatabaseAsync() { using (var context = new MenusContext()) { bool created = await context.Database.EnsureCreatedAsync(); string createdText = created ?"created":"already exists"; WriteLine($"database {createdText}"); } }
NOTE In case the database exists but has an older schema version, the EnsureCreatedAsync method doesn’t apply schema changes. You can make schema upgrades by invoking the Migrate method. Migrate is an extension method to the DatabaseFacade class that is defined in the Microsoft.Data.Entity namespace.
When you run the program, the tables MenuCard and Menu are created. Based on default conventions, the tables have the same name as the entity types. Another convention is used on creating the primary key: the column MenuCardId is defined as primary key because the property name ended with Id.
CREATE TABLE [dbo].[MenuCard] ( [MenuCardId] INT IDENTITY (1, 1) NOT NULL, [Title] NVARCHAR (MAX) NULL, CONSTRAINT [PK_MenuCard] PRIMARY KEY CLUSTERED ([MenuCardId] ASC) );
The Menu table defines the MenuCardId that is a foreign key to the MenuCard table. Deleting a MenuCard also deletes all associated Menu rows because of the DELETE CASCADE:
CREATE TABLE [dbo].[Menu] ( [MenuId] INT IDENTITY (1, 1) NOT NULL, [MenuCardId] INT NOT NULL, [Price] DECIMAL (18, 2) NOT NULL, [Text] NVARCHAR (MAX) NULL, CONSTRAINT [PK_Menu] PRIMARY KEY CLUSTERED ([MenuId] ASC), CONSTRAINT [FK_Menu_MenuCard_MenuCardId] FOREIGN KEY ([MenuCardId]) REFERENCES [dbo].[MenuCard] ([MenuCardId]) ON DELETE CASCADE );
There are some parts in the creation code that would be useful to change. For example, the size of the Text and Title column could be reduced in size from NVARCHAR(MAX), SQL Server defines a Money type that could be used for the Price column, and the schema name could be changed from dbo. Entity Framework gives you two options to make these changes from code: data annotations and the Fluent API, which are both discussed next.
Data Annotations
One way to influence the generated database is to add data annotations to the entity types. The name of the tables can be changed by using the Table attribute. To change the schema name, the Table attribute defines the Schema property. To specify a different length for a string type, you can apply the MaxLength attribute (code file MenusWithDataAnnotations/MenuCard.cs):
[Table("MenuCards", Schema ="mc")] public class MenuCard { public int MenuCardId { get; set; } [MaxLength(120)] public string Title { get; set; } public List<Menu> Menus { get; } }
With the Menu class, the Table and MaxLength attributes are applied as well. To change the SQL type, the Column attribute can be used (code file MenusWithDataAnnotations/Menu.cs):
[Table("Menus", Schema ="mc")] public class Menu { public int MenuId { get; set; } [MaxLength(50)] public string Text { get; set; } [Column(TypeName ="Money")] public decimal Price { get; set; } public int MenuCardId { get; set; } public MenuCard MenuCard { get; set; } }
After applying the migrations and creating the database, you can see the new names of the tables with the schema name, as well as the changed data types on the Title, Text, and Price columns:
CREATE TABLE [mc].[MenuCards] ( [MenuCardId] INT IDENTITY (1, 1) NOT NULL, [Title] NVARCHAR (120) NULL, CONSTRAINT [PK_MenuCard] PRIMARY KEY CLUSTERED ([MenuCardId] ASC) ); CREATE TABLE [mc].[Menus] ( [MenuId] INT IDENTITY (1, 1) NOT NULL, [MenuCardId] INT NOT NULL, [Price] MONEY NOT NULL, [Text] NVARCHAR (50) NULL, CONSTRAINT [PK_Menu] PRIMARY KEY CLUSTERED ([MenuId] ASC), CONSTRAINT [FK_Menu_MenuCard_MenuCardId] FOREIGN KEY ([MenuCardId]) REFERENCES [mc].[MenuCards] ([MenuCardId]) ON DELETE CASCADE );
Fluent API
Another way to influence the tables created is to use the Fluent API with the OnModelCreating method of the DbContext derived class. Using this has the advantage that you can keep the entity types simple without adding any attributes, and the fluent API also gives you more options than you have with applying attributes.
The following code snippet shows the override of the OnModelCreating method of the BooksContext class. The ModelBuilder class that is received as parameter offers a few methods, and several extension methods are defined. The HasDefaultSchema is an extension method that applies a default schema to the model that is now used with all types. The Entity method returns an EntityTypeBuilder that enables you to customize the entity, such as mapping it to a specific table name and defining keys and indexes (code file MenusSample/MenusContext.cs):
protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.HasDefaultSchema("mc"); modelBuilder.Entity<MenuCard>() .ToTable("MenuCards") .HasKey(c => c.MenuCardId); // etc. modelBuilder.Entity<Menu>() .ToTable("Menus") .HasKey(m => m.MenuId); // etc. }
The EntityTypeBuilder defines a Property method to configure a property. The Property method returns a PropertyBuilder that in turn enables you to configure the property with max length values, required settings, and SQL types and to specify whether values should be automatically generated (such as identity columns):
protected override void OnModelCreating(ModelBuilder modelBuilder) { // etc. modelBuilder.Entity<MenuCard>() .Property<int>(c => c.MenuCardId) .ValueGeneratedOnAdd(); modelBuilder.Entity<MenuCard>() .Property<string>(c => c.Title) .HasMaxLength(50); modelBuilder.Entity<Menu>() .Property<int>(m => m.MenuId) .ValueGeneratedOnAdd(); modelBuilder.Entity<Menu>() .Property<string>(m => m.Text) .HasMaxLength(120); modelBuilder.Entity<Menu>() .Property<decimal>(m => m.Price) .HasColumnType("Money"); // etc. }
To define one-to-many mappings, the EntityTypeBuilder defines mapping methods. The method HasMany combined with WithOne defines a mapping of many menus with one menu card. HasMany needs to be chained with WithOne. The method HasOne needs a chain with WithMany or WithOne. Chaining HasOne with WithMany defines a one-to-many relationship; chaining HasOne with WithOne defines a one-to-one relationship:
protected override void OnModelCreating(ModelBuilder modelBuilder) { // etc. modelBuilder.Entity<MenuCard>() .HasMany(c => c.Menus) .WithOne(m => m.MenuCard); modelBuilder.Entity<Menu>() .HasOne(m => m.MenuCard) .WithMany(c => c.Menus) .HasForeignKey(m => m.MenuCardId); }
After creating the mapping in the OnModelCreating method, you can create migrations as shown before.
Scaffolding a Model from the Database
Instead of creating the database from the model, you can also create the model from the database.
To do this from a SQL Server database you have to add the EntityFramework.MicrosoftSqlServer.Design NuGet package in addition to the other packages to a DNX project. Then you can use the following command from the Developer Command Prompt:
> dnx ef dbcontext scaffold "server=(localdb)MSSQLLocalDb;database=SampleDatabase; trusted_connection=true""EntityFramework.MicrosoftSqlServer"
The dbcontext command enables you to list DbContext objects from the project, as well as create DBContext objects. The command scaffold creates DbContext-derived classes as well as model classes. The dnx ef dbcontext scaffold needs two required arguments: the connection string to the database and the provider that should be used. With the statement shown earlier, the database SampleDatabase was accessed on the SQL Server (localdb)MSSQLLocalDb. The provider used was EntityFramework.MicrosoftSqlServer. This NuGet package as well as the NuGet package with the same name and the Design postfix need to be added to the project.
After running this command, you can see the DbContext derived classes as well as the model types generated. The configuration of the model by default is done using the fluent API. However, you can change that to using the data annotations supplying the -a option. You can also influence the generated context class name as well as the output directory. Just check the different available options using the option -h.
Working with Object State
After creating the database, you can write to it. In the first sample you’ve written to a single table. What about writing a relationship?
Adding Objects with Relations
The following code snippet writes a relationship, a MenuCard containing Menu objects. Here, the MenuCard and Menu objects are instantiated. The bidirectional associations are assigned. With the Menu, the MenuCard property is assigned to the MenuCard, and with the MenuCard, the Menus property is filled with Menu objects. The MenuCard instance is added to the context invoking the Add method of the MenuCards property. When you add an object to the context, by default all objects are added to the tree with the state added. Not only the MenuCard but also the Menu objects are saved. IncludeDependents is set. With this option, all the associated Menu objects are added to the context as well. Invoking SaveChanged on the context now creates four records (code file MenusSample/Program.cs):
private static async Task AddRecordsAsync() { // etc. using (var context = new MenusContext()) { var soupCard = new MenuCard(); Menu[] soups = { new Menu { Text ="Consommé Célestine (with shredded pancake)", Price = 4.8m, MenuCard = soupCard }, new Menu { Text ="Baked Potato Soup", Price = 4.8m, MenuCard = soupCard }, new Menu { Text ="Cheddar Broccoli Soup", Price = 4.8m, MenuCard = soupCard }, }; soupCard.Title ="Soups"; soupCard.Menus.AddRange(soups); context.MenuCards.Add(soupCard); ShowState(context); int records = await context.SaveChangesAsync(); WriteLine($"{records} added"); // etc. }
The method ShowState that is invoked after adding the four objects to the context shows the state of all objects that are associated with the context. The DbContext class has a ChangeTracker associated that can be accessed using the ChangeTracker property. The Entries method of the ChangeTracker returns all the objects the change tracker knows about. With the foreach loop, every object including its state is written to the console (code file MenusSample/Program.cs):
public static void ShowState(MenusContext context) { foreach (EntityEntry entry in context.ChangeTracker.Entries()) { WriteLine($"type: {entry.Entity.GetType().Name}, state: {entry.State}," + $" {entry.Entity}"); } WriteLine(); }
Run the application to see the Added state with these four objects:
type: MenuCard, state: Added, Soups type: Menu, state: Added, Consommé Célestine (with shredded pancake) type: Menu, state: Added, Baked Potato Soup type: Menu, state: Added, Cheddar Broccoli Soup
Because of this state, the SaveChangesAsync method creates SQL Insert statements to write every object to the database.
Object Tracking
You’ve seen the context knows about added objects. However, the context also needs to know about changes. To know about changes, every object retrieved needs its state in the context. For seeing this in action let’s create two different queries that return the same object. The following code snippet defines two different queries where each query returns the same object with the menus as they are stored in the database. Indeed, only one object gets materialized, as with the second query result it is detected that the record returned has the same primary key value as an object already referenced from the context. Verifying whether the references of the variables m1 and m2 are the same results in returning the same object (code file MenusSample/Program.cs):
private static void ObjectTracking() { using (var context = new MenusContext()) { var m1 = (from m in context.Menus where m.Text.StartsWith("Con") select m).FirstOrDefault(); var m2 = (from m in context.Menus where m.Text.Contains("(") select m).FirstOrDefault(); if (object.ReferenceEquals(m1, m2)) { WriteLine("the same object"); } else { WriteLine("not the same"); } ShowState(context); } }
The first LINQ query results in a SQL SELECT statement with a LIKE comparison to compare for the string to start with the value Con:
SELECT TOP(1) [m].[MenuId], [m].[MenuCardId], [m].[Price], [m].[Text] FROM [mc].[Menus] AS [m] WHERE [m].[Text] LIKE 'Con' + '%'
With the second LINQ query, the database needs to be consulted as well. Here, a LIKE comparison is done to compare for a ( in the middle of the text:
SELECT TOP(1) [m].[MenuId], [m].[MenuCardId], [m].[Price], [m].[Text] FROM [mc].[Menus] AS [m] WHERE [m].[Text] LIKE ('%' + '(') + '%'
When you run the application, the same object is written to the console, and only one object is kept with the ChangeTracker. The state is Unchanged:
the same object type: Menu, state: Unchanged, Consommé Célestine (with shredded pancake)
To not track the objects running queries from the database, you can invoke the AsNoTracking method with the DbSet:
var m1 = (from m in context.Menus.AsNoTracking() where m.Text.StartsWith("Con") select m).FirstOrDefault();
You can also configure the default tracking behavior of the ChangeTracker to QueryTrackingBehavior.NoTracking:
using (var context = new MenusContext()) { context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
With such a configuration, two queries are made to the database, two objects are materialized, and the state information is empty.
NOTE Using the NoTracking configuration is useful when the context is used to only read records, but changes are not made. This reduces the overhead of the context as state information is not kept.
Updating Objects
As objects are tracked, they can be updated easily, as shown in the following code snippet. First, a Menu object is retrieved. With this tracked object, the price is modified before the change is written to the database. In between all changes, state information is written to the console (code file MenusSample/Program.cs):
private static async Task UpdateRecordsAsync() { using (var context = new MenusContext()) { Menu menu = await context.Menus .Skip(1) .FirstOrDefaultAsync(); ShowState(context); menu.Price += 0.2m; ShowState(context); int records = await context.SaveChangesAsync(); WriteLine($"{records} updated"); ShowState(context); } }
When you run the application, you can see that the state of the object is Unchanged after loading the record, Modified after the property value is changed, and Unchanged after saving is completed:
type: Menu, state: Unchanged, Baked Potato Soup type: Menu, state: Modified, Baked Potato Soup 1 updated type: Menu, state: Unchanged, Baked Potato Soup
When you access the entries from the change tracker, by default changes are automatically detected. You configure this by setting the AutoDetectChangesEnabled property of the ChangeTracker. For checking manually to see whether changes have been done, you invoke the method DetectChanges. With the invocation of SaveChangesAsync, the state is changed back to Unchanged. You can do this manually by invoking the method AcceptAllChanges.
Updating Untracked Objects
Object contexts are usually very short-lived. Using Entity Framework with ASP.NET MVC, with one HTTP request one object context is created to retrieve objects. When you receive an update from the client, the object must again be created on the server. This object is not associated with the object context. To update it in the database, the object needs to be associated with the data context, and the state changed to create an INSERT, UPDATE, or DELETE statement.
Such a scenario is simulated with the next code snippet. The GetMenuAsync method returns a Menu object that is disconnected from the context; the context is disposed at the end of the method (code file MenusSample/Program.cs):
private static async Task<Menu> GetMenuAsync() { using (var context = new MenusContext()) { Menu menu = await context.Menus .Skip(2) .FirstOrDefaultAsync(); return menu; } }
The GetMenuAsync method is invoked by the method ChangeUntrackedAsync. This method changes the Menu object that is not associated with any context. After the change, the Menu object is passed to the method UpdateUntrackedAsync to save it in the database (code file MenusSample/Program.cs):
private static async Task ChangeUntrackedAsync() { Menu m = await GetMenuAsync(); m.Price += 0.7m; await UpdateUntrackedAsync(m); }
The method UpdateUntrackedAsync receives the updated object and needs to attach it with the context. One way to attach an object with the context is by invoking the Attach method of the DbSet, and set the state as needed. The Update method does both with one call: attaching the object and setting the state to Modified (code file MenusSample/Program.cs):
private static async Task UpdateUntrackedAsync(Menu m) { using (var context = new MenusContext()) { ShowState(context); // EntityEntry<Menu> entry = context.Menus.Attach(m); // entry.State = EntityState.Modified; context.Menus.Update(m); ShowState(context); await context.SaveChangesAsync(); } }
When you run the application with the ChangeUntrackedAsync method, you can see that the state is modified. The object was untracked at first, but because the state was explicitly updated, you can see the Modified state:
type: Menu, state: Modified, Cheddar Broccoli Soup
Conflict Handling
What if multiple users change the same record and then save the state? Who will win with the changes?
If multiple users accessing the same database work on different records, there’s no conflict. All users can save their data without interfering with data edited by other users. If multiple users work on the same record, though, you need to give some thought to conflict resolution. You have different ways to deal with this. The easiest one is that the last one wins. The user saving the data last overwrites changes from the user that did the changes previously.
Entity Framework also offers a way for letting the first one win. With this option, when saving a record, a verification is needed if the data originally read is still in the database. If this is the case, saving data can continue as no changes occurred between reading and writing. However, if the data changed, a conflict resolution needs to be done.
Let’s get into these different options.
The Last One Wins
The default scenario is that the last one saving changes wins. To see multiple accesses to the database, the BooksSample application is extended.
For an easy simulation of two users, the method ConflictHandlingAsync invokes the method PrepareUpdateAsync two times, makes different changes to two Book objects that reference the same record, and invokes the UpdateAsync method two times. Last, the book ID is passed to the CheckUpdateAsync method, which shows the actual state of the book from the database (code file BooksSample/Program.cs):
public static async Task ConflictHandlingAsync() { // user 1 Tuple<BooksContext, Book> tuple1 = await PrepareUpdateAsync(); tuple1.Item2.Title ="updated from user 1"; // user 2 Tuple<BooksContext, Book> tuple2 = await PrepareUpdateAsync(); tuple2.Item2.Title ="updated from user 2"; // user 1 await UpdateAsync(tuple1.Item1, tuple1.Item2); // user 2 await UpdateAsync(tuple2.Item1, tuple2.Item2); context1.Item1.Dispose(); context2.Item1.Dispose(); await CheckUpdateAsync(tuple1.Item2.BookId); }
The PrepareUpdateAsync method opens a BookContext and returns both the context and the book within a Tuple object. Remember, this method is invoked two times, and different Book objects associated with different context objects are returned (code file BooksSample/Program.cs):
private static async Task<Tuple<BooksContext, Book>> PrepareUpdateAsync() { var context = new BooksContext(); Book book = await context.Books .Where(b => b.Title =="Conflict Handling") .FirstOrDefaultAsync(); return Tuple.Create(context, book); }
NOTETuples are explained in Chapter 7, “Arrays and Tuples.”
The UpdateAsync method receives the opened BooksContext with the updated Book object to save the book to the database. Remember, this method is invoked two times as well (code file BooksSample/Program.cs):
private static async Task UpdateAsync(BooksContext context, Book book) { await context.SaveChangesAsync(); WriteLine($"successfully written to the database: id {book.BookId}" + $"with title {book.Title}"); }
The CheckUpdateAsync method writes the book with the specified id to the console (code file BooksSample/Program.cs):
private static async Task CheckUpdateAsync(int id) { using (var context = new BooksContext()) { Book book = await context.Books .Where(b => b.BookId == id) .FirstOrDefaultAsync(); WriteLine($"updated: {book.Title}"); } }
What happens when you run the application? You see the first update is successful, and so is the second update. When updating a record, it is not verified whether any changes happened after reading the record, which is the case with this sample application. The second update just overwrites the data from the first update, as you can see with the application output:
successfully written to the database: id 7038 with title updated from user 1 successfully written to the database: id 7038 with title updated from user 2 updated: updated from user 2
The First One Wins
In case you need a different behavior, such as the first user’s changes being saved to the record, you need to do some changes. The sample project ConflictHandlingSample uses the Book and BookContext objects like before, but it deals with the first-one-wins scenario.
This sample application makes use of the following dependencies and namespaces:
Dependencies
NETStandard.Library
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
Namespaces
Microsoft.EntityFrameworkCore Microsoft.EntityFrameworkCore.ChangeTracking System System.Linq System.Text System.Threading.Tasks static System.Console
For conflict resolution, you need to specify the properties that should be verified if any change happened between reading and updating with a concurrency token. Based on the property you specify, the SQL UPDATE statement is modified to verify not only for the primary key, but also all properties that are marked with the concurrency token. Adding many concurrency tokens to the entity type creates a huge WHERE clause with the UPDATE statement, which is not very efficient. Instead you can add a property that is updated from SQL Server with every UPDATE statement—and this is what’s done with the Book class. The property TimeStamp is defined as timeStamp in SQL Server (code file ConflictHandlingSample/Book.cs):
public class Book { public int BookId { get; set; } public string Title { get; set; } public string Publisher { get; set; } public byte[] TimeStamp { get; set; } }
To define the TimeStamp property as a timestamp type in SQL Server, you use the Fluent API. The SQL data type is defined using the HasColumnType method. The method ValueGeneratedOnAddOrUpdate informs the context that with every SQL INSERT or UPDATE statement the TimeStamp property can change, and it needs to be set with the context after these operations. The IsConcurrencyToken method marks this property as required to check whether it didn’t change after reading it (code file ConflictHandlingSample/BooksContext.cs):
protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); var book = modelBuilder.Entity<Book>(); book.HasKey(p => p.BookId); book.Property(p => p.Title).HasMaxLength(120).IsRequired(); book.Property(p => p.Publisher).HasMaxLength(50); book.Property(p => p.TimeStamp) .HasColumnType("timestamp") .ValueGeneratedOnAddOrUpdate() .IsConcurrencyToken(); }
NOTE Instead of using the IsConcurrencyToken method with the Fluent API, you can also apply the attribute ConcurrencyCheck to the property where concurrency should be checked.
The process of the conflict-handling check is similar to what was done before. Both user 1 and user 2 invoke the PrepareUpdateAsync method, change the book title, and call the UpdateAsync method to make the change in the database (code file ConflictHandlingSample/Program.cs):
public static async Task ConflictHandlingAsync() { // user 1 Tuple<BooksContext, Book> tuple1 = await PrepareUpdateAsync(); tuple1.Item2.Title ="user 1 wins"; // user 2 Tuple<BooksContext, Book> tuple2 = await PrepareUpdateAsync(); tuple2.Item2.Title ="user 2 wins"; // user 1 await UpdateAsync(tuple1.Item1, tuple1.Item2); // user 2 await UpdateAsync(tuple2.Item1, tuple2.Item2); context1.Item1.Dispose(); context2.Item1.Dispose(); await CheckUpdateAsync(context1.Item2.BookId); }
The PrepareUpdateAsync method is not repeated here, as this method is implemented in the same way as with the previous sample. What’s quite different is the UpdateAsync method. To see the different timestamps, before and after the update, a custom extension method StringOutput for the byte array is implemented that writes the byte array in a readable form to the console. Next, the changes of the Book object are shown calling the ShowChanges helper method. The SaveChangesAsync method is invoked to write all updates to the database. In case the update fails with a DbUpdateConcurrencyException, information is written to the console about the failure (code file ConflictHandlingSample/Program.cs):
private static async Task UpdateAsync(BooksContext context, Book book, string user) { try { WriteLine($"{user}: updating id {book.BookId}," + $"timestamp: {book.TimeStamp.StringOutput()}");ShowChanges(book.BookId, context.Entry(book)); int records = await context.SaveChangesAsync(); WriteLine($"{user}: updated {book.TimeStamp.StringOutput()}"); WriteLine($"{user}: {records} record(s) updated while updating" + $"{book.Title}"); } catch (DbUpdateConcurrencyException ex) { WriteLine($"{user}: update failed with {book.Title}"); WriteLine($"error: {ex.Message}"); foreach (var entry in ex.Entries) { Book b = entry.Entity as Book; WriteLine($"{b.Title} {b.TimeStamp.StringOutput()}"); ShowChanges(book.BookId, context.Entry(book)); } } }
With objects that are associated with the context, you can access the original values and the current values with a PropertyEntry object. The original values that were retrieved when reading the object from the database can be accessed with the OriginalValue property, the current values with the CurrentValue property. The PropertyEntry object can be accessed with the Property method of an EntityEntry as shown in the ShowChanges and ShowChange methods (code file ConflictHandlingSample/Program.cs):
private static void ShowChanges(int id, EntityEntry entity) { ShowChange(id, entity.Property("Title")); ShowChange(id, entity.Property("Publisher")); } private static void ShowChange(int id, PropertyEntry propertyEntry) { WriteLine($"id: {id}, current: {propertyEntry.CurrentValue}," + $"original: {propertyEntry.OriginalValue}," + $"modified: {propertyEntry.IsModified}"); }
To convert the byte array of the TimeStamp property that is updated from SQL Server for visual output, the extension method StringOutput is defined (code file ConflictHandlingSample/Program.cs):
static class ByteArrayExtension { public static string StringOutput(this byte[] data) { var sb = new StringBuilder(); foreach (byte b in data) { sb.Append($"{b}."); } return sb.ToString(); } }
When you run the application, you can see output such as the following. The timestamp values and book IDs differ with every run. The first user updates the book with the original title sample book to the new title user 1 wins. The IsModified property returns true for the Title property but false for the Publisher property, as only the title changed. The original timestamp ends with 1.1.209; after the update to the database the timestamp is changed to 1.17.114. In the meantime, user 2 opened the same record; this book still has a timestamp of 1.1.209. User 2 updates this book, but here the update failed because the timestamp of this book does not match the timestamp from the database. Here, a DbUpdateConcurrencyException exception is thrown. In the exception handler, the reason of the exception is written to the console as you can see in the program output:
user 1: updating id 17, timestamp 0.0.0.0.0.1.1.209. id: 17, current: user 1 wins, original: sample book, modified: True id: 17, current: Sample, original: Sample, modified: False user 1: updated 0.0.0.0.0.1.17.114. user 1: 1 record(s) updated while updating user 1 wins user 2: updating id 17, timestamp 0.0.0.0.0.1.1.209. id: 17, current: user 2 wins, original: sample book, modified: True id: 17, current: Sample, original: Sample, modified: False user 2 update failed with user 2 wins user 2 error: Database operation expected to affect 1 row(s) but actually affected 0 row(s).
Data may have been modified or deleted since entities were loaded.
See http://go.microsoft.com/fwlink/?LinkId=527962 for information on understanding and handling optimistic concurrency exceptions.
user 2 wins 0.0.0.0.0.1.1.209. id: 17, current: user 2 wins, original: sample book, modified: True id: 17, current: Sample, original: Sample, modified: False updated: user 1 wins
When using concurrency tokens and handling the DbConcurrencyException, you can deal with concurrency conflicts as needed. You can, for example, automatically resolve concurrency issues. If different properties are changed, you can retrieve the changed record and merge the changes. If the property changed is a number where you do some calculations—for example, a point system—you can increment or decrement the values from both updates and just throw an exception if a limit is reached. You can also ask the user to resolve the concurrency issue by giving the user the information that’s currently in the database and ask what changes he or she would like to do. Just don’t ask too much from the user. It’s likely that the only thing the user wants is to get rid of this rarely shown dialog, which means he or she might click OK or Cancel without reading the content. For rare conflicts, you can also write logs and inform the system administrator that an issue needs to be resolved.
Using Transactions
Chapter 37 introduces programming with transactions. With every access of the database using the Entity Framework, a transaction is involved, too. You can use transactions implicitly or create them explicitly with configurations as needed. The sample project used with this section demonstrates transactions in both ways. Here, the Menu, MenuCard, and MenuContext classes are used as shown earlier with the MenusSample project. This sample application makes use of following dependencies and namespaces:
Dependencies
NETStandard.Library
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.SqlServer
Namespaces
Microsoft.EntityFrameworkCore Microsoft.EntityFrameworkCore.Storage System.Linq System.Threading System.Threading.Tasks static System.Console
Using Implicit Transactions
An invocation of the SaveChangesAsync method automatically resolves to one transaction. If one part of the changes that need to be done fails—for example, because of a database constraint—all the changes already done are rolled back. This is demonstrated with the following code snippet. Here, the first Menu (m1) is created with valid data. A reference to an existing MenuCard is done by supplying the MenuCardId. After the update succeeds, the MenuCard property of the Menu m1 is filled automatically. However, the second Menu created, mInvalid, references an invalid menu card by supplying a MenuCardId that is one value higher than the highest ID available in the database. Because of the defined foreign key relation between MenuCard and Menu, adding this object will fail (code file TransactionsSample/Program.cs):
private static async Task AddTwoRecordsWithOneTxAsync() { WriteLine(nameof(AddTwoRecordsWithOneTxAsync)); try { using (var context = new MenusContext()) { var card = context.MenuCards.First(); var m1 = new Menu { MenuCardId = card.MenuCardId, Text ="added", Price = 99.99m }; int hightestCardId = await context.MenuCards.MaxAsync(c => c.MenuCardId); var mInvalid = new Menu { MenuCardId = ++hightestCardId, Text ="invalid", Price = 999.99m }; context.Menus.AddRange(m1, mInvalid); int records = await context.SaveChangesAsync(); WriteLine($"{records} records added"); } } catch (DbUpdateException ex) { WriteLine($"{ex.Message}"); WriteLine($"{ex?.InnerException.Message}"); } WriteLine(); }
After running the application invoking the method AddTwoRecordsWithOneTxAsync, you can verify the content of the database to see that not a single record was added. The exception message as well as the message of the inner exception gives the details:
AddTwoRecordsWithOneTxAsync An error occurred while updating the entries. See the inner exception for details. The INSERT statement conflicted with the FOREIGN KEY constraint"FK_Menu_MenuCard_MenuCardId". The conflict occurred in database"MenuCards", table"mc.MenuCards", column 'MenuCardId'.
In case writing the first record to the database should be successful even if the second record write fails, you have to invoke the SaveChangesAsync method multiple times as shown in the following code snippet. In the method AddTwoRecordsWithTwoTxAsync, the first invocation of SaveChangesAsync inserts the m1 Menu object, whereas the second invocation tries to insert the mInvalid Menu object (code file TransactionsSample/Program.cs):
private static async Task AddTwoRecordsWithTwoTxAsync() { WriteLine(nameof(AddTwoRecordsWithTwoTxAsync)); try { using (var context = new MenusContext()) { var card = context.MenuCards.First(); var m1 = new Menu { MenuCardId = card.MenuCardId, Text ="added", Price = 99.99m }; context.Menus.Add(m1); int records = await context.SaveChangesAsync(); WriteLine($"{records} records added"); int hightestCardId = await context.MenuCards.MaxAsync(c => c.MenuCardId); var mInvalid = new Menu { MenuCardId = ++hightestCardId, Text ="invalid", Price = 999.99m }; context.Menus.Add(mInvalid); records = await context.SaveChangesAsync(); WriteLine($"{records} records added"); } } catch (DbUpdateException ex) { WriteLine($"{ex.Message}"); WriteLine($"{ex?.InnerException.Message}"); } WriteLine(); }
When you run the application, adding the first INSERT statement succeeds, but of course the second one results in a DbUpdateException. You can verify the database to see that one record was added this time:
AddTwoRecordsWithTwoTxAsync 1 records added An error occurred while updating the entries. See the inner exception for details. The INSERT statement conflicted with the FOREIGN KEY constraint"FK_Menu_MenuCard_MenuCardId". The conflict occurred in database"MenuCards", table"mc.MenuCards", column 'MenuCardId'.
Creating Explicit Transactions
Instead of using implicitly created transactions, you can also create them explicitly. This gives you the advantage of also having the option to roll back in case some of your business logic fails, and you can combine multiple invocations of SaveChangesAsync within one transaction. To start a transaction that is associated with the DbContext derived class, you need to invoke the BeginTransactionAsync method of the DatabaseFacade class that is returned from the Database property. The transaction returned implements the interface IDbContextTransaction. The SQL statements done with the associated DbContext are enlisted with the transaction. To commit or roll back, you have to explicitly invoke the methods Commit or Rollback. In the sample code, Commit is done when the end of the DbContext scope is reached; Rollback is done in cases where an exception occurs (code file TransactionsSample/Program.cs):
private static async Task TwoSaveChangesWithOneTxAsync() { WriteLine(nameof(TwoSaveChangesWithOneTxAsync)); IDbContextTransaction tx = null; try { using (var context = new MenusContext()) using (tx = await context.Database.BeginTransactionAsync()) { var card = context.MenuCards.First(); var m1 = new Menu { MenuCardId = card.MenuCardId, Text ="added with explicit tx", Price = 99.99m }; context.Menus.Add(m1); int records = await context.SaveChangesAsync(); WriteLine($"{records} records added"); int hightestCardId = await context.MenuCards.MaxAsync(c => c.MenuCardId); var mInvalid = new Menu { MenuCardId = ++hightestCardId, Text ="invalid", Price = 999.99m }; context.Menus.Add(mInvalid); records = await context.SaveChangesAsync(); WriteLine($"{records} records added"); tx.Commit(); } } catch (DbUpdateException ex) { WriteLine($"{ex.Message}"); WriteLine($"{ex?.InnerException.Message}"); WriteLine("rolling back…"); tx.Rollback(); } WriteLine(); }
When you run the application, you can see that no records have been added, although the SaveChangesAsync method was invoked multiple times. The first return of SaveChangesAsync lists one record as being added, but this record is removed based on the Rollback later on. Depending on the setting of the isolation level, the updated record can only be seen before the rollback was done within the transaction, but not outside the transaction.
TwoSaveChangesWithOneTxAsync 1 records added An error occurred while updating the entries. See the inner exception for details. The INSERT statement conflicted with the FOREIGN KEY constraint"FK_Menu_MenuCard_MenuCardId". The conflict occurred in database"MenuCards", table"mc.MenuCards", column 'MenuCardId'. rolling back…
NOTEWith the BeginTransactionAsync method, you can also supply a value for the isolation level to specify the isolation requirements and locks needed in the database. Isolation levels are discussed in Chapter 37.
Summary
This chapter introduced you to the features of the Entity Framework Core. You’ve learned how the object context keeps knowledge about entities retrieved and updated, and how changes can be written to the database. You’ve also seen how migrations can be used to create and change the database schema from C# code. To define the schema, you've seen how the database mapping can be done using data annotations, and you've also seen the fluent API that offers more features compared to the annotations.
You’ve seen possibilities for reacting to conflicts when multiple users work on the same record, as well as using transactions implicitly or explicitly for more transactional control.
The next chapter shows using Windows Services to create a program that automatically starts with the system. You can make use of Entity Framework within Windows Services.