Do I get my question back now that I answered my own question?
For anyone who is interested in a solution using the current ABP version (3.2.0) you must do all the hardwork yourself. While this issue has been assigned to version 4.1 preview, I took it upon myself to implement it and I've already created a pull request for it.
If you want to do this using the current version you will need a few things:
public interface IStreamService<TService>
{
Task UploadAsync(string route, Stream stream, string method = "POST", CancellationToken cancellationToken = default);
Task<StreamDownloadResult> DownloadAsync(string route, string method = "GET", CancellationToken cancellationToken = default);
}
public class StreamService<TService> : IStreamService<TService>
{
private readonly HttpClient _httpClient;
private readonly IJsonSerializer _jsonSerializer;
private readonly AbpHttpClientOptions _abpHttpClientOptions;
private readonly AbpRemoteServiceOptions _abpRemoteServiceOptions;
public StreamService(HttpClient httpClient, IJsonSerializer jsonSerializer, IOptions<AbpHttpClientOptions> abpHttpClientOptions, IOptions<AbpRemoteServiceOptions> abpRemoteServiceOptions)
{
_httpClient = httpClient;
_jsonSerializer = jsonSerializer;
_abpHttpClientOptions = abpHttpClientOptions.Value;
_abpRemoteServiceOptions = abpRemoteServiceOptions.Value;
}
public async Task<StreamDownloadResult> DownloadAsync(string route, string method = "GET", CancellationToken cancellationToken = default)
{
StreamDownloadResult result = new StreamDownloadResult();
var response = await makeRequest(route, null, method, cancellationToken);
result.ContentType = response.Content.Headers.ContentType.ToString();
result.ContentLength = response.Content.Headers.ContentLength.Value;
result.FileName = response.Content.Headers.ContentDisposition.FileName;
result.Stream = await response.Content.ReadAsStreamAsync();
return result;
}
public async Task UploadAsync(string route, Stream stream, string method = "POST", CancellationToken cancellationToken = default)
{
await makeRequest(route, new StreamContent(stream), method, cancellationToken);
}
private async Task ThrowExceptionForResponseAsync(HttpResponseMessage response)
{
if (response.Headers.Contains("_AbpErrorFormat"))
{
IJsonSerializer jsonSerializer = _jsonSerializer;
throw new AbpRemoteCallException(jsonSerializer.Deserialize<RemoteServiceErrorResponse>(await response.Content.ReadAsStringAsync(), true).Error);
}
var text = await response.Content.ReadAsStringAsync();
if (text.Contains(":"))
{
text = text.Substring(text.IndexOf(':') + 1);
if (text.Contains("\r\n "))
{
text = text.Substring(0, text.IndexOf("\r\n "));
throw new AbpRemoteCallException(new RemoteServiceErrorInfo() { Message = text });
}
}
throw new AbpException($"Remote service returns error! HttpStatusCode: {response.StatusCode}, ReasonPhrase: {response.ReasonPhrase}");
}
private async Task<HttpResponseMessage> makeRequest(string route, HttpContent content, string method, CancellationToken cancellationToken)
{
var serviceType = typeof(TService);
DynamicHttpClientProxyConfig clientConfig = _abpHttpClientOptions.HttpClientProxies.GetOrDefault(serviceType) ?? throw new AbpException("Could not get DynamicHttpClientProxyConfig for " + serviceType.FullName + ".");
RemoteServiceConfiguration remoteServiceConfig = _abpRemoteServiceOptions.RemoteServices.GetConfigurationOrDefault(clientConfig.RemoteServiceName);
string url = remoteServiceConfig.BaseUrl.EnsureEndsWith('/') + route.TrimStart('/');
var requestMessage = new HttpRequestMessage(new HttpMethod(method), url) { Content = content };
HttpResponseMessage response = await _httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
if (!response.IsSuccessStatusCode) await ThrowExceptionForResponseAsync(response);
return response;
}
}
public class StreamDownloadResult
{
public Stream Stream { get; set; }
public long ContentLength { get; set; }
public string ContentType { get; set; }
public string FileName { get; set; }
}
public class HttpClientAuthenticatorMessageHandler<TService> : DelegatingHandler
{
private readonly AbpHttpClientOptions _abpHttpClientOptions;
private readonly AbpRemoteServiceOptions _abpRemoteServiceOptions;
private readonly IHttpClientFactory _httpClientFactory;
private readonly IServiceProvider _serviceProvider;
public HttpClientAuthenticatorMessageHandler(IHttpClientFactory httpClientFactory, IOptions<AbpHttpClientOptions> abpHttpClientOptions, IOptions<AbpRemoteServiceOptions> abpRemoteServiceOptions, IServiceProvider serviceProvider)
{
_abpHttpClientOptions = abpHttpClientOptions.Value;
_abpRemoteServiceOptions = abpRemoteServiceOptions.Value;
_httpClientFactory = httpClientFactory;
_serviceProvider = serviceProvider;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var remoteServiceHttpClientAuthenticator = _serviceProvider.GetService<IRemoteServiceHttpClientAuthenticator>();
var serviceType = typeof(TService);
DynamicHttpClientProxyConfig clientConfig = _abpHttpClientOptions.HttpClientProxies.GetOrDefault(serviceType) ?? throw new AbpException("Could not get DynamicHttpClientProxyConfig for " + serviceType.FullName + ".");
RemoteServiceConfiguration remoteServiceConfig = _abpRemoteServiceOptions.RemoteServices.GetConfigurationOrDefault(clientConfig.RemoteServiceName);
using (var client = _httpClientFactory.CreateClient())
{
await remoteServiceHttpClientAuthenticator.Authenticate(new RemoteServiceHttpClientAuthenticateContext(client, request, remoteServiceConfig, clientConfig.RemoteServiceName));
foreach (var header in client.DefaultRequestHeaders)
request.Headers.Add(header.Key, header.Value);
return await base.SendAsync(request, cancellationToken);
}
}
}
public static void ConfigureStreamServices(ServiceConfigurationContext context)
{
context.Services.AddTransient<HttpClientAuthenticatorMessageHandler<YOUR APPLICATION SERVICE>>();
context.Services.AddHttpClient<IStreamService<YOUR APPLICATION SERVICE>, StreamService<YOUR APPLICATION SERVICE>>()
.AddHttpMessageHandler<HttpClientAuthenticatorMessageHandler<YOUR APPLICATION SERVICE>>();
}
public ClassConstructor(..., IStreamService<YOUR APPLICATION SERVICE> streamService, ...)
{
...
_streamService = streamService;
...
}
public async Task RunAsync()
{
var id = Guid.NewGuid();
string fileName = "asd";
await _streamService.UploadAsync($"/api/app/APP SERVICE PATH/upload/{id}", new FileStream(fileName, FileMode.Open));
var text = File.ReadAllText(fileName);
var dlStream = await _streamService.DownloadAsync($"/api/app/APP SERVICE PATH/download/{id}");
var reader = new StreamReader(dlStream.Stream);
var dlText = reader.ReadToEnd();
if (text != dlText)
{
throw new Exception("Uploaded log does not match downloaded log.");
}
}
Say what now? So I should waste all of my server's memory just because "application services shouldn't use IFormFile or Stream"? I agree that IFormFile should only be in the api controller but stream? What you are probably trying to say is that because most of the time application services interfaces are accessed using proxies one must use byte[] because Stream is not serializable. However that does not show that the proper way to handle this kind of work is using byte[], That just shows that you don't have any proper support for streams.
As this conversation continues it seems I need to clarify some things. I already have full support of streams done in my code, that is not the point of this thread. My question is whether any proper support for streaming will be included in ABP and whether there will be any documentation on how to authenticate an HttpClient inside a scope that already has application services proxies working.
At the moment there is no proper way to do any of this other than avoiding app service proxies and digging through abp framework's code to see what one has to copy from there to actually be able to create his own HttpClient and authenticate it to be able to do custom requests.
Totally unhelpful.
https://github.com/abpframework/abp/blob/dev/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Http/CliHttpClient.cs does not have any authentication provided to it.
The code for the FileDescriptorController uses IFormFile which is buffered according to MSDN. More over, based on default settings for IIS, buffered requests cannot go past 30 MB.
Nothing prevents me to create a new controller, that is true but there is no support/documentation for how to authenticate a http client to be able to use the authorization policies already in place.
Thanks, I hadn't thought of checking that but it makes sense. It might have been helpful to specify a reason for not being able to download the source code instead of a generic 403.
https://prnt.sc/tzegb2
I have been having this issue for the past two weeks. Are we not supposed to be able to download the sources?
The connection string issue is actually not because of code generation but because of a UI bug. I was just not paying enough attention although I am sure I am not the only one who faced this issue. The issue is when you click the Tiered checkbox first time - it resets the connection string. Why would it reset the connection string when it is after the text box for connection string is what I am wondering. https://www.youtube.com/watch?v=A8UTMm9VRhg&feature=youtu.be
"if a user reports bug in a question, we refund the credit for that question." - unwritten rule? The reason I was upset about this thing was that there were no such indication anywhere. Maybe add some text relaying this information in the view for posting a new issue and maybe even prompt people to post the bugs in the pinned topic.
I do wonder, if I were to post questions about how to do stuff that are undocumented along with their solution, would my question be refunded? Would you not want the community to contribute and make your support life better? The lack of documentation or the existing documentation being so unclear is annoying to say the least.
@sean.alford - While that is a solution, to not spend questions, why is there no "Report a bug" link anywhere? It would just have to forward people to the thread you said, nothing more. However everyone is left questioning whether there's even a point to reporting anything if there is no designated, clear channel for it. My first thought was that we should simply refund the purchase, there are other products available.
"The team is usually very quick to respond to issues." - It took them 7 days to release 3.0.4 and I have no clue if they fixed the issue I opened with this thread - no answers from them. Was I supposed to wait around 7 days for them to release a fix?
The aggressive schedule you are speaking of is not one that the comunity demanded, maybe only suggested. If they don't have the time to implement new features and also keep the product working correctly then they bit off more than they can chew, so to speak. For my first project using this product I had a broken product with no clear instructions as to how to select a specific version that I know previously worked. Same thing happened when I updated the CLI to 3.0.4 - generating views no longer worked, I had to update the projects then rebuild then try to generate views and it finally worked. Why is there no "upgrade" command to do all that so you don't have to guess what is going on? Apparently my project is of version 3.0.3 but upgraded to 3.0.4 libs - what is that supposed to mean?
What about disabling tenancy for queries, one could ask as I have. The API changed yet there is no documentation about it. If the IgnoreMultiTenancy attribute was supposed to do the job, it didn't. I had to deobfuscate their libraries and skim through their classes to find IsMultiTenantFilterEnabled in the AbpDbContext without any documentation as to how to change the value. I did appreciate it was a virtual field so I could easily override it.
Their software is nice, I am not saying it is not. But the price tag should mean there is a consumer first strategy, we're not talking about a small amount. Or maybe $2k is pocket change - certainly to some, but not to everyone.
While ABP support seems to slack off, check my question. https://support.abp.io/QA/Questions/291/Are-we-supposed-to-fix-your-bugs-in-your-software-that-we-paid-for-Is-this-a-joke It is the fix for your problem, or at least for @mattjoslin's.