Introduction

Entity Framework Core provides data access for Windows, web and mobile projects working as an object-relational mapper (O/RM) which supports many database engines including SQL-Server, Microsoft Access, Oracle, MySQL to name the top supported database engines. Entity Framework Core was born from a mature Entity Framework 6 with a multitude of new features and more ways to control working with data. With increased flexibility for working with data does not always make working with data easy especially with performance. Below there are tips and tricks to assist with efficiently working with Entity Framework Core overall but is not all that is needed to work with Entity Framework Core, consider these tips and tricks at start for getting started with Entity Framework. 

Prerequisite

Although Entity Framework is capable of creating a database and populate tables with data for novice developers the best way to learn Entity Framework is to first understand relational databases and how they work as Entity Framework can easily reverse engineer your databases while using Entity Framework Core to create databases and tables means knowing how to create related tables correctly. By allowing Entity Framework Core to work with an existing databases will reduce any possible frustrations as when there are issues one must ask, is it the code to generate the database or the code to interact with the database? Working from existing databases is easier.
  • Basics for creating relational databases, most database solutions require more than one table and have more than one table related to another table.
  • A tool to create databases, tables and queries, for SQL-Server SSMS (SQL-Server Management Studio), Oracle, SQL Developer, Microsoft Access etc.
    • Learn the basics of a good database design which includes primary keys and indexes as this will affect performance of data operations with Entity Framework Core.
    • Have a decent understanding of SQL, the following resource provides all the basics needed to assist with learning SQL. 
  • Basic understanding of LINQ and/or Lambda standard query operators, see Microsoft documentation on Projection operators.

First steps

Once a database with tables and data have been created to interact with them classes must be generated which is known as reverse engineering. Reverse engineering is the process of scaffolding entity type classes and a DbContext class based on a database schema. It can be performed using the Scaffold-DbContext command of the EF Core Package Manager Console (PMC) tools or the dotnet ef dbcontext scaffold command of the .NET Command-line Interface (CLI) tools.

Reverse engineering even with standard tools is not an easy task as the process is done via the Package Manager console within Visual Studio. For a pleasant first time experience use the following tool with source code or the following Visual Studio extension, EFCorePowerTools

Tip 1 code placement

Place all Entity Framework code into a class project so that if more than one front end project needs to use the code they can have a reference to the class project to access the code.

Tip 2 reverse engineering

When reverse engineering, a class will be created for controlling access to data, see the following for an example. This class will be placed in the root folder of the project which is fine unless there will be more than one database included in the project. If there will be multiple databases create a folder e.g. Context and move the classes to this folder for organization purposes. Optionally change the namespace to reflect the folder name which means each of the classes created in the reverse engineering process mus reference that namespace. Before doing this create a Model folder for each database and move those classes into the folder then add a using statement to reference back to the context class, see the attached source code for examples.

Tip 3 AsNoTracking

By default all EF Core select queries are tracked for changes made, in some cases there is no need for this such as populating a reference table into a ListBox or ComboBox. If there are a good deal of records use AsNoTracking which will not track changes to the data returned from the database. In the following example AsNoTracking speeds up reading data.

public static async Task<List<CustomerLister>> CustomerDisplay(this NorthwindContext context)
{
    return await context.Customers
        .AsNoTracking()
        .Select(customer => new CustomerLister
        {
            Name = customer.CompanyName,
            Id = customer.CustomerIdentifier
        }).ToListAsync();
}

Tip 4 common misconceptions 

Only returned data needed to perform an operation. In conventional data operations developers tend to use SELECT * FROM some table which means more data than needed is read which can affect performance of the application. For novice developers first starting out with EF Core the same thing is likely to happen, instead create a class which represents the data needed.

Example, a requirement is to show a company name in a ListBox with the primary key to display in the ListBox.  The following code is like SELECT *,
public List<Customer> GetCustomersForListBox()
{
    using (var context = new NorthwindContext())
    {
        return context.Customers.ToList();
    }
}

