File: ModelBinding\Binders\FormFileModelBinder.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 System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Logging;
 
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
 
/// <summary>
/// <see cref="IModelBinder"/> implementation to bind posted files to <see cref="IFormFile"/>.
/// </summary>
public partial class FormFileModelBinder : IModelBinder
{
    private readonly ILogger _logger;
 
    /// <summary>
    /// Initializes a new instance of <see cref="FormFileModelBinder"/>.
    /// </summary>
    /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
    public FormFileModelBinder(ILoggerFactory loggerFactory)
    {
        ArgumentNullException.ThrowIfNull(loggerFactory);
 
        _logger = loggerFactory.CreateLogger(typeof(FormFileModelBinder));
    }
 
    /// <inheritdoc />
    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        ArgumentNullException.ThrowIfNull(bindingContext);
 
        _logger.AttemptingToBindModel(bindingContext);
 
        var createFileCollection = bindingContext.ModelType == typeof(IFormFileCollection);
        if (!createFileCollection && !ModelBindingHelper.CanGetCompatibleCollection<IFormFile>(bindingContext))
        {
            // Silently fail if unable to create an instance or use the current instance.
            return;
        }
 
        ICollection<IFormFile> postedFiles;
        if (createFileCollection)
        {
            postedFiles = new List<IFormFile>();
        }
        else
        {
            postedFiles = ModelBindingHelper.GetCompatibleCollection<IFormFile>(bindingContext);
        }
 
        // If we're at the top level, then use the FieldName (parameter or property name).
        // This handles the fact that there will be nothing in the ValueProviders for this parameter
        // and so we'll do the right thing even though we 'fell-back' to the empty prefix.
        var modelName = bindingContext.IsTopLevelObject
            ? bindingContext.BinderModelName ?? bindingContext.FieldName
            : bindingContext.ModelName;
 
        await GetFormFilesAsync(modelName, bindingContext, postedFiles);
 
        // If ParameterBinder incorrectly overrode ModelName, fall back to OriginalModelName prefix. Comparisons
        // are tedious because e.g. top-level parameter or property is named Blah and it contains a BlahBlah
        // property. OriginalModelName may be null in tests.
        if (postedFiles.Count == 0 &&
            bindingContext.OriginalModelName != null &&
            !string.Equals(modelName, bindingContext.OriginalModelName, StringComparison.Ordinal) &&
            !modelName.StartsWith(bindingContext.OriginalModelName + "[", StringComparison.Ordinal) &&
            !modelName.StartsWith(bindingContext.OriginalModelName + ".", StringComparison.Ordinal))
        {
            modelName = ModelNames.CreatePropertyModelName(bindingContext.OriginalModelName, modelName);
            await GetFormFilesAsync(modelName, bindingContext, postedFiles);
        }
 
        object value;
        if (bindingContext.ModelType == typeof(IFormFile))
        {
            if (postedFiles.Count == 0)
            {
                // Silently fail if the named file does not exist in the request.
                _logger.DoneAttemptingToBindModel(bindingContext);
                return;
            }
 
            value = postedFiles.First();
        }
        else
        {
            if (postedFiles.Count == 0 && !bindingContext.IsTopLevelObject)
            {
                // Silently fail if no files match. Will bind to an empty collection (treat empty as a success
                // case and not reach here) if binding to a top-level object.
                _logger.DoneAttemptingToBindModel(bindingContext);
                return;
            }
 
            // Perform any final type mangling needed.
            var modelType = bindingContext.ModelType;
            if (modelType == typeof(IFormFile[]))
            {
                Debug.Assert(postedFiles is List<IFormFile>);
                value = ((List<IFormFile>)postedFiles).ToArray();
            }
            else if (modelType == typeof(IFormFileCollection))
            {
                Debug.Assert(postedFiles is List<IFormFile>);
                value = new FileCollection((List<IFormFile>)postedFiles);
            }
            else
            {
                value = postedFiles;
            }
        }
 
        // We need to add a ValidationState entry because the modelName might be non-standard. Otherwise
        // the entry we create in model state might not be marked as valid.
        bindingContext.ValidationState.Add(value, new ValidationStateEntry()
        {
            Key = modelName,
        });
 
        bindingContext.ModelState.SetModelValue(
            modelName,
            rawValue: null,
            attemptedValue: null);
 
        bindingContext.Result = ModelBindingResult.Success(value);
        _logger.DoneAttemptingToBindModel(bindingContext);
    }
 
    private async Task GetFormFilesAsync(
        string modelName,
        ModelBindingContext bindingContext,
        ICollection<IFormFile> postedFiles)
    {
        var request = bindingContext.HttpContext.Request;
        if (request.HasFormContentType)
        {
            var form = await request.ReadFormAsync();
 
            foreach (var file in form.Files)
            {
                // If there is an <input type="file" ... /> in the form and is left blank.
                if (file.Length == 0 && string.IsNullOrEmpty(file.FileName))
                {
                    continue;
                }
 
                if (file.Name.Equals(modelName, StringComparison.OrdinalIgnoreCase))
                {
                    postedFiles.Add(file);
                }
            }
 
            if (postedFiles.Count == 0)
            {
                Log.NoFilesFoundInRequest(_logger);
            }
        }
        else
        {
            _logger.CannotBindToFilesCollectionDueToUnsupportedContentType(bindingContext);
        }
    }
 
    private sealed class FileCollection : ReadOnlyCollection<IFormFile>, IFormFileCollection
    {
        public FileCollection(List<IFormFile> list)
            : base(list)
        {
        }
 
        public IFormFile? this[string name] => GetFile(name);
 
        public IFormFile? GetFile(string name)
        {
            for (var i = 0; i < Items.Count; i++)
            {
                var file = Items[i];
                if (string.Equals(name, file.Name, StringComparison.OrdinalIgnoreCase))
                {
                    return file;
                }
            }
 
            return null;
        }
 
        public IReadOnlyList<IFormFile> GetFiles(string name)
        {
            var files = new List<IFormFile>();
            for (var i = 0; i < Items.Count; i++)
            {
                var file = Items[i];
                if (string.Equals(name, file.Name, StringComparison.OrdinalIgnoreCase))
                {
                    files.Add(file);
                }
            }
 
            return files;
        }
    }
 
    private static partial class Log
    {
        [LoggerMessage(21, LogLevel.Debug, "No files found in the request to bind the model to.", EventName = "NoFilesFoundInRequest")]
        public static partial void NoFilesFoundInRequest(ILogger logger);
    }
}