castleproject / Core

Castle Core, including Castle DynamicProxy, Logging Services and DictionaryAdapter
http://www.castleproject.org/
Other
2.2k stars 467 forks source link

Support "with" for record proxies #671

Open Evengard opened 10 months ago

Evengard commented 10 months ago

Is it possible to have support for the with syntax for proxies?

I want to "enrich" my existing record with a mixin implementing a custom property, and make it persist across modifications of that record.

Unfortunately, it seems that when I use the with keyword, the proxy is gone and I end up with a completely new object. At the same time, if I do the same thing manually, aka

Example 1 ```csharp public record Base(int Value); public interface IMixin { string? MixedString { get; } } public record Inherited(int Value) : Base(Value), IMixin { public string? MixedString { get; init; } } Base rec = new Inherited(1) { MixedString = "Hello" }; Console.WriteLine(rec); var updated = rec with { Value = 2 }; Console.WriteLine(updated); ```

Or even:

Example 2 ```csharp public record Base(int Value); public interface IMixin { string? MixedString { get; } } public record Inherited(int Value) : Base(Value), IMixin { string IMixin.MixedString => "Hello"; } Base rec = new Inherited(1); Console.WriteLine(rec); Console.WriteLine((rec as IMixin).MixedString); var updated = rec with { Value = 2 }; Console.WriteLine(updated); Console.WriteLine((updated as IMixin).MixedString); ```

The mixed in property is preserved when using with keyword, because the actual underlying class is preserved when using the with keyword.

Unfortunately, it seems not to be the case for proxies.

Is there a way to achieve something similar to that but completely in runtime with DynamicProxy?

stakx commented 10 months ago

AFAIK, C# 9's r with { P1 = x1, P2 = x2, ... } under the hood is implemented as a call to r.<Clone>$(), which is supposed to return a copy rc of r (by means of calling its type's copy constructor), and then executing assignments rc.P1 = x1, rc.P2 = x2, ...

Which means that your proxy interceptor needs to intercept these calls. When intercepting <Clone>$, you'd have to create a new proxy of the same object type using your existing ProxyGenerator, and then initialize that new proxy so it's semantically going to appear like a copy of the original one. You may also want to intercept the assignments in a meaningful way.

This will only work if the C# compiler chooses to mark the <Clone>$ method as virtual. The specification for with does not require that to be the case (possibly to support record structs) but normally the Roslyn compiler does appear to make that method virtual.

Note also that the <Clone>$ method name is an implementation detail of the Roslyn compiler, relying on it therefore carries a (perhaps mostly hypothetical, but negligible in practice) risk of brittleness.

Evengard commented 10 months ago

I actually attempted going this way. The problem I got is that I didn't manage to transfer the mixin from the old object to the new without recreating it from scratch. There's no methods to retrieve the mixins like there's a way to retrieve a target. And when attempting to attach the proxied mixin I just got an exception.

stakx commented 10 months ago

Could you add a mixin to your proxies that returns the mixins that you added to them?

Evengard commented 10 months ago

That's one hell of a workaround =) That would probably work, although that's really hacky...