С#: как создать атрибут метода, запускающего событие при его вызове?

Есть ли способ в С# или .NET в целом создать атрибут для метода, который запускает событие при вызове метода? В идеале я мог бы запускать пользовательские действия до и после вызова метода.

Я имею в виду что-то вроде этого:

[TriggersMyCustomAction()]
public void DoSomeStuff()
{
}

Я совершенно не знаю, как это сделать и возможно ли это вообще, но System.Diagnostic.ConditionalAttribute может делать то же самое в фоновом режиме. Хотя я не уверен.

EDIT: я забыл упомянуть, что из-за обстоятельств моего конкретного случая производительность на самом деле не является проблемой.


person Tamas Czinege    schedule 22.10.2008    source источник
comment
Краткий ответ: да в сопровождении: я не знаю как. Что вы хотите сделать, так это манипулировать IL во время компиляции, чтобы ввести обратный вызов в первой и последней строках вашего метода. Сборка MS.VB имеет атрибут, который выполняет некоторые манипуляции с IL (чтобы сделать класс нестатическим синглтоном).   -  person cfeduke    schedule 22.10.2008
comment
@Tamas: Я знаю, что твой вопрос был задан давно, но я нашел ответ. Надеюсь, это поможет вам. :-)   -  person Matt    schedule 30.01.2014


Ответы (7)


Я знаю только один способ сделать это с помощью PostSharp. Он обрабатывает ваш IL и может делать то, что вы просили.

person OwenP    schedule 22.10.2008
comment
Я проголосовал за этот ответ, потому что это и моя мысль: вам понадобится какой-то инструмент для запуска после сборки, чтобы проанализировать ваш IL, найти методы с вашим атрибутом и внедрить некоторую логику событий. - person Judah Gabriel Himango; 22.10.2008
comment
Я только что узнал о PostSharp и отправился на поиски этого вопроса, чтобы опубликовать его в качестве ответа. - person cfeduke; 24.10.2008

Эта концепция используется в веб-приложениях MVC.

.NET Framework 4.x предоставляет несколько атрибутов, запускающих действия, например: ExceptionFilterAttribute (обработка исключений), AuthorizeAttribute (обработка авторизации). Оба определены в System.Web.Http.Filters.

Например, вы можете определить свой собственный атрибут авторизации следующим образом:

public class myAuthorizationAttribute : AuthorizeAttribute
{
    protected override bool IsAuthorized(HttpActionContext actionContext)
    {
        // do any stuff here
        // it will be invoked when the decorated method is called
        if (CheckAuthorization(actionContext)) 
           return true; // authorized
        else
           return false; // not authorized
    }

}

Затем в своем классе controller вы декорируете методы, которые должны использовать вашу авторизацию, следующим образом:

[myAuthorization]
public HttpResponseMessage Post(string id)
{
    // ... your code goes here
    response = new HttpResponseMessage(HttpStatusCode.OK); // return OK status
    return response;
}

Всякий раз, когда вызывается метод Post, он вызывает метод IsAuthorized внутри атрибута myAuthorization до выполнения кода внутри метода Post.

Если вы возвращаете false в методе IsAuthorized, вы сигнализируете, что авторизация не предоставлена ​​и выполнение метода Post прерывается.


Чтобы понять, как это работает, давайте рассмотрим другой пример: ExceptionFilter, который позволяет фильтровать исключения с помощью атрибутов, используется так же, как показано выше для AuthorizeAttribute (вы можете найти более подробное описание о его использование здесь) .

Чтобы использовать его, создайте класс DivideByZeroExceptionFilter из класса ExceptionFilterAttribute, как показано здесь и переопределите метод OnException:

public class DivideByZeroExceptionFilter : ExceptionFilterAttribute
{
    public override void OnException(HttpActionExecutedContext actionExecutedContext)
    {
        if (actionExecutedContext.Exception is DivideByZeroException)
        {
            actionExecutedContext.Response = new HttpResponseMessage() { 
                Content = new StringContent("A DIV error occured within the application.",
                                System.Text.Encoding.UTF8, "text/plain"), 
                StatusCode = System.Net.HttpStatusCode.InternalServerError
                };
        }
    }
}

Затем используйте следующий демонстрационный код, чтобы активировать его:

[DivideByZeroExceptionFilter]
public void Delete(int id)
{
    // Just for demonstration purpose, it
    // causes the DivideByZeroExceptionFilter attribute to be triggered:
    throw new DivideByZeroException(); 

    // (normally, you would have some code here that might throw 
    // this exception if something goes wrong, and you want to make
    // sure it aborts properly in this case)
}

Теперь, когда мы знаем, как это используется, нас в основном интересует реализация. Следующий код взят из .NET Framework. Он использует интерфейс IExceptionFilter внутри как контракт:

namespace System.Web.Http.Filters
{
    public interface IExceptionFilter : IFilter
    {
        // Executes an asynchronous exception filter.
        // Returns: An asynchronous exception filter.
        Task ExecuteExceptionFilterAsync(
                    HttpActionExecutedContext actionExecutedContext, 
                    CancellationToken cancellationToken);
    }
}

