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>
|
/// </summary>
|
||||||
public abstract string ModelName { get; set; }
|
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>
|
/// <summary>
|
||||||
/// Gets or sets the <see cref="ModelStateDictionary"/> used to capture <see cref="ModelStateEntry"/> values
|
/// 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.
|
/// 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.
|
// Because this is the top-level context, FieldName and ModelName should be the same.
|
||||||
FieldName = binderModelName ?? modelName,
|
FieldName = binderModelName ?? modelName,
|
||||||
ModelName = binderModelName ?? modelName,
|
ModelName = binderModelName ?? modelName,
|
||||||
|
OriginalModelName = binderModelName ?? modelName,
|
||||||
|
|
||||||
IsTopLevelObject = true,
|
IsTopLevelObject = true,
|
||||||
ModelMetadata = metadata,
|
ModelMetadata = metadata,
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
|
|
||||||
_logger = loggerFactory.CreateLogger<FormFileModelBinder>();
|
_logger = loggerFactory.CreateLogger<FormFileModelBinder>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task BindModelAsync(ModelBindingContext bindingContext)
|
public async Task BindModelAsync(ModelBindingContext bindingContext)
|
||||||
{
|
{
|
||||||
|
|
@ -85,6 +85,19 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
|
|
||||||
await GetFormFilesAsync(modelName, bindingContext, postedFiles);
|
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;
|
object value;
|
||||||
if (bindingContext.ModelType == typeof(IFormFile))
|
if (bindingContext.ModelType == typeof(IFormFile))
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
public async Task FormFileModelBinder_SingleFile_BindSuccessful()
|
public async Task FormFileModelBinder_SingleFile_BindSuccessful()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var formFiles = new FormFileCollection();
|
var formFiles = new FormFileCollection
|
||||||
formFiles.Add(GetMockFormFile("file", "file1.txt"));
|
{
|
||||||
|
GetMockFormFile("file", "file1.txt")
|
||||||
|
};
|
||||||
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
||||||
var bindingContext = GetBindingContext(typeof(IEnumerable<IFormFile>), httpContext);
|
var bindingContext = GetBindingContext(typeof(IEnumerable<IFormFile>), httpContext);
|
||||||
var binder = new FormFileModelBinder(NullLoggerFactory.Instance);
|
var binder = new FormFileModelBinder(NullLoggerFactory.Instance);
|
||||||
|
|
@ -38,6 +40,192 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
Assert.Null(entry.Metadata);
|
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]
|
[Fact]
|
||||||
public async Task FormFileModelBinder_ExpectMultipleFiles_BindSuccessful()
|
public async Task FormFileModelBinder_ExpectMultipleFiles_BindSuccessful()
|
||||||
{
|
{
|
||||||
|
|
@ -127,8 +315,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
public async Task FormFileModelBinder_ReturnsFailedResult_WhenNamesDoNotMatch()
|
public async Task FormFileModelBinder_ReturnsFailedResult_WhenNamesDoNotMatch()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var formFiles = new FormFileCollection();
|
var formFiles = new FormFileCollection
|
||||||
formFiles.Add(GetMockFormFile("different name", "file1.txt"));
|
{
|
||||||
|
GetMockFormFile("different name", "file1.txt")
|
||||||
|
};
|
||||||
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
||||||
var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
|
var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
|
||||||
var binder = new FormFileModelBinder(NullLoggerFactory.Instance);
|
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)
|
public async Task FormFileModelBinder_UsesFieldNameForTopLevelObject(bool isTopLevel, string expected)
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var formFiles = new FormFileCollection();
|
var formFiles = new FormFileCollection
|
||||||
formFiles.Add(GetMockFormFile("FieldName", "file1.txt"));
|
{
|
||||||
formFiles.Add(GetMockFormFile("ModelName", "file1.txt"));
|
GetMockFormFile("FieldName", "file1.txt"),
|
||||||
|
GetMockFormFile("ModelName", "file1.txt")
|
||||||
|
};
|
||||||
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
||||||
|
|
||||||
var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
|
var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
|
||||||
|
|
@ -173,8 +365,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
public async Task FormFileModelBinder_ReturnsFailedResult_WithEmptyContentDisposition()
|
public async Task FormFileModelBinder_ReturnsFailedResult_WithEmptyContentDisposition()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var formFiles = new FormFileCollection();
|
var formFiles = new FormFileCollection
|
||||||
formFiles.Add(new Mock<IFormFile>().Object);
|
{
|
||||||
|
new Mock<IFormFile>().Object
|
||||||
|
};
|
||||||
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
||||||
var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
|
var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
|
||||||
var binder = new FormFileModelBinder(NullLoggerFactory.Instance);
|
var binder = new FormFileModelBinder(NullLoggerFactory.Instance);
|
||||||
|
|
@ -191,8 +385,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
public async Task FormFileModelBinder_ReturnsFailedResult_WithNoFileNameAndZeroLength()
|
public async Task FormFileModelBinder_ReturnsFailedResult_WithNoFileNameAndZeroLength()
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
var formFiles = new FormFileCollection();
|
var formFiles = new FormFileCollection
|
||||||
formFiles.Add(GetMockFormFile("file", ""));
|
{
|
||||||
|
GetMockFormFile("file", "")
|
||||||
|
};
|
||||||
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
var httpContext = GetMockHttpContext(GetMockFormCollection(formFiles));
|
||||||
var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
|
var bindingContext = GetBindingContext(typeof(IFormFile), httpContext);
|
||||||
var binder = new FormFileModelBinder(NullLoggerFactory.Instance);
|
var binder = new FormFileModelBinder(NullLoggerFactory.Instance);
|
||||||
|
|
@ -323,5 +519,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
|
||||||
private class FileList : List<IFormFile>
|
private class FileList : List<IFormFile>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class NestedFormFiles
|
||||||
|
{
|
||||||
|
public FileList Files { get; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue