How to use EntityframeworkCore's In-Memory Database and Dependency Injection in Console application


What you will learn

  • What is Entity Framework Core In-Memory?
  • Situations where EF Core In-Memory is useful
  • What is Dependency Injection
  • Use in-memory provider and dependency injection in .net core console application

What you should know / have

  • Comfortable coding in C# programming language
  • Visual Studio 2019 or above. Download here
  • Basic knowledge of Entity Framework Core
  • How to add packages in Visual Studio using Nuget Package Manager

Functional Requirements

  • Display all Customers
  • Display first 10 Orders
  • Display all orders from Customer with ID = 1
  • Display Total amount of orders for Customer with ID = 1

Introduction

This post demonstrates how to use EF Core In-Memory database provider and Dependency Injection in .NET Core console application. We will achieve this by implementing all functional requirements mentioned above. Complete source code available here.

What is Entity Framework Core In-Memory?

EF Core In-Memory is a database provider for Entity Framework Core. It is useful when testing components that require simulations of database operations like Create, Read, Update and Delete. It eliminates the overhead of setting up an actual database.

Situations where EF Core In-Memory is useful

Some situations where in-memory database is suitable are:

  • Test doubles such as mock and fake are suitable for testing a piece of business logic that might need some data from database, but is not essentially testing database interactions. However, when unit testing something that uses DbContext, instead of mocking DbContext or IQueryable, use an in-memory database. Using an in-memory database is appropriate because the test is not dependent on database behaviors.
  • When building prototypes where database performance is not required or tested. This is especially helpful in a constrained environment with no access to databases. Sharing your application becomes easy because database setup is not required.

WARNING: The in-memory database is designed for testing only. It often behaves differently than relational databases hence should not be used to match real relational database. If it is expensive to test against production database, consider using SQLite database provider in-memory mode to match common relational database behaviors more closely.

What is Dependency Injection?

According to Wikipedia, dependency injection is a technique in which an object receives other objects that it depends on. These other objects are called dependencies. In the typical "using" relationship the receiving object is called a client and the passed object is called a service.

Imagine a Car class that depends on other car components or classes such as engine, wheels, etc. to create a complete Car object. When creating a new object of Car, your code need not worry about other components the Car class depends on, even though it needs them to function. The Car class provides the dependencies to your code at run-time. One advantage of this technique is the ability to provide a new set of dependencies at run-time. For example, your Car object can use electric engine and alloy wheels instead of the default gas engine and traditional wheels.

Why Dependency Injection?

  1. It allows your class to no longer be responsible for instantiating its own dependencies.
  2. It's about object creation on the fly. Usually handled by Dependency Injection Container.
  3. It helps you develop loosely coupled code.
  4. It is derived from the Inversion of Control (IoC) concept.

3 ways to inject dependencies:

  1. Constructor injection
  2. Method injection
  3. Property injection

Now that theory is out of the way, let's hit the ground running with some coding. We will begin by creating .NET Core Console Application in Visual Studio or your favorite editor. Note: Some steps may be different depending on your editor or version of Visual Studio. Adjust accordingly.

If you are familiar with Visual Studio, you can skip step 1 to step 5 by creating a new .NET Core Console application Checkout in a solution EShop.

Step 1

Let's start by creating a blank solution first, then we can add our console project afterwards. Run Visual Studio (I'm using Visual Studio 2019) and click on "Create a new project" on the right side of the window as shown below.

step 1 - create a new project

Step 2

From Create a new project window, search for blank solution on the search textbox on top, then click on Blank Solution from the results as shown below and click Next at the bottom right of the screen.

step 2 - blank solution

Step 3

In Configure your new project window, type EShop for Solution name and select a location on your computer to store the solution. See mine below. Then click on Create at the bottom right of the screen to create the solution.

step 3 - configure your new project

Step 4

We could have created .NET Core console app from Step 1 above but I prefer to start with a blank solution. Now let's create a new console application project.

Right-click on Solution 'EShop' (0 projects), select Add, then New Project... From the Add a new project window, search for Console App and select Console App (.NET Core) from the list and click Next. See below

step 4 - create asp net core console app

Step 5

In Configure your new project window, name the project as Checkout and click Create at the bottom right of the screen to create the project.

step 5 - configure your new project

This will create a new .NET Core console application Checkout in EShop solution as shown below.

solution window with project

Now that the project is created, let's move forward by creating model classes we will need in the application.

Step 6

