Browse Source

Support using IRemoteStreamContent in Dto

Resolve #8298
pull/8302/head
maliming 5 years ago
parent
commit
31b1608e9a
  1. 16
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs
  2. 127
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/AbpRemoteStreamContentModelBinder.cs
  3. 28
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/AbpRemoteStreamContentModelBinderProvider.cs
  4. 30
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/RemoteStreamContentInputFormatter.cs
  5. 4
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpConventionalControllerOptions.cs
  6. 64
      framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/DynamicProxying/RequestPayloadBuilder.cs
  7. 3
      framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/ParameterBindingSources.cs
  8. 8
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/AbpHttpClientTestModule.cs
  9. 88
      framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs
  10. 11
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/CreateFileInput.cs
  11. 14
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/CreateMultipleFileInput.cs
  12. 7
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IPeopleAppService.cs
  13. 41
      framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs

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

@ -1,4 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.Mvc.Auditing;
using Volo.Abp.AspNetCore.Mvc.ContentFormatters;
@ -10,6 +13,7 @@ using Volo.Abp.AspNetCore.Mvc.ModelBinding;
using Volo.Abp.AspNetCore.Mvc.Response;
using Volo.Abp.AspNetCore.Mvc.Uow;
using Volo.Abp.AspNetCore.Mvc.Validation;
using Volo.Abp.Content;
namespace Volo.Abp.AspNetCore.Mvc
{
@ -27,7 +31,6 @@ namespace Volo.Abp.AspNetCore.Mvc
private static void AddFormatters(MvcOptions options)
{
options.InputFormatters.Insert(0, new RemoteStreamContentInputFormatter());
options.OutputFormatters.Insert(0, new RemoteStreamContentOutputFormatter());
}
@ -60,13 +63,16 @@ namespace Volo.Abp.AspNetCore.Mvc
{
options.ModelBinderProviders.Insert(0, new AbpDateTimeModelBinderProvider());
options.ModelBinderProviders.Insert(1, new AbpExtraPropertiesDictionaryModelBinderProvider());
options.ModelBinderProviders.Insert(2, new AbpRemoteStreamContentModelBinderProvider());
}
private static void AddMetadataProviders(MvcOptions options, IServiceCollection services)
{
options.ModelMetadataDetailsProviders.Add(
new AbpDataAnnotationAutoLocalizationMetadataDetailsProvider(services)
);
options.ModelMetadataDetailsProviders.Add(new AbpDataAnnotationAutoLocalizationMetadataDetailsProvider(services));
options.ModelMetadataDetailsProviders.Add(new BindingSourceMetadataProvider(typeof(IRemoteStreamContent), BindingSource.FormFile));
options.ModelMetadataDetailsProviders.Add(new BindingSourceMetadataProvider(typeof(IEnumerable<IRemoteStreamContent>), BindingSource.FormFile));
options.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(IRemoteStreamContent)));
}
}
}

127
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/AbpRemoteStreamContentModelBinder.cs

