如何将映射实体包括到非映射实体中? [英] How to include Mapped entity to NotMapped entity?

查看:52
本文介绍了如何将映射实体包括到非映射实体中?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

Assume that we have a mapped entity named "Product" as:

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

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

And also a not mapped entity named "Cart" as:

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

将产品"填充到购物车"中的最佳方法是什么?如果两个实体都被映射,则可以这样执行:

What is the best way to populate "Product" into "Cart"? If both entities were mapped then it could be performed like this:

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

是否可以使用类似的语法,或者我应该查询产品"表?

Is it possible to use similar syntax or I should query "Product" table?

修改:这就是我最终所做的:

This is what I finally did:

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

推荐答案

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

An entity reflects the table structure. So yes, if a cart isn't in the data structure and has no entity, then you load products directly.

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

It looks like you are working on an example like a shopping cart where-by your cart reflects the selected products to purchase. In this sense a cart only needs to exist as a view model. The first consideration is to differentiate between view model and model. (Entity) These should be entirely separate and not intermixed. Entities should only exist within the boundaries of the DbContext that loaded them. They can be detached at treated as POCO C# objects, but this can easily lead to bugs, performance issues, and vulnerabilities.

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

Can a cart only consist of one product? or a list of different products and quantities?

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

On the assumption that a cart is a single product (and quantity)

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实例:

When the view goes to get a list of products to display, the controller returns back ProductViewModel instances:

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

"Why not just return the entity?" As the system develops the product table will likely grow to include more columns, and more relationships that need to be considered. For example, associating stock levels. The view does not need to know about anything more than it needs to know about and a serializer will attempt to serialize everything it is fed. This means sending more data to the client than is needed, and can lead to problems where you may have circular references. By utilizing .Select() you optimize a query to just return the fields needed and only expose that data to the client. The ProductViewModel should contain all information you need to display on the client, and the details you will need to complete any action against that product. With Automapper you can set up mapping rules for Entity to ViewModel and utilize ProjectTo<T>() to replace the Select() without the need to map the properties manually each time.

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

With the list of available products your client-side code can select one of these view models to associate with the Cart and record a quantity. You can calculate a total on-screen using the ProductViewModel's Price value.

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

When you go to complete an order, you could pass the selected product ViewModel back to the server along with the quantity, but it would be better to just pass the Product's ID back with the quantity. Assuming the server call is going to create an Order for that product:

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

You might be tempted to do something like this with a view model:

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%的折扣.我们的订单将以客户发送的价格为基础.

The problem with this is that we are too trusting the data coming back from the client. Someone using debugging tools can intercept the call to our server from their client and set the product.Price to $0.00 or give themselves a 50% discount. Our order would base the cost on the price sent from the client.

相反:

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篡改为无效值,我们的应用程序异常处理(或您可以为每个操作添加异常处理)将记录故障,并在怀疑篡改时终止会话.篡改无法更改订单成本,因为每次数据都来自我们的服务器.

In this case we have verified that the Product actually exists, and we only use the Price from our trusted data, not what was passed by the client. If for instance the client tampered with the Product ID to an invalid value, our application exception handling (or you can add exception handling per action) will record the fault and can terminate the session if it suspects tampering. The tampering cannot change the cost of the order because that data comes from our server every time.

为了概述为什么不希望将实体发送出去,特别是不希望从客户端将实体发送回服务器,让我们看一下视图模型示例,除了传递回Product实体之外:

To outline why you do not want to send Entities around, especially back to the server from the client, let's look at the view model example, except pass back a Product entity:

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(以及其他任何篡改数据)的重复产品条目,要么引发关于重复主键的异常.

This may look "Ok", but there is a big problem. "context" in this case does not know about that instance of Product. It was deserialized from the request and for all intensive purposes looks like a new Product instance you new'ed up. This would result in EF either creating a duplicate Product entry with a new ID (and any other tampered data), or throwing an exception about a duplicate primary key.

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

Now that is duplicate or error is fixable, we just need to attach the entity...

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也会保存骇客客户端对产品所做的任何更改.例如:

And it should work. In some cases the context may already know about the entity and throw an error if it already exists. (such as cases where we would be looping through data which may have duplicate references) Except we are still trusting the data coming from the client. The user can still have modified the price which will be reflected in the order. This is also potentially very dangerous because if we later go to modify something on the product, or simply mark it as Modified, EF will save any alterations that the hacking client has made to the product as well. For instance:

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,修改日期等的管理功能的地方.由于我们信任从客户端返回的实体,因此未必有更新.即使您记录了该用户破坏了数据的事实,也可能破坏系统.

