Update `FormFileModelBinder` to re-add prefix `ParameterBinder` removed incorrectly
- #7562 part 2 - add `OriginalModelName` to `ModelBindingContext` nit: take VS suggestions, mostly to inline collection initialization in `FormFileModelBinderTest`
This commit is contained in:
parent
c13e2498a8
commit
47d6d4e82c
|
|
@ -67,6 +67,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
/// </summary>
|
||||
public abstract string ModelName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the top-level model. This is not reset to <see cref="string.Empty"/> when value
|
||||
/// providers have no match for that model.
|
||||
/// </summary>
|
||||
public string OriginalModelName { get; protected set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the <see cref="ModelStateDictionary"/> used to capture <see cref="ModelStateEntry"/> values
|
||||
/// for properties in the object graph of the model when binding.
|
||||
|
|
|
|||
|
|
@ -237,6 +237,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
|
|||
// Because this is the top-level context, FieldName and ModelName should be the same.
|
||||
FieldName = binderModelName ?? modelName,
|
||||
ModelName = binderModelName ?? modelName,
|
||||
OriginalModelName = binderModelName ?? modelName,
|
||||
|
||||
IsTopLevelObject = true,
|
||||
ModelMetadata = metadata,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
|
||||
_logger = loggerFactory.CreateLogger<FormFileModelBinder>();
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task BindModelAsync(ModelBindingContext bindingContext)
|
||||
{
|
||||
|
|
@ -85,6 +85,19 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
|
||||
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))
|
||||
{
|
||||
|
|
|
|||
|
|
@ -20,8 +20,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
public async Task FormFileModelBinder_SingleFile_BindSuccessful()
|
||||
{
|
||||
// Arrange
|
||||
var formFiles = new FormFileCollection();
|
||||
formFiles.Add(GetMockFormFile("file", "file1.txt"));
|
||||
var formFiles = new FormFileCollection
|
||||
{
|
||||
GetMockFormFile("file", "file1.txt")
|
||||
};
|
||||
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
||||
var bindingContext = GetBindingContext(typeof(IEnumerable<IFormFile>), httpContext);
|
||||
var binder = new FormFileModelBinder(NullLoggerFactory.Instance);
|
||||
|
|
@ -38,6 +40,192 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
Assert.Null(entry.Metadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FormFileModelBinder_SingleFileAtTopLevel_BindSuccessfully_WithEmptyModelName()
|
||||
{
|
||||
// Arrange
|
||||
var formFiles = new FormFileCollection
|
||||
{
|
||||
GetMockFormFile("file", "file1.txt")
|
||||
};
|
||||
|
||||
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
||||
var binder = new FormFileModelBinder(NullLoggerFactory.Instance);
|
||||
|
||||
// Mimic ParameterBinder overwriting ModelName on top level model. In this top-level binding case,
|
||||
// FormFileModelBinder uses FieldName from the get-go. (OriginalModelName will be checked but ignored.)
|
||||
var bindingContext = DefaultModelBindingContext.CreateBindingContext(
|
||||
new ActionContext { HttpContext = httpContext },
|
||||
Mock.Of<IValueProvider>(),
|
||||
new EmptyModelMetadataProvider().GetMetadataForType(typeof(IFormFile)),
|
||||
bindingInfo: null,
|
||||
modelName: "file");
|
||||
bindingContext.ModelName = string.Empty;
|
||||
|
||||
// Act
|
||||
await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(bindingContext.Result.IsModelSet);
|
||||
|
||||
var entry = bindingContext.ValidationState[bindingContext.Result.Model];
|
||||
Assert.False(entry.SuppressValidation);
|
||||
Assert.Equal("file", entry.Key);
|
||||
Assert.Null(entry.Metadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FormFileModelBinder_SingleFileWithinTopLevelPoco_BindSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
const string propertyName = nameof(NestedFormFiles.Files);
|
||||
var formFiles = new FormFileCollection
|
||||
{
|
||||
GetMockFormFile($"{propertyName}", "file1.txt")
|
||||
};
|
||||
|
||||
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
||||
var binder = new FormFileModelBinder(NullLoggerFactory.Instance);
|
||||
|
||||
// In this non-top-level binding case, FormFileModelBinder tries ModelName and succeeds.
|
||||
var propertyInfo = typeof(NestedFormFiles).GetProperty(propertyName);
|
||||
var metadata = new EmptyModelMetadataProvider().GetMetadataForProperty(
|
||||
propertyInfo,
|
||||
propertyInfo.PropertyType);
|
||||
var bindingContext = DefaultModelBindingContext.CreateBindingContext(
|
||||
new ActionContext { HttpContext = httpContext },
|
||||
Mock.Of<IValueProvider>(),
|
||||
metadata,
|
||||
bindingInfo: null,
|
||||
modelName: "FileList");
|
||||
bindingContext.IsTopLevelObject = false;
|
||||
bindingContext.Model = new FileList();
|
||||
bindingContext.ModelName = propertyName;
|
||||
|
||||
// Act
|
||||
await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(bindingContext.Result.IsModelSet);
|
||||
|
||||
var entry = bindingContext.ValidationState[bindingContext.Result.Model];
|
||||
Assert.False(entry.SuppressValidation);
|
||||
Assert.Equal($"{propertyName}", entry.Key);
|
||||
Assert.Null(entry.Metadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FormFileModelBinder_SingleFileWithinTopLevelPoco_BindSuccessfully_WithShortenedModelName()
|
||||
{
|
||||
// Arrange
|
||||
const string propertyName = nameof(NestedFormFiles.Files);
|
||||
var formFiles = new FormFileCollection
|
||||
{
|
||||
GetMockFormFile($"FileList.{propertyName}", "file1.txt")
|
||||
};
|
||||
|
||||
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
||||
var binder = new FormFileModelBinder(NullLoggerFactory.Instance);
|
||||
|
||||
// Mimic ParameterBinder overwriting ModelName on top level model then ComplexTypeModelBinder entering a
|
||||
// nested context for the NestedFormFiles property. In this non-top-level binding case, FormFileModelBinder
|
||||
// tries ModelName then falls back to add an (OriginalModelName + ".") prefix.
|
||||
var propertyInfo = typeof(NestedFormFiles).GetProperty(propertyName);
|
||||
var metadata = new EmptyModelMetadataProvider().GetMetadataForProperty(
|
||||
propertyInfo,
|
||||
propertyInfo.PropertyType);
|
||||
var bindingContext = DefaultModelBindingContext.CreateBindingContext(
|
||||
new ActionContext { HttpContext = httpContext },
|
||||
Mock.Of<IValueProvider>(),
|
||||
metadata,
|
||||
bindingInfo: null,
|
||||
modelName: "FileList");
|
||||
bindingContext.IsTopLevelObject = false;
|
||||
bindingContext.Model = new FileList();
|
||||
bindingContext.ModelName = propertyName;
|
||||
|
||||
// Act
|
||||
await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(bindingContext.Result.IsModelSet);
|
||||
|
||||
var entry = bindingContext.ValidationState[bindingContext.Result.Model];
|
||||
Assert.False(entry.SuppressValidation);
|
||||
Assert.Equal($"FileList.{propertyName}", entry.Key);
|
||||
Assert.Null(entry.Metadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FormFileModelBinder_SingleFileWithinTopLevelDictionary_BindSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var formFiles = new FormFileCollection
|
||||
{
|
||||
GetMockFormFile("[myFile]", "file1.txt")
|
||||
};
|
||||
|
||||
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
||||
var binder = new FormFileModelBinder(NullLoggerFactory.Instance);
|
||||
|
||||
// In this non-top-level binding case, FormFileModelBinder tries ModelName and succeeds.
|
||||
var bindingContext = DefaultModelBindingContext.CreateBindingContext(
|
||||
new ActionContext { HttpContext = httpContext },
|
||||
Mock.Of<IValueProvider>(),
|
||||
new EmptyModelMetadataProvider().GetMetadataForType(typeof(IFormFile)),
|
||||
bindingInfo: null,
|
||||
modelName: "FileDictionary");
|
||||
bindingContext.IsTopLevelObject = false;
|
||||
bindingContext.ModelName = "[myFile]";
|
||||
|
||||
// Act
|
||||
await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(bindingContext.Result.IsModelSet);
|
||||
|
||||
var entry = bindingContext.ValidationState[bindingContext.Result.Model];
|
||||
Assert.False(entry.SuppressValidation);
|
||||
Assert.Equal("[myFile]", entry.Key);
|
||||
Assert.Null(entry.Metadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FormFileModelBinder_SingleFileWithinTopLevelDictionary_BindSuccessfully_WithShortenedModelName()
|
||||
{
|
||||
// Arrange
|
||||
var formFiles = new FormFileCollection
|
||||
{
|
||||
GetMockFormFile("FileDictionary[myFile]", "file1.txt")
|
||||
};
|
||||
|
||||
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
||||
var binder = new FormFileModelBinder(NullLoggerFactory.Instance);
|
||||
|
||||
// Mimic ParameterBinder overwriting ModelName on top level model then DictionaryModelBinder entering a
|
||||
// nested context for the KeyValuePair.Value property. In this non-top-level binding case,
|
||||
// FormFileModelBinder tries ModelName then falls back to add an OriginalModelName prefix.
|
||||
var bindingContext = DefaultModelBindingContext.CreateBindingContext(
|
||||
new ActionContext { HttpContext = httpContext },
|
||||
Mock.Of<IValueProvider>(),
|
||||
new EmptyModelMetadataProvider().GetMetadataForType(typeof(IFormFile)),
|
||||
bindingInfo: null,
|
||||
modelName: "FileDictionary");
|
||||
bindingContext.IsTopLevelObject = false;
|
||||
bindingContext.ModelName = "[myFile]";
|
||||
|
||||
// Act
|
||||
await binder.BindModelAsync(bindingContext);
|
||||
|
||||
// Assert
|
||||
Assert.True(bindingContext.Result.IsModelSet);
|
||||
|
||||
var entry = bindingContext.ValidationState[bindingContext.Result.Model];
|
||||
Assert.False(entry.SuppressValidation);
|
||||
Assert.Equal("FileDictionary[myFile]", entry.Key);
|
||||
Assert.Null(entry.Metadata);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FormFileModelBinder_ExpectMultipleFiles_BindSuccessful()
|
||||
{
|
||||
|
|
@ -127,8 +315,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
public async Task FormFileModelBinder_ReturnsFailedResult_WhenNamesDoNotMatch()
|
||||
{
|
||||
// Arrange
|
||||
var formFiles = new FormFileCollection();
|
||||
formFiles.Add(GetMockFormFile("different name", "file1.txt"));
|
||||
var formFiles = new FormFileCollection
|
||||
{
|
||||
GetMockFormFile("different name", "file1.txt")
|
||||
};
|
||||
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
||||
var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
|
||||
var binder = new FormFileModelBinder(NullLoggerFactory.Instance);
|
||||
|
|
@ -147,9 +337,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
public async Task FormFileModelBinder_UsesFieldNameForTopLevelObject(bool isTopLevel, string expected)
|
||||
{
|
||||
// Arrange
|
||||
var formFiles = new FormFileCollection();
|
||||
formFiles.Add(GetMockFormFile("FieldName", "file1.txt"));
|
||||
formFiles.Add(GetMockFormFile("ModelName", "file1.txt"));
|
||||
var formFiles = new FormFileCollection
|
||||
{
|
||||
GetMockFormFile("FieldName", "file1.txt"),
|
||||
GetMockFormFile("ModelName", "file1.txt")
|
||||
};
|
||||
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
||||
|
||||
var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
|
||||
|
|
@ -173,8 +365,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
public async Task FormFileModelBinder_ReturnsFailedResult_WithEmptyContentDisposition()
|
||||
{
|
||||
// Arrange
|
||||
var formFiles = new FormFileCollection();
|
||||
formFiles.Add(new Mock<IFormFile>().Object);
|
||||
var formFiles = new FormFileCollection
|
||||
{
|
||||
new Mock<IFormFile>().Object
|
||||
};
|
||||
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
||||
var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
|
||||
var binder = new FormFileModelBinder(NullLoggerFactory.Instance);
|
||||
|
|
@ -191,8 +385,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
public async Task FormFileModelBinder_ReturnsFailedResult_WithNoFileNameAndZeroLength()
|
||||
{
|
||||
// Arrange
|
||||
var formFiles = new FormFileCollection();
|
||||
formFiles.Add(GetMockFormFile("file", ""));
|
||||
var formFiles = new FormFileCollection
|
||||
{
|
||||
GetMockFormFile("file", "")
|
||||
};
|
||||
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
||||
var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
|
||||
var binder = new FormFileModelBinder(NullLoggerFactory.Instance);
|
||||
|
|
@ -323,5 +519,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
|||
private class FileList : List<IFormFile>
|
||||
{
|
||||
}
|
||||
|
||||
private class NestedFormFiles
|
||||
{
|
||||
public FileList Files { get; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue