diff --git a/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration.Tests/ConfigurationLiveTests.cs b/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration.Tests/ConfigurationLiveTests.cs index f85af74c3c49..03ce679c67d3 100644 --- a/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration.Tests/ConfigurationLiveTests.cs +++ b/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration.Tests/ConfigurationLiveTests.cs @@ -237,7 +237,7 @@ public async Task SetKeyValueLabel() await service.DeleteAsync(key, label); } } - + [Test] public async Task GetRequestId() { @@ -745,6 +745,7 @@ public async Task GetBatchSettingWithFields() { await service.DeleteAsync(s_testSetting.Key, s_testSetting.Label); } + } } diff --git a/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration.Tests/ConfigurationMockTests.cs b/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration.Tests/ConfigurationMockTests.cs index cbf9b0308f19..6040dda78c54 100644 --- a/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration.Tests/ConfigurationMockTests.cs +++ b/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration.Tests/ConfigurationMockTests.cs @@ -51,7 +51,7 @@ public async Task Get() AssertRequestCommon(request); Assert.AreEqual(HttpPipelineMethod.Get, request.Method); - Assert.AreEqual("https://contoso.azconfig.io/kv/test_key", request.Uri.ToString()); + Assert.AreEqual("https://contoso.azconfig.io/kv/test_key", request.UriBuilder.ToString()); Assert.AreEqual(s_testSetting, setting); } @@ -70,7 +70,7 @@ public async Task GetWithLabel() AssertRequestCommon(request); Assert.AreEqual(HttpPipelineMethod.Get, request.Method); - Assert.AreEqual("https://contoso.azconfig.io/kv/test_key?label=test_label", request.Uri.ToString()); + Assert.AreEqual("https://contoso.azconfig.io/kv/test_key?label=test_label", request.UriBuilder.ToString()); Assert.AreEqual(s_testSetting, setting); } @@ -103,7 +103,7 @@ public async Task Add() AssertRequestCommon(request); Assert.AreEqual(HttpPipelineMethod.Put, request.Method); - Assert.AreEqual("https://contoso.azconfig.io/kv/test_key?label=test_label", request.Uri.ToString()); + Assert.AreEqual("https://contoso.azconfig.io/kv/test_key?label=test_label", request.UriBuilder.ToString()); Assert.True(request.TryGetHeader("If-None-Match", out var ifNoneMatch)); Assert.AreEqual("*", ifNoneMatch); AssertContent(SerializationHelpers.Serialize(s_testSetting, SerializeRequestSetting), request); @@ -124,7 +124,7 @@ public async Task Set() AssertRequestCommon(request); Assert.AreEqual(HttpPipelineMethod.Put, request.Method); - Assert.AreEqual("https://contoso.azconfig.io/kv/test_key?label=test_label", request.Uri.ToString()); + Assert.AreEqual("https://contoso.azconfig.io/kv/test_key?label=test_label", request.UriBuilder.ToString()); AssertContent(SerializationHelpers.Serialize(s_testSetting, SerializeRequestSetting), request); Assert.AreEqual(s_testSetting, setting); } @@ -143,7 +143,7 @@ public async Task Update() AssertRequestCommon(request); Assert.AreEqual(HttpPipelineMethod.Put, request.Method); - Assert.AreEqual("https://contoso.azconfig.io/kv/test_key?label=test_label", request.Uri.ToString()); + Assert.AreEqual("https://contoso.azconfig.io/kv/test_key?label=test_label", request.UriBuilder.ToString()); AssertContent(SerializationHelpers.Serialize(s_testSetting, SerializeRequestSetting), request); Assert.AreEqual(s_testSetting, setting); Assert.True(request.TryGetHeader("If-Match", out var ifMatch)); @@ -164,7 +164,7 @@ public async Task Delete() AssertRequestCommon(request); Assert.AreEqual(HttpPipelineMethod.Delete, request.Method); - Assert.AreEqual("https://contoso.azconfig.io/kv/test_key", request.Uri.ToString()); + Assert.AreEqual("https://contoso.azconfig.io/kv/test_key", request.UriBuilder.ToString()); } [Test] @@ -181,7 +181,7 @@ public async Task DeleteWithLabel() AssertRequestCommon(request); Assert.AreEqual(HttpPipelineMethod.Delete, request.Method); - Assert.AreEqual("https://contoso.azconfig.io/kv/test_key?label=test_label", request.Uri.ToString()); + Assert.AreEqual("https://contoso.azconfig.io/kv/test_key?label=test_label", request.UriBuilder.ToString()); } [Test] @@ -248,12 +248,12 @@ public async Task GetBatch() MockRequest request1 = mockTransport.Requests[0]; Assert.AreEqual(HttpPipelineMethod.Get, request1.Method); - Assert.AreEqual("https://contoso.azconfig.io/kv/?key=*&label=*", request1.Uri.ToString()); + Assert.AreEqual("https://contoso.azconfig.io/kv/?key=*&label=*", request1.UriBuilder.ToString()); AssertRequestCommon(request1); MockRequest request2 = mockTransport.Requests[1]; Assert.AreEqual(HttpPipelineMethod.Get, request2.Method); - Assert.AreEqual("https://contoso.azconfig.io/kv/?key=*&label=*&after=5", request2.Uri.ToString()); + Assert.AreEqual("https://contoso.azconfig.io/kv/?key=*&label=*&after=5", request2.UriBuilder.ToString()); AssertRequestCommon(request1); } diff --git a/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration.Tests/ConfigurationSettingTests.cs b/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration.Tests/ConfigurationSettingTests.cs index 62150c64a988..f5170e2c880b 100644 --- a/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration.Tests/ConfigurationSettingTests.cs +++ b/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration.Tests/ConfigurationSettingTests.cs @@ -33,10 +33,11 @@ public void FilterReservedCharacter() Labels = new List() { "my_label", "label,label" }, }; - var builder = new UriBuilder(); + var builder = new HttpPipelineUriBuilder(); + builder.Uri = new Uri("http://localhost/"); service.BuildBatchQuery(builder, selector); - Assert.AreEqual(builder.Uri.AbsoluteUri, @"http://localhost/?key=my_key,key%5C,key&label=my_label,label%5C,label"); + Assert.AreEqual(@"http://localhost/?key=my_key,key%5C,key&label=my_label,label%5C,label", builder.Uri.AbsoluteUri); } @@ -50,10 +51,11 @@ public void FilterContains() Labels = new List() { "*label*" }, }; - var builder = new UriBuilder(); + var builder = new HttpPipelineUriBuilder(); + builder.Uri = new Uri("http://localhost/"); service.BuildBatchQuery(builder, selector); - Assert.AreEqual(builder.Uri.AbsoluteUri, "http://localhost/?key=*key*&label=*label*"); + Assert.AreEqual("http://localhost/?key=*key*&label=*label*", builder.Uri.AbsoluteUri); } [Test] @@ -65,10 +67,11 @@ public void FilterNullLabel() Labels = new List() { "" }, }; - var builder = new UriBuilder(); + var builder = new HttpPipelineUriBuilder(); + builder.Uri = new Uri("http://localhost/"); service.BuildBatchQuery(builder, selector); - Assert.AreEqual(builder.Uri.AbsoluteUri, "http://localhost/?key=*&label=%00"); + Assert.AreEqual("http://localhost/?key=*&label=%00", builder.Uri.AbsoluteUri); } [Test] @@ -79,10 +82,11 @@ public void FilterOnlyKey() var key = "my-key"; var selector = new SettingSelector(key); - var builder = new UriBuilder(); + var builder = new HttpPipelineUriBuilder(); + builder.Uri = new Uri("http://localhost/"); service.BuildBatchQuery(builder, selector); - Assert.AreEqual(builder.Uri.AbsoluteUri, $"http://localhost/?key={key}"); + Assert.AreEqual($"http://localhost/?key={key}", builder.Uri.AbsoluteUri); } [Test] @@ -93,10 +97,11 @@ public void FilterOnlyLabel() var label = "my-label"; var selector = new SettingSelector(null, label); - var builder = new UriBuilder(); + var builder = new HttpPipelineUriBuilder(); + builder.Uri = new Uri("http://localhost/"); service.BuildBatchQuery(builder, selector); - Assert.AreEqual(builder.Uri.AbsoluteUri, $"http://localhost/?key=*&label={label}"); + Assert.AreEqual($"http://localhost/?key=*&label={label}", builder.Uri.AbsoluteUri); } [Test] diff --git a/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration/AuthenticationPolicy.cs b/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration/AuthenticationPolicy.cs index 16e66a3f7ba9..b7cc101af59d 100644 --- a/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration/AuthenticationPolicy.cs +++ b/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration/AuthenticationPolicy.cs @@ -42,7 +42,7 @@ public override async Task ProcessAsync(HttpPipelineMessage message, ReadOnlyMem using (var hmac = new HMACSHA256(_secret)) { - var uri = message.Request.Uri; + var uri = message.Request.UriBuilder.Uri; var host = uri.Host; var pathAndQuery = uri.PathAndQuery; @@ -63,4 +63,4 @@ public override async Task ProcessAsync(HttpPipelineMessage message, ReadOnlyMem } } -} \ No newline at end of file +} diff --git a/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration/ConfigurationClient.cs b/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration/ConfigurationClient.cs index fe64ab4c57f3..d40440648810 100644 --- a/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration/ConfigurationClient.cs +++ b/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration/ConfigurationClient.cs @@ -46,13 +46,13 @@ public async Task> AddAsync(ConfigurationSetting if (setting == null) throw new ArgumentNullException(nameof(setting)); if (string.IsNullOrEmpty(setting.Key)) throw new ArgumentNullException($"{nameof(setting)}.{nameof(setting.Key)}"); - Uri uri = BuildUriForKvRoute(setting); - using (var request = _pipeline.CreateRequest()) { ReadOnlyMemory content = Serialize(setting); - request.SetRequestLine(HttpPipelineMethod.Put, uri); + request.Method = HttpPipelineMethod.Put; + + BuildUriForKvRoute(request.UriBuilder, setting); request.AddHeader(IfNoneMatch, "*"); request.AddHeader(MediaTypeKeyValueApplicationHeader); @@ -81,14 +81,12 @@ public async Task> SetAsync(ConfigurationSetting if (setting == null) throw new ArgumentNullException(nameof(setting)); if (string.IsNullOrEmpty(setting.Key)) throw new ArgumentNullException($"{nameof(setting)}.{nameof(setting.Key)}"); - Uri uri = BuildUriForKvRoute(setting); - using (var request = _pipeline.CreateRequest()) { ReadOnlyMemory content = Serialize(setting); - request.SetRequestLine(HttpPipelineMethod.Put, uri); - + request.Method = HttpPipelineMethod.Put; + BuildUriForKvRoute(request.UriBuilder, setting); request.AddHeader(MediaTypeKeyValueApplicationHeader); request.AddHeader(HttpHeader.Common.JsonContentType); @@ -120,14 +118,12 @@ public async Task> UpdateAsync(ConfigurationSetti if (setting == null) throw new ArgumentNullException(nameof(setting)); if (string.IsNullOrEmpty(setting.Key)) throw new ArgumentNullException($"{nameof(setting)}.{nameof(setting.Key)}"); - Uri uri = BuildUriForKvRoute(setting); - using (var request = _pipeline.CreateRequest()) { ReadOnlyMemory content = Serialize(setting); - request.SetRequestLine(HttpPipelineMethod.Put, uri); - + request.Method = HttpPipelineMethod.Put; + BuildUriForKvRoute(request.UriBuilder, setting); request.AddHeader(MediaTypeKeyValueApplicationHeader); request.AddHeader(HttpHeader.Common.JsonContentType); @@ -162,11 +158,10 @@ public async Task DeleteAsync(string key, string label = default, ETag { if (string.IsNullOrEmpty(key)) throw new ArgumentNullException(nameof(key)); - Uri uri = BuildUriForKvRoute(key, label); - using (var request = _pipeline.CreateRequest()) { - request.SetRequestLine(HttpPipelineMethod.Delete, uri); + request.Method = HttpPipelineMethod.Delete; + BuildUriForKvRoute(request.UriBuilder, key, label); if (etag != default) { @@ -187,13 +182,12 @@ public async Task> GetAsync(string key, string la { if (string.IsNullOrEmpty(key)) throw new ArgumentNullException($"{nameof(key)}"); - Uri uri = BuildUriForKvRoute(key, label); - using (var request = _pipeline.CreateRequest()) { - request.SetRequestLine(HttpPipelineMethod.Get, uri); - + request.Method = HttpPipelineMethod.Get; + BuildUriForKvRoute(request.UriBuilder, key, label); request.AddHeader(MediaTypeKeyValueApplicationHeader); + if (acceptDateTime != default) { var dateTime = acceptDateTime.UtcDateTime.ToString(AcceptDateTimeFormat); @@ -212,12 +206,10 @@ public async Task> GetAsync(string key, string la public async Task> GetBatchAsync(SettingSelector selector, CancellationToken cancellation = default) { - var uri = BuildUriForGetBatch(selector); - using (var request = _pipeline.CreateRequest()) { - request.SetRequestLine(HttpPipelineMethod.Get, uri); - + request.Method = HttpPipelineMethod.Get; + BuildUriForGetBatch(request.UriBuilder, selector); request.AddHeader(MediaTypeKeyValueApplicationHeader); if (selector.AsOf.HasValue) { @@ -237,12 +229,10 @@ public async Task> GetBatchAsync(SettingSelector selector public async Task> GetRevisionsAsync(SettingSelector selector, CancellationToken cancellation = default) { - var uri = BuildUriForRevisions(selector); - using (var request = _pipeline.CreateRequest()) { - request.SetRequestLine(HttpPipelineMethod.Get, uri); - + request.Method = HttpPipelineMethod.Get; + BuildUriForRevisions(request.UriBuilder, selector); request.AddHeader(MediaTypeKeyValueApplicationHeader); if (selector.AsOf.HasValue) { diff --git a/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration/ConfigurationClient_private.cs b/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration/ConfigurationClient_private.cs index edd283d1bc12..3b850ac1fc38 100644 --- a/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration/ConfigurationClient_private.cs +++ b/src/SDKs/Azure.ApplicationModel.Configuration/data-plane/Azure.ApplicationModel.Configuration/ConfigurationClient_private.cs @@ -80,20 +80,19 @@ static void ParseConnectionString(string connectionString, out Uri uri, out stri }; } - Uri BuildUriForKvRoute(ConfigurationSetting keyValue) - => BuildUriForKvRoute(keyValue.Key, keyValue.Label); // TODO (pri 2) : does this need to filter ETag? + void BuildUriForKvRoute(HttpPipelineUriBuilder builder, ConfigurationSetting keyValue) + => BuildUriForKvRoute(builder, keyValue.Key, keyValue.Label); // TODO (pri 2) : does this need to filter ETag? - Uri BuildUriForKvRoute(string key, string label) + void BuildUriForKvRoute(HttpPipelineUriBuilder builder, string key, string label) { - var builder = new UriBuilder(_baseUri); - builder.Path = KvRoute + key; + builder.Uri = _baseUri; + builder.AppendPath(KvRoute); + builder.AppendPath(key); if (label != null) { builder.AppendQuery(LabelQueryFilter, label); } - - return builder.Uri; } private string EscapeReservedCharacters(string input) @@ -113,7 +112,7 @@ private string EscapeReservedCharacters(string input) return resp; } - internal void BuildBatchQuery(UriBuilder builder, SettingSelector selector) + internal void BuildBatchQuery(HttpPipelineUriBuilder builder, SettingSelector selector) { if (selector.Keys.Count > 0) { @@ -163,22 +162,18 @@ internal void BuildBatchQuery(UriBuilder builder, SettingSelector selector) } } - Uri BuildUriForGetBatch(SettingSelector selector) + void BuildUriForGetBatch(HttpPipelineUriBuilder builder, SettingSelector selector) { - var builder = new UriBuilder(_baseUri); - builder.Path = KvRoute; + builder.Uri = _baseUri; + builder.AppendPath(KvRoute); BuildBatchQuery(builder, selector); - - return builder.Uri; } - Uri BuildUriForRevisions(SettingSelector selector) + void BuildUriForRevisions(HttpPipelineUriBuilder builder, SettingSelector selector) { - var builder = new UriBuilder(_baseUri); - builder.Path = RevisionsRoute; + builder.Uri = _baseUri; + builder.AppendPath(RevisionsRoute); BuildBatchQuery(builder, selector); - - return builder.Uri; } static ReadOnlyMemory Serialize(ConfigurationSetting setting) @@ -209,4 +204,4 @@ static ReadOnlyMemory Serialize(ConfigurationSetting setting) public override string ToString() => base.ToString(); #endregion } -} +} \ No newline at end of file diff --git a/src/SDKs/Azure.Base/data-plane/Azure.Base.Tests/HttpClientTransportTests.cs b/src/SDKs/Azure.Base/data-plane/Azure.Base.Tests/HttpClientTransportTests.cs index 25f17bf1105a..89fcff72a77c 100644 --- a/src/SDKs/Azure.Base/data-plane/Azure.Base.Tests/HttpClientTransportTests.cs +++ b/src/SDKs/Azure.Base/data-plane/Azure.Base.Tests/HttpClientTransportTests.cs @@ -80,7 +80,7 @@ public async Task HostHeaderSetFromUri() await ExecuteRequest(request, transport); - // HttpClientHandler would correctly set Host header from Uri when it's not set explicitly + // HttpClientHandler would correctly set Host header from UriBuilder when it's not set explicitly Assert.AreEqual("http://example.com:340/", uri.ToString()); Assert.Null(host); } @@ -142,7 +142,7 @@ public async Task CanGetAndSetUri() var request = transport.CreateRequest(null); request.SetRequestLine(HttpPipelineMethod.Get, expectedUri); - Assert.AreEqual(expectedUri, request.Uri); + Assert.AreEqual(expectedUri.ToString(), request.UriBuilder.ToString()); await ExecuteRequest(request, transport); diff --git a/src/SDKs/Azure.Base/data-plane/Azure.Base.Tests/HttpPipelineUriBuilderTest.cs b/src/SDKs/Azure.Base/data-plane/Azure.Base.Tests/HttpPipelineUriBuilderTest.cs new file mode 100644 index 000000000000..ac4901e393d2 --- /dev/null +++ b/src/SDKs/Azure.Base/data-plane/Azure.Base.Tests/HttpPipelineUriBuilderTest.cs @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using NUnit.Framework; + +namespace Azure.Base.Tests +{ + public class HttpPipelineUriBuilderTest + { + public static Uri[] Uris { get; } = { + new Uri("https://localhost:20/query?query"), + new Uri("https://localhost"), + new Uri("http://localhost"), + new Uri("http://localhost?query"), + new Uri("https://localhost:443/"), + new Uri("http://localhost:80/"), + new Uri("http://localhost:80/ ? "), + }; + + [TestCaseSource(nameof(Uris))] + public void RoundtripWithUri(Uri uri) + { + var uriBuilder = new HttpPipelineUriBuilder(); + uriBuilder.Uri = uri; + + Assert.AreEqual(uri.Scheme, uriBuilder.Scheme); + Assert.AreEqual(uri.Host, uriBuilder.Host); + Assert.AreEqual(uri.Port, uriBuilder.Port); + Assert.AreEqual(uri.AbsolutePath, uriBuilder.Path); + Assert.AreEqual(uri.Query, uriBuilder.Query); + Assert.AreEqual(uri, uriBuilder.Uri); + Assert.AreSame(uri, uriBuilder.Uri); + } + + [TestCase("", "http://localhost/")] + [TestCase("/", "http://localhost/")] + [TestCase("a", "http://localhost/a")] + [TestCase("/a", "http://localhost/a")] + public void AddsLeadingSlashToPath(string path, string expected) + { + var uriBuilder = new HttpPipelineUriBuilder(); + uriBuilder.Scheme = "http"; + uriBuilder.Host = "localhost"; + uriBuilder.Port = 80; + uriBuilder.Path = path; + + Assert.AreEqual(expected, uriBuilder.Uri.ToString()); + } + + [TestCase("", "http://localhost/")] + [TestCase("?", "http://localhost/?")] + [TestCase("a", "http://localhost/?a")] + [TestCase("?a", "http://localhost/?a")] + public void AddsLeadingQuestionMarkToQuery(string query, string expected) + { + var uriBuilder = new HttpPipelineUriBuilder(); + uriBuilder.Scheme = "http"; + uriBuilder.Host = "localhost"; + uriBuilder.Port = 80; + uriBuilder.Query = query; + + Assert.AreEqual(expected, uriBuilder.Uri.ToString()); + } + + [TestCase(null)] + [TestCase("")] + public void SettingQueryToEmptyRemovesQuestionMark(string query) + { + var uriBuilder = new HttpPipelineUriBuilder(); + uriBuilder.Scheme = "http"; + uriBuilder.Host = "localhost"; + uriBuilder.Port = 80; + uriBuilder.Query = "a"; + + Assert.AreEqual("http://localhost/?a", uriBuilder.Uri.ToString()); + + uriBuilder.Query = query; + + Assert.AreEqual("http://localhost/", uriBuilder.Uri.ToString()); + } + + [TestCase("\u1234\u2345", "%E1%88%B4%E2%8D%85")] + [TestCase("\u1234", "%E1%88%B4")] + [TestCase("\u1234\u2345", "%E1%88%B4%E2%8D%85")] + [TestCase(" ", "%20")] + [TestCase("%#?&", "%25#?&")] + public void PathIsEscaped(string path, string expectedPath) + { + var uriBuilder = new HttpPipelineUriBuilder(); + uriBuilder.Scheme = "http"; + uriBuilder.Host = "localhost"; + uriBuilder.Port = 80; + uriBuilder.Path = path; + + Assert.AreEqual("http://localhost/" + expectedPath, uriBuilder.Uri.OriginalString); + } + + [Test] + public void QueryIsNotEscaped() + { + var uriBuilder = new HttpPipelineUriBuilder(); + uriBuilder.Scheme = "http"; + uriBuilder.Host = "localhost"; + uriBuilder.Port = 80; + uriBuilder.Query = "\u1234"; + + Assert.AreEqual("http://localhost/?\u1234", uriBuilder.Uri.ToString()); + } + + [Test] + public void AppendQueryWithEmptyValueWorks() + { + var uriBuilder = new HttpPipelineUriBuilder(); + uriBuilder.Scheme = "http"; + uriBuilder.Host = "localhost"; + uriBuilder.Port = 80; + uriBuilder.AppendQuery("a", null); + + Assert.AreEqual("http://localhost/?a=", uriBuilder.Uri.ToString()); + } + + [TestCase(null, "http://localhost/?a=b&c=d")] + [TestCase("", "http://localhost/?a=b&c=d")] + [TestCase("a", "http://localhost/?a&a=b&c=d")] + [TestCase("?", "http://localhost/?a=b&c=d")] + [TestCase("?initial", "http://localhost/?initial&a=b&c=d")] + public void AppendQueryWorks(string initialQuery, string expectedResult) + { + var uriBuilder = new HttpPipelineUriBuilder(); + uriBuilder.Scheme = "http"; + uriBuilder.Host = "localhost"; + uriBuilder.Port = 80; + uriBuilder.Query = initialQuery; + uriBuilder.AppendQuery("a","b"); + uriBuilder.AppendQuery("c","d"); + + Assert.AreEqual(expectedResult, uriBuilder.Uri.ToString()); + } + + [TestCase(null, "", "http://localhost/")] + [TestCase("/", "/", "http://localhost/")] + [TestCase(null, "p", "http://localhost/p")] + [TestCase("/", "p", "http://localhost/p")] + [TestCase("/", "/p", "http://localhost/p")] + [TestCase("", "\u1234", "http://localhost/%E1%88%B4")] + public void AppendPathWorks(string initialPath, string append, string expectedResult) + { + var uriBuilder = new HttpPipelineUriBuilder(); + uriBuilder.Scheme = "http"; + uriBuilder.Host = "localhost"; + uriBuilder.Port = 80; + uriBuilder.Path = initialPath; + uriBuilder.AppendPath(append); + + Assert.AreEqual(expectedResult, uriBuilder.Uri.OriginalString); + } + + [Test] + public void AppendingQueryResetsUri() + { + var uriBuilder = new HttpPipelineUriBuilder(); + uriBuilder.Uri = new Uri("http://localhost/"); + uriBuilder.AppendQuery("a","b"); + + Assert.AreEqual("http://localhost/?a=b", uriBuilder.Uri.ToString()); + } + + [Test] + public void AppendingPathResetsUri() + { + var uriBuilder = new HttpPipelineUriBuilder(); + uriBuilder.Uri = new Uri("http://localhost/"); + uriBuilder.AppendPath("a"); + + Assert.AreEqual("http://localhost/a", uriBuilder.Uri.ToString()); + } + } +} diff --git a/src/SDKs/Azure.Base/data-plane/Azure.Base.Tests/PolicyTestBase.cs b/src/SDKs/Azure.Base/data-plane/Azure.Base.Tests/PolicyTestBase.cs index 8552751b9a80..ddb7def2a4ca 100644 --- a/src/SDKs/Azure.Base/data-plane/Azure.Base.Tests/PolicyTestBase.cs +++ b/src/SDKs/Azure.Base/data-plane/Azure.Base.Tests/PolicyTestBase.cs @@ -16,7 +16,7 @@ protected static Task SendGetRequest(HttpPipelineTransport transport, using (HttpPipelineRequest request = transport.CreateRequest(null)) { request.Method = HttpPipelineMethod.Get; - request.Uri = new Uri("http://example.com"); + request.UriBuilder.Uri = new Uri("http://example.com"); var pipeline = new HttpPipeline(transport, new [] { policy }); return pipeline.SendRequestAsync(request, CancellationToken.None); } diff --git a/src/SDKs/Azure.Base/data-plane/Azure.Base.Tests/Testing/MockRequest.cs b/src/SDKs/Azure.Base/data-plane/Azure.Base.Tests/Testing/MockRequest.cs index b9e53ee23e18..e573bc696074 100644 --- a/src/SDKs/Azure.Base/data-plane/Azure.Base.Tests/Testing/MockRequest.cs +++ b/src/SDKs/Azure.Base/data-plane/Azure.Base.Tests/Testing/MockRequest.cs @@ -40,7 +40,7 @@ public override bool TryGetHeader(string name, out string value) public override string RequestId { get; set; } - public override string ToString() => $"{Method} {Uri}"; + public override string ToString() => $"{Method} {UriBuilder}"; public override void Dispose() { diff --git a/src/SDKs/Azure.Base/data-plane/Azure.Base/Diagnostics/HttpPipelineEventSource.cs b/src/SDKs/Azure.Base/data-plane/Azure.Base/Diagnostics/HttpPipelineEventSource.cs index 51c51dd83425..f313b00f2532 100644 --- a/src/SDKs/Azure.Base/data-plane/Azure.Base/Diagnostics/HttpPipelineEventSource.cs +++ b/src/SDKs/Azure.Base/data-plane/Azure.Base/Diagnostics/HttpPipelineEventSource.cs @@ -42,7 +42,7 @@ public void Request(HttpPipelineRequest request) { if (IsEnabled(EventLevel.Informational, EventKeywords.None)) { - Request(request.RequestId, request.Method.ToString().ToUpperInvariant(), request.Uri.ToString(), FormatHeaders(request.Headers)); + Request(request.RequestId, request.Method.ToString().ToUpperInvariant(), request.UriBuilder.ToString(), FormatHeaders(request.Headers)); } } diff --git a/src/SDKs/Azure.Base/data-plane/Azure.Base/Http/HttpPipelineRequest.cs b/src/SDKs/Azure.Base/data-plane/Azure.Base/Http/HttpPipelineRequest.cs index 704c4d438cd0..0a8af3d96af2 100644 --- a/src/SDKs/Azure.Base/data-plane/Azure.Base/Http/HttpPipelineRequest.cs +++ b/src/SDKs/Azure.Base/data-plane/Azure.Base/Http/HttpPipelineRequest.cs @@ -8,14 +8,14 @@ namespace Azure.Base.Http { public abstract class HttpPipelineRequest : IDisposable { - public virtual Uri Uri { get; set; } + public virtual HttpPipelineUriBuilder UriBuilder { get; set; } = new HttpPipelineUriBuilder(); public virtual HttpPipelineMethod Method { get; set; } public virtual void SetRequestLine(HttpPipelineMethod method, Uri uri) { Method = method; - Uri = uri; + UriBuilder.Uri = uri; } public virtual HttpPipelineRequestContent Content { get; set; } diff --git a/src/SDKs/Azure.Base/data-plane/Azure.Base/Http/Pipeline/HttpClientTransport.cs b/src/SDKs/Azure.Base/data-plane/Azure.Base/Http/Pipeline/HttpClientTransport.cs index 7776a0744135..b0680ac8f330 100644 --- a/src/SDKs/Azure.Base/data-plane/Azure.Base/Http/Pipeline/HttpClientTransport.cs +++ b/src/SDKs/Azure.Base/data-plane/Azure.Base/Http/Pipeline/HttpClientTransport.cs @@ -100,12 +100,6 @@ public PipelineRequest() RequestId = Guid.NewGuid().ToString(); } - public override Uri Uri - { - get => _requestMessage.RequestUri; - set => _requestMessage.RequestUri = value; - } - public override HttpPipelineMethod Method { get => HttpPipelineMethodConverter.Parse(_requestMessage.Method.Method); @@ -151,7 +145,7 @@ public HttpRequestMessage BuildRequestMessage(CancellationToken cancellation) { // A copy of a message needs to be made because HttpClient does not allow sending the same message twice, // and so the retry logic fails. - var request = new HttpRequestMessage(_requestMessage.Method, _requestMessage.RequestUri); + var request = new HttpRequestMessage(_requestMessage.Method, UriBuilder.ToString()); CopyHeaders(_requestMessage.Headers, request.Headers); diff --git a/src/SDKs/Azure.Base/data-plane/Azure.Base/HttpPipelineUriBuilder.cs b/src/SDKs/Azure.Base/data-plane/Azure.Base/HttpPipelineUriBuilder.cs new file mode 100644 index 000000000000..ddc1ade4b84f --- /dev/null +++ b/src/SDKs/Azure.Base/data-plane/Azure.Base/HttpPipelineUriBuilder.cs @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Text; + +namespace Azure +{ + + public class HttpPipelineUriBuilder + { + private const char QuerySeparator = '?'; + + private const char PathSeparator = '/'; + + private readonly StringBuilder _pathAndQuery = new StringBuilder(); + + private int _queryIndex = -1; + + private Uri _uri; + + private int _port; + + private string _host; + + private string _scheme; + + public string Scheme + { + get => _scheme; + set + { + ResetUri(); + _scheme = value; + } + } + + public string Host + { + get => _host; + set + { + ResetUri(); + _host = value; + } + } + + public int Port + { + get => _port; + set + { + ResetUri(); + _port = value; + } + } + + public string Query + { + get => HasQuery ? _pathAndQuery.ToString(_queryIndex, _pathAndQuery.Length - _queryIndex) : string.Empty; + set + { + ResetUri(); + if (HasQuery) + { + _pathAndQuery.Remove(_queryIndex, _pathAndQuery.Length - _queryIndex); + _queryIndex = -1; + } + + if (!string.IsNullOrEmpty(value)) + { + _queryIndex = _pathAndQuery.Length; + if (value[0] != QuerySeparator) + { + _pathAndQuery.Append(QuerySeparator); + } + _pathAndQuery.Append(value); + } + } + } + + public string Path + { + get => HasQuery ? _pathAndQuery.ToString(0, _queryIndex) : _pathAndQuery.ToString(); + set + { + if (HasQuery) + { + _pathAndQuery.Remove(0, _queryIndex); + _pathAndQuery.Insert(0, value); + _queryIndex = value.Length; + } + else + { + _pathAndQuery.Remove(0, _pathAndQuery.Length); + _pathAndQuery.Append(value); + } + } + } + + private bool HasQuery => _queryIndex != -1; + + private int QueryLength => HasQuery ? _pathAndQuery.Length - _queryIndex : 0; + + private int PathLength => HasQuery ? _queryIndex : _pathAndQuery.Length; + + public string PathAndQuery => _pathAndQuery.ToString(); + + public Uri Uri + { + get + { + if (_uri == null) + { + _uri = new Uri(ToString()); + } + return _uri; + } + set + { + Scheme = value.Scheme; + Host = value.Host; + Port = value.Port; + Path = value.AbsolutePath; + Query = value.Query; + _uri = value; + } + } + + public void AppendQuery(string name, string value) + { + ResetUri(); + if (!HasQuery) + { + _pathAndQuery.Append(QuerySeparator); + _queryIndex = _pathAndQuery.Length; + } + else if (!(QueryLength == 1 && _pathAndQuery[_queryIndex] == QuerySeparator)) + { + _pathAndQuery.Append('&'); + } + + _pathAndQuery.Append(name); + _pathAndQuery.Append('='); + _pathAndQuery.Append(value); + } + + public void AppendPath(string value) + { + if (string.IsNullOrEmpty(value)) + { + return; + } + + ResetUri(); + int startIndex = 0; + if (PathLength == 1 && _pathAndQuery[0] == PathSeparator && value[0] == PathSeparator) + { + startIndex = 1; + } + if (HasQuery) + { + _pathAndQuery.Insert(_queryIndex, value.Substring(startIndex, value.Length - startIndex)); + _queryIndex += value.Length; + } + else + { + _pathAndQuery.Append(value, startIndex, value.Length - startIndex); + } + } + + public override string ToString() + { + var stringBuilder = new StringBuilder(); + stringBuilder.Append(Scheme); + stringBuilder.Append("://"); + stringBuilder.Append(Host); + if (!HasDefaultPortForScheme) + { + stringBuilder.Append(':'); + stringBuilder.Append(Port); + } + + if (_pathAndQuery.Length == 0 || _pathAndQuery[0] != PathSeparator) + { + stringBuilder.Append(PathSeparator); + } + + // TODO: Escaping can be done in-place + if (!HasQuery) + { + stringBuilder.Append(Uri.EscapeUriString(_pathAndQuery.ToString())); + } + else + { + stringBuilder.Append(Uri.EscapeUriString(_pathAndQuery.ToString(0, _queryIndex))); + stringBuilder.Append(_pathAndQuery.ToString(_queryIndex, _pathAndQuery.Length - _queryIndex)); + } + + return stringBuilder.ToString(); + } + + private bool HasDefaultPortForScheme => + (Port == 80 && Scheme.Equals("http", StringComparison.InvariantCultureIgnoreCase)) || + (Port == 443 && Scheme.Equals("https", StringComparison.InvariantCultureIgnoreCase)); + + private void ResetUri() + { + _uri = null; + } + } +} diff --git a/src/SDKs/Azure.Base/data-plane/Azure.Base/UriExtensions.cs b/src/SDKs/Azure.Base/data-plane/Azure.Base/UriExtensions.cs deleted file mode 100644 index 036cd8704035..000000000000 --- a/src/SDKs/Azure.Base/data-plane/Azure.Base/UriExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; - -namespace Azure -{ - public static class UriExtensions - { - public static void AppendQuery(this UriBuilder builder, string name, string value) - { - if(!string.IsNullOrEmpty(builder.Query)) { - builder.Query = builder.Query + "&" + name + "=" + value; - } - else { - builder.Query = name + "=" + value; - } - } - - public static void AppendQuery(this UriBuilder builder, string name, long value) - => AppendQuery(builder, name, value.ToString()); - } -}