Как включить Mapped сущность в NotMapped сущность? - PullRequest
0 голосов
/ 30 апреля 2019

Предположим, что у нас есть сопоставленная сущность с именем "Product" как:

public class Product
{
    public int Id { get; set; }
    public string Title { get; set; }
    public decimal Price { get; set; }
}

А также не отображенный объект с именем "Корзина", как:

[NotMapped]
public class Cart
{
    public int Id { get; set; }
    public int ProductId { get; set; }
    public int Quantity { get; set; }
    public Product Product { get; set; }
}

Как лучше всего заполнить «Товар» в «Корзине»? Если бы оба объекта были сопоставлены, то это можно выполнить так:

dbContext.Cart.Include(c => c.Product);

Можно ли использовать подобный синтаксис или я должен запросить таблицу "Продукт"?

Изменить: Вот что я наконец-то сделал:

var selectedProductIds = cartList.Select(c => c.ProductId).ToList(); 
var selectedProducts = db.Products.Where(p => selectedProductIds.Contains(p.Id)).ToList(); 
var cart = cartList.GroupJoin(selectedProducts, c => c.ProductId, p => p.Id, (c, p)=> new Cart() { Product = p.First(), Quantity=c.Quantity}).ToList();

1 Ответ

0 голосов
/ 01 мая 2019

Сущность отражает структуру таблицы. Так что, если корзина не входит в структуру данных и не имеет сущности, вы загружаете товары напрямую.

Похоже, что вы работаете над примером, подобным корзине покупок, где ваша корзина отражает выбранные товары для покупки. В этом смысле корзина должна существовать только как модель представления. Первое соображение - провести различие между моделью представления и моделью. (Сущность). Они должны быть полностью отдельными и не смешанными. Сущности должны существовать только в границах DbContext, который их загрузил. Они могут быть отсоединены в объектах POCO C #, но это может легко привести к ошибкам, проблемам с производительностью и уязвимостям.

Может ли корзина состоять только из одного товара? или список разных товаров и количеств?

При условии, что корзина представляет собой один товар (и количество)

