Share via


Entity Framework Core 3 Windows Forms (VB.NET)

Introduction

Learn the basics on how to work with Entity Framework Core 3 using SQL-Server and VB.NET programming language.

Preparation

Since the release of Entity Framework 6 Visual Studio provided developers with an automated process to scaffold a database which has gone away with Entity Framework Core. Unlike C# where a database can be scaffold directly in Visual Studio when the scaffold command executes against a VB.NET project the following will output.
The project language 'VB' isn't supported by the built-in IModelCodeGenerator service. You can try looking for an additional NuGet package, which supports this language; moving your DbContext type to a C# class library referenced by this project; or manually implementing and registering the design-time service for programming language.  
There are two ways to scaffold VB.NET, using a third party tool, extension or to write code. In this article code will be written to express tables in a database for use with Entity Framework Core. Rather than write code for an entire database only enough will be done with several tables to understand how to proceed with more tables in a relational database.

All steps are not completely explained but are there to create models yet using the models in the GitHub repository the parts not fully explained can be understood.

Required

  • Microsoft SQL-Server service running which Visual Studio handles during setup.
  • Microsoft SSMS (SQL-Server Management Studio)
  • Microsoft Visual Studio 2017 or higher
  • Microsoft Framework 4.7 or higher

Tables

The following tables will be setup for Entity Framework Core, Customers the main table, Countries, Contact and ContactType.

The first step is create the database and tables in SSMS using the data script include in the source code on GitHub listed at the bottom of this article.

Next step, list primary and foreign keys for Customers, Contact and ContactType tables which will be needed when wiring up the DbContext. An easy method is to run the following in a new query window in SSMS.

SELECT       
    obj.name AS  fk_name, sch.name  AS schema_name, 
    tab1.name AS  [table], col1.name AS  [column], 
    tab2.name AS  referenced_table, col2.name AS  referenced_column
FROM
sys.foreign_key_columns AS  fkc INNER  JOIN
sys.objects AS  obj ON  obj.object_id = fkc.constraint_object_id INNER JOIN
sys.tables AS  tab1 ON  tab1.object_id = fkc.parent_object_id INNER JOIN
sys.schemas AS  sch ON  tab1.schema_id = sch.schema_id INNER JOIN
sys.columns AS  col1 ON  col1.column_id = fkc.parent_column_id AND col1.object_id = 
    tab1.object_id INNER  JOIN
sys.tables AS  tab2 ON  tab2.object_id = fkc.referenced_object_id INNER JOIN
sys.columns AS  col2 ON  col2.column_id = fkc.referenced_column_id AND col2.object_id = 
    tab2.object_id INNER  JOIN
sys.types AS  TY ON  col1.system_type_id = TY.system_type_id AND col1.user_type_id = 
    TY.user_type_id AND tab1.name = 'Customers';
 
SELECT       
    pk.name AS  pk_name, 
    SCHEMA_NAME(tab.schema_id) AS  schema_name, 
    tab.name AS  table_name, 
    ic.index_column_id AS  column_id, 
    col.name AS  column_name
FROM
sys.tables AS  tab INNER  JOIN
sys.indexes AS  pk ON  tab.object_id = pk.object_id AND  pk.is_primary_key = 1 INNER JOIN
sys.index_columns AS  ic ON  ic.object_id = pk.object_id AND  ic.index_id = pk.index_id INNER JOIN
sys.columns AS  col ON  pk.object_id = col.object_id AND  col.column_id = ic.column_id
WHERE        (tab.name = N'Customers')
ORDER BY  schema_name, pk_name, column_id;

The following is the output which each value can be copied to code via right clicking on a value and select copy.

NuGet packages

The following packages need to be installed

Visual Studio solution

Entity Framework Core classes will reside in a class project while presentation layer will be a Windows Form project. There are many reasons for separating backend classes from the presentation layer ranging from providing the ability to utilizing the Entity Framework Core classes in other projects to scalability e.g. place a business layer between the backend classes and the presentation layer.

Coding step 1

  • Create the class project, in this case named EntityFrameworkCoreNorthWind
    • Add both NuGet packages listed above.
    • Create a folder named Models which will house classes which represent the tables, Customers, Contact, ContactType and Countries.
      • Create a new class files for each of the following: Customers, Contact, ContactType and Countries
    • Create a folder named Contexts
      • Create an new class file named CustomerContext
    • Create a folder named Classes
      • Create a new class file named ConfiguationHelper
    • Create a folder named Extensions
      • Create a new code module file named QueryExtensions
    • Create a folder named Projections
      • Create a new class file named Customer
      • Create a new class file named Customeritem

