Exercise - Interacting with data
In this unit, you'll write code to interact with the database.
CRUD methods
Let's complete the PizzaService
implementation. Complete the following steps in Services\PizzaService.cs:
Make the following changes as shown in the example below:
- Add a
using ContosoPizza.Data;
directive. - Add a
using Microsoft.EntityFrameworkCore;
directive. - Add a class-level field for the
PizzaContext
before the constructor. - Change the constructor method signature to accept a
PizzaContext
parameter. - Change the constructor method code to assign the parameter to the field.
using ContosoPizza.Models; using ContosoPizza.Data; using Microsoft.EntityFrameworkCore; namespace ContosoPizza.Services; public class PizzaService { private readonly PizzaContext _context; public PizzaService(PizzaContext context) { _context = context; } /// ... /// CRUD operations removed for brevity /// ... }
When the
PizzaService
instance is created, aPizzaContext
will be injected as a dependency.- Add a
Replace the
GetAll
method with the following code:public IEnumerable<Pizza> GetAll() { return _context.Pizzas .AsNoTracking() .ToList(); }
In the preceding code:
- The
Pizzas
collection contains all the rows in the pizzas table. - The
AsNoTracking
extension method instructs EF Core to disable change tracking. Since this operation is read-only,AsNoTracking
can optimize performance. - All of the pizzas are returned with
ToList
.
- The
Replace the
GetById
method with the following code:public Pizza? GetById(int id) { return _context.Pizzas .Include(p => p.Toppings) .Include(p => p.Sauce) .AsNoTracking() .SingleOrDefault(p => p.Id == id); }
In the preceding code:
- The
Include
extension method takes a lambda expression to specify that theToppings
andSauce
navigation properties are to be included in the result (eager loading). Without this, EF Core will return null for those properties. - The
SingleOrDefault
method returns a pizza that matches the lambda expression.- If no records match,
null
is returned. - If multiple records match, an exception is thrown.
- The lambda expression describes records where the
Id
property is equal to theid
parameter.
- If no records match,
- The
Replace the
Create
method with the following code:public Pizza Create(Pizza newPizza) { _context.Pizzas.Add(newPizza); _context.SaveChanges(); return newPizza; }
In the preceding code:
newPizza
is assumed to be a valid object. EF Core doesn't do data validation, so any validation must be handled by the ASP.NET Core runtime or user code.- The
Add
method adds thenewPizza
entity to EF Core's object graph. - The
SaveChanges
method instructs EF Core to persist the object changes to the database.
Replace the
UpdateSauce
method with the following code:public void UpdateSauce(int pizzaId, int sauceId) { var pizzaToUpdate = _context.Pizzas.Find(pizzaId); var sauceToUpdate = _context.Sauces.Find(sauceId); if (pizzaToUpdate is null || sauceToUpdate is null) { throw new InvalidOperationException("Pizza or sauce does not exist"); } pizzaToUpdate.Sauce = sauceToUpdate; _context.SaveChanges(); }
In the preceding code:
- References to an existing
Pizza
andSauce
are created usingFind
.Find
is an optimized method to query records by their primary key.Find
searches the local entity graph first before querying the database. - The
Pizza.Sauce
property is set to theSauce
object. - An
Update
method call is unnecessary because EF Core detects that we set theSauce
property onPizza
. - The
SaveChanges
method instructs EF Core to persist the object changes to the database.
- References to an existing
Replace the
AddTopping
method with the following code:public void AddTopping(int pizzaId, int toppingId) { var pizzaToUpdate = _context.Pizzas.Find(pizzaId); var toppingToAdd = _context.Toppings.Find(toppingId); if (pizzaToUpdate is null || toppingToAdd is null) { throw new InvalidOperationException("Pizza or topping does not exist"); } if(pizzaToUpdate.Toppings is null) { pizzaToUpdate.Toppings = new List<Topping>(); } pizzaToUpdate.Toppings.Add(toppingToAdd); _context.SaveChanges(); }
In the preceding code:
- References to an existing
Pizza
andTopping
are created usingFind
. - The
Topping
is added to thePizza.Toppings
collection with the.Add
method. A new collection is created if it doesn't exist. - The
SaveChanges
method instructs EF Core to persist the object changes to the database.
- References to an existing
Replace the
DeleteById
method with the following code:public void DeleteById(int id) { var pizzaToDelete = _context.Pizzas.Find(id); if (pizzaToDelete is not null) { _context.Pizzas.Remove(pizzaToDelete); _context.SaveChanges(); } }
In the preceding code:
- The
Find
method retrieves a pizza by the primary key (in this case,Id
). - The
Remove
method removes thepizzaToDelete
entity in EF Core's object graph. - The
SaveChanges
method instructs EF Core to persist the object changes to the database.
- The
Save your changes.
Database seeding
You've coded the CRUD operations for PizzaService
, but it will be easier to test the "read" operation if there's good data in the database. Let's modify the app to seed the database on startup.
Warning
Be careful using this database seeding strategy in distributed environments, as it doesn't account for race conditions.
In the Data folder, add a new file named DbInitializer.cs.
Add the following code to Data\DbInitializer.cs:
using ContosoPizza.Models; namespace ContosoPizza.Data { public static class DbInitializer { public static void Initialize(PizzaContext context) { if (context.Pizzas.Any() && context.Toppings.Any() && context.Sauces.Any()) { return; // DB has been seeded } var pepperoniTopping = new Topping { Name = "Pepperoni", Calories = 130 }; var sausageTopping = new Topping { Name = "Sausage", Calories = 100 }; var hamTopping = new Topping { Name = "Ham", Calories = 70 }; var chickenTopping = new Topping { Name = "Chicken", Calories = 50 }; var pineappleTopping = new Topping { Name = "Pineapple", Calories = 75 }; var tomatoSauce = new Sauce { Name = "Tomato", IsVegan = true }; var alfredoSauce = new Sauce { Name = "Alfredo", IsVegan = false }; var pizzas = new Pizza[] { new Pizza { Name = "Meat Lovers", Sauce = tomatoSauce, Toppings = new List<Topping> { pepperoniTopping, sausageTopping, hamTopping, chickenTopping } }, new Pizza { Name = "Hawaiian", Sauce = tomatoSauce, Toppings = new List<Topping> { pineappleTopping, hamTopping } }, new Pizza { Name="Alfredo Chicken", Sauce = alfredoSauce, Toppings = new List<Topping> { chickenTopping } } }; context.Pizzas.AddRange(pizzas); context.SaveChanges(); } } }
In the preceding code:
- The
DbInitializer
class andInitialize
method are both defined asstatic
. Initialize
accepts aPizzaContext
as a parameter.- If there are no records in any of the three tables,
Pizza
,Sauce
, andTopping
objects are created. - The
Pizza
objects (and theirSauce
andTopping
navigation properties) are added to the object graph withAddRange
. - The object graph changes are committed to the database with
SaveChanges
.
- The
In the Data folder, add a new file named Extensions.cs.
Add the following code to Data\Extensions.cs:
namespace ContosoPizza.Data; public static class Extensions { public static void CreateDbIfNotExists(this IHost host) { { using (var scope = host.Services.CreateScope()) { var services = scope.ServiceProvider; var context = services.GetRequiredService<PizzaContext>(); context.Database.EnsureCreated(); DbInitializer.Initialize(context); } } } }
In the preceding code:
The
CreateDbIfNotExists
method is defined as an extension ofIHost
.A reference to the
PizzaContext
service is created.EnsureCreated ensures the database exists.
Important
EnsureCreated
creates a new database if one doesn't exist. The new database isn't configured for migrations, so use this with caution.The
DbIntializer.Initialize
method is called, passing thePizzaContext
as a parameter.
In Program.cs, replace
// Add the CreateDbIfNotExists method call
comment with the following code:app.CreateDbIfNotExists();
This code calls the extension method defined in the previous step whenever the app runs.
Save all your changes and build.
You've written all the code you need to do basic CRUD operations and seed the database on startup. In the next unit, you'll test those operations in the app.
Check your knowledge
Need help? See our troubleshooting guide or provide specific feedback by reporting an issue.