Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Model configuration: A property that will be shadow and is used as part of an FK must be configured as a shadow property first #6823

Closed
FransBouma opened this issue Oct 20, 2016 · 13 comments
Labels
closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. type-bug
Milestone

Comments

@FransBouma
Copy link

FransBouma commented Oct 20, 2016

Steps to reproduce

AdventureWorks 2008.
I have this mapping to map SalesOrderHeader:

/// <summary>Defines the mapping information for the entity 'SalesOrderHeader'</summary>
/// <param name="config">The configuration to modify.</param>
protected virtual void MapSalesOrderHeader(EntityTypeBuilder<SalesOrderHeader> config)
{
    config.ToTable("SalesOrderHeader", "Sales");
    config.HasKey(t => t.SalesOrderId);
    config.Property(t => t.SalesOrderId).HasColumnName("SalesOrderID").ValueGeneratedOnAdd();
    config.Property(t => t.RevisionNumber);
    config.Property(t => t.OrderDate);
    config.Property(t => t.DueDate);
    config.Property(t => t.ShipDate);
    config.Property(t => t.Status);
    config.Property(t => t.OnlineOrderFlag);
    config.Property(t => t.SalesOrderNumber).HasMaxLength(25).ValueGeneratedOnAddOrUpdate().IsRequired();
    config.Property(t => t.PurchaseOrderNumber).HasMaxLength(25);
    config.Property(t => t.AccountNumber).HasMaxLength(15);
    config.Property(t => t.CreditCardApprovalCode).HasMaxLength(15);
    config.Property(t => t.SubTotal);
    config.Property(t => t.TaxAmt);
    config.Property(t => t.Freight);
    config.Property(t => t.TotalDue).ValueGeneratedOnAddOrUpdate();
    config.Property(t => t.Comment).HasMaxLength(128);
    config.Property(t => t.Rowguid).HasColumnName("rowguid");
    config.Property(t => t.ModifiedDate);
    config.HasOne(t => t.Address).WithMany(t => t.SalesOrderHeaders).HasForeignKey("BillToAddressID").IsRequired();
    config.HasOne(t => t.Address_).WithMany(t => t.SalesOrderHeaders_).HasForeignKey("ShipToAddressID").IsRequired();
    config.HasOne(t => t.Contact).WithMany(t => t.SalesOrderHeaders).HasForeignKey("ContactID").IsRequired();
    config.HasOne(t => t.CreditCard).WithMany(t => t.SalesOrderHeaders).HasForeignKey("CreditCardID").IsRequired();
    config.HasOne(t => t.CurrencyRate).WithMany(t => t.SalesOrderHeaders).HasForeignKey("CurrencyRateID").IsRequired();
    config.HasOne(t => t.Customer).WithMany(t => t.SalesOrderHeaders).HasForeignKey("CustomerID").IsRequired();
    config.HasOne(t => t.SalesPerson).WithMany(t => t.SalesOrderHeaders).HasForeignKey("SalesPersonID");
    config.HasOne(t => t.SalesTerritory).WithMany(t => t.SalesOrderHeaders).HasForeignKey("TerritoryID").IsRequired();
    config.HasOne(t => t.ShipMethod).WithMany(t => t.SalesOrderHeaders).HasForeignKey("ShipMethodID").IsRequired();
}

This mapping to SalesOrderDetail:

/// <summary>Defines the mapping information for the entity 'SalesOrderDetail'</summary>
/// <param name="config">The configuration to modify.</param>
protected virtual void MapSalesOrderDetail(EntityTypeBuilder<SalesOrderDetail> config)
{
    config.ToTable("SalesOrderDetail", "Sales");
    config.HasKey(t => new { t.SalesOrderDetailId, t.SalesOrderId });
    config.Property(t => t.SalesOrderId).HasColumnName("SalesOrderID");
    config.Property(t => t.SalesOrderDetailId).HasColumnName("SalesOrderDetailID").ValueGeneratedOnAdd();
    config.Property(t => t.CarrierTrackingNumber).HasMaxLength(25);
    config.Property(t => t.OrderQty);
    config.Property(t => t.UnitPrice);
    config.Property(t => t.UnitPriceDiscount);
    config.Property(t => t.LineTotal).ValueGeneratedOnAddOrUpdate();
    config.Property(t => t.Rowguid).HasColumnName("rowguid");
    config.Property(t => t.ModifiedDate);
    config.HasOne(t => t.SalesOrderHeader).WithMany(t => t.SalesOrderDetails).HasForeignKey(t => t.SalesOrderId).IsRequired().OnDelete(DeleteBehavior.Cascade);
    config.HasOne(t => t.SpecialOfferProduct).WithMany(t => t.SalesOrderDetails).HasForeignKey("ProductID", "SpecialOfferID").IsRequired();  // <<<<<< Problematic line
}

For completeness: the SpecialOfferProduct mapping, with the compound PK

/// <summary>Defines the mapping information for the entity 'SpecialOfferProduct'</summary>
/// <param name="config">The configuration to modify.</param>
protected virtual void MapSpecialOfferProduct(EntityTypeBuilder<SpecialOfferProduct> config)
{
    config.ToTable("SpecialOfferProduct", "Sales");
    config.HasKey(t => new { t.ProductId, t.SpecialOfferId });
    config.Property(t => t.SpecialOfferId).HasColumnName("SpecialOfferID");
    config.Property(t => t.ProductId).HasColumnName("ProductID");
    config.Property(t => t.Rowguid).HasColumnName("rowguid");
    config.Property(t => t.ModifiedDate);
    config.HasOne(t => t.Product).WithMany(t => t.SpecialOfferProducts).HasForeignKey(t => t.ProductId).IsRequired();
    config.HasOne(t => t.SpecialOffer).WithMany(t => t.SpecialOfferProducts).HasForeignKey(t => t.SpecialOfferId).IsRequired();
}

The FK fields aren't part of the POCO:

//------------------------------------------------------------------------------
// <auto-generated>This code was generated by LLBLGen Pro v5.1.</auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;

namespace AW.EntityClasses
{
    /// <summary>Class which represents the entity 'SalesOrderHeader'.</summary>
    public partial class SalesOrderHeader : CommonEntityBase
    {
        #region Class Property Declarations
        /// <summary>Gets or sets the AccountNumber field. </summary>
        public System.String AccountNumber { get; set;}
        /// <summary>Gets or sets the Comment field. </summary>
        public System.String Comment { get; set;}
        /// <summary>Gets or sets the CreditCardApprovalCode field. </summary>
        public System.String CreditCardApprovalCode { get; set;}
        /// <summary>Gets or sets the DueDate field. </summary>
        public System.DateTime DueDate { get; set;}
        /// <summary>Gets or sets the Freight field. </summary>
        public System.Decimal Freight { get; set;}
        /// <summary>Gets or sets the ModifiedDate field. </summary>
        public System.DateTime ModifiedDate { get; set;}
        /// <summary>Gets or sets the OnlineOrderFlag field. </summary>
        public System.Boolean OnlineOrderFlag { get; set;}
        /// <summary>Gets or sets the OrderDate field. </summary>
        public System.DateTime OrderDate { get; set;}
        /// <summary>Gets or sets the PurchaseOrderNumber field. </summary>
        public System.String PurchaseOrderNumber { get; set;}
        /// <summary>Gets or sets the RevisionNumber field. </summary>
        public System.Byte RevisionNumber { get; set;}
        /// <summary>Gets or sets the Rowguid field. </summary>
        public System.Guid Rowguid { get; set;}
        /// <summary>Gets or sets the SalesOrderId field. </summary>
        public System.Int32 SalesOrderId { get; set;}
        /// <summary>Gets or sets the SalesOrderNumber field. </summary>
        public System.String SalesOrderNumber { get; set;}
        /// <summary>Gets or sets the ShipDate field. </summary>
        public Nullable<System.DateTime> ShipDate { get; set;}
        /// <summary>Gets or sets the Status field. </summary>
        public System.Byte Status { get; set;}
        /// <summary>Gets or sets the SubTotal field. </summary>
        public System.Decimal SubTotal { get; set;}
        /// <summary>Gets or sets the TaxAmt field. </summary>
        public System.Decimal TaxAmt { get; set;}
        /// <summary>Gets or sets the TotalDue field. </summary>
        public System.Decimal TotalDue { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'SalesOrderHeader.Address - Address.SalesOrderHeaders (m:1)'</summary>
        public Address Address { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'SalesOrderHeader.Address_ - Address.SalesOrderHeaders_ (m:1)'</summary>
        public Address Address_ { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'SalesOrderHeader.Contact - Contact.SalesOrderHeaders (m:1)'</summary>
        public Contact Contact { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'SalesOrderHeader.CreditCard - CreditCard.SalesOrderHeaders (m:1)'</summary>
        public CreditCard CreditCard { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'SalesOrderHeader.CurrencyRate - CurrencyRate.SalesOrderHeaders (m:1)'</summary>
        public CurrencyRate CurrencyRate { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'SalesOrderHeader.Customer - Customer.SalesOrderHeaders (m:1)'</summary>
        public Customer Customer { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'SalesOrderDetail.SalesOrderHeader - SalesOrderHeader.SalesOrderDetails (m:1)'</summary>
        public List<SalesOrderDetail> SalesOrderDetails { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'SalesOrderHeaderSalesReason.SalesOrderHeader - SalesOrderHeader.SalesOrderHeaderSalesReasons (m:1)'</summary>
        public List<SalesOrderHeaderSalesReason> SalesOrderHeaderSalesReasons { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'SalesOrderHeader.SalesPerson - SalesPerson.SalesOrderHeaders (m:1)'</summary>
        public SalesPerson SalesPerson { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'SalesOrderHeader.SalesTerritory - SalesTerritory.SalesOrderHeaders (m:1)'</summary>
        public SalesTerritory SalesTerritory { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'SalesOrderHeader.ShipMethod - ShipMethod.SalesOrderHeaders (m:1)'</summary>
        public ShipMethod ShipMethod { get; set;}
        #endregion
    }
}

Running this simple query:

[Test]
public void LoadSoHIDs()
{
    using(var ctx = new AdventureWorksDataContext())
    {
        var allRows = (from s in ctx.SalesOrderHeaders
                        select new SalesOrderHeader() { SalesOrderId = s.SalesOrderId}).ToList();
        Assert.IsTrue(allRows.Count > 0);
    }
}

gives:

System.InvalidOperationException : The property 'ProductID' cannot be added to the entity type 'SalesOrderDetail' because there was no property type specified and there is no corresponding CLR property. To add a shadow state property the property type needs to be specified.
   at Microsoft.EntityFrameworkCore.Metadata.Internal.InternalEntityTypeBuilder.GetOrCreateProperties(IEnumerable`1 propertyNames, ConfigurationSource configurationSource, IEnumerable`1 referencedProperties)
   at Microsoft.EntityFrameworkCore.Metadata.Internal.InternalRelationshipBuilder.HasForeignKey(IReadOnlyList`1 propertyNames, ConfigurationSource configurationSource)
   at Microsoft.EntityFrameworkCore.Metadata.Builders.ReferenceCollectionBuilder`2.HasForeignKey(String[] foreignKeyPropertyNames)
   at AW.AdventureWorksModelBuilder.MapSalesOrderDetail(EntityTypeBuilder`1 config) in C:\Temp\generatortest\test1\Persistence\AdventureWorksModelBuilder.cs:line 733
   at AW.AdventureWorksModelBuilder.BuildModel(ModelBuilder modelBuilder) in C:\Temp\generatortest\test1\Persistence\AdventureWorksModelBuilder.cs:line 62
   at AW.AdventureWorksDataContext.OnModelCreating(ModelBuilder modelBuilder) in C:\Temp\generatortest\test1\Persistence\AdventureWorksDataContext.cs:line 26
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.CreateModel(DbContext context, IConventionSetBuilder conventionSetBuilder, IModelValidator validator)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel()
   at Microsoft.EntityFrameworkCore.Internal.LazyRef`1.get_Value()
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model()
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.ScopedCallSite.Invoke(ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ConstructorCallSite.Invoke(ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.ScopedCallSite.Invoke(ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ConstructorCallSite.Invoke(ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.ScopedCallSite.Invoke(ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.ScopedCallSite.Invoke(ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ConstructorCallSite.Invoke(ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.ScopedCallSite.Invoke(ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ConstructorCallSite.Invoke(ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.ScopedCallSite.Invoke(ServiceProvider provider)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider)
   at Microsoft.EntityFrameworkCore.Infrastructure.AccessorExtensions.GetService[TService](IInfrastructure`1 accessor)
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.<.ctor>b__2_0()
   at Microsoft.EntityFrameworkCore.Internal.LazyRef`1.get_Value()
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.System.Linq.IQueryable.get_Provider()
   at System.Linq.Queryable.Select[TSource,TResult](IQueryable`1 source, Expression`1 selector)
   at AWTests.ReadTests.LoadSoHIDs() in C:\Temp\generatortest\Test1\AWTests\ReadTests.cs:line 34

However, this is odd, as there's no way to specify the type for the field in HasForeignKey(). It also shouldn't have to, as the type can be derived from the PK field it corresponds with (which is mapped properly).

The issue

Mapping fails, which shouldn't. Likely compound PK handling.

Further technical details

EF Core version: 1.0.1, running on .NET full 4.5.2
Operating system: windows 8.1
Visual Studio version: 2015

@FransBouma
Copy link
Author

FransBouma commented Oct 20, 2016

Another situation, different error, likely same cause (as in: HasForeignKey is broken):

Poco:

// <auto-generated>This code was generated by LLBLGen Pro v5.1.</auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;

namespace AW.EntityClasses
{
    /// <summary>Class which represents the entity 'UnitMeasure'.</summary>
    public partial class UnitMeasure : CommonEntityBase
    {

        /// <summary>Gets or sets the ModifiedDate field. </summary>
        public System.DateTime ModifiedDate { get; set;}
        /// <summary>Gets or sets the Name field. </summary>
        public System.String Name { get; set;}
        /// <summary>Gets or sets the UnitMeasureCode field. </summary>
        public System.String UnitMeasureCode { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'BillOfMaterial.UnitMeasure - UnitMeasure.BillOfMaterials (m:1)'</summary>
        public List<BillOfMaterial> BillOfMaterials { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'Product.UnitMeasure - UnitMeasure.Products (m:1)'</summary>
        public List<Product> Products { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'Product.UnitMeasure_ - UnitMeasure.Products_ (m:1)'</summary>
        public List<Product> Products_ { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'ProductVendor.UnitMeasure - UnitMeasure.ProductVendors (m:1)'</summary>
        public List<ProductVendor> ProductVendors { get; set;}
    }
}

//------------------------------------------------------------------------------
// <auto-generated>This code was generated by LLBLGen Pro v5.1.</auto-generated>
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;

namespace AW.EntityClasses
{
    /// <summary>Class which represents the entity 'Product'.</summary>
    public partial class Product : CommonEntityBase
    {

        /// <summary>Gets or sets the Class field. </summary>
        public System.String Class { get; set;}
        /// <summary>Gets or sets the Color field. </summary>
        public System.String Color { get; set;}
        /// <summary>Gets or sets the DaysToManufacture field. </summary>
        public System.Int32 DaysToManufacture { get; set;}
        /// <summary>Gets or sets the DiscontinuedDate field. </summary>
        public Nullable<System.DateTime> DiscontinuedDate { get; set;}
        /// <summary>Gets or sets the FinishedGoodsFlag field. </summary>
        public System.Boolean FinishedGoodsFlag { get; set;}
        /// <summary>Gets or sets the ListPrice field. </summary>
        public System.Decimal ListPrice { get; set;}
        /// <summary>Gets or sets the MakeFlag field. </summary>
        public System.Boolean MakeFlag { get; set;}
        /// <summary>Gets or sets the ModifiedDate field. </summary>
        public System.DateTime ModifiedDate { get; set;}
        /// <summary>Gets or sets the Name field. </summary>
        public System.String Name { get; set;}
        /// <summary>Gets or sets the ProductId field. </summary>
        public System.Int32 ProductId { get; set;}
        /// <summary>Gets or sets the ProductLine field. </summary>
        public System.String ProductLine { get; set;}
        /// <summary>Gets or sets the ProductNumber field. </summary>
        public System.String ProductNumber { get; set;}
        /// <summary>Gets or sets the ReorderPoint field. </summary>
        public System.Int16 ReorderPoint { get; set;}
        /// <summary>Gets or sets the Rowguid field. </summary>
        public System.Guid Rowguid { get; set;}
        /// <summary>Gets or sets the SafetyStockLevel field. </summary>
        public System.Int16 SafetyStockLevel { get; set;}
        /// <summary>Gets or sets the SellEndDate field. </summary>
        public Nullable<System.DateTime> SellEndDate { get; set;}
        /// <summary>Gets or sets the SellStartDate field. </summary>
        public System.DateTime SellStartDate { get; set;}
        /// <summary>Gets or sets the Size field. </summary>
        public System.String Size { get; set;}
        /// <summary>Gets or sets the StandardCost field. </summary>
        public System.Decimal StandardCost { get; set;}
        /// <summary>Gets or sets the Style field. </summary>
        public System.String Style { get; set;}
        /// <summary>Gets or sets the Weight field. </summary>
        public Nullable<System.Decimal> Weight { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'BillOfMaterial.Product - Product.BillOfMaterials (m:1)'</summary>
        public List<BillOfMaterial> BillOfMaterials { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'BillOfMaterial.Product_ - Product.BillOfMaterials_ (m:1)'</summary>
        public List<BillOfMaterial> BillOfMaterials_ { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'ProductCostHistory.Product - Product.ProductCostHistories (m:1)'</summary>
        public List<ProductCostHistory> ProductCostHistories { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'ProductDocument.Product - Product.ProductDocuments (m:1)'</summary>
        public List<ProductDocument> ProductDocuments { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'ProductInventory.Product - Product.ProductInventories (m:1)'</summary>
        public List<ProductInventory> ProductInventories { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'ProductListPriceHistory.Product - Product.ProductListPriceHistories (m:1)'</summary>
        public List<ProductListPriceHistory> ProductListPriceHistories { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'Product.ProductModel - ProductModel.Products (m:1)'</summary>
        public ProductModel ProductModel { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'ProductProductPhoto.Product - Product.ProductProductPhotos (m:1)'</summary>
        public List<ProductProductPhoto> ProductProductPhotos { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'ProductReview.Product - Product.ProductReviews (m:1)'</summary>
        public List<ProductReview> ProductReviews { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'Product.ProductSubcategory - ProductSubcategory.Products (m:1)'</summary>
        public ProductSubcategory ProductSubcategory { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'ProductVendor.Product - Product.ProductVendors (m:1)'</summary>
        public List<ProductVendor> ProductVendors { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'PurchaseOrderDetail.Product - Product.PurchaseOrderDetails (m:1)'</summary>
        public List<PurchaseOrderDetail> PurchaseOrderDetails { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'ShoppingCartItem.Product - Product.ShoppingCartItems (m:1)'</summary>
        public List<ShoppingCartItem> ShoppingCartItems { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'SpecialOfferProduct.Product - Product.SpecialOfferProducts (m:1)'</summary>
        public List<SpecialOfferProduct> SpecialOfferProducts { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'TransactionHistory.Product - Product.TransactionHistories (m:1)'</summary>
        public List<TransactionHistory> TransactionHistories { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'Product.UnitMeasure - UnitMeasure.Products (m:1)'</summary>
        public UnitMeasure UnitMeasure { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'Product.UnitMeasure_ - UnitMeasure.Products_ (m:1)'</summary>
        public UnitMeasure UnitMeasure_ { get; set;}
        /// <summary>Represents the navigator which is mapped onto the association 'WorkOrder.Product - Product.WorkOrders (m:1)'</summary>
        public List<WorkOrder> WorkOrders { get; set;}
    }
}

Mappings:

/// <summary>Defines the mapping information for the entity 'Product'</summary>
/// <param name="config">The configuration to modify.</param>
protected virtual void MapProduct(EntityTypeBuilder<Product> config)
{
    config.ToTable("Product", "Production");
    config.HasKey(t => t.ProductId);
    config.Property(t => t.ProductId).HasColumnName("ProductID").ValueGeneratedOnAdd();
    config.Property(t => t.Name).HasMaxLength(50).IsRequired();
    config.Property(t => t.ProductNumber).HasMaxLength(25).IsRequired();
    config.Property(t => t.MakeFlag);
    config.Property(t => t.FinishedGoodsFlag);
    config.Property(t => t.Color).HasMaxLength(15);
    config.Property(t => t.SafetyStockLevel);
    config.Property(t => t.ReorderPoint);
    config.Property(t => t.StandardCost);
    config.Property(t => t.ListPrice);
    config.Property(t => t.Size).HasMaxLength(5);
    config.Property(t => t.Weight);
    config.Property(t => t.DaysToManufacture);
    config.Property(t => t.ProductLine).HasMaxLength(2);
    config.Property(t => t.Class).HasMaxLength(2);
    config.Property(t => t.Style).HasMaxLength(2);
    config.Property(t => t.SellStartDate);
    config.Property(t => t.SellEndDate);
    config.Property(t => t.DiscontinuedDate);
    config.Property(t => t.Rowguid).HasColumnName("rowguid");
    config.Property(t => t.ModifiedDate);
    config.HasOne(t => t.ProductModel).WithMany(t => t.Products).HasForeignKey("ProductModelID").IsRequired();
    config.HasOne(t => t.ProductSubcategory).WithMany(t => t.Products).HasForeignKey("ProductSubcategoryID").IsRequired();
    config.HasOne(t => t.UnitMeasure).WithMany(t => t.Products).HasForeignKey("SizeUnitMeasureCode").IsRequired();
    config.HasOne(t => t.UnitMeasure_).WithMany(t => t.Products_).HasForeignKey("WeightUnitMeasureCode").IsRequired();
}

//...

/// <summary>Defines the mapping information for the entity 'UnitMeasure'</summary>
/// <param name="config">The configuration to modify.</param>
protected virtual void MapUnitMeasure(EntityTypeBuilder<UnitMeasure> config)
{
    config.ToTable("UnitMeasure", "Production");
    config.HasKey(t => t.UnitMeasureCode);
    config.Property(t => t.UnitMeasureCode).HasMaxLength(3);
    config.Property(t => t.Name).HasMaxLength(50).IsRequired();
    config.Property(t => t.ModifiedDate);
}

Gives error when running any query:

System.InvalidOperationException : The relationship from 'Product.UnitMeasure' to 'UnitMeasure.Products' with foreign key properties {'SizeUnitMeasureCode' : Nullable<int>} cannot target the primary key {'UnitMeasureCode' : string} because it is not compatible. Configure a principal key or a set of compatible foreign key properties for this relationship.
   at Microsoft.EntityFrameworkCore.Internal.ModelValidator.ShowError(String message)
   at Microsoft.EntityFrameworkCore.Internal.ModelValidator.EnsureNoShadowKeys(IModel model)
   at Microsoft.EntityFrameworkCore.Internal.ModelValidator.Validate(IModel model)
   at Microsoft.EntityFrameworkCore.Internal.RelationalModelValidator.Validate(IModel model)
   at Microsoft.EntityFrameworkCore.Infrastructure.ModelSource.CreateModel(DbContext context, IConventionSetBuilder conventionSetBuilder, IModelValidator validator)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.CreateModel()
   at Microsoft.EntityFrameworkCore.Internal.LazyRef`1.get_Value()
   at Microsoft.EntityFrameworkCore.Internal.DbContextServices.get_Model()
   at lambda_method(Closure , ServiceProvider )
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at lambda_method(Closure , ServiceProvider )
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider provider)
   at Microsoft.EntityFrameworkCore.Infrastructure.AccessorExtensions.GetService[TService](IInfrastructure`1 accessor)
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.<.ctor>b__2_0()
   at Microsoft.EntityFrameworkCore.Internal.LazyRef`1.get_Value()
   at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.System.Collections.Generic.IEnumerable<TEntity>.GetEnumerator()
   at AWTests.ReadTests.LoadSoHs2() in C:\Temp\generatortest\Test1\AWTests\ReadTests.cs:line 60

I have no idea how it thinks it's a Nullable<int>

@smitpatel
Copy link
Contributor

For config 1:
Are you calling MapSpecialOfferProduct before MapSalesOrderDetail?
For config 2:
Are you calling MapProduct before MapUnitMeasure?

@FransBouma
Copy link
Author

They're mapped alphabetical: https://gist.github.com/FransBouma/23ebe450d7e09dc1812fc2ff3c546b86

So MapSpecialOfferProduct is mapped after MapSalesOrderDetail and MapProduct is mapped before MapUnitMeasure.

It shouldn't matter btw, as there might not be an order in which they can be mapped in a single pass, nor should the user (if it's done by hand) worry about that as topological sorting isn't a human's strong point ;) (or a model might not be a DAG)

@FransBouma
Copy link
Author

(the file in the gist has FK fields, the order is the same for when no FK fields are present, the situation of this issue).

@smitpatel
Copy link
Contributor

@FransBouma - Order does matter in some cases. It is not about topological sorting which user has to worry about.
Take example of config 1 in which you are configuring composite FK using string overload. If you have not declared composite PK on principal end then it would have a temporary property as key. If you try to declared composite FK before declared that PK then EF really doesn't know from where to copy the types for the FK properties. It is just lack of data which cannot be bypassed.

For config1 if you are configuring composite PK before FK then it should work.
For config 2 it looks like EF is not correcting previously added FK after PK is reconfigured.

@FransBouma
Copy link
Author

@smitpatel

Order does matter in some cases. It is not about topological sorting which user has to worry about.
Take example of config 1 in which you are configuring composite FK using string overload. If you have not declared composite PK on principal end then it would have a temporary property as key. If you try to declared composite FK before declared that PK then EF really doesn't know from where to copy the types for the FK properties. It is just lack of data which cannot be bypassed.

No, sorry, this is silly. It's saying EF can't do a proper model check after everything is mapped and I have to do it for EF. How am I suppose to know what the order is in a dense model like adventureworks? Impossible without using a graph with a sort operation. But, I do have defined a composite PK on the principal end. It just hasn't been mapped yet. EF should take care of this, not me as a developer. Interestingly, if I generate the model with FK fields, everything is fine, even though the same applies: the PK hasn't been defined yet, so it can't verify whether the PK is compatible either!

So in short, the EF team is suggesting a user of your framework should manually do ordering of the mappings in such a way that the EF model verifier is satisfied because you guys can't implement a 2-pass system? How about this: you keep track of a list with lambdas which perform work for you to do after the modelbuilder has been completed. In the case like situation 1, it can't decide yet what to do, so it stores a lambda which will perform what to do at that point in the list. Then when the modelbuilder is run, you run the lambdas in the list till the list is empty. It's the same as deserializing a graph from e.g. xml or binary sources: sometimes you run into a reference that's not been deserialized yet. You can give up and say to the user they should serialize things in the 'right' order, but you can also solve it with a simple 2 pass system. If I can come up with things like this, so can a whole team at microsoft. (as my designer can load entity models and derived models without problems, in any order. It's not a hard problem, you know that).

For config1 if you are configuring composite PK before FK then it should work.

Maybe that's not possible (not a DAG, although it's rare). For a human to figure out the order this way is impossible. For 2 entities, it's fine. For 20 it's not. My designer can spit out the code in the 'right' order, but the reason why is, sorry, beyond comprehension.

For config 2 it looks like EF is not correcting previously added FK after PK is reconfigured.

How is the PK reconfigured? It's defined once.

@ajcvickers
Copy link
Contributor

@FransBouma Use Property calls to map your FK fields as shadow properties.

@FransBouma
Copy link
Author

@ajcvickers

Use Property calls to map your FK fields as shadow properties.

Ok, and then it will magically be solved? (as the properties are already known and have a type) ? I can do that. Will try tomorrow.

You guys seriously need to update the docs, it's completely unclear this is needed.

@rowanmiller
Copy link
Contributor

You guys seriously need to update the docs, it's completely unclear this is needed.

Agreed, I will do this. We have some improvements in 1.1 that may help in this scenario... but in general our guidance should be to define the shadow FK properties and then use them in HasForeignKey.

@AndriySvyryd
Copy link
Member

AndriySvyryd commented Oct 20, 2016

@FransBouma Using .Property you can specify the type for the shadow properties.
For the second issue you can call .HasPrincipalKey in the relationship configuration call before .HasForeignKey.

The first scenario should work in 1.1, and we can make the second scenario work in the future.

@FransBouma
Copy link
Author

Mapping the non-PK fk fields as shadow properties first makes it work indeed.

I'll leave it open as it's not documented at all.

@rowanmiller rowanmiller added this to the 1.2.0 milestone Oct 24, 2016
@rowanmiller
Copy link
Contributor

@AndriySvyryd assigning this to you to assess how much work we should do to enable this.

I will update the docs to recommend calling Property first to add the shadow property

@rowanmiller
Copy link
Contributor

Our docs are currently migrating to a new doc platform, so opened dotnet/EntityFramework.Docs#280 to update them once the migration is done.

AndriySvyryd added a commit that referenced this issue Nov 11, 2016
AndriySvyryd added a commit that referenced this issue Nov 18, 2016
@AndriySvyryd AndriySvyryd removed their assignment Nov 18, 2016
@AndriySvyryd AndriySvyryd added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Nov 18, 2016
@ajcvickers ajcvickers changed the title Specifying an FK field that's not in the model is impossible Model configuration: Allow using a shadow property in an FK before the property has been configured May 9, 2017
@ajcvickers ajcvickers changed the title Model configuration: Allow using a shadow property in an FK before the property has been configured Model configuration: A property that will be shadow and is used as part of an FK must be configured as a shadow property first May 9, 2017
@divega divega added closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. and removed closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. labels May 10, 2017
@ajcvickers ajcvickers modified the milestones: 2.0.0-preview1, 2.0.0 Oct 15, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. type-bug
Projects
None yet
Development

No branches or pull requests

6 participants