Coding step 2

Here code is written in the classes Customers, Contacts, Countries and ContactType which represent columns in the database tables and relationships between related tables.

Without the ability to scaffold models as possible with C# it can be a tedious task of hand coding each class.  A hidden benefit in having to code most of the code is there is a better understanding of the mechanics of what it takes to configure models while the downside is those are just starting out this may be more than they can handle. For this just starting out try working with Entity Framework 6 Code first from database to get a better understanding of configuration as Visual Studio has templates for Entity Framework 6 although there are a good deal of changes when moving to Entity Framework Core, even from Entity Framework Core first edition to the current edition. The following article Entity Framework dynamic order by (done in Windows forms, Entity Framework 6) may help to get a running start.

To assist use the following SQL in SSMS.

DECLARE @TableName sysname = 'Customers';
DECLARE @Result VARCHAR(MAX) = 'Public Class '  + @TableName; 
 
SELECT  @Result = @Result + '
    Public Property ' + ColumnName + ' As ' + ColumnType + NullableSign
FROM    ( SELECT    REPLACE(col.name, ' ', '_') ColumnName ,
                    column_id ColumnId ,
                    CASE typ.name
                      WHEN 'bigint'  THEN 'Long'
                      WHEN 'binary'  THEN 'Byte[]'
                      WHEN 'bit'  THEN 'Boolean'
                      WHEN 'char'  THEN 'String'
                      WHEN 'date'  THEN 'DateTime'
                      WHEN 'datetime'  THEN 'DateTime'
                      WHEN 'datetime2'  THEN 'DateTime'
                      WHEN 'datetimeoffset'  THEN 'DateTimeOffset'
                      WHEN 'decimal'  THEN 'Decimal'
                      WHEN 'float'  THEN 'Float'
                      WHEN 'image'  THEN 'Byte()'
                      WHEN 'int'  THEN 'Integer'
                      WHEN 'money'  THEN 'Decimal'
                      WHEN 'nchar'  THEN 'String'
                      WHEN 'ntext'  THEN 'String'
                      WHEN 'numeric'  THEN 'Decimal'
                      WHEN 'nvarchar'  THEN 'String'
                      WHEN 'real'  THEN 'Double'
                      WHEN 'smalldatetime'  THEN 'DateTime'
                      WHEN 'smallint'  THEN 'Short'
                      WHEN 'smallmoney'  THEN 'Decimal'
                      WHEN 'text'  THEN 'String'
                      WHEN 'time'  THEN 'TimeSpan'
                      WHEN 'timestamp'  THEN 'DateTime'
                      WHEN 'tinyint'  THEN 'Byte'
                      WHEN 'uniqueidentifier'  THEN 'Guid'
                      WHEN 'varbinary'  THEN 'Byte()'
                      WHEN 'varchar'  THEN 'String'
                      ELSE 'UNKNOWN_' + typ.name
                    END ColumnType ,
                    CASE WHEN  col.is_nullable = 1
                              AND typ.name IN ( 'bigint', 'bit',  'date',
                                                'datetime', 'datetime2',
                                                'datetimeoffset', 'decimal',
                                                'float', 'int',  'money',
                                                'numeric', 'real',
                                                'smalldatetime', 'smallint',
                                                'smallmoney', 'time',
                                                'tinyint', 'uniqueidentifier'  )
                         THEN '?'
                         ELSE ''
                    END NullableSign
          FROM      sys.columns col
                    JOIN sys.types typ ON col.system_type_id = typ.system_type_id
                                          AND col.user_type_id = typ.user_type_id
          WHERE     object_id = OBJECT_ID(@TableName)
        ) t
ORDER BY  ColumnId;
 
SET @Result = @Result + '
End Class';
 
PRINT @Result;

Which for the example above produces.

Public Class Customers
    Public Property CustomerIdentifier As Integer
    Public Property CompanyName As String
    Public Property ContactName As String
    Public Property ContactId As Integer?
    Public Property Address As String
    Public Property City As String
    Public Property Region As String
    Public Property PostalCode As String
    Public Property CountryIdentifier As Integer?
    Public Property Phone As String
    Public Property Fax As String
    Public Property ContactTypeIdentifier As Integer?
    Public Property ModifiedDate As DateTime?
