Description
(See also #32520.)
API proposal
namespace System
{
public static class Activator
{
// EXISTING APIs (abridged):
public static T CreateInstance<T>();
public static object? CreateInstance(Type type);
public static object? CreateInstance(Type type, bool nonPublic);
// NEW PROPOSED APIs:
public static Func<T> CreateFactory<T>(); // no new() constraint, matches existing API
public static Func<object?> CreateFactory(Type type);
public static Func<object?> CreateFactory(Type type, bool nonPublic);
}
}
// STRETCH GOAL APIs (see end of proposal):
namespace System.Reflection
{
public class ConstructorInfo
{
public static TDelegate CreateDelegate<TDelegate>();
public static Delegate CreateDelegate(Type delegateType);
}
}
// REMOVED FROM PROPOSAL
/*
namespace System.Runtime.CompilerServices
{
public static class RuntimeHelpers
{
// EXISTING API:
public static object GetUninitializedObject(Type type);
// NEW PROPOSED API:
public static Func<object> GetUninitializedObjectFactory(Type type);
}
}
*/
Discussion
Instantiating objects whose types are not known until runtime is fairly common practice among frameworks and libraries. Deserializers perform this task frequently. Web frameworks and DI frameworks might create per-request instances of objects, then destroy those objects at the end of the request.
APIs like Activator.CreateInstance
and the System.Reflection surface area can help with this to a large extent. However, those are sometimes seen as heavyweight solutions. We've built some caching mechanisms into the framework to suppose these use cases. But it's still fairly common for high-performance frameworks to bypass Activator
and the reflection stack and to go straight to manual codegen. See below for some examples.
See below for some concrete samples.
System.Text.Json
ASP.NET Core
dotnet/aspnetcore#14615 (though they're using TypeBuilder
to work around this right now)
Other runtime + libraries
Ref emit incurs a substantial upfront perf hit, but it does generally provide a win amortized over the lifetime of the cached method as it's invoked over and over again. However, this comes with its own set of problems. It's difficult for developers to get the exact IL correct across the myriad edge cases that might exist. It's not very memory-efficient. And as runtimes that don't allow codegen become more commonplace, it complicates the callers' code to have to decide the best course of action to take for any given runtime.
These proposed APIs attempt to solve the problem of creating a basic object factory using the best mechanism applicable to the current runtime. The exact mechanism used can vary based on runtime: perhaps it's codegen, perhaps it's reflection, perhaps it's something else. But the idea is that the performance of these APIs should rival the best hand-rolled implementations that library authors can create.
Shortcomings, not solved here
This API is not a panacea to address all performance concerns developers have with the reflection stack. For example, this won't change the perf characteristics of MethodInfo.Invoke
. But it could be used to speed up the existing Activator.CreateInstance<T>
APIs and to make other targeted improvements. In general, this API provides an alternative pattern that developers can use so that they don't have to roll solutions themselves.
It also does not fully address the concern of calls to parameterized ctors, such as you might find in DI systems. The API RuntimeHelpers.GetUninitializedObjectFactory
does help with this to some extent. The caller can cache that factory to instantiate "blank" objects quickly, then use whatever existing fast mechanism they wish to call the object's real ctor over the newly allocated instance.
I hope to have a better solution for this specific scenario in a future issue.
Stretch goal APIs
The APIs ConstructorInfo.CreateDelegate<TDelegate>()
and friends are meant to allow invoking parameterless or parameterful ctors. These are primarily useful when the objects you're constructing have a common signature in their constructors, but you don't know the actual type of the object at compile time.
Consider:
public class MyService
{
public MyService(IServiceProvider serviceProvider) { }
}
public class MyOtherService
{
public MyOtherService(IServiceProvider serviceProvider) { }
}
In these cases, the caller would get the ConstructorInfo
they care about, then call constructorInfo.CreateDelegate<Func<IServiceProvider, object>>()
. Depending on which ConstructorInfo
was provided, the returned delegate will create either a MyService
or a MyOtherService
, calling the appropriate ctor with the caller-provided IServiceProvider
.
I say "stretch goal" because the parameter shuffling involved here would involve spinning up the JIT. We can keep this overhead to a minimum because within the runtime we can take shortcuts that non-runtime components can't take with the typical DynamicMethod
-based way of doing things. So while there's less JIT cost compared to a standard DynamicMethod
-based solution, there's still some JIT cost, so it doesn't fully eliminate startup overhead. (This overhead isn't much different than the overhead the runtime already incurs when it sees code like Func<string, bool> func = string.IsNullOrEmpty
;, since the runtime already needs to create a parameter shuffling thunk for this scenario.)