Introducing ByteArrayModelBinder.

-Checks for ModelType before processing.
-Ignores quotes in ByteArrayModelBinder.
-Unit,functional Tests.
-ModelStateError is set when Covert.FromBase64String(value) throws.
This commit is contained in:
sornaks 2014-08-11 13:00:29 -07:00
parent 8d3d15fb4a
commit 9c4d7806a7
18 changed files with 387 additions and 27 deletions

15
Mvc.sln
View File

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.21916.0
VisualStudioVersion = 14.0.21902.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{DAAE4C74-D06F-4874-A166-33305D2643CE}"
EndProject
@ -61,6 +61,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.Header
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.Mvc.HeaderValueAbstractions.Tests", "test\Microsoft.AspNet.Mvc.HeaderValueAbstractions.Test\Microsoft.AspNet.Mvc.HeaderValueAbstractions.Tests.kproj", "{E69FD235-2042-43A4-9970-59CB29955B4E}"
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ModelBindingWebSite", "test\WebSites\ModelBindingWebSite\ModelBindingWebSite.kproj", "{EE1AB716-F102-4CA3-AD2C-214A44B459A0}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FAD65E9C-3CF3-4F68-9757-C7358604030B}"
ProjectSection(SolutionItems) = preProject
global.json = global.json
@ -328,6 +330,16 @@ Global
{E69FD235-2042-43A4-9970-59CB29955B4E}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{E69FD235-2042-43A4-9970-59CB29955B4E}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{E69FD235-2042-43A4-9970-59CB29955B4E}.Release|x86.ActiveCfg = Release|Any CPU
{EE1AB716-F102-4CA3-AD2C-214A44B459A0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EE1AB716-F102-4CA3-AD2C-214A44B459A0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EE1AB716-F102-4CA3-AD2C-214A44B459A0}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{EE1AB716-F102-4CA3-AD2C-214A44B459A0}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{EE1AB716-F102-4CA3-AD2C-214A44B459A0}.Debug|x86.ActiveCfg = Debug|Any CPU
{EE1AB716-F102-4CA3-AD2C-214A44B459A0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EE1AB716-F102-4CA3-AD2C-214A44B459A0}.Release|Any CPU.Build.0 = Release|Any CPU
{EE1AB716-F102-4CA3-AD2C-214A44B459A0}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{EE1AB716-F102-4CA3-AD2C-214A44B459A0}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{EE1AB716-F102-4CA3-AD2C-214A44B459A0}.Release|x86.ActiveCfg = Release|Any CPU
{C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
@ -379,6 +391,7 @@ Global
{14F79E79-AE79-48FA-95DE-D794EF4EABB3} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{98335B23-E4B9-4CAD-9749-0DED32A659A1} = {32285FA4-6B46-4D6B-A840-2B13E4C8B58E}
{E69FD235-2042-43A4-9970-59CB29955B4E} = {3BA657BF-28B1-42DA-B5B0-1C4601FCF7B1}
{EE1AB716-F102-4CA3-AD2C-214A44B459A0} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{C6E5AFFA-890A-448F-8DE3-878B1D3C9FC7} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
{A353B17E-A940-4CE8-8BF9-179E24A9041F} = {16703B76-C9F7-4C75-AE6C-53D92E308E3C}
EndGlobalSection

View File

@ -0,0 +1,51 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// ModelBinder to bind Byte Arrays.
/// </summary>
public class ByteArrayModelBinder : IModelBinder
{
/// <inheritdoc />
public async Task<bool> BindModelAsync([NotNull] ModelBindingContext bindingContext)
{
if (bindingContext.ModelType != typeof(byte[]))
{
return false;
}
var valueProviderResult = await bindingContext.ValueProvider.GetValueAsync(bindingContext.ModelName);
// case 1: there was no <input ... /> element containing this data
if (valueProviderResult == null)
{
return false;
}
var value = valueProviderResult.AttemptedValue;
// case 2: there was an <input ... /> element but it was left blank
if (string.IsNullOrEmpty(value))
{
return false;
}
try
{
bindingContext.Model = Convert.FromBase64String(value);
}
catch (Exception ex)
{
ModelBindingHelper.AddModelErrorBasedOnExceptionType(bindingContext, ex);
}
return true;
}
}
}