End Class

Now at the least data annotation and child relations need to be coded into the generated class and optionally implement INotifyPropertyChanged Interface.

For the Customers class there is a property for each column in the customers table along with data annotations which indicate if a property/column is required, maximum length of a column and related tables.

  • To express relations between a model to a child model (Customers to ContactType) use InverseProperty as shown below.
    • The InverseProperty attribute is used to denote the inverse navigation property of a relationship when the same type takes part in multiple relationships.
      • It is used when you need to indicate that navigation property in one class is related to the same foreign key as another navigation property in another class.
    • In the child relations, in this case ContactType the InverseProperty is done via <InverseProperty("ContactTypeNavigation")> which means this is easily broken if not careful when changing property names.
  • Overriding .ToString serves two purposes, when debugging in this case CompanyName will be displayed when examining customer objects and the second, if a List(Of Customers) were to be displayed in a ComboBox or ListBox .ToString is used for DisplayMember.
Imports System
Imports System.Collections.Generic
Imports System.ComponentModel.DataAnnotations
Imports System.ComponentModel.DataAnnotations.Schema
 
Namespace NorthWindEntityFrameworkCore
    Partial Public  Class Customers
        <Key>
        Public Property  CustomerIdentifier() As Integer
        <Required, StringLength(40)>
        Public Property  CompanyName() As  String
        <StringLength(30)>
        Public Property  ContactName() As  String
        Public Property  ContactId() As  Integer?
        <StringLength(60)>
        Public Property  Address() As  String
        <StringLength(15)>
        Public Property  City() As  String
        <StringLength(15)>
        Public Property  Region() As  String
        <StringLength(10)>
        Public Property  PostalCode() As  String
        Public Property  CountryIdentifier() As Integer?
        <StringLength(24)>
        Public Property  Phone() As  String
        <StringLength(24)>
        Public Property  Fax() As  String
        Public Property  ContactTypeIdentifier() As Integer?
        Public Property  ModifiedDate() As  Date?
 
        <ForeignKey(NameOf(ContactId)), InverseProperty(NameOf(Contacts.Customers))>
        Public Overridable  Property Contact() As Contacts
        <ForeignKey(NameOf(ContactTypeIdentifier)), InverseProperty(NameOf(ContactType.Customers))>
        Public Overridable  Property ContactTypeNavigation() As ContactType
        <ForeignKey(NameOf(CountryIdentifier)), InverseProperty(NameOf(Countries.Customers))>
        Public Overridable  Property CountryIdentifierNavigation()  As  Countries
 
        Public Overrides  Function ToString() As String
            Return CompanyName
        End Function
    End Class
End Namespace

This is done for each table in the database. As presented when in a DataGridView or TextBox bound to a BindingList which has change notification there will be no change notification as coded. For change notification implement INotifyPropertyChanged Interface.

Imports System
Imports System.Collections.Generic
Imports System.ComponentModel
Imports System.ComponentModel.DataAnnotations
Imports System.ComponentModel.DataAnnotations.Schema
Imports System.Runtime.CompilerServices
 
