Expose Castle.Core ProxyGenerationOptions for customization #27073
Labels
area-change-tracking
area-perf
area-proxies
consider-for-current-release
customer-reported
punted-for-7.0
Originally planned for the EF Core 7.0 (EF7) release, but moved out due to resource constraints.
type-enhancement
Milestone
Summary
EF6 Classic had extremely fast lazy loading proxy creation. Not sure why, maybe because it was able to take advantage of windows-only APIs that can't be used in .NET core. Whatever the reason, creating proxies in .NET core is extremely slow by comparison.
I am proposing that ProxyGenerationOptions from Castle.Core be made available to override by user code. This could provide serious performance improvements when using lazy loading if the proxied types have additional virtual properties or methods that should not be proxied.
Our Need for Lazy Loading
Our project makes heavy use of lazy loading to provide data access to a Liquid template engine. Since the user is in charge of the template, we don't know what navigation properties are going to be used so we use lazy loading to only load the ones accessed by the user. Sometimes this is none, sometimes it's a few dozen, so eager loading isn't really a good option.
As we are slowly porting this project to EF Core and .NET Core/5, we have been struggling with the extremely slow model build time when lazy loading is enabled. We currently have around 250 models, each with dozens of virtual properties and methods. Nearly all of these inherit from a base model to implement common functionality (which is why many methods are virtual). We probably shouldn't have implemented the methods on the models themselves, but water under the bridge, we are stuck with that.
Timings
Currently, loading the model results in the following timings. These timings were gathered by creating a scope, a database context and querying for a single entity. This causes the model to be built which in turn causes all the proxy types to be created. I used a C# Stopwatch instance wrapped around this small piece of code so it does not include any other application startup time:
As you can see, the cost of creating the proxy types is over a 10x startup delay. If this was only an issue while in production we could probably live with it. However, a 2 minute delay while debugging when you have to stop and start constantly is a major killer to productivity.
Proposed Solution
Mentioned above is a "custom proxy factory". Castle.Core takes a ProxyGenerationOptions object that describes a bit of the "how" a proxy is generated. One of those options is the ProxyGenerationHook that allows the caller to determine which methods (and by extension, properties) should be proxied. The default options uses a default hook that allows all methods.
I created a custom IProxyFactory in order to get access to the ProxyGenerationOptions so we could use a custom ProxyGenerationHook. Because that interface is internal it isn't a solution we want to employ. However, for testing it let us supply a custom ProxyGenerationHook. This hook limits the methods to only those that are navigation properties.
As you can see in the table above, the custom proxy factory dropped our lazy load initialization time from 137,960ms to 17,972ms because we were able to filter out hundreds (thousands?) of methods that didn't need to be proxied for lazy loading to work.
Implementation Details
I suggest that the .UseLazyLoadingProxies() method be updated to accept a configuration action. This action could be used to set the ProxyGenerationOptions that will be passed to Castle.Core - including the ProxyGenerationHook. The default value would simply be what is currently used.
This value would then be stored in the ProxiesOptionsExtension so that it can be accessed from inside the IProxyFactory implementation and passed on to Castle.Core.
Notes
Related to issues: #20135, #24902
The text was updated successfully, but these errors were encountered: