开发者

How do I make attributes on a base class method apply in the inherited class?

开发者 https://www.devze.com 2023-03-23 02:23 出处:网络
Note: This question has been updated with new info. Please see the bottom half of this text. (The original quesiton is left here for context.)

Note: This question has been updated with new info. Please see the bottom half of this text. (The original quesiton is left here for context.)


Is there any way I can define my attribute so that if it's defined on a method that is overidden, the attribute is still applied?

The reason I ask is that I have an attribute which injects some behavior into the method, but the behavior is not applied when the method is called as in any of the cases in the child class, and I would like it to be.

class BaseClass
{
    [MyThing]
    virtual void SomeMethod()
    {
        // Do something fancy because of the attribute.
    }
}

class ChildClass
{
    override void SomeMethod()
    {
        // Fancy stuff does happen here too...
        base.SomeMethod();
    }

    void AnotherMethod()
    {
        // ...but not here. And I'd like it to =(
        base.SomeMethod();
    }
}

The attribute is defined like so:

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class MyThingAttribute : Attribute

The current code for finding methods with the attribute is the following:

var implementation = typeof(TheTypeWereCurrentlyInvestigating);
var allMethods = (from m in implementation.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy)
                  let attribs = (TransactionAttribute[]) m.GetCustomAttributes(typeof (TransactionAttribute), true)
                  where attribs.Length > 0
                  select Tuple.Create(m, attribs.Length > 0 ? attribs[0] : null)).ToList();

I didn't write that part, and I can't say I am 100% of what every part of it does... But we can assume, for now, that I am in control of all the involved code. (It's an opensource project, so I can at least create my own version, and submit a patch to the project owners...)

I have a couple of other cases too - basically I want to have this behavior injected whenever I call the method on the base class, no matter which way I got there - but if I solve this one I might get ideas on how to get the others working. If not, I'll get back with them.


UPDATE:

OK, so I sat down with the Castle.Transactions project and created some very simple tests to see what works and what doesn't. It turns out th开发者_高级运维at my original assumptions on what works and what doesn't were somewhat off.

What I did:

I created a test class which has one method, decorated with the attribute, and which calls an Assert method that verifies that the behavior was injected correctly (i.e. that there is a transaction). I then created a couple of classes which inherit this test class, to see in which cases everything works as I expect it to.

What I found:

By calling the test method directly on the test class and from various methods on the child classes, I discovered the following about what works and what doesn't:

Method called                 Access modifiers     Does it work?
*************                 ****************     *************
SomeMethod() on base class*   N/A                  Yes
OtherMethod() on child        neither              NO <-- headache!
OtherMethod() on child        hiding (new)         No
SomeMethod() on child         hiding (new)         No
OtherMethod() on child        overrides            No
OtherMethod() on child*       overrides            Yes
SomeMethod() on child         overrides            Yes

In all cases except the one marked with *, base.SomeMethod() was called from the method applied in the test. In the first case the same method was called but directly from the test, since no child class is involved. In the second case (of the ones marked *), the overriding method was called, i.e. this.SomeMethod(), so that's really equivalent of the last case. I'm not using any redundant qualifiers, so in that method the call is simply SomeMethod().

What I want:

It's the case marked "headache" that I really want to solve; how to inject behavior into the base class even though I'm calling it from my child class.

The reason I need that specific case to work is that I'm using this pattern in a repository, where the base class defines a Save(T entity) method decorated with the Transaction attribute. Currently, I have to override this method just to get the transaction orchestration, which makes it impossible to change the return type; on the base class it's void, but on my implementation I'd like to make it Error<T> instead. This is impossible when overriding, and since I can't solve the problem by naming the method differently, I'm at a loss.


If I were you I'd try and change your design. Calling base.SomeMethod() in AnotherMethod() when it has been overridden in AnotherMethod's class really smells.

Can't you factor out in a protected method the relevant part of BaseClass.SomeMethod(), place your attribute on this new method and call it in BaseClass.SomeMethod() and AnotherMethod(), assuming ChildClass.SomeMethod() would still call the method it overrides?


Can't go over it. Can't go under it. Gotta go around it.

Considering your circumstances as I understand them:

  • Can't apply the existing attribute to inject commit/rollback: It would never roll back because you're catching exceptions yourself in AnotherMethod().
  • You need the commit/rollback injection in AnotherMethod().

I suspect that TransactionAttribute is wrapping the method's body in a try-catch block, transforming this (pseudocode):

public void SomeMethod() {
    DoStuff();
}

Into something like this (pseudocode, and very simplified):

public void SomeMethod() {
    transaction.Begin();
    try {
        DoStuff();
        transaction.Commit();
    }
    catch {
        transaction.Rollback();
    }
}

With that in mind, you may be able to apply TransactionAttribute to AnotherMethod() and re-throw the exceptions you catch:

[TransactionAttribute]
public void AnotherMethod() {
    try {
        DoStuff();
    }
    catch (Exception ex) {
        //deal with exception
        throw;
    }
}

If that is not feasible -- such as if you only want part of the behavior that TransactionAttribute injects -- then you will likely have to make a new TransactionAttribute that injects the behavior you want it to inject. One possibility might be that it looks for try-catch blocks and places commits and rollbacks in the appropriate places, but that could be trickier than the current version.


Shot in the dark here, but...

I am assuming that the transaction behavior is injected by an IoC container, and that it does this by making a proxy when you resolve a ChildClass. Therefore the transaction code runs before\after ChildClass.SomeMethod via the proxy. I'm guessing that the behavior you're seeing is that there is no code injection on BaseClass.SomeMethod, so calling it from ChildClass.AnotherMethod does not involve any proxy code injection, it just goes straight through.

If this is the case, you could use a composition pattern and injection of a BaseClass to solve the problem.

If you resolved the following class via your container, it would inject a proxied BaseClass which had the appropriate before\after transaction code for BaseClass.SomeMethod method. You would therefore get your transaction behavior, plus your graceful exception handling.

You can play around with the usual OO mechanisms to sort out the issue of making AnotherChildClass interchangeable for a BaseClass, or use an interface, etc, etc.

public class AnotherChildClass
{
    private readonly BaseClass _bling;

    public AnotherChildClass(BaseClass bling)
    {
        _bling = bling;
    }

    public void AnotherMethod()
    {
        try
        {
            _bling.SomeMethod();
        }
        catch (Exception)
        {
            //Do nothing...
        }
    }
}

For example, a bit urgh, but you get the picture:

public class AnotherChildClass : BaseClass
{
    private readonly BaseClass _bling;

    public AnotherChildClass(BaseClass bling)
    {
        _bling = bling;
    }

    public override void SomeMethod()
    {
        _bling.SomeMethod();
    }

    public void AnotherMethod()
    {
        try
        {
            _bling.SomeMethod();
        }
        catch (Exception)
        {
            //Do nothing...
        }
    }
}

Update

I'm guessing that from your latest investigations the cases where you have used 'new' are not working as you are now blocking the generated IoC container proxy from overriding SomeMethod and therefore injecting the code. Try creating a derived class of your Child class, and try overriding the new SomeMethod method. This illustrates how the proxy is blocked.

    private class BaseClass
    {
        public virtual void SomeMethod(){}
    }

    private class ChildClass : BaseClass
    {
        public new void SomeMethod() //<- Declaring new method will block proxy
        {
            base.SomeMethod();
        }
    }

    private class ChildClassIocProxy : ChildClass
    {
        public override void SomeMethod() //<-- Not possible!
        {
            //Injected - before Tx
            base.SomeMethod();
            //Injected - after Tx
        }
    }
0

精彩评论

暂无评论...
验证码 换一张
取 消