问题
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.
How to pass
AuthFunc
(theCourses.CheckCoursesOfTeacher(CourseId)
method) to action filter?How to get
CourseId
action parameter in action filter attribute (passCourseId
orExerciseId
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:
- Add it to the enum
- 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