Могу ли я использовать Content Negotiation, чтобы вернуть View to browers и JSON в вызовы API в ASP.NET Core?
У меня есть довольно простой метод контроллера, который возвращает список клиентов. Я хочу, чтобы он возвращал представление списка, когда пользователь просматривает его, и возвращает JSON для запросов с application/json
в заголовке Accept.
Возможно ли это в ASP.NET Core MVC 1.0?
Я пробовал это:
[HttpGet("")]
public async Task<IActionResult> List(int page = 1, int count = 20)
{
var customers = await _customerService.GetCustomers(page, count);
return Ok(customers.Select(c => new { c.Id, c.Name }));
}
Но это возвращает JSON по умолчанию, даже если это не в списке Accept. Если я ударил "/клиентов" в своем браузере, я получаю вывод JSON, а не мой взгляд.
Мне казалось, что мне нужно написать OutputFormatter, который обрабатывал text/html
, но я не могу понять, как я могу вызвать метод View()
из OutputFormatter
, так как эти методы находятся на Controller
, и Мне нужно было бы узнать имя View, которое я хотел бы сделать.
Есть ли способ или свойство, которое я могу вызвать, чтобы проверить, сможет ли MVC найти OutputFormatter
для рендеринга? Что-то вроде следующего:
[HttpGet("")]
public async Task<IActionResult> List(int page = 1, int count = 20)
{
var customers = await _customerService.GetCustomers(page, count);
if(Response.WillUseContentNegotiation)
{
return Ok(customers.Select(c => new { c.Id, c.Name }));
}
else
{
return View(customers.Select(c => new { c.Id, c.Name }));
}
}
Ответы
Ответ 1
Я не пробовал это, но вы могли бы просто проверить этот тип контента в запросе и соответственно возвратить:
var result = customers.Select(c => new { c.Id, c.Name });
if (Request.Headers["Accept"].Contains("application/json"))
return Json(result);
else
return View(result);
Ответ 2
Я думаю, что это разумный вариант использования, поскольку он упростит создание API, которые возвращают как HTML, так и JSON/XML/etc из одного контроллера. Это обеспечило бы прогрессивное улучшение, а также ряд других преимуществ, хотя это может не сработать в случаях, когда поведение API и Mvc должно быть существенно иным.
Я сделал это с помощью настраиваемого фильтра с некоторыми оговорками ниже:
public class ViewIfAcceptHtmlAttribute : Attribute, IActionFilter
{
public void OnActionExecuted(ActionExecutedContext context)
{
if (context.HttpContext.Request.Headers["Accept"].ToString().Contains("text/html"))
{
var originalResult = context.Result as ObjectResult;
var controller = context.Controller as Controller;
if(originalResult != null && controller != null)
{
var model = originalResult.Value;
var newResult = controller.View(model);
newResult.StatusCode = originalResult.StatusCode;
context.Result = newResult;
}
}
}
public void OnActionExecuting(ActionExecutingContext context)
{
}
}
который может быть добавлен к контроллеру или действию:
[ViewIfAcceptHtml]
[Route("/foo/")]
public IActionResult Get(){
return Ok(new Foo());
}
или зарегистрирован глобально в Startup.cs
services.AddMvc(x=>
{
x.Filters.Add(new ViewIfAcceptHtmlAttribute());
});
Это работает для моего использования и выполняет задачу поддержки text/html и application/json от одного и того же контроллера. Я подозреваю, что это не "лучший" подход, поскольку он дополняет пользовательские форматы. В идеале (на мой взгляд), этот код будет просто другим Formatter, таким как Xml и Json, но это выводит Html с помощью механизма просмотра View. Однако этот интерфейс немного запутан, и это было самое простое, что работает сейчас.
Ответ 3
Мне понравилась идея Даниэля и была вдохновлена, поэтому здесь также был основан подход на основе конвенции. Поскольку часто ViewModel должен включать в себя немного больше "материала", чем просто необработанные данные, возвращаемые из API, и ему также может потребоваться проверить разные вещи до того, как он выполнит свою работу, это позволит это и поможет в использовании ViewModel для каждого принципала просмотра. Используя это соглашение, вы можете написать два метода контроллера <Action>
и <Action>View
, оба из которых будут отображаться на один и тот же маршрут. Применяемое ограничение выберет <Action>View
, если в заголовке Accept содержится текст /html.
public class ContentNegotiationConvention : IActionModelConvention
{
public void Apply(ActionModel action)
{
if (action.ActionName.ToLower().EndsWith("view"))
{
//Make it match to the action of the same name without 'view', exa: IndexView => Index
action.ActionName = action.ActionName.Substring(0, action.ActionName.Length - 4);
foreach (var selector in action.Selectors)
//Add a constraint which will choose this action over the API action when the content type is apprpriate
selector.ActionConstraints.Add(new TextHtmlContentTypeActionConstraint());
}
}
}
public class TextHtmlContentTypeActionConstraint : ContentTypeActionConstraint
{
public TextHtmlContentTypeActionConstraint() : base("text/html") { }
}
public class ContentTypeActionConstraint : IActionConstraint, IActionConstraintMetadata
{
string _contentType;
public ContentTypeActionConstraint(string contentType)
{
_contentType = contentType;
}
public int Order => -10;
public bool Accept(ActionConstraintContext context) =>
context.RouteContext.HttpContext.Request.Headers["Accept"].ToString().Contains(_contentType);
}
который добавляется при запуске здесь:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(o => { o.Conventions.Add(new ContentNegotiationConvention()); });
}
В контроллере вы можете написать пары методов, например:
public class HomeController : Controller
{
public ObjectResult Index()
{
//General checks
return Ok(new IndexDataModel() { Property = "Data" });
}
public ViewResult IndexView()
{
//View specific checks
return View(new IndexViewModel(Index()));
}
}
Где я создал классы ViewModel, предназначенные для вывода результатов действий API, другой шаблон, который соединяет API с выходом View и усиливает намерение, что эти два представляют одно и то же действие:
public class IndexViewModel : ViewModelBase
{
public string ViewOnlyProperty { get; set; }
public string ExposedDataModelProperty { get; set; }
public IndexViewModel(IndexDataModel model) : base(model)
{
ExposedDataModelProperty = model?.Property;
ViewOnlyProperty = ExposedDataModelProperty + " for a View";
}
public IndexViewModel(ObjectResult apiResult) : this(apiResult.Value as IndexDataModel) { }
}
public class ViewModelBase
{
protected ApiModelBase _model;
public ViewModelBase(ApiModelBase model)
{
_model = model;
}
}
public class ApiModelBase { }
public class IndexDataModel : ApiModelBase
{
public string Property { get; internal set; }
}