Skip to content

Commit 965db4d

Browse files
authored
Merge pull request #8302 from abpframework/IRemoteStreamContent-patch
Support using IRemoteStreamContent in Dto
2 parents 0f206fc + b4619a8 commit 965db4d

File tree

13 files changed

+402
-51
lines changed

13 files changed

+402
-51
lines changed

framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
using Microsoft.AspNetCore.Mvc;
1+
using System.Collections.Generic;
2+
using Microsoft.AspNetCore.Mvc;
3+
using Microsoft.AspNetCore.Mvc.ModelBinding;
4+
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
25
using Microsoft.Extensions.DependencyInjection;
36
using Volo.Abp.AspNetCore.Mvc.Auditing;
47
using Volo.Abp.AspNetCore.Mvc.ContentFormatters;
@@ -10,6 +13,7 @@
1013
using Volo.Abp.AspNetCore.Mvc.Response;
1114
using Volo.Abp.AspNetCore.Mvc.Uow;
1215
using Volo.Abp.AspNetCore.Mvc.Validation;
16+
using Volo.Abp.Content;
1317

1418
namespace Volo.Abp.AspNetCore.Mvc
1519
{
@@ -27,7 +31,6 @@ public static void AddAbp(this MvcOptions options, IServiceCollection services)
2731

2832
private static void AddFormatters(MvcOptions options)
2933
{
30-
options.InputFormatters.Insert(0, new RemoteStreamContentInputFormatter());
3134
options.OutputFormatters.Insert(0, new RemoteStreamContentOutputFormatter());
3235
}
3336

@@ -60,13 +63,19 @@ private static void AddModelBinders(MvcOptions options)
6063
{
6164
options.ModelBinderProviders.Insert(0, new AbpDateTimeModelBinderProvider());
6265
options.ModelBinderProviders.Insert(1, new AbpExtraPropertiesDictionaryModelBinderProvider());
66+
options.ModelBinderProviders.Insert(2, new AbpRemoteStreamContentModelBinderProvider());
6367
}
6468

6569
private static void AddMetadataProviders(MvcOptions options, IServiceCollection services)
6670
{
67-
options.ModelMetadataDetailsProviders.Add(
68-
new AbpDataAnnotationAutoLocalizationMetadataDetailsProvider(services)
69-
);
71+
options.ModelMetadataDetailsProviders.Add(new AbpDataAnnotationAutoLocalizationMetadataDetailsProvider(services));
72+
73+
options.ModelMetadataDetailsProviders.Add(new BindingSourceMetadataProvider(typeof(IRemoteStreamContent), BindingSource.FormFile));
74+
options.ModelMetadataDetailsProviders.Add(new BindingSourceMetadataProvider(typeof(IEnumerable<IRemoteStreamContent>), BindingSource.FormFile));
75+
options.ModelMetadataDetailsProviders.Add(new BindingSourceMetadataProvider(typeof(RemoteStreamContent), BindingSource.FormFile));
76+
options.ModelMetadataDetailsProviders.Add(new BindingSourceMetadataProvider(typeof(IEnumerable<RemoteStreamContent>), BindingSource.FormFile));
77+
options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(IRemoteStreamContent)));
78+
options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(RemoteStreamContent)));
7079
}
7180
}
7281
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading.Tasks;
5+
using Microsoft.AspNetCore.Mvc.ModelBinding;
6+
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
7+
using Volo.Abp.Content;
8+
9+
namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters
10+
{
11+
public class AbpRemoteStreamContentModelBinder : IModelBinder
12+
{
13+
public async Task BindModelAsync(ModelBindingContext bindingContext)
14+
{
15+
if (bindingContext == null)
16+
{
17+
throw new ArgumentNullException(nameof(bindingContext));
18+
}
19+
20+
var postedFiles = new List<IRemoteStreamContent>();
21+
22+
// If we're at the top level, then use the FieldName (parameter or property name).
23+
// This handles the fact that there will be nothing in the ValueProviders for this parameter
24+
// and so we'll do the right thing even though we 'fell-back' to the empty prefix.
25+
var modelName = bindingContext.IsTopLevelObject
26+
? bindingContext.BinderModelName ?? bindingContext.FieldName
27+
: bindingContext.ModelName;
28+
29+
await GetFormFilesAsync(modelName, bindingContext, postedFiles);
30+
31+
// If ParameterBinder incorrectly overrode ModelName, fall back to OriginalModelName prefix. Comparisons
32+
// are tedious because e.g. top-level parameter or property is named Blah and it contains a BlahBlah
33+
// property. OriginalModelName may be null in tests.
34+
if (postedFiles.Count == 0 &&
35+
bindingContext.OriginalModelName != null &&
36+
!string.Equals(modelName, bindingContext.OriginalModelName, StringComparison.Ordinal) &&
37+
!modelName.StartsWith(bindingContext.OriginalModelName + "[", StringComparison.Ordinal) &&
38+
!modelName.StartsWith(bindingContext.OriginalModelName + ".", StringComparison.Ordinal))
39+
{
40+
modelName = ModelNames.CreatePropertyModelName(bindingContext.OriginalModelName, modelName);
41+
await GetFormFilesAsync(modelName, bindingContext, postedFiles);
42+
}
43+
44+
object value;
45+
if (bindingContext.ModelType == typeof(IRemoteStreamContent) || bindingContext.ModelType == typeof(RemoteStreamContent))
46+
{
47+
if (postedFiles.Count == 0)
48+
{
49+
// Silently fail if the named file does not exist in the request.
50+
return;
51+
}
52+
53+
value = postedFiles.First();
54+
}
55+
else
56+
{
57+
if (postedFiles.Count == 0 && !bindingContext.IsTopLevelObject)
58+
{
59+
// Silently fail if no files match. Will bind to an empty collection (treat empty as a success
60+
// case and not reach here) if binding to a top-level object.
61+
return;
62+
}
63+
64+
// Perform any final type mangling needed.
65+
var modelType = bindingContext.ModelType;
66+
if (modelType == typeof(IRemoteStreamContent[]) || modelType == typeof(RemoteStreamContent[]))
67+
{
68+
value = postedFiles.ToArray();
69+
}
70+
else
71+
{
72+
value = postedFiles;
73+
}
74+
}
75+
76+
// We need to add a ValidationState entry because the modelName might be non-standard. Otherwise
77+
// the entry we create in model state might not be marked as valid.
78+
bindingContext.ValidationState.Add(value, new ValidationStateEntry()
79+
{
80+
Key = modelName,
81+
});
82+
83+
bindingContext.ModelState.SetModelValue(
84+
modelName,
85+
rawValue: null,
86+
attemptedValue: null);
87+
88+
bindingContext.Result = ModelBindingResult.Success(value);
89+
}
90+
91+
private async Task GetFormFilesAsync(
92+
string modelName,
93+
ModelBindingContext bindingContext,
94+
ICollection<IRemoteStreamContent> postedFiles)
95+
{
96+
var request = bindingContext.HttpContext.Request;
97+
if (request.HasFormContentType)
98+
{
99+
var form = await request.ReadFormAsync();
100+
101+
foreach (var file in form.Files)
102+
{
103+
// If there is an <input type="file" ... /> in the form and is left blank.
104+
if (file.Length == 0 && string.IsNullOrEmpty(file.FileName))
105+
{
106+
continue;
107+
}
108+
109+
if (file.Name.Equals(modelName, StringComparison.OrdinalIgnoreCase))
110+
{
111+
postedFiles.Add(new RemoteStreamContent(file.OpenReadStream())
112+
{
113+
ContentType = file.ContentType
114+
});
115+
}
116+
}
117+
}
118+
else
119+
{
120+
postedFiles.Add(new RemoteStreamContent(request.Body)
121+
{
122+
ContentType = request.ContentType
123+
});
124+
}
125+
}
126+
}
127+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Microsoft.AspNetCore.Mvc.ModelBinding;
4+
using Volo.Abp.Content;
5+
6+
namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters
7+
{
8+
public class AbpRemoteStreamContentModelBinderProvider : IModelBinderProvider
9+
{
10+
public IModelBinder GetBinder(ModelBinderProviderContext context)
11+
{
12+
if (context == null)
13+
{
14+
throw new ArgumentNullException(nameof(context));
15+
}
16+
17+
if (context.Metadata.ModelType == typeof(IRemoteStreamContent) ||
18+
context.Metadata.ModelType == typeof(RemoteStreamContent) ||
19+
typeof(IEnumerable<IRemoteStreamContent>).IsAssignableFrom(context.Metadata.ModelType) ||
20+
typeof(IEnumerable<RemoteStreamContent>).IsAssignableFrom(context.Metadata.ModelType))
21+
{
22+
return new AbpRemoteStreamContentModelBinder();
23+
}
24+
25+
return null;
26+
}
27+
}
28+
}

framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentInputFormatter.cs

Lines changed: 0 additions & 30 deletions
This file was deleted.

framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpConventionalControllerOptions.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Reflection;
44
using JetBrains.Annotations;
55
using Microsoft.AspNetCore.Http;
6+
using Volo.Abp.Content;
67
using Volo.Abp.Http.Modeling;
78

89
namespace Volo.Abp.AspNetCore.Mvc.Conventions
@@ -25,7 +26,8 @@ public AbpConventionalControllerOptions()
2526

2627
FormBodyBindingIgnoredTypes = new List<Type>
2728
{
28-
typeof(IFormFile)
29+
typeof(IFormFile),
30+
typeof(IRemoteStreamContent)
2931
};
3032
}
3133

framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/RequestPayloadBuilder.cs

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using System.Net.Http;
5+
using System.Net.Http.Headers;
46
using System.Text;
57
using JetBrains.Annotations;
68
using Volo.Abp.Content;
@@ -68,32 +70,75 @@ private static HttpContent GenerateFormPostData(ActionApiDescriptionModel action
6870
{
6971
var parameters = action
7072
.Parameters
71-
.Where(p => p.BindingSourceId == ParameterBindingSources.Form)
73+
.Where(p => p.BindingSourceId == ParameterBindingSources.Form || p.BindingSourceId == ParameterBindingSources.FormFile)
7274
.ToArray();
7375

7476
if (!parameters.Any())
7577
{
7678
return null;
7779
}
7880

79-
var postDataBuilder = new StringBuilder();
80-
81-
var isFirstParam = true;
82-
foreach (var queryStringParameter in parameters)
81+
if (parameters.Any(x => x.BindingSourceId == ParameterBindingSources.FormFile))
8382
{
84-
var value = HttpActionParameterHelper.FindParameterValue(methodArguments, queryStringParameter);
85-
if (value == null)
83+
var postDataBuilder = new MultipartFormDataContent();
84+
foreach (var parameter in parameters)
8685
{
87-
continue;
88-
}
86+
var value = HttpActionParameterHelper.FindParameterValue(methodArguments, parameter);
87+
if (value == null)
88+
{
89+
continue;
90+
}
8991

90-
postDataBuilder.Append(isFirstParam ? "?" : "&");
91-
postDataBuilder.Append(queryStringParameter.Name + "=" + System.Net.WebUtility.UrlEncode(value.ToString()));
92+
if (value is IRemoteStreamContent remoteStreamContent)
93+
{
94+
var streamContent = new StreamContent(remoteStreamContent.GetStream());
95+
if (!remoteStreamContent.ContentType.IsNullOrWhiteSpace())
96+
{
97+
streamContent.Headers.ContentType = new MediaTypeHeaderValue(remoteStreamContent.ContentType);
98+
}
99+
postDataBuilder.Add(streamContent, parameter.Name, parameter.Name);
100+
}
101+
else if (value is IEnumerable<IRemoteStreamContent> remoteStreamContents)
102+
{
103+
foreach (var content in remoteStreamContents)
104+
{
105+
var streamContent = new StreamContent(content.GetStream());
106+
if (!content.ContentType.IsNullOrWhiteSpace())
107+
{
108+
streamContent.Headers.ContentType = new MediaTypeHeaderValue(content.ContentType);
109+
}
110+
postDataBuilder.Add(streamContent, parameter.Name, parameter.Name);
111+
}
112+
}
113+
else
114+
{
115+
postDataBuilder.Add(new StringContent(value.ToString(), Encoding.UTF8), parameter.Name);
116+
}
117+
}
92118

93-
isFirstParam = false;
119+
return postDataBuilder;
94120
}
121+
else
122+
{
123+
var postDataBuilder = new StringBuilder();
95124

96-
return new StringContent(postDataBuilder.ToString(), Encoding.UTF8, MimeTypes.Application.XWwwFormUrlencoded);
125+
var isFirstParam = true;
126+
foreach (var parameter in parameters.Where(p => p.BindingSourceId == ParameterBindingSources.Form))
127+
{
128+
var value = HttpActionParameterHelper.FindParameterValue(methodArguments, parameter);
129+
if (value == null)
130+
{
131+
continue;
132+
}
133+
134+
postDataBuilder.Append(isFirstParam ? "?" : "&");
135+
postDataBuilder.Append(parameter.Name + "=" + System.Net.WebUtility.UrlEncode(value.ToString()));
136+
137+
isFirstParam = false;
138+
}
139+
140+
return new StringContent(postDataBuilder.ToString(), Encoding.UTF8, MimeTypes.Application.XWwwFormUrlencoded);
141+
}
97142
}
98143
}
99144
}

framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/ParameterBindingSources.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ public static class ParameterBindingSources
77
public const string Body = "Body";
88
public const string Path = "Path";
99
public const string Form = "Form";
10+
public const string FormFile = "FormFile";
1011
public const string Header = "Header";
1112
public const string Custom = "Custom";
1213
public const string Services = "Services";
1314
}
14-
}
15+
}

framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpClientTestModule.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
using Microsoft.Extensions.DependencyInjection;
22
using Volo.Abp.AspNetCore.Mvc;
3+
using Volo.Abp.AspNetCore.Mvc.Conventions;
34
using Volo.Abp.Http.Client;
45
using Volo.Abp.Http.DynamicProxying;
56
using Volo.Abp.Http.Localization;
67
using Volo.Abp.Localization;
78
using Volo.Abp.Localization.ExceptionHandling;
89
using Volo.Abp.Modularity;
910
using Volo.Abp.TestApp;
11+
using Volo.Abp.TestApp.Application.Dto;
1012
using Volo.Abp.VirtualFileSystem;
1113

1214
namespace Volo.Abp.Http
@@ -44,6 +46,12 @@ public override void ConfigureServices(ServiceConfigurationContext context)
4446
{
4547
options.MapCodeNamespace("Volo.Abp.Http.DynamicProxying", typeof(HttpClientTestResource));
4648
});
49+
50+
Configure<AbpAspNetCoreMvcOptions>(options =>
51+
{
52+
options.ConventionalControllers.FormBodyBindingIgnoredTypes.Add(typeof(CreateFileInput));
53+
options.ConventionalControllers.FormBodyBindingIgnoredTypes.Add(typeof(CreateMultipleFileInput));
54+
});
4755
}
4856
}
4957
}

0 commit comments

Comments
 (0)