And this is what is the definition of the Customer class, more than needed to populate a ListBox. And to show the Company name DisplayMember must be set on the ListBox.
public partial class Customer
{
    public Customer()
    {
        Orders = new HashSet<Order>();
    }
 
    public int CustomerIdentifier { get; set; }
    [Required]
    [StringLength(40)]
    public string CompanyName { get; set; }
    [StringLength(30)]
    public string ContactName { get; set; }
    public int? ContactId { get; set; }
    [StringLength(60)]
    public string Address { get; set; }
    [StringLength(15)]
    public string City { get; set; }
    [StringLength(15)]
    public string Region { get; set; }
    [StringLength(10)]
    public string PostalCode { get; set; }
    public int? CountryIdentifier { get; set; }
    [StringLength(24)]
    public string Phone { get; set; }
    [StringLength(24)]
    public string Fax { get; set; }
    public int? ContactTypeIdentifier { get; set; }
    public DateTime? ModifiedDate { get; set; }
 
    [ForeignKey("ContactId")]
    [InverseProperty("Customers")]
    public virtual Contact Contact { get; set; }
    [ForeignKey("ContactTypeIdentifier")]
    [InverseProperty("Customers")]
    public virtual ContactType ContactTypeIdentifierNavigation { get; set; }
    [ForeignKey("CountryIdentifier")]
    [InverseProperty("Customers")]
    public virtual Country CountryIdentifierNavigation { get; set; }
    [InverseProperty("CustomerIdentifierNavigation")]
    public virtual ICollection<Order> Orders { get; set; }
}

Using the following class only returns the data needed to populate a ListBox plus by overriding ToString this takes the place of setting DisplayMember for the ListBox.

public class CustomerLister
{
    public string Name { get; set; }
    public int Id { get; set; }
    public override string ToString() => Name;
}

In the form
List<CustomerLister> results = await context.CustomerDisplay();
CustomerListBox.DataSource = results;


Tip 5 understanding LINQ/Lambda

By understanding basics of LINQ/Lambda operators developers can avoid many performance issues. For instance in the following query too much time is taken. The issue is that data is read into via .ToList which returns all records for the table then the .FirstOrDefault is applied against .ToList.
public Customer GetCustomerByIdentifier(int customerIdentfier)
{
    using (var context = new NorthwindContext())
    {
        return context.Customers.ToList()
            .FirstOrDefault(customer =>
                customer.CustomerIdentifier == customerIdentfier);
    }
}

To fix this eliminate the .ToList which then means the FirstOrDefault is going to return only one record or null if the record is not found.
public Customer GetCustomerByIdentifier(int customerIdentfier)
{
    using (var context = new NorthwindContext())
    {
        return context.Customers
            .FirstOrDefault(customer =>
                customer.CustomerIdentifier == customerIdentfier);
    }
}

A very common mistaken which is similar to using .ToList then .FirstOrDefault is to use a predicate in a Where then use FirstOrDefault e.g.
public Customer GetCustomerByIdentifier(int customerIdentfier)
{
    using (var context = new NorthwindContext())
    {
 
        // ReSharper disable once ReplaceWithSingleCallToFirstOrDefault
        return context.Customers
            .Where(customer => customer.CustomerIdentifier == customerIdentfier)
            .FirstOrDefault();
    }
}

There is no need for Where as the FirstOrDefault extension accepts a predicate.
public Customer GetCustomerByIdentifier(int customerIdentfier)
{
    using (var context = new NorthwindContext())
    {
        return context.Customers.ToList()
            .FirstOrDefault(customer =>
                customer.CustomerIdentifier == customerIdentfier);
    }
}

Tip 6 eager loading

Since Entity Framework Core is eager loading related data is not returned by default.  To load related data Entity Framework Core has the .Include extension method. This means each time related data is required Include or ThenInclude must be used as shown below.

