如何将映射实体包括到非映射实体?

时间:2019-04-30 18:14:55

标签: c# asp.net-mvc entity-framework

假设我们有一个名为“ Product”的映射实体,如下:

app = Flask(__name__)
app.config['ERROR_404_HELP'] = False

还有一个名为“ Cart”的未映射实体,如下:

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);

1 个答案:

答案 0 :(得分:0)

一个实体反映了表的结构。因此,是的,如果购物车不在数据结构中并且没有实体,那么您可以直接加载产品。

您似乎正在处理一个购物车之类的示例,其中您的购物车反映了要购买的所选产品。从这个意义上说,购物车仅需要作为视图模型存在。首先要考虑的是区分视图模型和模型。 (实体)这些应完全分开且不可混合。实体只能存在于加载它们的DbContext的边界之内。它们 可以作为POCO C#对象分离,但这很容易导致bug,性能问题和漏洞。

购物车只能包含一种产品吗?或不同产品和数量的列表?

假设购物车是单一产品(和数量)

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(),而无需每次都手动映射属性。

使用可用产品列表,您的客户端代码可以选择这些视图模型之一与购物车关联并记录数量。您可以使用ProductViewModel的Price值在屏幕上计算总金额。

当您完成订单时,可以将选定的产品ViewModel与数量一起传递回服务器,但是最好将产品ID与数量一起传递回服务器。假设服务器调用将为该产品创建订单:

您可能会想在视图模型中执行以下操作:

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();
    }
}

在这种情况下,我们已验证产品确实存在,并且我们仅使用来自受信任数据的价格,而不使用客户传递的价格。例如,如果客户将产品ID篡改为无效值,我们的应用程序异常处理(或您可以为每个操作添加异常处理)将记录故障,并在怀疑篡改时终止会话。篡改无法更改订单成本,因为每次数据都来自我们的服务器。

为概述为什么不希望将实体发送到周围,尤其是从客户端发送回服务器的原因,让我们看一下视图模型示例,除了传递回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();
    }
}

这看起来可能是“好”,但是有一个大问题。在这种情况下,“上下文”不知道该产品实例。它已从请求中反序列化,并且出于所有深入的目的,看起来就像您新建的新Product实例。这将导致EF要么创建具有新ID(以及其他任何篡改数据)的重复产品条目,要么引发关于重复主键的异常。

现在重复或错误可以修复,我们只需要附加实体...

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,因为修改后的实体与上下文相关联,标记为已修改,并且更改已保存。在可能具有管理功能(用于更改产品,跟踪用户ID,修改日期等)的地方,由于我们信任从客户端返回的实体,因此未必有更新。即使您记录了该用户破坏了数据的事实,也可能破坏系统。

您可以选择保存传递时间的实体,而只是在返回途中不信任它们并始终重新加载该实体。但是随着系统的成熟,风险是某人会变得草率或懒惰,无论如何都认为该实体存在,并且会针对上下文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的集合,本质上是要订购的产品ID和数量的值对。我们可以在ProductViewModel中添加一个数量值,然后设置该客户端并将其传递回去,但是作为一般规则,最好遵循单一职责原则(SOLID中的“ S”),以便每个类或方法都可以使用一个,并且仅一个目的,使它只有一个改变的理由。这也使我们的有效负载仅按需要传输。列出可用产品时,数量无用。 (除非我们要显示库存数量,但这与订购数量不同。)在创建订单时,产品价格甚至名称之类的属性均无用。如上所述,从客户那里接受该产品实际上可能很危险,因为他们可能会意外地被信任和使用。

上面的示例是非常简单的,但是应该演示一下这个想法。例如,我经常使用工作单元模式(Mehdime DbContextScope)来管理DbContext引用,这样我就不需要传递引用了。您可以在模块级上下文中使用生命周期范围的请求,该范围由IoC容器(如Autofac,Unity或Windsor)管理的请求,也可以。选择可行的方法,然后从那里进行优化。关键是不要信任来自客户端的数据,并保持有效载荷较小。实体框架在按ID从数据库中提取实体方面非常有效,因此无需考虑需要缓存实体或通过不从客户端传递实体来重新加载数据来节省时间。它容易被人为破坏,客户端和服务器之间的通信量很大,并且容易出现各种错误和意外行为。 (例如,在首次读取数据并将其发送给客户端之前以及客户端将其返回以进行更新之间的陈旧数据。(并发更新)检测和处理数据的唯一方法是无论如何都要从DB重新加载数据。)

相关问题