Assuming our Product had an available quantity property that we want to update when placing an order, this call makes that adjustment. The entity is now marked as modified. What we didn't notice is that where our product's price is normally $100, and $100 was sent to the client, that hacking user saw that we were passing back the whole product and was curious what would happen if he changed the price to $50 in the data sent back to the server. Not only would his order be at $50 per product, but he's now changed our product price from $100 to $50 because the modified entity was associated to the context, marked as modified, and the changes were saved. Where we might have had admin functions for altering products, tracking user IDs, modified dates, etc. none of that necessarily may have been updated since we trusted an entity coming back from a client. Even if you record the fact that this user corrupted the data, the potential for disruption to your system becomes huge.

您可以选择保存传递时间的实体,而只是在返回途中不信任它们,并始终重新加载该实体.但是随着系统的成熟,风险是某人会变得草率或懒惰,无论如何都要确定该实体是否存在,并对上下文进行 Attach / Update .您可以在StackOverflow中找到几个问题示例,在此过程中,人们提出了有关错误或问题的问题.

You can choose to save time passing entities and just not trust them on the way back and always reload the entity. But the risk as your system matures is that someone will get sloppy or lazy, figure the entity is there anyways and Attach / Update against the context. You can find several examples of questions in StackOverflow where people have raised questions about errors or issues while doing exactly that.

对于每个购物车有多种产品和数量:您将要在购物车客户端的集合结构中表示所选产品,然后在进行下订单等操作时传递该集合.

For a many product+quantity per cart: You will want to represent the selected products in a collection structure within the cart client side, then pass that collection when doing something like placing an order.

因此ProductViewModel可以保持不变,但是我们在调用Order时引入了一个简单的OrderedProductViewModel来表示订购的产品:

So the ProductViewModel can stay the same, but then we introduce a simple OrderedProductViewModel to represent the ordered products when calling 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; }
}

购物车"的概念严格来说是客户端,但是如果我们确实有一个在服务器端存在的购物车模型:(不是实体)

The concept of a "cart" is strictly client-side, but if we do have a cart model that exists server-side: (just not an entity)

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

因此,产品列表仍然将ProductViewModels的集合返回到视图.当用户将产品添加到购物车(客户端)时,您将存储由产品ID和数量组成的对象数组.

So the product list still returns a collection of ProductViewModels to the view. When the user goes to add a product to the cart (client-side) you store an array of objects consisting of the Product ID and Quantity.

CreateOrder变为:

CreateOrder becomes something like:

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"),以便每个类或方法都可以使用一个,并且仅一个目的,使它只有一个改变的理由.它还可以使我们的有效载荷仅按需要的大小传输.列出可用产品时,数量无用.(除非我们要显示库存数量,但这与订购数量不同.)在创建订单时,产品价格甚至名称之类的属性均无用.如上所述,从客户那里接受该产品实际上可能很危险,因为它们可能会意外地被信任和使用.

It accepts a collection of OrderedProductViewModels, essentially value pairs of product ID and quantity to order. We could have added a quantity value to ProductViewModel and just set that client side and passed that back, but as a general rule it's better to follow Single Responsibility Principle ("S" from S.O.L.I.D.) so that each class or method serves one, and only one purpose so that it only has one reason to change. It also keeps our payload being transmitted only as large as it needs to be. Quantity serves no purpose when listing available products. (unless we want to display stock quantity, but that is a different purpose to ordered quantity) Attributes like product price or even name serve no purpose when creating an order, and as outlined above, can actually be dangerous to accept from the client, as they may accidentally be trusted and used.

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

The above example is very bare-bones, but should demonstrate the idea. For instance I regularly use a unit of work pattern (Mehdime DbContextScope) for managing the DbContext reference so that I don't need to pass references around. You could have a module-level Context with a lifetime scope to the request managed by an IoC container like Autofac, Unity, or Windsor and that's fine too. Go with what works and refine from there. The key point is not to trust data from the client, and keep payloads small. Entity Framework is very efficient at pulling entities from the DB by ID so there's no need to think you need to cache entities or save time by not reloading data by passing entities from the client. It's prone to vandalism, heavy on traffic between client and server, and prone to all kinds of bugs & unexpected behaviour. (Such as stale data between when data was first read and sent to the client, and when the client returns it to be updated. (concurrent updates) The only way to detect this and handle it is reloading the data from the DB anyways.)

这篇关于如何将映射实体包括到非映射实体中?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