efcore icon indicating copy to clipboard operation
efcore copied to clipboard

Option to turn off automatic setting of navigation properties

Open dgxhubbard opened this issue 7 years ago • 111 comments

From loading related data EF Core page is the following tip:

Entity Framework Core will automatically fix-up navigation properties to any other entities that were previously loaded into the context instance. So even if you don't explicitly include the data for a navigation property, the property may still be populated if some or all of the related entities were previously loaded.

I would like an option to turn this property off so that if I choose not to load a navigation property it is not loaded for me. The cost of transporting data that is not needed is slowing response time. If there are 70 gages and each gage has a status then that status has 70 gages. There is a reference looping option mentioned it the loading related data article which I did try but the status object still contained the list of gages which I verified through postman.

Below is an example showing the loading of data and clearing the navigation properties that are automatically "fixed up" by EF Core. I am going to need this code in all of my gets in order to drastically reduce the JSON payload size,.

When we use an Include we know the data that is needed for an object. "Fixing up" is something that is not needed. Please remove this feature or please give us a way to disable it.

Code to load data:

try
{
  using ( var context = new MyContext() )
  {
    res =
        ( from c in
            context.Gages.
                 Include ( "Status" )
             select c ).ToList ();

    // code to clear nav property loaded by EF Core
                res.ForEach ( 
                    g =>
                    {
                        if ( g != null && g.Status != null && g.Status.Gages != null )
                            g.Status.Gages.Clear ();
                    } );


  }

}
catch ( Exception ex )
{
	throw ex;
}

Entities

[Table("Gages", Schema = "MYSCHEMA" )]
class Gage
{
#region Properties

	[Key]
    [System.Runtime.Serialization.DataMember]
	
    public virtual int Gage_RID
    { get; set; }

	[Required]
    [StringLength(255)]
    [System.Runtime.Serialization.DataMember]
	
    public virtual string Gage_ID
    { get; set; }


    [System.Runtime.Serialization.DataMember]
	
    public virtual int Status_RID
    { get; set; }
	
	#endregion
	
    #region Navigation Properties
	
    [System.Runtime.Serialization.DataMember]
    public virtual Status Status
    { get; set; }
	
    #endregion
	
}

[Table("Status", Schema = "MYSCHEMA" )]
public class Status
{
    #region Properties

    [Key]
    [DatabaseGenerated ( DatabaseGeneratedOption.Identity )]
    [System.Runtime.Serialization.DataMember]
	
    public virtual int Status_RID
    { get; set; }
    
    
    [Required]
    [StringLength(255)]
    [System.Runtime.Serialization.DataMember]
	
    public virtual string StatusName
    { get; set; }

    #endregion
	
    #region Navigation Properties
	
    [System.Runtime.Serialization.DataMember]

    public virtual ICollection<Gage> Gages
    { get; set; }
	
    #endregion
}

public partial class MyContext : DbContext
{
    public virtual DbSet<Gage> Gages
    { get; set; }

    public virtual DbSet<Status> Status
    { get; set; }

}

dgxhubbard avatar Apr 05 '18 21:04 dgxhubbard

Or you could just use ViewModels...

smitpatel avatar Apr 05 '18 21:04 smitpatel

We do use view model in both wpf and angular. what would a view model do to reduce the size of the data that if being set by ef core

dgxhubbard avatar Apr 05 '18 22:04 dgxhubbard

In the wpf case you have references sitting around so they don't take up that much space. but sending the data over json increases the size drastically

dgxhubbard avatar Apr 05 '18 22:04 dgxhubbard

var res =
( from c in
context.Gages
select new GageModel {
    .... // Assign scalar properties
    Status = c.Status
} ).ToList();

No references will be fixed up.

smitpatel avatar Apr 05 '18 22:04 smitpatel

you referenced a view model rather that the getting of data. status is an object that is a nav property that has not been retrieved by ef so would this not set a null to status

dgxhubbard avatar Apr 05 '18 22:04 dgxhubbard

we have our scalar props, these would be retrieved by the select, but if status had not been queried by an include then it would be null.

dgxhubbard avatar Apr 05 '18 22:04 dgxhubbard

If you are referencing c.Status in your DTO then it will be populated by EF Core without needing any include.

smitpatel avatar Apr 05 '18 22:04 smitpatel

ok but wouldn't ef core still fix up the Gages nav property on Status, getting back to the same point?

dgxhubbard avatar Apr 05 '18 22:04 dgxhubbard

Where is the Gage being loaded in the query? Query is creating objects of GageModel & Status only.

smitpatel avatar Apr 05 '18 22:04 smitpatel

I just tried your suggestion and looked at the json in postman and it works. But I would still want to have the option to turn this off ""Fixing things up". Its not a desirable feature for us. Yes this is a legitimate work around, but in our case Gage and Status are the DTO. So the objects have been created by the EF query why recreate more objects?

dgxhubbard avatar Apr 05 '18 22:04 dgxhubbard

I have worked with EF prior to EF Core and maybe it is me being used to explicitly saying this is what I want using multiple includes and "." notation such as .Include( "mynav1.sub1" ).

dgxhubbard avatar Apr 05 '18 22:04 dgxhubbard

@dgxhubbard Few things to note here:

  • The part of the docs highlighted is not really relevant in this case. Fixup is not happening to entities that have already been queried and are being tracked. It is happening between the entities returned by the query. The same behavior will happen for no-tracking queries.
  • Only the entities requested by the query are being returned. EF is not creating any additional objects, only fixing up the references between the objects already returned by the query.
  • This is the same behavior that every version of EF from the first release in 2008 until now has had. There is nothing different here in the behavior of EF Core.
  • If the navigation property should never be populated, then consider removing it from the model. Unidirectional relationships with only one navigation property are perfectly valid.
  • If the navigation property is sometimes used and sometimes not, then a view model seems appropriate, as @smitpatel suggested.

All that being said, we will discuss this in triage and consider if there if a different mode for Include would be useful.

ajcvickers avatar Apr 07 '18 16:04 ajcvickers

Isn't there a way to ignore a property when serializing to json?

Tarig0 avatar Apr 10 '18 01:04 Tarig0

@Tarig0 Of course: see https://github.com/aspnet/Mvc/issues/4160

ajcvickers avatar Apr 10 '18 14:04 ajcvickers

I know in EF 6 that the properties that are not included are not loaded, and EF6 honors what should be and not be loaded. There is no request for loading the Gages property of Status, but the property is being loaded. There are cases when these properties will be loaded so turning it off by ignoring in json is not the answer.

When serializing to return to front end we want a list of gages with their status. Suppose there are a 100 gages returned to the front end, and each has a status, with the behavior or EF Core the size of the payload will be increased by 100 fold.

If include is honored this would be a great solution. I proposed a boolean to turn off the fixup if there if include code cannot be made to behave like EF 6.

The doc says:

Entity Framework Core will automatically fix-up navigation properties to any other entities that were previously loaded into the context instance. So even if you don't explicitly include the data for a navigation property, the property may still be populated if some or all of the related entities were previously loaded.

My interprtation of this is the first query, for gages, the list returned would be just the gages and their status (no Gages would be set on the status). However, on the second query and every query after, for gages, the Gages property will be "fixed up". So, except on the first query, the json payload in the 100 gages case will always be 100 times larger than the original request.

dgxhubbard avatar Apr 10 '18 17:04 dgxhubbard

@dgxhubbard EF6 and EF Core behave in the same way here. Also, you mention a first and a second query, but your code above only shows a single query.

ajcvickers avatar Apr 10 '18 17:04 ajcvickers

Code is part of a web api, so there will be multiple queries executed against it. On the first query the connection will store the gages for the next request. Then every request after it will Fixup the object gragh so that Gages of Status will be filled with items. I will double check my EF6 code.

dgxhubbard avatar Apr 10 '18 18:04 dgxhubbard

I am sorry you are right it does have this behavior. I would not have thought that because of serialization over the wire

I will go back to my original comment on that. In one case I have a WPF app that makes calls against the database, using EF6. In this case there are references to the gages set in the Gages propety and these are in memory, so the size increases but not as much as the serialization in json. Using json, the gages are sent out over the wire so they are not sent as a reference but as a gage and its properties, so the json payload will be roughly increased in size by the number of gages that are returned.

I added the code mentioned in the article

            .AddJsonOptions (
                options =>
                {
                    options.SerializerSettings.ReferenceLoopHandling = 
                            Newtonsoft.Json.ReferenceLoopHandling.Ignore;
                    options.SerializerSettings.ContractResolver = new DefaultContractResolver ();
                } );

and checked the result in postman, and each gage, in Gages property of Status, the properties of the gage are listed.

dgxhubbard avatar Apr 10 '18 18:04 dgxhubbard

Also in the mock code here I have only listed minimal properties there are more than properties on Gage and Status.

dgxhubbard avatar Apr 10 '18 18:04 dgxhubbard

@dgxhubbard In the code above, a new context is created for each query, so the context will not be tracking anything each time the query executes.

ajcvickers avatar Apr 10 '18 19:04 ajcvickers

Unless you're setting the context as a singleton.

Tarig0 avatar Apr 10 '18 19:04 Tarig0

@Tarig0 The code above uses "new".

ajcvickers avatar Apr 10 '18 19:04 ajcvickers

Ah yes lost the context

Tarig0 avatar Apr 10 '18 19:04 Tarig0

Tracking anything? I don't understand. A new context is created, but doesn't the context use results that have already been cached? If that is where you are going

dgxhubbard avatar Apr 10 '18 19:04 dgxhubbard

I do know the Gages property on Status is filled in on the first query

dgxhubbard avatar Apr 10 '18 20:04 dgxhubbard

@dgxhubbard Nope.

ajcvickers avatar Apr 10 '18 20:04 ajcvickers

My test with the EF 6 code and EF core both showed the same thing. The Gages property is set. This test was start the web api project and hitting with one request from postman. There were no other requests. If there is a problem with what I am doing or interpreting the result please correct me. Sorry this uses full code and there is json in the txt file.

Gages.txt

dgxhubbard avatar Apr 10 '18 20:04 dgxhubbard

@dgxhubbard context.Gages.Include("Status") means for every Gage, also load the Status relationship. The relationship consists of one foreign key property and two navigation properties--Gage.Status and its inverse Status.Gages. When loading the relationship both navigation properties are set.

ajcvickers avatar Apr 10 '18 20:04 ajcvickers

We are running EF Core 2.1 and according to the doc for lazy loading if you call UseLazyLoadingProxies, which we do, then any virtual nav property will be lazy loaded. If I am wrong please correct me. My example above is pseudo and leaves out the important face that we are calling UseLazyLoadingProxies. I apologize for that.

dgxhubbard avatar Apr 10 '18 20:04 dgxhubbard

Here is our factory method to create a context.

public static MyContext Create ( bool useLogger = false )
        {

            var optionsBuilder = new DbContextOptionsBuilder<GtContext> ();
            if ( optionsBuilder == null )
                throw new NullReferenceException ( "Failed to create db context options builder" );


           optionsBuilder.UseSqlServer ( MsSqlConnectionString );
            optionsBuilder.UseLazyLoadingProxies ();

            if ( useLogger )
                optionsBuilder.UseLoggerFactory ( DbContextLoggingExtensions.LoggerFactory );


            var context = new MyContext ( optionsBuilder.Options );
            if ( context == null )
                throw new NullReferenceException ( "Failed to create context" );

            // disable change tracking
            context.ChangeTracker.AutoDetectChangesEnabled = false;
            context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;

            return context;
        }

dgxhubbard avatar Apr 10 '18 20:04 dgxhubbard