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

Expose Castle.Core ProxyGenerationOptions for customization #27073

Open
Tracked by #22954
cabal95 opened this issue Dec 29, 2021 · 0 comments
Open
Tracked by #22954

Expose Castle.Core ProxyGenerationOptions for customization #27073

cabal95 opened this issue Dec 29, 2021 · 0 comments
Assignees
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

Comments

@cabal95
Copy link

cabal95 commented Dec 29, 2021

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.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
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
Projects
None yet
Development

No branches or pull requests

3 participants