Starts in:
2 DAYS
20 HRS
56 MIN
41 SEC
Starts in:
2 D
20 H
56 M
41 S

Activities of "alexandru-bagu"

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:

  1. IStreamService<TService> in <Namespace>.HttpApi.Client
   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);
    }
  1. StreamService<TService> in <Namespace>.HttpApi.Client
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;
        }
    }
  1. StreamDownloadResult in <Namespace>.HttpApi.Client
    public class StreamDownloadResult
    {
        public Stream Stream { get; set; }
        public long ContentLength { get; set; }
        public string ContentType { get; set; }
        public string FileName { get; set; }
    }
  1. HttpClientAUthenticatorMessageHandler<TService> in <Namespace>.HttpApi.Client
    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);
            }
        }
    }
  1. Configure your stream service in <Namespace>.HttpApi.Client module
        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>>();
        }
  1. Make use of IStreamService<YOUR APPLICATION SERVICE> using Dependency Injection in one of your classes:
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.

Why is there no solution offered for this? Is there actually no one else needing raw access to streams? Am I the only one who has to consider possibly uploading files worth of gigabytes?

Issues I encountered so far:

  1. There is no way to send a stream straight over a proxied interface. As far as I can tell one would only have to modify Volo.Abp.Http.Client.DynamicProxying.RequestPayloadBuilder::BuildContent to generate a proper StreamContent for streams. I don't actually see a reason why you simply ignore streams all together from being sent over the proxied httpclient.
  2. What if I want to download a large blob without converting it to a dto, but actually getting it raw as you're meant to work with blobs? My current only solution is to replicate what you do for the proxied http client and call the api myself and handle the http response message (and content) as I see fit.
  3. Why is System.IO.AbpStreamExtensions::CopyToAsync changing the stream by setting Position = 0? In the real world we don't work only with MemoryStream and even if we did, that extension which is used EVERYWHERE, because I guess Stream.CopyToAsync was not good enough, would mess things up if you did not want to include the whole stream.
  4. Why the duck do you guys love so much MemoryStreams and not use proper dotnet apis? Why when I request a stream for a blob backed by Volo.Abp.BlobStoring.FileSystem you guys read the whole file in a MemoryStream then return it, much wow here - not setting it's position to 0. Like... why??!? So if I want to store blobs that are huge (up to 1gb) I must also have the ram to load it in the memory when I don't even need that. All I should do is give the FileStream to the MVC controller and it knows all by it's widdle self how to manage it.

Please add support for streams and either undo what you did with that extension "CopyToAsync" or make it a service so we can replace it so it works as it should.

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.

Showing 1 to 10 of 12 entries
Made with ❤️ on ABP v9.1.0-preview. Updated on November 20, 2024, 13:06