We use cookies to provide you with a great user experience, analyze traffic and serve targeted promotions.   Learn More   Accept
Card image cap

Exploring ASP.NET Core Fundamentals - Understanding ORMs and Using EF Core

ASP.NET Core  • Posted one year ago

Entity Framework Core (shortly known as EF Core) is an ORM (object relational mapper) developed by Microsoft for .NET Framework. It is lightweight, extensible and open-sourced similar to the features of Dotnet Core. It helps developers conveniently work with database objects without having need to write SQL scripts for the same. An ORM (Object Relational Mapper) acts as an interface to the Database from the business layer and translates database objects (such as tables) into plain class objects, so that the object manipulations on the classes translate into database operations. EF Core works in tandem with datastructures such as Lists for handling sets of objects for a particular class type, which translates to a table entity. The properties of the class type translate to the columns of the type defined by the property data types.

Since .NET framework comes with LINQ (Linked Queries) on the generic types, its easy to conceptualize and visualize a query operation or a database transaction (DML / Data Manipulation operations). In this article we shall look into configuring EF Core on a Dotnet Core application and how a table can be created and data operations are performed. While the actual Entity Framework of the .NET Framework comes with two approaches: Code-First and Database-First approach, the EF Core offers code-first approach by design.

Prerequisite - Install EF Core (For ASP.NET Core 3.0 and Above)

Starting with dotnet core SDK 3.0 and above, we need to install EF Core as an additional tool on top of the core SDK using the below command:


> dotnet tool install dotnet-ef --global

This installs the EF Core toolkit globally within our machine.

Configuring EF Core in Dotnet Core Application:

EF Core doesn't come pre installed with any template of dotnet core, and needs to be installed using the below CLI commands:

dotnet add package Microsoft.EntityFrameworkCore.SqlServer

dotnet add package Microsoft.EntityFrameworkCore.Design	

The first command installs EF Core with library support for Sql Server provider, other providers include MySql, SQLite and In Memory database.

The second command installs EF Core CLI packages for dotnet CLI, which we shall use to create and update databases.

Once these commands are run in any project directory, the EF Core libraries will be installed and the project will be configured to use EF Core for database operations. Next, we need to provide EF Core with connection string of the database that we're interested in to connect to. In general, the connection strings are placed in the appsettings.json file under the section "ConnectionStrings", which can contain more than one connection string setting as a key value pair, similar to the example below:

"ConnectionStrings": {
    "DefaultConnection": "Server=abc.mysite.net;Initial Catalog=mydevdb;Persist Security Info=False;User ID=admin;Password=abc@123;MultipleActiveResultSets=False;TrustServerCertificate=False;Connection Timeout=30;"
    "DevConnection": "Server=dev.mysite.net;Initial Catalog=mydevdb;Persist Security Info=False;User ID=admin;Password=abc@123;MultipleActiveResultSets=False;TrustServerCertificate=False;Connection Timeout=30;"
  }

And these connection strings come as a part of the IConfiguration object which is injected into the Startup class constructor as shown below:

public class Startup
{
	// configuration settings are bound from appsettings.json and are 
  // available under the IConfiguration as key-value dictionary
	public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

	// services definition
	public void ConfigureServices(IServiceCollection services)
        {
	}

	public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
	}

}

EF Core is configured and inducted into the pipeline with a predefined connection string as shown below:

public class Startup
{
	// configuration settings are bound from appsettings.json and are 
  // available under the IConfiguration as key-value dictionary
	public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

	// services definition
	public void ConfigureServices(IServiceCollection services)
        {
		services.AddDbContext<DatabaseContext>((options) =>
            	{
                	options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")
                  );
            	});
	}

	public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
	}

}

The method Configuration.GetConnectionString("DefaultConnection") returns the connection string of the key name "DefaultConnection" from the "ConnectionStrings" section.

Here we pass in a type DatabseContext which is defined as shown below:

public class DatabaseContext : DbContext
{
	public DatabaseContext(DbContextOptions<DatabaseContext> options) 
  : base(options)
        {
        }

	protected override void OnConfiguring(
    DbContextOptionsBuilder optionsBuilder
  )
        {
		// additional database configurations can come under here
        }

	protected override void OnModelCreating(ModelBuilder builder)
        {
		// any manipulations or processing of model data like 
    // initializing sample data into database etc., can be done under here
	}

