Motivation for moving to Entity Framework Core (C#)
Introduction
For years, many developers relied on interacting with databases using DataSet and DataTable containers populated using a TableAdapter, DataAdapter or using a connection and command object coupled with events for editing, deleting, filtering and adding new data. Although these methods are still viable the intent here is to present compelling reasons to use Entity Framework Core in Windows, form projects which when done properly can transfer to other project types like WPF and web based solutions.
The database will be SQL-Server although the same techniques presented will work for the majority of data providers offered for Entity Framework Core, main differences are how each database handles specifics as not all databases perform exactly the same way.
EF Core 7: This release has many features to make life easier along with performance enhancements, see code samples.
Working with conventional non Entity Framework data access
There is guesswork from the start for those just starting out with working with databases.
- How to design a database (the same is true for Entity Framework)?
- How to create a connection (Entity Framework Core does this for you)?
- How to interact with the data (Entity Framework Core once working with it for a while is easier)?
For designing a database, the first step is to know exactly what will be stored then figure out tables and columns which can be done first with pencil and paper keeping in mind normalization. A simple example, there is a need to store contact information, if a contact table were to be created in a new database with a primary key, first and last name followed by work, home and cell phone then later there is a need for a secondary work phone? Many novice developers will add a second work phone while the proper way is to have a contact device table with a foreign key back to the contact table so if a secondary work phone is needed simply add a row in the device table. One step farther would be have a table for device type in the event there is a new type other than work, home or cell. Similarly for a contact there is a contact type e.g. owner, sales representative, what happens if one of the titles changed? Each row in the contact table needs to be updated, instead having a contact type table with a foreign key back to the contact table means one change for all contacts.
The downside if time is not spent with a normalized database writing queries in SSMS (SQL-Server Management Studio) but instead writing queries in code it can turn into frustration and lost time while taking time to test the database design in SSMS can validate or invalidate a design and those queries can be copied and pasted into code.
For a good resource for database designs can be found on a site named Database Answers.
When a database is poorly designed this will be reflected in code be it with DataSets, Entity Framework or Entity Framework Core.
Code sample database
The database used for examples is a modified Microsoft NorthWind database which works fine yet can be improved slightly e.g. a table named Order Details could had been named OrderDetails, there is an problem with an index which in the next part of this series will be remedied with a deeper dive into Entity Framework Core, for now each code sample works.
Database connection
Exploration for connecting to a database.
Connecting to a database with conventional methods
When working with a data provider e.g. SqlClient, a connection string is required to connect to the database. The following site ConnectionStrings has all connection strings.
Example, connect to a local SQL-Server database the connection string in this case is privately scoped to a data class. As shown below the connection string is created by hand.
/// <summary>
/// Connection string for database and the catalog to work with
/// </summary>
private static string ConnectionString =
"Data Source=.\\SQLEXPRESS;" +
"Initial Catalog=NorthWindAzureForInserts;" +
"Integrated Security=True";
Which is then used in a connection object
public static DataTable CountryTable()
{
var dt = new DataTable();
using (var cn = new SqlConnection(ConnectionString))
{
using (var cmd = new SqlCommand() {Connection = cn})
{
cmd.CommandText = "SELECT CountryIdentifier, Name FROM dbo.Countries;";
cn.Open();
dt.Load(cmd.ExecuteReader());
}
}
return dt;
}
Connecting to a database with Entity Framework Core
A connection string is created using the following Visual Studio extension which is free and used by thousands of developers.
- Create a new windows form or class project.
- Install Entity Framework Core SQL-Server using NuGet. (this is great for novice to expert developers but can also be done by hand coding yet not advisable for new to coding or new to Entity Framework Core)
- Right click on the project
- Select EF Power tools
- Select Reverse engineer.
- Follow the prompts which starts with selecting a database followed by which tables to create classes for. There are several options which are self-explanatory such as creating folders for models (classes representing tables), data annotations, custom configurations (gone over in the next part of the series) and more.
- Once done a DbContext class is generated with the connection string done for us (along with a comment to move the connection string, also gone over in the next part of this series but for now works).
optionsBuilder.UseSqlServer(
"Data Source=.\\SQLEXPRESS;Initial Catalog=NorthWindAzureForInserts;Integrated Security=True");
Now to get the same data for contact types as done above. Note there is no open method, this is handled for you by Entity Framework Core.
public static List<ContactType> ContactTypes()
{
using (var context = new NorthWindContext())
{
return context.ContactType.AsNoTracking().ToList();
}
}
In both cases, conventional and Entity Framework Core data is read without anyway to directly update the data. With the conventional method a decent amount of code is needed to supplement for saving changes while with Entity Framework Core the NorthWindContext can be scoped to the class level and now saves are done by calling SaveChanges or SaveChangesAsync, In some cases a BindingList coupled with implementing INotifyPropertyChanged interface may be needed as shown in the following code sample.
Important: When viewing the provided code samples don't get discourage by the additional amount of code there is for Entity Framework Core, more code in this case means greater control over every aspect of interacting with a database. Many developers think less code should be needed yet those with this mentality soon learn less is not always better.
Reading data
Note: Both conventional and EF Core versions have the exact same functionality.
When dealing with very simple non relational data SQL SELECT statements are simple while this is rare except for support read operations such as reading a list of countries. In real life the following query is normal, three tables joined together using conventional data access.
public static async Task<DataTable> GetCustomersAsync()
{
var dataTable = new DataTable();
await Task.Run(async () =>
{
using (var cn = new SqlConnection(ConnectionString))
{
using (var cmd = new SqlCommand() { Connection = cn })
{
cmd.CommandText =
"SELECT cust.CustomerIdentifier, cust.CompanyName, cust.ContactId, Contacts.FirstName, " +
"Contacts.LastName, ct.ContactTitle, cust.ContactTypeIdentifier, cust.CountryIdentifier, " +
"Countries.Name AS CountryName FROM Customers AS cust " +
"INNER JOIN Contacts ON cust.ContactId = Contacts.ContactId " +
"INNER JOIN ContactType AS ct ON cust.ContactTypeIdentifier = ct.ContactTypeIdentifier " +
"INNER JOIN Countries ON cust.CountryIdentifier = Countries.CountryIdentifier " +
"ORDER BY cust.CompanyName";
await cn.OpenAsync();
dataTable.Load(await cmd.ExecuteReaderAsync());
}
}
});
return dataTable;
}
The SELECT statement above should be written in SSMS as hand coding this in C# code unless an SQL expert will turn out wrong, even using SSMS can prove to be a challenge for those just starting out with SQL in regards to the JOIN aspects.
Now let's look at Entity Framework Core version. Still looks complex to the beginner yet here are the advantages
- Each aspect of the query has Intellisense as each class Customers, Contact, ContactType and Country are all classes in the project created using EF Power Tools
- JOINs are replaced with two extension methods (which also have Intellisense), .Include and .ThenInclude. Note that .ThenInclude is new to EF Core 3.1, Entity Framework 6 does not have it, the method for EF 6 is query.Include(x => x.Collection.Select(y => y.Property)) or to using LINQ or lambda with joins similar to the above SQL SELECT.
public static async Task<List<CustomerItem>> GetCustomersAsync()
{
return await Task.Run(async () =>
{
using (var context = new NorthwindContext())
{
return await context.Customers.AsNoTracking()
.Include(customer => customer.Contact)
.ThenInclude(contact => contact.ContactDevices)
.ThenInclude(contactDevices => contactDevices.PhoneTypeIdentifierNavigation)
.Include(customer => customer.ContactTypeIdentifierNavigation)
.Include(customer => customer.CountryIdentifierNavigation)
.Select(customer => new CustomerItem()
{
CustomerIdentifier = customer.CustomerIdentifier,
CompanyName = customer.CompanyName,
ContactId = customer.Contact.ContactId,
Street = customer.Street,
City = customer.City,
PostalCode = customer.PostalCode,
CountryIdentifier = customer.CountryIdentifier,
Phone = customer.Phone,
ContactTypeIdentifier = customer.ContactTypeIdentifier,
Country = customer.CountryIdentifierNavigation.Name,
FirstName = customer.Contact.FirstName,
LastName = customer.Contact.LastName,
ContactTitle = customer.ContactTypeIdentifierNavigation.ContactTitle,
OfficePhoneNumber = customer.Contact.ContactDevices.FirstOrDefault(contactDevices => contactDevices.PhoneTypeIdentifier == 3).PhoneNumber
}).ToListAsync();
}
});
}
Presentation in Windows Forms
Below there is an example for conventional loading of data into a DataGridView and one for Entity Framework loading data into a DataGridView.
First conventional loading
using System;
using System.Data;
using System.Windows.Forms;
using DataGridViewLibrary;
using DataOperationsConventional;
namespace SimpleReadConventional
{
public partial class Form1 : Form
{
private readonly BindingSource _customersBindingSource = new BindingSource();
public Form1()
{
InitializeComponent();
dataGridView1.AutoGenerateColumns = false;
Shown += Form1_Shown;
}
private async void Form1_Shown(object sender, EventArgs e)
{
_customersBindingSource.DataSource = await Operations.GetCustomersAsync();
CountryColumn.DisplayMember = "Name";
CountryColumn.ValueMember = "CountryIdentifier";
CountryColumn.DataPropertyName = "CountryIdentifier";
CountryColumn.DataSource = Operations.CountryTable();
CountryColumn.DisplayStyle = DataGridViewComboBoxDisplayStyle.Nothing;
ContactTitleColumn.DisplayMember = "ContactTitle";
ContactTitleColumn.ValueMember = "ContactTypeIdentifier";
ContactTitleColumn.DataPropertyName = "ContactTypeIdentifier";
ContactTitleColumn.DataSource = Operations.ContactTypeTable();
ContactTitleColumn.DisplayStyle = DataGridViewComboBoxDisplayStyle.Nothing;
dataGridView1.DataSource = _customersBindingSource;
dataGridView1.ExpandColumns();
CountryNamesComboBox.DataSource = Operations.CountryNameList();
CountryNamesComboBox.SelectedIndex = 1;
}
private void CurrentCustomerButton_Click(object sender, EventArgs e)
{
if (_customersBindingSource.DataSource != null && _customersBindingSource.Current != null)
{
var customer = ((DataRowView) _customersBindingSource.Current).Row;
MessageBox.Show($"Id: {customer.Field<int>("CustomerIdentifier")}" +
$"\nContact Id: {customer.Field<int>("ContactId")}");
}
}
private void FilterButton_Click(object sender, EventArgs e)
{
if (CountryNamesComboBox.DataSource == null) return;
if (CountryNamesComboBox.Text == @"Remove filter")
{
_customersBindingSource.Filter = "";
}
else
{
_customersBindingSource.Filter = $"CountryName = '{CountryNamesComboBox.Text}'";
}
}
}
}
Now using Entity Framework Core
using System;
using System.Windows.Forms;
using DataGridViewLibrary;
using DataOperationsEntityFrameworkCore.Classes;
using DataOperationsEntityFrameworkCore.Projections;
using Equin.ApplicationFramework;
namespace SimpleReadEntityFrameworkCore
{
public partial class Form1 : Form
{
private BindingListView<CustomerItem> _customerView;
private readonly BindingSource _customersBindingSource = new BindingSource();
public Form1()
{
InitializeComponent();
dataGridView1.AutoGenerateColumns = false;
Shown += Form1_Shown;
}
private async void Form1_Shown(object sender, EventArgs e)
{
_customerView = new BindingListView<CustomerItem>(await Operations.GetCustomersAsync());
_customersBindingSource.DataSource = _customerView;
CountryColumn.DisplayMember = "Name";
CountryColumn.ValueMember = "CountryIdentifier";
CountryColumn.DataPropertyName = "CountryIdentifier";
CountryColumn.DataSource = Operations.Countries();
CountryColumn.DisplayStyle = DataGridViewComboBoxDisplayStyle.Nothing;
ContactTitleColumn.DisplayMember = "ContactTitle";
ContactTitleColumn.ValueMember = "ContactTypeIdentifier";
ContactTitleColumn.DataPropertyName = "ContactTypeIdentifier";
ContactTitleColumn.DataSource = Operations.ContactTypes();
ContactTitleColumn.DisplayStyle = DataGridViewComboBoxDisplayStyle.Nothing;
dataGridView1.DataSource = _customersBindingSource;
dataGridView1.ExpandColumns();
CountryNamesComboBox.DataSource = Operations.CountryNameList();
CountryNamesComboBox.SelectedIndex = 1;
}
private void CurrentCustomerButton_Click(object sender, EventArgs e)
{
if (_customersBindingSource.DataSource != null && _customersBindingSource.Current != null)
{
var customer = _customerView[_customersBindingSource.Position].Object;
MessageBox.Show($"Id: {customer.CustomerIdentifier}\nContact Id: {customer.ContactId}");
}
}
private void FilterButton_Click(object sender, EventArgs e)
{
if (CountryNamesComboBox.DataSource == null) return;
if (CountryNamesComboBox.Text == @"Remove filter")
{
_customerView.RemoveFilter();
}
else
{
_customerView.ApplyFilter(customer => customer.CountryName == CountryNamesComboBox.Text);
}
}
}
}
Between the two EF Core has two less lines which is not all that important, what is important is even though both are close in lines of code that they are extremely similar.
CRUD operations
Although there are no code samples for these here are the concepts between conventional and EF Core.
Conventional CRUD
When working with a TableAdapter, CRUD operations are coded for the developer and difficult to alter. With a DataAdapter CRUD operations in many cases the DataAdapter handles the CRUD operations with options to override the automatic configuration. With a connection and command object there is a good deal more code involve with many different ways to handle changes ranging from subscribing to DataTable events to DataTable events coupled with various forms of data binding and data binding events coupled with events of a DataGridView.
Entity Framework CRUD
For Entity Framework, implementing INotificationChanged interface with a BindingList is suffice in most cases. Couple with with a) the DbContext is active using SaveChanges will save changes back to the database b) when the DbContext is not active EF Core has methods to attach a detached changed entity back to the database.
Validating data
Checking if data will be accepted by the database bases on things like length of a string, a column is required etc.
Conventional validation
Code is written directly in a form or a backing data class, if rules change in the database it's harder to update rules in code.
Entity Framework validation
Classes which represent table in a database can be setup with validation requirements.
In the following class
- CompanyName is required denoted by [Required] annotation, [StringLength(40)] indicates 40 characters is the max string length.
- For more on data annotations.
- Data annotations along is not enough to check for violations, the following classes provide the base to validate data which will be talked about in the next part in this series. To see a working example, see the following TechNet article on unit testing with uses validating classes.
namespace NorthWithFoldersAnnotations.Models
{
public partial class Customer
{
public Customer()
{
Orders = new HashSet<Order>();
}
[Key]
/// <summary>
/// Id
/// </summary>
public int CustomerIdentifier { get; set; }
[Required]
[StringLength(40)]
/// <summary>
/// Company
/// </summary>
public string CompanyName { get; set; }
/// <summary>
/// ContactId
/// </summary>
public int? ContactId { get; set; }
[StringLength(60)]
/// <summary>
/// Street
/// </summary>
public string Street { get; set; }
[StringLength(15)]
/// <summary>
/// City
/// </summary>
public string City { get; set; }
[StringLength(15)]
/// <summary>
/// Region
/// </summary>
public string Region { get; set; }
[StringLength(10)]
/// <summary>
/// Postal Code
/// </summary>
public string PostalCode { get; set; }
/// <summary>
/// CountryIdentifier
/// </summary>
public int? CountryIdentifier { get; set; }
[StringLength(24)]
/// <summary>
/// Phone
/// </summary>
public string Phone { get; set; }
[StringLength(24)]
/// <summary>
/// Fax
/// </summary>
public string Fax { get; set; }
/// <summary>
/// ContactTypeIdentifier
/// </summary>
public int? ContactTypeIdentifier { get; set; }
/// <summary>
/// Modified Date
/// </summary>
public DateTime? ModifiedDate { get; set; }
[ForeignKey(nameof(ContactId))]
[InverseProperty("Customers")]
public virtual Contact Contact { get; set; }
[ForeignKey(nameof(ContactTypeIdentifier))]
[InverseProperty(nameof(ContactType.Customers))]
public virtual ContactType ContactTypeIdentifierNavigation { get; set; }
[ForeignKey(nameof(CountryIdentifier))]
[InverseProperty(nameof(Country.Customers))]
public virtual Country CountryIdentifierNavigation { get; set; }
[InverseProperty(nameof(Order.CustomerIdentifierNavigation))]
public virtual ICollection<Order> Orders { get; set; }
}
}
Summary
Basic examples have been presented to show how to get started with Entity Framework Core 3 coming from a background of working with conventional data access operations e.g. TableAdapter, DataAdapter etc. in an effort to show its not much different between the two different methods and that keeping with basics Entity Framework is actually easier to work with.
There is always the option to stay with conventional data operations using TableAdapter, DataAdapter, connection and command object while taking time to learn Entity Framework Core provides a pathway to moving current projects to WPF and web solutions or to understand EF Core for starting new projects for WPF or the web while there are other differences the core to access data remains the same.
In the next part of the series a deeper dive into Entity Framework Core will be done without discussing conventional data access methods.
Important: Code presented is not intended to represent a well working CRUD project but instead showing the very basics for differences between conventional and Entity Framework Core data access.
External resources
Entity Framework Core overview
Entity Framework Core 3 breaking changes
Entity Framework code first DataGridView
See also
Entity Framework Core shadow properties (C#)
Entity Framework Core/Windows Forms tips and tricks
Visual Studio: structuring an Entity Framework project for Windows forms
Entity Framework Windows Forms validation
Entity Framework Core Find all by primary key (C#)
Source code
- Download or clone the following GitHub repository directly in Visual Studio.
- Run the following database script.
- In Visual Studio, right click on the top node in solution explorer, select restore NuGet packages.
- Build each project and run them.
- Go back and review the code between the conventional data access project and the Entity Framework Core project