[ Полезный рекламный блок ]
Попробуйте свои силы в игре, где ваши навыки программирования на C# станут решающим фактором. Переходите по ссылке 🔰.
У цій статті, ми продовжимо писати магазин на Asp.Net Core і Entity Framework Core. У другій частині ми додали засоби для модифікації та видалення даних у БД. Для ознайомлення з другою частиною, перейдіть за посиланням.
У цьому розділі модель даних для додатка GameStore буде розширено за рамки єдиного класу Product. Ви побачите, як нормалізувати дані, замінюючи строкову властивість окремим класом, і дізнаєтеся, яким чином отримувати доступ до даних після їх створення. Крім того, додається підтримка для представлення замовлень покупців, яка є важливою частиною будь-якого інтернет-магазину.
Магазин на Asp.Net Core MVC EF. Частина 3
Додавання відношення до моделі даних
Оновлення контексту та створення сховища
Створення та застосування міграції
Створення контролера та подання
Заповнення бази даних категоріями
Заполнение базы данных категориями
Додавання підтримки для замовлень
Створення сховища і підготовка бази даних
Створення та застосування міграції
Створення контролерів і уявлень
Підготовчі кроки
У рамках підготовки ми консолідуємо процес створення і редагування об’єктів Product в єдиному поданні. У наступному коді об’єднуємо методи дій контролера Home, що додають або оновлюють об’єкти Product, і видаляємо дії, які виконували масові оновлення:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
public class HomeController : Controller { private readonly IProduct _products; public HomeController(IProduct products) { _products = products; } [HttpGet] public IActionResult Index() { return View(_products.GetAllProducts()); } [HttpGet] public IActionResult UpdateProduct(int id) { return View(id == 0 ? new Product() : _products.GetProduct(id)); } [HttpPost] public IActionResult UpdateProduct(Product product) { if (product.Id == 0) { _products.AddProduct(product); } else { _products.UpdateProduct(product); } return RedirectToAction(nameof(Index)); } [HttpPost] public IActionResult DeleteProduct(Product product) { _products.DeleteProduct(product); return RedirectToAction(nameof(Index)); } } |
Під час з’ясування, чи бажає користувач модифікувати наявний об’єкт або ж створити новий, об’єднані дії спираються на стандартне значення для Id int.
Тепер оновимо подання Index, яке відобразить зміни, внесені в контролер:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
@{ ViewData["Title"] = "Все товары"; } @model IQueryable<Product> <h3 class="p-2 bg-primary text-white text-center">Товары</h3> <div class="container"> <div class="row"> <div class="col fw-bold">Название</div> <div class="col fw-bold">Категория</div> <div class="col fw-bold">Закупочная цена</div> <div class="col fw-bold">Розничная цена</div> <div class="col"></div> </div> @foreach (Product product in Model) { <div class="row р-2"> <div class="col">@product.Name</div> <div class="col">@product.Category</div> <div class="col text-right">@product.PurchasePrice</div> <div class="col text-right">@product.RetailPrice</div> <div class="col"> <form asp-action="DeleteProduct" method="post"> <input type="hidden" name="Id" value="@product.Id"> <a asp-action="UpdateProduct" asp-route-id="@product.Id" class="btn btn-outline-primary">Редактировать</a> <button type="submit" class="btn btn-outline-danger">Удалить</button> </form> </div> </div> } <div class="text-cente r р-2"> <a asp-action="UpdateProduct" asp-route-id="0" class="btn btn-primary">Добавить</a> </div> </div> |
Запустивши додаток, ви побачите такий вміст:
Виконайте видалення всіх даних із таблиці.
Додавання відношення до моделі даних
Наразі кожен об’єкт Product створюється зі значенням властивостей Category, яке має тип string. У реальному проєкті питання лише в тому, скільки часу мине до ситуації, коли через помилки товар буде поміщено в невідповідну категорію. Щоб уникнути подібних проблем, дані застосунку можна нормалізувати з використанням відношень, у підсумку скорочуючи дублювання і гарантуючи безпеку.
Додавання класу моделі даних
Відправною точкою стане створення нового класу моделі даних. Додайте в папку Models файл на ім’я Category.cs і визначте в ньому клас, як показано в наступному коді:
1 2 3 4 5 6 |
public class Category { public int Id { get; set; } public string Name { get; set; } public string Description { get; set; } } |
Клас Category представляє категорію товарів. Властивість Id містить первинний ключ, а значення для властивостей Name і Description будуть надані користувачем під час створення нової категорії та її збереження в БД.
Створення стосунків
На наступному кроці створюється відношення між двома класами моделі даних, що робиться шляхом додавання властивостей до одного з класів. У будь-якому відношенні між даними один із класів відомий як залежна сутність і саме до нього додаються властивості. Щоб з’ясувати, який клас є залежною сутністю, поставте собі запитання, об’єкт якого з типів не може існувати без іншого. У випадку додатка GameStore категорія здатна існувати, навіть не маючи в собі товарів, але кожен товар має належати до якоїсь категорії – і це означає, що в такій ситуації залежною сутністю виявляється клас Product. Додайте в клас Product дві властивості, які створюють відношення з класом Category:
1 2 3 4 5 6 7 8 9 10 11 |
public class Product { public int Id { get; set; } public string Name { get; set; } //public string Category { get; set; } public decimal PurchasePrice { get; set; } public decimal RetailPrice { get; set; } public int CategoryId { get; set; } public Category Category { get; set; } } |
Першим тут додано властивість на ім’я СategoryId, що є прикладом властивості зовнішнього ключа, яку інфраструктура Entity Framework Core буде при міняти для відстеження відношення за рахунок присвоєння їй значення первинного ключа, що ідентифікує об’єкт Category. Ім’я властивості зовнішнього ключа складається з імені класу плюс ім’я властивості первинного ключа, даючи в результаті Categoryld.
Друга властивість замінює наявну властивість Category і являє собою приклад навігаційної властивості. Інфраструктура Еntity Framework Core заповнюватиме цю властивість об’єктом Сategory, який ідентифікується властивістю зовнішнього ключа, що робить його більш придатним для роботи з даними в БД.
Оновлення контексту та створення сховища
Для забезпечення доступу до об’єктів Category додайте в клас контексту БД властивість DbSet<T>:
1 2 3 4 5 6 7 8 9 |
public class ApplicationContext : DbContext { public ApplicationContext(DbContextOptions<ApplicationContext> context) : base(context) { } public DbSet<Product> Products { get; set; } public DbSet<Category> Categories { get; set; } } |
Нова властивість слідує за тим самим шаблоном, що й наявна властивість: вона оголошена як властивість public з конструкціями get і set, а повертає екземпляр DbSet<T>, де Т – клас, який потрібно зберігати в БД.
Коли модель даних розширюється, ви можете забезпечити решту коду в застосунку доступом до нових типів даних, додавши члени до наявного класу сховища або створивши новий такий клас. Для додатка GameStore давайте створимо окреме сховище, просто щоб продемонструвати, як це робиться.
У папку Interfaces, додайте інтерфейс ICategory, з таким вмістом:
1 2 3 4 5 6 7 |
public interface ICategory { IEnumerable<Category> GetAllCategories(); void AddCategory(Category category); void UpdateCategory(Category category); void DeleteCategory(Category category); } |
У папку Repository додайте клас CategoryRepository, з таким вмістом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
{ private ApplicationContext _context; public CategoryRepository(ApplicationContext context) { _context = context; } public IEnumerable<Category> GetAllCategories() { return _context.Categories; } public void AddCategory(Category category) { _context.Categories.Add(category); _context.SaveChanges(); } public void UpdateCategory(Category category) { _context.Categories.Update(category); _context.SaveChanges(); } public void DeleteCategory(Category category) { _context.Categories.Remove(category); _context.SaveChanges(); } } |
Ми створили інтерфейс сховища і клас реалізації, за аналогією, як і з класом Product.
Зареєструйте сховище та його реалізацію в класі Program для застосування із засобом впровадження залежностей:
1 |
builder.Services.AddTransient<ICategory, CategoryRepository>(); |
Створення та застосування міграції
Інфраструктура Entity Framework Core не зможе зберігати об’єкти Category доти, доки БД не буде оновлено для відповідності змінам, внесеним у модель даних. Щоб оновити БД, потрібно створити і застосувати до неї міграцію.
Для створення міграції у вікні Package Manager Console введіть таку команду:
1 |
Add-Migration AddCategories |
Для виконання інструкцій міграції, у вікні Package Manager Console виконайте команду:
1 |
Update-Database |
Перша команда створює нову міграцію на ім’я Categories, яка міститиме команди, необхідні для підготовки БД до зберігання нових об’єктів. Друга команда виконує такі команди для оновлення БД.
Створення контролера та подання
Ми створили обов’язкове відношення між класами Product і Category, тобто кожен об’єкт Product має бути асоційований з об’єктом Category. При відношенні такого виду корисно забезпечувати користувача засобами для управління об’єктами Category в БД. Додайте в папку Controllers файл класу на ім’я CategoriesController.cs і помістіть у нього такий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
public class CategoriesController : Controller { private readonly ICategory _categories; public CategoriesController(ICategory categories) { _categories = categories; } public IActionResult Index() { return View(_categories.GetAllCategories()); } [HttpPost] public IActionResult AddCategory(Category category) { _categories.AddCategory(category); return RedirectToAction(nameof(Index)); } public IActionResult EditCategory(long id) { ViewBag.Editid = id; return View(nameof(Index), _categories.GetAllCategories()); } [HttpPost] public IActionResult UpdateCategory(Category category) { _categories.UpdateCategory(category); return RedirectToAction(nameof(Index)); } [HttpPost] public IActionResult DeleteCategory(Category category) { _categories.DeleteCategory(category); return RedirectToAction(nameof(Index)); } } |
Контролер Categories приймає у своєму конструкторі об’єкт сховища для доступу до даних категорій і визначає дії, які підтримують запитування БД, а також створення, оновлення та видалення об’єктів Category. Щоб забезпечити контролер поданням, створіть папку Views/Categories і додайте в неї файл на ім’я Index.cshtml з таким вмістом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
@{ ViewData["Title"] = "Все категории"; } @model IEnumerable<Category> <h3 class="p-2 bg-primary text-white text-center">Категории</h3> <div class="container-fluid mt-3"> <div class="row"> <div class="col-1 fw-bold">Id</div> <div class="col fw-bold">Название</div> <div class="col fw-bold">Описание</div> <div class="col-3"></div> </div> @if (ViewBag.EditId == null) { <form asp-action="AddCategory" method="post"> @Html.Partial("CategoryEditor", new Category()) </form> } @foreach (Category c in Model) { @if (c.Id == ViewBag.EditId) { <form asp-action="UpdateCategory" method="post"> <input type="hidden" name="Id" value="@c.Id" /> @Html.Partial("CategoryEditor",c) </form> } else { <div class="row p-2"> <div class="col-1">@c.Id</div> <div class="col">@c.Name</div> <div class="col">@c.Description</div> <div class="col-3"> <form asp-action="DeleteCategory" method="post"> <input type="hidden" name="Id" value="@c.Id" /> <a asp-action="EditCategory" asp-route-id="@c.Id" class="btn btn-outline-primary">Редактировать</a> <button type="submit" class="btn btn-outline-danger">Удалить</button> </form> </div> </div> } } </div> |
Подання Index пропонує єдиний інтерфейс для керування категоріями та доручає створення і редагування об’єктів частковому поданню. Щоб створити часткове подання, додайте в папку Views/Categories файл на ім’я CategoryEditor.cshtml і помістіть у нього такий вміст:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
@{ ViewData["Title"] = "Создание / Обновление категории"; } @model Category <div class="row р-2"> <div class="col-1"></div> <div class="col"> <input asp-for="Name" class="form-control" /> </div> <div class="col"> <input asp-for="Description" class="form-control" /> </div> <div class="col-3"> @if (Model.Id == 0) { <button type="submit" class="btn btn-primary">Добавить</button> } else { <button type="submit" class="btn btn-outline-primary">Сохранить</button> <a asp-action="Index" class="btn btn-outline-secondary">Отмена</a> } </div> </div> |
Для полегшення переміщення додатком у файл Views – Shared – _Layout.cshtml додайте такий код:
1 2 3 4 5 6 7 8 9 10 11 |
//... <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between"> <ul class="navbar-nav flex-grow-1"> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Главная</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Categories" asp-action="Index">Категории</a> </li> </ul> </div> |
Запустіть додаток і перевірте його роботу.
Заповнення бази даних категоріями
Запустіть застосунок і додайте 3 будь-які категорії на сторінці “Категорії”:
Робота з пов’язаними даними
Інфраструктура Entity Framework Сore ігнорує відносини, якщо тільки їх явно не включають у запити. Це означає, що навігаційні властивості, як-от властивість Category, визначена в класі Product, за замовчуванням залишатимуться рівними null. Розширювальний метод Include() застосовується для повідомлення інфраструктурі EF про необхідність заповнення навігаційної властивості пов’язаними даними.
Перейдемо в клас ProductRepository і змінимо його реалізацію, включивши об’єкт категорії:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
public class ProductRepository : IProduct { private ApplicationContext _context; public ProductRepository(ApplicationContext context) { _context = context; } public void AddProduct(Product product) { _context.Products.Add(product); _context.SaveChanges(); } public IEnumerable<Product> GetAllProducts() { return _context.Products.Include(e => e.Category); } public Product GetProduct(int id) { return _context.Products.Include(e => e.Category).FirstOrDefault(e => e.Id == id); } public void UpdateProduct(Product product) { Product product2 = _context.Products.Find(product.Id); product2.Name = product.Name; //product2.Category = product.Category; product2.RetailPrice = product.RetailPrice; product2.PurchasePrice = product.PurchasePrice; product2.CategoryId = product.CategoryId; _context.SaveChanges(); } public void UpdateAll(Product[] products) { // _context.Products.UpdateRange(products); Dictionary<int, Product> data = products.ToDictionary(e => e.Id); IEnumerable<Product> baseline = _context.Products.Where(e => data.Keys.Contains(e.Id)); foreach (Product product in baseline) { Product requestProduct = data[product.Id]; product.Name = requestProduct.Name; product.Category = requestProduct.Category; product.RetailPrice = requestProduct.RetailPrice; product.PurchasePrice = requestProduct.PurchasePrice; } _context.SaveChanges(); } public void DeleteProduct(Product product) { _context.Products.Remove(product); _context.SaveChanges(); } } |
Метод Include() визначено в просторі імен Microsoft.Entity FrameworkCore, він приймає лямбда-вираз, що вибирає навігаційну властивість, яку бажано включити до запиту. Метод Find(), що застосовується для методу GetProduct(), не може використовуватися з методом Include(), тому він замінений методом First(), який призводить до того ж самого ефекту. Результат внесених змін полягає в тому, що інфраструктура Entity Framework Core буде заповнювати навігаційну властивість Product.Сategory для об’єктів Product, створених властивістю Products і методом GetProduct().
Зверніть увагу на зміни в методі UpdateProduct(). По-перше, вихідні дані запитуються безпосередньо, а не через метод GetProduct(), тому що завантажувати пов’язані дані під час виконання оновлення небажано. По-друге, закомментовано оператор, що встановлює властивість Category, і додано оператор, який натомість встановлює властивість Сategoryid. Встановлення властивості зовнішнього ключа – все, що необхідно інфраструктурі Entity Framework Core для оновлення відношення між двома об’єктами в БД.
Вибір категорії для товару
Оновимо контролер Home, щоб він мав доступ до даних Category через сховище і передавав їх своєму поданню. Це дасть змогу поданням запропонувати вибір із повного набору категорій під час редагування або створення об’єкта Product:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
public class HomeController : Controller { private readonly IProduct _products; private readonly ICategory _categories; public HomeController(IProduct products, ICategory categories) { _products = products; _categories = categories; } [HttpGet] public IActionResult Index() { return View(_products.GetAllProducts()); } [HttpGet] public IActionResult UpdateProduct(int id) { ViewBag.Categories = _categories.GetAllCategories(); return View(id == 0 ? new Product() : _products.GetProduct(id)); } [HttpPost] public IActionResult UpdateProduct(Product product) { if (product.Id == 0) { _products.AddProduct(product); } else { _products.UpdateProduct(product); } return RedirectToAction(nameof(Index)); } [HttpPost] public IActionResult DeleteProduct(Product product) { _products.DeleteProduct(product); return RedirectToAction(nameof(Index)); } } |
Щоб надати користувачеві можливість вибору однієї з категорій при створенні або редагуванні об’єкта Product, додамо в подання UpdateProduct елемент select:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
@{ ViewData["Title"] = "Создание / Обновление товара"; } @model Product <div class="p-5 text-center bg-light"> <h2 class="mb-3">Создание / Обновление товара</h2> </div> <form asp-action="UpdateProduct" method="post"> <div class="form-group"> <label asp-for="Id"></label> <input asp-for="Id" class="form-control" readonly="readonly" /> </div> <div class="form-group"> <label asp-for="Name"></label> <input asp-for="Name" class="form-control" /> </div> <div class="form-group"> <label asp-for="Category"></label> <select class="form-control" asp-for="CategoryId"> @if (Model.Id == 0) { <option disabled selected>Выберите категорию</option> } @foreach (Category category in ViewBag.Categories) { <option selected="@(Model.Category?.Id == category.Id)" value="@category.Id">@category.Name</option> } </select> </div> <div class="form-group"> <label asp-for="PurchasePrice"></label> <input asp-for="PurchasePrice" class="form-control" /> </div> <div class="form-group"> <label asp-for="RetailPrice"></label> <input asp-for="RetailPrice" class="form-control" /> </div> <div class="form-group text-center"> <button class="btn btn-primary" type="submit">Сохранить</button> <a asp-action="Index" class="btn btn-secondary">Отмена</a> </div> </form> |
У розмітку включено елемент-заповнювач option на випадок, якщо подання використовується для створення нового об’єкта Product, і передбачено вираз Razor для застосування атрибута selected, якщо поточний об’єкт редагується. Залишилося лише оновити подання Index, щоб прослідувати за навігаційною властивістю і відобразити для кожного об’єкта Product назву обраної категорії. Перейдемо в Home / Index.cshtml і змінимо вміст:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
@{ ViewData["Title"] = "Все товары"; } @model IQueryable<Product> <h3 class="p-2 bg-primary text-white text-center">Товары</h3> <div class="container"> <div class="row"> <div class="col fw-bold">Название</div> <div class="col fw-bold">Категория</div> <div class="col fw-bold">Закупочная цена</div> <div class="col fw-bold">Розничная цена</div> <div class="col"></div> </div> @foreach (Product product in Model) { <div class="row р-2"> <div class="col">@product.Name</div> <div class="col">@product.Category.Name</div> <div class="col text-right">@product.PurchasePrice</div> <div class="col text-right">@product.RetailPrice</div> <div class="col"> <form asp-action="DeleteProduct" method="post"> <input type="hidden" name="Id" value="@product.Id"> <a asp-action="UpdateProduct" asp-route-id="@product.Id" class="btn btn-outline-primary">Редактировать</a> <button type="submit" class="btn btn-outline-danger">Удалить</button> </form> </div> </div> } <div class="text-cente r р-2"> <a asp-action="UpdateProduct" asp-route-id="0" class="btn btn-primary">Добавить</a> </div> </div> |
Запустіть застосунок, виконайте додавання і редагування товару, переконайтеся, що вибір і відображення категорії працює коректно.
Після створення кожного об’єкта ініціюється дія Index для відображення результатів, яка змушує інфраструктуру Entity Framework Core запросити в БД дані Product і пов’язані з ними об’єкти Category. Ви можете побачити, як він транслюється в SQL-запит, переглянувши згенеровані додатком журнальні повідомлення:
1 2 3 |
SELECT [p].[Id], [p].[CategoryId], [p].[Name], [p].[PurchasePrice], [p].[RetailPrice], [c].[Id], [c].[Description], [c].[Name] FROM [Products] AS [p] INNER JOIN [Categories] AS [c] ON [p].[CategoryId] = [c].[Id] |
Інфраструктура Entity Framework Core використовує зовнішній ключ для запиту даних, які необхідні для створення об’єктів Category, пов’язаних з об’єктами Product, і застосовує внутрішнє з’єднання для об’єднання даних з таблиць Products і Categories.
P.S. Якщо ви видалите об’єкт Category, то пов’язані з ним об’єкти Product, також видаляться, що є стандартною конфігурацією для обов’язкових відносин.
Додавання підтримки для замовлень
Щоб продемонструвати більш складне відношення, ми додамо підтримку для створення і збереження замовлень і будемо використовувати їх для представлення вибору товарів, зробленого покупцями. У наступних розділах ми розширимо модель даних додатковими масами, оновимо БД і додамо контролер для управління новими даними.
Створення класів моделі даних
Почнемо з додавання в папку Models файлу на ім’я Order.cs з таким вмістом:
1 2 3 4 5 6 7 8 9 10 11 |
public class Order { public int Id { get; set; } public string CustomerName { get; set; } public string Address { get; set; } public string State { get; set; } public string ZipCode { get; set; } public bool Shipped { get; set; } public IEnumerable<OrderLine> Lines { get; set; } } |
Клас Order має властивості, які зберігають ім’я та адресу покупця, а також ознаку, чи доставлені товари. Є також навігаційна властивість, що забезпечує доступ до пов’язаних об’єктів OrderLine, які представлятимуть окремі вибрані товари.
Для створення класу OrderLine додайте в папку Models файл на ім’я OrderLine.cs з таким вмістом:
1 2 3 4 5 6 7 8 9 10 11 |
public class OrderLine { public int Id { get; set; } public int ProductId { get; set; } public int OrderId { get; set; } public int Quantity { get; set; } public Product Product { get; set; } public Order Order { get; set; } } |
Кожен об’єкт OrderLine пов’язаний з об’єктами Order і Product і має властивість, яка відображає, скільки товару покупець замовив. Щоб забезпечити зручний доступ до даних Order, додамо в клас контексту ApplicationContext такі властивості:
1 2 3 4 5 6 7 8 9 10 11 12 |
public class ApplicationContext : DbContext { public ApplicationContext(DbContextOptions<ApplicationContext> context) : base(context) { } public DbSet<Product> Products { get; set; } public DbSet<Category> Categories { get; set; } public DbSet<Order> Orders { get; set; } public DbSet<OrderLine> OrderLines { get; set; } } |
Створення сховища і підготовка бази даних
Для надання узгодженого доступу до нових даних решті коду додатка додайте в папку Interfaces файл на ім’я IOrder.cs з таким вмістом:
1 2 3 4 5 6 7 8 |
public interface IOrder { IEnumerable<Order> GetAllOrders(); Order GetOrder(int id); void AddOrder(Order order); void UpdateOrder(Order order); void DeleteOrder(Order order); } |
Додайте в папку Repository файл на ім’я OrderRepository.cs з таким вмістом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public class OrderRepository : IOrder { private ApplicationContext _context; public OrderRepository(ApplicationContext context) { _context = context; } public IEnumerable<Order> GetAllOrders() { return _context.Orders.Include(e => e.Lines).ThenInclude(e => e.Product); } public Order GetOrder(int id) { return _context.Orders.Include(e => e.Lines).FirstOrDefault(e => e.Id == id); } public void AddOrder(Order order) { _context.Orders.Add(order); _context.SaveChanges(); } public void DeleteOrder(Order order) { _context.Orders.Remove(order); _context.SaveChanges(); } public void UpdateOrder(Order order) { _context.Orders.Update(order); _context.SaveChanges(); } } |
Реалізація сховища слідує шаблону, прийнятому для інших сховищ, і заради простоти не задіює засіб виявлення змін. Зверніть увагу на застосування методів Include() і Theninclude() для навігації моделлю даних і додавання в запити пов’язаних даних.
Додайте в клас Program оператор, щоб система впровадження залежностей розпізнавала залежності від інтерфейсу IOrderRepository з використанням короткочасних об’єктів OrderRepository:
1 |
builder.Services.AddTransient<IOrder, OrderRepository>(); |
Створення та застосування міграції
Інфраструктура Entity Framework Core не зможе зберігати об’єкти Order доти, доки БД не буде оновлено для відповідності змінам, внесеним у модель даних. Щоб оновити БД, потрібно створити і застосувати до неї міграцію.
Для створення міграції у вікні Package Manager Console введіть таку команду:
1 |
Add-Migration AddOrders |
Для виконання інструкцій міграції, у вікні Package Manager Console виконайте команду:
1 |
Update-Database |
Перша команда створює нову міграцію на ім’я Orders, яка міститиме команди, потрібні для підготовки БД до зберігання нових об’єктів. Друга команда виконує такі команди для оновлення БД.
Поточна структура застосованих міграцій:
Поточна структура проєкту:
Створення контролерів і уявлень
Увесь сполучний код Entity Framework Core для роботи з об’єктами Order на місці; наступний крок передбачає додавання засобів MVC, що дадуть змогу створювати та керувати екземплярами. Додайте в папку Controllers контролер на ім’я OrdersController.cs, з таким вмістом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public class OrdersController : Controller { private readonly IProduct _products; private readonly IOrder _orders; public OrdersController(IProduct products, IOrder orders) { _products = products; _orders = orders; } public IActionResult Index() { return View(_orders.GetAllOrders()); } public IActionResult EditOrder(int id) { var products = _products.GetAllProducts(); Order order = id == 0 ? new Order() : _orders.GetOrder(id); IDictionary<int, OrderLine> lineMaps = order.Lines?.ToDictionary(e => e.ProductId) ?? new Dictionary<int, OrderLine>(); ViewBag.Lines = products.Select(e => lineMaps.ContainsKey(e.Id) ? lineMaps[e.Id] : new OrderLine { Product = e, ProductId = e.Id, Quantity = 0 }); return View(order); } [HttpPost] public IActionResult AddOrUpdateOrder(Order order) { //Вскоре допишем... return RedirectToAction(nameof(Index)); } [HttpPost] public IActionResult DeleteOrder(Order order) { _orders.DeleteOrder(order); return RedirectToAction(nameof(Index)); } } |
Реалізація методу AddOrUpdateOrder() буде завершена, коли стануть доступними інші засоби.
Оператори LINQ у методі дії EditOrder() можуть виглядати заплутаними, але вони здійснюють підготовку даних OrderLine, щоб мати один об’єкт OrderLine для кожного об’єкта Product, навіть якщо раніше цей товар не вибирався.
Таким чином, для нового замовлення властивість ViewBag.Lines буде заповнюватися послідовністю об’єктів OrderLine, що відповідають кожному об’єкту Product у БД, зі встановленими в 0 властивостями Id і Quantity. Коли об’єкт зберігається в БД, нульове значення Id вказуватиме, що об’єкт є новим, і сервер баз даних присвоїть йому новий унікальний первинний ключ.
Для наявних замовлень властивість ViewBag.Lines заповнюватиметься об’єктами OrderLine, прочитаними з БД, і додатковими об’єктами з нульовими властивостями Id для решти товарів.
Далі необхідно створити подання, яке відображатиме список усіх об’єктів у БД. Додайте папку Views/Orderers і помістіть у неї файл на ім’я Index.cshtml з таким вмістом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
@{ ViewData["Title"] = "Все заказы"; } @model IEnumerable<Order> <h3 class="p-2 bg-primary text-white text-center">Заказы</h3> <div class="container-fluid mt-3"> <div class="row"> <div class="col-1 fw-bold">Id</div> <div class="col fw-bold">Название</div> <div class="col fw-bold">Zip</div> <div class="col fw-bold">Всего</div> <div class="col fw-bold">Сумма</div> <div class="col fw-bold">Статус</div> <div class="col-3"></div> </div> </div> <div> @foreach (Order order in Model) { <div class="row p-2"> <div class="col-1">@order.Id</div> <div class="col">@order.CustomerName</div> <div class="col">@order.ZipCode</div> <div class="col">@order.Lines.Sum(e=>e.Quantity * e.Product.RetailPrice - e.Product.PurchasePrice)</div> <div class="col">@(order.Shipped ? "Отправлен" : "Ожидается отправка")</div> <div class="col-3 text-right"> <form asp-action="DeleteOrder" method="post"> <input type="hidden" name="Id" value="@order.Id"> <a asp-action="EditOrder" asp-route-id="@order.Id" class="btn btn-outline-primary">Редактировать</a> <button type="submit" class="btn btn-outline-danger">Удалить</button> </form> </div> </div> } </div> <div class="text-center"> <a asp-action="EditOrder" class="btn btn-primary">Создать</a> </div> |
Подання відображає зведення по об’єктах Order з БД, а також сумарну вартість замовлених товарів і розмір прибутку, який буде отримано. Тут присутні кнопки для створення нового замовлення і для редагування та видалення наявного замовлення.
Щоб забезпечити додаток поданням для створення або редагування замовлення, додайте в папку Views/Orderers файл на ім’я EditOrder.cshtml, з таким вмістом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
@{ ViewData["Title"] = "Создание / Обновление заказа"; } @model Order <div class="p-5 text-center bg-light"> <h2 class="mb-3">Создание / Обновление заказа</h2> </div> <form asp-action="AddOrUpdateOrder" method="post"> <div class="form-group"> <label asp-for="Id"></label> <input asp-for="Id" class="form-control" readonly="readonly" /> </div> <div class="form-group"> <label asp-for="CustomerName"></label> <input asp-for="CustomerName" class="form-control" /> </div> <div class="form-group"> <label asp-for="Address"></label> <input asp-for="Address" class="form-control" /> </div> <div class="form-group"> <label asp-for="State"></label> <input asp-for="State" class="form-control" /> </div> <div class="form-group"> <label asp-for="ZipCode"></label> <input asp-for="ZipCode" class="form-control" /> </div> <div class="form-check"> <label class="form-check-label"> <input type="checkbox" asp-for="Shipped" class="form-check-input" /> Отправлен </label> </div> <h6 class="mt-1 р-2 bg-primary text-white text-center"> Заказанные товары </h6> <div class="container-fluid"> <div class="row"> <div class="col font-weight-bold">Товар</div> <div class="col font-weight-bold">Категория</div> <div class="col font-weight-bold">Количество</div> </div> @{ int counter = 0; } @foreach (OrderLine line in ViewBag.Lines) { <input type="hidden" name="lines[@counter].Id" value="@line.Id" /> <input type="hidden" name="lines[@counter].ProductId" value="@line.ProductId" /> <input type="hidden" name="lines[@counter].OrderId" value="@line.OrderId" /> <div class="row mt-1"> <div class="col">@line.Product.Name</div> <div class="col">@line.Product.Category.Name</div> <div class="col"> <input type="number" name="lines[@counter].Quantity" value="@line.Quantity" /> </div> </div> counter++; } </div> <div class="form-group text-center"> <button class="btn btn-primary" type="submit">Сохранить</button> <a asp-action="Index" class="btn btn-secondary">Отмена</a> </div> </form> |
Подання EditOrder пропонує користувачеві форму з елементами input для властивостей, визначених у класі Order, і елементами для всіх об’єктів Product у БД, які заповнюватимуться замовленою кількістю під час редагування наявних об’єктів.
Для полегшення доступу до замовлень, додамо посилання в меню, для цього змінимо вміст Views / Shared / _Layout.cshtml, таким чином:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//... <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between"> <ul class="navbar-nav flex-grow-1"> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Главная</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Categories" asp-action="Index">Категории</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Orders" asp-action="Index">Заказы</a> </li> </ul> </div> |
Запустіть додаток, переконайтеся, що форма замовлення відображається коректно:
Збереження даних замовлення
Клацання на кнопці Зберегти не призводить до збереження будь-яких даних, тому що метод AddOrUpdateOrder() залишився незавершеним. Перейдемо в контролер Orders і допишемо цей метод:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class OrdersController : Controller { //... [HttpPost] public IActionResult AddOrUpdateOrder(Order order) { order.Lines = order.Lines.Where(e => e.Id > 0 || (e.Id == 0 && e.Quantity > 0)).ToArray(); if (order.Id == 0) { _orders.AddOrder(order); } else { _orders.UpdateOrder(order); } return RedirectToAction(nameof(Index)); } } |
Оператори в методі дії покладаються на зручну функціональну особливість Entity Framework Core: у разі передавання об’єкта Order методу AddOrder() або UpdateOrder() сховища інфраструктура Entity Framework Core збереже не тільки цей об’єкт Order, а й пов’язані з ним об’єкти OrderLine. Зазначена особливість може не виглядати важливою, але вона спрощує процес, який інакше вимагав послідовності ретельно скоординованих оновлень.
Щоб переглянути SQL-команди, що генеруються, запустіть додаток і виконайте додавання одного замовлення, з 2-3-ма товарами:
1 2 3 4 5 6 7 8 9 10 11 |
INSERT INTO[Orders] ([Address], [CustomerName], [Shipped], [State], [ZipCode]) VALUES(@p0, @p1, @p2, @p3, @p4); SELECT[Id] FROM[Orders] WHERE @@ROWCOUNT = 1 AND[Id] = scope_identity(); INSERT INTO[OrderLines] ([OrderId], [ProductId], [Quantity]) VALUES(@p0, @p1, @p2); SELECT[Id] FROM[OrderLines] WHERE @@ROWCOUNT = 1 AND[Id] = scope_identity(); |
Перша команда зберігає об’єкт Order, а друга отримує значення, призначене первинному ключу. Далі EF використовується первинний ключ об’єкта Order для збереження об’єктів OrderLine.
Останній момент, який потрібно відзначити, стосується наступного оператора:
1 |
order.Lines = order.Lines.Where(e => e.Id > 0 || (e.Id == 0 && e.Quantity > 0)).ToArray(); |
Оператор виключає будь-який об’єкт OrderLine, для якого не було вибрано ненульову кількість, окрім об’єктів, що вже зберігаються в БД. Це гарантує, що БД не переповниться об’єктами OrderLine, які не є частиною якогось замовлення, але дозволяє вносити зміни в раніше збережені дані.
Після збереження даних відобразиться зведення за замовленням:
Підсумок
У розділі модель даних GameStore було розширено за рахунок додавання нових класів і створення відносин між ними. Ви дізналися, як запитувати пов’язані дані, яким чином виконувати оновлення. У наступному розділі буде показано, яким чином пристосувати частини MVC і Entity Framework Core до роботи з великими обсягами даних.
На цьому стаття “Магазин на Asp.Net Core MVC EF”, підійшла до кінця, сподіваюся вам було цікаво. Ви можете завантажити вихідний код у моєму репозиторії – Github.
Поділіться вашим досвідом у коментарях, який був ваш перший проєкт, магазин на Asp.Net Core MVC EF або щось інше?
Так само вам може бути цікава попередня стаття:
Ви хочете навчитися писати код мовою програмування C#?
Створювати різні інформаційні системи, що складаються з сайтів, мобільних клієнтів, десктопних додатків, телеграм-ботів тощо.
Переходьте до нас на сторінку Dijix і ознайомтеся з умовами навчання, ми спеціалізуємося тільки на індивідуальних заняттях, як для початківців, так і для просунутих програмістів. Ви можете взяти як одне заняття для опрацювання питання, що вас цікавить, так і кілька, для більш щільної роботи. Завдяки особистому кабінету, кожен студент підвищить якість свого навчання, у вашому розпорядженні:
- Доступ до пройденого матеріалу
- Тематичні статті
- Бібліотека книг
- Онлайн тестування
- Спілкування в закритих групах