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

Add support to use externally provided HttpClient/HttpClientHandler #2400

Closed
mconnew opened this issue Nov 20, 2017 · 14 comments
Closed

Add support to use externally provided HttpClient/HttpClientHandler #2400

mconnew opened this issue Nov 20, 2017 · 14 comments
Assignees
Milestone

Comments

@mconnew
Copy link
Member

mconnew commented Nov 20, 2017

API's involved

There are 2 existing API mechanisms which could be used to achieve this.

BindingParameterCollection

A developer could create a class which implements IEndpointBehavior. In the AddBindingParameters method, they would add the relevant instance to the BindingParameterCollection. This would look like this:

    public class UseMyHttpClientEndpointBehavior : IEndpointBehavior
    {
        public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
        {
            HttpClientHandler httpClientHandler = new HttpClientHandler();
            // Set any properties on httpClientHandler
            bindingParameters.Add(new HttpClient(httpClientHandler));
        }
        public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { }
        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { }
        public void Validate(ServiceEndpoint endpoint) { }
    }

This is a naïve implementation, e.g. HttpClient needs to have timeouts disabled to prevent contention on the global TimerManager, but it shows the general pattern.

MessageProperties

A developer could add an outgoing message property for the relevant instance to the OutgoingMessageProperties of an OperationContext. This could be done either with a class which implements IClientMessageInspector or with the use of OperationContextScope. The latter would look like this:

    HttpClientHandler httpClientHandler = new HttpClientHandler();
    // Set any properties on httpClientHandler
    HttpClient httpClient = new HttpClient();
    using(new OperationContextScope((IContextChannel)myClient))
    {
        OperationContext.Current.OutgoingMessageProperties.Add("System.Net.Http.HttpClient", httpClient);
        myClient.DoWork();
    }

Which class(es) to support

We have multiple options about which class(es) we could potentially support being added to a BindingParameterCollection or OutgoingMessageProperties, each having their own limitations. We would be unable to modify any parameters on any passed in objects for 2 reasons. 1) Any property we might set could potentially be something which is intended to be overridden. 2) There are use cases where the passed in object might have already been used so is now immutable.

System.Net.Http.HttpClient

WCF sets HttpClient.Timeout to infinite to prevent a timer being registered for each request. We have logic to coalesce CancellationToken timers to prevent high contention on the global timer queue. Without setting this value, HttpClient will cause a lot of contention.

System.Net.Http.HttpMessageInvoker

This is the base class to HttpClient. It lacks a lot of the helper api's that HttpClient provides such as the verb specific request api's, e.g. GetAsync and PostAsync. WCF only needs to use SendAsync so this shouldn't be an issue. Although we don't currently do this, WCF could make use of HttpClient.DefaultRequestHeaders for the headers which will always be the same for all requests from the same HttpChannelFactory. If we decide to support the use of a single instance of HttpClient with multiple ChannelFactory instances, then we can't use DefaultRequestHeaders.

System.Net.Http.HttpClientHandler

There are many properties on this class which WCF sets. One of the more important ones is Credentials. A developer would need to set the credentials on HttpClientHandler themselves. We would still be able to set any properties on the HttpClient instance we would create to wrap the HttpClientHandler. Supporting allowing this class to be provided would limit developers to using client implementations which ship with the framework.

System.Net.Http.HttpMessageHandler

This is the base class for HttpClientHandler and is the type that the constructor for HttpClient accepts. This has none of the properties that HttpClientHandler exposes, but as we can't modify any properties as explained earlier, this does not present any additional problems. This would enable developers to provide any implementation of Http which works with HttpClient.

System.Net.Http.DelegatingHandler

This is a special class which allows to provide an existing HttpMessageHandler to be wrapped and potentially provide custom behavior. There is a property InnerHandler which would allow WCF to set the HttpClientHandler that we create on the DelegatingHandler. Presuming the constraint of only being able to give WCF a delegating handler which has not been used yet, we would be able to provide an unused HttpClientHandler to the delegating handler which would be able to modify any properties before first usage. This would allow WCF to set credentials etc on an HttpClientHandler while also allowing a developer to modify everything as much as they wish.

Decisions to be made

  • Which of the extensibility methods do we want to support?
  • Which class types do we want to support being provided?
  • What constraints are acceptable? E.g. can instances be shared with multiple ChannelFactory's?
@Code-Grump
Copy link

I don't think setting a single handler is compatible with the current implementations which assume ownership of the handler. It might be easier if it were possible to supply instead a factory to create and customise HttpMessageHandler instances as required by the channels. Something as simple as a Func<HttpMessageHandler>, unless we want opportunities to customise the handler based on some contextual information.

@zhenlan zhenlan added the feature label Dec 1, 2017
@zhenlan zhenlan added this to the 2.1.0 milestone Dec 1, 2017
@mconnew
Copy link
Member Author

mconnew commented Dec 15, 2017

@Tragedian, the difficulty with a factory is it should return an instance of HttpMessageHandler which then means we can't set anything like credentials or certificate validation callbacks. What do you think about instead having a mechanism which specifies a Type which must derive from DelegatingHandler? We could look for either a default constructor (in which case we construct an instance and then call the setter on InnerHandler with our HttpClientHandler) or a constructor which takes an HttpMessageHandler so we can pass our configured HttpClientHandler to the constructor. The implementation would be free to ignore the passed in HttpClientHandler if it wanted to use a completely different implementation or could modify it or simply modify the request in the SendAsync call path.

@mconnew
Copy link
Member Author

mconnew commented Dec 15, 2017

Thinking about it a bit more, it could be a bit simpler than that. We could use a Func<HttpClientHandler, HttpMessageHandler> so we pass our HttpClientHandler with everything configured how we want to the Func and that returns an HttpMessageHandler which we then use to construct an HttpClient. The factory Func can ignore the HttpClientHandler, modify it, or wrap it in a DelegatingHandler and use it as is and just modify the requests.

@Code-Grump
Copy link

Code-Grump commented Jan 13, 2018

The problem with this (and any approach I could imagine) is where the HTTP abstractions are built to work on HttpMessageHandler, but all the properties we'd want to examine or modify are on the HttpClientHandler. Either you have to up-cast, or use HttpClientHandler in all the contracts, significantly limiting the ability to wrap the handler. Whether that scenario is important is unclear, but it feels like shutting the door on using existing delegating handlers is a decision not to be taken lightly.

@Code-Grump
Copy link

Assuming HttpMessageHandler is the right level of abstraction, I would be interested in exploring adding something to the binding parameters which implements a Configure(ref HttpMessageHandler) contract. The channel could call this method (or indeed a collection of them) to examine, configure or even replace the handler.

@mconnew
Copy link
Member Author

mconnew commented Jan 17, 2018

@Tragedian, WCF doesn't touch anything on HttpClientHandler after we've set all the properties, we just pass it to the constructor of HttpClient and only interact with that. Having a delegate which takes an HttpClientHandler would give the delegate access to all the properties WCF sets. There are then 3 options:

  1. Return the same instance of HttpClientHandler with some properties modified
  2. Wrap the HttpClientHandler with a DelegatingHandler which allows getting on the SendAsync call path and allows lots of interesting things.
  3. Return a completely different implementation of an HttpMessageHandler

Is there anything about having a delegate which looks like HttpMessageHandler Configure(HttpClientHandler) which wouldn't suit what you need?

@Code-Grump
Copy link

My only questions:

  • Where would Configure live? A client behaviour, or something else entirely?
  • Would there have to be specific protection to prevent having multiple Configure implementations applied to client?

@mconnew
Copy link
Member Author

mconnew commented Jan 18, 2018

It would still be a Func which you would provide by adding it as a binding parameter. To add this, you would need to create a behavior similar to the first example code snippet at the top.
I don't believe you can add more than one binding parameter of the same type as it's backed by a KeyedByTypeCollection<Type> which means you can only have one item in the collection with the same key, with the key being the Type of the object, in this case the relevant Func type.

@Code-Grump
Copy link

I don't believe you can add more than one binding parameter of the same type as it's backed by a KeyedByTypeCollection which means you can only have one item in the collection with the same key, with the key being the Type of the object, in this case the relevant Func type.

Under this, if there were two behaviours which attempted to modify the handler, the expected result will be to get an exception thrown when trying to add the second configure method to the binding parameters. That doesn't seem like an easy situation to diagnose if a user has just added two behaviours which conflict. Can we do better?

Knowing to add a Func<HttpClientHandler, HttpMessageHandler> to the binding parameters doesn't seem very easy knowledge to discover. Not that I don't like Func<> for its composability, but perhaps there's something a little more friendly?

@mconnew
Copy link
Member Author

mconnew commented Jan 19, 2018

We have the constraint that we can't add any explicit api's as any new api's also need to be added to the desktop full framework. As the full framework doesn't use HttpClient, that's a problem. Do you have any other suggestions on how to do this?
By the way, that problem you described is the same for any existing behavior that wants to add a binding parameter. Whatever the final solution will be, I would create a scenario test using it. We decided to have the policy that the scenario tests should be examples of best practices of how to use WCF with the idea that developers can copy/paste things like test behaviors for a particular feature and to have it do the correct thing. With this in mind, I could definitely write the scenario test such that the behavior probes for an existing one first and then ensures it chains them together. Would that be sufficient to have a best effort to make sure this is unlikely to happen?

@Code-Grump
Copy link

The constraint on not being able to add any explicit new APIs certainly makes anything other than some kind of Func<> impossible. Even something as lightweight as appropriately-named delegate would violate this constraint. It gives this whole exercise a bit of a hacky feel, trying to add new behaviour to an API without actually changing the API in a visible way.

The scenario-test-as-an-example sounds like a decent way to document the new behaviour with some "best practice". It wouldn't even have to do something as complex as chaining (I actually can't picture how that would work with the asymmetric Func<>) ; just an exception with a clear message would be a good thing to encourage.

@mconnew
Copy link
Member Author

mconnew commented Jan 20, 2018

We do normally have the option of adding an API, the problem we have though is with how different the inner workings are between .Net Core and the full framework for HTTP. If there were a parallel for NetTcp, we could probably add an API. But semantically binding parameters are the right way to do this anyway. It's how you provide parameters which a binding element would pick up to modify the behavior of the binding element.
There is precedent in corefx for adding .Net Core only api's but I don't like that idea as it makes code less portable. Without this constraint, how would you suggest doing this?

@mconnew
Copy link
Member Author

mconnew commented Feb 22, 2018

This change has now been merged in PR #2534. An example behavior using this feature is available here.

@fsielimowicz
Copy link

Nice guys, what about passing cancellation token to wcf call after HttpClient is bound ?

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

No branches or pull requests

4 participants