Expose Castle.Core ProxyGenerationOptions for customization
- Dominant language
- C#
- Stars
- 14.8k
- Forks
- 3.4k
- PR merge metrics
- PR metrics pending
Description
### 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:
| w/o Lazy Loading | w/ Lazy Load and default proxy factory | w/ Lazy Load and custom proxy factory |
| --- | --- | --- |
| 9,031ms | 137,960ms | 17,972ms |
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.
```cs
optionsBuilder.UseLazyLoadingProxies( o => {
o.ProxyGenerationOptions = new ProxyGenerationOptions( new CustomGenerationHook() );
} );
```
### Notes
* This method that I use would not work if you are using change tracking proxies, since all properties would need to be proxied. However, it would probably still provide a performance boost since we could at least filter out any virtual methods that do not begin with "get_" or "set_".
* As such, it would be up to the person writing their own ProxyGenerationHook to ensure that the right methods are still proxied. They can't just say "don't proxy anything" and expect everything to magically work.
Related to issues: #20135, #24902
Contributor guide
Assessment
This issue has not been assessed yet.