我可以使用内容协商将视图返回到ASP.NET核心中的浏览器和JSON到API调用吗?

我有一个非常基本的控制器方法,它返回一个客户列表。 我希望它在用户浏览时返回列表视图,并将JSON返回给Accept标头中具有application/json请求。

这可能在ASP.NET Core MVC 1.0中吗?

我试过这个:

  [HttpGet("")] public async Task 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列表中。 如果我在浏览器中点击“/ customers”,我会得到JSON输出,而不是我的视图。

我想我可能需要编写一个处理text/html的OutputFormatter,但我无法弄清楚如何从OutputFormatter调用View()方法,因为那些方法在Controller ,我需要知道我要渲染的视图的名称。

有没有我可以调用的方法或属性来检查MVC是否能够找到要呈现的OutputFormatter ? 类似于以下内容:

 [HttpGet("")] public async Task 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 })); } } 

我认为这是一个合理的用例,因为它可以简化创建从单个控制器返回HTML和JSON / XML /等的API。 这将允许渐进增强,以及其他一些好处,但在API和Mvc行为需要完全不同的情况下可能无法正常工作。

我已经使用自定义filter完成了此操作,下面有一些注意事项:

 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的目标。 我怀疑这不是“最好”的方法,因为它可以自定义格式化程序。 理想情况下(在我看来),这段代码只是另一种格式化程序,如Xml和Json,但它使用View渲染引擎输出Html。 但是,这个界面更加复杂,这是目前最简单的方法。

我没有试过这个,但你可以在请求中测试该内容类型并相应地返回:

  var result = customers.Select(c => new { c.Id, c.Name }); if (Request.Headers["Accept"].Contains("application/json")) return Json(result); else return View(result); 

我喜欢丹尼尔的想法并感受到了灵感,所以这里也是一种基于会议的方法。 因为ViewModel通常需要包含比从API返回的原始数据更多的“东西”,并且它还可能需要在它工作之前检查不同的东西,这将允许并帮助遵循ViewModel对于每个View主体。 使用此约定,您可以编写两个控制器方法View ,这两个方法将映射到同一路径。 如果“text / html”在Accept标头中,则应用的约束将选择View

 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; } }