-
Notifications
You must be signed in to change notification settings - Fork 39
Entity Framework
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.
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 is the most popular ORM in .NET. It's one of the best tools to rapidly prototype persistence.
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!).
You have 2 options: command line or visual GUI.
Right click on a project you want to add packages to. Select Manage 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).
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:
-
Click on
Tools
(1), selectNuGet Package Manager
(2) and selectPackage Manager Console
. -
A new window will open at the bottom. Type the following
dotnet add package Microsoft.EntityFrameworkCore
and hit enter.
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>
.
Multiple DbSet
s are then stored as properties of a DbContext
. It's used for grouping different DbSet
s 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.
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.
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 DbSet
s.
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 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.
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.
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 can be done in 2 different ways.
- Get existing item
- Make changes
- 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();
}
- Make changes
- Attach entity to the context
- 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).
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);
}
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.
- Create a new model and map non-suitable model to a stuiable model.
Instead of doing multiple Remove(x)
, Add(x)
do RemoveRange(xs)
, AddRange(xs)
.
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.
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 ==
.
Refactor lesson 8 and use a database for persistence using EF Core. Persist electricity providers.
- 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?
Fundamentals of practical programming
Problem 1: International Recipe ConverterLesson 1: C# Keywords and User Input
Lesson 2: Control Flow, Array and string
Lesson 3: Files, error handling and debugging
Lesson 4: Frontend using WinForms
RESTful Web API and More Fundamentals
Problem 2: Your Online Shopping ListLesson 5: RESTful, objects and JSON
Lesson 6: Code versioning
Lesson 7: OOP
Lesson 8: Understanding WebApi & Dependency Injection
Lesson 9: TDD
Lesson 10: LINQ and Collections
Lesson 11: Entity Framework
Lesson 12: Databases and SQL