【问题标题】:Can I use Content Negotiation to return a View to browers and JSON to API calls in ASP.NET Core?我可以使用内容协商将视图返回给浏览器,并将 JSON 返回给 ASP.NET Core 中的 API 调用吗?
【发布时间】:2016-10-28 08:00:42
【问题描述】:

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

这在 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,即使它不在接受列表中。如果我在浏览器中点击“/customers”,我会得到 JSON 输出,而不是我的视图。

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

是否有我可以调用的方法或属性来检查 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 }));
    }
}

【问题讨论】:

  • 简而言之,是的,它可能在核心中,就像在普通 MVC 中一样。关于你的输出格式化程序,你能扩展一下吗
  • @SimonPrice 我在问题中添加了更多内容

标签: c# asp.net-mvc asp.net-core


【解决方案1】:

我认为这是一个合理的用例,因为它可以简化创建从单个控制器返回 HTML 和 JSON/XML/等的 API。这将允许渐进式增强以及其他一些好处,尽管在 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 的目标。我怀疑这不是“最佳”方法,因为它避开了自定义格式化程序。理想情况下(在我看来),这段代码只是另一个格式化程序,如 Xml 和 Json,但使用 View 渲染引擎输出 Html。不过,这个界面有点复杂,这是目前最简单的方法。

【讨论】:

    【解决方案2】:

    我还没有尝试过,但是您能否在请求中测试该内容类型并相应地返回:

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

    【讨论】:

    • 这不会遵守 Accept 标头优先规则,但它可能没问题。
    【解决方案3】:

    我喜欢 Daniel 的想法并受到启发,因此这里也是一种基于约定的方法。因为通常 ViewModel 需要包含比从 API 返回的原始数据多一点的“东西”,而且它还可能需要在工作之前检查不同的东西,这将允许这样做并有助于遵循 ViewModel对于每个 View 主体。使用这个约定,您可以编写两个控制器方法&lt;Action&gt;&lt;Action&gt;View,它们都将映射到相同的路由。如果“text/html”在 Accept 标头中,则应用的约束将选择 &lt;Action&gt;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 连接到视图输出并强化这两者表示相同操作的意图的模式:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-03-22
      • 2017-07-11
      • 2021-11-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-03-13
      相关资源
      最近更新 更多