Open DaleStan opened 4 years ago
@DaleStan why not just create derived class which provides the needed arg? Alternatively take the argument through the property instead?
@krwq I don't think I understand your question about creating a derived class.
Today, when I use DispatchProxy.Create
, I have to create a class derived from DispatchProxy
, and that class has to have a public parameterless constructor. DispatchProxy.Create
will call that constructor, no matter what else I do in the derived class.
If I create a class derived from the dynamically-generated type that DispatchProxy.Create
returns, that removes the benefit of using DispatchProxy
, plus the generated type's constructor takes an Action<object>
that isn't (and probably shouldn't be) documented.
I do know I don't want to use a property, because then I can't store the property value in a readonly field, nor can I use an auto-implemented get-only or get/init property.
I meant to create derived class per usage, i.e.:
class FooProxy : Proxy
{
public FooProxy() : base("foo") {}
}
In production code, I'm passing the result of a SQL query, instead of a compile-time constant. I can do something like this:
public class Proxy : DispatchProxy
{
private class ProxyHelper : Proxy
{
[ThreadStatic]
internal static new IDataReader _target;
public ProxyHelper() : base(_target) { }
}
private readonly IDataReader _target;
private Proxy(IDataReader target) => _target = target;
// Call to get a proxy for an IDataReader
public static Proxy Create(IDataReader target)
{
ProxyHelper._target = target;
return (Proxy)Create<IDataReader, ProxyHelper>();
}
// ...
}
This is a little better than the code I'm using currently (the Proxy
constructor is no longer public) but I was hoping to get rid of that [ThreadStatic]
field entirely.
It is possible to wrap the DispatchProxy.Create()
with your own static "Create()" method and set state on your proxy after it's created. Below is a sample that does this where the state is just the proxy's target instance. It is a general-use proxy (can work against any Type).
It is possible to wrap the
DispatchProxy.Create()
with your own static "Create()" method
That is one of the "No API change" options I mentioned. For the sake of comparison, my proposed API would let the MyProxy
class look like this instead:
MyProxy
class after proposed API changeNote that the similar issue https://github.com/dotnet/runtime/issues/16614 was closed because it overlaps with this issue -- both need an alternative way to create a proxy in order to easily pass or set parameters.
Background and Motivation
In my use of
System.Reflection.DispatchProxy
, I often find that I want to add readonly fields or properties to my proxy implementation. The clean way to do this is to pass the desired values to the constructor, for exampleI cannot do this since DispatchProxy currently requires a public default constructor. I would like to relax this requirement to either "exactly one public constructor" or "at least one public constructor".
In this proposal, I have settled on "at least one public constructor with at most one parameter"; see the Alternative Designs section for more thoughts.
Proposed API
Usage Examples
One current use I have is interception of calls to
IDataReader
, where I want to allow arbitrary SQL queries (executed locally, on a SQLite database), but I also want to present the data in a useful fashion. The timestamps stored the table are milliseconds-since-midnight, which is useless for matching with "it happened about 8:25 PM" or for determining the time between events more than a few seconds apart, so I want to automatically convert them to hh:mm:ss.fff. With this new API, my interception class would be this:If there are multiple constructors,
CreateWithParameter
would use the specialized type ofTConstructorParam
to select the one-parameter constructor to invoke.Alternative Designs
TProxy.Create
method. Ugly, especially for read-only auto-properties, but the best current option in a technical sense.[ThreadStatic]
fields to pass the parameters from theTProxy.Create
method to the default constructor. Not quite as ugly, but a pit of failure. If you forget the[ThreadStatic]
attribute, multi-threaded use could end up with splinched proxies (if there are multiple readonly fields) or two proxies with the same data.TProxy.Create
method. The most likely solution a developer would select, but this requires removing the readonly field modifier, and making any read-only auto-properties writable.Create
instead ofCreateWithParameter
This is a breaking change for anyone who currently callstypeof(DispatchProxy).GetMethod("Create")
.CreateWithParameter<T, TProxy>(object constructorParameter)
This follows the pattern set byThread.Start
, and removes the possibly-irritating third generic parameter, but limits theTProxy
type to one single-parameter constructor.CreateWithParameter<T, TProxy>(params object[] constructorParameters)
This follows the pattern ofMethodInfo.Invoke
, butMethodInfo.Invoke
already knows what method it is invoking, whileCreateWithParameter
would have to do overload resolution. TheTConstructorParameter
generic parameter can almost always be intuited by the compiler, but it cannot necessarily be intuited at runtime. If I try to callCreateWithParameter<Interface, Proxy>(null);
and there are multiple single-parameter constructors, there's no way to specify which type of null I passed.CreateWithParameter<T, TProxy>(object[] constructorParameters, Type[] constructorParameterTypes)
Using aType[]
follows the pattern ofType.GetConstructor
, and solves the previous issue, but is starting us down the path of passing six parameters, to match the five-parameter version ofGetConstructor
. This is one of the best alternative designs, especially if overload resolution is internally handled byType.GetConstructor
, but I'm reluctant to request an API where I'd always pass null for one or more parameters.CreateWithParameter<T, TProxy>(object[] constructorParameters, ConstructorInfo constructor = null)
This is a variation on the previous, where the caller is responsible for callingType.GetConstructor
, unless the constructor to be called is 'obvious'. 'Obvious' can be defined in several ways, including "it's never obvious", "the single public constructor", "the single public constructor that takes the specified number of parameters", and "the constructor that would be selected by the overload resolution in the theparams object[]
option".CreateWithParameters<T, TProxy, TConstructorParameter1, TConstructorParameter2>(TConstrucorParameter1 constructorParameter1, TConstructorParameter2 constructorParameter2)
, and so on. This follows the pattern ofAction
andFunc
, but doesn't buy any additional behavior; if the types of the parameters are known at compile-time, a struct, record, or tuple can be created to pack all parameters into one object for passage throughCreateWithParameters
. It may also increase the effort required to do overload resolution.CreateWithParameter
, instead of placing it inDispatchProxy
. This alleviates the remaining breaking change in the current proposal and previous alternatives. In this case I would also rename the method back toCreate
.DispatchProxyCreator<T, TProxy>.Create<TConstructorParameter>(TConstructorParameter constructorParameter)
This allowsTConstructorParameter
to be intuited by the compiler, but, as mentioned, violates FxCop's guidelines.Risks
The proposed addition is still a breaking change for anyone who calls
typeof(DispatchProxy).GetMethods().Single()
. I don't know what sorts of Reflection breaks are acceptable, but I seem to have decided that it is acceptable to assume "This class has only one method namedCreate
; there will never be another one," but not acceptable to assume "This class has only one public method; there will never be another one."The proposed addition requires (1a) additional time and memory when creating the additional constructors in
Create
, or (1b) additional memory for the creation of a second proxy type if the first was created using the existing default-constructor-onlyCreate
. In most cases, it also requires (2) additional time when creating a proxy object withCreateWithParameters
, for runtime overload resolution. 1a is a regression for existing programs, while 1b is poor behavior for new programs that use bothCreate
andCreateWithParameters
. I expect the regression to be minimal, and would tend to choose 1b over 1a, but I don't know how to measure this. 2 is a general note thatCreateWithParameters
has to do more work per object thanCreate
, and will be slower.