Right-click on Checkout project, select Add, then New Folder to add a new folder to the project. Name the folder Models. This folder will store our domain models. We will be working with 4 model classes:

  1. Customers
  2. Items
  3. Orders
  4. OrderDetails

Create Customers class as shown below in the Models folder. Right-click on Models, Add, then select Class. Name the class Customers, add class properties and add references as required. Repeat these steps for Items, Orders and OrderDetails classes.

Customers.cs.

  using System.ComponentModel.DataAnnotations;

  namespace Checkout.Models
  {
      public class Customers
      {
          [Key]
          public int Id { get; set; }
          public string FirstName { get; set; }
          public string LastName { get; set; }
          public string Email { get; set; }
          public string Phone { get; set; }

      }
  }

Items.cs

  using System.ComponentModel.DataAnnotations;

  namespace Checkout.Models
  {
      public class Items
      {
          [Key]
          public int Id { get; set; }
          public string ItemName { get; set; }
          public decimal ItemPrice { get; set; }

      }
  }

Orders.cs

  using System;
  using System.ComponentModel.DataAnnotations;
  using System.ComponentModel.DataAnnotations.Schema;

  namespace Checkout.Models
  {
      public class Orders
      {
          [Key]
          public int Id { get; set; }
          public DateTimeOffset OrderDate { get; set; }

          public int CustomerId { get; set; }

          [ForeignKey("CustomerId")]
          public virtual Customers Customer { get; set; }
      }
  }

OrderDetails.cs

  using System.ComponentModel.DataAnnotations;
  using System.ComponentModel.DataAnnotations.Schema;

  namespace Checkout.Models
  {
      public class OrderDetails
      {
          [Key]
          public int Id { get; set; }
          public decimal ItemPrice { get; set; }
          public int Quantity { get; set; }
          public decimal Total { get { return ItemPrice * Quantity; } }

          public int OrderId { get; set; }

          [ForeignKey("OrderId")]
          public virtual Orders Order { get; set; }

          public int ItemId { get; set; }

          [ForeignKey("ItemId")]
          public virtual Items Item { get; set; }

      }
  }

Now let's build the project to ensure there's no issue.

Click on Build menu, then Build Solution to build the project. If everything went well, you should see Build: 1 succeeded....

Congratulations!!

So far, we have created 4 classes to store data for our application. Now we are ready to create CheckoutDbContext class so we can define DbSet properties for our model classes.

Step 7 - Creating CheckoutDbContext Class

Right-click on Models folder, select Add, then Class... to add a new class. Name the class CheckoutDbContext. This class will inherit from DbContext class.

DbContext class provides a bridge between your model entities and the database. It simplifies the interaction of an application with the database.

To use the DbContext class, add Microsoft.EntityFrameworkCore package using Nuget Package Manager. If you use the code below before adding the package, follow suggestion by Visual Studio to add this package by right-clicking on the squiggle lines under DbContext.

Let's pass DbContextOptions argument to the base class, and create DbSet for our model classes as shown below.

CheckoutDbContext.cs

  using Microsoft.EntityFrameworkCore;

  namespace Checkout.Models
  {
      public class CheckoutDbContext : DbContext
      {
          public CheckoutDbContext(DbContextOptions<CheckoutDbContext> options) 
          : base(options)
          {

          }

          public DbSet<Customers> Customers { get; set; }
          public DbSet<Items> Items { get; set; }
          public DbSet<Orders> Orders { get; set; }
          public DbSet<OrderDetails> OrderDetails { get; set; }
      }
  }

Step 8 - Data Seeding

Data seeding is the process of populating a database with initial set of data. We are going to create some data for our application. Right-click on Models folder and add a new class CheckoutDbContextSeedData. Populate the class with the codes shown below.