@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Volo.Abp.Content;
namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters
{
public class AbpRemoteStreamContentModelBinder : IModelBinder
{
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
{
throw new ArgumentNullException(nameof(bindingContext));
}
var postedFiles = new List<IRemoteStreamContent>();
// If we're at the top level, then use the FieldName (parameter or property name).
// This handles the fact that there will be nothing in the ValueProviders for this parameter
// and so we'll do the right thing even though we 'fell-back' to the empty prefix.
var modelName = bindingContext.IsTopLevelObject
? bindingContext.BinderModelName ?? bindingContext.FieldName
: bindingContext.ModelName;
await GetFormFilesAsync(modelName, bindingContext, postedFiles);
// If ParameterBinder incorrectly overrode ModelName, fall back to OriginalModelName prefix. Comparisons
// are tedious because e.g. top-level parameter or property is named Blah and it contains a BlahBlah
// property. OriginalModelName may be null in tests.
if (postedFiles.Count == 0 &&
bindingContext.OriginalModelName != null &&
!string.Equals(modelName, bindingContext.OriginalModelName, StringComparison.Ordinal) &&
!modelName.StartsWith(bindingContext.OriginalModelName + "[", StringComparison.Ordinal) &&
!modelName.StartsWith(bindingContext.OriginalModelName + ".", StringComparison.Ordinal))
{
modelName = ModelNames.CreatePropertyModelName(bindingContext.OriginalModelName, modelName);
await GetFormFilesAsync(modelName, bindingContext, postedFiles);
}
object value;
if (bindingContext.ModelType == typeof(IRemoteStreamContent) || bindingContext.ModelType == typeof(RemoteStreamContent))
{
if (postedFiles.Count == 0)
{
// Silently fail if the named file does not exist in the request.
return;
}
value = postedFiles.First();
}
else
{
if (postedFiles.Count == 0 && !bindingContext.IsTopLevelObject)
{
// Silently fail if no files match. Will bind to an empty collection (treat empty as a success
// case and not reach here) if binding to a top-level object.
return;
}
// Perform any final type mangling needed.
var modelType = bindingContext.ModelType;
if (modelType == typeof(IRemoteStreamContent[]) || modelType == typeof(RemoteStreamContent[]))
{
value = postedFiles.ToArray();
}
else
{
value = postedFiles;
}
}
// We need to add a ValidationState entry because the modelName might be non-standard. Otherwise
// the entry we create in model state might not be marked as valid.
bindingContext.ValidationState.Add(value, new ValidationStateEntry()
{
Key = modelName,
});
bindingContext.ModelState.SetModelValue(
modelName,
rawValue: null,
attemptedValue: null);
bindingContext.Result = ModelBindingResult.Success(value);
}
private async Task GetFormFilesAsync(
string modelName,
ModelBindingContext bindingContext,
ICollection<IRemoteStreamContent> postedFiles)
{
var request = bindingContext.HttpContext.Request;
if (request.HasFormContentType)
{
var form = await request.ReadFormAsync();
foreach (var file in form.Files)
{
// If there is an <input type="file" ... /> in the form and is left blank.
if (file.Length == 0 && string.IsNullOrEmpty(file.FileName))
{
continue;
}
if (file.Name.Equals(modelName, StringComparison.OrdinalIgnoreCase))
{
postedFiles.Add(new RemoteStreamContent(file.OpenReadStream())
{
ContentType = file.ContentType
});
}
}
}
else
{
postedFiles.Add(new RemoteStreamContent(request.Body)
{
ContentType = request.ContentType
});
}
}
}
}

28
framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ContentFormatters/AbpRemoteStreamContentModelBinderProvider.cs

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Volo.Abp.Content;
namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters
{
public class AbpRemoteStreamContentModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (context.Metadata.ModelType == typeof(IRemoteStreamContent) ||
context.Metadata.ModelType == typeof(RemoteStreamContent) ||
typeof(IEnumerable<IRemoteStreamContent>).IsAssignableFrom(context.Metadata.ModelType) ||
typeof(IEnumerable<RemoteStreamContent>).IsAssignableFrom(context.Metadata.ModelType))
{
return new AbpRemoteStreamContentModelBinder();
}
return null;
}
}
}

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

@ -1,30 +0,0 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;
using Volo.Abp.Content;
namespace Volo.Abp.AspNetCore.Mvc.ContentFormatters
{
public class RemoteStreamContentInputFormatter : InputFormatter
{
public RemoteStreamContentInputFormatter()
{
SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("*/*"));
}
protected override bool CanReadType(Type type)
{
return type == typeof(IRemoteStreamContent) ||
type == typeof(RemoteStreamContent);
}
public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
{
return InputFormatterResult.SuccessAsync(new RemoteStreamContent(context.HttpContext.Request.Body)
{
ContentType = context.HttpContext.Request.ContentType
});
}
}
}

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

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.AspNetCore.Http;
using Volo.Abp.Content;
using Volo.Abp.Http.Modeling;
namespace Volo.Abp.AspNetCore.Mvc.Conventions
@ -25,7 +26,8 @@ namespace Volo.Abp.AspNetCore.Mvc.Conventions
FormBodyBindingIgnoredTypes = new List<Type>
{
typeof(IFormFile)
typeof(IFormFile),
typeof(IRemoteStreamContent)
};
}

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

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using JetBrains.Annotations;
using Volo.Abp.Content;
@ -68,7 +69,7 @@ namespace Volo.Abp.Http.Client.DynamicProxying
{
var parameters = action
.Parameters
.Where(p => p.BindingSourceId == ParameterBindingSources.Form)
.Where(p => p.BindingSourceId == ParameterBindingSources.Form || p.BindingSourceId == ParameterBindingSources.FormFile)
.ToArray();
if (!parameters.Any())
@ -76,24 +77,61 @@ namespace Volo.Abp.Http.Client.DynamicProxying
return null;
}
var postDataBuilder = new StringBuilder();
var isFirstParam = true;
foreach (var queryStringParameter in parameters)
if (parameters.Any(x => x.BindingSourceId == ParameterBindingSources.FormFile))
{
var value = HttpActionParameterHelper.FindParameterValue(methodArguments, queryStringParameter);
if (value == null)
var postDataBuilder = new MultipartFormDataContent();
foreach (var parameter in parameters)
{
continue;
}
var value = HttpActionParameterHelper.FindParameterValue(methodArguments, parameter);
if (value == null)
{
continue;
}
postDataBuilder.Append(isFirstParam ? "?" : "&");
postDataBuilder.Append(queryStringParameter.Name + "=" + System.Net.WebUtility.UrlEncode(value.ToString()));
if (value is IRemoteStreamContent remoteStreamContent)
{
var streamContent = new StreamContent(remoteStreamContent.GetStream());
streamContent.Headers.ContentType = new MediaTypeHeaderValue(remoteStreamContent.ContentType);
postDataBuilder.Add(streamContent, parameter.Name, parameter.Name);
}
else if (value is IEnumerable<IRemoteStreamContent> remoteStreamContents)
{
foreach (var content in remoteStreamContents)
{
var streamContent = new StreamContent(content.GetStream());
streamContent.Headers.ContentType = new MediaTypeHeaderValue(content.ContentType);
postDataBuilder.Add(streamContent, parameter.Name, parameter.Name);
}
}
else
{
postDataBuilder.Add(new StringContent(value.ToString(), Encoding.UTF8), parameter.Name);
}
}
isFirstParam = false;
return postDataBuilder;
}
else
{
var postDataBuilder = new StringBuilder();
return new StringContent(postDataBuilder.ToString(), Encoding.UTF8, MimeTypes.Application.XWwwFormUrlencoded);
var isFirstParam = true;
foreach (var parameter in parameters.Where(p => p.BindingSourceId == ParameterBindingSources.Form))
{
var value = HttpActionParameterHelper.FindParameterValue(methodArguments, parameter);
if (value == null)
{
continue;
}
postDataBuilder.Append(isFirstParam ? "?" : "&");
postDataBuilder.Append(parameter.Name + "=" + System.Net.WebUtility.UrlEncode(value.ToString()));
isFirstParam = false;
}
return new StringContent(postDataBuilder.ToString(), Encoding.UTF8, MimeTypes.Application.XWwwFormUrlencoded);
}
}
}
}

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

@ -7,8 +7,9 @@ namespace Volo.Abp.Http.ProxyScripting.Generators
public const string Body = "Body";
public const string Path = "Path";
public const string Form = "Form";
public const string FormFile = "FormFile";
public const string Header = "Header";
public const string Custom = "Custom";
public const string Services = "Services";
}
}
}

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

@ -1,5 +1,6 @@
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.Conventions;
using Volo.Abp.Http.Client;
using Volo.Abp.Http.DynamicProxying;
using Volo.Abp.Http.Localization;
@ -7,6 +8,7 @@ using Volo.Abp.Localization;
using Volo.Abp.Localization.ExceptionHandling;
using Volo.Abp.Modularity;
using Volo.Abp.TestApp;
using Volo.Abp.TestApp.Application.Dto;
using Volo.Abp.VirtualFileSystem;
namespace Volo.Abp.Http
@ -44,6 +46,12 @@ namespace Volo.Abp.Http
{
options.MapCodeNamespace("Volo.Abp.Http.DynamicProxying", typeof(HttpClientTestResource));
});
Configure<AbpAspNetCoreMvcOptions>(options =>
{
options.ConventionalControllers.FormBodyBindingIgnoredTypes.Add(typeof(CreateFileInput));
options.ConventionalControllers.FormBodyBindingIgnoredTypes.Add(typeof(CreateMultipleFileInput));
});
}
}
}

88
framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PersonAppServiceClientProxy_Tests.cs

@ -4,10 +4,13 @@ using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using NSubstitute.Extensions;
using Shouldly;
using Volo.Abp.Application.Dtos;
using Volo.Abp.AspNetCore.Mvc.Conventions;
using Volo.Abp.Content;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Http.Client;
@ -197,5 +200,90 @@ namespace Volo.Abp.Http.DynamicProxying
});
result.ShouldBe("UploadAsync:application/rtf");
}
[Fact]
public async Task UploadMultipleAsync()
{
var memoryStream = new MemoryStream();
await memoryStream.WriteAsync(Encoding.UTF8.GetBytes("File1"));
memoryStream.Position = 0;
var memoryStream2 = new MemoryStream();
await memoryStream2.WriteAsync(Encoding.UTF8.GetBytes("File2"));
memoryStream2.Position = 0;
var result = await _peopleAppService.UploadMultipleAsync(new List<IRemoteStreamContent>()
{
new RemoteStreamContent(memoryStream)
{
ContentType = "application/rtf"
},
new RemoteStreamContent(memoryStream2)
{
ContentType = "application/rtf2"
}
});
result.ShouldBe("File1:application/rtfFile2:application/rtf2");
}
[Fact]
public async Task CreateFileAsync()
{
var memoryStream = new MemoryStream();
await memoryStream.WriteAsync(Encoding.UTF8.GetBytes("CreateFileAsync"));
memoryStream.Position = 0;
var result = await _peopleAppService.CreateFileAsync(new CreateFileInput()
{
Name = "123.rtf",
Content = new RemoteStreamContent(memoryStream)
{
ContentType = "application/rtf"
}
});
result.ShouldBe("123.rtf:CreateFileAsync:application/rtf");
}
[Fact]
public async Task CreateMultipleFileAsync()
{
var memoryStream = new MemoryStream();
await memoryStream.WriteAsync(Encoding.UTF8.GetBytes("File1"));
memoryStream.Position = 0;
var memoryStream2 = new MemoryStream();
await memoryStream2.WriteAsync(Encoding.UTF8.GetBytes("File2"));
memoryStream2.Position = 0;
var memoryStream3 = new MemoryStream();
await memoryStream3.WriteAsync(Encoding.UTF8.GetBytes("File3"));
memoryStream3.Position = 0;
var result = await _peopleAppService.CreateMultipleFileAsync(new CreateMultipleFileInput()
{
Name = "123.rtf",
Contents = new List<IRemoteStreamContent>()
{
new RemoteStreamContent(memoryStream)
{
ContentType = "application/rtf"
},
new RemoteStreamContent(memoryStream2)
{
ContentType = "application/rtf2"
}
},
Inner = new CreateFileInput()
{
Name = "789.rtf",
Content = new RemoteStreamContent(memoryStream3)
{
ContentType = "application/rtf3"
}
}
});
result.ShouldBe("123.rtf:File1:application/rtf123.rtf:File2:application/rtf2789.rtf:File3:application/rtf3");
}
}
}

11
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/CreateFileInput.cs

@ -0,0 +1,11 @@
using Volo.Abp.Content;
namespace Volo.Abp.TestApp.Application.Dto
{
public class CreateFileInput
{
public string Name { get; set; }
public RemoteStreamContent Content { get; set; }
}
}

14
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/Dto/CreateMultipleFileInput.cs

@ -0,0 +1,14 @@
using System.Collections.Generic;
using Volo.Abp.Content;
namespace Volo.Abp.TestApp.Application.Dto
{
public class CreateMultipleFileInput
{
public string Name { get; set; }
public IEnumerable<IRemoteStreamContent> Contents { get; set; }
public CreateFileInput Inner { get; set; }
}
}

7
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IPeopleAppService.cs

@ -25,5 +25,12 @@ namespace Volo.Abp.TestApp.Application
Task<IRemoteStreamContent> DownloadAsync();
Task<string> UploadAsync(IRemoteStreamContent streamContent);
Task<string> UploadMultipleAsync(IEnumerable<IRemoteStreamContent> streamContents);
Task<string> CreateFileAsync(CreateFileInput input);
Task<string> CreateMultipleFileAsync(CreateMultipleFileInput input);
}
}

41
framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs

@ -86,5 +86,46 @@ namespace Volo.Abp.TestApp.Application
return await reader.ReadToEndAsync() + ":" + streamContent.ContentType;
}
}
public async Task<string> UploadMultipleAsync(IEnumerable<IRemoteStreamContent> streamContents)
{
var str = "";
foreach (var content in streamContents)
{
using (var reader = new StreamReader(content.GetStream()))
{
str += await reader.ReadToEndAsync() + ":" + content.ContentType;
}
}
return str;
}
public async Task<string> CreateFileAsync(CreateFileInput input)
{
using (var reader = new StreamReader(input.Content.GetStream()))
{
return input.Name + ":" + await reader.ReadToEndAsync() + ":" + input.Content.ContentType;
}
}
public async Task<string> CreateMultipleFileAsync(CreateMultipleFileInput input)
{
var str = "";
foreach (var content in input.Contents)
{
using (var reader = new StreamReader(content.GetStream()))
{
str += input.Name + ":" + await reader.ReadToEndAsync() + ":" + content.ContentType;
}
}
using (var reader = new StreamReader(input.Inner.Content.GetStream()))
{
str += input.Inner.Name + ":" + await reader.ReadToEndAsync() + ":" + input.Inner.Content.ContentType;
}
return str;
}
}
}

Loading…
Cancel
Save