在ASP .NET Core中的运行时设置模型绑定表单字段名称

问题描述 投票:0回答:2

而不是硬编码DTO的预期表单字段名称,它们是否可以在运行时动态/确定?

背景:我正在实现一个webhook,它将使用form-url编码的数据调用(webhook将被调用的数据的形状不受我的控制)。

目前我的控制器操作的签名如下所示:

public async Task<IActionResult> PerformSomeAction([FromForm]SomeWebhookRequestDto request)

DTO在很大程度上有很多属性,如下所示:

    [ModelBinder(Name = "some_property")]
    [BindRequired]
    public string SomeProperty { get; set; }

其中表单字段名称事先已知为“some_property”(永远不会更改)

但是对于某些属性,我想在运行时确定表单字段名称:

    [ModelBinder(Name = "field[xxx]")]
    [BindRequired]
    public DateTime? AnotherProperty { get; set; }

请注意,xxx将替换为数字(将根据URL中的信息进行更改)。

请注意,如果可以的话,我宁愿避免实现自定义模型绑定器 - 看起来我应该能够挂钩IValueProvider - 我已经去做了(添加了IValueProviderFactory,在0位注册) - 但它似乎[FromForm]贪婪,所以我的IValueProvider(Factory)永远不会有机会。

澄清一些观点:

  • 请求都具有相同的意图(它们都是请求我的API执行单个特定事务的请求)
  • 请求都具有相同的语义形状(假设有10个字段,所有10个字段必须填充该字段的有效数据 - 日期应该去的日期,字符串应该去的字符串)。字段值的含义也是一致的。
  • 对于必须在运行时确定名称的字段,字段名称将类似于“field [132]”或“field [130]”。这些字段的名称将取决于URL中提供的信息 - 我的API将执行查找以确定最终名称应该是什么。
  • 可能存在大量这些配置,因此为每个配置设置单独的端点是不可行的。
  • 虽然上述情况有点像噩梦,但如果没有拒绝参加演出,那就不可能了
c# asp.net-core model-binding
2个回答
2
投票

你破坏了几个良好的API设计规则,只是简单地设计一下。

首先,DTO的整个点都是以一种形式接受数据,因此您可以在另一种形式中操纵它。换句话说,如果您在不同的请求中有不同的数据,则每种类型的数据都应该有不同的DTO。

其次,API的重点在于它是一个应用程序编程接口。就像编程中的实际界面一样,它定义了一个契约。客户端必须以定义的格式发送数据,否则服务器会拒绝它。期。 API不负责接受客户决定发送并尝试使用它做任何事情的任何无所畏惧的数据;相反,客户有责任坚持界面。

第三,如果您确实需要接受不同类型的数据,那么您的API需要额外的端点。每个端点都应该处理一个资源。客户端永远不应该向同一端点提交多种不同类型的资源。因此,不应该需要“动态”属性。

最后,如果情况只是所有数据都是针对相同的资源类型,但只有部分数据可以与任何给定的请求一起提交,那么您的DTO应该仍然包含所有潜在的属性。不要求在请求中提供所有可能的属性;模型绑定器将填补它所能做的。那么,您的操作应该接受HTTP方法PATCH,它通过定义意味着您只处理特定资源的一部分。


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;
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.