Skip to content

Commit ddf28ee

Browse files
committed
POST multipart/form-data support, IFormFile and upload
1 parent 7560489 commit ddf28ee

File tree

12 files changed

+192
-42
lines changed

12 files changed

+192
-42
lines changed

README.md

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Flying Proxy aims to:
1717

1818
### Usage for Client Side
1919

20-
#### Startup ConfigureServices (ASP.NET Core method gets called by the runtime.)
20+
#### Startup ConfigureServices
2121
```csharp
2222
public void ConfigureServices(IServiceCollection services)
2323
{
@@ -33,7 +33,7 @@ public void ConfigureServices(IServiceCollection services)
3333
}
3434
```
3535

36-
#### Example Dependency Injection
36+
#### Example dependency injection
3737
```csharp
3838
public class TestController : Controller
3939
{
@@ -52,8 +52,8 @@ public class TestController : Controller
5252
}
5353
```
5454

55-
#### Add Configuration Section to the appsettings.json file (or your configuration file)
56-
##### Example for development environment:
55+
#### Add configuration section to the appsettings.json file (or your configuration file)
56+
##### Example:
5757
```json
5858
"ProxySettings": {
5959
"RegionKeys": {
@@ -68,6 +68,7 @@ public class TestController : Controller
6868

6969
#### API Contract Definition (Default HttpMethod is HttpGet)
7070
```csharp
71+
// This APIs expose methods from localhost:5000 and localhost:5001 as configured on ProxySettings
7172
[ApiRoute("api/[controller]", regionKey: "Main")]
7273
public interface IGuidelineApi : IApiContract
7374
{
@@ -86,7 +87,7 @@ public interface IGuidelineApi : IApiContract
8687
}
8788
```
8889

89-
### Usage for Backend - Server Side
90+
### Backend - Server Side
9091
#### API Contract Implementation
9192
```csharp
9293
[Route("api/[controller]")]
@@ -152,6 +153,24 @@ public class GuidelineController : Controller, IGuidelineApi
152153
}
153154
```
154155

156+
#### Multipart form data:
157+
Proxy sends all POST methods as JSON but if the method parameter model contains IFormFile type property it converts the content-type to multipart/form-data. In this case, use any model to POST multipart/form-data to API without [FromBody] attribute on action parameter. For example:
158+
159+
```csharp
160+
// Interface
161+
[HttpPostMarker]
162+
Task<AlbumViewModel> SaveAlbumSubmit(AlbumViewModelSubmit model)
163+
```
164+
165+
```csharp
166+
// API Controller
167+
[HttpPost(nameof(SaveAlbumSubmit))]
168+
public async Task<AlbumViewModel> SaveAlbumSubmit(AlbumViewModelSubmit model)
169+
```
170+
171+
172+
173+
155174

156175
### Prerequisites
157176
> [ASP.NET Core](https://github.com/aspnet/Home)

src/NetCoreStack.Proxy/DefaultProxyContentStreamProvider.cs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using NetCoreStack.Contracts;
1+
using Microsoft.AspNetCore.Http;
2+
using NetCoreStack.Contracts;
23
using NetCoreStack.Proxy.Extensions;
34
using NetCoreStack.Proxy.Internal;
45
using Newtonsoft.Json;
@@ -7,6 +8,7 @@
78
using System.Linq;
89
using System.Net;
910
using System.Net.Http;
11+
using System.Net.Http.Headers;
1012
using System.Text;
1113
using System.Threading.Tasks;
1214

@@ -24,9 +26,39 @@ protected virtual StringContent SerializeToString(object value)
2426
return new StringContent(JsonConvert.SerializeObject(value), Encoding.UTF8, "application/json");
2527
}
2628

27-
protected virtual MultipartFormDataContent GetMultipartFormDataContent()
29+
protected virtual MultipartFormDataContent GetMultipartFormDataContent(IDictionary<string,object> values)
2830
{
2931
MultipartFormDataContent multipartFormDataContent = new MultipartFormDataContent();
32+
foreach (KeyValuePair<string, object> entry in values)
33+
{
34+
var parameterContext = entry.Value as PropertyContext;
35+
if (parameterContext != null)
36+
{
37+
if (parameterContext.PropertyContentType == PropertyContentType.Multipart)
38+
{
39+
IFormFile formFile = parameterContext.Value as IFormFile;
40+
if (formFile != null)
41+
{
42+
using (var ms = new MemoryStream())
43+
{
44+
formFile.CopyTo(ms);
45+
var fileContent = new ByteArrayContent(ms.ToArray());
46+
fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(formFile.ContentType);
47+
fileContent.Headers.ContentDisposition = ContentDispositionHeaderValue.Parse(formFile.ContentDisposition);
48+
multipartFormDataContent.Add(fileContent, entry.Key, formFile.FileName);
49+
}
50+
}
51+
}
52+
else
53+
{
54+
var stringContent = parameterContext.Value?.ToString();
55+
if (!string.IsNullOrEmpty(stringContent))
56+
{
57+
multipartFormDataContent.Add(new StringContent(stringContent), entry.Key);
58+
}
59+
}
60+
}
61+
}
3062

3163
return multipartFormDataContent;
3264
}
@@ -84,6 +116,12 @@ public async Task CreateRequestContentAsync(RequestContext requestContext,
84116
var keys = new List<string>(argsDic.Keys);
85117
if (descriptor.HttpMethod == HttpMethod.Post)
86118
{
119+
if (descriptor.IsMultiPartFormData)
120+
{
121+
request.Content = GetMultipartFormDataContent(argsDic);
122+
return;
123+
}
124+
87125
if (argsCount == 1)
88126
request.Content = SerializeToString(argsDic.First().Value);
89127
else

src/NetCoreStack.Proxy/Extensions/DictionaryExtensions.cs

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
using Microsoft.AspNetCore.Mvc.Abstractions;
2-
using Microsoft.AspNetCore.WebUtilities;
1+
using Microsoft.AspNetCore.WebUtilities;
32
using System;
43
using System.Collections.Generic;
54
using System.Globalization;
5+
using System.Reflection;
66

77
namespace NetCoreStack.Proxy.Extensions
88
{
@@ -24,7 +24,7 @@ private static string TypeParser(KeyValuePair<string, object> selector)
2424
return selector.Value.ToString();
2525
}
2626

27-
public static void Merge(this IDictionary<string, object> instance, IDictionary<string, object> from, bool replaceExisting)
27+
internal static void Merge(this IDictionary<string, object> instance, IDictionary<string, object> from, bool replaceExisting)
2828
{
2929
foreach (KeyValuePair<string, object> entry in from)
3030
{
@@ -35,18 +35,45 @@ public static void Merge(this IDictionary<string, object> instance, IDictionary<
3535
}
3636
}
3737

38-
public static void MergeArgs(this IDictionary<string, object> dictionary, object[] args, ProxyParameterDescriptor[] parameters)
38+
internal static void MergeArgs(this IDictionary<string, object> dictionary,
39+
object[] args,
40+
ProxyParameterDescriptor[] parameters,
41+
bool isMultiPartFormData)
3942
{
4043
if (args.Length == 0)
4144
return;
4245

46+
if (!isMultiPartFormData)
47+
{
48+
for (int i = 0; i < parameters.Length; i++)
49+
{
50+
dictionary.Add(parameters[i].Name, args[i]);
51+
}
52+
53+
return;
54+
}
55+
56+
// Multipart form data context preparing
4357
for (int i = 0; i < parameters.Length; i++)
4458
{
45-
dictionary.Add(parameters[i].Name, args[i]);
59+
foreach (var prop in parameters[i].Properties)
60+
{
61+
string name = prop.Key;
62+
PropertyContentTypeInfo contentTypeInfo = prop.Value;
63+
64+
var parameterContext = new PropertyContext
65+
{
66+
Name = name,
67+
Value = contentTypeInfo.PropertyInfo.GetValue(args[i]),
68+
PropertyContentType = contentTypeInfo.PropertyContentType
69+
};
70+
71+
dictionary.Add(name, parameterContext);
72+
}
4673
}
4774
}
4875

49-
public static string ToQueryString(this Uri baseUrl, IDictionary<string, object> objectDics)
76+
internal static string ToQueryString(this Uri baseUrl, IDictionary<string, object> objectDics)
5077
{
5178
if (objectDics == null)
5279
return baseUrl.AbsoluteUri;

src/NetCoreStack.Proxy/Internal/Constants.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ public class Constants
55
public static string Prefix = "NetCoreStack";
66
public const string ProxySettings = "ProxySettings";
77
public const string ContentTypeJsonWithEncoding = "application/json; charset=utf-8";
8-
public readonly static string ClientUserAgentHeader = "User-Agent";
8+
public const string ContentTypeMultipartFormData = "multipart/form-data";
9+
public readonly static string ClientUserAgentHeader = "User-Agent";
910
}
1011
}

src/NetCoreStack.Proxy/Internal/DefaultProxyTypeManager.cs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
using Microsoft.AspNetCore.Http;
2-
using Microsoft.AspNetCore.Mvc.Abstractions;
3-
using Microsoft.AspNetCore.Mvc.ModelBinding;
1+
using Microsoft.AspNetCore.Mvc.ModelBinding;
42
using NetCoreStack.Contracts;
53
using NetCoreStack.Proxy.Extensions;
64
using System;
@@ -32,10 +30,10 @@ private IList<ProxyDescriptor> GetProxyDescriptors()
3230
{
3331
var pathAttr = proxyType.GetTypeInfo().GetCustomAttribute<ApiRouteAttribute>();
3432
if (pathAttr == null)
35-
throw new ArgumentNullException($"{nameof(ApiRouteAttribute)} required for Proxy - Api interface.");
33+
throw new ProxyException($"{nameof(ApiRouteAttribute)} required for Proxy - Api interface.");
3634

3735
if (!pathAttr.RegionKey.HasValue())
38-
throw new ArgumentOutOfRangeException($"Specify the \"{nameof(pathAttr.RegionKey)}\"!");
36+
throw new ProxyException($"Specify the \"{nameof(pathAttr.RegionKey)}\"!");
3937

4038
var route = proxyType.Name.GetApiRootPath(pathAttr.RouteTemplate);
4139

@@ -78,7 +76,7 @@ private IList<ProxyDescriptor> GetProxyDescriptors()
7876
proxyMethodDescriptor.HttpMethod = HttpMethod.Get;
7977
else if (httpMethodAttribute is HttpPostMarkerAttribute)
8078
proxyMethodDescriptor.HttpMethod = HttpMethod.Post;
81-
else if(httpMethodAttribute is HttpPutMarkerAttribute)
79+
else if (httpMethodAttribute is HttpPutMarkerAttribute)
8280
proxyMethodDescriptor.HttpMethod = HttpMethod.Put;
8381
else if (httpMethodAttribute is HttpDeleteMarkerAttribute)
8482
proxyMethodDescriptor.HttpMethod = HttpMethod.Delete;
@@ -95,14 +93,30 @@ private IList<ProxyDescriptor> GetProxyDescriptors()
9593
var parameterType = parameter.ParameterType;
9694
var properties = parameterType.GetProperties().ToList();
9795

96+
var proxyParameterDescriptor = new ProxyParameterDescriptor(properties);
97+
if (proxyParameterDescriptor.HasFormFile)
98+
{
99+
if (parameter.CustomAttributes
100+
.Any(a => a.AttributeType.Name == "FromBodyAttribute"))
101+
{
102+
throw new ProxyException($"Parameter: \"{parameter.ParameterType.Name} as {parameter.Name}\" " +
103+
"contains IFormFile type property. " +
104+
"Remove FromBody attribute to proper model binding.");
105+
}
106+
}
107+
98108
proxyMethodDescriptor.Parameters.Add(new ProxyParameterDescriptor(properties)
99109
{
100110
Name = parameter.Name,
101111
ParameterType = parameterType,
102112
BindingInfo = BindingInfo.GetBindingInfo(parameter.GetCustomAttributes().OfType<object>())
103113
});
104114
}
115+
116+
var isMultipartFormData = proxyMethodDescriptor.Parameters.SelectMany(p => p.Properties)
117+
.Any(c => c.Value.PropertyContentType == PropertyContentType.Multipart);
105118

119+
proxyMethodDescriptor.IsMultiPartFormData = isMultipartFormData;
106120
descriptor.Methods.Add(method, proxyMethodDescriptor);
107121
}
108122
#endregion // method resolver

src/NetCoreStack.Proxy/Internal/ProxyException.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ namespace NetCoreStack.Proxy
44
{
55
public class ProxyException : Exception
66
{
7+
public ProxyException(string message)
8+
:base(message)
9+
{
10+
11+
}
12+
713
public ProxyException(string message, Exception innerException)
814
: base(message, innerException)
915
{
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System.Reflection;
2+
3+
namespace NetCoreStack.Proxy
4+
{
5+
public enum PropertyContentType
6+
{
7+
String = 0,
8+
Multipart = 1
9+
}
10+
11+
public class PropertyContentTypeInfo
12+
{
13+
public PropertyInfo PropertyInfo { get; }
14+
public PropertyContentType PropertyContentType { get; }
15+
16+
public PropertyContentTypeInfo(PropertyInfo propertyInfo, PropertyContentType contentType)
17+
{
18+
PropertyInfo = propertyInfo;
19+
PropertyContentType = contentType;
20+
}
21+
}
22+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace NetCoreStack.Proxy
2+
{
3+
public class PropertyContext
4+
{
5+
public string Name { get; set; }
6+
public object Value { get; set; }
7+
public PropertyContentType PropertyContentType { get; set; }
8+
}
9+
}

src/NetCoreStack.Proxy/ProxyMethodDescriptor.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public class ProxyMethodDescriptor
2222

2323
public TimeSpan? Timeout { get; set; }
2424

25+
public bool IsMultiPartFormData { get; set; }
26+
2527
public List<ProxyParameterDescriptor> Parameters { get; set; }
2628

2729
public bool IsVoidReturn { get; }
@@ -44,9 +46,10 @@ public ProxyMethodDescriptor(MethodInfo methodInfo)
4446
}
4547
}
4648

49+
// per request resolver
4750
public IDictionary<string, object> Resolve(object[] args)
4851
{
49-
var values = new Dictionary<string, object>();
52+
var values = new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
5053
if (HttpMethod == HttpMethod.Get)
5154
{
5255
// Ref type parameter resolver
@@ -63,7 +66,7 @@ public IDictionary<string, object> Resolve(object[] args)
6366
}
6467
}
6568

66-
values.MergeArgs(args, Parameters.ToArray());
69+
values.MergeArgs(args, Parameters.ToArray(), IsMultiPartFormData);
6770
return values;
6871
}
6972
}
Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,32 @@
11
using Microsoft.AspNetCore.Http;
22
using Microsoft.AspNetCore.Mvc.Abstractions;
3+
using System;
34
using System.Collections.Generic;
4-
using System.Linq;
55
using System.Reflection;
66

77
namespace NetCoreStack.Proxy
88
{
99
public class ProxyParameterDescriptor : ParameterDescriptor
1010
{
11-
public List<PropertyInfo> Properties { get; }
12-
13-
public bool EnsureMultipartFormData { get; }
14-
15-
11+
public IDictionary<string, PropertyContentTypeInfo> Properties { get; }
12+
13+
public bool HasFormFile { get; }
1614

1715
public ProxyParameterDescriptor(List<PropertyInfo> properties)
1816
{
19-
Properties = properties;
20-
EnsureMultipartFormData = Properties
21-
.Where(p => typeof(IFormFile).IsAssignableFrom(p.PropertyType) ||
22-
typeof(byte[]).IsAssignableFrom(p.PropertyType)).Any();
17+
Properties = new Dictionary<string, PropertyContentTypeInfo>(StringComparer.OrdinalIgnoreCase);
18+
19+
foreach (var prop in properties)
20+
{
21+
PropertyContentType contentType = PropertyContentType.String;
22+
if (typeof(IFormFile).IsAssignableFrom(prop.PropertyType))
23+
{
24+
HasFormFile = true;
25+
contentType = PropertyContentType.Multipart;
26+
}
27+
28+
Properties.Add(prop.Name, new PropertyContentTypeInfo(prop, contentType));
29+
}
2330
}
2431
}
2532
}

0 commit comments

Comments
 (0)