var customer = await context.Customers
    .Include(cust => cust.CountryIdentifierNavigation)
    .Include(cust => cust.Contact)
    .Include(cust => cust.ContactTypeIdentifierNavigation)
    .FirstOrDefaultAsync(cust => cust.CustomerIdentifier == customerIdentifier);

This means code is cluttered up and when used in multiple locations if a requirement changes each instance must be changed. Use an extension method instead as per below.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using NorthWindCoreLibrary.Contexts;
using NorthWindCoreLibrary.HelperClasses;
using NorthWindCoreLibrary.Models;
 
namespace NorthWindCoreLibrary.Extensions
{
    public static class CustomerExtensions
    {
        public static async Task<Customer> CustomerPartial(
            this NorthwindContext context, int customerIdentifier)
        {
            return  await context.Customers
                .AsNoTracking()
                .Include(customer => customer.CountryIdentifierNavigation)
                .Include(customer => customer.Contact)
                .Include(customer => customer.ContactTypeIdentifierNavigation)
                .FirstOrDefaultAsync(cust => cust.CustomerIdentifier == customerIdentifier);
        }
    }
}

Code is now reduced to the following.

var customer = await context.CustomerPartial(customerIdentifier);

Some developer do not like to use language extension methods, in this case a static method can be used.

using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using NorthWindCoreLibrary.Contexts;
using NorthWindCoreLibrary.Models;
 
namespace NorthWindCoreLibrary.HelperClasses
{
    public class CustomerHelpers
    {
        public static async Task<Customer> CustomerEntityAsync(
            NorthwindContext context, int customerIdentifier)
        {
            return await context.Customers
                .Include(customer => customer.CountryIdentifierNavigation)
                .Include(customer => customer.Contact)
                .Include(customer => customer.ContactTypeIdentifierNavigation)
                .FirstOrDefaultAsync(cust => cust.CustomerIdentifier == customerIdentifier);
        }
    }
}

Then as with the language extension clutter is gone and easy to maintain.

var customer = await CustomerHelpers.CustomerEntityAsync(context, customerIdentifier);

Tip 7 logging

At one point or another code will not perform as expected or there will be a runtime exception that just can not be figured out. In these cases use a LoggerFactory. To log to Visual Studio output window install Microsoft.Extensions.Logging from NuGet. Next add the following line of code to the DbContext class. See the following example.

public static readonly ILoggerFactory LoggerFactory = new LoggerFactory().AddConsole((_, ___) => true);

Next i OnConfiguring method in the DbContext class add the following property which permits using or not using logging in client code.

public bool Diagnostics { get; set; }

Next configure OnConfiguring as shown here which allows turning logging on/off. In a form with a CheckBox to control logging.
using (var context = new NorthwindContext())
{
    context.Diagnostics = LogConsoleCheckBox.Checked;
    var customer = await context.CustomerPartial(customerIdentifier);
}

Now when logging is enabled make sure the IDE Output window is open to see diagnostics. 

Important: Unless there is a good reason make sure when an application goes to production built in release mode use the following to turn off logging.
#if DEBUG
   context.Diagnostics = LogConsoleCheckBox.Checked;
#endif

Lastly, depending on the release of Entity Framework Core used the implementation of logging may change, see the following for specific versions.

Tip 8 asserting connections

In some cases such as a remote client setup via a installation program there is always the possibility of connection issues. Consider using assertion and display a screen while performing the initial operation for Entity Framework. In the screenshot below (include in the attached source code) is a animated gif to indicate something is going on to the user.  The first operation uses CanConnectAsync to ensure a connection can be made to the database. CanConnectAsync has an overload which accepts a CancellationToken also.



