Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RFC] RestClientFactory for better DI support #1791

Open
alexeyzimarev opened this issue Mar 15, 2022 · 37 comments
Open

[RFC] RestClientFactory for better DI support #1791

alexeyzimarev opened this issue Mar 15, 2022 · 37 comments

Comments

@alexeyzimarev
Copy link
Member

Issue

When registering the client instance, the last registered client will be injected, if the client is registered as AddSingleton<RestClient>. The only way to work around this is to use typed clients or wrappers.

It works similarly to AddHttpClient, except that AddHttpClient registered HttpClient as a transient dependency, and uses the client factory behind the scenes to pool the message handlers.

It would be nice to support registering named instances, where each named instance has its own options. Then, resolving a particular client instance would be through the factory. It's also similar to IHttpClientFactory behaviour in that sense. It should also be possible to register typed clients the same way as it's done with IHttpClientFactory, where the type name of the typed client is used as the options name.

What's unclear

  • Should named RestClient instances be registered as singletons, or transient? Transient is more work (see the next point)
  • Should we use IHttpClientFactory behind the scenes to delegate the handler pooling work? It only makes sense if we decide to support the transient scope

Based on the decision for the above points, it would be possible to decide on the implementation.

@kendallb
Copy link
Contributor

I would suggest layering on top of IHttpClientFactory for this and letting it do all the hard work. One of the biggest advantages that I see with using transient HttpClient's grabbed from IHttpClientFactory, is that those are also expected to be transient and behind the scenes are using pooled HttpHandlers and have a life cycle of their own and are disposed of after a period of time (2 minutes by default I think) to avoid stale DNS issues.

That way you can then support transient RestClient's again using transient HttpClient's internally and let IHttpClientFactory do all the hard work.

One thing I am not clear on with IHttpClientFactory is how they handle cookies because the cookie container is always registered with the HttpClientHandler. I suspect (and would need to check the IHttpClientFactory implementation to be sure), that the cookie container is always set to a null value, or an empty container when a new HttpClient is requested from the factory, so that the user of the HttpClient can set it's own cookie container and it would not end up getting re-used the next time another request makes use of a pooled HttpClientHandler.

@kendallb
Copy link
Contributor

Well that answers that. Don't use Cookies with HttpClient! LOL. Which means to support cookies correctly in RestClient and static HttpClient's (or using HttpClientFactory), the cookie handling has to be removed and handled at the request level with custom code.

https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-requests?view=aspnetcore-6.0#cookies

Cookies
The pooled HttpMessageHandler instances results in CookieContainer objects being shared. Unanticipated CookieContainer object sharing often results in incorrect code. For apps that require cookies, consider either:

  • Disabling automatic cookie handling
  • Avoiding IHttpClientFactory

@alexeyzimarev
Copy link
Member Author

Yes, I mentioned that before in another issue, don't remember which one. That's why the RestClient constructor that uses the provided HttpClient instead of creating its own will throw if the cookie container is not null in the options.

@kendallb
Copy link
Contributor

If we use my PR to fix the cookie issues, we probably do not need to use HttpClientFactory and then maybe RestClientFactory is not that much use anymore?

#1795

Although I do suspect having RestClientFactory to implement a named (or typed) factory for materializing RestClient instances might be a good way to explain to those looking to use RestSharp that the clients really need to be single instanced on a per API basis.

@kendallb
Copy link
Contributor

kendallb commented Mar 19, 2022

I have built my own factory and am now using it to test our code in development as I am done with the port (which took a week). I still think having the ability to use RestClient per request can help, if we used a HttpClient cache internally.

Would you be interested in me building one in another PR?

@alexeyzimarev
Copy link
Member Author

If you can share the factory code somewhere, I am more than happy to have a look.

@kendallb
Copy link
Contributor

kendallb commented Mar 19, 2022

Well my factory code is a factory for RestClient itself but I think the correct solution is an internal factory for HttpClient. Once my changes for cookies are accept upstream, then it really all boils down to managing HttpClient instances and reusing them across requests when the options match. At least that is the approach I have taken.