View File

@ -10,6 +10,11 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// </summary>
public interface IModelBinder
{
/// <summary>
/// Async function to bind to a particular model.
/// </summary>
/// <param name="bindingContext">The binding context which has the object to be bound.</param>
/// <returns>A Task with a bool implying the success or failure of the operation.</returns>
Task<bool> BindModelAsync(ModelBindingContext bindingContext);
}
}

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ModelBinding.Internal;
@ -36,30 +35,10 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
}
catch (Exception ex)
{
if (IsFormatException(ex))
{
// there was a type conversion failure
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex.Message);
}
else
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
}
ModelBindingHelper.AddModelErrorBasedOnExceptionType(bindingContext, ex);
}
return true;
}
private static bool IsFormatException(Exception ex)
{
for (; ex != null; ex = ex.InnerException)
{
if (ex is FormatException)
{
return true;
}
}
return false;
}
}
}

View File

@ -93,5 +93,29 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Internal
throw new ArgumentException(message, "bindingContext");
}
}
internal static void AddModelErrorBasedOnExceptionType(ModelBindingContext bindingContext, Exception ex)
{
if (IsFormatException(ex))
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex.Message);
}
else
{
bindingContext.ModelState.AddModelError(bindingContext.ModelName, ex);
}
}
internal static bool IsFormatException(Exception ex)
{
for (; ex != null; ex = ex.InnerException)
{
if (ex is FormatException)
{
return true;
}
}
return false;
}
}
}

View File

@ -26,6 +26,7 @@
<Compile Include="Binders\BindingBehaviorAttribute.cs" />
<Compile Include="Binders\BindNeverAttribute.cs" />
<Compile Include="Binders\BindRequiredAttribute.cs" />
<Compile Include="Binders\ByteArrayModelBinder.cs" />
<Compile Include="Binders\CollectionModelBinder.cs" />
<Compile Include="Binders\ComplexModelDto.cs" />
<Compile Include="Binders\ComplexModelDtoModelBinder.cs" />

View File

@ -28,6 +28,7 @@ namespace Microsoft.AspNet.Mvc
// Set up ModelBinding
options.ModelBinders.Add(new TypeConverterModelBinder());
options.ModelBinders.Add(new TypeMatchModelBinder());
options.ModelBinders.Add(new ByteArrayModelBinder());
options.ModelBinders.Add(typeof(GenericModelBinder));
options.ModelBinders.Add(new MutableObjectModelBinder());
options.ModelBinders.Add(new ComplexModelDtoModelBinder());

View File

@ -33,6 +33,7 @@
<Compile Include="BasicTests.cs" />
<Compile Include="ConnegTests.cs" />
<Compile Include="AntiForgeryTests.cs" />
<Compile Include="ModelBindingTests.cs" />
<Compile Include="ViewEngineTests.cs" />
<Compile Include="ActivatorTests.cs" />
<Compile Include="ValueProviderTests.cs" />

View File

@ -0,0 +1,52 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.TestHost;
using Xunit;
namespace Microsoft.AspNet.Mvc.FunctionalTests
{
public class ModelBindingTests
{
private readonly IServiceProvider _services;
private readonly Action<IBuilder> _app = new ModelBindingWebSite.Startup().Configure;
public ModelBindingTests()
{
_services = TestHelper.CreateServices("ModelBindingWebSite");
}
[Fact]
public async Task ModelBindingBindsBase64StringsToByteArrays()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.Handler;
// Act
var response = await client.GetAsync("http://localhost/Home/Index?byteValues=SGVsbG9Xb3JsZA==");
//Assert
Assert.Equal(200, response.StatusCode);
Assert.Equal("HelloWorld", await response.ReadBodyAsStringAsync());
}
[Fact]
public async Task ModelBindingBindsEmptyStringsToByteArrays()
{
// Arrange
var server = TestServer.Create(_services, _app);
var client = server.Handler;
// Act
var response = await client.GetAsync("http://localhost/Home/Index?byteValues=");
//Assert
Assert.Equal(200, response.StatusCode);
Assert.Equal("\0", await response.ReadBodyAsStringAsync());
}
}
}