	public DbSet<User> Users { get; set; }
}

public class User 
{

	[Key]
        [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
	public int Id {get; set;}

	public string Name {get; set;}

}

In the above code snippet, the DatabaseContext class extends the DbContext class which provides the methods and constructor for passing in configurations and performing additional data processing while connecting or passing data onto the database via EF Core. The OnConfiguring method takes in the same parameters as defined in AddDbContext in Startup.cs; an instance of DbContextOptionsBuilder. This instance can be used to define settings such as the provider, the connection string and others. It can be noted that the EF Core looks for configuration setup using the DbContextOptionsBuilder object during induction, if not found then triggers the method OnConfiguring for settings. Hence it is required that the settings be configured either at the startup level or at the OnConfiguring method.

Then comes the property Users which is of type DbSet. It infers to the mapping of this property Users to an auto-generated databse object Users, which is of type table. And the properties defined in the type User will be translated into columns for the table Users in database. It can also be observed that the property Id bears two attributes Key and DatabaseGenerated, which are from the namespace System.ComponentModel.DataAnnotations. These are used to define schema related attributes such as PrimaryKey, ForeignKey, Indexes and The Indentity. In this case, the attribute Key defines that the field Id is a primary key for the table Users and the attribute DatabaseGeneratedOption.Identity defines that the field shall use auto incremented Identity for values.

Read: Database-First approach for Existing Databases using EF Core

Now that the types and configurations are done, when we run the code the schema will not be generated onto the database. Why? Because we need to instruct the EF Core to treat these as changes (also called as Migrations) and direct the ORM to replicate these to the databse, which we shall do using the EF Core CLI.

step 1: Create Migration - which is actually a changeset of all the code-level schema changes we've made that needs to be updated on to the database. EF Core maintains these changesets in the form

of c# classes under the directory Migrations. And when we run the below command:

dotnet ef migrations add 'InitialCreate' <Or any other name you may wish to use>

the library auto generates a class InitialCreate which contains two methods Up and Down. Up corresponds to the changes that need to be replicated when the database is generated, and Down corresponds to the changes that need to be removed. One the above command completes, we can start to observe the changes reflected into our database.

step 2: We commit these changes onto the database, using the below command:

dotnet ef database update

And now when we run the application, the EF Core establishes a mapping between the auto generated table Users, with the DbSet property Users in the DatabaseContext class. To query data from the table Users, we can simply run a query on the local property Users of the DatabaseContext object. This object is accessed by injecting the object wherever is required. You may ask what would be the lifetime of the object DatabaseContext, well by default it is scoped, which means only one object is created for a request and is reused. It is also important to remember that a DatabaseContext CANNOT BE A SINGLETON and SHOULD NOT BE INJECTED INTO A SINGLETON.

Data Manipulation using Context:

public MyController : Controller 
{
	DatabaseContext context;
	
	public MyController(DatabaseContext ctx) {

		this.context = ctx;

	}

	public void Query() 
	{
		// querying for records
		var records = context.Users.Where(X => x.Id == 1).FirstOrDefault(); 
    //returns the first record of the table Users which has Id value 1.
	}

	public void Insert() 
	{
		var user = new User { Name = "Alan" };
	
		context.Users.Add(user); 
    //add a record to the table
	
		context.SaveChanges(); 
    //commit changes to table (which actually writes the object onto the database)
	}

	public void Update() 
	{
		var record = context.Users.FirstOrDefault(x => x.Id == 1); 
    //Fetch Record to update

		record.Name = "Bibo"; 
    // do the update

		context.SaveChanges(); 
    // the ORM tracks the changes onto the object modified and 
    // commits changes back to database
	}

	public void Delete()
	{
		var record = context.Users.FirstOrDefault(x => x.Id == 1); 
    //Fetch record to delete

		context.Users.Remove(record); 
    //remove record from the table

		context.SaveChanges(); 
    //commit changes to table (which actually deletes the record from the database)
	}	
}

Although the above approach may seem simple, its actually a bad design to use Context object directly into the calling class. It is because it completely exposes the context and there's no separation between the business and data layers. We can solve this by using the Repository design pattern which we shall discuss in the next article. We shall also discuss much in detail about how the DbContext works and making the Context much more configurable in our next article.