Сегодня мы погрузимся в мир изучения
работы фильтров в ASP.NET MVC 5. Мы рассмотрим виды фильтров, создание своих фильтров и добавление кастомных фильтров для Web API (ApiController).
Для начала создадим новое ASP.NET Web Application приложение и назовем его UsingFilterSample.
Для начала создадим новое ASP.NET Web Application приложение и назовем его UsingFilterSample.
Затем выберем шаблон Web API.
После того, как мы создали наше приложение, вернемся к теории. По умолчанию в ASP.NET MVC 5 выделяют 5 типов фильтров, которые
приведены в таблице ниже.
Как видите, единственным фильтром, у которого
нет встроенной реализации, является IAuthenticationFilter, для всех остальных есть реализация по
умолчанию.
Давайте рассмотрим методы, которые предоставляют нам эти фильтры.
Возьмем существующие классы для
реализации наших фильтров и на каждый из них реализуем свою реализацию, в
которую для начала добавим простое логирование. Чтобы приступить к
реализации, остановимся детальнее на классе ActionFilterAttribute. Этот класс уже реализует фильтры IResultFilter и IActionFilter. Функция их описана выше в таблице, а теперь давайте рассмотрим на практике их использование. Для этого на каждый тип фильтров мы создадим свой
вариант, в котором просто будем логировать наше действие. Добавим новую папку Filters, если такой папки у
вас еще нет. Затем будем по порядку добавлять нужные нам фильтры.
IAuthenticationFilter
Первым делом добавим новый класс AuthenticationCustomFilter, в который добавим следующую логику.
public class AuthenticationCustomFilter : FilterAttribute,
IAuthenticationFilter
{
public void
OnAuthentication(AuthenticationContext filterContext)
{
IPrincipal user = filterContext.HttpContext.User;
Debug.WriteLine("OnAuthentication: Controller: {2}, Action Name: {0}, User Name: {1}",
filterContext.ActionDescriptor.ActionName,
user.Identity.Name,
filterContext.ActionDescriptor.ControllerDescriptor.ControllerName);
}
public void
OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
{
}
}
Так как мы тестируем сейчас MVC фильтры, то в каждый добавленный нами класс
добавляем следующие namespace:
using System.Web.Mvc;
using System.Web.Mvc.Filters;
В приведенном выше примере мы попросту
имплементировали интерфейс IAuthenticationFilter, задачей которого стоит проверка
аутентификации пользователя. В нашем же примере, поскольку мы работаем под
анонимным пользователем, не используя какой-либо авторизации и аутентификации,
там будет всегда пусто.
AuthorizeAttribute
Затем в эту же папку добавим новый класс AuthorizationCustomFilter,
задачей которого будет показ того, как работает авторизация. Ниже приведен код этого класса.
public class AuthorizationCustomFilter : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
IPrincipal user = filterContext.HttpContext.User;
Debug.WriteLine("OnAuthorization: Controller: {2}, Action Name: {0}, User Name: {1}",
filterContext.ActionDescriptor.ActionName,
user.Identity.Name,
filterContext.ActionDescriptor.ControllerDescriptor.ControllerName);
}
}
Здесь мы просто логируем пользователя, который к нам залогинился. Зачастую приведенный выше класс AuthorizeAttribute используют без дополнительного написания кода. Например, как показано ниже.
[Authorize(Users = "Joe, Mary",
Roles = "Admin,
Manager")]
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Title = "Home Page";
return View();
}
}
Пример чисто синтетический и не несет
особой смысловой нагрузки, поэтому не стоит его копировать.
ActionFilterAttribute
Класс ActionFilterAttribute представляет собой имплементацию
интерфейсов IResultFilter и IActionFilter. Для того чтобы
посмотреть, как можно использовать фильтры действий и результатов, добавим в
папку Filters новый класс, который назовем ActionCustomFilter.
public class ActionCustomFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
Debug.WriteLine("OnActionExecuting: Controller: {0}, Action Name: {1}",
filterContext.ActionDescriptor.ControllerDescriptor.ControllerName,
filterContext.ActionDescriptor.ActionName);
}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
Debug.WriteLine("OnActionExecuted: Controller: {0}, Action Name: {1}",
filterContext.ActionDescriptor.ControllerDescriptor.ControllerName,
filterContext.ActionDescriptor.ActionName);
}
public override void OnResultExecuting(ResultExecutingContext filterContext)
{
Debug.WriteLine("OnResultExecuting: Controller: {0}, Action Name: {1}", filterContext.RouteData.Values["controller"],
filterContext.RouteData.Values["action"]);
}
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
Debug.WriteLine("OnResultExecuted: Controller: {0}, Action Name: {1}", filterContext.RouteData.Values["controller"],
filterContext.RouteData.Values["action"]);
}
}
Здесь мы тоже ничего особенного не делали
кроме логирования.
HandleErrorAttribute
Нам осталось напоследок добавить реализацию
интерфейса IExceptionFilter, который реализован в классе HandleErrorAttribute, реализацию которого мы и рассмотрим ниже.
Для этого мы в нашу папку Filters добавим новый класс CustomExceptionFilter.
public class CustomExceptionFilter : HandleErrorAttribute
{
public override void OnException(ExceptionContext filterContext)
{
Debug.WriteLine("OnException: Controller: {0}, Action Name: {1}, Message: {2}",
filterContext.RouteData.Values["controller"],
filterContext.RouteData.Values["action"],
filterContext.Exception.Message);
}
}
Для того чтобы проверить, как работает наш
только что созданный фильтр, нужно в наш HomeController добавить следующий
метод:
public ActionResult Home()
{
throw new Exception("Test
Custom exceptions");
}
Configuring Filters in ASP.NET MVC
Существуют разные уровни использования фильтров, и сейчас
мы по ним пройдемся.
Action Level
Это самый низкий уровень фильтров, которые
можно применить, и применяется он на уровне метода. Ниже представлен пример
такого метода.
[ActionCustomFilter]
public ActionResult Index()
{
ViewBag.Title = "Home Page";
return View();
}
Controller Level
Этот тип фильтров применяется на уровне
контроллера. Отличие от предыдущего заключается в том, что под влияние попадают все методы,
которые описаны в контроллере.
[Authorize(Users = "Joe, Mary",
Roles = "Admin,
Manager")]
public class HomeController : Controller
{
public ActionResult Index()
{
ViewBag.Title = "Home Page";
return View();
}
}
Global Level
Регистрация глобальных фильтров, которые
действуют на уровне всего проекта. Регистрируются они в Global.asax файле в методе
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
Ниже представлена полная реализация,
которая у меня вышла для Global.asax.
public class WebApiApplication : System.Web.HttpApplication
{
protected void
Application_Start()
{
AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
BundleConfig.RegisterBundles(BundleTable.Bundles);
}
}
Поскольку для теста предлагаю
зарегистрировать все наши фильтры глобально, давайте перейдем в метод RegisterGlobalFilters и изменим его следующим образом:
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new CustomExceptionFilter());
filters.Add(new ActionCustomFilter());
filters.Add(new AuthorizationCustomFilter());
filters.Add(new AuthenticationCustomFilter());
}
}
Теперь давайте запустим наш проект и
проверим, что все работает.
А затем в адресной строке браузера введем Home/Home, чтобы посмотреть на наш фильтр обработки
ошибок.
Web API Filters
Давайте немного пробежимся по Web API фильтрам. Судя по названию, это
фильтры, которые применимы для Web API контроллеров и методов, описанных в них. Все эти
фильтры лежат в пространстве имен System.Web.Http.Filters. Если сравнивать Web API фильтры с фильтрами MVC, то у нас остаются
практически те же фильтры с той же областью, как и фильтры за исключением того,
что в среде Web API фильтров нет интерфейса IResultFilter; вместо него всю эту
роль выполняет IActionFilter.
Теперь рассмотрим, как регистрировать
глобальные фильтры для Web API. Регистрируются они в файле Global.asax в методе WebApiConfig.Register. Ниже в примере показан пример добавления
глобальных фильтров для web api контроллеров.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Configure Web API to use only bearer token
authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
}
}
Для того чтобы посмотреть, как работает
использование фильтров на уровне методов, откроем файл AccountController и
найдем там метод GetUserInfo.
// GET
api/Account/UserInfo
[HostAuthentication(DefaultAuthenticationTypes.ExternalBearer)]
[Route("UserInfo")]
public UserInfoViewModel
GetUserInfo()
{
ExternalLoginData
externalLogin = ExternalLoginData.FromIdentity(User.Identity as ClaimsIdentity);
return new UserInfoViewModel
{
Email = User.Identity.GetUserName(),
HasRegistered = externalLogin == null,
LoginProvider = externalLogin != null
? externalLogin.LoginProvider : null
};
}
В этом же классе мы сможем увидеть, как работают
фильтры на уровне контроллера.
[Authorize]
[RoutePrefix("api/Account")]
public class AccountController : ApiController
{
Чтобы более продуктивно
подойти к изучению фильтров для ApiController, рассмотрим более практическое применение
этих фильтров. Для этого мы в нашу папку Filters добавим новый класс ValidateModelAttribute, реализация которого приведена ниже.
using System.Net;
using System.Net.Http;
using System.Web.Http.Filters;
using System.Web.Http.Controllers;
namespace UsingFilterSample.Filters
{
public class ValidateModelAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(HttpActionContext actionContext)
{
if
(actionContext.ModelState.IsValid == false)
{
actionContext.Response =
actionContext.Request.CreateErrorResponse(
HttpStatusCode.BadRequest, actionContext.ModelState);
}
}
}
}
Этот фильтр позволит добавить валидацию для
ваших моделей данных. Как видите, достаточно неплохое применение ActionFilterAttribute. Теперь разберемся, как можно использовать кастомную обработку
ошибок. Для этого добавим новый класс DatabaseExceptionFilterAttribute
в папку Filters. Ниже представлен
код его реализации.
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Web.Http.Filters;
namespace UsingFilterSample.Filters
{
public class DatabaseExceptionFilterAttribute : ExceptionFilterAttribute
{
public override void OnException(HttpActionExecutedContext context)
{
var
e = context.Exception;
HttpResponseMessage message = null;
if
(e is DbUpdateException)
{
var
ue = e as DbUpdateException;
var
errorMessage = e.InnerException?.InnerException?.Message ??
ue.InnerException?.Message ?? ue.Message;
message =
context.Request.CreateErrorResponse(HttpStatusCode.BadRequest,
errorMessage);
}
else if
(e is DbEntityValidationException)
{
var
ev = e as DbEntityValidationException;
var
sb = new StringBuilder();
foreach (var validationError in
ev.EntityValidationErrors.SelectMany(entityValidationError
=> entityValidationError.ValidationErrors)
)
{
sb.AppendLine(string.Format("Property:{0}, ErrorMesage:{1}", validationError.PropertyName, validationError.ErrorMessage));
}
message =
context.Request.CreateErrorResponse(HttpStatusCode.BadRequest,
sb.ToString());
}
else
{
base.OnException(context);
return;
}
context.Response = message;
base.OnException(context);
}
}
}
Польза данного класса состоит в том, что мы
можем расширить ответ, который присылаем клиенту, и клиент может понять, почему
ему не удалось сохранить введенные им данные в базу данных. Ниже приведен кусок
кода, как это может быть отражено на клиенте через вспомогательные окна или
логирование с помощью AngularJS.
$scope.save
= function () {
$http.post('api/Clients', $scope.client).success(function () {
console.log("Client saved succesfully");
}).error(function (result) {
$scope.errors = {};
errorHelper.validateForms($scope.clientForm, $scope.errors,
result.modelState, toastr);
toastr.error(result.message);
});
};
Если у нас произошла какая-то ошибка, то мы
ее отображаем через message без необходимости проверять тип ошибки и получать
необходимое свойство. Например, для DbUpdateException нам пришлось бы детальную ошибку смотреть в
InnerException на клиенте, а так фильтры нам помогли упростить нашу задачу.
После проделанной работы давайте
зарегистрируем наши фильтры глобально в WebApiConfig.Register, как показано ниже.
config.Filters.Add(new ValidateModelAttribute());
config.Filters.Add(new DatabaseExceptionFilterAttribute());
Полную реализацию можно увидеть ниже.
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Configure Web API to use only bearer token
authentication.
config.SuppressDefaultHostAuthentication();
config.Filters.Add(new HostAuthenticationFilter(OAuthDefaults.AuthenticationType));
config.Filters.Add(new ValidateModelAttribute());
config.Filters.Add(new DatabaseExceptionFilterAttribute());
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new
{ id = RouteParameter.Optional }
);
}
}
Итоги
Надеюсь, небольшая справка по использованию фильтров в ваших ASP.NET приложениях будет полезна для работы, и вы сможете разобраться, как применять те или иные фильтры, в зависимости от ситуации.