How to pass a method or parameter to an action filter in ASP.Net MVC

我怕爱的太早我们不能终老 提交于 2020-06-29 04:06:38

问题


I'm going to handle authentication and authorization in an action filter and create an action filter like below:

public class Auth : ActionFilterAttribute
{
    public int Access { get; set; }
    public string Roles { get; set; } = "Default";
    public Func<bool> AuthFunc { get; set; }

    public override void OnActionExecuting(HttpActionContext actionContext)
    {
        string UserId = HttpContext.Current.User.Identity.GetUserId();
        //Authentication 
        if (Roles != "Default" && UserManager.IsInRole(UserId, Roles))
        {
           //Authorization 
           if (AuthFunc) { base.OnActionExecuting(actionContext); }
           else
             {
                var response = actionContext.Request.CreateResponse(HttpStatusCode.Redirect);
                Uri requestUrl = actionContext.Request.RequestUri;
                response.Headers.Location = new Uri($"{requestUrl.Scheme}://{requestUrl.Host}:{requestUrl.Port}");
                actionContext.Response = response;
             }
        }
        else
        {
            var response = actionContext.Request.CreateResponse(HttpStatusCode.Redirect);
            Uri requestUrl = actionContext.Request.RequestUri;
            response.Headers.Location = new Uri($"{requestUrl.Scheme}://{requestUrl.Host}:{requestUrl.Port}");
            actionContext.Response = response;
        }
    }
}

And in the controller:

[Auth(Roles="Teacher" , Access = (short)TableEnum.Course , AuthFunc = Courses.CheckCoursesOfTeacher(CourseId))]
public ActionResult ShowExerciseAnswers(int CourseId,int ExerciseId)
{
    return View(model: ChapterExerciseAnswer.ExerciseAnswerList(CourseId,ExerciseId));
}

The AuthFunc method maybe has multiple inputs but just a bool return value.

  1. How to pass AuthFunc (the Courses.CheckCoursesOfTeacher(CourseId) method) to action filter?

  2. How to get CourseId action parameter in action filter attribute (pass CourseId or ExerciseId as an attribute value)?

    What is the best way of handling these issues(functions and variables can't be sent to an action filter)?


回答1:


The Problem with Passing Functions in Attribute Parameters

I found myself looking for a solution like this recently. Parameters for attributes have to follow the following rules, per MS Docs:

Parameters to an attribute constructor are limited to simple types/literals: bool, int, double, string, Type, enums, etc and arrays of those types. You can not use an expression or a variable. You are free to use positional or named parameters.

Because of this, passing a function to the filter via an attribute parameter is not something we can do. There are probably lots of alternatives, but here's what I chose to do:

A Solution

I used dependency injection to inject my action filter with a manager, and then used Reflection to tell the filter which method on the manager to execute. Here's what it looks like:

Class:

public class Phone : AuditBase 
{
    ...other props...

    [AuditRuleset(AuditRule.PhoneNumber)]
    public string Number { get; set; }
}

Enum:

public enum AuditRule
{
    PhoneNumber // You can add [Description] if you want
}

Attribute:

public class AuditRulesetAttribute : Attribute
{
    private readonly AuditRule _rule;

    public AuditRulesetAttribute(AuditRule rule) => _rule = rule;
}

Filter:

public class FormAuditActionFilter : IActionFilter
{
    private ILog _log { get; set; }
    private IFormAuditor _auditor { get; set; }

    public FormAuditActionFilter(ILog log, IFormAuditor auditor)
    {
        _log = log;
        _auditor = auditor;
    }
    ...lots of filter code...
    ... The following is from OnActionExecuted, having stored the props of the submitted object in objectProperties...

    foreach(PropertyInfo propertyInfo in objectProperties)
    {
        // Check first for any special audit comparison rules which should be followed
        var auditRuleAttributes = propertyInfo.CustomAttributes
            .Where(x => x.AttributeType.Name == typeof(AuditRulesetAttribute).Name)
            .ToList();

        if (auditRuleAttributes.Any())
        {
            IEnumerable<IList<CustomAttributeTypedArgument>> attrList = auditRuleAttributes
                .Select(x => x.ConstructorArguments);

            foreach(IList<CustomAttributeTypedArgument> attr in attrList)
                foreach(CustomAttributeTypedArgument arg in attr)
                    if (_auditRuleManager.IsChanged(oldValue, newValue, (AuditRule)arg.Value))
                        result.Add(BuildValueHistory(propertyInfo.Name, oldValue, newValue));
            continue;
        }
    }
    ...lots more filter code...
}

AuditRuleManager:

public class AuditRuleManager : IAuditRuleManager
{
public bool IsChanged(object val1, object val2, AuditRule rule)
{
    object[] objArray = {val1, val2};

    var comparisonResult = typeof(AuditRuleManager)
        .GetMethods(BindingFlags.NonPublic | BindingFlags.Instance)
        .Single(m => m.Name == rule.GetDescription()) // Try to get description, but falls back to name by default
        .Invoke(this, objArray) as bool?;

    return (bool)comparisonResult; // Throw an exception if the comparison result was not a valid bool
}

// Compare phone numbers with special rules, and return their equality
private bool PhoneNumber(object val1, object val2) // NOTE: Name of method matches name of enum value
    => GetNumbersFromString(val1 as string) != GetNumbersFromString(val2 as string);

The last piece that took me a while was the DI for the filter using Ninject. Here's how it worked in my

Global.asax.cs:

kernel.BindFilter<FormAuditActionFilter>(FilterScope.Action, 0)
                  .WhenActionMethodHas<FormAuditAttribute>()
                  .WithConstructorArgument("log", log)
                  .WithConstructorArgument("auditor", auditManager);

Summary

Instead of passing a function as an attribute parameter, I used DI to inject a manager into my filter. This gives your filter access to the functions you want. Second, I used an enum to hold the name of the function that should be executed. So essentially, all you have to do to create a new function and execute it with a parameter is to:

  1. Add it to the enum
  2. Add a function of the same name to the manager

I hope this helps!

Further Reading

https://blogs.cuttingedge.it/steven/posts/2014/dependency-injection-in-attributes-dont-do-it/



来源:https://stackoverflow.com/questions/53936979/how-to-pass-a-method-or-parameter-to-an-action-filter-in-asp-net-mvc

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!