Code to implement checking a connection following by loading a ListBox with data.
private async void Form1_Shown(object sender, EventArgs e)
{
    var waitForm = new ConnectStatusForm() {Top = Top, Left = Left, TopMost = true};
 
    await Task.Delay(20);
 
    waitForm.Show();
 
    await Task.Delay(10);
 
    using (var context = new NorthwindContext())
    {
 
        // set to true for logging Entity Framework operations
        // for this instance of the DbContext.
        // context.Diagnostics = true;
        
        try
        {
            if (await context.Database.CanConnectAsync())
            {
                var results = await context.CustomerDisplay();
                CustomerListBox.DataSource = results;
 
                IncludeStatementsConventionalButton.Enabled = true;
                IncludeStatementsUsingExtensionButton.Enabled = true;
 
            }
            else
            {
                waitForm.Close();                      
                MessageBox.Show("Failed to connection to server, please check the connection string.");
            }
        }
        catch (Exception ex)
        {
            waitForm.Close();
            MessageBox.Show(ex.Message);
        }
        finally
        {
            waitForm.Dispose();
        }
 
    }
 
}

Tip 9 asynchronous

If you’re not doing it already, start using async and await to increase performance of your application. Especially on I/O intensive operations like heavier SQL queries this will allow your application to respond to other requests while waiting for the previous operations to complete. For many Windows desktop developers this is a foreign concept which when working with Entity Framework Core is easy as many of the methods have both synchronous and asynchronous counterparts.

public Customer GetCustomerByIdentifier(int customerIdentfier)
{
    using (var context = new NorthwindContext())
    {
        return context.Customers
            .FirstOrDefault(customer =>
                customer.CustomerIdentifier == customerIdentfier);
    }
}
public async Task<Customer> GetCustomerByIdentifierAsync(int customerIdentfier)
{
    using (var context = new NorthwindContext())
    {
        return await context.Customers
            .FirstOrDefaultAsync(customer =>
                customer.CustomerIdentifier == customerIdentfier);
    }
}

Note the minor differences



Tip 10 connection string

Be careful with Connection string as the  default is to store connection string in the DbContext class but can be placed into app.config under the ConnectionStrings section which means they are exposed and may be manipulated or allow a user to connect to the database outside of the application. This may also be problematic when there are user names and passwords. Unfortunately Windows Forms does not have the same level of security as ASP.NET projects have vis the secret manager tool so be careful with setting up a connection string.

See the following project included in the attached source for ideas on securing a connection string.

Tip 11 unit testing

Write unit test for all Entity Framework Core queries, by writing unit test this allows a developer to know each query works as expected. When implementing queries into the front end there may be unexpected results which may be the fault of a untested query or how the query is used. By having appropriate unit test methods can assist in diagnosing problems. The only downside is that test methods tak a considerable amount of time to rig up but are well worth time spent later down the road.

Entity Framework Core provides in-memory testing via SqlLite which is not the same as testing against a real database and if considering using in-memory testing this is not foolproof. 

Tip 12 must checks

Make sure
  • Each and every table has an auto-incrementing primary key as Entity Framework will not function in regards to inserts without this.
  • Ensure before reverse engineering that tables have proper indexes which can be done using proper tools such as in the case of SQL-Server via SSMS profiling tools and the ability to get action plans when running conventional SQL statements.

Tip 13 global query filter

Entity Framework Core Fluent API HasQueryFilter method, a global query filter to a specific entity, so that the filter is included in all query statements generated for the entity by EF Core. 

One example, an application presents list of products in the front end which are available. In each query there needs to be a condition to filter out discontinued products e.g.

var products =
    context
        .Products
        .Where(product => product.CategoryId == categoryIdentifier && !product.Discontinued)
        .ToList();

Implementing HasQueryFilter the query changes to 
var products =
    context
        .Products
        .Where(product => product.CategoryId == categoryIdentifier)
        .ToList();

HasFilter are implemented in the DbContext class in OnModelCreating. In the following example (included in attached source code) any product.Discontinued is true are filtered out.

modelBuilder.Entity<Product>(entity =>
{
    entity.HasOne(d => d.Category)
        .WithMany(p => p.Products)
        .HasForeignKey(d => d.CategoryId)
        .HasConstraintName("FK_Products_Categories");
 
    entity.HasOne(d => d.Supplier)
        .WithMany(p => p.Products)
        .HasForeignKey(d => d.SupplierId)
        .HasConstraintName("FK_Products_Suppliers");
 
    entity.HasQueryFilter(prod => prod.Discontinued == false);
});