CheckoutDbContextSeedData.cs

  using System;
  using System.Linq;

  namespace Checkout.Models
  {
      public static class CheckoutDbContextSeedData
      {
          static object synchlock = new object();
          static volatile bool seeded = false;

          public static void EnsureSeedData(this CheckoutDbContext context)
          {
              if (!seeded && context.Customers.Count() == 0)
              {
                  lock (synchlock)
                  {
                      if (!seeded)
                      {
                          var customers = GenerateCustomers();
                          var items = GenerateItems();
                          var orders = GenerateOrders();
                          var orderDetails = GenerateOrderDetails();

                          context.Customers.AddRange(customers);
                          context.Items.AddRange(items);
                          context.Orders.AddRange(orders);
                          context.OrderDetails.AddRange(orderDetails);

                          context.SaveChanges();
                          seeded = true;
                      }
                  }
              }
          }

          #region Data
          public static Customers[] GenerateCustomers()
          {
              return new Customers[] {
                  new Customers
                  {
                      FirstName = "George",
                      LastName  = "Santos",
                      Email     = "hello@test.com",
                      Phone     = "1234-5678-90"
                  },
                  new Customers
                  {
                      FirstName = "Edisson",
                      LastName  = "Williams",
                      Email     = "test@edi.com",
                      Phone     = "000-000-000"
                  },
                  new Customers
                  {
                      FirstName = "Josephine",
                      LastName  = "Maxwell",
                      Email     = "info@club.com",
                      Phone     = "212-000-333"
                  },
                  new Customers
                  {
                      FirstName = "Susan",
                      LastName  = "McDonald",
                      Email     = "susy@test.com",
                      Phone     = "333-000-333"
                  },
              };
          }

          public static Items[] GenerateItems()
          {
              return new Items[] {
                  new Items
                  {
                      ItemName = "Iphone 13",
                      ItemPrice = 1999.9M
                  },
                  new Items
                  {
                      ItemName = "Samsung Galaxy F",
                      ItemPrice = 1009.5M
                  },
                  new Items
                  {
                      ItemName = "MacBook Pro",
                      ItemPrice = 2999.9M
                  },
                  new Items
                  {
                      ItemName = "Smart LG TV",
                      ItemPrice = 1500M
                  },
              };
          }

          public static Orders[] GenerateOrders()
          {
              return new Orders[] {
                  new Orders
                  {
                      OrderDate = DateTimeOffset.UtcNow,
                      CustomerId = 1
                  },
                  new Orders
                  {
                      OrderDate = DateTimeOffset.UtcNow,
                      CustomerId = 2
                  },
                  new Orders
                  {
                      OrderDate = DateTimeOffset.UtcNow,
                      CustomerId = 3
                  },
                  new Orders
                  {
                      OrderDate = DateTimeOffset.UtcNow,
                      CustomerId = 4
                  },
              };
          }

          public static OrderDetails[] GenerateOrderDetails()
          {
              return new OrderDetails[] {
                  new OrderDetails
                  {
                      OrderId = 1,
                      ItemId = 1,
                      ItemPrice = 1999.9M,
                      Quantity = 4
                  },
                  new OrderDetails
                  {
                      OrderId = 1,
                      ItemId = 2,
                      ItemPrice = 1009.5M,
                      Quantity = 5
                  },
                  new OrderDetails
                  {
                      OrderId = 1,
                      ItemId = 3,
                      ItemPrice = 2999.9M,
                      Quantity = 6
                  },
                  new OrderDetails
                  {
                      OrderId = 1,
                      ItemId = 4,
                      ItemPrice = 1500M,
                      Quantity = 4
                  },
                  new OrderDetails
                  {
                      OrderId = 2,
                      ItemId = 4,
                      ItemPrice = 1500M,
                      Quantity = 2
                  },
                  new OrderDetails
                  {
                      OrderId = 3,
                      ItemId = 3,
                      ItemPrice = 2999.9M,
                      Quantity = 6
                  },
                  new OrderDetails
                  {
                      OrderId = 4,
                      ItemId = 1,
                      ItemPrice = 1999.9M,
                      Quantity = 6
                  },
              };
          }

          #endregion
      }
  }

We are making this class static because we want to use it as an extension method in CheckoutDbContext class. The this CheckoutDbContext context in EnsureSeedData method makes EnsureSeedData method an extension method of CheckoutDbContext class.

This class provides the data we need to use in our application. Now let's call the EnsureSeedData method in CheckoutDbContext class constructor. Updated CheckDbContext constructor should look like the code below.

CheckoutDbContext.cs class constructor

    public CheckoutDbContext(DbContextOptions<CheckoutDbContext> options) 
    : base(options)
    {
        this.EnsureSeedData(); // new line added
    }

Build the solution to ensure it succeeds.

We have set up CheckoutDbContext and CheckoutDbContextSeedData classes for our application, but we have not yet informed DbContext which database provider to use. Let's handle that in the next steps.

Step 9 - Using In-Memory Database provider

Now we are ready to tell DbContext class to use In-Memory database for our application. Before we do that, we need to add Microsoft.EntityFrameworkCore.InMemory package to the solution.

Right-click on the solution, choose Manage Nuget Packages for Solution.... In the Nuget Package Manager window, search for Microsoft.EntityFrameworkCore.InMemory and add it to the Checkout project.

Now that In-Memory database provider is added to our project, there are two ways we can set it up:

  1. By overriding OnConfuguring method of the DbContext class as shown below

      protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
      {
          optionsBuilder.UseInMemoryDatabase("EShop");
      }
  2. In the service class for Checkout project, and passing that option to the context class. This is the option we are going for.

Step 10 - Preparing the Service class

In the Models folder, create a new class OrderDetailsDto. This class will serve as data table object when querying Order data from the database. We are creating it at this stage because it will be used in the next, in ICheckoutService interface.

OrderDetailsDto.cs

  namespace Checkout.Models
  {
      public class OrderDetailsDto
      {
          public string CustomerName { get; set; }
          public string OrderItem { get; set; }
          public decimal OrderItemPrice { get; set; }
          public int OrderQuantity { get; set; }
          public decimal OrderTotal { get; set; }

      }
  }

Create a new folder Services in Checkout project, add an interface ICheckoutService and a new class called CheckoutService. The CheckoutService class will serve as repository class for handling all database related operations for the application.

ICheckoutService.cs interface

  using Checkout.Models;
  using System.Collections.Generic;
  using System.Linq;
  using System.Threading.Tasks;

  namespace Checkout.Services
  {
      public interface ICheckoutService
      {
          Task<IEnumerable<Customers>> GetAllCustomers();
          IQueryable<OrderDetailsDto> GetAllOrders();
          Task<IList<OrderDetailsDto>> GetOrdersByCustomer(
          int customerId, int orderId);
      }
  }

Add the following code to CheckoutService class.

CheckoutService.cs

  using Microsoft.EntityFrameworkCore;
  using Checkout.Models;
  using System.Collections.Generic;
  using System.Threading.Tasks;
  using System.Linq;

  namespace Checkout.Services
  {
      public class CheckoutService: ICheckoutService
      {
          private readonly CheckoutDbContext _context;
          public CheckoutService()
          {
              var options = new DbContextOptionsBuilder<CheckoutDbContext>()
                  .UseInMemoryDatabase("Checkout")
                  .Options;

              _context = new CheckoutDbContext(options);
          }

          public async Task<IEnumerable<Customers>> GetAllCustomers() => 
          await _context.Customers.ToListAsync();

          public IQueryable<OrderDetailsDto> GetAllOrders()
          {
              return
                  (from a in _context.OrderDetails
                   join b in _context.Orders on a.OrderId equals b.Id
                   join c in _context.Customers on b.CustomerId equals c.Id
                   select new OrderDetailsDto
                   {
                       CustomerName = $"{c.FirstName} {c.LastName}",
                       OrderItem = a.Item.ItemName,
                       OrderItemPrice = a.ItemPrice,
                       OrderQuantity = a.Quantity,
                       OrderTotal = a.Total

                   }).AsQueryable();
          }

          public async Task<IList<OrderDetailsDto>> GetOrdersByCustomer(
          int customerId, int orderId)
          {
              var orders = from a in _context.OrderDetails
                           join b in _context.Orders on a.OrderId equals b.Id
                           join c in _context.Customers on 
                                       b.CustomerId equals c.Id
                           where c.Id == customerId && b.Id == orderId
                           select new OrderDetailsDto
                           {
                               CustomerName = $"{c.FirstName} {c.LastName}",
                               OrderItem = a.Item.ItemName,
                               OrderItemPrice = a.ItemPrice,
                               OrderQuantity = a.Quantity,
                               OrderTotal = a.Total
                           };

              return await orders.ToListAsync();
          }
      }
  }

First, we are implementing the ICheckoutService interface and providing implementations to its methods. I want to draw your attention to the constructor method of CheckoutService. Inside this method, we specify that the application will use In-Memory database, and this option is then passed to CheckoutDbContext class.

Build the project to confirm everything is OK.

Step 11 - Dependency Injection

We are two steps away from running the application and seeing some data. Recall when we created an interface in Step 10 and implemented its methods in CheckoutService class. Now let's register CheckoutService class as a service in Program.cs because it is the entry point to our application.

Program.cs

  using Checkout.Services;
  using Microsoft.Extensions.DependencyInjection;

  namespace Checkout
  {
      class Program
      {
          static void Main(string[] args)
          {
              var serviceProvider = new ServiceCollection()
                  .AddTransient<ICheckoutService, CheckoutService>()
                  .BuildServiceProvider();
          }
      }
  }

