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
PizzaContextbefore the constructor. - Change the constructor method signature to accept a
PizzaContextparameter. - 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
PizzaServiceinstance is created, aPizzaContextwill be injected as a dependency.- Add a
Replace the
GetAllmethod with the following code:public IEnumerable<Pizza> GetAll() { return _context.Pizzas .AsNoTracking() .ToList(); }In the preceding code:
- The
Pizzascollection contains all the rows in the pizzas table. - The
AsNoTrackingextension method instructs EF Core to disable change tracking. Since this operation is read-only,AsNoTrackingcan optimize performance. - All of the pizzas are returned with
ToList.
- The
Replace the
GetByIdmethod 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
Includeextension method takes a lambda expression to specify that theToppingsandSaucenavigation properties are to be included in the result (eager loading). Without this, EF Core will return null for those properties. - The
SingleOrDefaultmethod returns a pizza that matches the lambda expression.- If no records match,
nullis returned. - If multiple records match, an exception is thrown.
- The lambda expression describes records where the
Idproperty is equal to theidparameter.
- If no records match,
- The
Replace the
Createmethod with the following code:public Pizza Create(Pizza newPizza) { _context.Pizzas.Add(newPizza); _context.SaveChanges(); return newPizza; }In the preceding code:
newPizzais 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
Addmethod adds thenewPizzaentity to EF Core's object graph. - The
SaveChangesmethod instructs EF Core to persist the object changes to the database.
Replace the
UpdateSaucemethod 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
PizzaandSauceare created usingFind.Findis an optimized method to query records by their primary key.Findsearches the local entity graph first before querying the database. - The
Pizza.Sauceproperty is set to theSauceobject. - An
Updatemethod call is unnecessary because EF Core detects that we set theSauceproperty onPizza. - The
SaveChangesmethod instructs EF Core to persist the object changes to the database.
- References to an existing
Replace the
AddToppingmethod 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
PizzaandToppingare created usingFind. - The
Toppingis added to thePizza.Toppingscollection. A new collection is created if it doesn't exist. - The
Updatemethod flags thepizzaToUpdateentity as updated in EF Core's object graph. - The
SaveChangesmethod instructs EF Core to persist the object changes to the database.
- References to an existing
Replace the
DeleteByIdmethod 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
Findmethod retrieves a pizza by the primary key (in this case,Id). - The
Removemethod removes thepizzaToDeleteentity in EF Core's object graph. - The
SaveChangesmethod 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
DbInitializerclass andInitializemethod are both defined asstatic. Initializeaccepts aPizzaContextas a parameter.- If there are no records in any of the three tables,
Pizza,Sauce, andToppingobjects are created. - The
Pizzaobjects (and theirSauceandToppingnavigation 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
CreateDbIfNotExistsmethod is defined as an extension ofIHost.A reference to the
PizzaContextservice is created.EnsureCreated ensures the database exists.
Important
EnsureCreatedcreates a new database if one doesn't exist. The new database is not configured for migrations, so use this with caution.The
DbIntializer.Initializemethod is called, passing thePizzaContextas a parameter.
In Program.cs, replace
// Add the CreateDbIfNotExists method callcomment 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.