View File

@ -19,6 +19,7 @@
"Microsoft.Framework.Logging": "1.0.0-*",
"Microsoft.Framework.Runtime.Interfaces": "1.0.0-*",
"Microsoft.AspNet.PipelineCore": "1.0.0-*",
"ModelBindingWebSite": "",
"RoutingWebSite": "",
"RazorWebSite": "",
"ValueProvidersSite": "",

View File

@ -0,0 +1,128 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#if NET45
using System;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{
public class ByteArrayModelBinderTests
{
[Theory]
[InlineData(null)]
[InlineData("")]
public async Task BindModelSetsModelToNullOnNullOrEmptyString(string value)
{
// Arrange
var valueProvider = new SimpleHttpValueProvider()
{
{ "foo", value }
};
var bindingContext = GetBindingContext(valueProvider, typeof(byte[]));
var binder = new ByteArrayModelBinder();
// Act
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(binderResult);
Assert.Null(bindingContext.Model);
}
[Fact]
public async Task BindModel()
{
// Arrange
var valueProvider = new SimpleHttpValueProvider()
{
{ "foo", "Fys1" }
};
var bindingContext = GetBindingContext(valueProvider, typeof(byte[]));
var binder = new ByteArrayModelBinder();
// Act
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(binderResult);
var bytes = Assert.IsType<byte[]>(bindingContext.Model);
Assert.Equal(new byte[] { 23, 43, 53 }, bytes);
}
[Fact]
public async Task BindModelAddsModelErrorsOnInvalidCharacters()
{
// Arrange
var valueProvider = new SimpleHttpValueProvider()
{
{ "foo", "\"Fys1\"" }
};
var bindingContext = GetBindingContext(valueProvider, typeof(byte[]));
var binder = new ByteArrayModelBinder();
// Act
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
Assert.True(binderResult);
Assert.False(bindingContext.ModelState.IsValid);
Assert.Equal(1, bindingContext.ModelState.Values.Count);
Assert.Equal("The input is not a valid Base-64 string as it contains a non-base 64 character," +
" more than two padding characters, or an illegal character among the padding characters. ",
bindingContext.ModelState.Values.First().Errors[0].ErrorMessage);
}
[Fact]
public async Task BindModelReturnsFalseWhenValueNotFound()
{
// Arrange
var valueProvider = new SimpleHttpValueProvider()
{
{ "someName", "" }
};
var bindingContext = GetBindingContext(valueProvider, typeof(byte[]));
var binder = new ByteArrayModelBinder();
// Act
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(binderResult);
}
[Fact]
public async Task ByteArrayModelBinderReturnsFalseForOtherTypes()
{
// Arrange
var bindingContext = GetBindingContext(null, typeof(int[]));
var binder = new ByteArrayModelBinder();
// Act
var binderResult = await binder.BindModelAsync(bindingContext);
// Assert
Assert.False(binderResult);
}
private static ModelBindingContext GetBindingContext(IValueProvider valueProvider, Type modelType)
{
var metadataProvider = new EmptyModelMetadataProvider();
var bindingContext = new ModelBindingContext
{
ModelMetadata = metadataProvider.GetMetadataForType(null, modelType),
ModelName = "foo",
ValueProvider = valueProvider,
MetadataProvider = metadataProvider
};
return bindingContext;
}
}
}
#endif

View File

@ -200,6 +200,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
{ "friends[0].friends[0].firstname", "nested friend"},
{ "friends[1].firstName", "some other"},
{ "friends[1].lastName", "name"},
{ "resume", "4+mFeTp3tPF=" }
};
var bindingContext = CreateBindingContext(binder, valueProvider, typeof(Person));
@ -218,6 +219,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.Equal("nested friend", nestedFriend.FirstName);
Assert.Equal("some other", model.Friends[1].FirstName);
Assert.Equal("name", model.Friends[1].LastName);
Assert.Equal(new byte[] { 227, 233, 133, 121, 58, 119, 180, 241 }, model.Resume);
}
[Fact]
@ -294,6 +296,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
var binders = new IModelBinder[]
{
new TypeMatchModelBinder(),
new ByteArrayModelBinder(),
new GenericModelBinder(serviceProvider, typeActivator.Object),
new ComplexModelDtoModelBinder(),
new TypeConverterModelBinder(),
@ -331,6 +334,8 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
public int Age { get; set; }
public List<Person> Friends { get; set; }
public byte[] Resume { get; set; }
}
private class User : IValidatableObject

