Castle Dynamic Proxy tutorial part VI: handling non-virtual methods

This is part six of my tuto­r­ial on Cas­tle Dynamic Proxy.

We talked about how Dynamic Proxy prox­y­ing mech­a­nism works, but so far we only briefly spoke about its lim­i­ta­tions. The main lim­i­ta­tion is the fact, that it can not inter­cept non-virtual calls. This may be an issue of vary­ing importance.

In case of libraries such as NHiber­nate, which uses Dynam­icProxy for lazy load­ing, when your mem­ber is not vir­tual, you can't lazy load it. This may be a draw­back for per­for­mance, but your pro­gram should be able to work nonethe­less.
On the other hand, for libraries like Rhino.Mocks, or MoQ, that use Dynam­icProxy for cre­at­ing mocks and stubs, not being able to inter­cept a call, means you can’t mock its behav­ior and the response. This is a crit­i­cal issue.
Sim­i­larly in case of our sam­ple Freez­able library, if a set­ter is not vir­tual, we can't inter­cept calls to it, and that means we can not war­rant that the state of the object will not change.

When we talked about Prox­y­Gen­er­a­tionHook, I men­tioned that it has a Non­Vir­tualMem­ber­Noti­fi­ca­tion method, that gets called when proxy gen­er­a­tor encoun­ters method it can not proxy.
In case of freez­able library, we took sim­plis­tic assump­tion that state of the object can only change via prop­erty set­ters. In this case if just any method we encounter is non-virtual we don't care. Since we assume that it can't change the state of our object any­way, we may as well let it be non-virtual and still deliver on our promise.
How­ever, if we encounter a non-virtual prop­erty set­ter, we should throw an excep­tion to indi­cate that we deny tak­ing any respon­si­bil­ity for this objects immutability.

Enough the­ory — let's write some tests to get a cleaner pic­ture of what we want to achieve.

[Fact]
public void Freezable_should_freeze_classes_with_nonVirtual_methods()
{
    var pet = Freezable.MakeFreezable<WithNonVirtualMethod>();
    pet.Name = "Rex";
    pet.NonVirtualMethod();
} 

[Fact]
public void Freezable_should_throw_when_trying_to_freeze_classes_with_nonVirtual_setters()
{
    var exception = Assert.Throws<InvalidOperationException>( () =>
        Freezable.MakeFreezable<WithNonVirtualSetter>() );
    Assert.Equal(
        "Property NonVirtualProperty is not virtual. Can't freeze classes with non-virtual properties.",
        exception.Message );
}

If we run the tests the first one will pass, the sec­ond one will fail. Is that a sur­prise? We don't have any check­ing code in place, so when we encounter a non vir­tual mem­ber, we do noth­ing. that's why we effec­tively ignored the Non­Vir­tual­Method in the first test (which is the behav­ior we want), but we also ignored the non-virtual prop­erty set­ter in the sec­ond test, which is a no-no.

To make them pass, we need to actu­ally imple­ment the Non­Vir­tualMem­ber­Noti­fi­ca­tion method, to throw an excep­tion when it encoun­ters a prop­erty set­ter. There's no spe­cific excep­tion type we're oblig­ated to throw, so we'll just throw Invali­d­Op­er­a­tionEx­cep­tion as spec­i­fied in our test.

Here's the implementation:

public void NonVirtualMemberNotification(Type type, MemberInfo memberInfo)
{
    var method = memberInfo as MethodInfo;
    if(method!=null)
    {
        this.ValidateNotSetter( method );
    }
} 

private void ValidateNotSetter(MethodInfo method)
{
    if( method.IsSpecialName && IsSetterName( method.Name ) )
        throw new InvalidOperationException(
            string.Format(
                "Property {0} is not virtual. Can't freeze classes with non-virtual properties.",
                method.Name.Substring( "set_".Length )
                )
            );
}

Sim­i­lar to Should­In­ter­cept­Method method Mar­shal­ByRe­fOb­ject and Object classes are spe­cial cases, and by default Dynam­icProxy will just ignore them.

With this change, all our tests now pass.

Other than the two meth­ods we already dis­cussed, IProx­y­Gen­er­a­tionHook has one more method: Meth­od­sIn­spected. It gets called, after all meth­ods on prox­ied type have been inspected, so it is the last method to be called by proxy gen­er­a­tor. It is use­ful in cases when you hold some pre­cious resources needed by two other meth­ods.
For exam­ple if our hook imple­men­ta­tion asked some exter­nal ser­vice (like a WCF ser­vice or data­base) about what to do with non-virtual meth­ods, Meth­od­sIn­spected would be the place to close the con­nec­tion and dis­pose of all resources that are no longer needed.

With this part we basi­cally cov­ered almost all of basics of Dynamic Proxy. In the next part we’ll dis­cuss other kinds of prox­ies you can cre­ate with Dynamic Proxy (yes, there are a few). Then we’ll talk about more advanced sce­nar­ios, like mixins.

I’m still open for feed­back, so if you feel I missed some impor­tant topic or should have expand on some­thing let me know in the comments.

The code, as always, is here.

Tech­no­rati Tags: , ,
  • javier

    Awe­some Tutorial.

  • Matt

    This is a great tuto­r­ial, thank you for putting it together.

    I have to admit, I'm a lit­tle dis­ap­pointed that Dynam­icProxy won't work with non-virtual meth­ods. And I believe I under­stand *why* — It appears the proxy works by gen­er­at­ing, at run­time, a new child class to the tar­get class. And this child class imple­ments an over­ride with call­backs to the Inter­cep­tors. And you can't over­ride a non-virtual method. You can hide it using the "new" key­word, but as we all know if you call the non vir­tual method "Foo" on a Child object you have to explic­itly be work­ing with said child object in a type safe man­ner oth­er­wise you'll exe­cute "Foo" against the par­ent type.

    Any­way, it a shame because this brings us *soooo* close to a com­pletely decou­pled solu­tion. As it stands, if I want to be Dynam­icProx­y­ing any of my classes, I have to mod­ify them with the vir­tual attribute. So the proxy is still affect­ing the code-time deci­sions being made in the classes. I'm sure there are plenty of use cases where a vir­tual method is what makes sense any­way, but we can also be sure there are plenty of use cases where vir­tual does not make sense.

    And, the worst part, there is no explicit dec­la­ra­tion of this cou­pling in the code. Unless the coder took the time to doc­u­ment every vir­tual method say­ing "This is vir­tual because we want to be able to do a Dynam­icProxy" then one would be left in the dark, com­pletely and utterly, when see­ing all these vir­tual meth­ods and absolutely no indi­ca­tion as to why they are virtual.

    Any­way, I'm just voic­ing some per­sonal dis­ap­point­ment in this lim­i­ta­tion enforced by the lan­guage. Regard­less, excel­lent tuto­r­ial. I'm not sure I would be using dynamic proxy *at all* with­out it.

  • Matt

    As a fol­low up — I imag­ine prox­y­ing of non-virtaul meth­ods is some­thing that will be pos­si­ble in .NET 4.0 with the sup­port for dynamic typing.

  • Matt

    Fol­low up to my fol­low up —

    Not sure about my sec­ond state­ment there. It wouldn't actu­ally solve the prob­lem. (think­ing out loud) The prob­lem is with the compile-time type check­ing, and the fact that the con­sumer of the prox­ied object doesn't (and shouldn't) know that it's being prox­ied. So, the con­sumer of the object expects a Pet object, and will treat it as a Pet object, instead of a Pet­Proxy. The addi­tion of dynamic typ­ing doesn't avoid this prob­lem — I imag­ine if a user calls Non­Vir­tual­Method() with the dynamic approach it will work, much like call­ing Non­Vir­tual­Method() works just fine if you had con­sumed it as a "var" instead of a "Pet". But you still have the prob­lem of peo­ple treat­ing it as a "Pet", will always invoke the par­ent non vir­tual method.

  • http://kozmic.pl/Default.aspx Krzysztof Koźmic

    Matt, strongly typed con­tract is still strongly typed con­tract. With dynamic you can add inter­cep­tion to any type (includ­ing sealed classes, and non-virtual meth­ods) by wrap­ping them (see Type Wrap­ping here: using.castleproject.org/…/Dynamic+Proxy+3+des…)

    You would how­ever pay the per­for­mance and com­piler check penalty due to weakly typed nature of such code.