Namespace NorthWindEntityFrameworkCore
    ''' <summary>
    ''' INotifyPropertyChanged is optional, if not needed each
    ''' property can be an auto-property
    ''' </summary>
    Partial Public  Class Customers
        Implements INotifyPropertyChanged
 
        Private _companyName As String
        Private _customerIdentifier As Integer
        Private _contactIdentifier As Integer?
        Private _countryIdentifierNavigation As Countries
        Private _contactTypeNavigation As ContactType
        Private _contact1 As Contacts
        Private _contactTypeIdentifier As Integer?
        Private _phone As String
        Private _postalCode As String
        Private _city As String
        Private _address As String
        Private _countryIdentifier As Integer?
 
        <Key>
        Public Property  CustomerIdentifier() As Integer
            Get
                Return _customerIdentifier
            End Get
            Set
                _customerIdentifier = Value
                OnPropertyChanged()
            End Set
        End Property
        <Required, StringLength(40)>
        Public Property  CompanyName() As  String
            Get
                Return _companyName
            End Get
            Set
                _companyName = Value
                OnPropertyChanged()
            End Set
        End Property
 
        Public Property  ContactId() As  Integer?
            Get
                Return _contactIdentifier
            End Get
            Set
                _contactIdentifier = Value
                OnPropertyChanged()
            End Set
        End Property
 
        <StringLength(60)>
        Public Property  Address() As  String
            Get
                Return _address
            End Get
            Set
                _address = Value
                OnPropertyChanged()
            End Set
        End Property
 
        <StringLength(15)>
        Public Property  City() As  String
            Get
                Return _city
            End Get
            Set
                _city = Value
                OnPropertyChanged()
            End Set
        End Property
 
        <StringLength(10)>
        Public Property  PostalCode() As  String
            Get
                Return _postalCode
            End Get
            Set
                _postalCode = Value
                OnPropertyChanged()
            End Set
        End Property
 
        Public Property  CountryIdentifier() As Integer?
            Get
                Return _countryIdentifier
            End Get
            Set
                _countryIdentifier = Value
                OnPropertyChanged()
            End Set
        End Property
 
        <StringLength(24)>
        Public Property  Phone() As  String
            Get
                Return _phone
            End Get
            Set
                _phone = Value
                OnPropertyChanged()
            End Set
        End Property
 
        Public Property  ContactTypeIdentifier() As Integer?
            Get
                Return _contactTypeIdentifier
            End Get
            Set
                _contactTypeIdentifier = Value
                OnPropertyChanged()
            End Set
        End Property
 
        Public Property  ModifiedDate() As  Date?
 
        <ForeignKey(NameOf(ContactId)),
            InverseProperty(NameOf(Contacts.Customers))>
        Public Overridable  Property Contact() As Contacts
            Get
                Return _contact1
            End Get
            Set
                _contact1 = Value
                OnPropertyChanged()
            End Set
        End Property
 
        <ForeignKey(NameOf(ContactTypeIdentifier)),
            InverseProperty(NameOf(ContactType.Customers))>
        Public Overridable  Property ContactTypeNavigation() As ContactType
            Get
                Return _contactTypeNavigation
            End Get
            Set
                _contactTypeNavigation = Value
                OnPropertyChanged()
            End Set
        End Property
 
        <ForeignKey(NameOf(CountryIdentifier)),
            InverseProperty(NameOf(Countries.Customers))>
        Public Overridable  Property CountryIdentifierNavigation()  As  Countries
            Get
                Return _countryIdentifierNavigation
            End Get
            Set
                _countryIdentifierNavigation = Value
                OnPropertyChanged()
            End Set
        End Property
 
        Public Overrides  Function ToString() As String
            Return CompanyName
        End Function
        Public Event  PropertyChanged As  PropertyChangedEventHandler _
            Implements INotifyPropertyChanged.PropertyChanged
 
        Protected Overridable  Sub OnPropertyChanged(<CallerMemberName>
        Optional memberName As String  = Nothing)
 
            RaiseEvent PropertyChanged(Me, New  PropertyChangedEventArgs(memberName))
 
        End Sub
    End Class
End Namespace

Once done with Customers class follow the same pattern for Contact, ContactType and Country.

Contacts, use the SQL mentioned earlier to generate the class and properties. Then add in the property to relate back to Customers.

<InverseProperty("Contact")>
Public Overridable  Property Customers() As ICollection(Of Customers)

Similarly in ContactTypes add the following property to references back to Customers

<InverseProperty("ContactTypeNavigation")>
Public Overridable  Property Customers() As ICollection(Of Customers)

And finally for Countries

<InverseProperty("CountryIdentifierNavigation")>
Public Overridable  Property Customers() As ICollection(Of Customers)

As with Customers the above three tables can implement INotifyPropertyChanged interface which is done in the GitHub repository.

DbContext step 3 

This is the main class to configure Entity Framework Core

Class signature

Partial Public  Class CustomerContext
    Inherits DbContext

Next, a private variable to remember the database connection

Private ConnectionString As String

This is followed by the class constructors, the first, parameter-less constructor is what is used in this code sample. This constructor obtains settings from the app.config file and creates a well formed connection string in ConfigurationHelper class

Public Sub  New()
    Dim configurationHelper = New ConfigurationHelper
    ConnectionString = configurationHelper.ConnectionString
End Sub
 
Public Sub  New(options As DbContextOptions(Of CustomerContext))
    MyBase.New(options)
End Sub

The following is optional, this code sets up console logging which means for each request for data, updates, deletes and additions will present SQL to Visual Studio's output window.

''' <summary>
''' Configure logging for app
''' https://docs.microsoft.com/en-us/ef/core/miscellaneous/logging?tabs=v3
''' https://github.com/dotnet/EntityFramework.Docs/blob/master/entity-framework/core/miscellaneous/logging.md
''' </summary>
Public Shared  ReadOnly ConsoleLoggerFactory As ILoggerFactory = LoggerFactory.
    Create(Function(builder) As  ILoggerFactory
               builder.AddFilter(
                   Function(category, level)
 
                       Return category = DbLoggerCategory.Database.Command.Name  AndAlso
                              level = LogLevel.Information
 
                   End Function)
 
               builder.AddConsole()
 
               Return Nothing
 
           End Function)

the following method, OnConfiguring will allow console logging only while working from Visual Studio, outside of Visual Studio there will be no logging.

''' <summary>
''' Connection string is obtained from app.config
''' Logging is only enabled with the Visual Studio debugger
''' </summary>
''' <param name="optionsBuilder"></param>
Protected Overrides  Sub OnConfiguring(optionsBuilder As DbContextOptionsBuilder)
 
#If DEBUG Then
    If Not  optionsBuilder.IsConfigured Then
        optionsBuilder.
            UseSqlServer(ConnectionString).
            UseLoggerFactory(ConsoleLoggerFactory).
            EnableSensitiveDataLogging()
    End If
#Else
    If Not  optionsBuilder.IsConfigured Then
        optionsBuilder.UseSqlServer(ConnectionString)
    End If
#End If
End Sub

Configuring relations, constraints, default values and more is done in OnModelCreating method.

Protected Overrides  Sub OnModelCreating(modelBuilder As ModelBuilder)
 
    modelBuilder.Entity(Of Customers)(
        Sub(entity)
 
            entity.HasKey(Function(customer) customer.CustomerIdentifier).
                                         HasName("PK_Customers_1")
 
            entity.Property(Function(customer) customer.ModifiedDate).
                                         HasDefaultValueSql("(getdate())")
 
            entity.HasOne(Function(customer) customer.Contact).
                                         WithMany(Function(contacts) contacts.Customers).
                                         HasForeignKey(Function(customer) customer.ContactId).
                                         HasConstraintName("FK_Customers_Contacts")
 
            entity.HasOne(Function(customer) customer.ContactTypeNavigation).
                                         WithMany(Function(contactType) contactType.Customers).
                                         HasForeignKey(Function(customer) customer.ContactTypeIdentifier).
                                         HasConstraintName("FK_Customers_ContactType")
 
            entity.HasOne(Function(customer) customer.CountryIdentifierNavigation).
                                         WithMany(Function(countries) countries.Customers).
                                         HasForeignKey(Function(customer) customer.CountryIdentifier).
                                         HasConstraintName("FK_Customers_Countries")
        End Sub)
 
    OnModelCreatingPartial(modelBuilder)
 
End Sub

Walk through configuring the above.

Working with the Customer entity. The following describes the primary key for the Customer table, to get the key name PK_Customers1 run the following script. Keep the script results open as they will be needed below.

entity.HasKey(Function(customer) customer.CustomerIdentifier).
                             HasName("PK_Customers_1")

ModifiedDate column of Customers has a default value of GetDate so we need to tell EF Core about this.

entity.Property(Function(customer) customer.ModifiedDate).
                                             HasDefaultValueSql("(getdate())")

Next, tell EF Core about relations using the script to get the foreign key names.

entity.HasOne(Function(customer) customer.Contact).
                             WithMany(Function(contacts) contacts.Customers).
                             HasForeignKey(Function(customer) customer.ContactId).
                             HasConstraintName("FK_Customers_Contacts")
 
entity.HasOne(Function(customer) customer.ContactTypeNavigation).
                             WithMany(Function(contactType) contactType.Customers).
                             HasForeignKey(Function(customer) customer.ContactTypeIdentifier).
                             HasConstraintName("FK_Customers_ContactType")
 
