-2

I'm currently refactoring a microservice which uses RestSharp's RestClient to call out to Personio, in order to use the latest version of RestSharp (v107), as well as to use ExecuteAsync instead of Execute.

I have the following method:

    [SuppressMessage("Style", "IDE0053:Use expression body for lambda expressions", Justification = "lambda leave Match ambiguous.")]
    public TryAsync<T> WithAuthorization<T>(Func<Token, Task<T>> doSomething, CancellationToken cancellationToken) =>
        TryAsync(async () =>
        {
            T? result = default;
            Exception? resultException = null;
            TryAsync<Token> authorizationAttempt = TryAuthorize(cancellationToken);
            _ = await apply(token => doSomething(token), authorizationAttempt)
                .Match(
                    Succ: async dataTask =>
                    {
                        result = await dataTask;
                    },
                    Fail: exception =>
                    {
                        resultException = exception;
                    }
                )
                .ConfigureAwait(false);

            // FIXME:  Does not wait!!!!

            return result
                ?? ((resultException == null)
                    ? throw new PersonioRequestException("Could not get data from Personio: Reason unknown.")
                    : throw new PersonioRequestException($"Could not get data from Personio: {resultException.Message}", resultException)
                );
        });

As indicated in the code above, the method does not wait before returning the result, or as the case happens to be throwing an exception with reason unknown.

With the debugger, I've been able to determine authorizationAttempt gets a value and doSomething() is invoked, but while waiting for a response, the error is thrown.

The code which consumes the above method, providing the function for doSomething() is this:

public TryAsync<RequestResponse<T>> TryGet<T>(RequestOptions options, CancellationToken cancellationToken) =>
        _authorizationClient.WithAuthorization<RequestResponse<T>>(
            async token =>
                {
                    UriBuilder urlBuilder = new(_personioConfig.BaseUrl.AppendPathSegment(options.Endpoint))
                    {
                        // This is used to add parameters which are used for filtering and may be unique for the record type, such as "updated_from".
                        Query = options.QueryParameters.ToString()
                    };

                    RestRequest request = new(urlBuilder.Uri, Method.Get);
                    request.Timeout = -1;
                    if (options.Pagination.IsActive)
                    {
                        request = request.AddQueryParameters(options.Pagination);
                    }

                    request = request
                        .AddHeader("Accept", "application/json")
                        .AddHeader("Authorization", $"Bearer {token.Value}");

                    return await GetRecords<T>(request, cancellationToken);
                },
                cancellationToken
            );

    private async Task<RequestResponse<T>> GetRecords<T>(RestRequest request, CancellationToken cancellationToken)
    {
        RestResponse<RequestResponse<T>> requestResponse = await _restClient.ExecuteAsync<RequestResponse<T>>(request, cancellationToken);

        // FIXME: The next line is never executed.

        RequestResponse<T>? dataResponse = JsonConvert.DeserializeObject<RequestResponse<T>>(requestResponse?.Content ?? "");
        return (requestResponse?.IsSuccessful ?? false)
            ? (dataResponse != null && dataResponse.WasSuccessful)
                ? dataResponse
                : throw new PersonioRequestException("Connected to Personio, but could not get records.")
            : throw (
                (requestResponse?.ErrorException != null)
                    ? new("Could not get records from Personio.", requestResponse.ErrorException)
                    : new($"Could not get records from Personio. {dataResponse?.Error?.Message ?? UnknownProblem}."));
    }

As indicated in the code above, the method GetRecords() is invoked, but the system throws an error from result (back in the first method above) being unpopulated before ExecuteAsync() has any sort of result.

An earlier form of the code, with an earlier version of RestSharp(v106) and synchronous execution worked fine. At that time, the TryGet looked like:

    public TryAsync<RequestResponse<T>> TryGet<T>(RequestOptions options) =>
        _authorizationClient.WithAuthorization<RequestResponse<T>>(token =>
        {
            UriBuilder urlBuilder = new(_personioConfig.BaseUrl.AppendPathSegment(options.Endpoint))
            {
                // This is used to add parameters which are used for filtering and may be unique for the record type, such as "updated_from".
                Query = options.QueryParameters.ToString()
            };
            _restClient.BaseUrl = urlBuilder.Uri;
            _restClient.Timeout = -1;

            IRestRequest request = new RestRequest(Method.GET);
            if (options.Pagination.IsActive)
            {
                request = request.AddQueryParameters(options.Pagination);
            }

            request = request
                .AddHeader("Accept", "application/json")
                .AddHeader("Authorization", $"Bearer {token.Value}");

            return Task.FromResult(GetRecords<T>(request));
        });

    private RequestResponse<T> GetRecords<T>(IRestRequest request)
    {
        IRestResponse<RequestResponse<T>> requestResponse = _restClient.Execute<RequestResponse<T>>(request);
        RequestResponse<T>? dataResponse = JsonConvert.DeserializeObject<RequestResponse<T>>(requestResponse.Content);
        return requestResponse.IsSuccessful
            ? (dataResponse != null && dataResponse.WasSuccessful)
                ? dataResponse
                : throw new PersonioRequestException("Connected to Personio, but could not get records.")
            : throw (
                (requestResponse.ErrorException != null)
                    ? new("Could not get records from Personio.", requestResponse.ErrorException)
                    : new($"Could not get records from Personio. {dataResponse?.Error?.Message ?? UnknownProblem}."));
    }

What am I doing wrong or missing? How can I make this work using RestSharp 107.x and ExecuteAsync()?