To ignore a filter use IgnoreQueryFilters.

var products =
    context
        .Products.IgnoreQueryFilters()
        .Where(product => product.CategoryId == categoryIdentifier)
        .ToList();

Important

Before using HasQueryFilter take time experimenting as there are some issues such as chaining multiple filters where in a case of two filters applied the last one is used while the first one is ignored. Also, use caution when using an unusual filter that other developers don't know about along with yourself days, months years later the filter has been forgotten and now wonder why a query does not return expected results. 

Take time to review the Microsoft documentation.

Tip 14 What's new

Entity Framework Core is constantly changing, from time to time read the "new features and breaking changes" for EF Core. Developers as a whole tend to stick to code they are comfortable with while there may and many times are better ways to achieve better results while this is not always the case this means just don't implement a new feature and expect it to be better than the current code.  This means to take time not only to read about new features and breaking changes but also try them out and when possible using unit test.

Tip 15 Source control

Not one developer is immune from writing code that fails to work and then needs to move back to a prior working version of code. As a developer, progresses with Entity Framework there are parts that can break and these parts are not easy to find. This is where source control can assist by undoing changes and or reviewing current code against prior versions. Take time to implement source control. 

Exploration

Entity Framework Core provides the ability to override many methods which provides developers the ability to incorporate their business logic ranging from validation injected into save changes, set values that are not in the model or to build in data manipulation that needs to be done outside of regular operations similar to doing a global filter mentioned earlier.

An easy way to explore/find potential overloads is to simply start typing public in the context class and a scrollable list appears to explore. 



Take the following, a developer would like to view changed data before performing the save operations. Override SaveChanges in the DbContext class.

public override int SaveChanges()
{
    OnBeforeSaving();
    return base.SaveChanges();
}

In the following code only modified entities are examined. An important piece of logic is to work only on non-virtual members done with in one of the assertions in OnBeforeSaving.

if (!prop.GetGetMethod().IsVirtual)

Here Console.WriteLine is used to display the primary key for each modified entity along with original and current values for any changed property.
private void OnBeforeSaving()
{
    var entries = ChangeTracker.Entries();
 
    /*
     * Only interested in modified entities
     */
    var modifiedEntities = ChangeTracker
        .Entries().Where(_ =>  _.State == EntityState.Modified);
 
    /*
     * Iterate any modified entities
     */
    foreach (var change in modifiedEntities)
    {
 
        /*
         * We are only concerned in this case with customer entities
         */
        if (change.Entity is Customer customer)
        {
            /*
             * Show primary key
             */
            Console.WriteLine($"Primary key: {GetEntityPrimaryKeyValue(change.Entity)}");
 
            foreach (var prop in change.Entity.GetType().GetTypeInfo().DeclaredProperties)
            {
                if (!prop.GetGetMethod().IsVirtual)
                {
                    /*
                     * Show property name, original and current values
                     */
                    Console.WriteLine(
                        $"Name: {prop.Name} original " +
                        $"'{change.Property(prop.Name).OriginalValue}' " +
                        $"current '{change.Property(prop.Name).CurrentValue}'");
                }
                 
            }
        }
    }
}

Besides overrides there are times when language extension methods make more sense when they are constructed generically, for instance to reset all unchanged entities or to perform annotation validation in an overridden save changes



Summary

Tips and tricks have been presented to assist in learning several best practices along with how to avoid some common issues which typically arise when first starting writing code with Entity Framework Core. As progressing with writing code for Entity Framework Core keep in mind that Entity Framework is still evolving which means new features on the horizon along with bug fixes which means it’s prudent to keep abreast of changes to Entity Framework Core over time. Take time to learn the basics rather than forge head first with a complex project, keep things simple until the basics are understood.

See also

External resources

Source code


The following GitHub repository contains code which needs the following prior to executing.