View File

@ -23,6 +23,7 @@
</ItemGroup>
<ItemGroup>
<Compile Include="Binders\ArrayModelBinderTest.cs" />
<Compile Include="Binders\ByteArrayModelBinderTests.cs" />
<Compile Include="Binders\CollectionModelBinderTest.cs" />
<Compile Include="Binders\ComplexModelDtoResultTest.cs" />
<Compile Include="Binders\ComplexModelDtoTest.cs" />

View File

@ -35,12 +35,13 @@ namespace Microsoft.AspNet.Mvc
setup.Setup(mvcOptions);
// Assert
Assert.Equal(5, mvcOptions.ModelBinders.Count);
Assert.Equal(6, mvcOptions.ModelBinders.Count);
Assert.Equal(typeof(TypeConverterModelBinder), mvcOptions.ModelBinders[0].OptionType);
Assert.Equal(typeof(TypeMatchModelBinder), mvcOptions.ModelBinders[1].OptionType);
Assert.Equal(typeof(GenericModelBinder), mvcOptions.ModelBinders[2].OptionType);
Assert.Equal(typeof(MutableObjectModelBinder), mvcOptions.ModelBinders[3].OptionType);
Assert.Equal(typeof(ComplexModelDtoModelBinder), mvcOptions.ModelBinders[4].OptionType);
Assert.Equal(typeof(ByteArrayModelBinder), mvcOptions.ModelBinders[2].OptionType);
Assert.Equal(typeof(GenericModelBinder), mvcOptions.ModelBinders[3].OptionType);
Assert.Equal(typeof(MutableObjectModelBinder), mvcOptions.ModelBinders[4].OptionType);
Assert.Equal(typeof(ComplexModelDtoModelBinder), mvcOptions.ModelBinders[5].OptionType);
}
[Fact]

View File

@ -0,0 +1,16 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Mvc;
namespace ModelBindingWebSite.Controllers
{
public class HomeController : Controller
{
[HttpGet]
public IActionResult Index(byte[] byteValues)
{
return Content(System.Text.Encoding.UTF8.GetString(byteValues));
}
}
}

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">12.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.Props" Condition="'$(VSToolsPath)' != ''" />
<PropertyGroup Label="Globals">
<ProjectGuid>ee1ab716-f102-4ca3-ad2c-214a44b459a0</ProjectGuid>
<OutputType>Web</OutputType>
</PropertyGroup>
<PropertyGroup Condition="$(OutputType) == 'Console'">
<DebuggerFlavor>ConsoleDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="$(OutputType) == 'Web'">
<DebuggerFlavor>WebDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'" Label="Configuration">
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DevelopmentServerPort>38820</DevelopmentServerPort>
</PropertyGroup>
<ItemGroup>
<Compile Include="Controllers\HomeController.cs" />
<Compile Include="Models\DummyClass.cs" />
<Compile Include="Startup.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="project.json" />
</ItemGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -0,0 +1,32 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
namespace ModelBindingWebSite
{
public class Startup
{
public void Configure(IBuilder app)
{
var configuration = app.GetTestConfiguration();
// Set up application services
app.UseServices(services =>
{
// Add MVC services to the services container
services.AddMvc(configuration);
});
// Add MVC to the request pipeline
app.UseMvc(routes =>
{
routes.MapRoute("ActionAsMethod", "{controller}/{action}",
defaults: new { controller = "Home", action = "Index" });
});
}
}
}

View File

@ -0,0 +1,10 @@
{
"dependencies": {
"Microsoft.AspNet.Mvc": "",
"Microsoft.AspNet.Mvc.TestConfiguration": ""
},
"configurations": {
"net45": { },
"k10": { }
}
}