diff --git a/src/libraries/Microsoft.Extensions.Http/tests/DependencyInjection/HttpClientFactoryServiceCollectionExtensionsTest.cs b/src/libraries/Microsoft.Extensions.Http/tests/DependencyInjection/HttpClientFactoryServiceCollectionExtensionsTest.cs index 6e526dbdad572b..fec6276e6ce26d 100644 --- a/src/libraries/Microsoft.Extensions.Http/tests/DependencyInjection/HttpClientFactoryServiceCollectionExtensionsTest.cs +++ b/src/libraries/Microsoft.Extensions.Http/tests/DependencyInjection/HttpClientFactoryServiceCollectionExtensionsTest.cs @@ -948,6 +948,7 @@ public async Task AddHttpClient_MessageHandler_SingletonDependency() var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/"); var response = await client.HttpClient.SendAsync(request); +#if NETFRAMEWORK Assert.Same( services.GetRequiredService(), request.Properties[nameof(SingletonService)]); @@ -955,6 +956,18 @@ public async Task AddHttpClient_MessageHandler_SingletonDependency() Assert.Same( client.Service, request.Properties[nameof(SingletonService)]); +#else +#nullable enable + request.Options.TryGetValue(new HttpRequestOptionsKey(nameof(SingletonService)), out SingletonService? optService); +#nullable disable + Assert.Same( + services.GetRequiredService(), + optService); + + Assert.Same( + client.Service, + optService); +#endif } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] @@ -979,6 +992,7 @@ public async Task AddHttpClient_MessageHandler_Scope_SingletonDependency() var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/"); var response = await client.HttpClient.SendAsync(request); +#if NETFRAMEWORK Assert.Same( services.GetRequiredService(), request.Properties[nameof(SingletonService)]); @@ -990,6 +1004,23 @@ public async Task AddHttpClient_MessageHandler_Scope_SingletonDependency() Assert.Same( client.Service, request.Properties[nameof(SingletonService)]); +#else +#nullable enable + request.Options.TryGetValue(new HttpRequestOptionsKey(nameof(SingletonService)), out SingletonService? optService); +#nullable disable + + Assert.Same( + services.GetRequiredService(), + optService); + + Assert.Same( + scope.ServiceProvider.GetRequiredService(), + optService); + + Assert.Same( + client.Service, + optService); +#endif } } @@ -1035,6 +1066,7 @@ public async Task AddHttpClient_MessageHandler_Scope_ScopedDependency() var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/"); var response = await client.HttpClient.SendAsync(request); +#if NETFRAMEWORK Assert.NotSame( scope.ServiceProvider.GetRequiredService(), request.Properties[nameof(ScopedService)]); @@ -1046,6 +1078,22 @@ public async Task AddHttpClient_MessageHandler_Scope_ScopedDependency() Assert.NotSame( client.Service, request.Properties[nameof(ScopedService)]); +#else +#nullable enable + request.Options.TryGetValue(new HttpRequestOptionsKey(nameof(ScopedService)), out ScopedService? optService); +#nullable disable + Assert.NotSame( + scope.ServiceProvider.GetRequiredService(), + optService); + + Assert.Same( + scope.ServiceProvider.GetRequiredService(), + client.Service); + + Assert.NotSame( + client.Service, + optService); +#endif } } @@ -1069,6 +1117,7 @@ public async Task AddHttpClient_MessageHandler_TransientDependency() var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/"); var response = await client.HttpClient.SendAsync(request); +#if NETFRAMEWORK Assert.NotSame( services.GetRequiredService(), request.Properties[nameof(TransientService)]); @@ -1076,6 +1125,18 @@ public async Task AddHttpClient_MessageHandler_TransientDependency() Assert.NotSame( client.Service, request.Properties[nameof(TransientService)]); +#else +#nullable enable + request.Options.TryGetValue(new HttpRequestOptionsKey(nameof(TransientService)), out TransientService? optService); +#nullable disable + Assert.NotSame( + services.GetRequiredService(), + optService); + + Assert.NotSame( + client.Service, + optService); +#endif } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] @@ -1100,6 +1161,7 @@ public async Task AddHttpClient_MessageHandler_Scope_TransientDependency() var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com/"); var response = await client.HttpClient.SendAsync(request); +#if NETFRAMEWORK Assert.NotSame( services.GetRequiredService(), request.Properties[nameof(TransientService)]); @@ -1107,6 +1169,18 @@ public async Task AddHttpClient_MessageHandler_Scope_TransientDependency() Assert.NotSame( client.Service, request.Properties[nameof(TransientService)]); +#else +#nullable enable + request.Options.TryGetValue(new HttpRequestOptionsKey(nameof(TransientService)), out TransientService? optService); +#nullable disable + Assert.NotSame( + services.GetRequiredService(), + optService); + + Assert.NotSame( + client.Service, + optService); +#endif } } @@ -1304,7 +1378,11 @@ public HandlerWithSingletonService(SingletonService service) protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { +#if NETFRAMEWORK request.Properties[nameof(SingletonService)] = Service; +#else + request.Options.Set(new HttpRequestOptionsKey(nameof(SingletonService)), Service); +#endif return Task.FromResult(new HttpResponseMessage()); } } @@ -1320,7 +1398,11 @@ public HandlerWithScopedService(ScopedService service) protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { +#if NETFRAMEWORK request.Properties[nameof(ScopedService)] = Service; +#else + request.Options.Set(new HttpRequestOptionsKey(nameof(ScopedService)), Service); +#endif return Task.FromResult(new HttpResponseMessage()); } } @@ -1336,7 +1418,11 @@ public HandlerWithTransientService(TransientService service) protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { +#if NETFRAMEWORK request.Properties[nameof(TransientService)] = Service; +#else + request.Options.Set(new HttpRequestOptionsKey(nameof(TransientService)), Service); +#endif return Task.FromResult(new HttpResponseMessage()); } } diff --git a/src/libraries/System.Net.Http/ref/System.Net.Http.cs b/src/libraries/System.Net.Http/ref/System.Net.Http.cs index dc1e77f19d7fb2..ef93293b5c2674 100644 --- a/src/libraries/System.Net.Http/ref/System.Net.Http.cs +++ b/src/libraries/System.Net.Http/ref/System.Net.Http.cs @@ -4,6 +4,8 @@ // Changes to this file must follow the https://aka.ms/api-review process. // ------------------------------------------------------------------------------ +using System.Diagnostics.CodeAnalysis; + namespace System.Net.Http { public partial class ByteArrayContent : System.Net.Http.HttpContent @@ -212,13 +214,44 @@ public HttpRequestMessage(System.Net.Http.HttpMethod method, System.Uri? request public System.Net.Http.HttpContent? Content { get { throw null; } set { } } public System.Net.Http.Headers.HttpRequestHeaders Headers { get { throw null; } } public System.Net.Http.HttpMethod Method { get { throw null; } set { } } + [Obsolete("Use Options instead.")] public System.Collections.Generic.IDictionary Properties { get { throw null; } } + public HttpRequestOptions Options { get { throw null; } } public System.Uri? RequestUri { get { throw null; } set { } } public System.Version Version { get { throw null; } set { } } public void Dispose() { } protected virtual void Dispose(bool disposing) { } public override string ToString() { throw null; } } + + public readonly struct HttpRequestOptionsKey + { + public HttpRequestOptionsKey(string key) {} + public string Key { get { throw null; } } + } + + public sealed class HttpRequestOptions : System.Collections.Generic.IDictionary + { + void System.Collections.Generic.IDictionary.Add(string key, object? value) { throw null; } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Keys { get { throw null; } } + System.Collections.Generic.ICollection System.Collections.Generic.IDictionary.Values { get { throw null; } } + bool System.Collections.Generic.IDictionary.Remove(string key) { throw null; } + bool System.Collections.Generic.ICollection>.Remove(System.Collections.Generic.KeyValuePair item) { throw null; } + bool System.Collections.Generic.IDictionary.TryGetValue(string key, out object? value) { throw null; } + object? System.Collections.Generic.IDictionary.this[string key] { get { throw null; } set { } } + void System.Collections.Generic.ICollection>.Add(System.Collections.Generic.KeyValuePair item) { throw null; } + void System.Collections.Generic.ICollection>.Clear() { throw null; } + bool System.Collections.Generic.ICollection>.Contains(System.Collections.Generic.KeyValuePair item) { throw null; } + bool System.Collections.Generic.IDictionary.ContainsKey(string key) { throw null; } + void System.Collections.Generic.ICollection>.CopyTo(System.Collections.Generic.KeyValuePair[] array, int arrayIndex) { throw null; } + int System.Collections.Generic.ICollection>.Count { get { throw null; } } + bool System.Collections.Generic.ICollection>.IsReadOnly { get { throw null; } } + System.Collections.Generic.IEnumerator> System.Collections.Generic.IEnumerable>.GetEnumerator() { throw null; } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { throw null; } + public bool TryGetValue(HttpRequestOptionsKey key, [MaybeNullWhen(false)] out TValue value) { throw null; } + public void Set(HttpRequestOptionsKey key, TValue value) { throw null; } + } + public partial class HttpResponseMessage : System.IDisposable { public HttpResponseMessage() { } diff --git a/src/libraries/System.Net.Http/src/System.Net.Http.csproj b/src/libraries/System.Net.Http/src/System.Net.Http.csproj index 65d63c045bbf58..3c29a2c0a3f125 100644 --- a/src/libraries/System.Net.Http/src/System.Net.Http.csproj +++ b/src/libraries/System.Net.Http/src/System.Net.Http.csproj @@ -1,4 +1,4 @@ - + Library System.Net.Http @@ -47,6 +47,8 @@ + + diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs index eb577324faa16f..4323b42ee02274 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/BrowserHttpHandler/BrowserHttpHandler.cs @@ -32,6 +32,9 @@ internal sealed class BrowserHttpHandler : HttpMessageHandler private static readonly JSObject? s_fetch = (JSObject)System.Runtime.InteropServices.JavaScript.Runtime.GetGlobalObject("fetch"); private static readonly JSObject? s_window = (JSObject)System.Runtime.InteropServices.JavaScript.Runtime.GetGlobalObject("window"); + private static readonly HttpRequestOptionsKey EnableStreamingResponse = new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"); + private static readonly HttpRequestOptionsKey> FetchOptions = new HttpRequestOptionsKey>("WebAssemblyFetchOptions"); + /// /// Gets whether the current Browser supports streaming responses /// @@ -129,8 +132,7 @@ protected internal override async Task SendAsync(HttpReques { var requestObject = new JSObject(); - if (request.Properties.TryGetValue("WebAssemblyFetchOptions", out object? fetchOptionsValue) && - fetchOptionsValue is IDictionary fetchOptions) + if (request.Options.TryGetValue(FetchOptions, out IDictionary? fetchOptions)) { foreach (KeyValuePair item in fetchOptions) { @@ -221,9 +223,13 @@ protected internal override async Task SendAsync(HttpReques HttpResponseMessage httpResponse = new HttpResponseMessage((HttpStatusCode)status.Status); - bool streamingEnabled = request.Properties.TryGetValue("WebAssemblyEnableStreamingResponse", out object? streamingEnabledValue) && (bool)(streamingEnabledValue ?? false); + bool streamingEnabled = false; + if (StreamingSupported) + { + request.Options.TryGetValue(EnableStreamingResponse, out streamingEnabled); + } - httpResponse.Content = StreamingSupported && streamingEnabled + httpResponse.Content = streamingEnabled ? new StreamContent(wasmHttpReadStream = new WasmHttpReadStream(status)) : (HttpContent)new BrowserHttpContent(status); diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestMessage.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestMessage.cs index 3cbb1d058c3987..83ee09fd178103 100644 --- a/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestMessage.cs +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestMessage.cs @@ -24,7 +24,7 @@ public class HttpRequestMessage : IDisposable private Version _version; private HttpContent? _content; private bool _disposed; - private IDictionary? _properties; + private HttpRequestOptions? _options; public Version Version { @@ -111,17 +111,10 @@ public HttpRequestHeaders Headers internal bool HasHeaders => _headers != null; - public IDictionary Properties - { - get - { - if (_properties == null) - { - _properties = new Dictionary(); - } - return _properties; - } - } + [Obsolete("Use Options instead.")] + public IDictionary Properties => Options; + + public HttpRequestOptions Options => _options ??= new HttpRequestOptions(); public HttpRequestMessage() : this(HttpMethod.Get, (Uri?)null) diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestOptions.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestOptions.cs new file mode 100644 index 00000000000000..195eb0d10fe621 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestOptions.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace System.Net.Http +{ + public sealed class HttpRequestOptions : IDictionary + { + private Dictionary Options { get; } = new Dictionary(); + object? IDictionary.this[string key] + { + get + { + return Options[key]; + } + set + { + Options[key] = value; + } + } + ICollection IDictionary.Keys => Options.Keys; + ICollection IDictionary.Values => Options.Values; + int ICollection>.Count => Options.Count; + bool ICollection>.IsReadOnly => ((IDictionary)Options).IsReadOnly; + void IDictionary.Add(string key, object? value) => Options.Add(key, value); + void ICollection>.Add(KeyValuePair item) => ((IDictionary)Options).Add(item); + void ICollection>.Clear() => Options.Clear(); + bool ICollection>.Contains(KeyValuePair item) => ((IDictionary)Options).Contains(item); + bool IDictionary.ContainsKey(string key) => Options.ContainsKey(key); + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) => + ((IDictionary)Options).CopyTo(array, arrayIndex); + IEnumerator> IEnumerable>.GetEnumerator() => Options.GetEnumerator(); + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => ((System.Collections.IEnumerable)Options).GetEnumerator(); + bool IDictionary.Remove(string key) => Options.Remove(key); + bool ICollection>.Remove(KeyValuePair item) => ((IDictionary)Options).Remove(item); + bool IDictionary.TryGetValue(string key, out object? value) => Options.TryGetValue(key, out value); + public bool TryGetValue(HttpRequestOptionsKey key, [MaybeNullWhen(false)] out TValue value) + { + if (Options.TryGetValue(key.Key, out object? _value) && _value is TValue tvalue) + { + value = tvalue; + return true; + } + + value = default(TValue); + return false; + } + + public void Set(HttpRequestOptionsKey key, TValue value) + { + Options[key.Key] = value; + } + } +} \ No newline at end of file diff --git a/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestOptionsKey.cs b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestOptionsKey.cs new file mode 100644 index 00000000000000..f51f8ebf162fe7 --- /dev/null +++ b/src/libraries/System.Net.Http/src/System/Net/Http/HttpRequestOptionsKey.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +namespace System.Net.Http +{ + public readonly struct HttpRequestOptionsKey + { + public string Key { get; } + public HttpRequestOptionsKey(string key) + { + Key = key; + } + } +} diff --git a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpRequestMessageTest.cs b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpRequestMessageTest.cs index 5e8928b7db9f25..8f5ff80d998d27 100644 --- a/src/libraries/System.Net.Http/tests/FunctionalTests/HttpRequestMessageTest.cs +++ b/src/libraries/System.Net.Http/tests/FunctionalTests/HttpRequestMessageTest.cs @@ -154,7 +154,7 @@ public void Properties_SetPropertiesAndGetTheirValue_MatchingValues() Assert.Equal(version, rm.Version); Assert.NotNull(rm.Headers); - Assert.NotNull(rm.Properties); + Assert.NotNull(rm.Options); } [Fact] diff --git a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj index 9ca98181f81fbb..01c061ae0a2464 100644 --- a/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj +++ b/src/libraries/System.Net.Http/tests/UnitTests/System.Net.Http.Unit.Tests.csproj @@ -206,6 +206,10 @@ Link="ProductionCode\System\Net\Http\RequestRetryType.cs" /> + + --> + \ No newline at end of file diff --git a/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/Http/HttpRequestMessageTest.cs b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/Http/HttpRequestMessageTest.cs new file mode 100644 index 00000000000000..80a8cd126f1562 --- /dev/null +++ b/src/libraries/System.Runtime.InteropServices.JavaScript/tests/System/Runtime/InteropServices/JavaScript/Http/HttpRequestMessageTest.cs @@ -0,0 +1,376 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.IO; +using System.Net.Http.Headers; +using System.Net.Http; +using System.Net; +using System.Threading.Tasks; + +using Xunit; +using Xunit.Abstractions; + +namespace System.Runtime.InteropServices.JavaScript.Http.Tests +{ + public class HttpRequestMessageTest + { + private readonly Version _expectedRequestMessageVersion = HttpVersion.Version11; + private HttpRequestOptionsKey EnableStreamingResponse = new HttpRequestOptionsKey("WebAssemblyEnableStreamingResponse"); +#nullable enable + private HttpRequestOptionsKey> FetchOptions = new HttpRequestOptionsKey>("WebAssemblyFetchOptions"); +#nullable disable + + [Fact] + public void Ctor_Default_CorrectDefaults() + { + var rm = new HttpRequestMessage(); + + Assert.Equal(HttpMethod.Get, rm.Method); + Assert.Null(rm.Content); + Assert.Null(rm.RequestUri); + } + + [Fact] + public void Ctor_RelativeStringUri_CorrectValues() + { + var rm = new HttpRequestMessage(HttpMethod.Post, "/relative"); + + Assert.Equal(HttpMethod.Post, rm.Method); + Assert.Equal(_expectedRequestMessageVersion, rm.Version); + Assert.Null(rm.Content); + Assert.Equal(new Uri("/relative", UriKind.Relative), rm.RequestUri); + } + + [Fact] + public void Ctor_AbsoluteStringUri_CorrectValues() + { + var rm = new HttpRequestMessage(HttpMethod.Post, "http://host/absolute/"); + + Assert.Equal(HttpMethod.Post, rm.Method); + Assert.Equal(_expectedRequestMessageVersion, rm.Version); + Assert.Null(rm.Content); + Assert.Equal(new Uri("http://host/absolute/"), rm.RequestUri); + } + + [Fact] + public void Ctor_NullStringUri_Accepted() + { + var rm = new HttpRequestMessage(HttpMethod.Put, (string)null); + + Assert.Null(rm.RequestUri); + Assert.Equal(HttpMethod.Put, rm.Method); + Assert.Equal(_expectedRequestMessageVersion, rm.Version); + Assert.Null(rm.Content); + } + + [Fact] + public void Ctor_RelativeUri_CorrectValues() + { + var uri = new Uri("/relative", UriKind.Relative); + var rm = new HttpRequestMessage(HttpMethod.Post, uri); + + Assert.Equal(HttpMethod.Post, rm.Method); + Assert.Equal(_expectedRequestMessageVersion, rm.Version); + Assert.Null(rm.Content); + Assert.Equal(uri, rm.RequestUri); + } + + [Fact] + public void Ctor_AbsoluteUri_CorrectValues() + { + var uri = new Uri("http://host/absolute/"); + var rm = new HttpRequestMessage(HttpMethod.Post, uri); + + Assert.Equal(HttpMethod.Post, rm.Method); + Assert.Equal(_expectedRequestMessageVersion, rm.Version); + Assert.Null(rm.Content); + Assert.Equal(uri, rm.RequestUri); + } + + [Fact] + public void Ctor_NullUri_Accepted() + { + var rm = new HttpRequestMessage(HttpMethod.Put, (Uri)null); + + Assert.Null(rm.RequestUri); + Assert.Equal(HttpMethod.Put, rm.Method); + Assert.Equal(_expectedRequestMessageVersion, rm.Version); + Assert.Null(rm.Content); + } + + [Fact] + public void Ctor_NullMethod_ThrowsArgumentNullException() + { + Assert.Throws(() => new HttpRequestMessage(null, "http://example.com")); + } + + [Fact] + public void Ctor_NonHttpUri_ThrowsArgumentException() + { + AssertExtensions.Throws("requestUri", () => new HttpRequestMessage(HttpMethod.Put, "ftp://example.com")); + } + + [Fact] + public void Dispose_DisposeObject_ContentGetsDisposedAndSettersWillThrowButGettersStillWork() + { + var rm = new HttpRequestMessage(HttpMethod.Get, "http://example.com"); + var content = new MockContent(); + rm.Content = content; + Assert.False(content.IsDisposed); + + rm.Dispose(); + rm.Dispose(); // Multiple calls don't throw. + + Assert.True(content.IsDisposed); + Assert.Throws(() => { rm.Method = HttpMethod.Put; }); + Assert.Throws(() => { rm.RequestUri = null; }); + Assert.Throws(() => { rm.Version = new Version(1, 0); }); + Assert.Throws(() => { rm.Content = null; }); + + // Property getters should still work after disposing. + Assert.Equal(HttpMethod.Get, rm.Method); + Assert.Equal(new Uri("http://example.com"), rm.RequestUri); + Assert.Equal(_expectedRequestMessageVersion, rm.Version); + Assert.Equal(content, rm.Content); + } + + [Fact] + public void Properties_SetOptionsAndGetTheirValue_MatchingValues() + { + var rm = new HttpRequestMessage(); + + var content = new MockContent(); + var uri = new Uri("https://example.com"); + var version = new Version(1, 0); + var method = new HttpMethod("custom"); + + rm.Content = content; + rm.Method = method; + rm.RequestUri = uri; + rm.Version = version; + + Assert.Equal(content, rm.Content); + Assert.Equal(uri, rm.RequestUri); + Assert.Equal(method, rm.Method); + Assert.Equal(version, rm.Version); + + Assert.NotNull(rm.Headers); + Assert.NotNull(rm.Options); + } + +#nullable enable + [Fact] + public void Properties_SetOptionsAndGetTheirValue_Set_FetchOptions() + { + var rm = new HttpRequestMessage(); + + var content = new MockContent(); + var uri = new Uri("https://example.com"); + var version = new Version(1, 0); + var method = new HttpMethod("custom"); + + rm.Content = content; + rm.Method = method; + rm.RequestUri = uri; + rm.Version = version; + + var fetchme = new Dictionary(); + fetchme.Add("hic", null); + fetchme.Add("sunt", 4444); + fetchme.Add("dracones", new List()); + rm.Options.Set(FetchOptions, fetchme); + + Assert.Equal(content, rm.Content); + Assert.Equal(uri, rm.RequestUri); + Assert.Equal(method, rm.Method); + Assert.Equal(version, rm.Version); + + Assert.NotNull(rm.Headers); + Assert.NotNull(rm.Options); + + rm.Options.TryGetValue(FetchOptions, out IDictionary? fetchOptionsValue); + Assert.NotNull(fetchOptionsValue); + if (fetchOptionsValue != null) + { + foreach (var item in fetchOptionsValue) + { + Assert.True(fetchme.ContainsKey(item.Key)); + } + } + } +#nullable disable + +#nullable enable + [Fact] + public void Properties_SetOptionsAndGetTheirValue_NotSet_FetchOptions() + { + var rm = new HttpRequestMessage(); + + var content = new MockContent(); + var uri = new Uri("https://example.com"); + var version = new Version(1, 0); + var method = new HttpMethod("custom"); + + rm.Content = content; + rm.Method = method; + rm.RequestUri = uri; + rm.Version = version; + + Assert.Equal(content, rm.Content); + Assert.Equal(uri, rm.RequestUri); + Assert.Equal(method, rm.Method); + Assert.Equal(version, rm.Version); + + Assert.NotNull(rm.Headers); + Assert.NotNull(rm.Options); + + rm.Options.TryGetValue(FetchOptions, out IDictionary? fetchOptionsValue); + Assert.Null(fetchOptionsValue); + } +#nullable disable + + [Fact] + public void Properties_SetOptionsAndGetTheirValue_Set_EnableStreamingResponse() + { + var rm = new HttpRequestMessage(); + + var content = new MockContent(); + var uri = new Uri("https://example.com"); + var version = new Version(1, 0); + var method = new HttpMethod("custom"); + + rm.Content = content; + rm.Method = method; + rm.RequestUri = uri; + rm.Version = version; + + rm.Options.Set(EnableStreamingResponse, true); + + Assert.Equal(content, rm.Content); + Assert.Equal(uri, rm.RequestUri); + Assert.Equal(method, rm.Method); + Assert.Equal(version, rm.Version); + + Assert.NotNull(rm.Headers); + Assert.NotNull(rm.Options); + + rm.Options.TryGetValue(EnableStreamingResponse, out bool streamingEnabledValue); + Assert.True(streamingEnabledValue); + } + + [Fact] + public void Properties_SetOptionsAndGetTheirValue_NotSet_EnableStreamingResponse() + { + var rm = new HttpRequestMessage(); + + var content = new MockContent(); + var uri = new Uri("https://example.com"); + var version = new Version(1, 0); + var method = new HttpMethod("custom"); + + rm.Content = content; + rm.Method = method; + rm.RequestUri = uri; + rm.Version = version; + + Assert.Equal(content, rm.Content); + Assert.Equal(uri, rm.RequestUri); + Assert.Equal(method, rm.Method); + Assert.Equal(version, rm.Version); + + Assert.NotNull(rm.Headers); + Assert.NotNull(rm.Options); + + rm.Options.TryGetValue(EnableStreamingResponse, out bool streamingEnabledValue); + Assert.False(streamingEnabledValue); + } + + [Fact] + public void RequestUri_SetNonHttpUri_ThrowsArgumentException() + { + var rm = new HttpRequestMessage(); + AssertExtensions.Throws("value", () => { rm.RequestUri = new Uri("ftp://example.com"); }); + } + + [Fact] + public void Version_SetToNull_ThrowsArgumentNullException() + { + var rm = new HttpRequestMessage(); + Assert.Throws(() => { rm.Version = null; }); + } + + [Fact] + public void Method_SetToNull_ThrowsArgumentNullException() + { + var rm = new HttpRequestMessage(); + Assert.Throws(() => { rm.Method = null; }); + } + + [Fact] + public void ToString_DefaultAndNonDefaultInstance_DumpAllFields() + { + var rm = new HttpRequestMessage(); + string expected = + "Method: GET, RequestUri: '', Version: " + + _expectedRequestMessageVersion.ToString(2) + + $", Content: , Headers:{Environment.NewLine}{{{Environment.NewLine}}}"; + Assert.Equal(expected, rm.ToString()); + + rm.Method = HttpMethod.Put; + rm.RequestUri = new Uri("http://a.com/"); + rm.Version = new Version(1, 0); + rm.Content = new StringContent("content"); + + // Note that there is no Content-Length header: The reason is that the value for Content-Length header + // doesn't get set by StringContent..ctor, but only if someone actually accesses the ContentLength property. + Assert.Equal( + "Method: PUT, RequestUri: 'http://a.com/', Version: 1.0, Content: " + typeof(StringContent).ToString() + ", Headers:" + Environment.NewLine + + $"{{{Environment.NewLine}" + + " Content-Type: text/plain; charset=utf-8" + Environment.NewLine + + "}", rm.ToString()); + + rm.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/plain", 0.2)); + rm.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/xml", 0.1)); + rm.Headers.Add("Custom-Request-Header", "value1"); + rm.Content.Headers.Add("Custom-Content-Header", "value2"); + + Assert.Equal( + "Method: PUT, RequestUri: 'http://a.com/', Version: 1.0, Content: " + typeof(StringContent).ToString() + ", Headers:" + Environment.NewLine + + "{" + Environment.NewLine + + " Accept: text/plain; q=0.2" + Environment.NewLine + + " Accept: text/xml; q=0.1" + Environment.NewLine + + " Custom-Request-Header: value1" + Environment.NewLine + + " Content-Type: text/plain; charset=utf-8" + Environment.NewLine + + " Custom-Content-Header: value2" + Environment.NewLine + + "}", rm.ToString()); + } + + #region Helper methods + + private class MockContent : HttpContent + { + public bool IsDisposed { get; private set; } + + protected override bool TryComputeLength(out long length) + { + throw new NotImplementedException(); + } + +#nullable enable + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) + { +#nullable disable + throw new NotImplementedException(); + } + + protected override void Dispose(bool disposing) + { + IsDisposed = true; + base.Dispose(disposing); + } + } + + #endregion + } +}