Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why reference properties works only through context

Tags:

c#

ef-core-2.0

I have two classes Order and OrderDetail:

  public class Order : Entity
{
    public Order(KitchenAppContext context) : base(context)
    {

    }
    public Order() : base()
    {

    }
    public DateTime Date { get; set; }
    public Guid MenuId { get; set; }
    public virtual Menu Menu { get; set; }
    public bool IsClosed { get; set; }
    public decimal Price { get; set; }
    public virtual int PeopleCount { get { return Details.Count; } }
    public virtual List<OrderDetail> Details { get; set; } = new List<OrderDetail>();
}

 public class OrderDetail : Entity
{
    public OrderDetail(KitchenAppContext context) : base(context)
    {

    }

    public OrderDetail() : base()
    {

    }
    public Guid UserId { get; set; }
    public virtual User User { get; set; }
    public virtual List<PaymentDetail> Payments { get; set; } = new List<PaymentDetail>();
    public virtual Order Order { get; set; }
    public Guid OrderId { get; set; }
}

They are mapped like this:

  void OrderMapping(ModelBuilder builder)
    {
        var etBuilder = builder.Entity<Order>();
        etBuilder.HasKey(m => new { m.Id });
        etBuilder.HasOne(o => o.Menu).WithMany(a => a.Orders).HasForeignKey(f => f.MenuId);
        etBuilder.HasMany(o => o.Details).WithOne(d => d.Order).HasForeignKey(f => f.OrderId);
    }

  void OrderDetailMapping(ModelBuilder builder)
    {
        var etBuilder = builder.Entity<OrderDetail>();
        etBuilder.HasKey(m => new { m.Id });
        etBuilder.HasOne(o => o.User).WithMany(u => u.Details).HasForeignKey(f => f.UserId);
        etBuilder.HasOne(o => o.Order).WithMany(u => u.Details).HasForeignKey(f => f.OrderId);
        etBuilder.HasMany(o => o.Payments).WithOne(d => d.OrderDetail).HasForeignKey(f => f.OrderDetailId);
    }

When I create order and order details :

  var order = new Order(Context);
        Context.Orders.Add(order);
        var oderDetail = new OrderDetail(Context) { Order = order };

Details of order empty there and OrderId of orderdetails is null also. When I add created order detail in context then it will be added to Details and OrderId becomes Id of created order. Why it works only when I add it to context? I want that it works without adding it to context. Maybe, I should do something in consctructor of classes (with Context parameter)? How can I do this?

EDIT: Order and OrderDetails classes inherited from abstact class Entity:

 public abstract class Entity
{
    Guid id;
    public Guid Id
    {
        get
        {
            if (id == null || id == Guid.Empty)
            {
                id = Guid.NewGuid();
            }
            return id;
        }
        set
        {
            id = value;
        }
    }

    public Entity(KitchenAppContext context)
    {
        Context = context;
    }

    public Entity()
    {

    }
    public MainContext Context;
}

Also, as you see, I have constructor without parameter. I created them because EF shows this message when I'm getting entities from context:

System.InvalidOperationException: 'A parameterless constructor was not found on entity type 'Order'. In order to create an instance of 'Order' EF requires that a parameterless constructor be declared.'

How can I avoid this error without creating conscrtuctors without parameter?

like image 760
Dilshod K Avatar asked May 17 '19 04:05

Dilshod K


2 Answers

I solved second issue (A parameterless constructor was not found... exception) like this:

  1. I setted default constructor of Entity class and sub entities as protected

  2. When I load entity from DB Context property of entities will be null, because EF uses default constructor. That's why I created my own IQuerable collection. It sets Context property when it's not setted:

    class IContextable<T> : IQueryable<T> where T : Entity {

    public IQueryable<T> SourceQuery { get; set; }
    public KitchenAppContext Context { get; set; }
    public IContextable(IQueryable<T> query, KitchenAppContext context)
    {
        SourceQuery = query;
        Context = context;
    }
    
    public Type ElementType => SourceQuery.ElementType;
    
    public Expression Expression => SourceQuery.Expression;
    
    public IQueryProvider Provider => SourceQuery.Provider;
    
    public IEnumerator<T> GetEnumerator()
    {
        foreach (var entity in SourceQuery)
        {
            if (entity.Context == null || entity.Context != Context)
                entity.Context = Context;
            yield return entity;
        }
    }
    
    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
    }
    

And my GetEntities method in my Context class:

public IQueryable<T> GetEntities<T>() where T : Entity
    {
        IQueryable<T> query = Set<T>();
        return new IContextable<T>(query, this);
    }

Maybe, there was better ways, but I couldn't found them. It works now, but I'm still waiting for good answer

like image 112
Dilshod K Avatar answered Sep 28 '22 19:09

Dilshod K


When you create the OrderDetail object you have to do something with it. Currently you have only set the Order property. And that's it, EntityFramework doesn't know that this object exists so it cannot do anything with it.

To make it "known" to EntityFramework you have to add it directly or indirectly to the context. The obvious way is to add the object directly to the context as you have already said and tried:

Context.OrderDetails.Add(orderDetail);

Another way is it to add the OrderDetail object to your Order objects Details property/list. When the Order object is added to the context and it references a OrderDetail object, then saving the Order object will also "see" the OrderDetail object and save it as well. This way the OrderDetail object is added indirectly to the context. The code might look like this:

var order = new Order(Context);
var orderDetail = new OrderDetail(Context)
{
    Order = order // might not be needed
};
order.Details.Add(orderDetail);
Context.Orders.Add(order);

This will save the Order object. Additionally it will check the references and links and will see the added OrderDetail object. So it will save the OrderDetail object as well.

like image 43
Progman Avatar answered Sep 28 '22 19:09

Progman