Сам ExceptionFilterAttribute определяется следующим образом:

namespace System.Web.Http.Filters
{
    // Represents the attributes for the exception filter.
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, 
            Inherited = true, AllowMultiple = true)]
    public abstract class ExceptionFilterAttribute : FilterAttribute, 
            IExceptionFilter, IFilter
    {
        // Raises the exception event.
        // actionExecutedContext: The context for the action.
        public virtual void OnException(
            HttpActionExecutedContext actionExecutedContext)
        {
        }
        // Asynchronously executes the exception filter.
        // Returns: The result of the execution.
        Task IExceptionFilter.ExecuteExceptionFilterAsync(
            HttpActionExecutedContext actionExecutedContext, 
            CancellationToken cancellationToken)
        {
            if (actionExecutedContext == null)
            {
                throw Error.ArgumentNull("actionExecutedContext");
            }
            this.OnException(actionExecutedContext);
            return TaskHelpers.Completed();
        }
    }
}

Внутри ExecuteExceptionFilterAsync вызывается метод OnException. Поскольку вы переопределили его, как показано ранее, теперь ошибка может быть обработана вашим собственным кодом.


Существует также коммерческий продукт, как упоминалось в ответе OwenP, PostSharp, который позволяет вам сделать это легко. Вот пример того, как вы можете сделать это с помощью PostSharp. Обратите внимание, что доступна версия Express, которую вы можете использовать бесплатно даже для коммерческих проектов.

Пример PostSharp (полное описание см. по ссылке выше):

public class CustomerService
{
    [RetryOnException(MaxRetries = 5)]
    public void Save(Customer customer)
    {
        // Database or web-service call.
    }
}

Здесь атрибут указывает, что метод Save вызывается до 5 раз при возникновении исключения. Следующий код определяет этот настраиваемый атрибут:

[PSerializable]
public class RetryOnExceptionAttribute : MethodInterceptionAspect
{
    public RetryOnExceptionAttribute()
    {
        this.MaxRetries = 3;
    }

    public int MaxRetries { get; set; }

    public override void OnInvoke(MethodInterceptionArgs args)
    {
        int retriesCounter = 0;

        while (true)
        {
            try
            {
                args.Proceed();
                return;
            }
            catch (Exception e)
            {
                retriesCounter++;
                if (retriesCounter > this.MaxRetries) throw;

                Console.WriteLine(
                  "Exception during attempt {0} of calling method {1}.{2}: {3}",
                  retriesCounter, args.Method.DeclaringType, args.Method.Name, e.Message);
            }
        }
    }
}
person Matt    schedule 30.01.2014

Вам нужен какой-то аспектно-ориентированный фреймворк. PostSharp сделает это, как и Windsor.

По сути, они подклассируют ваш объект и переопределяют этот метод...

тогда становится:

//proxy
public override void DoSomeStuff()
{
     if(MethodHasTriggerAttribute)
        Trigger();

     _innerClass.DoSomeStuff();
}

конечно все это скрыто от вас. Все, что вам нужно сделать, это запросить у Windsor тип, и он сделает за вас проксирование. Атрибут становится (настраиваемым) средством, я думаю, в Виндзоре.

person Ben Scheirman    schedule 22.10.2008

Вы можете использовать ContextBoundObject и IMessageSink. См. http://msdn.microsoft.com/nb-no/magazine/cc301356(en-us).aspx

Имейте в виду, что этот подход оказывает серьезное влияние на производительность по сравнению с прямым вызовом метода.

person Hallgrim    schedule 22.10.2008
comment
Ссылка, кажется, битая. Он относится к: Выпускам и загрузкам журнала MSDN, где перечислены все выпуски этого журнала. Поскольку ответ не содержит никакой дополнительной информации, он не поможет без рабочей ссылки. - person Matt; 14.12.2018
comment
точно так же, если кто-то ищет, например, может проверить git github.com/stdeepak22/CSharp_Method_Interceptor - person Deepak Sharma; 28.10.2020

Я не думаю, что есть способ сделать это только с помощью атрибута, но используя proxy классы и отражение, у вас может быть класс, который знает, как перехватывать экземпляры классов, которым вы приписали методы.

Затем прокси-класс может инициировать событие всякий раз, когда вызываются методы с атрибутами.

person wprl    schedule 22.10.2008

Атрибут дает информацию, это метаданные. Я не знаю, как это сделать навскидку, кто-нибудь может.

Вы можете посмотреть на частичные методы в .NET, которые позволяют выполнять некоторую легкую обработку событий. Вы предоставляете крючки и позволяете кому-то другому заниматься реализацией. Если метод не реализован, компилятор просто игнорирует его.

http://msdn.microsoft.com/en-us/library/wa80x488.aspx

person Nick    schedule 22.10.2008

Вы можете взглянуть на решение бедняка: см. шаблон декоратора.

person balintn    schedule 07.05.2018