Brian Kessler
  • 2,187
  • 6
  • 28
  • 58
  • 1
    If you want something to wait, then you need to `await` it. What is `TrayAsync`? Is it some custom implementation of `Task`? – DavidG Jan 24 '22 at 17:30
  • 2
    This looks awfully convoluted, I'd try and refactor this to be more straightforward... TryAsync, Match, apply - its not clear what any of these do, or why they are needed, they seem to just be complicating matters – Milney Jan 24 '22 at 17:55
  • 1
    I would replace `WithAuthorization` with a custom authenticator. I have an example here https://restsharp.dev/usage.html#authenticator. You really don't want to hit the authentication endpoint all the time. Plus, using Polly for handling retries will greatly simplify your code. Lastly, you mentioned that your code throws an exception, but you never mentioned what is the exception. – Alexey Zimarev Jan 24 '22 at 19:47
  • @AlexeyZimarev, Personio has a _really_stupid_ API. They _require_ a new token for _every_ call to their API. Yes, that is ridiculous and sad, but it makes it necessary.... As for the exception, it is an instance of PersonioRequestException("Could not get data from Personio: Reason unknown.") because result is null. – Brian Kessler Jan 24 '22 at 21:02
  • 1
    Ok, I see. But then again, my authenticator example can be easily changed to request the token every time. You'd need to remove the stored token property, and, preferably, keep the RestClient instance used for auth calls rather than wrapping it in `using`. – Alexey Zimarev Jan 24 '22 at 21:04
  • 1
    Btw the Accept header contains `application/json` anyway, you don't need to set it explicitly. Also, when calling `restClient.ExecuteAsync>(request)`, the `response.Data` will give you the deserialized value directly, you don't need to deserialize it manually again. – Alexey Zimarev Jan 24 '22 at 21:05
  • @AlexeyZimarev, I'll check into that tomorrow at work, thanks :-) – Brian Kessler Jan 24 '22 at 21:06
  • Just one point is that you use v106 or earlier, which is quite different from v107. My example is for v107. – Alexey Zimarev Jan 24 '22 at 21:06
  • @AlexeyZimarev, with the above, I am _trying_ to use v107, though you are correct that the working synchronous code is from v106. – Brian Kessler Jan 24 '22 at 21:07
  • 1
    I can suggest sending your requests using both v106 and v107 (try the latest pre-release, I've fixed some bugs there) to something like https://requestbin.com, you'll spot the difference right away. – Alexey Zimarev Jan 24 '22 at 21:09
  • @DavidG, `TryAsync` is part of C# language-ext: https://github.com/louthy/language-ext – Brian Kessler Jan 24 '22 at 21:11
  • @Milney, `TryAsync` is a functional way of handling try/catch; `Match` is used to ensure that the process was successful or errors are handled instead of ignored. – Brian Kessler Jan 24 '22 at 21:13
  • Have you tried it without the TryAsync? Suspiciously similar issue on their Github: https://github.com/louthy/language-ext/issues/964 ... maybe it's just a bug in that lib? – Milney Jan 24 '22 at 23:21
  • @Milney, while the issue could be related to the library, it would be a different issue since I see the code is being called, just the system is not waiting as expected. As I like the library (as I like functional programming), I would rather learn why it is not waiting and learn how to make it wait. Removing the TryAsync could be a plan B, though as mentioned above, it is working with TryAsync as long as I am not using ExecuteAsync. – Brian Kessler Jan 25 '22 at 08:06
  • @AlexeyZimarev, I got ExecuteAsync working (by removing some of the TryAsync and some dependency injection), but there appears to be some defects or limitations with response.Data. While this is being initialized, it isn't being populated with data from the response, so I still can't use it. :-( – Brian Kessler Jan 25 '22 at 15:37

1 Answers1

0

With thanks to @AlexeyZimarev, I was able to get the TryGet() method working.

It now looks like:

    public TryAsync<RequestResponse<T>> TryGet<T>(RequestOptions options, CancellationToken cancellationToken) =>
        TryAsync(async () =>
        {
            _restClient.Authenticator = _authorizationClient;
            Uri uri = new UriBuilder(_personioConfig.BaseUrl.AppendPathSegment(options.Endpoint)).Uri;
            RestRequest request = new RestRequest(uri, Method.Get)
               .AddQueryParameters(options.QueryParameters);

            if (options.Pagination.IsActive)
            {
                request = request.AddQueryParameters(options.Pagination);
            }

            request.Timeout = -1;
            return await GetRecords<T>(request, cancellationToken);
        });

The line _restClient.Authenticator = _authorizationClient; does not very safe here, since it is being assigned on an injected instead of RestClient, which in the future might be used elsewhere, but I "solved" this by making the client transient instead of a singleton.

I also removed all the TryAsync from PersonioAuthorizationClient since it didn't seem to play well with whatever AuthenticatorBase is doing.

Brian Kessler
  • 2,187
  • 6
  • 28
  • 58
  • 1
    If you use `RestClient` as a transient dependency, you'd need to give it an external `HttpClient` instance, which is instantiated by the Http client factory using the handler pool, but you probably are aware. – Alexey Zimarev Jan 25 '22 at 19:49
  • @AlexeyZimarev, can you please elaborate on this? I just used something like `services.AddTransient()` and it seems to work as expected, though I suspect it is suboptimal. – Brian Kessler Jan 25 '22 at 23:36
  • 1
    If you keep instantiating `RestClient`, it will also create an `HttpClient` and `HttpClientHandler` each time. Check this https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests#issues-with-the-original-httpclient-class-available-in-net. I'd say you need to do something like `services.AddHttpClient("Personio"); services.AddTransient(sp => new RestClient(sp.GetRequiredService().CreateClient("Personio")))` – Alexey Zimarev Jan 26 '22 at 07:22
  • Makes sense, thanks! – Brian Kessler Jan 26 '22 at 07:55