Open michaelstaib opened 3 years ago
I need to pass an Accept-Language header to translate the response. Right now I can't find a mechanism to pass the value for this header when invoking the operation. Would this proposal allow me to do that?
Why do you not use the HttpClientFactory?
I could, but it is cumbersome to pass data from the caller of the ExecuteAsync
method to the ConfigureHttpClient
method, as there is no opportunity to pass context. I now use a static AsyncLocal
variable, but I think that that is not pretty.
In the previous version of StrawBerryShake there was a partial parameter to each operation method that I could extend. Also, it had support for middleware that could access this operation parameter and read my extensions.
Anyway, AsyncLocal
is sufficient for now, but the previous solution was more elegant.
But to your question, this proposal would allow you to do that.
@jorrit do you have a snippet on how you used the AsyncLocal
? Is it used within the .ConfigureHttpClient
of the IHttpClientFactory
? Thanks!
I have this utility class:
using StrawberryShake;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
#nullable enable
namespace ANWBReizen.Travelhome.API.ApiClients.Product.Client
{
public static class LanguageOperation
{
private static readonly AsyncLocal<string> Store = new AsyncLocal<string>();
private static readonly string[] Languages = new[] { "en", "nl" };
public static string? Value => Store.Value;
public static async Task<IOperationResult<TResultData>> Run<TResultData>(string language, Func<CancellationToken, Task<IOperationResult<TResultData>>> implementation)
where TResultData : class
{
Store.Value = language;
return await implementation(CancellationToken.None);
}
public static async Task<IDictionary<string, TResultData>> RunAll<TResultData>(Func<CancellationToken, Task<IOperationResult<TResultData>>> implementation)
where TResultData : class
{
var tasks = Languages.ToDictionary(lang => lang, lang => Run(lang, implementation));
await Task.WhenAll(tasks.Values);
return tasks.ToDictionary(t => t.Key, t =>
{
var result = t.Value.Result;
result.EnsureNoErrors();
if (result.Data == null)
{
throw new Exception("No data");
}
return result.Data;
});
}
}
}
In Startup.cs I have:
var builder = services.AddProductApi()
.ConfigureHttpClient((sp, client) =>
{
var options = sp.GetRequiredService<IOptions<ProductOptions>>();
client.BaseAddress = options.Value.Url;
client.Timeout = timeout;
var language = LanguageOperation.Value;
if (language != null)
{
client.DefaultRequestHeaders.Add("Accept-Language", language);
}
});
I call it like this:
var results = await LanguageOperation.RunAll(_apiClient.VehicleFacets.ExecuteAsync);
Missing ability to set per-request HTTP headers is deal-breaker for me using Strawbery Shake. I think it is such basic feature I'm bafled how could it ship without it. And the workaround using AsyncLocal is iffy at best.
I still have a need for this even though the stale bot doesn't think so.
Hello,
Same boat here, same problem as in #3533. No apparent good way to obtain a token from a scoped service.
Any suggestions?
This would be very useful in multi-tenant situations where the client reads from multiple instances of the same service, but different access tokens and base url (ie, the Canvas LMS API) for each individual request.
as I commented in https://github.com/ChilliCream/graphql-platform/issues/6426 I would also appreciate the ability to specify custom headers per request.
as I commented in #6426 I would also appreciate the ability to specify custom headers per request.
I need that as well !
Hello,
I'm also interested in this, I need it the context of a Shopify App where multiple shop are managed and each one have a different access token.
I found a workaround if anyone need this until something better is added in Strawberry Shake, it's based on the previous work here https://github.com/ChilliCream/graphql-platform/issues/6446#issuecomment-1681161634
Using AsyncLocal
we can pass some context through multiple calls (kind of like if we added a new parameter)
So first define a context to store the addtional data you need
public record MyCustomContext(string CustomUrlPart, string CustomHeaderValue);
Then create a context accessor (same as IHttpContextAccessor
in ASP.NET Core)
public interface IMyCustomContextAccessor
{
MyCustomContext Current { get; set; }
}
public class MyCustomContextAccessor : IMyCustomContextAccessor
{
public static readonly AsyncLocal<MyCustomContext> Context = new();
public MyCustomContext Current
{
get => Context.Value ?? throw new Exception("Call `IMyCustomContextAccessor.Current = ` before calling .ExecuteAsync()");
set => Context.Value = value;
}
}
public class CustomHeaderGraphQlDelegationHandler(IMyCustomContextAccessor MyCustomContextAccessor) : DelegatingHandler
{
private void ConfigureCustomHeader(HttpRequestMessage request)
{
request.RequestUri = new Uri($"https://{MyCustomContextAccessor.Current.CustomUrlPart}/graphql.json");
request.Headers.Add("X-My-Custom-Header", MyCustomContextAccessor.Current.CustomHeaderValue);
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
ConfigureCustomHeader(request);
return await base.SendAsync(request, cancellationToken);
}
protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
{
ConfigureCustomHeader(request);
return base.Send(request, cancellationToken);
}
}
Register everything
services.AddSingleton<IMyCustomContextAccessor, MyCustomContextAccessor>();
services.AddSingleton<CustomHeaderGraphQlDelegationHandler>();
services.AddShopifyGraphQlClient().ConfigureHttpClient(
client => client.BaseAddress = new Uri("https://ignored.com"), // This is just a random URL which will be overwritten by the handler
builder => builder.AddHttpMessageHandler<CustomHeaderGraphQlDelegationHandler>());
Then you just need to inject IMyCustomContextAccessor
and then call ExecuteAsync()
.
private readonly IMyCustomContextAccessor accessor;
...
public Task DoSomething()
{
accessor.Current = new ...
client.Xxxx.ExecuteAsync()
}
You can also add an extension method to simplify usage
public static class OperationRequestFactoryExtensions
{
public static Task<TResult> ExecuteAsAsync<T, TResult>(
this T factory,
MyCustomContext auth,
Func<T, Task<TResult>> execute
) where T : IOperationRequestFactory
{
MyCustomContextAccessor.Context.Value = auth;
return execute(factory);
}
}
and then
var authContext = new MyCustomContext("abc", "def");
var result = await client.SomeMutation.ExecuteAsAsync(authContext, f => f.ExecuteAsync(new object(), cancellationToken));
Transport Context Data
In some cases, we want to pass in extra information with a GraphQL request that can be used in the client middleware to enrich the request.
Since Strawberry Shake supports multiple transports we want to do it in a way that does not bind this to a specific transport technology.
Context Directives
This is where context directives come in that we can use on operation definitions.
Let's say we have the following query request where we want to pass along some extra context-data that we can use in the request pipeline to either enrich the transport or even to enrich local processing.
In our example, we want to pass in an object that shall be used to create request headers when this request is executed over HTTP.
For this we will introduce a directive and an input type in the
schema.extensions.graphql
.This new directive can now be used on queries and allows us to tell the Strawberry Shake compiler to generate the C# request in a way that we need to pass in the extra information.
This will result in the generation of a required new parameter on the client.