Can you pass generic delegate without the type par

2020-07-23 06:27发布

I have three projects

  1. MVC Web application
  2. Service application which is kind of two layers business/repository
  3. Entity framework (all EF configuration lives here)

MVC references > service

Service references > EF

I have these three methods currently that do some work.

public bool StoreUpload<T>(UploadInformation information) 
   where T : class, IUploadEntity { }

public bool RemoveUpload<T>(UploadInformation information) 
   where T : class, IUploadEntity { }

public bool CommitUpload<T>(UploadInformation information) 
   where T : class, IUploadEntity { }

I call these three methods from my controller using these interfaces which delegate to the work methods above:

Boolean StoreUpload(UploadInformation information);
Boolean RemoveUpload(UploadInformation information);
Boolean CommitStoredDocuments(UploadInformation information);

Based on a condition from UploadTypes enumeration in a switch I call the correct work method. I do this because I don't want my mvc project to have access to the EF database types otherwise I know someone is going to start querying data from all over the application. I use these switch statements for all interfaced methods:

public bool StoreUpload(UploadInformation information)
{            
    switch (information.Type)
    {
        case UploadTypes.AutoIncident:
            return RemoveUpload<AutoIncident>(information);
        case UploadTypes.Incident:
            return RemoveUpload<IncidentInjury>(information);
        case UploadTypes.Inspection:
            return RemoveUpload<Inspection>(information);
        case UploadTypes.OtherIncident:
            return RemoveUpload<OtherIncident>(information);
        default:
            return false;
    }
}

public bool RemoveUpload(UploadInformation information) { ... }
public bool CommitStoredUpload(UploadInformation information) { ... }

This method might shed a little light on what the types parameters are being used for. I am updating tables in a generic way using EF.

private bool CommitStoredDocuments<T>(UploadInformation information) where T : class, IUploadEntity
{
        var uploads = GetStoredUploads(information.UniqueId);
        var entity = db.Set<T>().Include(e => e.Uploads)
             .Single(e => e.UniqueId == information.UniqueId);
        entity.Uploads.AddRange(uploads);
 ... 

} 

It would be nice to be able to pass the work method which requires a type parameter as a delegate to the switch work method calls.

public bool DoSomeWork(delegateMethod, information) {
    switch(information.Type) { 
         case UploadTypes.AutoInciden:
           return delegateMethod<AutoIncident>(information);
         ...
    }
} 

Can this be done? Also, I had trouble constructing a good title for this question so please comment if these is a better way to describe the challenge.

3条回答
兄弟一词,经得起流年.
2楼-- · 2020-07-23 06:47

Instead of using delegates, consider using an interface (or abstract class). This way, your methods can retain their generic nature.

For example, if you create an interface like:

interface IUploadAction
{
    bool Perform<T>(UploadInformation information)
       where T : class, IUploadEntity;
}

Note that the T is not exposed in the type, it's only on the method. This is the key part.

Now you can implement this for your database methods:

class CommitStoredDocuments : IUploadAction
{
    public bool Perform<T>(UploadInformation information)
       where T : class, IUploadEntity
    {
        var uploads = GetStoredUploads(information.UniqueId);
        var entity = db.Set<T>().Include(e => e.Uploads)
             .Single(e => e.UniqueId == information.UniqueId);
        entity.Uploads.AddRange(uploads);

        //...
    }
}

Your switching/dispatching method can look like this:

public bool DoAction(IUploadAction action, UploadInformation information)
{
    switch (information.Type)
    {
        case UploadTypes.AutoIncident:
            return action.Perform<AutoIncident>(information);
        case UploadTypes.Incident:
            return action.Perform<IncidentInjury>(information);
        case UploadTypes.Inspection:
            return action.Perform<Inspection>(information);
        case UploadTypes.OtherIncident:
            return action.Perform<OtherIncident>(information);
        default:
            return false;
    }
}

And then you can write something like:

IUploadAction storeUpload;

public bool StoreUpload(UploadInformation information) => DoAction(storeUpload, information);
查看更多
成全新的幸福
3楼-- · 2020-07-23 06:55

It cannot be done directly due to several reasons.

First of all, as you probably noticed, delegateMethod<FooBar>(information) simply does not compile. This is because in your example the delegateMethod is a local variable (method parameter actually, but still a variable), and you cannot apply "type arguments" <FooBar> to a variable - you can apply them only on an identifier that indicates a (generic) type or a (generic) method.

Second reason is more interesting. When you pass a method as a delegate, the delegate actually catches the whole method signature, including all parameter types.

void Blah<T>(UploadInformation information){ ... }

var one = new Action<int>(Blah); // -> Blah<int>
var two = new Action<float>(Blah); // -> Blah<float>
var thr = new Action<andsoon>(Blah); // -> Blah<andsoon>

MagicDoSomeWork(one, ...); // these all
MagicDoSomeWork(two, ...); // delegates are already bound
MagicDoSomeWork(thr, ...); // and remember their concrete T

You need to actually specify the type for the Action so a proper version of generic method will be picked from a general description called Blah. These delegates are bound to concrete versions of the method and will accept only that types. These delegates are 'closed' in terms of their type arguments. Using normal ways, the MagicDoSomeWork will simply have no way of altering the T which these delegates already have remembered.

That two things are a kind of show stoppers, since by normal code only, you cannot write things like

var nope1 = new Action(Blah);  // ctor for Action NEEDS type parameter

since Action constructor simply requires a type parameter. And once you pass any, it will lock the Blah type arguments

Also you cannot use open delegates:

var nope1 = new Action<>(Blah); // can't use empty <> in this context :(

since new operator requires a full type to create an object.

However, with a bit of reflection voodoo, it is possible to analyze and build a generic type or a generic method dynamically.

// first, build the delegate in a normal way
// and pick anything as the type parameters
// we will later replace them
var delegateWithNoType = new Action<object>(Blah);
// delegate has captured the methodinfo,
// but uses a stub type parameter - it's useless to call it
// but it REMEMBERS the method!

// .... pass the delegate around

// later, elsewhere, determine the type you want to use
Type myRealArgument;
switch(..oversomething..)
{
    default: throw new NotImplemented("Ooops");

    case ...: myRealArgument = typeof(UploadTypes.AutoIncident); break;
    ...
}

// look at the delegate definition
var minfo = delegateWithNoType.Method;
var target = delegateWithNoType.Target; // probably NULL since you cross layers

var gdef = minfo.GetGenericDefinition();
var newinfo = gdef.MakeGenericMethod( myRealArgument );

// now you have a new MethodInfo object that is bound to Blah method
// using the 'real argument' type as first generic parameter
// By using the new methodinfo and original target, you could now build
// an updated delegate object and use it instead the original "untyped" one
// That would be a NEW delegate object. You can't modify the original one.

// ...but since you want to call the method, why don't use the methodinfo

UploadInformation upinfo = ... ;
newinfo.Invoke(target, new object[] { upinfo });
// -> will call Blah<UploadTypes.AutoInciden>(upinfo)

word of warning: this is a sketch to show you how the delegate.Method/Target and methodinfo and getgenericdefinition and makegenericmethod work. I wrote it from memory, never compiled, never ran. It can contain minor typos, overlooked things and invisible rainbow unicorns. I didn't noticed any. Probably because they were invisible.

查看更多
Lonely孤独者°
4楼-- · 2020-07-23 07:00

You can do it like this

public bool Invoke(EntityType entityType, ActionType action, Object[] arguments)
    {
        var actionType = Enum.GetName(typeof(ActionType), action);
        var type = GetType();
        var method = type.GetMethods().Single(m => m.IsGenericMethod && m.Name == actionType);

        switch (entityType)
        {
            case EntityType.IncidentInjury:
                var genericMethod = method.MakeGenericMethod(typeof(IncidentInjury));
                return (bool)genericMethod.Invoke(this, arguments);
            default:
                return false;
        }
    }

The enum will just be a list of methods that I want to invoke this way and I create a base class for my services so I don't have to pass the instance to the Invoke method.

查看更多
登录 后发表回答