这里的简短回答是可以。为了实现这一点,您将需要使用鉴别器值并使用 ModelBinders。
在 API 上不使用版本的一种方法是通过 Content-Types。您可以随着时间的推移保持相同的端点并添加多个 Content-Type,从而允许您在不破坏现有功能的情况下扩展 API。想象一下,您的初始 api 仅具有一个使用 application.json 的端点,后来获得了接受新功能但保持旧集成到位的新要求。在这种情况下,您将使用一个 Content-Type: application.json 来访问您的方法,而无需来自正文的参数,而另一个 Content-Type: application+body.json 来访问另一个。对于额外的,您需要在 Startup 上注册它,以便请求可以从标头中读取它。然后在您的 ModelBinder 上,您将决定它绑定到哪个模型。最重要的是,您可以实现一个 ActionConstraint 以仅根据 Content-Type 访问您需要的端点。
以下是创建 .Net Core API 时默认提供的 WeatherForecast 示例。
正如您在下面的示例中看到的,我使用 2 个不同的 Dto 重载端点,模型绑定器考虑了 Content-Types 以将 Dto 绑定到正确的模型。
ActionContraint 限制请求命中端点
C# 允许您重载一个方法,但是使用 .NetCore 路由,如果它们保持相同的名称,您将没有使用相同的 ActionMethods 的那种灵活性,您将得到一个异常。这就是为什么您需要为您计划通过的每个 Dto 创建 2 种不同的方法。在您的情况下,您有一个为空的方法和另一个接收参数的方法,所以这是一个更简单的方案。
在 Startup.cs
上
public void ConfigureServices(IServiceCollection services)
{
var iMvcBuilder = services.AddControllers(mvcOptions =>
{
//mvcOptions.EnableEndpointRouting = false;
mvcOptions.ModelBinderProviders.Insert(0, new WeatherForecastForCreationModelBinderProvider());
var jsonOutputFormatter = mvcOptions.OutputFormatters.OfType<SystemTextJsonOutputFormatter>()
.FirstOrDefault();
//TODO: test to remove this, no need to have this if you put the correct Produces or Consumes attributes
if (jsonOutputFormatter != null)
{
jsonOutputFormatter.SupportedMediaTypes.Add("application/vnd.weatherforecast.type1.json");
jsonOutputFormatter.SupportedMediaTypes.Add("application/vnd.weatherforecast.type2.json);
}
})
模型绑定器WeatherForecastForCreationModelBinderProvider
public class WeatherForecastForCreationModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context.Metadata.ModelType == typeof(WeatherForecastForCreationDto1) || context.Metadata.ModelType == typeof(WeatherForecastForCreationDto2))
{
return new WeatherForecastForCreationModelBinder();
}
return null;
}
}
模型绑定器WeatherForecastForCreationModelBinder
public class WeatherForecastForCreationModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
string valueFromBody = string.Empty;
using (var sr = new StreamReader(bindingContext.HttpContext.Request.Body))
{
valueFromBody = sr.ReadToEndAsync().GetAwaiter().GetResult();
}
if (string.IsNullOrEmpty(valueFromBody))
{
return Task.CompletedTask;
}
string contentType = bindingContext.HttpContext.Request.Headers.FirstOrDefault(x => x.Key == HeaderNames.ContentType).Value;
dynamic model = null;
var isDeserializingOk = true;
switch (contentType)
{
case "application/vnd.weatherforecast.type1.json":
try
{
model = JsonConvert.DeserializeObject<WeatherForecastForCreationDto1>(valueFromBody);
}
catch
{
isDeserializingOk = false;
}
break;
case "application/vnd.weatherforecast.type2.json":
try
{
model = JsonConvert.DeserializeObject<WeatherForecastForCreationDto2>(valueFromBody);
}
catch
{
isDeserializingOk = false;
}
break;
}
if (!isDeserializingOk)
{
bindingContext.Result = ModelBindingResult.Failed();
return Task.CompletedTask;
}
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
控制器方法:
/// <summary>
/// Create Weather Forecast
/// </summary>
/// <param name="weatherForecast"></param>
/// <returns></returns>
[HttpPost(Name = "CreateWeatherForecast")]
[ProducesResponseType(201)]
[Consumes(HttpMediaTypes.WeatherForecastType1)]
[RequestHeaderMatchesMediaType("Content-Type", new[] { HttpMediaTypes.WeatherForecastType1 })]
public async Task<IActionResult> CreateWeatherForecast1(
[FromBody] WeatherForecastForCreationDto1 weatherForecastForCreationDto1,
[FromServices] IWeatherForcastService weatherForcastService,
[FromServices] IMapper mapper)
{
if (!ModelState.IsValid)
{
return ValidationFailureRequest();
}
var entity = await weatherForcastService.CreateSummary1Async(weatherForecastForCreationDto1);
var outputDto = mapper.Map<WeatherForecastDto1>(entity);
return StatusCode(StatusCodes.Status201Created, outputDto);
}
[HttpPost]
[ProducesResponseType(201)]
[Consumes(HttpMediaTypes.WeatherForecastType2)]
[RequestHeaderMatchesMediaType("Content-Type", new[] { HttpMediaTypes.WeatherForecastType2 })]
public async Task<IActionResult> CreateWeatherForecast2(
[FromBody] WeatherForecastForCreationDto2 weatherForecastForCreationDto2,
[FromServices] IWeatherForcastService weatherForcastService,
[FromServices] IMapper mapper)
{
if (!ModelState.IsValid)
{
return ValidationFailureRequest();
}
var entity = await weatherForcastService.CreateSummary2Async(weatherForecastForCreationDto2);
var outputDto = mapper.Map<WeatherForecastDto2>(entity);
return StatusCode(StatusCodes.Status201Created, outputDto);
}
DTO:WeatherForecastForCreationDto1 和 WeatherForecastForCreationDto2
public class WeatherForecastForCreationDto1
{
/// <summary>
/// Date
/// </summary>
/// <example>
/// <code>
/// DateTime.Now;
/// </code>
/// </example>
public DateTime Date { get; set; }
/// <summary>
/// Temperature C
/// </summary>
/// <example>
/// 14
/// </example>
public int TemperatureC { get; set; }
/// <summary>
/// Summary
/// </summary>
/// <example>
/// Scorching
/// </example>
public string Summary { get; set; }
}
public class WeatherForecastForCreationDto2
{
/// <summary>
/// Date
/// </summary>
/// <example>
/// <code>
/// DateTime.Now;
/// </code>
/// </example>
public DateTime Date { get; set; }
/// <summary>
/// Temperature C
/// </summary>
/// <example>
/// 35
/// </example>
public int TemperatureC { get; set; }
}
这是你的 ActionContraint RequestHeaderMatchesMediaTypeAttribute
[AttributeUsage(AttributeTargets.All, Inherited = true, AllowMultiple = true)]
public class RequestHeaderMatchesMediaTypeAttribute : Attribute, IActionConstraint
{
private readonly string[] _mediaTypes;
private readonly string _requestHeaderToMatch;
public RequestHeaderMatchesMediaTypeAttribute(string requestHeaderToMatch,
string[] mediaTypes)
{
_requestHeaderToMatch = requestHeaderToMatch;
_mediaTypes = mediaTypes;
}
public int Order => 0;
public string RequestHeaderToMatch => _requestHeaderToMatch;
public string[] MediaTypes => _mediaTypes;
public bool Accept(ActionConstraintContext context)
{
var requestHeaders = context.RouteContext.HttpContext.Request.Headers;
if (!requestHeaders.ContainsKey(_requestHeaderToMatch))
{
return false;
}
// if one of the media types matches, return true
foreach (var mediaType in _mediaTypes)
{
var mediaTypeMatches = string.Equals(requestHeaders[_requestHeaderToMatch].ToString(),
mediaType, StringComparison.OrdinalIgnoreCase);
if (mediaTypeMatches)
{
return true;
}
}
return false;
}
}