Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Nullable Owned types in EF Core

I my case I want to store an address but it has to be optional.

My mapping lookes like this:

map.OwnsOne(x => x.Address, cb => cb.OwnsOne(l => l.Location));

But when comitting my DbContext with Address as null iam getting this error:

InvalidOperationException: The entity of 'Member' is sharing the table 'Members' with 'Member.Address#StreetAddress', but there is no entity of this type with the same key value 'Id:-2147480644' that has been marked as 'Added'.

I then instantiated the Address and Location from the constructors, and now I can save the entity. But when fetching the data again I also gets an instantiated Address, where i really wanted a null value.

Is it not possible to make nullable Owned Types ?

like image 652
Rune Jensen Avatar asked Jan 02 '18 15:01

Rune Jensen


3 Answers

Is it not possible to make nullable Owned Types?

As of EF Core 3, this is now possible 🎉.

all dependents are now optional. (Shipping in preview 4): Source


Sample Code:

static void Main(string[] args)
{
  using (var context = new OwnedEntityContext())
  {
    context.Add(new DetailedOrder
    {
      Status = OrderStatus.Pending,
      OrderDetails = new OrderDetails
      {
        ShippingAddress = new StreetAddress
        {
          City = "London",
          Street = "221 B Baker St"
        }
        //testing 3.0: "Yes, all dependents are now optional"
        //reference: https://github.com/aspnet/EntityFrameworkCore/issues/9005#issuecomment-477741082
        //NULL Owned Type Testing
        //BillingAddress = new StreetAddress
        //{
        //    City = "New York",
        //    Street = "11 Wall Street"
        //}
      }
    });
    context.SaveChanges();
  }
  //read test
  using (var context = new OwnedEntityContext())
  {
    #region DetailedOrderQuery
    var order = context.DetailedOrders.First(o => o.Status == OrderStatus.Pending);
    Console.Write("NULL Owned Type Test, Is Billing Address NULL?");
    //PRINTS FALSE
    Console.WriteLine($"{order.OrderDetails.BillingAddress == null}");
    #endregion
  }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    #region OwnsOneNested
    modelBuilder.Entity<DetailedOrder>().OwnsOne(p => p.OrderDetails, od =>
    {
        od.OwnsOne(c => c.BillingAddress);
        od.OwnsOne(c => c.ShippingAddress);
    });
    #endregion

    #region OwnsOneTable
    modelBuilder.Entity<DetailedOrder>().OwnsOne(p => p.OrderDetails, od =>
    {
        od.OwnsOne(c => c.BillingAddress);
        od.OwnsOne(c => c.ShippingAddress);
        od.ToTable("OrderDetails");
        //Exception message:Microsoft.Data.SqlClient.SqlException:
        //'Cascading foreign key 'FK_OrderDetails_DetailedOrders_OrderId' cannot
        //be created where the referencing column 'OrderDetails.OrderId' is an identity column.
        //Could not create constraint or index. See previous errors.'
        //3.0 bug: https://github.com/aspnet/EntityFrameworkCore/issues/17448#issuecomment-525444101 
        //fixed in 3.1: https://github.com/aspnet/EntityFrameworkCore/pull/17458
        od.Property("OrderId")
            .ValueGeneratedNever();
    });
    #endregion
}
like image 116
spottedmahn Avatar answered Nov 21 '22 10:11

spottedmahn


One of the limitations of Owned Types is that no support for optional (i.e. nullable). I recommend you to follow this thread.

In my solution, I use the Empty Object approach and use the IsEmpty method to know if an Address is Empty instead of asking if the address is null. I hope this approach helps you.

public sealed class Address : ValueObject<Address>
{
    public string StreetAddress1 { get; private set; }
    public string StreetAddress2 { get; private set; }
    public string City { get; private set; }
    public string State { get; private set; }
    public string ZipCode { get; private set; }
    public string Country { get; private set; }

    private Address() { }
    public Address(string streetAddress1, string city, string state, string zipcode, string country)
    {
        StreetAddress1 = streetAddress1;
        City = city;
        State = state;
        ZipCode = zipcode;
        Country = country;
    }

    public Address(string streetAddress1, string streetAddress2, string city, string state, string zipcode, string country)
        : this(streetAddress1, city, state, zipcode, country)
    {
        StreetAddress2 = streetAddress2;
    }

    public static Address Empty()
    {
        return new Address("", "", "", "", "");
    }

    public bool IsEmpty()
    {
        if (string.IsNullOrEmpty(StreetAddress1)
         && string.IsNullOrEmpty(City)
         && string.IsNullOrEmpty(State)
         && string.IsNullOrEmpty(ZipCode)
         && string.IsNullOrEmpty(Country))
        {
            return true;
        }
        else
        {
            return false;
        }
    }

}
public class Firm : AggregateRoot<Guid>
{
    public string Name { get; private set; }
    public Address Address { get; private set; }

    private Firm() { }
    public Firm(string name)
    {
        if (String.IsNullOrEmpty(name))
            throw new ArgumentException();

        Id = Guid.NewGuid();
        Name = name;
        Address = Address.Empty();
    }
}
like image 34
Denny Puig Avatar answered Nov 21 '22 11:11

Denny Puig


Entity Framework document states that

Reference navigations to owned entity types cannot be null unless they are explicitly mapped to a separate table from the owner

So, in fact, there is a solution to your problem. You need to map your owned entity to a separate table instead of having it inside the same table as the owner.

map.OwnsOne(x => x.Address, cb => cb.OwnsOne(l => l.Location, l=> l.ToTable("Locations")));

By mapping the location entity into a separate table called Locations, the owned entity becomes nullable.

like image 41
Ehsan Mirsaeedi Avatar answered Nov 21 '22 11:11

Ehsan Mirsaeedi