[ Полезный рекламный блок ]
Попробуйте свои силы в игре, где ваши навыки программирования на 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; } } |
Первым здесь добавлено свойство по имени СаtegoryId, являющееся примером свойства внешнего ключа, которое инфраструктура Entity Framework Core будет при менять для отслеживания отношения за счет присваивания ему значения первичного ключа, идентифицирующего объект Category. Имя свойства внешнего ключа состоит из имени класса плюс имя свойства первичного ключа, давая в результате Categoryld.
Второе свойство заменяет существующее свойство Category и представляет собой пример навигационного свойства. Инфраструктура Eпtity Framework Core будет заполнять это свойство объектом Саtegory. который идентифицируется свойством внешнего ключа, что делает его более подходящим для работы с данными в БД.
Обновление контекста и создание хранилища
Для обеспечения доступа к объектам 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 Соге игнорирует отношения, если только они явно не включаются в запросы. Это означает, что навигационные свойства, такие как свойство 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.Саtegory для объектов Product, созданных свойством Products и методом GetProduct().
Обратите внимание на изменения в методе UpdateProduct(). Во-первых, исходные данные запрашиваются напрямую, а не через метод GetProduct(), потому что загружать связанные данные при выполнении обновления нежелательно. Во-вторых, закомментирован оператор, устанавливающий свойство Category, и добавлен оператор, который взамен устанавливает свойство Саtegoryid. Установка свойства внешнего ключа – все, что необходимо инфраструктуре 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-зaпpoc, просмотрев сгенерированные приложением журнальные сообщения:
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 / Orders и поместите в нее файл по имени 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 / Orders файл по имени 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#?
Пройдите наш тест на 13 вопросов, чтобы узнать, как много вы знаете на самом деле!
C# Braincheck |
Вы хотите научится писать код на языке программирования C#?
Создавать различные информационные системы, состоящие из сайтов, мобильных клиентов, десктопных приложений, телеграмм-ботов и т.д.
Переходите к нам на страницу Dijix и ознакомьтесь с условиями обучения, мы специализируемся только на индивидуальных занятиях, как для начинающих, так и для более продвинутых программистов. Вы можете взять как одно занятие для проработки интересующего Вас вопроса, так и несколько, для более плотной работы. Благодаря личному кабинету, каждый студент повысит качество своего обучения, в вашем распоряжении:
- Доступ к пройденному материалу
- Тематические статьи
- Библиотека книг
- Онлайн тестирование
- Общение в закрытых группах
Живи в своем мире, программируй в нашем.