| File: ModelBinding\Binders\FormFileModelBinder.cs | Web Access |
| Project: src\aspnetcore\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); } }