不是硬编码DTO的预期表单字段名称,是否可以在运行时动态/确定?
背景:我实现了一个webhook,它将使用form-url编码的数据调用(webhook将被调用的数据形状不受我的控制)。
目前我的控制器操作的签名如下所示:
public async Task<IActionResult> PerformSomeAction([FromForm]SomeWebhookRequestDto request)
DTO大部分都有如下所示的一系列属性:
[ModelBinder(Name = "some_property")]
[BindRequired]
public string SomeProperty { get; set; }
其中表单字段名称为&#34; some_property&#34;提前(永远不会改变)
但是对于某些属性,我想在运行时确定表单字段名称:
[ModelBinder(Name = "field[xxx]")]
[BindRequired]
public DateTime? AnotherProperty { get; set; }
请注意,xxx将替换为数字(将根据URL中的信息进行更改)。
请注意,如果可以的话,我宁愿避免使用自定义模型绑定器 - 看起来我应该能够挂钩IValueProvider
- 我已经做了这样的事情(添加了)一个IValueProviderFactory
,在0号注册) - 但似乎[FromForm]贪婪,所以我的IValueProvider(Factory)
永远不会有机会。
澄清一些观点:
答案 0 :(得分:2)
您打破了一些优秀的API设计规则,只是简单地设计一下。
首先,DTO的整个点都是以一种形式接受数据,因此您可以在另一种形式中操纵它。换句话说,如果您在不同的请求中有不同的数据,则每种类型的数据都应该有不同的DTO。
其次,API的重点在于它是一个应用程序编程接口。就像编程中的实际界面一样,它定义了一个契约。客户端必须以定义的格式发送数据,否则服务器会拒绝它。期。 API不负责接受客户决定发送并尝试使用它做任何事情的任何无所畏惧的数据;相反,遵守界面是客户的责任。
第三,如果您确实需要接受不同类型的数据,那么您的API需要额外的端点。每个端点都应该处理一个资源。客户端永远不应该向同一端点提交多种不同类型的资源。因此,不需要动态&#34;属性。
最后,如果情况只是所有数据都是针对相同的资源类型,但只有部分数据可以与任何给定的请求一起提交,那么您的DTO应该仍然包含所有数据潜在的属性。不要求在请求中提供所有可能的属性;模型绑定器将填补它所能做的。那么,您的操作应该接受HTTP方法PATCH,这通常意味着您只处理特定资源的一部分。
答案 1 :(得分:0)
通过删除[FromForm]
属性并实施IValueProvider
+ IValueProviderFactory
解决问题。
internal class CustomFieldFormValueProvider : IValueProvider
{
private static readonly Regex AliasedFieldValueRegex = new Regex("(?<prefix>.*)(?<fieldNameAlias>\\%.*\\%)$");
private readonly KeyValuePair<string, string>[] _customFields;
private readonly IRequestCustomFieldResolver _resolver;
private readonly ILogger _logger;
public CustomFieldFormValueProvider(IRequestCustomFieldResolver resolver, KeyValuePair<string, string>[] customFields) {
_resolver = resolver;
_customFields = customFields;
_logger = Log.ForContext(typeof(CustomFieldFormValueProvider));
}
public bool ContainsPrefix(string prefix) {
return AliasedFieldValueRegex.IsMatch(prefix);
}
public ValueProviderResult GetValue(string key) {
var match = AliasedFieldValueRegex.Match(key);
if (match.Success) {
var prefix = match.Groups["prefix"].Value;
var fieldNameAlias = match.Groups["fieldNameAlias"].Value;
// Unfortunately, IValueProvider::GetValue does not have an async variant :(
var customFieldNumber = Task.Run(() => _resolver.Resolve(fieldNameAlias)).Result;
var convertedKey = ConvertKey(prefix, customFieldNumber);
string customFieldValue = null;
try {
customFieldValue = _customFields.Single(pair => pair.Key.Equals(convertedKey, StringComparison.OrdinalIgnoreCase)).Value;
} catch (InvalidOperationException) {
_logger.Warning("Could not find a value for '{FieldNameAlias}' - (custom field #{CustomFieldNumber} - assuming null", fieldNameAlias, customFieldNumber);
}
return new ValueProviderResult(new StringValues(customFieldValue));
}
return ValueProviderResult.None;
}
private string ConvertKey(string prefix, int customFieldNumber) {
var path = prefix.Split('.')
.Where(part => !string.IsNullOrWhiteSpace(part))
.Concat(new[] {
"fields",
customFieldNumber.ToString()
})
.ToArray();
return path[0] + string.Join("", path.Skip(1).Select(part => $"[{part}]"));
}
}
public class CustomFieldFormValueProviderFactory : IValueProviderFactory
{
private static readonly Regex
CustomFieldRegex = new Regex(".*[\\[]]?fields[\\]]?[\\[]([0-9]+)[\\]]$");
public Task CreateValueProviderAsync(ValueProviderFactoryContext context) {
// Get the key/value pairs from the form which look like our custom fields
var customFields = context.ActionContext.HttpContext.Request.Form.Where(pair => CustomFieldRegex.IsMatch(pair.Key))
.Select(pair => new KeyValuePair<string, string>(pair.Key, pair.Value.First()))
.ToArray();
// Pull out the service we need
if (!(context.ActionContext.HttpContext.RequestServices.GetService(typeof(IRequestCustomFieldResolver)) is IRequestCustomFieldResolver resolver)) {
throw new InvalidOperationException($"No service of type {typeof(IRequestCustomFieldResolver).Name} available");
}
context.ValueProviders.Insert(0, new CustomFieldFormValueProvider(resolver, customFields));
return Task.CompletedTask;
}
}