简体   繁体   中英

Pass typeof(x) type to generic method

I've made some Middleware that logs all actions taken by a user within my application. Depending on the action taken, I need parse out some [FromBody] JSON into their respective key/value pairs for logging.

I need to deserialize my JSON within the middleware, but in order to do that, I need to send my DtoType along to the deserializer in order for it to parse out my key/values. I've got a method setup to do that, but I need to pass in a generic type because this will be different for every single action the user takes. (eg I have a UserDto, CustomerDto, etc...)

I've setup a dictionary in order to get the type that I need, however when I pass the var to my logging method to do the rest of the work, I get an error stating that this is not a type but a variable. This is true, however I have no idea how I'm supposed to get the type that I pulled out of my dictionary into the method generic type.

See my code below:

LoggingMiddleware.cs readonly dictionary

    private readonly Dictionary<string, Type> _postDictionary = new Dictionary<string, Type>
    {
        { "path/customers", typeof(CustomerPostDto) },
        ...//More entries//...
    };

LoggingMiddleware.cs Invoke Method

public async Task Invoke(HttpContext context)
{
    using (var streamCopy = new MemoryStream())
    {
        ...//Do some stuff here//...
        //Logging Actions
        if (request.Path != "/")
        {
            if (request.Method == "POST")
            {
               Type T = _postDictionary[path];
               logAction<T>(contextDto);
            }
        }
        ...//Do some stuff here//...
    }
}

LoggingMiddleware.cs logAction Method

private void logAction<T>(object contextDto)
{
    var dto = ControllerBase.ParseBody<T>(contextDto.Body);
    ...//Do some stuff here//...
}

EDIT: Following Example of Possible Duplicate - updated code

                if (request.Method == "POST")
                {
                    Type T = _postDictionary[path];

                    MethodInfo methodLogAction = typeof(LoggingMiddleware).GetMethod("logAction", BindingFlags.NonPublic);
                    MethodInfo generic = methodLogAction.MakeGenericMethod(T);
                    generic.Invoke(contextDto, null);
                }

The above never returns anything for GetMethod other than null.

The exception is telling you exactly what is wrong.

Type T = _postDictionary[path];

This line of code pulls a Type instance from the dictionary and stores it in the variable, T . Then, you try to use it like this:

logAction<T>(contextDTO);

However, a generic method expects a non-variable argument between the angle-brackets. Types don't change at run-time; but the type arguments to a generic method can. (There are some compiler-specific nuances to that statement, but we'll ignore those for now.)

What you're essentially trying to get at is this:

logAction<SomeType>(contextDTO);

But if you want to store the type in a Dictionary , you'll have to pass that type as an argument to your method, and lose the generic capability:

public void logAction(Type type, object data)
{
    // Log the data here
}; 

This is because the value of T is only known at runtime, not at compile time. You're going to have to reflect over T to get at its properties (as your question implies). In that event, you likely don't want a generic method, anyway.

If you're using json.net you could do something like:

    public void LogAction<T>(string contextDto, Type type)
    {
        T obj = (T)JsonConvert.DeserializeObject(contextDto, type) ;
    }

Or If I'm reading this wrong, and you and you want something like this, you could do this.

    public void LogAction<T>(T obj)
    {

    }
    public ActionResult Test([FromBody] Thing thing)
    {
        LogAction(thing);
    }

I was able to get this with help of the Duplicate Post.

Within my Invoke method, I used GetMethod to find my method and assign a generic type based on my dictionary. Since it was a private method, I had to use both the BindingFlags.NonPublic & BindingFlags.Instance flags in order for it to find the method.

            //Logging Actions
            if (request.Path != "/")
            {
                if (request.Method == "POST")
                {
                    Type T = _postDictionary[path];

                    MethodInfo methodLogAction = typeof(LoggingMiddleware).GetMethod("LogAction", BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] {typeof(object)}, null);
                    MethodInfo generic = methodLogAction.MakeGenericMethod(T);
                    generic.Invoke(this, new object[]{contextDto});
                }
            }

There is a difference between a Type (a class) and a generic type T. The T you are trying to get from your dictionary is simply a normal variable of the class Type, and not anything you can pass as a generic type parameter. You would probably have to change your approach a bit in order to achieve what you want without using reflection.

Approach 1. Let LogAction take a Type as a parameter and hope that there is an overloaded version that accepts such an argument:

private void LogAction(object contextDto, Type type) {
    ControllerBase.ParseBody(contextDto.Body, type);
}

Or you can look into using a Func to control your parsing behavior better, something like

    // Method to get a Func that can parse your object
    private static Func<System.IO.Stream, T> GetParser<T>()
    {
        return (contextDto) => ControllerBase.ParseBody<T>(contextDto.Body);
    }

    // Use that in your dictionary
    private Dictionary<string, Func<System.IO.Stream, object>> transformers = new Dictionary<string, Func<System.IO.Stream, object>>
    {
        {  "/myPath", GetParser<CustomerPostDto>() },
        {  "/myPath-2", GetParser<CustomerPostDto>() }
    };

    // Now the LogAction method can just take the DTO that will be inferred from our parser
    private void LogAction<T>(T dto)
    {
        ...//Do some stuff here//...
    }

    // And call it as such
    if (transformers.ContainsKey(path))
            LogAction(transformers[path](context.Response.Body));

I would recommend it over reflection as it should give you more control in the long run.

You can get some more fun and abstraction by separating what is logging, and the other unrelated code:

    // Return a logger with a specification on how to parse a stream from a body
    private static TypeLogger CreateLogger<T>()
    {
        return new TypeLogger<T>((ctx) => ControllerBase.ParseBody<T>(contextDto.Body));
    }

    // Create a list with paths and loggers of specified type
    private Dictionary<string, TypeLogger> loggers = new Dictionary<string, TypeLogger>
    {
        { "/path1", CreateLogger<CustomerPostDto>() },
        { "/path2", CreateLogger<CustomerPostDto>() },
    };

    // Abstract base logger class that allows you to log from a requestbody
    public abstract class TypeLogger
    {
        public abstract void Log(System.IO.Stream requestBody);
    }

    // An actual implementation to parse your dto using by using the parser previously specified
    public class TypeLogger<T> : TypeLogger
    {
        // Parser to use when getting entity
        public Func<System.IO.Stream, T> Parser { get; private set; }

        // Constructor that takes sa func which helps us parse
        public TypeLogger(Func<System.IO.Stream, T> parser) => Parser = parser;

        // The actual logging
        public override void Log(System.IO.Stream requestBody)
        {
            var dto = Parser(requestBody);

            //...
        }
    }

    // And usage
    if (loggers.Contains(path))
        loggers[path].Log(ctx.Response.Body);

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