Skip to content

Entity Framework

AlmantasK edited this page Jun 20, 2021 · 11 revisions

Introduction

After we send a message in website like Facebook, the next time we come back- the messages are still there. Not just on our screen, but also on the recipient's. How? The key to answering this question lies in understanding the concept of persistence.

Data which is not persisted is in-memory. Whatever is in-memory, after an application closes will be wiped so that new applications can be opened with their own memory. Persistence makes sure that the data is loaded to and from some external source and not RAM, so that we can keep it for use later. We can persist data in many different ways: file, physically, in a database, etc... If we talk about any serious applications, data persistence will involve a database.

Talking to a databse requires a language of its own. About that- in the next lesson. However, in this lesson you will learn how to take a shortcut and use C# to communicate with a database. You will learn the basics of Entity Framework (EF) Object Relational Mapper (ORM). Lastly, instead of writing a ton of our own code to work with EF, we will use third party code by downloading NuGet packages.

ORM

Object Relational Mapper (ORM)- is a class libarary that takes data returned from a database (in the format of that database) and converts it into language specific objects (in our case C# objects). It maps database types to language types which calls an ORM. It's not a .NET-specific thing and exists in most major programming languages.

Entity Framework

Entity Framework is the most popular ORM in .NET. It's one of the best tools to rapidly prototype persistence.

EF logo

Install NuGet Packages

Before we proceed, you will need to install a base libarary for Entity Framework. EF is not something that is a part of .NET Core or .NET Framework and that is fine. Therefore we need to add a new package to our project. In .NET, this is done using NuGet package manager.

Most code these days will be something that you composed, rather than writing by yourself. NuGet package manager is a tool that allows composing others code within your own project. Consider it a massive repository for lego bricks which you can freely use at your disposal (yes, all that you can find there is totally free!).

NuGet package manager is like a lego box

You have 2 options: command line or visual GUI.

Package Manager GUI

Right click on a project you want to add packages to. Select Manage NuGet packages.

Open Nuget Packages

A new window will open. Inside it, click the browse tab and type EntityFramework(1). If Then select EntityFrameworkCore package (2) and hit the install button (3). If the installation is successful- you will see a green checkmark next to the selected package (4).

Install EF Core

Console

If you prefer less clicks, straight to the point actions, then maybe a console window is more to your preference? In order to do the same with the console window, do the following:

  1. Click on Tools (1), select NuGet Package Manager (2) and select Package Manager Console.

    Install EF Core

  2. A new window will open at the bottom. Type the following dotnet add package Microsoft.EntityFrameworkCore and hit enter.

Data = DbSet

In EntityFramework, abstraction of many data is called a DbSet. DbSet is a generic data structure- DbSet<T>- DbSet of something.

Let's say we have a class Item:

public class Item
{
    public int Id{get;set;}
    public string Name{get;set;}
    public decimal Price{get;set;}
}

Item in our case is an Entity- a class made for persistence. Entities might either be shared models or dedicated models for persistence. It all depends on your scenario and how convenient it is for persisting/isolating the original (business logic) and persistence models.

For example, if you have many items, in order to persist them you would need a DbSet<Item>.

DbContext- Container of all Data

Multiple DbSets are then stored as properties of a DbContext. It's used for grouping different DbSets and applying changes in a database.

A context with a single DbSet would like like this:

public class StorageContext: DbContext
{
    public DbSet<Item> Items{get;set;}
}

Please note that we had to inherit a DbContext class.

Setting up Context

Now that we have the most simple DbContext we need to prepare it for initialization. We will be using a SQLite database. EF by itself is provider-agnostic. However, it is extensible and many 3rd party provider have integrated to it. In order to add support for a specific provider you will need to add yet another NuGet package.

In our case- Microsoft.EntityFrameworkCore.Sqlite. Either look it up in the NuGet browser window or install it using NuGet package manager console: dotnet add package Microsoft.EntityFrameworkCore.Sqlite.

Next, update the context class to include a method of connecting to the database using our chosen provider. When connecting to a SQL database, you will need to provide all the needed info of that database. Such info comes in a form of a special string called connection string. SQLite is an in-memory datatbase, so all we have to do is supply a path to DataSource. Our connection string will look like this: DataSource=ShoppingList.db;. The updated DBContext will look like this:

    public class StorageContext: DbContext
    {
        public DbSet<Item> Items{get;set;}

        public StorageContext(): base(UseSqlite())
        {
        }

        private static DbContextOptions UseSqlite()
        {
            return new DbContextOptionsBuilder()
                .UseSqlite(@"DataSource=StorageContext.db;")
                .Options;
        }
    }

Please note the use of UseSqlite. This might remind you of LINQ. How do providers, without having access to the original EF code able to make integrations to it? That's right, they use extension methods. UseSqlite is an extension method to DbContextOptionsBuilder which sets up SQLite.

Providers and Future Considerations

We should not be coupled to a single database provider simply because it might change. It's easy to be ready for different adapters. If DbContextOptionsBuilder is what allows specifying a provider, then by simply exposing it in a constructor we will be able to support every provider we want. A compelte StorageContext class will now look like this:

    public class StorageContext: DbContext
    {
        public DbSet<Item> Items{get;set;}

        // Supports injecting any provider we want
        public StorageContext(DbContextOptions<StorageContext> options): base(options)
        {
        }

        public StorageContext(): base(UseSqlite())
        {
        }

        private static DbContextOptions UseSqlite()
        {
            return new DbContextOptionsBuilder()
                .UseSqlite(@"DataSource=StorageContext.db;")
                .Options;
        }
    }

Please note that we have a single DbSet for demo purposes. Normally a context contains multiple DbSets.

Initialization

The remaining bit is wiring it all together within our application. You can simply initialize a DbContext, however if you are using a WebApi, or any IoC (inversion of control) container, you should use that container's injection mechanism. For a standard .NET Core container, use services.AddDbContext<StorageContext>();.

That done, by simply injecting StorageContext to whichever class it's needed, you can manage all the related and persisted data of it.

CRUD

CRUD is an acronym: Create Read Update Delete. All that you can do with data essentially fit within those 4 operations. If you have implemented them all for a single entity, then you can do whatever you want with that entity.

Most operations in EF are extremely similar to basic LINQ.

Let's say we have a service with all 4 methods:

public class ItemsRepository
{
    private readonly StorageContext _context

    public ItemsRepository(StorageContext context)
    {
        _context = context;
    }

    public void Create(Item item){...}
    public Item Get(int id){...}
    public IEnumerable<Item> Get(){...}
    public void Delete(int id){...}
    public void Update(Item item, int id){...}
}

Repository is a class that manages CRUD of a specific entity. The above is a typical collection of methods for it. It doesn't always have to be all 4 operations, but it should provide a simple way to manage item's state.

Create

In order to create a new Item to a db, we can:

public void Create(Item item)
{
    var item = new Item(){...};
    context.Items.Add(item);
    context.SaveChanges();
}

Note that at the end of a context, we called SaveChanges(). By default, DbContext lives in a transaction- nothing is changed until you confirm the changes. If things go wrong, the changes won't be done.

Read

Typically, a get operation involves getting a single item by id and all items.

public Item Get(int id)
{
    var item = context.Items.Find(id);
    return item;
}
public IEnumerable<Item> Get()
{
    var items = context.Items.ToList();
    return items;
}

The two methods are basic LINQ. However, it's worth putting emphasis on the second one's ToList. It's needed, because by default querying a DbSet returns IQueryable- and that is an expression that will call a database. If we don't call ToList, we will be working with a database under the hood. ToList executes the underlying SQL of IQueryable and returns the results in-memory.

Also, please note the use of Find in the first one. We could have used First and pass a lambda. There is a small difference between the two. Find will first look if Item was already retrieved and only then try to get it from a database. First will always get it from a database.

Update

Update can be done in 2 different ways.

Get

  1. Get existing item
  2. Make changes
  3. Save changes
    public void Update(Item item, int id)
    {
        var existingItem = context.Items.Find(id);
        existingItem.Name = item.Name;
        existingItem.Price= item.Price;
        context.SaveChanges();
    }

Attach

  1. Make changes
  2. Attach entity to the context
  3. Save changes
    public void Update(Item item)
    {
        // assumes that item comes with all the updates and includes id.
        context.Attach(item);
        context.SaveChanges();
    }

This is a niche scenario, way less preferable to the first option due to unpredicatability of it (when id doesn't exist).

Delete

Update is just a matter of finding an element and then removing it from a DbSet.

    public void Delete(int id)
    {
        var item = new Item(){Id = id};
        context.Items.Remove(item);
    }

or

    public void Delete(int id)
    {
        var item = context.First(i => i.Id == id);
        context.Items.Remove(item);
    }

Testing EF

Due to the support of multiple different db providers, EF comes with a lot of flexibility when it comes to testing.

The most simple and fastest kind of tests for EF are InMemory tests. For this, you will need InMemory db provider.

Install the following: Microsoft.EntityFrameworkCore.InMemory:

dotnet add package Microsoft.EntityFrameworkCore.InMemory

And when testing, simply feed the in memory options:

    public abstract class DbTests
    {
        protected ShoppingContext Context { get; set; }

        public DbTests()
        {
            Context = new ShoppingContext(
                new DbContextOptionsBuilder<ShoppingContext>()
                    .UseInMemoryDatabase(Guid.NewGuid().ToString())
                    .Options);
        }
    }

And then simply inherit this class.

In other cases- simple SQL might be involved- in those cases use Sqlite provider. And in other cases, provider-specific sql might be used- in those cases make sure you are using an actual local database of that provider so that the connected context and queries are compatible.

Pitfalls Of Persistence

My Model is Not Suitable For Persistance

  • Create a new model and map non-suitable model to a stuiable model.

Performance

Multiple Adds/Removes

Instead of doing multiple Remove(x), Add(x) do RemoveRange(xs), AddRange(xs).

Need Top Performance

Don't use EF. Use raw SQL ADO.NET or micro ORM- Dapper. SQL is meant for optimal execution and no matter how good of an ORM EF is, it still does a lot under the hood: tracking of the state, converting C# expressions into SQL, etc. With raw SQL we can go straight to executing exactly the SQL we want.

Using Too C#-Specific Expressions

You might be tempted to write .Equals(someName) when searching item by name. In other cases you might use a lib to square numbers and do searches that way. In both cases when EF tries to execute queries like these- it will run them in memory. In other words- it will get all the records and apply the filtering in-memory rather than in-db. In order to avoid that, try to stick to simple queries for EF. For example, instead of Equals use ==.

Homework

Refactor lesson 8 and use a database for persistence using EF Core. Persist electricity providers.

Did I understand the Topic?

  • What is an ORM?
  • What is a persistence?
  • What does CRUD stand for?
  • What is Entity Framework?
  • What is an entity?
  • What do we need to call after we have modified an entity of a database in order to persist the changes?
  • Why might it be a good idea to have a separate model for business logic and for persistence?
  • In what scenarios should EF not be used? What are the alternatives?
  • Why do we need a DbSet?
  • Why do we need a DbContext?
  • Why should we create a parameterised ctor for a DbContext?
  • Which db provider is the most suitable for testing EF?
  • In what scenarios is InMemory db provider not suitable for testing? What are the alternatives?
  • How many DbContexts can an application have?
Clone this wiki locally