简体   繁体   中英

How do I detect and log the parameter names and values in the method signature of a delegate?

Thanks for looking!

Background

I have an extension method that is used to wrap a given method in a try/catch and I am adding code for logging any caught exceptions:

 public static T HandleServerError<T>(this Func<T> func)
        {
            T result = default(T);
            try
            {
                result = func();
            }
            catch (Exception ex)
            {
                //******************************
                //Code for logging will go here.
                //******************************

                ErrorHandlers.ThrowServerErrorException(ex);
            }

            return result;
        }

Here is how the method is called:

var result = new Func<SomeClass.SomeType>(() => SomeClass.SomeMethod(id, name, color, quantity)).HandleServerError();
return result;

As you can see, whatever method I am calling is injected into the extension method and executed inside the try/catch.

We will be using NLog or ELMAH for logging, but that is largely irrelevant to this question.

Problem

If something goes wrong, I need to log as much information about the delegated method as possible since things like "Object reference not set to an instance of an object" is not in itself helpful.

I would like to log the class and name of the method being called as well as the parameters in the method signature along with their values. If possible, I would even like to log which line failed, and finally the actual stack trace.

I am guessing that I need to use reflection for this and maybe catch the binding flags somehow as the injected method executes but I am not entirely sure if that is the best approach or if it is even feasible.

Question

Using C#, how do I get the meta information (ie method name, class of origin, parameters, parameter values) about an injected/delegated method?

Thanks in advance.

It seems to me that there is a possibility for you to improve the way you are adding this logging cross-cutting concern to your application.

The main issue here is that although your solution prevents you from making any changes to SomeClass.SomeMethod (or any called method), you still need to make changes to the consuming code. In other words you are breaking the Open/closed principle , which tells us that it must be possible to make these kinds of changes without changing any existing code.

You might think I'm exaggerating, but you probably already have over a hundred calls to HandleServerError in your application, and the number of calls will only be growing. And you'll soon add even more of those 'functional decorators' to the system pretty soon. Did you ever think about doing any authorization checks, method argument validation, instrumentation, or audit trailing? And you must admit that doing new Func<T>(() => someCall).HandleServerError() just feels messy, doesn't it?

You can resolve all these problems, including the problem of your actual question, by introducing the right abstraction to the system.

First step is to promote the given method arguments into a Parameter Object :

public SomeMethodParameters
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Color Color { get; set; }
    public decimal Quantity { get; set; }

    public decimal Result { get; set; }
}

Instead of passing all the individual arguments into a method, we can pass them all together as one single object. What's the use of that, you may say? Read on.

Second step is to introduce a generic interface to hide the actual logic of the SomeClass.SomeMethod (or in fact any method) behind:

public interface IMethodHandler<TParameter>
{
    void Handle(TParameter parameter);
}

For each (business) operation in the system, you can write an IMethodHandler<TParameter> implementation. In your case you could simply create an implementation that wraps the call to SomeClass.SomeMethod , like this:

public class SomeMethodHandler 
    : IMethodHandler<SomeMethodParameters>
{
    void Handle(SomeMethodParameters parameter)
    {
        parameter.Result = SomeClass.SomeMethod(
            parameter.id,
            parameter.Name, 
            parameter.Color, 
            parameter.Quantity);
    }
}

It might look a bit silly to do things like this, but it allows you to implement this design quickly, and move the logic of the static SomeClass.SomeMethod inside of the SomeMethodHandler .

Third step is let consumers depend on a IMethodHandler<SomeMethodParameters> interface, instead of letting them depend on some static method in the system (in your case again the SomeClass.SomeMethod ). Think for a moment what the benefits are of depending on such abstraction.

One interesting result of this is that it makes it much easier to unit test the consumer. But perhaps you're not interested in unit testing. But you are interested in loosely coupling. When consumers depend on such abstraction instead of a real implementation (especially static methods), you can do all kinds of crazy things, such as adding cross-cutting concerns such as logging. A nice way to do this is to wrap IMethodHandler<T> implementations with a decorator . Here is a decorator for your use case:

public class LoggingMethodHandlerDecorator<T> 
    : IMethodHandler<T>
{
    private readonly IMethodHandler<T> handler;

    public LoggingMethodHandlerDecorator(
        IMethodHandler<T> handler)
    {
        this.handler = handler;
    }

    public void Handle(T parameters)
    {
        try
        {
            this.handler.Handle(parameters);
        }
        catch (Exception ex)
        {
            //******************************
            //Code for logging will go here.
            //******************************

            ErrorHandlers.ThrowServerErrorException(ex);

            throw;
        }
    }
}

See how the Handle method of this decorator contains the code of your original HandleServerError<T> method? It's in fact not that much different from what you were already doing, since the HandleServerError 'decorated' (or 'extended') the behavior of the original method with new behavior. But instead of using method calls now, we're using objects.

The nice thing about all this is, is that this single generic LoggingMethodHandlerDecorator<T> can be wrapped around every single IMethodHandler<T> implementation and can be used by every consumer. This way we can add cross-cutting concerns such as logging, etc, without both the consumer and the method to know about this. This is the Open/closed principle.

But there is something else really nice about this. Your initial question was about how to get the information about the method name and the parameters. Well, all this information is easily available now, because we've wrapped all arguments in an object instead of calling some custom method wrapped inside a Func delegate. We could implement the catch clause like this:

string messageInfo = string.Format("<{0}>{1}</{0}>",
    parameters.GetType().Name, string.Join("",
        from property in parameters.GetType().GetProperties()
        where property.CanRead
        select string.Format("<{0}>{1}</{0}>",
            property.Name, property.GetValue(parameters, null)));

This serializes the name of the TParameter object with its values to an XML format. Or you can of course use .NET's XmlSerializer to serialize the object to XML or use any other serialization you need. All the information if available in the metadata, which is quite nice. When you give the parameter object a good and unique name, it allows you to identify it in the log file right away. And together with the actual parameters and perhaps some context information (such as datetime, current user, etc) you will have all the information you need to fix a bug.

There is one difference between this LoggingMethodHandlerDecorator<T> and your original HandleServerError<T> , and that is the last throw statement. Your implementation implements some sort of ON ERROR RESUME NEXT which might not be the best thing to do. Is it actually safe to continue (and return the default value) when the method failed? In my experience it usually isn't, and continuing at this point, might make the developer writing the consuming class think that everything works as expected, or might even make the user of the application think that everything worked out as expected (that his changes were saved for instance, while in fact they weren't). There's usually not much you can do about this, and wrapping everything in catch statements only makes it worse, although I can imagine that you want to log this information. Don't be fooled by user requirements such as “the application must always work” or “we don't want to see any error pages”. Implementing those requirements by suppressing all errors will not help and will not fix the root cause. But nonetheless, if you really need to catch-and-continue, just remove the throw statement`, and you'll be back at the original behavior.

If you want to read more about this way of designing your system: start here .

You can simply access its Method and Target properties as it's basically any other delegate.

Just use func.Method and func.Target .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM