├── .gitattributes ├── .gitignore ├── LICENSE.txt ├── MultipartDataMediaFormatter.sln ├── README.markdown ├── src └── MultipartDataMediaFormatter │ ├── Converters │ ├── FormDataToObjectConverter.cs │ ├── HttpContentToFormDataConverter.cs │ └── ObjectToMultipartDataByteArrayConverter.cs │ ├── FormMultipartEncodedMediaTypeFormatter.cs │ ├── Infrastructure │ ├── Extensions │ │ └── TypeExtensions.cs │ ├── FormData.cs │ ├── HttpFile.cs │ ├── Logger │ │ ├── FormDataConverterLogger.cs │ │ ├── FormatterLoggerAdapter.cs │ │ └── IFormDataConverterLogger.cs │ ├── MultipartFormatterSettings.cs │ └── TypeConverters │ │ ├── BooleanConverterEx.cs │ │ ├── DateTimeConverterISO8601.cs │ │ └── FromStringConverterAdapter.cs │ ├── MultipartDataMediaFormatter.csproj │ ├── StrongNameKey.snk │ └── licenses │ └── LICENSE.txt └── test └── MultipartDataMediaFormatter.Tests ├── Controllers └── TestApiController.cs ├── Infrastructure └── WebApiHttpServer.cs ├── Models ├── ApiResult.cs ├── PersonModel.cs └── ResponseErrorItem.cs ├── MultipartDataMediaFormatter.Tests.csproj └── Tests.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | *.doc diff=astextplain 2 | *.DOC diff=astextplain 3 | *.docx diff=astextplain 4 | *.DOCX diff=astextplain 5 | *.dot diff=astextplain 6 | *.DOT diff=astextplain 7 | *.pdf diff=astextplain 8 | *.PDF diff=astextplain 9 | *.rtf diff=astextplain 10 | *.RTF diff=astextplain 11 | 12 | *.jpg binary 13 | *.png binary 14 | *.gif binary 15 | 16 | *.cs text diff=csharp 17 | *.vb text 18 | *.c text 19 | *.cpp text 20 | *.cxx text 21 | *.h text 22 | *.hxx text 23 | *.py text 24 | *.rb text 25 | *.java text 26 | *.html text 27 | *.htm text 28 | *.css text 29 | *.scss text 30 | *.sass text 31 | *.less text 32 | *.js text 33 | *.lisp text 34 | *.clj text 35 | *.sql text 36 | *.php text 37 | *.lua text 38 | *.m text 39 | *.asm text 40 | *.erl text 41 | *.fs text 42 | *.fsx text 43 | *.hs text 44 | 45 | *.csproj text merge=union 46 | *.vbproj text merge=union 47 | *.fsproj text merge=union 48 | *.dbproj text merge=union 49 | *.sln text eol=crlf merge=union -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Dd]ebugPublic/ 12 | [Rr]elease/ 13 | x64/ 14 | build/ 15 | bld/ 16 | [Bb]in/ 17 | [Oo]bj/ 18 | 19 | # Enable "build/" folder in the NuGet Packages folder since NuGet packages use it for MSBuild targets 20 | !packages/*/build/ 21 | 22 | # MSTest test Results 23 | [Tt]est[Rr]esult*/ 24 | [Bb]uild[Ll]og.* 25 | 26 | #NUNIT 27 | *.VisualState.xml 28 | TestResult.xml 29 | 30 | *_i.c 31 | *_p.c 32 | *_i.h 33 | *.ilk 34 | *.meta 35 | *.obj 36 | *.pch 37 | *.pdb 38 | *.pgc 39 | *.pgd 40 | *.rsp 41 | *.sbr 42 | *.tlb 43 | *.tli 44 | *.tlh 45 | *.tmp 46 | *.tmp_proj 47 | *.log 48 | *.vspscc 49 | *.vssscc 50 | .builds 51 | *.pidb 52 | *.svclog 53 | *.scc 54 | 55 | # Chutzpah Test files 56 | _Chutzpah* 57 | 58 | # Visual C++ cache files 59 | ipch/ 60 | *.aps 61 | *.ncb 62 | *.opensdf 63 | *.sdf 64 | *.cachefile 65 | 66 | # Visual Studio profiler 67 | *.psess 68 | *.vsp 69 | *.vspx 70 | 71 | # TFS 2012 Local Workspace 72 | $tf/ 73 | 74 | # Guidance Automation Toolkit 75 | *.gpState 76 | 77 | # ReSharper is a .NET coding add-in 78 | _ReSharper*/ 79 | *.[Rr]e[Ss]harper 80 | *.DotSettings.user 81 | 82 | # JustCode is a .NET coding addin-in 83 | .JustCode 84 | 85 | # TeamCity is a build add-in 86 | _TeamCity* 87 | 88 | # DotCover is a Code Coverage Tool 89 | *.dotCover 90 | 91 | # NCrunch 92 | *.ncrunch* 93 | _NCrunch_* 94 | .*crunch*.local.xml 95 | 96 | # MightyMoose 97 | *.mm.* 98 | AutoTest.Net/ 99 | 100 | # Installshield output folder 101 | [Ee]xpress/ 102 | 103 | # DocProject is a documentation generator add-in 104 | DocProject/buildhelp/ 105 | DocProject/Help/*.HxT 106 | DocProject/Help/*.HxC 107 | DocProject/Help/*.hhc 108 | DocProject/Help/*.hhk 109 | DocProject/Help/*.hhp 110 | DocProject/Help/Html2 111 | DocProject/Help/html 112 | 113 | # Click-Once directory 114 | publish/ 115 | 116 | # Publish Web Output 117 | *.Publish.xml 118 | *.azurePubxml 119 | 120 | # NuGet Packages Directory 121 | ## TODO: If you have NuGet Package Restore enabled, uncomment the next line 122 | packages/ 123 | ## TODO: If the tool you use requires repositories.config, also uncomment the next line 124 | #!packages/repositories.config 125 | 126 | # Windows Azure Build Output 127 | csx/ 128 | *.build.csdef 129 | 130 | # Windows Store app package directory 131 | AppPackages/ 132 | 133 | # Others 134 | sql/ 135 | *.Cache 136 | ClientBin/ 137 | [Ss]tyle[Cc]op.* 138 | ~$* 139 | *~ 140 | *.dbmdl 141 | *.dbproj.schemaview 142 | *.[Pp]ublish.xml 143 | *.pfx 144 | *.publishsettings 145 | 146 | # RIA/Silverlight projects 147 | Generated_Code/ 148 | 149 | # Backup & report files from converting an old project file to a newer 150 | # Visual Studio version. Backup files are not needed, because we have git ;-) 151 | _UpgradeReport_Files/ 152 | Backup*/ 153 | UpgradeLog*.XML 154 | UpgradeLog*.htm 155 | 156 | # SQL Server files 157 | App_Data/*.mdf 158 | App_Data/*.ldf 159 | 160 | # Business Intelligence projects 161 | *.rdl.data 162 | *.bim.layout 163 | *.bim_*.settings 164 | 165 | # Microsoft Fakes 166 | FakesAssemblies/ 167 | 168 | # ========================= 169 | # Windows detritus 170 | # ========================= 171 | 172 | # Windows image file caches 173 | Thumbs.db 174 | ehthumbs.db 175 | 176 | # Folder config file 177 | Desktop.ini 178 | 179 | # Recycle Bin used on file shares 180 | $RECYCLE.BIN/ 181 | *.testlog 182 | .vs/ -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2021 Alexander Kozlovskiy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /MultipartDataMediaFormatter.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2012 4 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultipartDataMediaFormatter.Tests", "test\MultipartDataMediaFormatter.Tests\MultipartDataMediaFormatter.Tests.csproj", "{346BD9BC-1EB3-4B74-8468-E86E29018C67}" 5 | EndProject 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultipartDataMediaFormatter", "src\MultipartDataMediaFormatter\MultipartDataMediaFormatter.csproj", "{1F851995-9C6B-4B34-82AF-AC4E075F00C2}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{9EB9640F-9380-4FBB-A390-36FFD844146D}" 9 | ProjectSection(SolutionItems) = preProject 10 | .nuget\NuGet.Config = .nuget\NuGet.Config 11 | .nuget\NuGet.exe = .nuget\NuGet.exe 12 | .nuget\NuGet.targets = .nuget\NuGet.targets 13 | EndProjectSection 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {346BD9BC-1EB3-4B74-8468-E86E29018C67}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {346BD9BC-1EB3-4B74-8468-E86E29018C67}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {346BD9BC-1EB3-4B74-8468-E86E29018C67}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {346BD9BC-1EB3-4B74-8468-E86E29018C67}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {1F851995-9C6B-4B34-82AF-AC4E075F00C2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {1F851995-9C6B-4B34-82AF-AC4E075F00C2}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {1F851995-9C6B-4B34-82AF-AC4E075F00C2}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {1F851995-9C6B-4B34-82AF-AC4E075F00C2}.Release|Any CPU.Build.0 = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(SolutionProperties) = preSolution 31 | HideSolutionNode = FALSE 32 | EndGlobalSection 33 | EndGlobal 34 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ASP.NET WebApi MultipartDataMediaFormatter 2 | ============= 3 | 4 | This is solution for automatic binding action parameters of custom types (including files) encoded as multipart/form-data. It works similar to ASP.NET MVC binding. This media type formatter can be used also for sending objects (using HttpClient) with automatic serialization to multipart/form-data. 5 | 6 | **This formatter can process:** 7 | 8 | * **custom non enumerable classes (deep nested classes supported)** 9 | * **all simple types that can be converted from/to string (using TypeConverter)** 10 | * **files (MultipartDataMediaFormatter.Infrastructure.HttpFile class)** 11 | * **generic arrays** 12 | * **generic lists** 13 | * **generic dictionaries** 14 | 15 | Using the code 16 | ================= 17 | 18 | Install formatter from Nuget: 19 | ```c# 20 | Install-Package MultipartDataMediaFormatter.V2 21 | ``` 22 | 23 | Add it to WebApi formatters collection: 24 | 25 | if WebApi hosted on IIS (on Application Start): 26 | 27 | ```c# 28 | GlobalConfiguration.Configuration.Formatters.Add(new FormMultipartEncodedMediaTypeFormatter(new MultipartFormatterSettings())); 29 | ``` 30 | if WebApi is self-hosted: 31 | 32 | ```c# 33 | new HttpSelfHostConfiguration().Formatters.Add(new FormMultipartEncodedMediaTypeFormatter(new MultipartFormatterSettings())); 34 | ``` 35 | Using formatter for sending objects (example from test project): 36 | 37 | ```c# 38 | private ApiResult PostModel(T model, string url) 39 | { 40 | var mediaTypeFormatter = new FormMultipartEncodedMediaTypeFormatter(new MultipartFormatterSettings() 41 | { 42 | SerializeByteArrayAsHttpFile = true, 43 | CultureInfo = CultureInfo.CurrentCulture, 44 | ValidateNonNullableMissedProperty = true 45 | }); 46 | using (new WebApiHttpServer(BaseApiAddress, mediaTypeFormatter)) 47 | using (var client = CreateHttpClient(BaseApiAddress)) 48 | using (HttpResponseMessage response = client.PostAsync(url, model, mediaTypeFormatter).Result) 49 | { 50 | if (response.StatusCode != HttpStatusCode.OK) 51 | { 52 | var err = response.Content.ReadAsStringAsync().Result; 53 | Assert.Fail(err); 54 | } 55 | 56 | var resultModel = response.Content.ReadAsAsync>(new[] { mediaTypeFormatter }).Result; 57 | return resultModel; 58 | } 59 | } 60 | ``` 61 | You can use MultipartDataMediaFormatter.Infrastructure.FormData class to access raw http data: 62 | 63 | ```c# 64 | [HttpPost] 65 | public void PostFileBindRawFormData(MultipartDataMediaFormatter.Infrastructure.FormData formData) 66 | { 67 | HttpFile file; 68 | formData.TryGetValue(, out file); 69 | } 70 | ``` 71 | Bind custom model example: 72 | 73 | ```c# 74 | //model example 75 | public class PersonModel 76 | { 77 | public string FirstName {get; set;} 78 | public string LastName {get; set;} 79 | public DateTime? BirthDate {get; set;} 80 | public HttpFile AvatarImage {get; set;} 81 | public List Attachments {get; set;} 82 | public List ConnectedPersons {get; set;} 83 | public PersonModel Creator {get; set;} 84 | public List Attributes {get; set;} 85 | } 86 | 87 | //api controller example 88 | [HttpPost] 89 | public void PostPerson(PersonModel model) 90 | { 91 | //do something with the model 92 | } 93 | 94 | /* 95 | Client http form keys: 96 | * FirstName 97 | * LastName 98 | * BirthDate 99 | * AvatarImage 100 | 101 | * Attachments[0] 102 | * Attachments[1] 103 | * ... other Attachments[0...n] 104 | 105 | * ConnectedPersons[0].FirstName 106 | * ConnectedPersons[0].LastName 107 | * ... other properties for ConnectedPersons[0] property 108 | 109 | * Creator.FirstName 110 | * Creator.LastName 111 | * ... other properties for Creator property 112 | 113 | * Attributes[0] 114 | * Attributes[1] 115 | * ... other Attributes[0...n] 116 | or you can use not indexed names for simple types: 117 | * Attributes 118 | * Attributes 119 | * ... other Attributes 120 | */ 121 | ``` 122 | 123 | ## History 124 | 125 | ##### Version 2.1.1 (2021-04-24) 126 | 127 | * add support of form data square notation (keys like model[property]) 128 | 129 | ##### Version 2.1.0 (2021-02-14) 130 | 131 | * add support of netstandard2.0 132 | * use ```Microsoft.Owin.Testing.TestServer``` instead of ```System.Web.Http.SelfHost.HttpSelfHostServer``` in test project to avoid running Visual Studio under administrator rights (for correct test completion) 133 | 134 | ##### Version 2.0.3 (2020-05-29) 135 | 136 | * permit zero byte / empty files; allow file mediaType to be null 137 | 138 | ##### Version 2.0.2 (2018-06-09) 139 | 140 | * signed the project with a strong name (without password) to allow referencing this project in projects that were signed with a strong name 141 | 142 | ##### Version 2.0.1 (2018-02-14) 143 | 144 | * added possibility of using IEnumerable<> and IDictionary<,> as types for model's properties, for example: ``` public IEnumerable Persons {get;set;}``` 145 | 146 | ##### Version 2.0.0 (2017-05-27) 147 | 148 | * added Nuget package [MultipartDataMediaFormatter.V2](https://www.nuget.org/packages/MultipartDataMediaFormatter.V2) 149 | 150 | ##### Version 1.0.2 (2016-08-12) 151 | 152 | * parsing lists of simple types and files with not indexed naming scheme (keys have same names like "propName" or "propName[]") 153 | * parsing values "on" and "off" for boolean properties 154 | * binding HttpFile from http request as byte array if model has such property 155 | * added class ``` MultipartDataMediaFormatter.Infrastructure.MultipartFormatterSettings``` to control: 156 | * CultureInfo 157 | * serializing byte array as HttpFile when sending data 158 | * validating non nullable value types properties if there is no appropriate keys in http request 159 | 160 | ##### Version 1.0.1 (2014-04-03) 161 | * fixed a bug that caused Exception (No MediaTypeFormatter is available to read an object of type ) when posted data use multipart boundary different from used inside formatter code 162 | * fixed a bug that caused error when binding model with recursive properties. 163 | 164 | ##### Version 1.0 (2013-11-22) 165 | * First release 166 | 167 | ## License 168 | 169 | Licensed under the [MIT License](http://www.opensource.org/licenses/mit-license.php). -------------------------------------------------------------------------------- /src/MultipartDataMediaFormatter/Converters/FormDataToObjectConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using MultipartDataMediaFormatter.Infrastructure; 6 | using MultipartDataMediaFormatter.Infrastructure.Extensions; 7 | using MultipartDataMediaFormatter.Infrastructure.Logger; 8 | 9 | namespace MultipartDataMediaFormatter.Converters 10 | { 11 | public class FormDataToObjectConverter 12 | { 13 | private readonly FormData SourceData; 14 | private readonly IFormDataConverterLogger Logger; 15 | private readonly MultipartFormatterSettings Settings; 16 | 17 | public FormDataToObjectConverter(FormData sourceData, IFormDataConverterLogger logger, MultipartFormatterSettings settings) 18 | { 19 | if (sourceData == null) 20 | throw new ArgumentNullException("sourceData"); 21 | if (logger == null) 22 | throw new ArgumentNullException("logger"); 23 | if (settings == null) 24 | throw new ArgumentNullException("settings"); 25 | 26 | Settings = settings; 27 | SourceData = sourceData; 28 | Logger = logger; 29 | } 30 | 31 | public object Convert(Type destinationType) 32 | { 33 | if (destinationType == null) 34 | throw new ArgumentNullException("destinationType"); 35 | 36 | if (destinationType == typeof(FormData)) 37 | return SourceData; 38 | 39 | var objResult = CreateObject(destinationType); 40 | return objResult; 41 | } 42 | 43 | private object CreateObject(Type destinationType, string propertyName = "") 44 | { 45 | object propValue = null; 46 | 47 | if (propertyName == null) 48 | { 49 | propertyName = ""; 50 | } 51 | 52 | object buf; 53 | if (TryGetAsNotIndexedListOrArray(destinationType, propertyName, out buf) 54 | || TryGetFromFormData(destinationType, propertyName, out buf) 55 | || TryGetAsGenericDictionary(destinationType, propertyName, out buf) 56 | || TryGetAsIndexedGenericListOrArray(destinationType, propertyName, out buf) 57 | || TryGetAsCustomType(destinationType, propertyName, out buf)) 58 | { 59 | propValue = buf; 60 | } 61 | else if (IsNotNullableValueType(destinationType) 62 | && IsNeedValidateMissedProperty(propertyName)) 63 | { 64 | Logger.LogError(propertyName, "The value is required."); 65 | } 66 | else if (!IsFileOrConvertableFromString(destinationType)) 67 | { 68 | Logger.LogError(propertyName, String.Format("Cannot parse type \"{0}\".", destinationType.FullName)); 69 | } 70 | 71 | return propValue; 72 | } 73 | 74 | private bool TryGetAsNotIndexedListOrArray(Type destinationType, string propertyName, out object propValue) 75 | { 76 | propValue = null; 77 | 78 | Type genericListItemType; 79 | if (IsGenericEnumerable(destinationType, out genericListItemType)) 80 | { 81 | var items = GetNotIndexedListItems(propertyName, genericListItemType); 82 | propValue = MakeList(genericListItemType, destinationType, items, propertyName); 83 | } 84 | 85 | return propValue != null; 86 | } 87 | 88 | private List GetNotIndexedListItems(string propertyName, Type genericListItemType) 89 | { 90 | List res; 91 | if (!TryGetListFromFormData(genericListItemType, propertyName, out res)) 92 | { 93 | TryGetListFromFormData(genericListItemType, propertyName + "[]", out res); 94 | } 95 | 96 | return res ?? new List(); 97 | } 98 | 99 | private bool TryGetFromFormData(Type destinationType, string propertyName, out object propValue) 100 | { 101 | propValue = null; 102 | List values; 103 | if (TryGetListFromFormData(destinationType, propertyName, out values)) 104 | { 105 | propValue = values.FirstOrDefault(); 106 | return true; 107 | } 108 | return false; 109 | } 110 | 111 | private bool TryGetListFromFormData(Type destinationType, string propertyName, out List propValue) 112 | { 113 | bool existsInFormData = false; 114 | propValue = null; 115 | 116 | if (destinationType == typeof(HttpFile) || destinationType == typeof(byte[])) 117 | { 118 | var files = SourceData.GetFiles(propertyName, Settings.CultureInfo); 119 | if (files.Any()) 120 | { 121 | existsInFormData = true; 122 | propValue = new List(); 123 | 124 | foreach (var httpFile in files) 125 | { 126 | var item = destinationType == typeof(byte[]) 127 | ? httpFile.Buffer 128 | : (object)httpFile; 129 | 130 | propValue.Add(item); 131 | } 132 | } 133 | } 134 | else 135 | { 136 | var values = SourceData.GetValues(propertyName, Settings.CultureInfo); 137 | if (values.Any()) 138 | { 139 | existsInFormData = true; 140 | propValue = new List(); 141 | 142 | foreach (var value in values) 143 | { 144 | object val; 145 | if(TryConvertFromString(destinationType, propertyName, value, out val)) 146 | { 147 | propValue.Add(val); 148 | } 149 | } 150 | } 151 | } 152 | 153 | return existsInFormData; 154 | } 155 | 156 | private bool TryConvertFromString(Type destinationType, string propertyName, string val, out object propValue) 157 | { 158 | propValue = null; 159 | var typeConverter = destinationType.GetFromStringConverter(); 160 | if (typeConverter == null) 161 | { 162 | Logger.LogError(propertyName, "Cannot find type converter for field - " + propertyName); 163 | } 164 | else 165 | { 166 | try 167 | { 168 | propValue = typeConverter.ConvertFromString(val, Settings.CultureInfo); 169 | return true; 170 | } 171 | catch (Exception ex) 172 | { 173 | Logger.LogError(propertyName, String.Format("Error parsing field \"{0}\": {1}", propertyName, ex.Message)); 174 | } 175 | } 176 | return false; 177 | } 178 | 179 | private bool TryGetAsGenericDictionary(Type destinationType, string propertyName, out object propValue) 180 | { 181 | propValue = null; 182 | Type keyType, valueType; 183 | if (IsGenericDictionary(destinationType, out keyType, out valueType)) 184 | { 185 | var dictType = typeof(Dictionary<,>).MakeGenericType(new[] { keyType, valueType }); 186 | var add = dictType.GetMethod("Add"); 187 | 188 | var pValue = Activator.CreateInstance(dictType); 189 | 190 | int index = 0; 191 | string origPropName = propertyName; 192 | bool isFilled = false; 193 | while (true) 194 | { 195 | string propertyKeyName = String.Format("{0}[{1}].Key", origPropName, index); 196 | var objKey = CreateObject(keyType, propertyKeyName); 197 | if (objKey != null) 198 | { 199 | string propertyValueName = String.Format("{0}[{1}].Value", origPropName, index); 200 | var objValue = CreateObject(valueType, propertyValueName); 201 | 202 | if (objValue != null) 203 | { 204 | add.Invoke(pValue, new[] { objKey, objValue }); 205 | isFilled = true; 206 | } 207 | } 208 | else 209 | { 210 | break; 211 | } 212 | index++; 213 | } 214 | 215 | if (isFilled || IsRootProperty(propertyName)) 216 | { 217 | propValue = pValue; 218 | } 219 | 220 | return true; 221 | } 222 | return false; 223 | } 224 | 225 | private bool TryGetAsIndexedGenericListOrArray(Type destinationType, string propertyName, out object propValue) 226 | { 227 | Type genericListItemType; 228 | if (IsGenericEnumerable(destinationType, out genericListItemType)) 229 | { 230 | var items = GetIndexedListItems(propertyName, genericListItemType); 231 | propValue = MakeList(genericListItemType, destinationType, items, propertyName); 232 | return true; 233 | } 234 | 235 | propValue = null; 236 | return false; 237 | } 238 | 239 | private object MakeList(Type genericListItemType, Type destinationType, List listItems, string propertyName) 240 | { 241 | object result = null; 242 | 243 | if (listItems.Any() || IsRootProperty(propertyName)) 244 | { 245 | var listType = typeof(List<>).MakeGenericType(genericListItemType); 246 | 247 | var add = listType.GetMethod("Add"); 248 | var pValue = Activator.CreateInstance(listType); 249 | 250 | foreach (var listItem in listItems) 251 | { 252 | add.Invoke(pValue, new[] { listItem }); 253 | } 254 | 255 | if (destinationType.IsArray) 256 | { 257 | var toArrayMethod = listType.GetMethod("ToArray"); 258 | result = toArrayMethod.Invoke(pValue, new object[0]); 259 | } 260 | else 261 | { 262 | result = pValue; 263 | } 264 | } 265 | 266 | return result; 267 | } 268 | 269 | private List GetIndexedListItems(string origPropName, Type genericListItemType) 270 | { 271 | var res = new List(); 272 | int index = 0; 273 | while (true) 274 | { 275 | var propertyName = String.Format("{0}[{1}]", origPropName, index); 276 | var objValue = CreateObject(genericListItemType, propertyName); 277 | if (objValue != null) 278 | { 279 | res.Add(objValue); 280 | } 281 | else 282 | { 283 | break; 284 | } 285 | 286 | index++; 287 | } 288 | return res; 289 | } 290 | 291 | private bool TryGetAsCustomType(Type destinationType, string propertyName, out object propValue) 292 | { 293 | propValue = null; 294 | bool isCustomNonEnumerableType = destinationType.IsCustomNonEnumerableType(); 295 | if (isCustomNonEnumerableType && IsRootPropertyOrAnyChildPropertiesExistsInFormData(propertyName)) 296 | { 297 | propValue = Activator.CreateInstance(destinationType); 298 | foreach (PropertyInfo propertyInfo in destinationType.GetProperties().Where(m => m.SetMethod != null)) 299 | { 300 | var propName = (!String.IsNullOrEmpty(propertyName) ? propertyName + "." : "") + propertyInfo.Name; 301 | 302 | var objValue = CreateObject(propertyInfo.PropertyType, propName); 303 | if (objValue != null) 304 | { 305 | propertyInfo.SetValue(propValue, objValue); 306 | } 307 | } 308 | } 309 | return isCustomNonEnumerableType; 310 | } 311 | 312 | 313 | private bool IsGenericDictionary(Type type, out Type keyType, out Type valueType) 314 | { 315 | var iDictType = GetGenericType(type, typeof(IDictionary<,>)); 316 | if (iDictType != null) 317 | { 318 | var types = iDictType.GetGenericArguments(); 319 | if (types.Length == 2) 320 | { 321 | keyType = types[0]; 322 | valueType = types[1]; 323 | return true; 324 | } 325 | } 326 | 327 | keyType = null; 328 | valueType = null; 329 | return false; 330 | } 331 | 332 | private bool IsGenericEnumerable(Type type, out Type itemType) 333 | { 334 | if (GetGenericType(type, typeof(IDictionary<,>)) == null //not a dictionary 335 | && !type.Equals(typeof(string))) //not a string 336 | { 337 | var enumerType = GetGenericType(type, typeof(IEnumerable<>)); 338 | if (enumerType != null) 339 | { 340 | Type[] genericArguments = enumerType.GetGenericArguments(); 341 | if (genericArguments.Length == 1) 342 | { 343 | itemType = genericArguments[0]; 344 | return true; 345 | } 346 | } 347 | } 348 | 349 | itemType = null; 350 | return false; 351 | } 352 | 353 | private Type GetGenericType(Type type, Type genericTypeDefinition) 354 | { 355 | return type.IsGenericType && genericTypeDefinition.Equals(type.GetGenericTypeDefinition()) 356 | ? type 357 | : type.GetInterface(genericTypeDefinition.Name); 358 | } 359 | 360 | private bool IsFileOrConvertableFromString(Type type) 361 | { 362 | if (type == typeof (HttpFile)) 363 | return true; 364 | 365 | return type.GetFromStringConverter() != null; 366 | } 367 | 368 | private bool IsNotNullableValueType(Type type) 369 | { 370 | if (!type.IsValueType) 371 | return false; 372 | 373 | return Nullable.GetUnderlyingType(type) == null; 374 | } 375 | 376 | private bool IsNeedValidateMissedProperty(string propertyName) 377 | { 378 | return Settings.ValidateNonNullableMissedProperty 379 | && !IsIndexedProperty(propertyName) 380 | && IsRootPropertyOrAnyParentsPropertyExistsInFormData(propertyName); 381 | } 382 | 383 | private bool IsRootPropertyOrAnyParentsPropertyExistsInFormData(string propertyName) 384 | { 385 | string parentName = ""; 386 | if (propertyName != null) 387 | { 388 | int lastDotIndex = propertyName.LastIndexOf('.'); 389 | if (lastDotIndex >= 0) 390 | { 391 | parentName = propertyName.Substring(0, lastDotIndex); 392 | } 393 | } 394 | 395 | bool result = IsRootPropertyOrAnyChildPropertiesExistsInFormData(parentName); 396 | return result; 397 | } 398 | 399 | private bool IsRootPropertyOrAnyChildPropertiesExistsInFormData(string propertyName) 400 | { 401 | if (IsRootProperty(propertyName)) 402 | return true; 403 | 404 | string prefixWithDot = propertyName + "."; 405 | bool result = SourceData.GetAllKeys().Any(m => m.StartsWith(prefixWithDot, true, Settings.CultureInfo)); 406 | return result; 407 | } 408 | 409 | private bool IsRootProperty(string propertyName) 410 | { 411 | return propertyName == ""; 412 | } 413 | 414 | private bool IsIndexedProperty(string propName) 415 | { 416 | return propName != null && propName.EndsWith("]"); 417 | } 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /src/MultipartDataMediaFormatter/Converters/HttpContentToFormDataConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Net.Http.Headers; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using MultipartDataMediaFormatter.Infrastructure; 10 | 11 | namespace MultipartDataMediaFormatter.Converters 12 | { 13 | public class HttpContentToFormDataConverter 14 | { 15 | public async Task Convert(HttpContent content) 16 | { 17 | if(content == null) 18 | throw new ArgumentNullException("content"); 19 | 20 | //commented to provide more details about incorrectly formatted data from ReadAsMultipartAsync method 21 | /*if (!content.IsMimeMultipartContent()) 22 | { 23 | throw new Exception("Unsupported Media Type"); 24 | }*/ 25 | 26 | //http://stackoverflow.com/questions/15201255/request-content-readasmultipartasync-never-returns 27 | MultipartMemoryStreamProvider multipartProvider = null; 28 | await Task.Factory 29 | .StartNew(() => multipartProvider = content.ReadAsMultipartAsync().Result, 30 | CancellationToken.None, 31 | TaskCreationOptions.LongRunning, // guarantees separate thread 32 | TaskScheduler.Default); 33 | 34 | var multipartFormData = await Convert(multipartProvider); 35 | return multipartFormData; 36 | } 37 | 38 | public async Task Convert(MultipartMemoryStreamProvider multipartProvider) 39 | { 40 | var multipartFormData = new FormData(); 41 | 42 | foreach (var file in multipartProvider.Contents.Where(x => IsFile(x.Headers.ContentDisposition))) 43 | { 44 | var name = FixName(file.Headers.ContentDisposition.Name); 45 | string fileName = FixFilename(file.Headers.ContentDisposition.FileName); 46 | string mediaType = file.Headers.ContentType?.MediaType; 47 | 48 | using (var stream = await file.ReadAsStreamAsync()) 49 | { 50 | byte[] buffer = ReadAllBytes(stream); 51 | if (buffer.Length >= 0) 52 | { 53 | multipartFormData.Add(name, new HttpFile(fileName, mediaType, buffer)); 54 | } 55 | } 56 | } 57 | 58 | foreach (var part in multipartProvider.Contents.Where(x => x.Headers.ContentDisposition.DispositionType == "form-data" 59 | && !IsFile(x.Headers.ContentDisposition))) 60 | { 61 | var name = FixName(part.Headers.ContentDisposition.Name); 62 | var data = await part.ReadAsStringAsync(); 63 | multipartFormData.Add(name, data); 64 | } 65 | 66 | return multipartFormData; 67 | } 68 | 69 | private bool IsFile(ContentDispositionHeaderValue disposition) 70 | { 71 | return !string.IsNullOrEmpty(disposition.FileName); 72 | } 73 | 74 | private static string FixName(string token) 75 | { 76 | var res = UnquoteToken(token); 77 | return NormalizeJQueryToMvc(res); 78 | } 79 | 80 | /// 81 | /// Remove bounding quotes on a token if present 82 | /// 83 | private static string UnquoteToken(string token) 84 | { 85 | if (String.IsNullOrWhiteSpace(token)) 86 | { 87 | return token; 88 | } 89 | 90 | if (token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal) && token.Length > 1) 91 | { 92 | return token.Substring(1, token.Length - 2); 93 | } 94 | 95 | return token; 96 | } 97 | 98 | // This is a helper method to use Model Binding over a JQuery syntax. 99 | // Normalize from JQuery to MVC keys. The model binding infrastructure uses MVC keys 100 | // x[] --> x 101 | // [] --> "" 102 | // x[field] --> x.field, where field is not a number 103 | private static string NormalizeJQueryToMvc(string key) 104 | { 105 | if (key == null) 106 | { 107 | return string.Empty; 108 | } 109 | 110 | StringBuilder sb = new StringBuilder(); 111 | int i = 0; 112 | while (true) 113 | { 114 | int indexOpen = key.IndexOf('[', i); 115 | if (indexOpen < 0) 116 | { 117 | sb.Append(key, i, key.Length - i); 118 | break; // no more brackets 119 | } 120 | 121 | sb.Append(key, i, indexOpen - i); // everything up to "[" 122 | 123 | // Find closing bracket. 124 | int indexClose = key.IndexOf(']', indexOpen); 125 | if (indexClose == -1) 126 | { 127 | throw new Exception($"Error find closing bracket in key \"{key}\""); 128 | } 129 | 130 | if (indexClose == indexOpen + 1) 131 | { 132 | // Empty bracket. Signifies array. Just remove. 133 | } 134 | else 135 | { 136 | if (char.IsDigit(key[indexOpen + 1])) 137 | { 138 | // array index. Leave unchanged. 139 | sb.Append(key, indexOpen, indexClose - indexOpen + 1); 140 | } 141 | else 142 | { 143 | // Field name. Convert to dot notation. 144 | sb.Append('.'); 145 | sb.Append(key, indexOpen + 1, indexClose - indexOpen - 1); 146 | } 147 | } 148 | 149 | i = indexClose + 1; 150 | if (i >= key.Length) 151 | { 152 | break; // end of string 153 | } 154 | } 155 | return sb.ToString(); 156 | } 157 | 158 | /// 159 | /// Amend filenames to remove surrounding quotes and remove path from IE 160 | /// 161 | private static string FixFilename(string originalFileName) 162 | { 163 | if (string.IsNullOrWhiteSpace(originalFileName)) 164 | return string.Empty; 165 | 166 | var result = originalFileName.Trim(); 167 | 168 | // remove leading and trailing quotes 169 | result = result.Trim('"'); 170 | 171 | // remove full path versions 172 | if (result.Contains("\\")) 173 | result = Path.GetFileName(result); 174 | 175 | return result; 176 | } 177 | 178 | private byte[] ReadAllBytes(Stream input) 179 | { 180 | using (var stream = new MemoryStream()) 181 | { 182 | input.CopyTo(stream); 183 | return stream.ToArray(); 184 | } 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/MultipartDataMediaFormatter/Converters/ObjectToMultipartDataByteArrayConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Text; 6 | using MultipartDataMediaFormatter.Infrastructure; 7 | using MultipartDataMediaFormatter.Infrastructure.Extensions; 8 | 9 | namespace MultipartDataMediaFormatter.Converters 10 | { 11 | public class ObjectToMultipartDataByteArrayConverter 12 | { 13 | private MultipartFormatterSettings Settings { get; set; } 14 | 15 | public ObjectToMultipartDataByteArrayConverter(MultipartFormatterSettings settings) 16 | { 17 | if (settings == null) 18 | throw new ArgumentNullException("settings"); 19 | 20 | Settings = settings; 21 | } 22 | 23 | public byte[] Convert(object value, string boundary) 24 | { 25 | if(value == null) 26 | throw new ArgumentNullException("value"); 27 | if (String.IsNullOrWhiteSpace(boundary)) 28 | throw new ArgumentNullException("boundary"); 29 | 30 | List> propertiesList = ConvertObjectToFlatPropertiesList(value); 31 | 32 | byte[] buffer = GetMultipartFormDataBytes(propertiesList, boundary); 33 | return buffer; 34 | } 35 | 36 | private List> ConvertObjectToFlatPropertiesList(object value) 37 | { 38 | var propertiesList = new List>(); 39 | if (value is FormData) 40 | { 41 | FillFlatPropertiesListFromFormData((FormData) value, propertiesList); 42 | } 43 | else 44 | { 45 | FillFlatPropertiesListFromObject(value, "", propertiesList); 46 | } 47 | 48 | return propertiesList; 49 | } 50 | 51 | private void FillFlatPropertiesListFromFormData(FormData formData, List> propertiesList) 52 | { 53 | foreach (var field in formData.Fields) 54 | { 55 | propertiesList.Add(new KeyValuePair(field.Name, field.Value)); 56 | } 57 | foreach (var field in formData.Files) 58 | { 59 | propertiesList.Add(new KeyValuePair(field.Name, field.Value)); 60 | } 61 | } 62 | 63 | private void FillFlatPropertiesListFromObject(object obj, string prefix, List> propertiesList) 64 | { 65 | if (obj != null) 66 | { 67 | Type type = obj.GetType(); 68 | 69 | if (obj is IDictionary) 70 | { 71 | var dict = obj as IDictionary; 72 | int index = 0; 73 | foreach (var key in dict.Keys) 74 | { 75 | string indexedKeyPropName = String.Format("{0}[{1}].Key", prefix, index); 76 | FillFlatPropertiesListFromObject(key, indexedKeyPropName, propertiesList); 77 | 78 | string indexedValuePropName = String.Format("{0}[{1}].Value", prefix, index); 79 | FillFlatPropertiesListFromObject(dict[key], indexedValuePropName, propertiesList); 80 | 81 | index++; 82 | } 83 | } 84 | else if (obj is ICollection && !IsByteArrayConvertableToHttpFile(obj)) 85 | { 86 | var list = obj as ICollection; 87 | int index = 0; 88 | foreach (var indexedPropValue in list) 89 | { 90 | string indexedPropName = String.Format("{0}[{1}]", prefix, index); 91 | FillFlatPropertiesListFromObject(indexedPropValue, indexedPropName, propertiesList); 92 | 93 | index++; 94 | } 95 | } 96 | else if (type.IsCustomNonEnumerableType()) 97 | { 98 | foreach (var propertyInfo in type.GetProperties()) 99 | { 100 | string propName = String.IsNullOrWhiteSpace(prefix) 101 | ? propertyInfo.Name 102 | : String.Format("{0}.{1}", prefix, propertyInfo.Name); 103 | object propValue = propertyInfo.GetValue(obj); 104 | 105 | FillFlatPropertiesListFromObject(propValue, propName, propertiesList); 106 | } 107 | } 108 | else 109 | { 110 | propertiesList.Add(new KeyValuePair(prefix, obj)); 111 | } 112 | } 113 | } 114 | 115 | private byte[] GetMultipartFormDataBytes(List> postParameters, string boundary) 116 | { 117 | if (postParameters == null || !postParameters.Any()) 118 | throw new Exception("Cannot convert data to multipart/form-data format. No data found."); 119 | 120 | Encoding encoding = Encoding.UTF8; 121 | 122 | using (var formDataStream = new System.IO.MemoryStream()) 123 | { 124 | bool needsCLRF = false; 125 | 126 | foreach (var param in postParameters) 127 | { 128 | // Add a CRLF to allow multiple parameters to be added. 129 | // Skip it on the first parameter, add it to subsequent parameters. 130 | if (needsCLRF) 131 | formDataStream.Write(encoding.GetBytes("\r\n"), 0, encoding.GetByteCount("\r\n")); 132 | 133 | needsCLRF = true; 134 | 135 | if (param.Value is HttpFile || IsByteArrayConvertableToHttpFile(param.Value)) 136 | { 137 | HttpFile httpFileToUpload = param.Value is HttpFile 138 | ? (HttpFile) param.Value 139 | : new HttpFile(null, null, (byte[]) param.Value); 140 | 141 | // Add just the first part of this param, since we will write the file data directly to the Stream 142 | string header = 143 | string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"; filename=\"{2}\"\r\nContent-Type: {3}\r\n\r\n", 144 | boundary, 145 | param.Key, 146 | httpFileToUpload.FileName ?? param.Key, 147 | httpFileToUpload.MediaType ?? "application/octet-stream"); 148 | 149 | formDataStream.Write(encoding.GetBytes(header), 0, encoding.GetByteCount(header)); 150 | 151 | // Write the file data directly to the Stream, rather than serializing it to a string. 152 | formDataStream.Write(httpFileToUpload.Buffer, 0, httpFileToUpload.Buffer.Length); 153 | } 154 | else 155 | { 156 | string objString = ""; 157 | if (param.Value != null) 158 | { 159 | var typeConverter = param.Value.GetType().GetToStringConverter(); 160 | if (typeConverter != null) 161 | { 162 | objString = typeConverter.ConvertToString(null, Settings.CultureInfo, param.Value); 163 | } 164 | else 165 | { 166 | throw new Exception(String.Format("Type \"{0}\" cannot be converted to string", param.Value.GetType().FullName)); 167 | } 168 | } 169 | 170 | string postData = 171 | string.Format("--{0}\r\nContent-Disposition: form-data; name=\"{1}\"\r\n\r\n{2}", 172 | boundary, 173 | param.Key, 174 | objString); 175 | formDataStream.Write(encoding.GetBytes(postData), 0, encoding.GetByteCount(postData)); 176 | } 177 | } 178 | 179 | // Add the end of the request. Start with a newline 180 | string footer = "\r\n--" + boundary + "--\r\n"; 181 | formDataStream.Write(encoding.GetBytes(footer), 0, encoding.GetByteCount(footer)); 182 | 183 | byte[] formData = formDataStream.ToArray(); 184 | 185 | return formData; 186 | } 187 | } 188 | 189 | private bool IsByteArrayConvertableToHttpFile(object value) 190 | { 191 | return value is byte[] && Settings.SerializeByteArrayAsHttpFile; 192 | } 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/MultipartDataMediaFormatter/FormMultipartEncodedMediaTypeFormatter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Net.Http.Formatting; 7 | using System.Net.Http.Headers; 8 | using System.Threading.Tasks; 9 | using MultipartDataMediaFormatter.Converters; 10 | using MultipartDataMediaFormatter.Infrastructure; 11 | using MultipartDataMediaFormatter.Infrastructure.Logger; 12 | 13 | namespace MultipartDataMediaFormatter 14 | { 15 | public class FormMultipartEncodedMediaTypeFormatter : MediaTypeFormatter 16 | { 17 | private const string SupportedMediaType = "multipart/form-data"; 18 | 19 | private readonly MultipartFormatterSettings Settings; 20 | 21 | public FormMultipartEncodedMediaTypeFormatter(MultipartFormatterSettings settings = null) 22 | { 23 | Settings = settings ?? new MultipartFormatterSettings(); 24 | SupportedMediaTypes.Add(new MediaTypeHeaderValue(SupportedMediaType)); 25 | } 26 | 27 | public override bool CanReadType(Type type) 28 | { 29 | return true; 30 | } 31 | 32 | public override bool CanWriteType(Type type) 33 | { 34 | return true; 35 | } 36 | 37 | public override void SetDefaultContentHeaders(Type type, HttpContentHeaders headers, MediaTypeHeaderValue mediaType) 38 | { 39 | base.SetDefaultContentHeaders(type, headers, mediaType); 40 | 41 | //need add boundary 42 | //(if add when fill SupportedMediaTypes collection in class constructor then receive post with another boundary will not work - Unsupported Media Type exception will thrown) 43 | if (headers.ContentType == null) 44 | { 45 | headers.ContentType = new MediaTypeHeaderValue(SupportedMediaType); 46 | } 47 | if (!String.Equals(headers.ContentType.MediaType, SupportedMediaType, StringComparison.OrdinalIgnoreCase)) 48 | { 49 | throw new Exception("Not a Multipart Content"); 50 | } 51 | if (headers.ContentType.Parameters.All(m => m.Name != "boundary")) 52 | { 53 | headers.ContentType.Parameters.Add(new NameValueHeaderValue("boundary", "MultipartDataMediaFormatterBoundary1q2w3e")); 54 | } 55 | } 56 | 57 | public override async Task ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, 58 | IFormatterLogger formatterLogger) 59 | { 60 | var httpContentToFormDataConverter = new HttpContentToFormDataConverter(); 61 | FormData multipartFormData = await httpContentToFormDataConverter.Convert(content); 62 | 63 | IFormDataConverterLogger logger; 64 | if (formatterLogger != null) 65 | logger = new FormatterLoggerAdapter(formatterLogger); 66 | else 67 | logger = new FormDataConverterLogger(); 68 | 69 | var dataToObjectConverter = new FormDataToObjectConverter(multipartFormData, logger, Settings); 70 | object result = dataToObjectConverter.Convert(type); 71 | 72 | logger.EnsureNoErrors(); 73 | 74 | return result; 75 | } 76 | 77 | public override async Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, 78 | TransportContext transportContext) 79 | { 80 | if (!content.IsMimeMultipartContent()) 81 | { 82 | throw new Exception("Not a Multipart Content"); 83 | } 84 | 85 | var boudaryParameter = content.Headers.ContentType.Parameters.FirstOrDefault(m => m.Name == "boundary" && !String.IsNullOrWhiteSpace(m.Value)); 86 | if (boudaryParameter == null) 87 | { 88 | throw new Exception("multipart boundary not found"); 89 | } 90 | 91 | var objectToMultipartDataByteArrayConverter = new ObjectToMultipartDataByteArrayConverter(Settings); 92 | byte[] multipartData = objectToMultipartDataByteArrayConverter.Convert(value, boudaryParameter.Value); 93 | 94 | await writeStream.WriteAsync(multipartData, 0, multipartData.Length); 95 | 96 | content.Headers.ContentLength = multipartData.Length; 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /src/MultipartDataMediaFormatter/Infrastructure/Extensions/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.ComponentModel; 4 | using MultipartDataMediaFormatter.Infrastructure.TypeConverters; 5 | 6 | namespace MultipartDataMediaFormatter.Infrastructure.Extensions 7 | { 8 | internal static class TypeExtensions 9 | { 10 | internal static FromStringConverterAdapter GetFromStringConverter(this Type type) 11 | { 12 | TypeConverter typeConverter = TypeDescriptor.GetConverter(type); 13 | 14 | if (typeConverter is BooleanConverter) 15 | { 16 | //replace default boolean converter for deserializing on/off values received from html page 17 | typeConverter = new BooleanConverterEx(); 18 | } 19 | if (typeConverter != null && !typeConverter.CanConvertFrom(typeof(String))) 20 | { 21 | typeConverter = null; 22 | } 23 | 24 | return typeConverter == null ? null : new FromStringConverterAdapter(type, typeConverter); 25 | } 26 | 27 | internal static TypeConverter GetToStringConverter(this Type type) 28 | { 29 | TypeConverter typeConverter = TypeDescriptor.GetConverter(type); 30 | if (typeConverter is DateTimeConverter) 31 | { 32 | //replace default datetime converter for serializing datetime in ISO 8601 format 33 | typeConverter = new DateTimeConverterISO8601(); 34 | } 35 | if (typeConverter != null && !typeConverter.CanConvertTo(typeof(String))) 36 | { 37 | typeConverter = null; 38 | } 39 | return typeConverter; 40 | } 41 | 42 | internal static bool IsCustomNonEnumerableType(this Type type) 43 | { 44 | var nullType = Nullable.GetUnderlyingType(type); 45 | if (nullType != null) 46 | { 47 | type = nullType; 48 | } 49 | if (type.IsGenericType) 50 | { 51 | type = type.GetGenericTypeDefinition(); 52 | } 53 | return type != typeof(object) 54 | && Type.GetTypeCode(type) == TypeCode.Object 55 | && type != typeof(HttpFile) 56 | && type != typeof(Guid) 57 | && type.GetInterface(typeof(IEnumerable).Name) == null; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/MultipartDataMediaFormatter/Infrastructure/FormData.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | 6 | namespace MultipartDataMediaFormatter.Infrastructure 7 | { 8 | public class FormData 9 | { 10 | private List _Files; 11 | private List _Fields; 12 | 13 | public List Files 14 | { 15 | get 16 | { 17 | if(_Files == null) 18 | _Files = new List(); 19 | return _Files; 20 | } 21 | set 22 | { 23 | _Files = value; 24 | } 25 | } 26 | 27 | public List Fields 28 | { 29 | get 30 | { 31 | if(_Fields == null) 32 | _Fields = new List(); 33 | return _Fields; 34 | } 35 | set 36 | { 37 | _Fields = value; 38 | } 39 | } 40 | 41 | public List GetAllKeys() 42 | { 43 | return Fields.Select(m => m.Name).Concat(Files.Select(m => m.Name)).ToList(); 44 | } 45 | 46 | public void Add(string name, string value) 47 | { 48 | Fields.Add(new ValueString() { Name = name, Value = value}); 49 | } 50 | 51 | public void Add(string name, HttpFile value) 52 | { 53 | Files.Add(new ValueFile() { Name = name, Value = value }); 54 | } 55 | 56 | public bool TryGetValue(string name, CultureInfo culture, out string value) 57 | { 58 | var field = Fields.FirstOrDefault(m => culture.CompareInfo.Compare(m.Name, name, CompareOptions.IgnoreCase) == 0); 59 | if (field != null) 60 | { 61 | value = field.Value; 62 | return true; 63 | } 64 | value = null; 65 | return false; 66 | } 67 | 68 | public bool TryGetValue(string name, CultureInfo culture, out HttpFile value) 69 | { 70 | var field = Files.FirstOrDefault(m => culture.CompareInfo.Compare(m.Name, name, CompareOptions.IgnoreCase) == 0); 71 | if (field != null) 72 | { 73 | value = field.Value; 74 | return true; 75 | } 76 | value = null; 77 | return false; 78 | } 79 | 80 | public List GetValues(string name, CultureInfo culture) 81 | { 82 | return Fields 83 | .Where(m => culture.CompareInfo.Compare(m.Name, name, CompareOptions.IgnoreCase) == 0) 84 | .Select(m => m.Value) 85 | .ToList(); 86 | } 87 | 88 | public List GetFiles(string name, CultureInfo culture) 89 | { 90 | return Files 91 | .Where(m => culture.CompareInfo.Compare(m.Name, name, CompareOptions.IgnoreCase) == 0) 92 | .Select(m => m.Value) 93 | .ToList(); 94 | } 95 | 96 | public bool Contains(string name, CultureInfo culture) 97 | { 98 | string val; 99 | HttpFile file; 100 | 101 | return TryGetValue(name, culture, out val) || TryGetValue(name, culture, out file); 102 | } 103 | 104 | public class ValueString 105 | { 106 | public string Name { get; set; } 107 | public string Value { get; set; } 108 | } 109 | 110 | public class ValueFile 111 | { 112 | public string Name { get; set; } 113 | public HttpFile Value { get; set; } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/MultipartDataMediaFormatter/Infrastructure/HttpFile.cs: -------------------------------------------------------------------------------- 1 | namespace MultipartDataMediaFormatter.Infrastructure 2 | { 3 | public class HttpFile 4 | { 5 | public string FileName { get; set; } 6 | public string MediaType { get; set; } 7 | public byte[] Buffer { get; set; } 8 | 9 | public HttpFile() { } 10 | 11 | public HttpFile(string fileName, string mediaType, byte[] buffer) 12 | { 13 | FileName = fileName; 14 | MediaType = mediaType; 15 | Buffer = buffer; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/MultipartDataMediaFormatter/Infrastructure/Logger/FormDataConverterLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace MultipartDataMediaFormatter.Infrastructure.Logger 6 | { 7 | public class FormDataConverterLogger : IFormDataConverterLogger 8 | { 9 | private Dictionary> Errors { get; set; } 10 | 11 | public FormDataConverterLogger() 12 | { 13 | Errors = new Dictionary>(); 14 | } 15 | 16 | public void LogError(string errorPath, Exception exception) 17 | { 18 | AddError(errorPath, new LogErrorInfo(exception)); 19 | } 20 | 21 | public void LogError(string errorPath, string errorMessage) 22 | { 23 | AddError(errorPath, new LogErrorInfo(errorMessage)); 24 | } 25 | 26 | public List GetErrors() 27 | { 28 | return Errors.Select(m => new LogItem() 29 | { 30 | ErrorPath = m.Key, 31 | Errors = m.Value.Select(t => t).ToList() 32 | }).ToList(); 33 | } 34 | 35 | public void EnsureNoErrors() 36 | { 37 | if (Errors.Any()) 38 | { 39 | var errors = Errors 40 | .Select(m => String.Format("{0}: {1}", m.Key, String.Join(". ", m.Value.Select(x => (x.ErrorMessage ?? (x.Exception != null ? x.Exception.Message : "")))))) 41 | .ToList(); 42 | 43 | string errorMessage = String.Join(" ", errors); 44 | 45 | throw new Exception(errorMessage); 46 | } 47 | } 48 | 49 | private void AddError(string errorPath, LogErrorInfo info) 50 | { 51 | List listErrors; 52 | if (!Errors.TryGetValue(errorPath, out listErrors)) 53 | { 54 | listErrors = new List(); 55 | Errors.Add(errorPath, listErrors); 56 | } 57 | listErrors.Add(info); 58 | } 59 | 60 | public class LogItem 61 | { 62 | public string ErrorPath { get; set; } 63 | public List Errors { get; set; } 64 | } 65 | 66 | public class LogErrorInfo 67 | { 68 | public string ErrorMessage { get; private set; } 69 | public Exception Exception { get; private set; } 70 | public bool IsException { get; private set; } 71 | 72 | public LogErrorInfo(string errorMessage) 73 | { 74 | ErrorMessage = errorMessage; 75 | IsException = false; 76 | } 77 | 78 | public LogErrorInfo(Exception exception) 79 | { 80 | Exception = exception; 81 | IsException = true; 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/MultipartDataMediaFormatter/Infrastructure/Logger/FormatterLoggerAdapter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http.Formatting; 3 | 4 | namespace MultipartDataMediaFormatter.Infrastructure.Logger 5 | { 6 | internal class FormatterLoggerAdapter : IFormDataConverterLogger 7 | { 8 | private IFormatterLogger FormatterLogger { get; set; } 9 | 10 | public FormatterLoggerAdapter(IFormatterLogger formatterLogger) 11 | { 12 | if(formatterLogger == null) 13 | throw new ArgumentNullException("formatterLogger"); 14 | FormatterLogger = formatterLogger; 15 | } 16 | 17 | public void LogError(string errorPath, Exception exception) 18 | { 19 | FormatterLogger.LogError(errorPath, exception); 20 | } 21 | 22 | public void LogError(string errorPath, string errorMessage) 23 | { 24 | FormatterLogger.LogError(errorPath, errorMessage); 25 | } 26 | 27 | public void EnsureNoErrors() 28 | { 29 | //nothing to do 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/MultipartDataMediaFormatter/Infrastructure/Logger/IFormDataConverterLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MultipartDataMediaFormatter.Infrastructure.Logger 4 | { 5 | public interface IFormDataConverterLogger 6 | { 7 | /// 8 | /// Logs an error. 9 | /// 10 | /// The path to the member for which the error is being logged. 11 | /// The exception to be logged. 12 | void LogError(string errorPath, Exception exception); 13 | 14 | /// 15 | /// Logs an error. 16 | /// 17 | /// The path to the member for which the error is being logged. 18 | /// The error message to be logged. 19 | void LogError(string errorPath, string errorMessage); 20 | 21 | /// 22 | /// throw exception if errors found 23 | /// 24 | void EnsureNoErrors(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/MultipartDataMediaFormatter/Infrastructure/MultipartFormatterSettings.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace MultipartDataMediaFormatter.Infrastructure 4 | { 5 | public class MultipartFormatterSettings 6 | { 7 | /// 8 | /// serialize byte array property as HttpFile when sending data if true or as indexed array if false 9 | /// (default value is "false) 10 | /// 11 | public bool SerializeByteArrayAsHttpFile { get; set; } 12 | 13 | /// 14 | /// add validation error "The value is required." if no value is present in request for non-nullable property if this parameter is "true" 15 | /// (default value is "false) 16 | /// 17 | public bool ValidateNonNullableMissedProperty { get; set; } 18 | 19 | private CultureInfo _CultureInfo; 20 | /// 21 | /// default is CultureInfo.CurrentCulture 22 | /// 23 | public CultureInfo CultureInfo 24 | { 25 | get { return _CultureInfo ?? CultureInfo.CurrentCulture; } 26 | set { _CultureInfo = value; } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/MultipartDataMediaFormatter/Infrastructure/TypeConverters/BooleanConverterEx.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Globalization; 4 | 5 | namespace MultipartDataMediaFormatter.Infrastructure.TypeConverters 6 | { 7 | public class BooleanConverterEx : BooleanConverter 8 | { 9 | public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) 10 | { 11 | if (value != null) 12 | { 13 | var str = value.ToString(); 14 | 15 | if (String.Compare(str, "on", culture, CompareOptions.IgnoreCase) == 0) 16 | return true; 17 | 18 | if (String.Compare(str, "off", culture, CompareOptions.IgnoreCase) == 0) 19 | return false; 20 | } 21 | 22 | return base.ConvertFrom(context, culture, value); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/MultipartDataMediaFormatter/Infrastructure/TypeConverters/DateTimeConverterISO8601.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Globalization; 4 | 5 | namespace MultipartDataMediaFormatter.Infrastructure.TypeConverters 6 | { 7 | /// 8 | /// convert datetime to ISO 8601 format string 9 | /// 10 | internal class DateTimeConverterISO8601 : DateTimeConverter 11 | { 12 | public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) 13 | { 14 | if (value != null && value is DateTime && destinationType == typeof (string)) 15 | { 16 | return ((DateTime)value).ToString("O"); // ISO 8601 17 | } 18 | return base.ConvertTo(context, culture, value, destinationType); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/MultipartDataMediaFormatter/Infrastructure/TypeConverters/FromStringConverterAdapter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Globalization; 4 | 5 | namespace MultipartDataMediaFormatter.Infrastructure.TypeConverters 6 | { 7 | public class FromStringConverterAdapter 8 | { 9 | private readonly Type Type; 10 | private readonly TypeConverter TypeConverter; 11 | public FromStringConverterAdapter(Type type, TypeConverter typeConverter) 12 | { 13 | if(type == null) 14 | throw new ArgumentNullException("type"); 15 | if (typeConverter == null) 16 | throw new ArgumentNullException("typeConverter"); 17 | 18 | Type = type; 19 | TypeConverter = typeConverter; 20 | } 21 | 22 | public object ConvertFromString(string src, CultureInfo culture) 23 | { 24 | var isUndefinedNullable = Nullable.GetUnderlyingType(Type) != null && src == "undefined"; 25 | if (isUndefinedNullable) 26 | return null; 27 | 28 | return TypeConverter.ConvertFromString(null, culture, src); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/MultipartDataMediaFormatter/MultipartDataMediaFormatter.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netstandard2.0;net45 4 | true 5 | StrongNameKey.snk 6 | true 7 | MultipartDataMediaFormatter.V2 8 | Alexander Kozlovskiy 9 | https://github.com/iLexDev/ASP.NET-WebApi-MultipartDataMediaFormatter 10 | LICENSE.txt 11 | Asp.Net WebApi web api MediaFormatters MultipartData Multipart MultipartFormData multipart/form-data 12 | embedded 13 | 2.1.1.0 14 | 2.1.1.0 15 | ASP.NET WebApi MultipartDataMediaFormatter 16 | ASP.NET WebApi MultipartDataMediaFormatter 17 | A library for binding custom types (including files) when sending and receiving multipart encoded form data 18 | Copyright © Alexander Kozlovskiy 2021 19 | https://github.com/iLexDev/ASP.NET-WebApi-MultipartDataMediaFormatter.git 20 | git 21 | true 22 | true 23 | true 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | True 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/MultipartDataMediaFormatter/StrongNameKey.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iLexDev/ASP.NET-WebApi-MultipartDataMediaFormatter/f71f640a127628733f1cee48d0d96f43628d40a8/src/MultipartDataMediaFormatter/StrongNameKey.snk -------------------------------------------------------------------------------- /src/MultipartDataMediaFormatter/licenses/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2021 Alexander Kozlovskiy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /test/MultipartDataMediaFormatter.Tests/Controllers/TestApiController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Linq; 4 | using System.Web.Http; 5 | using MultipartDataMediaFormatter.Converters; 6 | using MultipartDataMediaFormatter.Infrastructure; 7 | using MultipartDataMediaFormatter.Infrastructure.Logger; 8 | using MultipartDataMediaFormatter.Tests.Models; 9 | 10 | namespace MultipartDataMediaFormatter.Tests.Controllers 11 | { 12 | public class TestApiController : ApiController 13 | { 14 | private readonly MultipartFormatterSettings Settings = new MultipartFormatterSettings() 15 | { 16 | SerializeByteArrayAsHttpFile = true, 17 | CultureInfo = CultureInfo.GetCultureInfo("en-US"), 18 | ValidateNonNullableMissedProperty = true 19 | }; 20 | 21 | [HttpPost] 22 | public ApiResult PostPerson(PersonModel model) 23 | { 24 | return GetApiResult(model); 25 | } 26 | 27 | [HttpPost] 28 | public ApiResult PostPersonBindRawFormData(FormData formData) 29 | { 30 | var logger = new FormDataConverterLogger(); 31 | var dataToObjectConverter = new FormDataToObjectConverter(formData, logger, Settings); 32 | 33 | var person = (PersonModel)dataToObjectConverter.Convert(typeof(PersonModel)); 34 | logger.EnsureNoErrors(); 35 | 36 | return GetApiResult(person); 37 | } 38 | 39 | [HttpPost] 40 | public ApiResult PostFile(HttpFile file) 41 | { 42 | return GetApiResult(file); 43 | } 44 | 45 | [HttpPost] 46 | public ApiResult PostFileBindRawFormData(FormData formData) 47 | { 48 | HttpFile file; 49 | formData.TryGetValue("", Settings.CultureInfo, out file); 50 | return GetApiResult(file); 51 | } 52 | 53 | [HttpPost] 54 | public ApiResult PostString([FromBody] string data) 55 | { 56 | return GetApiResult(data); 57 | } 58 | 59 | [HttpPost] 60 | public ApiResult PostStringBindRawFormData(FormData formData) 61 | { 62 | string data; 63 | formData.TryGetValue("", Settings.CultureInfo, out data); 64 | return GetApiResult(data); 65 | } 66 | 67 | [HttpPost] 68 | public ApiResult PostFormData(FormData formData) 69 | { 70 | return GetApiResult(formData); 71 | } 72 | 73 | 74 | [HttpPost] 75 | public ApiResult PostEmptyModel(EmptyModel model) 76 | { 77 | return GetApiResult(model); 78 | } 79 | 80 | 81 | private ApiResult GetApiResult(T value) 82 | { 83 | var result = new ApiResult() { Value = value }; 84 | if (!ModelState.IsValid) 85 | { 86 | var errors = ModelState 87 | .Select(m => String.Format("{0}: {1}", m.Key, String.Join(". ", m.Value.Errors.Select(x => (x.ErrorMessage ?? (x.Exception != null ? x.Exception.Message : "")))))) 88 | .ToList(); 89 | 90 | result.ErrorMessage = String.Join(" ", errors); 91 | } 92 | return result; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /test/MultipartDataMediaFormatter.Tests/Infrastructure/WebApiHttpServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Http.Formatting; 4 | using System.Web.Http; 5 | using Microsoft.Owin.Testing; 6 | using Owin; 7 | 8 | namespace MultipartDataMediaFormatter.Tests.Infrastructure 9 | { 10 | public class WebApiHttpServer : IDisposable 11 | { 12 | private readonly TestServer Server; 13 | 14 | public WebApiHttpServer(MediaTypeFormatter formatter) 15 | { 16 | Server = TestServer.Create(builder => 17 | { 18 | var config = new HttpConfiguration(); 19 | 20 | config.Formatters.Clear(); 21 | config.Formatters.Add(formatter); 22 | config.Routes.MapHttpRoute( 23 | "API Default", "{controller}/{action}", 24 | new { id = RouteParameter.Optional }); 25 | 26 | builder.UseWebApi(config); 27 | }); 28 | } 29 | 30 | public HttpClient CreateClient() 31 | { 32 | return Server.HttpClient; 33 | } 34 | 35 | 36 | public void Dispose() 37 | { 38 | Server.Dispose(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/MultipartDataMediaFormatter.Tests/Models/ApiResult.cs: -------------------------------------------------------------------------------- 1 | using MultipartDataMediaFormatter.Infrastructure; 2 | 3 | namespace MultipartDataMediaFormatter.Tests.Models 4 | { 5 | public class ApiResult 6 | { 7 | public string ErrorMessage { get; set; } 8 | public T Value { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/MultipartDataMediaFormatter.Tests/Models/PersonModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using MultipartDataMediaFormatter.Infrastructure; 5 | 6 | namespace MultipartDataMediaFormatter.Tests.Models 7 | { 8 | public class PersonModel 9 | { 10 | public Guid PersonId { get; set; } 11 | 12 | public string FirstName { get; set; } 13 | 14 | [Required] 15 | public string LastName { get; set; } 16 | 17 | public DateTime? RegisteredDateTime { get; set; } 18 | 19 | public DateTime CreatedDateTime { get; set; } 20 | 21 | public int? Age { get; set; } 22 | 23 | public decimal? Score { get; set; } 24 | 25 | public double? ScoreScaleFactor { get; set; } 26 | 27 | public float? ActivityProgress { get; set; } 28 | 29 | public bool IsActive { get; set; } 30 | 31 | public PersonTypes? PersonType { get; set; } 32 | 33 | [Required] 34 | public HttpFile Photo { get; set; } 35 | 36 | public Dictionary Properties { get; set; } 37 | 38 | public List Roles { get; set; } 39 | 40 | public List Attachments { get; set; } 41 | 42 | public SomeValue SomeGenericProperty { get; set; } 43 | 44 | public List Ints { get; set; } 45 | 46 | public IDictionary IntProperties { get; set; } 47 | 48 | public byte[] Bytes { get; set; } 49 | 50 | public List Years { get; set; } 51 | 52 | public List ConnectedPersons { get; set; } 53 | 54 | public IEnumerable PersonsCollection { get; set; } 55 | public PersonProperty SomeProperty { get; set; } 56 | } 57 | 58 | public class PersonProperty 59 | { 60 | public int PropertyCode { get; set; } 61 | public string PropertyName { get; set; } 62 | public byte[] Bytes { get; set; } 63 | } 64 | 65 | public class PersonRole 66 | { 67 | public int RoleId { get; set; } 68 | public string RoleName { get; set; } 69 | public List Rights { get; set; } 70 | } 71 | 72 | public class SomeValue 73 | { 74 | public string Name { get; set; } 75 | [Required] 76 | public T GenericValue { get; set; } 77 | } 78 | 79 | public enum PersonTypes 80 | { 81 | Admin, 82 | User 83 | } 84 | 85 | public class EmptyModel 86 | { 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test/MultipartDataMediaFormatter.Tests/Models/ResponseErrorItem.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | 7 | namespace MultipartDataMediaFormatter.Tests.Models 8 | { 9 | public class ResponseErrorItem 10 | { 11 | public string Key { get; set; } 12 | public string Value { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/MultipartDataMediaFormatter.Tests/MultipartDataMediaFormatter.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net452 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/MultipartDataMediaFormatter.Tests/Tests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Http; 7 | using System.Net.Http.Formatting; 8 | using System.Net.Http.Headers; 9 | using KellermanSoftware.CompareNetObjects; 10 | using MultipartDataMediaFormatter.Infrastructure; 11 | using MultipartDataMediaFormatter.Tests.Infrastructure; 12 | using MultipartDataMediaFormatter.Tests.Models; 13 | using Xunit; 14 | using Xunit.Sdk; 15 | 16 | namespace MultipartDataMediaFormatter.Tests 17 | { 18 | public class Tests 19 | { 20 | public Tests() 21 | { 22 | //need for correct comparing validation messages 23 | var enCulture = CultureInfo.GetCultureInfo("en-US"); 24 | CultureInfo.DefaultThreadCurrentUICulture = enCulture; 25 | CultureInfo.DefaultThreadCurrentCulture = enCulture; 26 | } 27 | 28 | [Fact] 29 | public void TestComplexModelPost() 30 | { 31 | TestPost(PreparePersonModel(), "TestApi/PostPerson"); 32 | } 33 | 34 | [Fact] 35 | public void TestModelWithoutPropertiesPost() 36 | { 37 | var personModel = new EmptyModel(); 38 | TestPost(personModel, "TestApi/PostEmptyModel", "Cannot convert data to multipart/form-data format. No data found."); 39 | } 40 | 41 | [Fact] 42 | public void TestComplexModelWithValidationErrorsPost() 43 | { 44 | TestPost(PreparePersonModelWithValidationErrors(), "TestApi/PostPerson", 45 | "model.LastName: The LastName field is required. model.Photo: The Photo field is required. model.SomeGenericProperty.GenericValue: The GenericValue field is required."); 46 | } 47 | 48 | [Fact] 49 | public void TestComplexModelPostAndApiActionBindAsRawFormData() 50 | { 51 | TestPost(PreparePersonModel(), "TestApi/PostPersonBindRawFormData"); 52 | } 53 | 54 | [Fact] 55 | public void TestFilePost() 56 | { 57 | TestPost(PrepareFileModel(), "TestApi/PostFile"); 58 | } 59 | 60 | [Fact] 61 | public void TestEmptyFilePost() 62 | { 63 | TestPost(new HttpFile("testImage.png", "images/png", new byte[] { }), "TestApi/PostFile"); 64 | } 65 | 66 | [Fact] 67 | public void TestFilePostAndApiActionBindAsRawFormData() 68 | { 69 | TestPost(PrepareFileModel(), "TestApi/PostFileBindRawFormData"); 70 | } 71 | 72 | 73 | [Fact] 74 | public void TestStringPost() 75 | { 76 | TestPost(PrepareStringModel(), "TestApi/PostString"); 77 | } 78 | 79 | [Fact] 80 | public void TestStringPostAndApiActionBindAsRawFormData() 81 | { 82 | TestPost(PrepareStringModel(), "TestApi/PostStringBindRawFormData"); 83 | } 84 | 85 | 86 | [Fact] 87 | public void TestFormDataPost() 88 | { 89 | TestPost(PrepareFormDataModel(), "TestApi/PostFormData"); 90 | } 91 | 92 | [Fact] 93 | public void TestPostWithoutFormatter() 94 | { 95 | PersonModel model; 96 | var httpContent = PreparePersonModelHttpContent(out model); 97 | 98 | var result = PostPersonModelHttpContent(httpContent); 99 | 100 | Assert.Equal("model.PersonId: The value is required. model.CreatedDateTime: The value is required.", result.ErrorMessage); 101 | 102 | AssertModelsEquals(model, result.Value); 103 | } 104 | 105 | [Fact] 106 | public void TestPostWithoutFormatterNotNullableValidationNotRequired() 107 | { 108 | PersonModel model; 109 | var httpContent = PreparePersonModelHttpContent(out model); 110 | 111 | var formatter = new FormMultipartEncodedMediaTypeFormatter(new MultipartFormatterSettings() 112 | { 113 | SerializeByteArrayAsHttpFile = true, 114 | CultureInfo = CultureInfo.CurrentCulture, 115 | ValidateNonNullableMissedProperty = false 116 | }); 117 | 118 | var result = PostPersonModelHttpContent(httpContent, formatter); 119 | 120 | Assert.Null(result.ErrorMessage); 121 | 122 | AssertModelsEquals(model, result.Value); 123 | } 124 | 125 | [Fact] 126 | public void TestPostWithoutFormatterSerializeByteArrayAsIndexedArray() 127 | { 128 | PersonModel model; 129 | var httpContent = PreparePersonModelHttpContent(out model); 130 | 131 | var formatter = new FormMultipartEncodedMediaTypeFormatter(new MultipartFormatterSettings() 132 | { 133 | SerializeByteArrayAsHttpFile = false, 134 | CultureInfo = CultureInfo.CurrentCulture, 135 | ValidateNonNullableMissedProperty = false 136 | }); 137 | 138 | var result = PostPersonModelHttpContent(httpContent, formatter); 139 | 140 | Assert.Null(result.ErrorMessage); 141 | 142 | AssertModelsEquals(model, result.Value); 143 | } 144 | 145 | private void TestPost(T model, string url, string errorMessage = null) 146 | { 147 | ApiResult result = null; 148 | try 149 | { 150 | result = PostModel(model, url); 151 | } 152 | catch (Exception ex) 153 | { 154 | if (errorMessage != ex.GetBaseException().Message) 155 | { 156 | throw; 157 | } 158 | } 159 | 160 | if (result != null) 161 | { 162 | if (String.IsNullOrWhiteSpace(errorMessage)) 163 | { 164 | Assert.True(String.IsNullOrWhiteSpace(result.ErrorMessage), result.ErrorMessage); 165 | AssertModelsEquals(model, result.Value); 166 | } 167 | else 168 | { 169 | Assert.True(errorMessage == result.ErrorMessage, "Invalid ErrorMessage"); 170 | } 171 | } 172 | } 173 | 174 | private void AssertModelsEquals(object originalModel, object returnedModel) 175 | { 176 | var compareObjects = new CompareLogic(new ComparisonConfig() {MaxDifferences = 10 }); 177 | var comparisonResult = compareObjects.Compare(originalModel, returnedModel); 178 | Assert.True(comparisonResult.AreEqual, $"Source model is not the same as returned model. {comparisonResult.DifferencesString}"); 179 | } 180 | 181 | private ApiResult PostModel(T model, string url) 182 | { 183 | var mediaTypeFormatter = GetFormatter(); 184 | 185 | using(var server = new WebApiHttpServer(mediaTypeFormatter)) 186 | using(var client = server.CreateClient()) 187 | using (HttpResponseMessage response = client.PostAsync(url, model, mediaTypeFormatter).Result) 188 | { 189 | ApiResult resultModel; 190 | if (response.StatusCode != HttpStatusCode.OK) 191 | { 192 | var error = response.Content.ReadAsAsync>(new[] { mediaTypeFormatter }).Result; 193 | var err = error.Where(m => m.Key == "ExceptionMessage").Select(m => m.Value).FirstOrDefault(); 194 | if (String.IsNullOrWhiteSpace(err)) 195 | { 196 | var responseContent = response.Content.ReadAsStringAsync().Result; 197 | throw new XunitException(responseContent); 198 | } 199 | resultModel = new ApiResult() 200 | { 201 | ErrorMessage = err 202 | }; 203 | } 204 | else 205 | { 206 | resultModel = response.Content.ReadAsAsync>(new[] { mediaTypeFormatter }).Result; 207 | } 208 | return resultModel; 209 | } 210 | } 211 | 212 | private ApiResult PostPersonModelHttpContent(HttpContent httpContent, MediaTypeFormatter mediaTypeFormatter = null) 213 | { 214 | mediaTypeFormatter = mediaTypeFormatter ?? GetFormatter(); 215 | 216 | using (var server = new WebApiHttpServer(mediaTypeFormatter)) 217 | using (var client = server.CreateClient()) 218 | using (HttpResponseMessage response = client.PostAsync("TestApi/PostPerson", httpContent).Result) 219 | { 220 | if (response.StatusCode != HttpStatusCode.OK) 221 | { 222 | var err = response.Content.ReadAsStringAsync().Result; 223 | throw new XunitException(err); 224 | } 225 | var resultModel = response.Content.ReadAsAsync>(new[] { mediaTypeFormatter }).Result; 226 | return resultModel; 227 | } 228 | } 229 | 230 | private PersonModel PreparePersonModel() 231 | { 232 | return new PersonModel 233 | { 234 | PersonId = Guid.NewGuid(), 235 | FirstName = "John", 236 | LastName = "Doe", 237 | RegisteredDateTime = DateTime.Now, 238 | CreatedDateTime = DateTime.Now.AddDays(-10), 239 | Age = 33, 240 | Score = 150.7895m, 241 | ActivityProgress = 25.4587f, 242 | ScoreScaleFactor = 0.25879, 243 | IsActive = true, 244 | PersonType = PersonTypes.Admin, 245 | Photo = new HttpFile("photo.png", "image/png", new byte[] { 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }), 246 | SomeGenericProperty = new SomeValue() { Name = "newname", GenericValue = new PersonProperty() { PropertyCode = 8, PropertyName = "addname",}}, 247 | Properties = new Dictionary 248 | { 249 | { "first", new PersonProperty { PropertyCode = 1, PropertyName = "Alabama", Bytes = new byte[] { 11, 3, 24, 23 }} }, 250 | { "second", new PersonProperty { PropertyCode = 2, PropertyName = "New York" } } 251 | }, 252 | Roles = new List 253 | { 254 | new PersonRole { RoleId = 1, RoleName = "admin" }, 255 | new PersonRole { RoleId = 2, RoleName = "user" } 256 | }, 257 | Attachments = new List 258 | { 259 | new HttpFile("photo2.png", "image/png", new byte[] { 4, 3, 24, 23 }), 260 | new HttpFile("photo3.jpg", "image/jpg", new byte[] { 80, 31, 12, 3, 78, 45 }) 261 | }, 262 | ConnectedPersons = new List() 263 | { 264 | new PersonModel() 265 | { 266 | FirstName = "inner first name", 267 | LastName = "inner last name", 268 | Photo = new HttpFile("photo.png", "image/png", new byte[] { 0, 1, 2, 3, 7 }), 269 | Attachments = new List 270 | { 271 | new HttpFile("photo21.png", "image/png", new byte[] { 4, 3, 24, 24 }), 272 | }, 273 | IntProperties = new Dictionary() { { 1, 2 } }, 274 | }, 275 | new PersonModel() 276 | { 277 | FirstName = "inner first name 2", 278 | LastName = "inner last name 2", 279 | Photo = new HttpFile("photo.png", "image/png", new byte[] { 0, 1, 2, 3, 7 }), 280 | Attachments = new List 281 | { 282 | new HttpFile("photo211.png", "image/png", new byte[] { 4, 3, 24, 25 }), 283 | }, 284 | Bytes = new byte[] { 4, 3, 24, 23 }, 285 | Ints = new List() { 10 } 286 | } 287 | }, 288 | Ints = new List() { 10 }, 289 | IntProperties = new Dictionary() { { 1, 2 } }, 290 | Bytes = new byte[] { 4, 3, 24, 23 }, 291 | PersonsCollection = new List() 292 | { 293 | new PersonModel() 294 | { 295 | FirstName = "inner first name 3", 296 | LastName = "inner last name", 297 | Photo = new HttpFile("photo.png", "image/png", new byte[] { 0, 1, 2, 3, 7 }), 298 | Attachments = new List 299 | { 300 | new HttpFile("photo21.png", "image/png", new byte[] { 4, 3, 24, 24 }), 301 | }, 302 | IntProperties = new Dictionary() { { 1, 2 } }, 303 | }, 304 | new PersonModel() 305 | { 306 | FirstName = "inner first name 4", 307 | LastName = "inner last name 2", 308 | Photo = new HttpFile("photo.png", "image/png", new byte[] { 0, 1, 2, 3, 7 }), 309 | Attachments = new List 310 | { 311 | new HttpFile("photo211.png", "image/png", new byte[] { 4, 3, 24, 25 }), 312 | }, 313 | Bytes = new byte[] { 4, 3, 24, 23 }, 314 | Ints = new List() { 10 } 315 | } 316 | }, 317 | }; 318 | } 319 | 320 | private PersonModel PreparePersonModelWithValidationErrors() 321 | { 322 | return new PersonModel 323 | { 324 | FirstName = "John", 325 | SomeGenericProperty = new SomeValue() { Name = "newname" }, 326 | }; 327 | } 328 | 329 | private FormData PrepareFormDataModel() 330 | { 331 | var model = new FormData(); 332 | model.Add("first", "111"); 333 | model.Add("second", "string"); 334 | model.Add("file1", new HttpFile("photo2.png", "image/png", new byte[] { 4, 3, 24, 23 })); 335 | model.Add("file2", new HttpFile("photo3.jpg", "image/jpg", new byte[] { 80, 31, 12, 3, 78, 45 })); 336 | 337 | return model; 338 | } 339 | 340 | private HttpFile PrepareFileModel() 341 | { 342 | return new HttpFile("testImage.png", "images/png", new byte[] {10, 45, 7}); 343 | } 344 | 345 | private string PrepareStringModel() 346 | { 347 | return "some big text"; 348 | } 349 | 350 | private HttpContent PreparePersonModelHttpContent(out PersonModel personModel) 351 | { 352 | personModel = new PersonModel() 353 | { 354 | FirstName = "First", 355 | LastName = "Last", 356 | Photo = new HttpFile("photo.png", "image/png", new byte[] { 0, 1, 2, 3, 7 }), 357 | Years = new List() 358 | { 359 | 2001, 2010, 2015 360 | }, 361 | Roles = new List 362 | { 363 | new PersonRole() 364 | { 365 | RoleId = 1, 366 | Rights = new List(){ 1, 2, 5 } 367 | } 368 | }, 369 | IsActive = true, 370 | ActivityProgress = null, 371 | Attachments = new List() 372 | { 373 | new HttpFile("file1.tt", "text/plain", new byte[] { 1,3,5 }), 374 | new HttpFile("file2.cf", "text/plain", new byte[] { 4,2,5 }) 375 | }, 376 | SomeProperty = new PersonProperty() 377 | { 378 | PropertyName = "PROP", 379 | PropertyCode = 10, 380 | Bytes = new byte[] { 1, 2 } 381 | } 382 | }; 383 | 384 | var httpContent = new MultipartFormDataContent("testnewboundary"); 385 | 386 | httpContent.Add(new StringContent(personModel.LastName), "LastName"); 387 | httpContent.Add(new StringContent(personModel.FirstName), "FirstName"); 388 | httpContent.Add(new StringContent(personModel.ActivityProgress == null ? "undefined" : personModel.ActivityProgress.ToString()), "ActivityProgress"); 389 | httpContent.Add(new StringContent(personModel.IsActive ? "on" : "off"), "IsActive"); 390 | 391 | 392 | httpContent.Add(new StringContent(personModel.SomeProperty.PropertyName), "SomeProperty[PropertyName]"); 393 | httpContent.Add(new StringContent(personModel.SomeProperty.PropertyCode.ToString()), "SomeProperty[PropertyCode]"); 394 | for (int i = 0; i < personModel.SomeProperty.Bytes.Length; i++) 395 | { 396 | httpContent.Add(new StringContent(personModel.SomeProperty.Bytes[i].ToString()), $"SomeProperty[Bytes][{i}]"); 397 | } 398 | 399 | foreach (var year in personModel.Years) 400 | { 401 | httpContent.Add(new StringContent(year.ToString()), "Years"); 402 | } 403 | 404 | httpContent.Add(new StringContent(personModel.Roles[0].RoleId.ToString()), "Roles[0].RoleId"); 405 | foreach (var right in personModel.Roles[0].Rights) 406 | { 407 | httpContent.Add(new StringContent(right.ToString()), "Roles[0].Rights"); 408 | } 409 | 410 | var fileContent = new ByteArrayContent(personModel.Photo.Buffer); 411 | fileContent.Headers.ContentType = new MediaTypeHeaderValue(personModel.Photo.MediaType); 412 | fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") 413 | { 414 | FileName = personModel.Photo.FileName, 415 | Name = "Photo" 416 | }; 417 | httpContent.Add(fileContent); 418 | 419 | for (int i = 0; i < personModel.Attachments.Count; i++) 420 | { 421 | var attachment = personModel.Attachments[i]; 422 | var content = new ByteArrayContent(attachment.Buffer); 423 | content.Headers.ContentType = new MediaTypeHeaderValue(attachment.MediaType); 424 | content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") 425 | { 426 | FileName = attachment.FileName, 427 | Name = "Attachments" 428 | }; 429 | httpContent.Add(content); 430 | } 431 | 432 | return httpContent; 433 | } 434 | 435 | private MediaTypeFormatter GetFormatter() 436 | { 437 | return new FormMultipartEncodedMediaTypeFormatter(new MultipartFormatterSettings() 438 | { 439 | SerializeByteArrayAsHttpFile = true, 440 | CultureInfo = CultureInfo.CurrentCulture, 441 | ValidateNonNullableMissedProperty = true 442 | }); 443 | } 444 | } 445 | } 446 | --------------------------------------------------------------------------------