We faced this same issue, and went for the app-domain approach you mentioned, implementing a solution based on what is proposed here, which is a really nice write up of how to manage the execution of code in a separate app domain:
http://www.superstarcoders.com/blogs/posts/executing-code-in-a-separate-application-domain-using-c-sharp.aspx
We are using his Isolated class pretty much as is:
public sealed class Isolated<T> : IDisposable where T : MarshalByRefObject
{
private AppDomain _domain;
private readonly T _value;
public Isolated()
{
_domain = AppDomain.CreateDomain("Isolated:" + Guid.NewGuid(), null, AppDomain.CurrentDomain.SetupInformation);
var type = typeof(T);
_value = (T)_domain.CreateInstanceAndUnwrap(type.Assembly.FullName, type.FullName);
}
public T Value
{
get
{
return _value;
}
}
public void Dispose()
{
if (_domain == null) return;
AppDomain.Unload(_domain);
_domain = null;
}
}
And then we have a wrapper around the standard WebClient, that allows for the setting of the protocol:
public class WebClient : MarshalByRefObject, IWebClient
{
public WebClientResponse GetResponse(string address)
{
return GetResponse(address, null);
}
public WebClientResponse GetResponse(string address, string securityProtocol)
{
if (!string.IsNullOrWhiteSpace(securityProtocol))
ServicePointManager.SecurityProtocol = (SecurityProtocolType)Enum.Parse(typeof(SecurityProtocolType), securityProtocol);
var response = new WebClientResponse();
try
{
using (var wc = new System.Net.WebClient())
{
// <do stuff>
}
}
catch (Exception ex)
{
response.Exception = new GetResponseException(string.Format("Unable to get response from {0}", address), ex);
}
return response;
}
}
[Serializable]
public class WebClientResponse
{
public Exception Exception { get; set; }
public string Response { get; set; }
}
[Serializable]
public class GetResponseException : Exception
{
public GetResponseException(string message, Exception innerException)
: base(message, innerException)
{
}
public GetResponseException(SerializationInfo info, StreamingContext context) : base(info, context)
{
}
}
Tying them together, we have code that determines if it needs to override the protocol that's currently set. If so, it spins up the isolated app domain, if not it uses an existing WebClient:
...
WebClientResponse webClientResponse;
if (!string.IsNullOrWhiteSpace(ForceSecurityProtocol))
{
using (var isolated = new Isolated<WebClient>())
{
webClientResponse = isolated.Value.GetResponse(url, ForceSecurityProtocol);
}
}
else
{
webClientResponse = _webClient.GetResponse(url);
}
...
Note, our use isn't in a tremendously high throughput area of our application, so whatever performance price we're paying using this approach is really non-impactful. If we were going to put something like this in a place where it been in the way of a significant amount of traffic through our web app, we would have done some testing.