简体   繁体   中英

How can I resolve a “Do not call overridable methods in constructors” warning for virtual Entity Framework objects?

I'm using Entity Framework and want to use lazy loading on properties, so I'm making the properties virtual .

An example:

public class Child
{
    public string Name { get; set; }
}

public class Parent
{
    public Parent()
    {
        Child = new Child();
    }

    public virtual Child Child { get; set; }
}

When I do that, I get a CA2214 warning:

Severity    Code    Description Project File    Line    Suppression State
Warning CA2214  'Parent.Parent()' contains a call chain that results in a call to a virtual method defined by the class. Review the following call stack for unintended consequences: 

Parent..ctor()
Parent.set_Child(Child):Void    Project C:\Parent.cs    18  Active

I'd like to remove this warning, but if I mark Parent as sealed I get the expected error:

Severity    Code    Description Project File    Line    Suppression State
Error   CS0549  'Parent.Child.get' is a new virtual member in sealed class 'Parent' Project C:\Parent.cs    24  N/A

So how can I resolve this warning (without ignoring it) and still use virtual ?

Use an initializer on the property rather than the constructor.

public class Parent
{
    public virtual Child Child { get; set; } = new Child();
}

Edit: Regarding the above and that

...there are times when I would need to set properties for Child in the constructor...

The simple rule is "you probably shouldn't". An entity's role is to represent data state for that entity, nothing more. Initializing an entity "graph" should not be done in a top-level entity's constructor, but rather using a Factory pattern. For instance, I use a Repository pattern with EF which I manage not only getters, but also serve as the factory providing Create methods, as well as handling Delete for soft-delete scenarios. This helps ensure that an entity with dependencies is always created in a "minimally complete" and valid state.

Even the above example I would say is a bad example. Even though it doesn't trip the compiler warning, the entity at the point of a parent's construction isn't in a complete and valid state. If I were to do something like:

using (var context = new MyContext()) { var parent = new Parent(); parent.Name = "Myself"; context.SaveChanges(); }

If the Parent auto-initializes a Child, that SaveChanges will want to save that new Child, and there is nothing that ensured that all required fields on the child are set, that SaveChanges call will fail. Child isn't in a complete enough state.

The only place I would advocate auto-initializing would be collections:

public class Parent
{
    public virtual ICollection<Child> Children { get; internal set; } = new List<Child>();
}

The above is still "complete" in that an empty collection won't attempt to save anything for children if I populate a new parent without adding any children. It is also convenient so that when I create a new parent, I have the option to immediately start adding/associating children without tripping a null reference exception if I had no children.

To initialize an object graph with a factory method helps ensure that entities are always created in a minimally complete state, which means they can be saved immediately without error. As I mentioned above, I generally use my Repository to serve as the entity factory since it's already wired up with the DbContext through the unit of work to resolve dependencies as needed.

As an example if I have an Order entity that I can create that consists of an order number, customer reference, and one or more order lines for products which are required to save a valid order, my OrderRepository might have a CreateOrder method something like this:

public Order CreateOrder(int customerId, IEnumerable<OrderedProductViewModel> orderedProducts)
{
    if (!orderedProducts.Where(x => x.Quantity > 0).Any())
       throw new ArgumentException("No products selected.");

    var customer = Context.Customers.Single(x => x.CustomerId == customerId);
    var products = Context.Products.Where(x => orderedProducts.Where(o => o.Quantity > 0).Select(o => o.ProductId).Contains(x.ProductId)).ToList();
    if (products.Count() < orderedProducts.Count())
       throw new ArgumentException("Invalid products included in order.");
   var order = new Order 
   { 
      Customer = customer,
      OrderLines = orderedProducts.Select(x => new OrderLine 
      { 
         Product = products.Single(p => p.ProductId == x.ProductId),
         Quantity = x.Quantity
      }
   }
   Context.Orders.Add(order);
   return order;
}

This is a contextual example of a factory method I might use, and some of the basic validation. OrderedProductViewModel represents effectively a tuple of a ProductId and a Quantity. It along with a Customer ID represent the minimum state of an order I would allow to be saved. There may be other optional details that might be set outside of this method before an Order is considered complete enough to ship, but the factory ensures it is complete enough to save.

I could have calling code like:

using (var contextScope = ContextScopeFactory.Create())
{
    var order = OrderRepository.Create(selectedCustomerId, selectedProducts);
    contextScope.SaveChanges();
}

And that would be happy. Or I could continue to set available information on the order before calling SaveChanges. My OrderRepository would not have any business logic because that business logic may be dependent on configuration for the client and the repository has no business knowing or caring about that; but it could have a dependency for something like an IOrderValidator to pass the newly proposed Order to which the business logic could run across to assert that the Order is valid enough to be saved. The repoistory/factory asserts it is complete enough, but it can be tied to a validator back in the business logic to assert it is valid enough. (Ie minimum/ maximum order size / value, etc.)

This coupled with DDD where I make all setters internal and use action methods on my entities helps control ensuring that my entities are always maintained in a complete state. I'm guessing this is something you are trying to ensure through using the constructors so I thought I'd share the above as an example to provide some possible ideas and alternatives to accomplish that.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM