asp net core controller datetime参数绕过模型有效检查

aspnet core controller datetime parameter bypasses modelvalid check

我有很多这种形式的控制器:

public IActionResult GetSomething(int id, DateTime from) {
    ...
}

idfrom 参数在查询中作为查询参数给出。如果未提供 id,则 ModelValid 状态设置为 false。但是,如果未提供 from,则 ModelValid 为真且 from 设置为 1900-01-01 00:00:00 (DateTime.Min).

如果未提供所需的 DateTime 参数,如何使 ModelState 为假?

您可以通过创建一个具有 'From' 属性 验证属性的模型来解决此问题。 我还没有测试过代码。但代码应该是这样的:

public class Model
{
    public int Id { get; set; }
    [DateTimeShouldHaveValue]
    public DateTime From { get; set; }
}

public class DateTimeShouldHaveValueAttribute : ValidationAttribute
{
    public override bool IsValid(object value)
    {
        if (value == null)
            return false;

        var dateTimeTmp = value.ToString();
        DateTime dateTime;
        DateTime.TryParse(dateTimeTmp, out dateTime);
        if (dateTime == DateTime.MinValue)
            return false;

        return true;
    }
}

public IActionResult GetSomething(Model model)
{

}

我决定去实施 DateTime 模型活页夹。如果缺少 DateTime 参数,以下代码将不会在 ModelState 上设置 IsValid=true。约会时间? (nullable DateTime) 处理得很好,但同样,如果缺少查询参数,IsValid 将设置为 false,而不是将参数设置为默认值。

首先是 DateTimeModelBinderProvider:

public class DateTimeModelBinderProvider : IModelBinderProvider
{
    /// <inheritdoc />
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (!context.Metadata.IsComplexType)
        {
            // We can handle DateTime and nullable DateTime
            if ((context.Metadata.ModelType == typeof(DateTime)) ||
                (context.Metadata.IsNullableValueType && context.Metadata.UnderlyingOrModelType == typeof(DateTime)))
                return new DateTimeModelBinder(context.Metadata.ModelType);
        }

        return null;
    }
}

接下来是 DateTimeModelBinder。大部分代码是从 github 逐字复制的。其中一些可能会被遗漏,但它会按原样工作:

public class DateTimeModelBinder : IModelBinder
{
    private readonly TypeConverter _typeConverter;

    public DateTimeModelBinder(Type type)
    {
        if (type == null)
        {
            throw new ArgumentNullException(nameof(type));
        }

        _typeConverter = TypeDescriptor.GetConverter(type);
    }

    /// <inheritdoc />
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (valueProviderResult == ValueProviderResult.None)
        {
            // Do not accept an empty value provider result as being ok for DateTime (is ok for DateTime?)
            bindingContext.ModelState.TryAddModelError(
                    bindingContext.ModelName,
                    bindingContext.ModelMetadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
                        valueProviderResult.ToString()));

            // no entry
            return Task.CompletedTask;
        }

        bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);

        try
        {
            var value = valueProviderResult.FirstValue;

            object model = null;
            if (!string.IsNullOrWhiteSpace(value))
            {
                model = new DateTimeConverter().ConvertFrom(
                    context: null, 
                    culture: valueProviderResult.Culture,
                    value: value);
            }

            if (bindingContext.ModelType == typeof(string))
            {
                var modelAsString = model as string;
                if (bindingContext.ModelMetadata.ConvertEmptyStringToNull &&
                    string.IsNullOrEmpty(modelAsString))
                {
                    model = null;
                }
            }

            // When converting newModel a null value may indicate a failed conversion for an otherwise required
            // model (can't set a ValueType to null). This detects if a null model value is acceptable given the
            // current bindingContext. If not, an error is logged.
            if (model == null && !bindingContext.ModelMetadata.IsReferenceOrNullableType)
            {
                bindingContext.ModelState.TryAddModelError(
                    bindingContext.ModelName,
                    bindingContext.ModelMetadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
                        valueProviderResult.ToString()));

                return Task.CompletedTask;
            }
            else
            {
                bindingContext.Result = ModelBindingResult.Success(model);
                return Task.CompletedTask;
            }
        }
        catch (Exception exception)
        {
            var isFormatException = exception is FormatException;
            if (!isFormatException && exception.InnerException != null)
            {
                // TypeConverter throws System.Exception wrapping the FormatException,
                // so we capture the inner exception.
                exception = ExceptionDispatchInfo.Capture(exception.InnerException).SourceException;
            }

            bindingContext.ModelState.TryAddModelError(
                bindingContext.ModelName,
                exception,
                bindingContext.ModelMetadata);

            // Were able to find a converter for the type but conversion failed.
            return Task.CompletedTask;
        }
    }
}

还有记得激活哦。我将它插入提供程序列表的开头,以确保优先使用我的 DateTime 提供程序而不是默认处理程序:

        var mvc = services.AddMvc(config => {
            config.ModelBinderProviders.Insert(0, new DateTimeModelBinderProvider());
        });