By registering the service, we are instructing ServiceCollection to replace ICheckoutService interface with CheckoutService class anywhere it is used in the application. The primary advantage of this approach is, if there is a new concrete implementation of ICheckoutService, we can replace CheckoutService with the new implementation in one place - where the service was registered. Other advantages are ease of unit testing and loose coupling.

Step 12 - Run the application

Finally we have reached the point where we can run the application to see some output. Now that we have our service registered, let's ask for the service from ServiceCollection.

Program.cs

  ...
  var service = serviceProvider.GetService<ICheckoutService>();

Now we are ready to view Customer data. Add the code below.

Program.cs

  ...
  Console.WriteLine("*********************Customers**********************");
  Console.WriteLine($"ID        Name                Email            Phone");
  foreach (var customer in await service.GetAllCustomers())
  {
      Console.WriteLine($"{customer.Id}     {customer.FirstName}    {customer.LastName}      {customer.Email}      {customer.Phone}");
  }

At this point, your Program.cs class should look like the one below.

Program.cs

    using Checkout.Services;
    using Microsoft.Extensions.DependencyInjection;
    using System;
    using System.Threading.Tasks;

    namespace Checkout
    {
        class Program
        {
            static async Task Main(string[] args)
            {
                var serviceProvider = new ServiceCollection()
                    .AddTransient<ICheckoutService, CheckoutService>()
                    .BuildServiceProvider();

                var service = serviceProvider.GetService<ICheckoutService>();

                Console.WriteLine("************Customers**********************");
                Console.WriteLine($"ID        Name         Email         Phone");
                foreach (var customer in await service.GetAllCustomers())
                {
                    Console.WriteLine($"{customer.Id} {customer.FirstName}
                      {customer.LastName}      {customer.Email}     
                      {customer.Phone}"
                    );
                }

                Console.ReadKey();
            }
        }
    }

Build and run the application. If you did not run into any issue, then your output should match mine.

step 12 - output

Bravo!! That feeling when you see your application running as expected.

As a final step, let's display more records on the screen by calling the rest of the methods in our ICheckoutService interface. Your final Program.cs class should look like this

Program.cs

  using Checkout.Services;
  using Microsoft.Extensions.DependencyInjection;
  using System;
  using System.Linq;
  using System.Threading.Tasks;

  namespace Checkout
  {
      class Program
      {
          static async Task Main(string[] args)
          {
              var serviceProvider = new ServiceCollection()
                  .AddTransient<ICheckoutService, CheckoutService>()
                  .BuildServiceProvider();

              var service = serviceProvider.GetService<ICheckoutService>();

              Console.WriteLine("**************Customers*******************");
              Console.WriteLine($"ID       Name          Email           Phone");
              foreach (var customer in await service.GetAllCustomers())
              {
                  Console.WriteLine($"{customer.Id}     {customer.FirstName} 
                    {customer.LastName}      {customer.Email}      
                    {customer.Phone}"
                  );
              }

              // new code added from here
              int customerId = 1; 
              int orderId = 1;

              Console.WriteLine();
              Console.WriteLine("************ALL ORDERS******************");
              foreach (var order in service.GetAllOrders().Take(10))
              {
                  Console.WriteLine($"{order.CustomerName} - {order.OrderItem} - 
                  {order.OrderItemPrice:C} - " +
                      $"{order.OrderQuantity} - {order.OrderTotal:C}");
              }

              Console.WriteLine();
              Console.WriteLine("******ALL ORDERS FOR CUSTOMER ID: 1*******");
              var orders = await service.GetOrdersByCustomer(customerId, 
              orderId);
              foreach (var order in orders)
              {
                  Console.WriteLine($"{order.CustomerName} - {order.OrderItem} - 
                  {order.OrderItemPrice:C} - " +
                      $"{order.OrderQuantity} - {order.OrderTotal:C}");
              }

              Console.WriteLine();
              Console.WriteLine($"Final Total: {
              orders.Sum(x => x.OrderTotal):C}");

              Console.ReadKey();
          }
      }
  }

Build and run the application. The output will look like the one below.

step 12 - output-2

Conclusion

Congratulations on completing this tutorial on using In-Memory database provider and Dependency Injection technique in .NET Core console application. All four functional requirements are implemented. We have achieved our goals of using in-memory database and dependency injection technique. Share your new knowledge with friends and colleagues to re-enforce the concepts.

Happy coding!!

Comments