File: ModelBinding\Binders\TryParseModelBinder.cs
Web Access
Project: src\src\Mvc\Mvc.Core\src\Microsoft.AspNetCore.Mvc.Core.csproj (Microsoft.AspNetCore.Mvc.Core)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.Extensions.Logging;
 
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
 
/// <summary>
/// An <see cref="IModelBinder"/> for simple types.
/// </summary>
internal sealed class TryParseModelBinder : IModelBinder
{
    private static readonly MethodInfo AddModelErrorMethod = typeof(TryParseModelBinder).GetMethod(nameof(AddModelError), BindingFlags.NonPublic | BindingFlags.Static)!;
    private static readonly MethodInfo SuccessBindingResultMethod = typeof(ModelBindingResult).GetMethod(nameof(ModelBindingResult.Success), BindingFlags.Public | BindingFlags.Static)!;
    private static readonly ParameterExpression BindingContextExpression = Expression.Parameter(typeof(ModelBindingContext), "bindingContext");
    private static readonly ParameterExpression ValueProviderResultExpression = Expression.Parameter(typeof(ValueProviderResult), "valueProviderResult");
    private static readonly MemberExpression BindingResultExpression = Expression.Property(BindingContextExpression, nameof(ModelBindingContext.Result));
    private static readonly MemberExpression ValueExpression = Expression.Property(ValueProviderResultExpression, nameof(ValueProviderResult.FirstValue));
    private static readonly MemberExpression CultureExpression = Expression.Property(ValueProviderResultExpression, nameof(ValueProviderResult.Culture));
 
    private readonly Func<ValueProviderResult, ModelBindingContext, object?> _tryParseOperation;
    private readonly ILogger _logger;
 
    /// <summary>
    /// Initializes a new instance of <see cref="SimpleTypeModelBinder"/>.
    /// </summary>
    /// <param name="modelType">The model type.</param>
    /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
    public TryParseModelBinder(Type modelType, ILoggerFactory loggerFactory)
    {
        ArgumentNullException.ThrowIfNull(modelType);
        ArgumentNullException.ThrowIfNull(loggerFactory);
 
        _tryParseOperation = CreateTryParseOperation(modelType);
        _logger = loggerFactory.CreateLogger(typeof(TryParseModelBinder));
    }
 
    /// <inheritdoc />
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        ArgumentNullException.ThrowIfNull(bindingContext);
 
        _logger.AttemptingToBindModel(bindingContext);
 
        var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (valueProviderResult == ValueProviderResult.None)
        {
            _logger.FoundNoValueInRequest(bindingContext);
 
            // no entry
            _logger.DoneAttemptingToBindModel(bindingContext);
            return Task.CompletedTask;
        }
 
        bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
 
        try
        {
            var value = valueProviderResult.FirstValue;
            if (string.IsNullOrWhiteSpace(value))
            {
                // Most TryParse() methods trim the value and fail if the result is empty.
 
                // When converting 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 (!bindingContext.ModelMetadata.IsReferenceOrNullableType)
                {
                    bindingContext.ModelState.TryAddModelError(
                        bindingContext.ModelName,
                        bindingContext.ModelMetadata.ModelBindingMessageProvider.ValueMustNotBeNullAccessor(
                            valueProviderResult.ToString()));
                }
                else
                {
                    bindingContext.Result = ModelBindingResult.Success(null);
                }
            }
            else
            {
                _tryParseOperation(valueProviderResult, bindingContext);
            }
        }
        catch (Exception exception)
        {
            // Conversion failed.
            AddModelError(bindingContext, exception);
        }
 
        _logger.DoneAttemptingToBindModel(bindingContext);
        return Task.CompletedTask;
    }
 
    private static void AddModelError(ModelBindingContext bindingContext, Exception exception)
    {
        // Conversion failed.
        bindingContext.ModelState.TryAddModelError(
            bindingContext.ModelName,
            exception,
            bindingContext.ModelMetadata);
    }
 
    private static Func<ValueProviderResult, ModelBindingContext, object?> CreateTryParseOperation(Type modelType)
    {
        modelType = Nullable.GetUnderlyingType(modelType) ?? modelType;
        var tryParseMethodExpession = ModelMetadata.FindTryParseMethod(modelType)
            ?? throw new InvalidOperationException(Resources.FormatTryParseModelBinder_InvalidType(modelType, nameof(TryParseModelBinder)));
 
        // var tempSourceString = valueProviderResult.FirstValue;
        // object model = null;
        // if ([modeltype].TryParse(tempSourceString, [valueProviderResult.Culture,] out [modelType] parsedValue))
        // {
        //     model = (object)parsedValue;
        //     bindingContext.Result = ModelBindingResult.Success(model);
        // }
        // else
        // {
        //     AddModelError(bindingContext, new FormatException());
        // }
        // return model;
 
        var parsedValue = Expression.Variable(modelType, "parsedValue");
        var modelValue = Expression.Variable(typeof(object), "model");
 
        var expression = Expression.Block(
            new[] { parsedValue, modelValue, ParameterBindingMethodCache.SharedExpressions.TempSourceStringExpr },
            Expression.Assign(ParameterBindingMethodCache.SharedExpressions.TempSourceStringExpr, ValueExpression),
            Expression.IfThenElse(tryParseMethodExpession(parsedValue, CultureExpression),
                Expression.Block(
                    Expression.Assign(modelValue, Expression.Convert(parsedValue, modelValue.Type)),
                    Expression.Assign(BindingResultExpression, Expression.Call(SuccessBindingResultMethod, modelValue))),
                Expression.Call(AddModelErrorMethod, BindingContextExpression, Expression.Constant(new FormatException()))),
            modelValue);
 
        return Expression.Lambda<Func<ValueProviderResult, ModelBindingContext, object?>>(expression, new[] { ValueProviderResultExpression, BindingContextExpression }).Compile();
    }
}