entity.HasOne(Function(customer) customer.CountryIdentifierNavigation).
                             WithMany(Function(countries) countries.Customers).
                             HasForeignKey(Function(customer) customer.CountryIdentifier).
                             HasConstraintName("FK_Customers_Countries")

EntityTypeConfigurationtion methods 

  • HasKey method is used to denote the property that uniquely identifies an entity (the EntityKey), and which is mapped to the Primary Key field in a database
  • HasForeignKey method is used to specify which property is the foreign key in a relationship.
  • HasConstraintName method enables you to specify the name of the foreign key constraint for a relationship.
  • WithMany method is used to configure the many side of a one-to-many relationship.

Database Connection

Rather than hard code the database connection string, the following class reads the connection string from the app.config file. This is common practice for changing from development, test and production database environments.

Imports System.Configuration
 
Namespace Classes
    ''' <summary>
    ''' Get database server and default catalog from app.config
    ''' </summary>
    Public Class  ConfigurationHelper
 
        Private Shared  Function GetAppSetting(config As Configuration, key As String) As  String
 
            Dim element As KeyValueConfigurationElement = config.AppSettings.Settings(key)
 
            If element IsNot Nothing Then
                Dim value As String  = element.Value
                If Not  String.IsNullOrWhiteSpace(value) Then
                    Return value
                End If
            End If
 
            Return ""
 
        End Function
 
        Public ReadOnly  Property ConnectionString() As String
            Get
 
                Dim config As Configuration = Nothing
                Dim exeConfigPath As String  = Me.GetType().Assembly.Location
                Try
                    config = ConfigurationManager.OpenExeConfiguration(exeConfigPath)
                Catch ex As Exception
                End Try
 
                If config IsNot Nothing Then
                    Return $"Data Source={GetAppSetting(config, "DatabaseServer")};" &
                           $"Initial Catalog={GetAppSetting(config, "DefaultCatalog")};" &
                           "Integrated Security=True"
                Else
                    Return ""
                End If
            End Get
        End Property
    End Class
End Namespace

app.config

<appSettings>
  <add key="DatabaseServer" value = ".\SQLEXPRESS" />
  <add key="DefaultCatalog" value = "NorthWindAzureForInserts" />
  <add key="UsingLogging" value = "true" />
</appSettings>

Loading data - Includes

Entity Framework Core uses eager for loading data.

Eager loading is the process whereby a query for one type of entity also loads related entities as part of the query, so that we don't need to execute a separate query for related entities. Eager loading is achieved using the Include() method.

This means the following statement to return the first customer will not have navigation properties populated.

Public Shared  Sub Simple()
    Using context As  New CustomerContext
        Dim customer = context.Customers.FirstOrDefault()
    End Using
End Sub

To get navigation properties use .Include and .ThenInclude (which is not used here).

Public Shared  Sub Simple()
    Using context As  New CustomerContext
        Dim customer = context.Customers.Include(Function(cust) cust.Contact).
                Include(Function(cust) cust.ContactTypeNavigation).
                Include(Function(cust) cust.CountryIdentifierNavigation).
                FirstOrDefault()
    End Using
End Sub

If in a application a read operation is only performed once the above code is fine while when there are several reads consider using the following language extension methods.

The first returns all customers along with their navigation properties while the second returns a single customer. Both extensions are included in the GitHub repository source. Note that both are asynchronous methods where an asynchronous method such as these could be a millisecond slower then it's synchronous counter part using asynchronous means the user interface remains responsive.

Simple example

Public Shared  Async Sub  Simple(Optional  identifier As  Integer = 1)
    Using context As  New CustomerContext
        Dim customer = Await context.Customers.IncludeContactCountry(identifier)
    End Using
End Sub
Imports System.Runtime.CompilerServices
Imports EntityFrameworkCoreNorthWind.NorthWindEntityFrameworkCore
Imports Microsoft.EntityFrameworkCore
 
Namespace Extensions
 
    Public Module  QueryExtensions
        <Extension>
        Public Function  IncludeContactCountry(
            query As  IQueryable(Of Customers)) As IQueryable(Of Customers)
            Return query.
                Include(Function(customer) customer.Contact).
                Include(Function(customer) customer.ContactTypeNavigation).
                Include(Function(customer) customer.CountryIdentifierNavigation)
 
        End Function
        <Extension>
        Public Function  IncludeContactCountry(
            query As  IQueryable(Of Customers), identifier As Integer) As  Task(Of Customers)
            Return query.
                Include(Function(customer) customer.Contact).
                Include(Function(customer) customer.ContactTypeNavigation).
                Include(Function(customer) customer.CountryIdentifierNavigation).
                FirstOrDefaultAsync(Function(customer) customer.CustomerIdentifier = identifier)
 
        End Function
    End Module
End Namespace

Making changes while using these methods when attached to a DbContext and hooking up to a BindingList with implementing INotifyPropertyChanged any changes will be saved via SaveChanges. If data is not going to change a projection can be used.

Projections

A projection is just a way of mapping one set of properties to another. In relation to Entity Framework specifically, it’s a way of translating a full entity (database table) into a VB.NET class with a subset of those properties. The values can also be altered/joined/removed on the way through as well. Anything you can do in a SELECT statement in SQL is possible. (from Expression and Projection Magic for Entity Framework Core)

The following class is a projection which returns a subset of properties of Customers class and are read only.

Imports System.Linq.Expressions
Imports EntityFrameworkCoreNorthWind.NorthWindEntityFrameworkCore
 
Namespace Projections
    Public Class  CustomerItem
        Public Property  CustomerIdentifier() As Integer
        Public Property  CompanyName() As  String
        Public Property  ContactName() As  String
        Public Property  ContactId() As  Integer?
        Public Property  CountryIdentifier() As Integer?
        Public Property  CountryName() As  String
        Public Property  ContactTypeIdentifier() As Integer?
 
        Public Overrides  Function ToString() As String
            Return $"{CompanyName} - {ContactName}"
        End Function
 
        Public Shared  ReadOnly Property  Projection() As  Expression(Of Func(Of Customers, CustomerItem))
            Get
                Return Function(customers) New  CustomerItem() With  {
                        .CustomerIdentifier = customers.CustomerIdentifier,
                        .CompanyName = customers.CompanyName,
                        .ContactId = customers.ContactId,
                        .ContactName = $"{customers.Contact.FirstName} {customers.Contact.LastName}",
                        .CountryIdentifier = customers.CountryIdentifier,
                        .CountryName = customers.CountryIdentifierNavigation.Name,
                        .ContactTypeIdentifier = customers.CountryIdentifier
                    }
            End Get
        End Property
    End Class
End Namespace

To implement, returning all Customers.

Method within a class.

Public Shared  Async Function  ReadCustomersProjected() As Task(Of List(Of CustomerItem))
 
    Return Await Task.Run(
        Async Function()
            Using context As  New CustomerContext
                Return Await context.Customers.IncludeContactCountry.Select(CustomerItem.Projection).ToListAsync()
            End Using
        End Function)
 
End Function

In a form load data into a ComboBox.

Private Async Sub ListCustomerItemsButton_Click(sender As Object, e As  EventArgs) _
    Handles ListCustomerItemsButton.Click
 
    Dim customerItem As List(Of CustomerItem) =
            Await Operations.ReadCustomersProjected()
 
    CustomerItemComboBox.DataSource = customerItem
 
End Sub

In the sample project which is very simple several read methods are demonstrated by populating ComboBox and TextBox controls. Demonstrations were intentionally done simple for easy of learning. In a following up to this article code samples will be presented using the same class project and windows form project to work with a DataGridView to present data with text and combo box DataGridView columns with immediate updates similar to how Microsoft Access performs updates. The code for this will be adapted from several article using Entity Framework, see the GitHub repository which list the TechNet articles. Another follow up will be on sorting Entity Framework Core data in a DataGridView, see the following article for how this is done in Entity Framework which will work with Entity Framework Core.

 

External resources

Various sources to assist learning how to work with Entity Framework Core. Note that most show code in C#

Microsoft documentation: 

Other

Summary

In this article, first steps to implement Entity Framework Core into a Windows Form solution have been presented. There will be several following up articles teaching how to implement Entity Framework Core with Windows forms controls and customizing the DbContext. 

See also

VB.NET Entity Framework: Wiki portal

Source code

Clone, open in Visual Studio or download, unzip and open in Visual Studio.

https://github.com/karenpayneoregon/ef-core-projections

Once opened in Visual Studio copy the database script included in the Visual Studio solution, run in SSMS followed by performing a NuGet restore packages.