I could potentially implement my own HttpClient factory and use that to allocate instances to pass to RestClient, but it would be nice for that to simply be managed internally. The main problem I think is that while most options are simple to key off of, some are not. In my case we don’t use any of those options so I can make my keys relatively simple. The key stuff once cookies are gone is all the configuration of the underlying handler:

handler.Credentials = Options.Credentials;

The problem ones are the proxy, certificate callback and certificates themselves. You would need a way to generate a signature for those to keep them separate. For the proxy I am able to generate a sting to represent the proxy config given how we use it, and for the certificate callback we only use it to disable SSL for some services (mostly internal testing stuff) so I just made that a Boolean. And we don’t use client certificates so no issue there. Maybe there is a way to use the certificate thumbprints for sort that out?

You can see why HttpClientFactory allows for typed handlers as that makes it a lot simple that is for sure. I can’t use typed handlers as we have a massive client class called DocumentLoaded that is used all over the place in older code that handles requests to any url, so that one I need to generate a named key to keep them separate :(

And for my case we are currently still on .net 4.8 using Autofac so we can’t really use HttpClientFactory anyway I don’t think (and I would still need to generate a similar key to get named instances if I did want to use it).

@kendallb
Copy link
Contributor

Here is what we are currently using for our client factory. But as explained above, we cannot support some stuff so I just turned it off so if we do try to use it with those set in RestClientOptions, it would fail. Not entirely sure there is a clean way to implement keying off those options.

We also assume that the appropriate serializer set on the client is never changed for that client instance so we don't vary that. I don't know of a clean way to key on that since it's a function callback and we have quite a few spots where we need to use custom JSON serialization settings for certain REST services we connect to. That is one reason I think the seralizer really should not be part of the client at all, if we are expected to use them as singletons - it should be set on the request so you could theoretically have a different serializer for different requests to the same REST endpoint. Or more importantly if you happened to share an instance across two different endpoints (like in our DocumentLoader code), it won't cause an issue. But in our code our DocumentLoader is never used for REST services at all, it's just for HTML scraping stuff so the serializer does not matter. I probably should re-write that to live directly on top of HttpClient anyway I think.

using System;
using System.Collections.Generic;
using System.Net;
using System.Text;
using RestSharp;
using RestSharp.Serializers;
using RestSharp.Serializers.NewtonsoftJson;
using RestSharp.Serializers.Xml;

namespace AMain.RestClientHelper
{
    public static class RestClientFactory
    {
        /// <summary>
        /// Dictionary of all static client instances based on the client API base URL
        /// </summary>
        private static readonly Dictionary<string, RestClient> _clientCache = new();

        /// <summary>
        /// Get a client bound to a specific URL that uses only JSON for serialization
        /// </summary>
        /// <param name="cachePrefix">Cache prefix to use for the client cache</param>
        /// <param name="baseUrl">The client base URL</param>
        /// <param name="disableSslChecks">True to disable SSL certificate checks</param>
        /// <returns>RestClient instance to use</returns>
        public static RestClient GetJsonClient(
            string cachePrefix,
            string baseUrl,
            bool disableSslChecks = false)
        {
            return GetClient(cachePrefix, new RestClientOptions(baseUrl), disableSslChecks, () => new JsonNetSerializer());
        }

        /// <summary>
        /// Get a client bound to a specific URL that uses only XML for serialization
        /// </summary>
        /// <param name="cachePrefix">Cache prefix to use for the client cache</param>
        /// <param name="baseUrl">The client base URL</param>
        /// <param name="disableSslChecks">True to disable SSL certificate checks</param>
        /// <returns>RestClient instance to use</returns>
        public static RestClient GetXmlClient(
            string cachePrefix,
            string baseUrl,
            bool disableSslChecks = false)
        {
            return GetClient(cachePrefix, new RestClientOptions(baseUrl), disableSslChecks, () => new XmlRestSerializer());
        }

        /// <summary>
        /// Get a client bound to a specific set of client options. We store static instances of RestClient for each
        /// base URL and related options as we do not want to have an instance per request, and we can cache them statically
        /// and reuse them over and over as they are thread safe provided we fully configure the RestClient in here.
        /// </summary>
        /// <param name="cachePrefix">Cache prefix to use for the client cache</param>
        /// <param name="options">The client options for this client</param>
        /// <param name="disableSslChecks">True to disable SSL certificate checks</param>
        /// <param name="serializerFactory">Optional custom serializer factory to use for requests and responses</param>
        /// <returns>RestClient instance to use</returns>
        public static RestClient GetClient(
            string cachePrefix,
            RestClientOptions options,
            bool disableSslChecks = false,
            Func<IRestSerializer> serializerFactory = null)
        {
            // Bail if we see unsupported features
            if (options.ConfigureMessageHandler != null) {
                throw new ArgumentException("RestClientFactory does not support ConfigureMessageHandler!", nameof(RestClientOptions));
            }
            if (options.Credentials != null || options.UseDefaultCredentials) {
                throw new ArgumentException("RestClientFactory does not support Credentials!", nameof(RestClientOptions));
            }
            if (options.ClientCertificates != null) {
                throw new ArgumentException("RestClientFactory does not support ClientCertificates!", nameof(RestClientOptions));
            }
            if (options.RemoteCertificateValidationCallback != null) {
                throw new ArgumentException("RestClientFactory does not support RemoteCertificateValidationCallback!", nameof(RestClientOptions));
            }
            if (options.Timeout > 0) {
                throw new ArgumentException("RestClientFactory does not support Timeout! Please set it at the request level", nameof(RestClientOptions));
            }

            // Determine the string to use for the proxy if passed in
            var proxyString = "";
            if (options.Proxy is WebProxy proxy) {
                if (proxy.Credentials != null) {
                    var c = proxy.Credentials as NetworkCredential;
                    proxyString = $"http://{c.UserName}:{c.Password}@{proxy.Address.Authority}/";
                }
                proxyString = proxy.Address.AbsoluteUri;
            }

            // Build a key to represent the client in the cache
            var sbKey = new StringBuilder()
                .Append(cachePrefix).Append(':')
                .Append(options.BaseUrl).Append(':')
                .Append(disableSslChecks).Append(':')
                .Append(options.DisableCharset).Append(':')
                .Append(options.AutomaticDecompression).Append(':')
                .Append(options.FollowRedirects).Append(':')
                .Append(options.MaxRedirects).Append(':')
                .Append(proxyString).Append(':')
                .Append(options.Encoding).Append(':')
                .Append(options.PreAuthenticate).Append(':')
                .Append(options.Expect100Continue);
            var key = sbKey.ToString();

            // If we have an existing instance of this client, return it outside of the lock
            if (_clientCache.TryGetValue(key, out var client)) {
                return client;
            }

            // Create a new instance, but make sure only one thread can create it
            lock (_clientCache) {
                // Try again to get it, as it might have just been created by another thread
                if (_clientCache.TryGetValue(key, out client)) {
                    return client;
                }

                // Disable the SSL certificate checks if needed
                if (disableSslChecks) {
                    options.RemoteCertificateValidationCallback = (_, _, _, _) => true;
                }

                // Set up the client with an infinite timeout so the request timeout will be the one that always matters
                options.Timeout = int.MaxValue;

                // Create the client and cache it
                client = new RestClient(options);

                // Set up the single serializer we want to use for this client if desired. If this is passed in as null,
                // it will use the defaults which is both the XML and Newtonsoft.JSON serializers. We do not use the
                // System.Text.Json one as it has a bunch of issues with our code.
                if (serializerFactory != null) {
                    client.UseOnlySerializer(serializerFactory);
                } else {
                    client.UseSerializer<JsonNetSerializer>();
                }

                _clientCache.Add(key, client);
                return client;
            }
        }
    }
}

@alexeyzimarev
Copy link
Member Author

So, basically, you don't support properties that are reference types (functions, objects, etc), right?

My very first attempt to move to HttpClient has the options record that only had value types. It allowed easier equality, so it was possible to detect if the options record is different, so I can detect the need to create another client instance. However, HttpClient has some complex types in its properties, so it didn't work. I don't want to throw an "unsupported exception".

I will keep looking to integrate with IHttpClientFactory and named options using MS DI.

@kendallb
Copy link
Contributor

Yeah that’s the problem with HttpClient. It has too much configured on it (even the handler does too). The problem I see with IHttpClientFactory is that it’s not easy to use outside of .net core is it?

@kendallb
Copy link
Contributor

kendallb commented Mar 21, 2022

My main question with HttpClientFactory is whether there is a way to access the underlying handler as it’s configured when you create HttpClient and it’s not configured if someone passes one in. As far as I can tell once HttpClient is created you cannot access the underlying handler and configure it?

handler.Credentials = Options.Credentials;

so even with HttpClientFactory, you have to request clients by name or type, but I don’t see how you configured the underlying handler in the examples I have seen? What am I missing?

@kendallb
Copy link
Contributor

kendallb commented Mar 21, 2022

Ok yeah, so all the HttpHandler stuff is configured with the ConfigurePrimaryHttpMessageHandler extension. Which is used to create a new handler, configure it and return it. Internally I imagine it is pooling these handlers and re-using them only within the confines of a particular named or typed HttpClient, otherwise they would be configured wrong:

https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.httpclientbuilderextensions.configureprimaryhttpmessagehandler?view=dotnet-plat-ext-6.0

So I don't see any practical way to combine that with RestSharp, since it's pretty much required that you configure it all as part of setting up the DI for your application. Which pretty much means you can't really set up RestClient to be configured that way, or it also needs to be set up and configured by DI also. I personally think this was a really bad design choice on Microsoft's part to tie this so heavily to DI. Probably why this guy did this:

https://github.com/uhaciogullari/HttpClientFactoryLite

Because if you have to rely on DI to use HttpClientFactory, it's not a useful pattern for a library like RestSharp, or any library that depends on RestSharp (like say EasyPost) because the opinions on how to configure it are then enforced on the client library also. Ie: you cannot just new up a HttpClientFactory inside RestSharp, and hence you can't just new up a RestClient and have it do the right things in a transient scope.

At the end of the day though, the critical part is the pooling, reuse and limited lifetime of HttpClientHandler internally as that is where all the problems stem from, and what we need to be reusing. Perhaps the best solution if there is going to be some kind HttpClientHandler pooling inside of RestSharp, similar to what HttpClientFactory does so for folks who do just want to new up a RestClient can do so, and everything works correctly with transient scope RestClient's. And developers who are using HttpClientFactory can still use RestSharp by creating their own opinionated instances of HttpClient and passing it to RestClient to use.

@alexeyzimarev
Copy link
Member Author

That's exactly my concern about the HTTP client factory, as all of it is heavily container-dependant. It simply won't work if you don't accept the factory itself as a dependency. For example, creating a named client instance would require you to resolve the client using the factory as a dependency, providing the client name in the call.

I'm afraid that managing the pool of handlers inside RestSharp would increase the complexity significantly.

This code is what I am talking about: https://github.com/uhaciogullari/HttpClientFactoryLite/tree/hcf-lite/src/HttpClientFactory/Http/src

It's also hard (you already encountered it) to ensure the uniqueness of handlers by looking at the options object, as some of the options are properties of reference types.

@kendallb
Copy link
Contributor

Yes. I almost wonder if since v107 is such a breaking change, and it's only be a few months since it went live, that RestSharp be changed to a typed client? If you were forced to pass in a type for RestSharp to initialize it, we can then use that type internally to manage a pool of HttpClientHandlers for RestSharp similar to what is done in HttpClientFactory? I don't think we can use HttpClientFactory due to the fact everything needs to be wired up via DI, but we could implement something similar.

The biggest problem I foresee with the way v107 is currently implemented is while it does fix the issued with connection exhaustion, it does not fix the issues with DNS resolution problems. So anyone using RestSharp v107 and singleton instances, or anyone relying on a library that does the same (like EasyPost if they accept my patches) can potentially run into DNS related problems if their application needs to respect DNS changes.

But if RestSharp was changed to be a typed class, and we can use that type to manage the internal cache pool, we can both ensure folks using the library do it correctly, they can use transient instance of RestClient again without problems and also get the benefit of fixing connection exhaustion and DNS issues. Basically the same stuff HttpClientFactory solves, but in a way that can be used to benefit a library, not just application developers like HttpClientFactory does.

@alexeyzimarev
Copy link
Member Author

Typed clients aren't hard as the type name can be used as the registered options name. That's exactly how the DefaultHttpClientFactory works. What I am not sure is if it should be internal, as the only way to do it is to have a static factory. For the pure DI support, it can be a registered factory, keeping the options for typed and named clients.

@stale
Copy link

stale bot commented Apr 25, 2022

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

@stale stale bot added the wontfix label Apr 25, 2022
@repo-ranger
Copy link

repo-ranger bot commented Apr 25, 2022

⚠️ This issue has been marked wontfix and will be closed in 3 days

@restsharp restsharp deleted a comment from stale bot Jun 13, 2022
@paulmctigue
Copy link

paulmctigue commented Jun 22, 2022

Hi @alexeyzimarev

Just be clear, for "general purpose" are either these options ok . If restclient is created as transient but with a constructor injected httpclient then for moderate applications perhaps is it worth the overhead to get the benefit of DNS resolution changes. Not quite sure I'm fully understanding the magic of AddHttpClient

services.AddSingleton<IMyClient, MyClient>();

public class MyClient : IMyClient
{
	protected readonly RestClient _restClient;

	public MyClient()
	{
		_restClient = new RestClient();
	}
}

OR

services.AddHttpClient<IMyClient, MyClient>();

public class MyClient : IMyClient
{
	protected readonly RestClient _restClient;

	public MyClient(HttpClient httpClient)
	{
		_restClient = new RestClient(httpClient);
	}
}

@kendallb
Copy link
Contributor

kendallb commented Nov 30, 2022

Since IHttpClientFactory is now supported in .NET Framework (has since v5.0 apparently) I think it's a pretty viable option for v109 to use that for the HttpClients internally? That way it solves all the problems with socket exhaustion, but it also solves the issues with DNS propagation as well, since the clients are transient but the underlying handlers are not? And then RestClient could go back to being transient as well.

https://www.nuget.org/packages/Microsoft.Extensions.Http#dependencies-body-tab

@alexeyzimarev
Copy link
Member Author

Yes, it's in the plans for v109. The question is what the API should look like. For example, named configurations with the factory require you to inject the factory and get the client from it.

I can definitely create a simple extension for IHttpClientFactory that will return an instance of IRestClient. But then most of the options need to be provided in the named HttpClient configuration, and options for RestClient must be added manually when calling the factory.

However, if we want to have a named configuration option of IRestClient, it will be more work. I'd need to wrap the HttpClientFactory in something like IRestClientFactory and somehow deal with named configurations. I didn't spend enough time understanding how that can be done.

@kendallb
Copy link
Contributor

kendallb commented Dec 1, 2022

Yeah figuring out how the IRestClient works and how the factory works I think will be a key part of it. If the rest clients are no longer singletons but will be transient again, then all the options etc within that client will be transient. So unless I am missing something, couldn't RestClient internally use the HttpClientFactory to get a transient instance HttpClient so that can be completely abstracted away from the user?

@alexeyzimarev
Copy link
Member Author

Well, it's not that RestClient will be forced to become transient. As HttpClient, RestClient can be used according to the needs of a developer. It should be perfectly possible to use it as a singleton, and using a factory must be optional, not mandatory. Also, RestClient cannot force taking a dependency on IHttpClientFactory and use some hidden ways to get the client under the hood.

The way how the client is constructed now should stay. However, the options that are used to configure the message handler must be separated from RestClient's options. The options that are used to configure HttpClient should be evaluated for mutability. If HttpClient allows changing those options, they can also remain mutable for RestClient.

As IHttpClientFactory implementation used in the application is decided by the application developer. RestSharp cannot make such decisions or enforce them. Also, the client factory of .NET allows configuring the client, either the default one or using named configuration. This functionality either needs to be reproduced for RestSharp's own factory or RestSharp should use the .NET factory itself to get the RestClient instance.

@Edgaras91
Copy link

However this will be done, I want to push for urgency.

I had a big issue where 1% of calls were failing due to an SSL Cypher issue. I blamed old code (frequent WebClient instantiations), and moved to use RestSharp (I'm a big fan), so I registered as a singleton as per 107 docs. After deploying to prod, the issue persisted. Had to ditch this new code in favour of HttpClient and IHttpClientFactory registration - this resolved the issue for us, but RestSharp is out of our project.

Moving forward, for any future projects that I will be deploying onto poor "cypher-set" machines (like Windows Server 2012 R2) I will be avoiding RestSharp until we can register it via IHttpClientFactory

@kendallb
Copy link
Contributor

Interesting. Do you have any clear idea why using IHttpClientFactory helped resolve the problem? Using a singleton HttpClient is not all that different except for the fact that under the covers IHttpClientFactory implements a transient pattern for the HttpClient instances because it will reuse HttpHandlers internally but toss them after a set period of time. So my assumption would be that getting new HttpHandlers on a regular basis is what resolves the issue for you?

BTW you could still use RestSharp with IHttpClientFactory as you can instantiate transient instances of RestClient and pass in your own copy of HttpClient with one of the constructor options. If the HttpClient is transient and comes from IHttpClientFactory you can then use transient instances of RestClient and have them also manage the lifetime of the HttpClient you pass in (I think that is an option) so when you dispose of the RestClient instance it would also dispose of the transient HttpClient instance.

I am considering doing something myself, but we currently do not suffer from DNS problems and we push code updates so regularly (usually 1-2 times a day) that our application instances and hence singleton HttpClient instances get recycled regularly anyway. But I would prefer to not rely on that and use IHttpClientFactory myself.

@kendallb
Copy link
Contributor

The way how the client is constructed now should stay. However, the options that are used to configure the message handler must be separated from RestClient's options. The options that are used to configure HttpClient should be evaluated for mutability. If HttpClient allows changing those options, they can also remain mutable for RestClient.

Yes, I agree.

However if someone does use a singleton RestClient which in turn would end up with a singleton HttpClient, I wonder if the DNS issues still exist? Its not 100% clear to me with IHttpClientFactory when the pool of http handlers is accessed? My understanding is that it grabs a new handler from the pool when an HttpClient instance is requested and is released back to the pool when the instance is disposed of (or tossed if it's lifetime has expired). So if you allocated a singletone instance of RestClient that was using an instance of HttpClient from the factory, then it probably still would suffer from DNS issues? Ie: the only way to fix the DNS problem is HttpClient instances (and hence RestSharp instances) need to become transient?

As IHttpClientFactory implementation used in the application is decided by the application developer. RestSharp cannot make such decisions or enforce them. Also, the client factory of .NET allows configuring the client, either the default one or using named configuration. This functionality either needs to be reproduced for RestSharp's own factory or RestSharp should use the .NET factory itself to get the RestClient instance.

Right, its already possible to use some other mechanism to create an HttpClient instance and use that to construct a RestClient instance.

I almost think that if RestSharp wants to make things work similar to how v107 works with a singleton instance of RestSharp, that internally it needs to be grabbing HttpClient instances per request from a client factory, so that all the issues with DNS etc are resolved? So should it just create and use it's own client factory somehow, or implement it's own version of a client factory? Its not 100% necessary IMHO for it to somehow need to be getting access to the developers own registered IHttpClientFactory, but perhaps there is a way to make that an optional constructor argument so it would be used if passed in, but if not it would do it's own thing internally to solve the issues 'automagically' for the user? Then if the user has a singleton RestSharp, it works. But if they are using a transient RestSharp instance, it also still works?

Although in that case I am not sure how you manage the client factory as you would not want to create an instance of that on every new instance of RestSharp :(

@kendallb
Copy link
Contributor

What if we did something similar to this (pretty simple client factory) internally, but then allow IHttpClientFactory to be passed to the constructor if the user is using that?

https://github.com/yuzd/HttpClientFactory

@alexeyzimarev
Copy link
Member Author

So if you allocated a singletone instance of RestClient that was using an instance of HttpClient from the factory, then it probably still would suffer from DNS issues?

Yes, I think it's the correct observation about DNS issues. The only way to avoid it would be to request the HttpClient instance from the factory on each request.

Alternatively, get the RestClient from the RestClientFactory that would take IHttpClientFactory as a dependency before making the request.

What if we did something similar to this (pretty simple client factory) internally, but then allow IHttpClientFactory to be passed to the constructor if the user is using that?

That's what I thought about too. I remember you also pointed to the code of a factory that produced RestClient. It didn't support some properties of RestClientOptions where the property is of a reference type, which makes sense; it's hard to know if it's the same thing.

@alexeyzimarev
Copy link
Member Author

I added a simple static factory, it uses a concurrent dictionary for caching HttpClient instances and BaseUrl is used as a key. I added a couple of lines to the docs about it.

Proper support for ASP.NET Core DI is a different store, I will add a new project and package for it as it requires some additional dependencies.

@kendallb
Copy link
Contributor

Ok cool. I forget why at the moment but for our static factory I used more than just the base URL for the key as I also used some of the options so you would get a different client for different options with the same base URL. May not be 100% necessary but that is what I did for ours.

@alexeyzimarev
Copy link
Member Author

I remember your factory, it was using the options that are value types for equality check, and didn't allow any options that have reference types to be set as it would break equality. So, the attempt was to have the whole options object as a key.

The base URL key is not my idea, it's what Flurl does as well. I wasn't able to create anything more complex than that, and I don't think it makes sense anyway. Supporting named options and IHttpClientFactory is, in my opinion, the way to go. I am not exactly sure if it will work though. Will find out soon enough...

@unhammer
Copy link

the seralizer really should not be part of the client at all, if we are expected to use them as singletons - it should be set on the request

Having new RestRequest(url, serializer: mySerializer) would be great. I just bumped into this issue when trying to reuse the client. Is there a workaround for different serialisers per request that doesn't involve creating one RestClient per serialiser-option, or is it just not possible until this issue is fixed?

@alexeyzimarev
Copy link
Member Author

Having new RestRequest(url, serializer: mySerializer) would be great.

Please open a separate issue for it. I don't see any problem adding it.

@joeizy
Copy link

joeizy commented Jul 9, 2023

IMO would love a singleton because of the idea of the perf benefits although in practice is doesn't make that big of a deal for most apps. Unfortunately, right now the whole FW implementation is based on transient HttpClient so that makes it difficult.

One possible option (unclear to me if this was the intent of a previous comment or not) is to accept IHttpClientFactory as a parameter and internally (in RestClient) decide when to reuse the HttpClient instance and when request a new one from the factory. This would basically be a similar implementation of PooledConnectionLifetime and that's probably not the worst considering a lot of mapping is happening from RestSharp types to underlying types anyway. In the context of a Web API This internal reuse of HttpClient would actual be a perf bump to the standard because it wouldn't create a new HttpClient (or multiple) for every HTTP Request.

Alternatively (and maybe a more appropriate option) is to follow the guidance in MS Docs for Alternatives to IHttpClientFactory and leverage SocketsHttpHandler directly.

image

Helper Extension methods on service registration to accomplish the above would also be a big win IMO.

A few MS Docs links to assist (if not already aware):

@kendallb
Copy link
Contributor

Yes, I ideally RestSharp should support and instance per request for the client and handle all of the magic to use a single HttpClient internally. But I do like the potential alternative of creating new HttpClient instances per request using pools SocketsHttpHandler instances. Does that work on .NET 4.8 and .Net Core?

One of the big downsides of a single instance of HttpClient is all of the cookie handling is baked into the client, so we have to do our own cookie handling at the moment to be able to reuse HttpClient. Which works, but now it won't handle redirects and maintaining the cookies across redirects (the caller needs to do the redirect management themselves). So if that is an alternative way to have an HttpClient per request using pooled socket handlers, that would allow us to remove the cookie container stuff also and let HttpClient do it and properly handle redirects?

@alexeyzimarev
Copy link
Member Author

RestSharp already does that in a simplistic way

/// <param name="useClientFactory">Set to true if you wish to reuse the <seealso cref="HttpClient"/> instance</param>

@alexeyzimarev
Copy link
Member Author

but now it won't handle redirects and maintaining the cookies across redirects

I think RestSharp needs its own redirects handling. The default one from HttpClient is very limiting.
#2059

@kendallb
Copy link
Contributor

I like the idea of redirect handling at the RestSharp level. I don't think I saw the original ticket for that, but that seems like a good idea to me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants