У меня есть следующий код, повторяемый несколько раз в приложении mvc.
public ActionResult AnAction(int Id)
{
var claim = GetClaim(Id);
if (claim == null)
{
return View("ClaimNotFound");
}
// do stuff here
....
return ....;
}
Пока этот шаблон используется 4 раза, и он становится уродливым. Каков наилучший способ реорганизации?
Edit:
Несколько примеров использования
public ActionResult Claim(int Id)
{
var claim = GetClaim(Id);
if (claim == null)
{
return View("ClaimNotFound");
}
return View("Claim", claim);
}
public ActionResult MedicalPV(int Id)
{
var claim = GetClaim(Id);
if (claim == null)
{
return View("ClaimNotFound");
}
return PartialView(claim.MedCerts.AsQueryable<MedCert>());
}
Обычно мне требуется доступ к объекту в представлении. Этот конкретный код используется только в одном контроллере, но мне может понадобиться нечто подобное в других контроллерах с разными объектами и представлениями.
Если для всех действий требуется требование, вы можете попытаться извлечь его в OnActionExecuting и установить Результат в ViewResult при ошибке. Если только некоторые действия требуют, возможно, ActionFilter, который проверяет, чтобы запрос был доступен до запуска метода, а если нет, задает правильный вид.
private Claim Claim { get; set; }
public override void OnActionExecuting( ActionExecutingContext context )
{
this.Claim = GetClaim( int.Parse( context.RouteData["id"] ) );
if (this.Claim == null)
{
context.Result = View( "ClaimNotFound" );
}
}
ИЛИ
public class RequiresClaimIdAttribute : ActionFilterAttribute
{
public override void OnActionExecuting( ActionExecutingContext context )
{
var claim = GetClaim( int.Parse( context.RouteData["id"] ) );
if (claim == null)
{
context.Result = new ViewResult
{
ViewName = "ClaimNotFound",
ViewData = context.Controller.ViewData
};
}
else
{
var property = context.Controller
.GetType()
.GetProperty( "Claim",
BindingFlags.Public
| BindingFlags.NonPublic
| BindingFlags.Instance);
if (property != null)
{
property.SetValue(context.Controller,claim);
}
}
}
}
[RequiresClaimId]
public ActionResult AnAction( int id )
{
this.Claim.Updated = DateTime.Now;
...
}
ActionExecutingContext
и ActionFilterAttribute
? Работаете ли вы с SeanX или вы хотели, чтобы реализация этих классов была неявной?
Обновление 2. Я не понимал, что этот вопрос касается MVC. Извини за это. Следующий ответ по-прежнему действителен с более общей точки зрения.
Первый рефакторинг, который я бы сделал, - это вызов GetClaim
из метода. Похоже, что AnAction
не мог (и, следовательно, не должен) зависеть от GetClaim
. Он должен получать вместо этого Claim
;
Во-вторых, я применил бы то же правило к вызову View
. Вместо этого AnAction
должен просто не допускать null Claim
.
Код будет выглядеть так:
public ActionResult AnAction(Claim claim)
{
if (claim == null) throw new ArgumentNullException("cliam");
// do stuff here
....
return ....;
}
Вы могли бы сказать, что я просто отказался от ответственности за обработку null Claim
в другое место. И это именно то, что я предлагаю вам сделать. Первоначальная проблема заключалась в том, что все ваши действия были слишком ответственны. Я применяю принцип единой ответственности.
Обновление 1. После создания рефакторинга, который я предложил, вы можете заменить все вызовы методам действий вызовом следующего метода. Возможно, это не решение, которое вы ищете, но это похоже на хорошее начало (по крайней мере для меня).
public static ActionResult InvokeAction(Func<Claim,ActionResult> action, int id)
{
var claim = GetClaim(Id);
if (claim == null) return View("ClaimNotFound");
return action(claim);
}
Вы можете использовать настраиваемое связующее устройство.
public ActionResult AnAction(Claim claim)
{
if (!ModelState.IsValid)
return View("Invalid claim");
}
public class ClaimModelBinder: IModelBinder
{
public object BindModel(BindingContext context) // don't remember exactly
{
var id = context.ValueProvider.GetRawValue(context.ModelName);
if (string.IsNullOrEmpty(id))
{
context.ModelState.AddModelError(context.ModelName, "Empty id");
return null;
}
var claim = GetClaim(id);
if (claim == null)
{
context.ModelState.AddModelError(context.ModelName, "Invalid claim");
return null;
}
return claim;
}
}
// in global.asax.cs
ModelBinders.Binders.Add(typeof(Claim), new ClaimCustomerBinder());
Почему это лучше, чем фильтр действий:
работает для свойств претензии другого класса и даже IList в случае, если вы хотите передать несколько объектов претензии
Но вы не можете обрабатывать определенные действия, т.е. вернуть View ( "NotFound" ) для этой ошибки. Однако это легко сделать, если вы разработаете собственное соглашение: например, ваш ModelBinder может выполнять
context.ModelState.AddModelError(context.ModelName, new NotFoundException("Invalid claim", "ClaimNotFound"));
public override void OnActionExecuting(ActionExecutingContext context)
{
var notfound = from o in ModelState
from e in o.Value.Errors where
where e.Exception is NotFoundException
select e.Exception;
if (notfound.Count() == 1)
context.Result = View { Name = notfound.First().ViewName };
}
Альтернатива просто
if (!ModelState.IsValid)
// this view will just show all ModelState errors
return View("IncorrectInput");
Это может показаться сложнее, но это хорошее разделение проблем. Модельное связующее отображает данные из HTTP на ваши классы, тогда вы можете либо проверить ModelState вручную, либо полагаться на некоторую автоматическую обработку. Это может показаться излишним для простых приложений, но если вы начнете получать дубликаты здесь и там, а ваши модели станут сложными, модельные вяжущие могут значительно упростить вашу жизнь, так как ваши действия с контроллером получат готовые к использованию объекты, и вы можете сконцентрироваться на реальном бизнес-коде вместо инфраструктуры/сантехники.
Это (я считаю), для чего предназначен ASP.NET MVC - вы можете создавать собственный набор соглашений вокруг него.
Если это логика проверки требований, которую вы хотите реорганизовать, лучшим вариантом будет написать настраиваемый фильтр авторизации. Вы сделали бы это, создав класс, который наследует от FilterAttribute и реализует IAuthorizationFilter.
У IAuthorizationFilter есть один метод: OnAuthorization, в котором вы должны поставить логику проверки требований. Вместо того, чтобы возвращать ViewResult (как вы это делаете выше), вы должны установить свойство Result объекта AuthorizationContext, которое будет передано в метод OnAuthorization. Он будет выглядеть примерно так:
public class RequiresClaim : FilterAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationContext filterContext)
{
var claim = GetClaim(filterContext.RouteData["id"]);
if (claim == null) { filterContext.Result = new ViewResult { ViewName = "ClaimNotFound" }; }
}
}
После того, как у вас есть этот фильтр, вы можете просто поместить атрибут в каждый метод действий, который вы хотите, чтобы эта проверка заявки произошла. Если вы хотите, чтобы проверка требований выполнялась для каждого действия в контроллере, вы можете просто положить его на контроллер.