File: ModelBinding\Binders\KeyValuePairModelBinder.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.
 
#nullable enable
 
using Microsoft.Extensions.Logging;
 
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
 
/// <summary>
/// An <see cref="IModelBinder"/> for <see cref="KeyValuePair{TKey, TValue}"/>.
/// </summary>
/// <typeparam name="TKey">The key type.</typeparam>
/// <typeparam name="TValue">The value type.</typeparam>
public class KeyValuePairModelBinder<TKey, TValue> : IModelBinder
{
    private readonly IModelBinder _keyBinder;
    private readonly IModelBinder _valueBinder;
    private readonly ILogger _logger;
 
    /// <summary>
    /// Creates a new <see cref="KeyValuePair{TKey, TValue}"/>.
    /// </summary>
    /// <param name="keyBinder">The <see cref="IModelBinder"/> for <typeparamref name="TKey"/>.</param>
    /// <param name="valueBinder">The <see cref="IModelBinder"/> for <typeparamref name="TValue"/>.</param>
    /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
    public KeyValuePairModelBinder(IModelBinder keyBinder, IModelBinder valueBinder, ILoggerFactory loggerFactory)
    {
        ArgumentNullException.ThrowIfNull(keyBinder);
        ArgumentNullException.ThrowIfNull(valueBinder);
        ArgumentNullException.ThrowIfNull(loggerFactory);
 
        _keyBinder = keyBinder;
        _valueBinder = valueBinder;
        _logger = loggerFactory.CreateLogger(typeof(KeyValuePairModelBinder<TKey, TValue>));
    }
 
    /// <inheritdoc />
    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        ArgumentNullException.ThrowIfNull(bindingContext);
 
        _logger.AttemptingToBindModel(bindingContext);
 
        var keyModelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, "Key");
        var keyResult = await KeyValuePairModelBinder<TKey, TValue>.TryBindStrongModel<TKey?>(bindingContext, _keyBinder, "Key", keyModelName);
 
        var valueModelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, "Value");
        var valueResult = await KeyValuePairModelBinder<TKey, TValue>.TryBindStrongModel<TValue?>(bindingContext, _valueBinder, "Value", valueModelName);
 
        if (keyResult.IsModelSet && valueResult.IsModelSet)
        {
            var model = new KeyValuePair<TKey?, TValue?>(
                ModelBindingHelper.CastOrDefault<TKey?>(keyResult.Model),
                ModelBindingHelper.CastOrDefault<TValue?>(valueResult.Model));
 
            bindingContext.Result = ModelBindingResult.Success(model);
            _logger.DoneAttemptingToBindModel(bindingContext);
            return;
        }
 
        if (!keyResult.IsModelSet && valueResult.IsModelSet)
        {
            bindingContext.ModelState.TryAddModelError(
                keyModelName,
                bindingContext.ModelMetadata.ModelBindingMessageProvider.MissingKeyOrValueAccessor());
            _logger.DoneAttemptingToBindModel(bindingContext);
            return;
        }
 
        if (keyResult.IsModelSet && !valueResult.IsModelSet)
        {
            bindingContext.ModelState.TryAddModelError(
                valueModelName,
                bindingContext.ModelMetadata.ModelBindingMessageProvider.MissingKeyOrValueAccessor());
            _logger.DoneAttemptingToBindModel(bindingContext);
            return;
        }
 
        // If we failed to find data for a top-level model, then generate a
        // default 'empty' model and return it.
        if (bindingContext.IsTopLevelObject)
        {
            var model = new KeyValuePair<TKey?, TValue?>();
            bindingContext.Result = ModelBindingResult.Success(model);
        }
        _logger.DoneAttemptingToBindModel(bindingContext);
    }
 
    internal static async Task<ModelBindingResult> TryBindStrongModel<TModel>(
        ModelBindingContext bindingContext,
        IModelBinder binder,
        string propertyName,
        string propertyModelName)
    {
        var propertyModelMetadata = bindingContext.ModelMetadata.Properties[propertyName]!;
 
        using (bindingContext.EnterNestedScope(
            modelMetadata: propertyModelMetadata,
            fieldName: propertyName,
            modelName: propertyModelName,
            model: null))
        {
            await binder.BindModelAsync(bindingContext);
 
            return bindingContext.Result;
        }
    }
}