public class CartViewModel
{
    public ProductViewModel { get; set; }
    public int Quantity { get; set; }
}
public class ProductViewModel
{
    public int ProductId { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Когда представление переходит к списку продуктов для отображения, контроллер возвращает обратно экземпляры ProductViewModel:

return context.Products
    .Where(x => x.IsActive)
    .Select(x => new ProductViewModel
    {
        ProductId = x.ProductId,
        Name = x.Name,
        Price = x.Price
    }).ToList();

"Почему бы просто не вернуть сущность?" По мере развития системы таблица продуктов, вероятно, будет расширяться и включать в себя больше столбцов и большее количество взаимосвязей, которые необходимо учитывать. Например, связывание уровней запасов. Представлению не нужно знать о чем-то большем, чем нужно, и сериализатор попытается сериализовать все, что ему подают. Это означает отправку клиенту большего количества данных, чем необходимо, и может привести к проблемам, когда у вас могут быть циклические ссылки. Используя .Select(), вы оптимизируете запрос, чтобы просто возвращать необходимые поля и предоставлять эти данные только клиенту. ProductViewModel должен содержать всю информацию, необходимую для отображения на клиенте, и сведения, которые вам понадобятся для выполнения любых действий с этим продуктом. С Automapper вы можете настроить правила отображения Entity для ViewModel и использовать ProjectTo<T>() для замены Select() без необходимости каждый раз отображать свойства вручную.

С помощью списка доступных продуктов ваш клиентский код может выбрать одну из этих моделей представления, чтобы связать ее с корзиной и записать количество. Вы можете рассчитать итоговое значение на экране, используя значение PriceViewModel's Price.

Когда вы собираетесь выполнить заказ, вы можете передать выбранный продукт ViewModel обратно на сервер вместе с количеством, но было бы лучше просто передать идентификатор продукта вместе с количеством. Предполагая, что серверный вызов собирается создать Заказ для этого продукта:

У вас может возникнуть соблазн сделать что-то подобное с моделью вида:

public ActionResult CreateOrder(ProductViewModel product, int quantity)
{
    if (quantity <= 0 || quantity > MaximumOrderSize)
        throw new ArgumentException("quantity", "Naughty User!");

    using (var context = new MyDbContext())
    {
        var order = new Order
        {
            ProductId = product.ProductId,
            Cost = product.Price * quantity,
            // ... obviously other details, like the current user from session state...
        };
        context.Orders.Add(order);
        context.SaveChanges();
    }
}

Проблема в том, что мы слишком доверяем данным, полученным от клиента. Кто-то, использующий средства отладки, может перехватить звонок на наш сервер от своего клиента и установить продукт. Цена - $ 0,00 или скидка 50%. Наш заказ будет основывать стоимость на цене, отправленной клиентом.

Вместо того, чтобы:

public ActionResult CreateOrder(int productId, int quantity)
{
    if (quantity <= 0 || quantity > MaximumOrderSize)
        throw new ArgumentException("quantity", "Naughty User!");

    using (var context = new MyDbContext())
    {
        var product = context.Products.Single(x => x.ProductId == productId); // Ensures we have a valid product.
        var order = new Order
        {
            Product = product, // set references rather than FKs.
            Cost = product.Price * quantity,
            // ...
        };
        context.Orders.Add(order);
        context.SaveChanges();
    }
}

В этом случае мы убедились, что Продукт действительно существует, и мы используем только цену из наших доверенных данных, а не из того, что было передано клиентом. Например, если клиент подделал идентификатор продукта в недопустимое значение, наша обработка исключений приложения (или вы можете добавить обработку исключений для каждого действия) запишет ошибку и может прервать сеанс, если он подозревает подделку. Фальсификация не может изменить стоимость заказа, потому что эти данные поступают с нашего сервера каждый раз.

Чтобы пояснить, почему вы не хотите отправлять сущности вокруг, особенно обратно на сервер с клиента, давайте рассмотрим пример модели представления, за исключением того, что вернем сущность Product:

public ActionResult CreateOrder(Product product, int quantity)
{
    if (quantity <= 0 || quantity > MaximumOrderSize)
        throw new ArgumentException("quantity", "Naughty User!");

    using (var context = new MyDbContext())
    {
        var order = new Order
        {
            Product = product,
            Cost = product.Price * quantity,
            // ...
        };
        context.Orders.Add(order);
        context.SaveChanges();
    }
}

Это может выглядеть "хорошо", но есть большая проблема.«context» в этом случае не знает об этом экземпляре Product.Он был десериализован из запроса и для всех интенсивных целей выглядит как новый экземпляр продукта, который вы создали.Это приведет к тому, что EF либо создаст дубликат записи Product с новым идентификатором (и любыми другими подделанными данными), либо сгенерирует исключение о дублированном первичном ключе.

Теперь, если дубликат или ошибка исправимы, мы простонужно прикрепить сущность ...

public ActionResult CreateOrder(Product product, int quantity)
{
    if (quantity <= 0 || quantity > MaximumOrderSize)
        throw new ArgumentException("quantity", "Naughty User!");

    using (var context = new MyDbContext())
    {
        context.Products.Attach(product);
        var order = new Order
        {
            Product = product,
            Cost = product.Price * quantity,
            // ...
        };
        context.Orders.Add(order);
        context.SaveChanges();
    }
}

И это должно работать.В некоторых случаях контекст уже может знать о сущности и выдавать ошибку, если она уже существует.(например, в случаях, когда мы перебираем данные, которые могут иметь повторяющиеся ссылки) За исключением того, что мы все еще доверяем данным, поступающим от клиента.Пользователь все еще может изменить цену, которая будет отражена в заказе.Это также потенциально очень опасно, потому что если мы позже перейдем к изменению чего-либо в продукте или просто пометим его как «Измененный», EF сохранит всех изменений, внесенных хакерским клиентом в продукт.Например:

public ActionResult CreateOrder(Product product, int quantity)
{
    if (quantity <= 0 || quantity > MaximumOrderSize)
        throw new ArgumentException("quantity", "Naughty User!");

    using (var context = new MyDbContext())
    {
        context.Products.Attach(product);
        product.AvailableQuantity -= quantity;
        var order = new Order
        {
            Product = product,
            Cost = product.Price * quantity,
            // ...
        };
        context.Orders.Add(order);
        context.SaveChanges();
    }
}

Предполагая, что у нашего Продукта есть свойство доступного количества, которое мы хотим обновить при оформлении заказа, этот вызов выполняет эту настройку.Объект теперь помечен как измененный.Что мы не заметили, так это то, что, когда цена нашего продукта обычно составляет 100 долларов, а клиенту отправляется 100 долларов, этот хакерский пользователь увидел, что мы возвращаем весь продукт, и ему было любопытно, что произойдет, если он изменит цену на 50 долларов.данные отправляются обратно на сервер.Его заказ не только будет стоить 50 долларов за продукт, но теперь он изменил цену нашего продукта со 100 до 50 долларов, потому что измененная сущность была связана с контекстом, помечена как измененная, и изменения были сохранены.Там, где у нас могли быть функции администратора для изменения продуктов, отслеживания идентификаторов пользователей, модифицированных дат и т. Д., Ничего из этого не могло быть обновлено, поскольку мы доверяли объекту, возвращающемуся от клиента.Даже если вы зарегистрируете тот факт, что этот пользователь повредил данные, вероятность нарушения работы вашей системы станет огромной.

Вы можете сэкономить время прохождения сущностей и просто не доверять им на обратном пути и всегда перезагружать сущность.Но риск по мере развития вашей системы состоит в том, что кто-то станет неаккуратным или ленивым, полагая, что сущность существует в любом случае и Attach / Update против контекста.Вы можете найти несколько примеров вопросов в StackOverflow, где люди задавали вопросы об ошибках или проблемах, делая именно это.

Редактировать: Для многих товаров + количество в корзине: Вы хотите представлять выбранные товары вструктуру коллекции на стороне клиента корзины, затем передайте эту коллекцию, когда делаете что-то вроде размещения заказа.

Таким образом, ProductViewModel может остаться прежним, но затем мы вводим простой OrderedProductViewModel для представления заказанных продуктов при вызове Order:

public class ProductViewModel
{
    public int ProductId { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}
public class OrderedProductViewModel
{
    public int ProductId { get; set; }
    public int Quantity { get; set; }
}

Понятие "корзина" строго на стороне клиента, но если у нас есть модель тележки, которая существует на стороне сервера: (просто не сущность)

public class CartViewModel
{
    public ICollection<OrderedProductViewModel> { get; set; } = new List<OrderedProductViewModel>();
}

Таким образом, список продуктов по-прежнему возвращает коллекцию ProductViewModels в представление.Когда пользователь идет, чтобы добавить продукт в корзину (на стороне клиента), вы сохраняете массив объектов, состоящий из ID продукта и количества.

CreateOrder становится чем-то вроде:

public ActionResult CreateOrder(ICollection<OrderedProductViewModel> products)
{
    if(products == null || !products.Any())
        throw new ArgumentException("Can't create an order without any selected products.");

    using (var context = new MyDbContext())
    {
        var order = new Order
        {
            OrderLines = products.Select(x => createOrderLine(context, x)).ToList(),
            // ... obviously other details, like the current user from session state...
        };
        context.Orders.Add(order);
        context.SaveChanges();
    }
}

private OrderLine createOrderLine(MyDbContext context, OrderedProductViewModel orderedProduct)
{
    if (orderedProduct.Quantity <= 0 || orderedProduct.Quantity > MaximumOrderSize)
        throw new ArgumentException("orderedProduct.Quantity", "Naughty User!");

    var product = context.Products.Single(x => x.ProductId == orderedProduct.ProductId); 
    var orderLine = new OrderLine
    {
        Product = product,
        Quantity = orderedProduct.Quantity,
        UnitCost = product.Price,
        Cost = product.Price * orderedProduct.Quantity,
        // ...
    };
    return orderLine;
}

Он принимает коллекцию OrderedProductViewModels, по существу пары значений идентификатора продукта и количества на заказ.Мы могли бы добавить количественное значение в ProductViewModel и просто установить эту сторону клиента и передать ее обратно, но, как правило, лучше следовать принципу единой ответственности («S» из SOLID), чтобы каждый класс или метод обслуживал один и толькоодна цель, так что у него есть только одна причина для изменения.Кроме того, наша полезная нагрузка будет передаваться настолько большой, насколько это необходимо.Количество не имеет смысла при перечислении доступных продуктов.(если мы не хотим отображать количество на складе, но это не является целью по сравнению с заказанным количеством) Атрибуты, такие как цена продукта или даже название, не имеют смысла при создании заказа и, как указано выше, могут быть опасны для принятия от клиента, так каким можно случайно доверять и использовать.

Приведенный выше пример очень прост, но должен продемонстрировать идею.Например, я регулярно использую шаблон единицы работы (Mehdime DbContextScope) для управления ссылкой DbContext, так что мне не нужно передавать ссылки вокруг.Вы можете иметь контекст уровня модуля с областью действия на весь срок действия для запроса, управляемого контейнером IoC, таким как Autofac, Unity или Windsor, и это тоже хорошо.Иди с тем, что работает, и уточняй оттуда.Ключевой момент не в том, чтобы доверять данным от клиента, а в том, чтобы полезные нагрузки были небольшими.Entity Framework очень эффективен при извлечении сущностей из БД по идентификатору, поэтому нет необходимости думать, что вам нужно кэшировать сущности или экономить время, не перезагружая данные путем передачи сущностей от клиента.Он подвержен вандализму, перегружен трафиком между клиентом и сервером, подвержен всевозможным ошибкам и неожиданному поведению.(Например, устаревшие данные между моментом, когда данные были впервые прочитаны и отправлены клиенту, и когда клиент возвращает их для обновления. (Одновременные обновления) Единственный способ обнаружить это и обработать это - перезагрузить данные из БД в любом случае.)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...