From 0fb69a07ea1668f5206a5f83af0e03526c4f8f12 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Thu, 4 Nov 2021 18:52:41 +0800 Subject: [PATCH 01/75] Add support for indented json via config/scoped json --- src/ServiceStack.Text/JsConfig.cs | 11 +++++++ src/ServiceStack.Text/JsConfigScope.cs | 3 ++ .../Json/JsonWriter.Generic.cs | 7 ++++- src/ServiceStack.Text/JsonSerializer.cs | 29 +++++++++++++++---- .../ServiceStack.Text.Tests/JsConfigTests.cs | 21 ++++++++++++-- 5 files changed, 61 insertions(+), 10 deletions(-) diff --git a/src/ServiceStack.Text/JsConfig.cs b/src/ServiceStack.Text/JsConfig.cs index 964ceac6e..455ac33ec 100644 --- a/src/ServiceStack.Text/JsConfig.cs +++ b/src/ServiceStack.Text/JsConfig.cs @@ -96,6 +96,11 @@ public static JsConfigScope CreateScope(string config, JsConfigScope scope = nul case "includetypeinfo": scope.IncludeTypeInfo = boolValue; break; + case "i": + case "pp": //pretty-print + case "indent": + scope.Indent = boolValue; + break; case "eccn": case "emitcamelcasenames": scope.TextCase = boolValue ? TextCase.CamelCase : scope.TextCase; @@ -399,6 +404,12 @@ public static bool IncludeTypeInfo set => Config.AssertNotInit().IncludeTypeInfo = value; } + public static bool Indent + { + get => JsConfigScope.Current != null ? JsConfigScope.Current.Indent : Config.Instance.Indent; + set => Config.AssertNotInit().Indent = value; + } + public static string TypeAttr { get => JsConfigScope.Current != null ? JsConfigScope.Current.TypeAttr : Config.Instance.TypeAttr; diff --git a/src/ServiceStack.Text/JsConfigScope.cs b/src/ServiceStack.Text/JsConfigScope.cs index 7212eca66..81603de19 100644 --- a/src/ServiceStack.Text/JsConfigScope.cs +++ b/src/ServiceStack.Text/JsConfigScope.cs @@ -120,6 +120,7 @@ private Config(Config config) public bool TreatEnumAsInteger { get; set; } public bool ExcludeTypeInfo { get; set; } public bool IncludeTypeInfo { get; set; } + public bool Indent { get; set; } private string typeAttr; public string TypeAttr @@ -196,6 +197,7 @@ public bool EmitLowercaseUnderscoreNames TreatEnumAsInteger = false, ExcludeTypeInfo = false, IncludeTypeInfo = false, + Indent = false, TypeAttr = JsWriter.TypeAttr, DateTimeFormat = null, TypeWriter = AssemblyUtils.WriteType, @@ -240,6 +242,7 @@ public Config Populate(Config config) TreatEnumAsInteger = config.TreatEnumAsInteger; ExcludeTypeInfo = config.ExcludeTypeInfo; IncludeTypeInfo = config.IncludeTypeInfo; + Indent = config.Indent; TypeAttr = config.TypeAttr; DateTimeFormat = config.DateTimeFormat; TypeWriter = config.TypeWriter; diff --git a/src/ServiceStack.Text/Json/JsonWriter.Generic.cs b/src/ServiceStack.Text/Json/JsonWriter.Generic.cs index 3dcf75d2f..c5a9cc865 100644 --- a/src/ServiceStack.Text/Json/JsonWriter.Generic.cs +++ b/src/ServiceStack.Text/Json/JsonWriter.Generic.cs @@ -214,12 +214,17 @@ public static void WriteObject(TextWriter writer, object value) } public static void WriteRootObject(TextWriter writer, object value) + { + GetRootObjectWriteFn(value)(writer, value); + } + + public static WriteObjectDelegate GetRootObjectWriteFn(object value) { TypeConfig.Init(); JsonSerializer.OnSerialize?.Invoke(value); JsState.Depth = 0; - CacheFn(writer, value); + return CacheFn; } } diff --git a/src/ServiceStack.Text/JsonSerializer.cs b/src/ServiceStack.Text/JsonSerializer.cs index 4caa4787c..d3468fb74 100644 --- a/src/ServiceStack.Text/JsonSerializer.cs +++ b/src/ServiceStack.Text/JsonSerializer.cs @@ -99,7 +99,7 @@ public static string SerializeToString(T value) } else { - JsonWriter.WriteRootObject(writer, value); + WriteObjectToWriter(value, JsonWriter.GetRootObjectWriteFn(value), writer); } return StringWriterThreadStatic.ReturnAndFree(writer); } @@ -116,7 +116,7 @@ public static string SerializeToString(object value, Type type) else { OnSerialize?.Invoke(value); - JsonWriter.GetWriteFn(type)(writer, value); + WriteObjectToWriter(value, JsonWriter.GetWriteFn(type), writer); } return StringWriterThreadStatic.ReturnAndFree(writer); } @@ -141,7 +141,7 @@ public static void SerializeToWriter(T value, TextWriter writer) } else { - JsonWriter.WriteRootObject(writer, value); + WriteObjectToWriter(value, JsonWriter.GetRootObjectWriteFn(value), writer); } } @@ -155,7 +155,7 @@ public static void SerializeToWriter(object value, Type type, TextWriter writer) } OnSerialize?.Invoke(value); - JsonWriter.GetWriteFn(type)(writer, value); + WriteObjectToWriter(value, JsonWriter.GetWriteFn(type), writer); } public static void SerializeToStream(T value, Stream stream) @@ -175,7 +175,7 @@ public static void SerializeToStream(T value, Stream stream) else { var writer = new StreamWriter(stream, JsConfig.UTF8Encoding, BufferSize, leaveOpen:true); - JsonWriter.WriteRootObject(writer, value); + WriteObjectToWriter(value, JsonWriter.GetRootObjectWriteFn(value), writer); writer.Flush(); } } @@ -184,10 +184,27 @@ public static void SerializeToStream(object value, Type type, Stream stream) { OnSerialize?.Invoke(value); var writer = new StreamWriter(stream, JsConfig.UTF8Encoding, BufferSize, leaveOpen:true); - JsonWriter.GetWriteFn(type)(writer, value); + WriteObjectToWriter(value, JsonWriter.GetWriteFn(type), writer); writer.Flush(); } + private static void WriteObjectToWriter(object value, WriteObjectDelegate serializeFn, TextWriter writer) + { + if (!JsConfig.Indent) + { + serializeFn(writer, value); + } + else + { + var sb = StringBuilderCache.Allocate(); + using var captureJson = new StringWriter(sb); + serializeFn(captureJson, value); + captureJson.Flush(); + var json = StringBuilderCache.ReturnAndFree(sb); + var indentJson = json.IndentJson(); + writer.Write(indentJson); + } + } public static T DeserializeFromStream(Stream stream) { return (T)MemoryProvider.Instance.Deserialize(stream, typeof(T), DeserializeFromSpan); diff --git a/tests/ServiceStack.Text.Tests/JsConfigTests.cs b/tests/ServiceStack.Text.Tests/JsConfigTests.cs index dc1627c18..b42d3d3b1 100644 --- a/tests/ServiceStack.Text.Tests/JsConfigTests.cs +++ b/tests/ServiceStack.Text.Tests/JsConfigTests.cs @@ -201,6 +201,19 @@ class TestObject } private const string AssertMessageFormat = "Cannot find correct property value ({0})"; + + [Test] + public void Can_indent_in_scoped_json() + { + var obj = new TestObject { Id = 1 }; + using (JsConfig.With(new Config { Indent = true })) + { + var scopedJson = obj.ToJson(); + Assert.That(scopedJson.NormalizeNewLines(), Is.EqualTo("{\n \"Id\": 1,\n \"RootId\": 0\n}")); + } + var json = obj.ToJson(); + Assert.That(json, Is.EqualTo("{\"Id\":1,\"RootId\":0}")); + } } [TestFixture] @@ -209,11 +222,12 @@ public class JsConfigCreateTests [Test] public void Does_create_scope_from_string() { - var scope = JsConfig.CreateScope("emitlowercaseunderscorenames,IncludeNullValues:false,ExcludeDefaultValues:0,IncludeDefaultEnums:1"); + var scope = JsConfig.CreateScope("emitlowercaseunderscorenames,IncludeNullValues:false,ExcludeDefaultValues:0,IncludeDefaultEnums:1,indent"); Assert.That(scope.TextCase, Is.EqualTo(TextCase.SnakeCase)); Assert.That(!scope.IncludeNullValues); Assert.That(!scope.ExcludeDefaultValues); Assert.That(scope.IncludeDefaultEnums); + Assert.That(scope.Indent); scope.Dispose(); scope = JsConfig.CreateScope("DateHandler:ISO8601,timespanhandler:durationformat,PropertyConvention:strict,TextCase:CamelCase"); @@ -227,11 +241,12 @@ public void Does_create_scope_from_string() [Test] public void Does_create_scope_from_string_using_CamelCaseHumps() { - var scope = JsConfig.CreateScope("eccn,inv:false,edv:0,ide:1"); + var scope = JsConfig.CreateScope("eccn,inv:false,edv:0,ide:1,pp"); Assert.That(scope.TextCase, Is.EqualTo(TextCase.CamelCase)); Assert.That(!scope.IncludeNullValues); Assert.That(!scope.ExcludeDefaultValues); Assert.That(scope.IncludeDefaultEnums); + Assert.That(scope.Indent); scope.Dispose(); scope = JsConfig.CreateScope("dh:ISO8601,tsh:df,pc:strict,tc:cc"); @@ -274,7 +289,7 @@ public void Does_not_allow_setting_multiple_inits_in_StrictMode() Env.StrictMode = true; - Assert.Throws(() => JsConfig.Init()); + Assert.Throws(JsConfig.Init); } [Test] From 00a5972c844d303fd028e5e4a23cb022fc6c44fe Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Fri, 5 Nov 2021 21:57:34 +0800 Subject: [PATCH 02/75] Implement JsonObject.GetEnumerator() so returns same value as indexer --- src/ServiceStack.Text/JsonObject.cs | 11 +++++++++++ tests/ServiceStack.Text.Tests/JsonObjectTests.cs | 15 +++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/ServiceStack.Text/JsonObject.cs b/src/ServiceStack.Text/JsonObject.cs index 5333f39ba..eaacf5fe4 100644 --- a/src/ServiceStack.Text/JsonObject.cs +++ b/src/ServiceStack.Text/JsonObject.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -88,6 +89,16 @@ public class JsonObject : Dictionary set => base[key] = value; } + public Enumerator GetEnumerator() + { + var to = new Dictionary(); + foreach (var key in Keys) + { + to[key] = this[key]; + } + return to.GetEnumerator(); + } + public static JsonObject Parse(string json) { return JsonSerializer.DeserializeFromString(json); diff --git a/tests/ServiceStack.Text.Tests/JsonObjectTests.cs b/tests/ServiceStack.Text.Tests/JsonObjectTests.cs index 29fc04b4d..bb20f7fcf 100644 --- a/tests/ServiceStack.Text.Tests/JsonObjectTests.cs +++ b/tests/ServiceStack.Text.Tests/JsonObjectTests.cs @@ -474,5 +474,20 @@ public void Does_serialize_ObjectDictionaryWrapper_line_breaks() Assert.That(nested.ToJson(), Is.EqualTo("{\"Prop\":{\"a\":{\"text\":\"line\\nbreak\"}}}")); } + [Test] + public void Enumerating_JsonObject_returns_same_unescaped_value_as_indexer() + { + var obj = JsonObject.Parse(@"{""a"":""b\\c""}"); + Assert.That(obj["a"], Is.EqualTo("b\\c")); + + foreach (var entry in obj) + { + if (entry.Key == "a") + { + Assert.That(entry.Value, Is.EqualTo("b\\c")); + } + } + } + } } \ No newline at end of file From 99a556f76b77ae43bf3d38f0b5e0924abf60eebe Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Fri, 5 Nov 2021 22:54:04 +0800 Subject: [PATCH 03/75] Also override JsonObject's IEnumerable> --- src/ServiceStack.Text/JsonObject.cs | 7 +++-- .../JsonObjectTests.cs | 28 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/ServiceStack.Text/JsonObject.cs b/src/ServiceStack.Text/JsonObject.cs index eaacf5fe4..47fbfb044 100644 --- a/src/ServiceStack.Text/JsonObject.cs +++ b/src/ServiceStack.Text/JsonObject.cs @@ -78,7 +78,7 @@ public static Dictionary ToDictionary(this JsonObject jsonObject } } - public class JsonObject : Dictionary + public class JsonObject : Dictionary, IEnumerable> { /// /// Get JSON string value @@ -89,7 +89,7 @@ public class JsonObject : Dictionary set => base[key] = value; } - public Enumerator GetEnumerator() + public new Enumerator GetEnumerator() { var to = new Dictionary(); foreach (var key in Keys) @@ -99,6 +99,9 @@ public Enumerator GetEnumerator() return to.GetEnumerator(); } + IEnumerator> IEnumerable>.GetEnumerator() + => GetEnumerator(); + public static JsonObject Parse(string json) { return JsonSerializer.DeserializeFromString(json); diff --git a/tests/ServiceStack.Text.Tests/JsonObjectTests.cs b/tests/ServiceStack.Text.Tests/JsonObjectTests.cs index bb20f7fcf..667781dfd 100644 --- a/tests/ServiceStack.Text.Tests/JsonObjectTests.cs +++ b/tests/ServiceStack.Text.Tests/JsonObjectTests.cs @@ -487,6 +487,34 @@ public void Enumerating_JsonObject_returns_same_unescaped_value_as_indexer() Assert.That(entry.Value, Is.EqualTo("b\\c")); } } + + var asEnumerable = (IEnumerable>)obj; + foreach (var entry in asEnumerable) + { + if (entry.Key == "a") + { + Assert.That(entry.Value, Is.EqualTo("b\\c")); + } + } + + var asIDict = (IDictionary)obj; + foreach (var entry in asIDict) + { + if (entry.Key == "a") + { + Assert.That(entry.Value, Is.EqualTo("b\\c")); + } + } + + // Warning: can't override concrete Dictionary enumerator + // var asDict = (Dictionary)obj; + // foreach (var entry in asDict) + // { + // if (entry.Key == "a") + // { + // Assert.That(entry.Value, Is.EqualTo("b\\c")); + // } + // } } } From da5efc0dd2bda9d95bb95b2fc4614e7d3b1017ac Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Fri, 5 Nov 2021 23:23:46 +0800 Subject: [PATCH 04/75] Fix tests --- src/ServiceStack.Text/Common/WriteDictionary.cs | 4 ++++ src/ServiceStack.Text/JsonObject.cs | 11 +++++++++++ src/ServiceStack.Text/Pcl.Dynamic.cs | 2 +- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/ServiceStack.Text/Common/WriteDictionary.cs b/src/ServiceStack.Text/Common/WriteDictionary.cs index a40501384..4041e801b 100644 --- a/src/ServiceStack.Text/Common/WriteDictionary.cs +++ b/src/ServiceStack.Text/Common/WriteDictionary.cs @@ -205,6 +205,10 @@ public static void WriteGenericIDictionary( writer.Write(JsonUtils.Null); return; } + + if (map is JsonObject jsonObject) + map = (IDictionary) jsonObject.ToUnescapedDictionary(); + writer.Write(JsWriter.MapStartChar); var encodeMapKey = Serializer.GetTypeInfo(typeof(TKey)).EncodeMapKey; diff --git a/src/ServiceStack.Text/JsonObject.cs b/src/ServiceStack.Text/JsonObject.cs index 47fbfb044..196a13d44 100644 --- a/src/ServiceStack.Text/JsonObject.cs +++ b/src/ServiceStack.Text/JsonObject.cs @@ -102,6 +102,17 @@ public class JsonObject : Dictionary, IEnumerable> IEnumerable>.GetEnumerator() => GetEnumerator(); + public Dictionary ToUnescapedDictionary() + { + var to = new Dictionary(); + var enumerateAsConcreteDict = (Dictionary)this; + foreach (var entry in enumerateAsConcreteDict) + { + to[entry.Key] = entry.Value; + } + return to; + } + public static JsonObject Parse(string json) { return JsonSerializer.DeserializeFromString(json); diff --git a/src/ServiceStack.Text/Pcl.Dynamic.cs b/src/ServiceStack.Text/Pcl.Dynamic.cs index 0a3a84451..0a462ab61 100644 --- a/src/ServiceStack.Text/Pcl.Dynamic.cs +++ b/src/ServiceStack.Text/Pcl.Dynamic.cs @@ -102,7 +102,7 @@ public static dynamic Deserialize(string json) { // Support arbitrary nesting by using JsonObject var deserialized = JsonSerializer.DeserializeFromString(json); - var hash = deserialized.ToDictionary, string, object>(entry => entry.Key, entry => entry.Value); + var hash = deserialized.ToUnescapedDictionary().ToObjectDictionary(); return new DynamicJson(hash); } From 4a4ad4c5be466f936f61221d851171db73da4cb7 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Tue, 9 Nov 2021 19:01:17 +0800 Subject: [PATCH 05/75] Use 5.* wildcard version for .Text deps --- tests/Northwind.Common/Northwind.Common.csproj | 2 +- tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Northwind.Common/Northwind.Common.csproj b/tests/Northwind.Common/Northwind.Common.csproj index 2ae3f767f..c575bd4b0 100644 --- a/tests/Northwind.Common/Northwind.Common.csproj +++ b/tests/Northwind.Common/Northwind.Common.csproj @@ -26,7 +26,7 @@ - + diff --git a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj index 266f235a4..c5861f375 100644 --- a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj +++ b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj @@ -34,7 +34,7 @@ - + $(DefineConstants);NET45 From 8df4588b426e0d75b6057a9de9e627096efd4a2c Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Tue, 9 Nov 2021 22:40:48 +0800 Subject: [PATCH 06/75] bump to v5.13.0 --- src/Directory.Build.props | 2 +- src/ServiceStack.Text/Env.cs | 2 +- tests/Directory.Build.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 37d61bb6e..7bc3a2084 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - 5.12.1 + 5.13.0 ServiceStack ServiceStack, Inc. © 2008-2022 ServiceStack, Inc diff --git a/src/ServiceStack.Text/Env.cs b/src/ServiceStack.Text/Env.cs index 53bbfce0e..b0a6a6eda 100644 --- a/src/ServiceStack.Text/Env.cs +++ b/src/ServiceStack.Text/Env.cs @@ -115,7 +115,7 @@ static Env() public static string VersionString { get; set; } - public static decimal ServiceStackVersion = 5.121m; + public static decimal ServiceStackVersion = 5.130m; public static bool IsLinux { get; set; } diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index a3ad75bd7..cd75a1be6 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -1,7 +1,7 @@ - 5.12.1 + 5.13.0 latest false From 67d23143769983d3d8465123aad7476762487a7a Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Wed, 10 Nov 2021 18:19:39 +0800 Subject: [PATCH 07/75] Change tests to run recommended net472 or net6.0 TFMs --- .../DictionaryTests.cs | 21 +++++++++++-------- .../ServiceStack.Text.Tests.csproj | 14 ++++++------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/tests/ServiceStack.Text.Tests/DictionaryTests.cs b/tests/ServiceStack.Text.Tests/DictionaryTests.cs index d20ccdd3f..400d44f51 100644 --- a/tests/ServiceStack.Text.Tests/DictionaryTests.cs +++ b/tests/ServiceStack.Text.Tests/DictionaryTests.cs @@ -250,7 +250,7 @@ public void Test_ServiceStack_Text_JsonSerializer_Array_Value_Deserializes_Corre } [Test] - public void deserizes_to_decimal_by_default() + public void Deserializes_to_decimal_by_default() { JsConfig.TryToParsePrimitiveTypeValues = true; @@ -279,7 +279,7 @@ public NumericType(decimal min, decimal max, Type type) } [Test] - public void deserizes_signed_bytes_into_to_best_fit_numeric() + public void Deserializes_signed_bytes_into_to_best_fit_numeric() { JsConfig.TryToParsePrimitiveTypeValues = true; JsConfig.TryToParseNumericType = true; @@ -292,8 +292,9 @@ public void deserizes_signed_bytes_into_to_best_fit_numeric() Assert.That(deserializedDict["max"], Is.EqualTo(sbyte.MaxValue)); } +#if NETFX [Test] - public void deserizes_floats_into_to_best_fit_floating_point() + public void Deserializes_floats_into_to_best_fit_floating_point() { JsConfig.TryToParsePrimitiveTypeValues = true; JsConfig.TryToParseNumericType = true; @@ -301,7 +302,8 @@ public void deserizes_floats_into_to_best_fit_floating_point() float floatValue = 1.1f; //TODO find a number that doesn't suck which throws in float.Parse() but not double.Parse() - double doubleValue = double.MaxValue - Math.Pow(2, 1000); + double Offset = Math.Pow(2, 1000); + double doubleValue = double.MaxValue - Offset; var intValue = int.MaxValue; var longValue = long.MaxValue; @@ -311,7 +313,7 @@ public void deserizes_floats_into_to_best_fit_floating_point() var toFloatValue = float.Parse(floatValue.ToString()); Assert.AreEqual(toFloatValue, floatValue, 1); var toDoubleValue = double.Parse(doubleValue.ToString()); - Assert.AreEqual(toDoubleValue, doubleValue, Math.Pow(2, 1000)); + Assert.AreEqual(toDoubleValue, doubleValue, Offset); var json = "{{\"float\":{0},\"double\":{1},\"int\":{2},\"long\":{3}}}" .Fmt(CultureInfo.InvariantCulture, floatValue, doubleValue, intValue, longValue); @@ -319,9 +321,9 @@ public void deserizes_floats_into_to_best_fit_floating_point() Assert.That(map["float"], Is.TypeOf()); Assert.That(map["float"], Is.EqualTo(floatValue)); - + Assert.That(map["double"], Is.TypeOf()); - Assert.AreEqual((double)map["double"], doubleValue, Math.Pow(2, 1000)); + Assert.AreEqual((double)map["double"], doubleValue, Offset); Assert.That(map["int"], Is.TypeOf()); Assert.That(map["int"], Is.EqualTo(intValue)); @@ -331,9 +333,10 @@ public void deserizes_floats_into_to_best_fit_floating_point() JsConfig.Reset(); } +#endif [Test] - public void deserizes_signed_types_into_to_best_fit_numeric() + public void Deserializes_signed_types_into_to_best_fit_numeric() { var unsignedTypes = new[] { @@ -365,7 +368,7 @@ public void deserizes_signed_types_into_to_best_fit_numeric() } [Test] - public void deserizes_unsigned_types_into_to_best_fit_numeric() + public void Deserializes_unsigned_types_into_to_best_fit_numeric() { var unsignedTypes = new[] { diff --git a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj index c5861f375..7092dec2b 100644 --- a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj +++ b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj @@ -1,6 +1,6 @@  - net461;netcoreapp2.1 + net472;net6.0 portable ServiceStack.Text.Tests Library @@ -16,7 +16,7 @@ latest - Full + Full @@ -36,10 +36,10 @@ - - $(DefineConstants);NET45 + + $(DefineConstants);NETFX - + @@ -49,10 +49,10 @@ - + $(DefineConstants);NETCORE;NETSTANDARD2_0 - + From 6463f003853521bcbb58b9941ce870aa76c15190 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Wed, 10 Nov 2021 20:36:46 +0800 Subject: [PATCH 08/75] Add support for .NET 6 DateOnly/TimeOnly data types --- .../Common/DeserializeBuiltin.cs | 12 ++ .../Common/ITypeSerializer.cs | 11 +- src/ServiceStack.Text/Common/JsWriter.cs | 18 +++ src/ServiceStack.Text/DateTimeExtensions.cs | 5 + .../Json/JsonTypeSerializer.cs | 45 +++++- .../Jsv/JsvTypeSerializer.cs | 39 +++++ .../ServiceStack.Text.Tests/Net6TypeTests.cs | 138 ++++++++++++++++++ .../ServiceStack.Text.Tests.csproj | 6 +- .../ServiceStack.Text.TestsConsole.csproj | 2 +- 9 files changed, 269 insertions(+), 7 deletions(-) create mode 100644 tests/ServiceStack.Text.Tests/Net6TypeTests.cs diff --git a/src/ServiceStack.Text/Common/DeserializeBuiltin.cs b/src/ServiceStack.Text/Common/DeserializeBuiltin.cs index 0889051c9..78471e323 100644 --- a/src/ServiceStack.Text/Common/DeserializeBuiltin.cs +++ b/src/ServiceStack.Text/Common/DeserializeBuiltin.cs @@ -74,6 +74,12 @@ private static ParseStringSpanDelegate GetParseStringSpanFn() return value => DateTimeSerializer.ParseDateTimeOffset(value.ToString()); if (typeof(T) == typeof(TimeSpan)) return value => DateTimeSerializer.ParseTimeSpan(value.ToString()); +#if NET6_0 + if (typeof(T) == typeof(DateOnly)) + return value => DateOnly.FromDateTime(DateTimeSerializer.ParseShortestXsdDateTime(value.ToString())); + if (typeof(T) == typeof(TimeOnly)) + return value => TimeOnly.FromTimeSpan(DateTimeSerializer.ParseTimeSpan(value.ToString())); +#endif } else { @@ -119,6 +125,12 @@ private static ParseStringSpanDelegate GetParseStringSpanFn() return value => value.IsNullOrEmpty() ? (Guid?)null : value.ParseGuid(); if (typeof(T) == typeof(DateTimeOffset?)) return value => DateTimeSerializer.ParseNullableDateTimeOffset(value.ToString()); +#if NET6_0 + if (typeof(T) == typeof(DateOnly?)) + return value => value.IsNullOrEmpty() ? default : DateOnly.FromDateTime(DateTimeSerializer.ParseShortestXsdDateTime(value.ToString())); + if (typeof(T) == typeof(TimeOnly?)) + return value => value.IsNullOrEmpty() ? default : TimeOnly.FromTimeSpan(DateTimeSerializer.ParseTimeSpan(value.ToString())); +#endif } return null; diff --git a/src/ServiceStack.Text/Common/ITypeSerializer.cs b/src/ServiceStack.Text/Common/ITypeSerializer.cs index 4545e0b62..2f25ebaa9 100644 --- a/src/ServiceStack.Text/Common/ITypeSerializer.cs +++ b/src/ServiceStack.Text/Common/ITypeSerializer.cs @@ -30,8 +30,8 @@ public interface ITypeSerializer void WriteNullableDateTime(TextWriter writer, object dateTime); void WriteDateTimeOffset(TextWriter writer, object oDateTimeOffset); void WriteNullableDateTimeOffset(TextWriter writer, object dateTimeOffset); - void WriteTimeSpan(TextWriter writer, object dateTimeOffset); - void WriteNullableTimeSpan(TextWriter writer, object dateTimeOffset); + void WriteTimeSpan(TextWriter writer, object timeSpan); + void WriteNullableTimeSpan(TextWriter writer, object timeSpan); void WriteGuid(TextWriter writer, object oValue); void WriteNullableGuid(TextWriter writer, object oValue); void WriteBytes(TextWriter writer, object oByteValue); @@ -50,6 +50,13 @@ public interface ITypeSerializer void WriteDecimal(TextWriter writer, object decimalValue); void WriteEnum(TextWriter writer, object enumValue); +#if NET6_0_OR_GREATER + void WriteDateOnly(TextWriter writer, object oDateOnly); + void WriteNullableDateOnly(TextWriter writer, object oDateOnly); + void WriteTimeOnly(TextWriter writer, object oTimeOnly); + void WriteNullableTimeOnly(TextWriter writer, object oTimeOnly); +#endif + ParseStringDelegate GetParseFn(); ParseStringSpanDelegate GetParseStringSpanFn(); ParseStringDelegate GetParseFn(Type type); diff --git a/src/ServiceStack.Text/Common/JsWriter.cs b/src/ServiceStack.Text/Common/JsWriter.cs index daf12c4e5..e9bb5dd00 100644 --- a/src/ServiceStack.Text/Common/JsWriter.cs +++ b/src/ServiceStack.Text/Common/JsWriter.cs @@ -278,6 +278,24 @@ public WriteObjectDelegate GetValueTypeToStringMethod(Type type) if (type == typeof(Guid?)) return Serializer.WriteNullableGuid; + +#if NET6_0 + if (type == typeof(DateOnly)) + if (isNullable) + return Serializer.WriteNullableDateOnly; + else + return Serializer.WriteDateOnly; + if (type == typeof(DateOnly?)) + return Serializer.WriteDateOnly; + + if (type == typeof(TimeOnly)) + if (isNullable) + return Serializer.WriteNullableTimeOnly; + else + return Serializer.WriteTimeOnly; + if (type == typeof(TimeOnly?)) + return Serializer.WriteTimeOnly; +#endif } else { diff --git a/src/ServiceStack.Text/DateTimeExtensions.cs b/src/ServiceStack.Text/DateTimeExtensions.cs index a11dedb4d..0373037f3 100644 --- a/src/ServiceStack.Text/DateTimeExtensions.cs +++ b/src/ServiceStack.Text/DateTimeExtensions.cs @@ -76,6 +76,11 @@ public static long ToUnixTimeMs(this long ticks) return (ticks - UnixEpoch) / TimeSpan.TicksPerMillisecond; } +#if NET6_0 + public static long ToUnixTimeMs(this DateOnly dateOnly) => dateOnly.ToDateTime(default, DateTimeKind.Utc).ToUnixTimeMs(); + public static long ToUnixTime(this DateOnly dateOnly) => dateOnly.ToDateTime(default, DateTimeKind.Utc).ToUnixTime(); +#endif + public static DateTime FromUnixTimeMs(this double msSince1970) { return UnixEpochDateTimeUtc + TimeSpan.FromMilliseconds(msSince1970); diff --git a/src/ServiceStack.Text/Json/JsonTypeSerializer.cs b/src/ServiceStack.Text/Json/JsonTypeSerializer.cs index 7101a82fc..4d38b238d 100644 --- a/src/ServiceStack.Text/Json/JsonTypeSerializer.cs +++ b/src/ServiceStack.Text/Json/JsonTypeSerializer.cs @@ -138,7 +138,6 @@ public void WriteTimeSpan(TextWriter writer, object oTimeSpan) public void WriteNullableTimeSpan(TextWriter writer, object oTimeSpan) { - if (oTimeSpan == null) return; WriteTimeSpan(writer, ((TimeSpan?)oTimeSpan).Value); } @@ -289,6 +288,50 @@ public void WriteEnum(TextWriter writer, object enumValue) JsWriter.WriteEnumFlags(writer, enumValue); } + +#if NET6_0 + public void WriteDateOnly(TextWriter writer, object oDateOnly) + { + var dateOnly = (DateOnly)oDateOnly; + switch (JsConfig.DateHandler) + { + case DateHandler.UnixTime: + writer.Write(dateOnly.ToUnixTime()); + break; + case DateHandler.UnixTimeMs: + writer.Write(dateOnly.ToUnixTimeMs()); + break; + default: + writer.Write(JsWriter.QuoteString); + writer.Write(dateOnly.ToString("O")); + writer.Write(JsWriter.QuoteString); + break; + } + } + + public void WriteNullableDateOnly(TextWriter writer, object oDateOnly) + { + if (oDateOnly == null) + writer.Write(JsonUtils.Null); + else + WriteDateOnly(writer, oDateOnly); + } + + public void WriteTimeOnly(TextWriter writer, object oTimeOnly) + { + var stringValue = JsConfig.TimeSpanHandler == TimeSpanHandler.StandardFormat + ? oTimeOnly.ToString() + : DateTimeSerializer.ToXsdTimeSpanString(((TimeOnly)oTimeOnly).ToTimeSpan()); + WriteRawString(writer, stringValue); + } + + public void WriteNullableTimeOnly(TextWriter writer, object oTimeOnly) + { + if (oTimeOnly == null) return; + WriteTimeSpan(writer, ((TimeOnly?)oTimeOnly).Value.ToTimeSpan()); + } +#endif + [MethodImpl(MethodImplOptions.AggressiveInlining)] public ParseStringDelegate GetParseFn() { diff --git a/src/ServiceStack.Text/Jsv/JsvTypeSerializer.cs b/src/ServiceStack.Text/Jsv/JsvTypeSerializer.cs index 2b78ed86b..4c7a48306 100644 --- a/src/ServiceStack.Text/Jsv/JsvTypeSerializer.cs +++ b/src/ServiceStack.Text/Jsv/JsvTypeSerializer.cs @@ -248,6 +248,45 @@ public void WriteEnum(TextWriter writer, object enumValue) JsWriter.WriteEnumFlags(writer, enumValue); } +#if NET6_0 + public void WriteDateOnly(TextWriter writer, object oDateOnly) + { + var dateOnly = (DateOnly)oDateOnly; + switch (JsConfig.DateHandler) + { + case DateHandler.UnixTime: + writer.Write(dateOnly.ToUnixTime()); + break; + case DateHandler.UnixTimeMs: + writer.Write(dateOnly.ToUnixTimeMs()); + break; + default: + writer.Write(dateOnly.ToString("O")); + break; + } + } + + public void WriteNullableDateOnly(TextWriter writer, object oDateOnly) + { + if (oDateOnly == null) return; + WriteDateOnly(writer, oDateOnly); + } + + public void WriteTimeOnly(TextWriter writer, object oTimeOnly) + { + var stringValue = JsConfig.TimeSpanHandler == TimeSpanHandler.StandardFormat + ? oTimeOnly.ToString() + : DateTimeSerializer.ToXsdTimeSpanString(((TimeOnly)oTimeOnly).ToTimeSpan()); + WriteRawString(writer, stringValue); + } + + public void WriteNullableTimeOnly(TextWriter writer, object oTimeOnly) + { + if (oTimeOnly == null) return; + WriteTimeSpan(writer, ((TimeOnly?)oTimeOnly).Value.ToTimeSpan()); + } +#endif + public ParseStringDelegate GetParseFn() => JsvReader.Instance.GetParseFn(); public ParseStringDelegate GetParseFn(Type type) => JsvReader.GetParseFn(type); diff --git a/tests/ServiceStack.Text.Tests/Net6TypeTests.cs b/tests/ServiceStack.Text.Tests/Net6TypeTests.cs new file mode 100644 index 000000000..e3a097dad --- /dev/null +++ b/tests/ServiceStack.Text.Tests/Net6TypeTests.cs @@ -0,0 +1,138 @@ +using System; +using NUnit.Framework; + +#if NET6_0 +namespace ServiceStack.Text.Tests; + +public class DateOnlyDto +{ + public DateOnly Date { get; set; } + + protected bool Equals(DateOnlyDto other) => Date.Equals(other.Date); + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((DateOnlyDto)obj); + } + public override int GetHashCode() => Date.GetHashCode(); +} + +public class TimeOnlyDto +{ + public TimeOnly Time { get; set; } + + protected bool Equals(TimeOnlyDto other) => Time.Equals(other.Time); + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((TimeOnlyDto)obj); + } + public override int GetHashCode() => Time.GetHashCode(); +} + +public class NullableDateOnlyDto +{ + public DateOnly? Date { get; set; } + + protected bool Equals(NullableDateOnlyDto other) => Date.Equals(other.Date); + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((NullableDateOnlyDto)obj); + } + public override int GetHashCode() => (Date ?? default).GetHashCode(); +} + +public class NullableTimeOnlyDto +{ + public TimeOnly? Time { get; set; } + + protected bool Equals(NullableTimeOnlyDto other) => Time.Equals(other.Time); + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + return Equals((NullableTimeOnlyDto)obj); + } + public override int GetHashCode() => (Time ?? default).GetHashCode(); +} + +public class Net6TypeTests +{ + [Test] + public void Can_serialize_DateOnly() + { + var date = new DateOnly(2001, 1, 13); + var json = date.ToJson(); + Assert.That(json, Is.EqualTo("\"2001-01-13\"")); + + var fromJson = json.FromJson(); + Assert.That(fromJson, Is.EqualTo(date)); + + var dto = new DateOnlyDto { Date = date }; + json = dto.ToJson(); + Assert.That(json, Is.EqualTo("{\"Date\":\"2001-01-13\"}")); + var fromJsonDto = json.FromJson(); + Assert.That(fromJsonDto, Is.EqualTo(dto)); + + var nullableDto = new NullableDateOnlyDto { Date = date }; + json = nullableDto.ToJson(); + Assert.That(json, Is.EqualTo("{\"Date\":\"2001-01-13\"}")); + var fromJsonNullableDto = json.FromJson(); + Assert.That(fromJsonNullableDto, Is.EqualTo(nullableDto)); + } + + [Test] + public void Can_serialize_DateOnly_UnixTime() + { + using (JsConfig.With(new Config { DateHandler = DateHandler.UnixTime })) + { + var date = new DateOnly(2001, 1, 13); + var json = date.ToJson(); + Assert.That(json, Is.EqualTo("979344000")); + + var fromJson = json.FromJson(); + Assert.That(fromJson, Is.EqualTo(date)); + + var dto = new DateOnlyDto { Date = date }; + json = dto.ToJson(); + Assert.That(json, Is.EqualTo("{\"Date\":979344000}")); + + var nullableDto = new NullableDateOnlyDto { Date = date }; + json = nullableDto.ToJson(); + Assert.That(json, Is.EqualTo("{\"Date\":979344000}")); + } + } + + [Test] + public void Can_serialize_TimeOnly() + { + var time = new TimeOnly(13, 13, 13); + var json = time.ToJson(); + Assert.That(json, Is.EqualTo("\"PT13H13M13S\"")); + + var fromJson = json.FromJson(); + Assert.That(fromJson, Is.EqualTo(time)); + + var dto = new TimeOnlyDto { Time = time }; + json = dto.ToJson(); + Assert.That(json, Is.EqualTo("{\"Time\":\"PT13H13M13S\"}")); + var fromJsonDto = json.FromJson(); + Assert.That(fromJsonDto, Is.EqualTo(dto)); + + var nullableDto = new NullableTimeOnlyDto { Time = time }; + json = nullableDto.ToJson(); + Assert.That(json, Is.EqualTo("{\"Time\":\"PT13H13M13S\"}")); + var fromJsonNullableDto = json.FromJson(); + Assert.That(fromJsonNullableDto, Is.EqualTo(nullableDto)); + } + +} +#endif \ No newline at end of file diff --git a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj index 7092dec2b..f50ae1ca7 100644 --- a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj +++ b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj @@ -23,9 +23,9 @@ - - - + + + diff --git a/tests/ServiceStack.Text.TestsConsole/ServiceStack.Text.TestsConsole.csproj b/tests/ServiceStack.Text.TestsConsole/ServiceStack.Text.TestsConsole.csproj index 3aba157f5..7c573aa4a 100644 --- a/tests/ServiceStack.Text.TestsConsole/ServiceStack.Text.TestsConsole.csproj +++ b/tests/ServiceStack.Text.TestsConsole/ServiceStack.Text.TestsConsole.csproj @@ -1,7 +1,7 @@  Exe - net5.0 + net6.0 From 28faf27b542c4875940586deda7b8e57759ba808 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Wed, 10 Nov 2021 20:42:57 +0800 Subject: [PATCH 09/75] Upgrade ServiceStack.Memory to net6.0 --- src/ServiceStack.Memory/ServiceStack.Memory.csproj | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ServiceStack.Memory/ServiceStack.Memory.csproj b/src/ServiceStack.Memory/ServiceStack.Memory.csproj index 2cc2a99a7..d5efec16f 100644 --- a/src/ServiceStack.Memory/ServiceStack.Memory.csproj +++ b/src/ServiceStack.Memory/ServiceStack.Memory.csproj @@ -1,10 +1,13 @@  - netcoreapp2.1 + netcoreapp2.1;net6.0 $(DefineConstants);NETSTANDARD;NETCORE;NETCORE2_1 + + $(DefineConstants);NETSTANDARD;NETCORE;NET6_0 + From 7c6f5c37590e30ee0215fef74b93b5b1a39e63cd Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Thu, 11 Nov 2021 20:31:56 +0800 Subject: [PATCH 10/75] Add JSV tests for new DateOnly TimeOnly .NET 6 types --- .../ServiceStack.Text.Tests/Net6TypeTests.cs | 74 ++++++++++++++++++- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/tests/ServiceStack.Text.Tests/Net6TypeTests.cs b/tests/ServiceStack.Text.Tests/Net6TypeTests.cs index e3a097dad..c9187b43f 100644 --- a/tests/ServiceStack.Text.Tests/Net6TypeTests.cs +++ b/tests/ServiceStack.Text.Tests/Net6TypeTests.cs @@ -67,7 +67,7 @@ public override bool Equals(object obj) public class Net6TypeTests { [Test] - public void Can_serialize_DateOnly() + public void Can_json_serialize_DateOnly() { var date = new DateOnly(2001, 1, 13); var json = date.ToJson(); @@ -90,7 +90,30 @@ public void Can_serialize_DateOnly() } [Test] - public void Can_serialize_DateOnly_UnixTime() + public void Can_jsv_serialize_DateOnly() + { + var date = new DateOnly(2001, 1, 13); + var json = date.ToJsv(); + Assert.That(json, Is.EqualTo("2001-01-13")); + + var fromJson = json.FromJsv(); + Assert.That(fromJson, Is.EqualTo(date)); + + var dto = new DateOnlyDto { Date = date }; + json = dto.ToJsv(); + Assert.That(json, Is.EqualTo("{Date:2001-01-13}")); + var fromJsonDto = json.FromJsv(); + Assert.That(fromJsonDto, Is.EqualTo(dto)); + + var nullableDto = new NullableDateOnlyDto { Date = date }; + json = nullableDto.ToJsv(); + Assert.That(json, Is.EqualTo("{Date:2001-01-13}")); + var fromJsonNullableDto = json.FromJsv(); + Assert.That(fromJsonNullableDto, Is.EqualTo(nullableDto)); + } + + [Test] + public void Can_json_serialize_DateOnly_UnixTime() { using (JsConfig.With(new Config { DateHandler = DateHandler.UnixTime })) { @@ -110,9 +133,31 @@ public void Can_serialize_DateOnly_UnixTime() Assert.That(json, Is.EqualTo("{\"Date\":979344000}")); } } + + [Test] + public void Can_jsv_serialize_DateOnly_UnixTime() + { + using (JsConfig.With(new Config { DateHandler = DateHandler.UnixTime })) + { + var date = new DateOnly(2001, 1, 13); + var json = date.ToJsv(); + Assert.That(json, Is.EqualTo("979344000")); + + var fromJson = json.FromJsv(); + Assert.That(fromJson, Is.EqualTo(date)); + + var dto = new DateOnlyDto { Date = date }; + json = dto.ToJsv(); + Assert.That(json, Is.EqualTo("{Date:979344000}")); + + var nullableDto = new NullableDateOnlyDto { Date = date }; + json = nullableDto.ToJsv(); + Assert.That(json, Is.EqualTo("{Date:979344000}")); + } + } [Test] - public void Can_serialize_TimeOnly() + public void Can_json_serialize_TimeOnly() { var time = new TimeOnly(13, 13, 13); var json = time.ToJson(); @@ -133,6 +178,29 @@ public void Can_serialize_TimeOnly() var fromJsonNullableDto = json.FromJson(); Assert.That(fromJsonNullableDto, Is.EqualTo(nullableDto)); } + + [Test] + public void Can_jsv_serialize_TimeOnly() + { + var time = new TimeOnly(13, 13, 13); + var json = time.ToJsv(); + Assert.That(json, Is.EqualTo("PT13H13M13S")); + + var fromJson = json.FromJsv(); + Assert.That(fromJson, Is.EqualTo(time)); + + var dto = new TimeOnlyDto { Time = time }; + json = dto.ToJsv(); + Assert.That(json, Is.EqualTo("{Time:PT13H13M13S}")); + var fromJsonDto = json.FromJsv(); + Assert.That(fromJsonDto, Is.EqualTo(dto)); + + var nullableDto = new NullableTimeOnlyDto { Time = time }; + json = nullableDto.ToJsv(); + Assert.That(json, Is.EqualTo("{Time:PT13H13M13S}")); + var fromJsonNullableDto = json.FromJsv(); + Assert.That(fromJsonNullableDto, Is.EqualTo(nullableDto)); + } } #endif \ No newline at end of file From ed60b5e1ef14b51839c311134e9391b754af8898 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Fri, 12 Nov 2021 11:47:36 +0800 Subject: [PATCH 11/75] Remove ServiceStack.Memory/obj in builds --- build/build-core.proj | 1 + build/build-deps.proj | 1 + build/build.proj | 1 + 3 files changed, 3 insertions(+) diff --git a/build/build-core.proj b/build/build-core.proj index ae2482002..bc499908e 100644 --- a/build/build-core.proj +++ b/build/build-core.proj @@ -41,6 +41,7 @@ + diff --git a/build/build-deps.proj b/build/build-deps.proj index 127763cae..5f5663b97 100644 --- a/build/build-deps.proj +++ b/build/build-deps.proj @@ -39,6 +39,7 @@ + diff --git a/build/build.proj b/build/build.proj index 1996c2f91..870a5ebbc 100644 --- a/build/build.proj +++ b/build/build.proj @@ -46,6 +46,7 @@ + From 4b43ecb7b971c518cdb7e9da73ef1a84dfe0ccc8 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Fri, 12 Nov 2021 12:06:30 +0800 Subject: [PATCH 12/75] bump v5.13.1 --- src/Directory.Build.props | 2 +- src/ServiceStack.Text/Env.cs | 7 ++++--- tests/Directory.Build.props | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 7bc3a2084..5a4855052 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - 5.13.0 + 5.13.1 ServiceStack ServiceStack, Inc. © 2008-2022 ServiceStack, Inc diff --git a/src/ServiceStack.Text/Env.cs b/src/ServiceStack.Text/Env.cs index b0a6a6eda..db1dd1468 100644 --- a/src/ServiceStack.Text/Env.cs +++ b/src/ServiceStack.Text/Env.cs @@ -108,14 +108,15 @@ static Env() + PclExport.Instance.PlatformName + (IsMono ? "/Mono" : "") + (IsLinux ? "/Linux" : IsOSX ? "/OSX" : IsUnix ? "/Unix" : IsWindows ? "/Windows" : "/UnknownOS") - + (IsIOS ? "/iOS" : IsAndroid ? "/Android" : IsUWP ? "/UWP" : ""); + + (IsIOS ? "/iOS" : IsAndroid ? "/Android" : IsUWP ? "/UWP" : "") + + (IsNet6 ? "/net6" : IsNetStandard20 ? "/std2.0" : IsNetFramework ? "netfx" : ""); __releaseDate = new DateTime(2001,01,01); } public static string VersionString { get; set; } - public static decimal ServiceStackVersion = 5.130m; + public static decimal ServiceStackVersion = 5.131m; public static bool IsLinux { get; set; } @@ -271,7 +272,7 @@ private static bool IsInAppContainer break; case 0: // ERROR_SUCCESS case 122: // ERROR_INSUFFICIENT_BUFFER - // Success is actually insufficent buffer as we're really only looking for + // Success is actually insufficient buffer as we're really only looking for // not NO_APPLICATION and we're not actually giving a buffer here. The // API will always return NO_APPLICATION if we're not running under a // WinRT process, no matter what size the buffer is. diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index cd75a1be6..0e785d9a3 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -1,7 +1,7 @@ - 5.13.0 + 5.13.1 latest false From 6a3e924921c341d1b5d1e4dc880c81634e3c88bb Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Fri, 12 Nov 2021 13:06:57 +0800 Subject: [PATCH 13/75] clear test folders --- build/build-core.proj | 1 + build/build-deps.proj | 1 + build/build.proj | 3 +++ 3 files changed, 5 insertions(+) diff --git a/build/build-core.proj b/build/build-core.proj index bc499908e..395a22659 100644 --- a/build/build-core.proj +++ b/build/build-core.proj @@ -42,6 +42,7 @@ + diff --git a/build/build-deps.proj b/build/build-deps.proj index 5f5663b97..ca0f8332f 100644 --- a/build/build-deps.proj +++ b/build/build-deps.proj @@ -40,6 +40,7 @@ + diff --git a/build/build.proj b/build/build.proj index 870a5ebbc..247a5d6ca 100644 --- a/build/build.proj +++ b/build/build.proj @@ -12,6 +12,7 @@ $(MSBuildProjectDirectory)/.. $(BuildSolutionDir)/src + $(BuildSolutionDir)/tests Release $(BuildSolutionDir)/NuGet/ $(MajorVersion).$(MinorVersion).$(PatchVersion) @@ -47,6 +48,8 @@ + + From cb3bd5cab3fcf71e0c44a22628f92d020f904acc Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Sat, 20 Nov 2021 14:10:43 +0800 Subject: [PATCH 14/75] bump to v5.13.2 --- src/Directory.Build.props | 2 +- src/ServiceStack.Text/Env.cs | 2 +- tests/Directory.Build.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 5a4855052..a80436bb5 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - 5.13.1 + 5.13.2 ServiceStack ServiceStack, Inc. © 2008-2022 ServiceStack, Inc diff --git a/src/ServiceStack.Text/Env.cs b/src/ServiceStack.Text/Env.cs index db1dd1468..5b79611fc 100644 --- a/src/ServiceStack.Text/Env.cs +++ b/src/ServiceStack.Text/Env.cs @@ -116,7 +116,7 @@ static Env() public static string VersionString { get; set; } - public static decimal ServiceStackVersion = 5.131m; + public static decimal ServiceStackVersion = 5.132m; public static bool IsLinux { get; set; } diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 0e785d9a3..4c31d3ee8 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -1,7 +1,7 @@ - 5.13.1 + 5.13.2 latest false From beecf371b6532f570d708ed233f908b3c5ff5e3d Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Sat, 20 Nov 2021 16:06:40 +0800 Subject: [PATCH 15/75] bump to v5.13.3 --- src/Directory.Build.props | 2 +- src/ServiceStack.Text/Env.cs | 2 +- tests/Directory.Build.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a80436bb5..a0f23e7be 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - 5.13.2 + 5.13.3 ServiceStack ServiceStack, Inc. © 2008-2022 ServiceStack, Inc diff --git a/src/ServiceStack.Text/Env.cs b/src/ServiceStack.Text/Env.cs index 5b79611fc..0a1ee1a27 100644 --- a/src/ServiceStack.Text/Env.cs +++ b/src/ServiceStack.Text/Env.cs @@ -116,7 +116,7 @@ static Env() public static string VersionString { get; set; } - public static decimal ServiceStackVersion = 5.132m; + public static decimal ServiceStackVersion = 5.133m; public static bool IsLinux { get; set; } diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 4c31d3ee8..bbeaaa709 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -1,7 +1,7 @@ - 5.13.2 + 5.13.3 latest false From c6898dffff3dfcca60b1db5f207cf1ca0d3018f3 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 29 Nov 2021 18:25:42 +0800 Subject: [PATCH 16/75] Update .Source.csproj's to only netstandard/net6 --- global.json | 5 ----- src/ServiceStack.Text/ServiceStack.Text.csproj | 11 ++--------- 2 files changed, 2 insertions(+), 14 deletions(-) delete mode 100644 global.json diff --git a/global.json b/global.json deleted file mode 100644 index 550e1dfc7..000000000 --- a/global.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "sdk": { - "version": "6.0.100-rc.2.21505.57" - } -} \ No newline at end of file diff --git a/src/ServiceStack.Text/ServiceStack.Text.csproj b/src/ServiceStack.Text/ServiceStack.Text.csproj index a874505fa..8af1d0f91 100644 --- a/src/ServiceStack.Text/ServiceStack.Text.csproj +++ b/src/ServiceStack.Text/ServiceStack.Text.csproj @@ -2,8 +2,7 @@ ServiceStack.Text ServiceStack.Text - - net45;netstandard2.0;netcoreapp2.1;net6.0 + netstandard2.0;netcoreapp2.1;net6.0 .NET's fastest JSON Serializer by ServiceStack .NET's fastest JSON, JSV and CSV Text Serializers. Fast, Light, Resilient. @@ -15,19 +14,13 @@ $(DefineConstants);NETCORE;NETCORE2_1 - - $(DefineConstants);NETCORE;NETSTANDARD;NETSTANDARD2_0 - - $(DefineConstants);NETCORE;NET6_0;NET6_0_OR_GREATER + $(DefineConstants);NETSTANDARD;NET6_0 - - - From 15a9d708d72e4f8c52a0d43e1b14abe4c7cbc0ac Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Tue, 30 Nov 2021 03:52:24 +0800 Subject: [PATCH 17/75] remove xml doc warning --- src/ServiceStack.Text/ServiceStack.Text.Core.csproj | 1 + src/ServiceStack.Text/ServiceStack.Text.csproj | 1 + tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj | 1 + 3 files changed, 3 insertions(+) diff --git a/src/ServiceStack.Text/ServiceStack.Text.Core.csproj b/src/ServiceStack.Text/ServiceStack.Text.Core.csproj index 0d528ff6b..f0cd48f9e 100644 --- a/src/ServiceStack.Text/ServiceStack.Text.Core.csproj +++ b/src/ServiceStack.Text/ServiceStack.Text.Core.csproj @@ -12,6 +12,7 @@ https://github.com/ServiceStack/ServiceStack.Text JSON;Text;Serializer;CSV;JSV;HTTP;Auto Mapping;Dump;Reflection;JS;Utils;Fast + 1591 $(DefineConstants);NETCORE;NETCORE2_1 diff --git a/src/ServiceStack.Text/ServiceStack.Text.csproj b/src/ServiceStack.Text/ServiceStack.Text.csproj index 8af1d0f91..564226165 100644 --- a/src/ServiceStack.Text/ServiceStack.Text.csproj +++ b/src/ServiceStack.Text/ServiceStack.Text.csproj @@ -10,6 +10,7 @@ https://github.com/ServiceStack/ServiceStack.Text JSON;Text;Serializer;CSV;JSV;HTTP;Auto Mapping;Dump;Reflection;JS;Utils;Fast + 1591 $(DefineConstants);NETCORE;NETCORE2_1 diff --git a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj index f50ae1ca7..c1c8de7e1 100644 --- a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj +++ b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj @@ -14,6 +14,7 @@ false false latest + 1591 Full From 52082aeb2ae7b619afd4fd3a38a0eec5c090e9dd Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Tue, 30 Nov 2021 03:59:02 +0800 Subject: [PATCH 18/75] restore .csproj --- src/ServiceStack.Text/ServiceStack.Text.csproj | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/ServiceStack.Text/ServiceStack.Text.csproj b/src/ServiceStack.Text/ServiceStack.Text.csproj index 564226165..ef46d4470 100644 --- a/src/ServiceStack.Text/ServiceStack.Text.csproj +++ b/src/ServiceStack.Text/ServiceStack.Text.csproj @@ -2,7 +2,7 @@ ServiceStack.Text ServiceStack.Text - netstandard2.0;netcoreapp2.1;net6.0 + net45;netstandard2.0;netcoreapp2.1;net6.0 .NET's fastest JSON Serializer by ServiceStack .NET's fastest JSON, JSV and CSV Text Serializers. Fast, Light, Resilient. @@ -12,16 +12,22 @@ JSON;Text;Serializer;CSV;JSV;HTTP;Auto Mapping;Dump;Reflection;JS;Utils;Fast 1591 + + $(DefineConstants);NETCORE;NETSTANDARD;NETSTANDARD2_0 + $(DefineConstants);NETCORE;NETCORE2_1 - $(DefineConstants);NETSTANDARD;NET6_0 + $(DefineConstants);NETCORE;NET6_0;NET6_0_OR_GREATER + + + From 52f79076158b2a0129325f5061e4ca7cf5ef4647 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Tue, 28 Dec 2021 21:49:28 +0800 Subject: [PATCH 19/75] Add new options --- src/ServiceStack.Text/Env.cs | 3 +- src/ServiceStack.Text/LicenseUtils.cs | 169 +++++++++++++++++++++++++- 2 files changed, 170 insertions(+), 2 deletions(-) diff --git a/src/ServiceStack.Text/Env.cs b/src/ServiceStack.Text/Env.cs index 0a1ee1a27..b65466d03 100644 --- a/src/ServiceStack.Text/Env.cs +++ b/src/ServiceStack.Text/Env.cs @@ -109,7 +109,8 @@ static Env() + (IsMono ? "/Mono" : "") + (IsLinux ? "/Linux" : IsOSX ? "/OSX" : IsUnix ? "/Unix" : IsWindows ? "/Windows" : "/UnknownOS") + (IsIOS ? "/iOS" : IsAndroid ? "/Android" : IsUWP ? "/UWP" : "") - + (IsNet6 ? "/net6" : IsNetStandard20 ? "/std2.0" : IsNetFramework ? "netfx" : ""); + + (IsNet6 ? "/net6" : IsNetStandard20 ? "/std2.0" : IsNetFramework ? "netfx" : "") + + ($"/{LicenseUtils.Info}"); __releaseDate = new DateTime(2001,01,01); } diff --git a/src/ServiceStack.Text/LicenseUtils.cs b/src/ServiceStack.Text/LicenseUtils.cs index ddc1d2ccb..e673e97eb 100644 --- a/src/ServiceStack.Text/LicenseUtils.cs +++ b/src/ServiceStack.Text/LicenseUtils.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.IO; using System.Linq; @@ -22,6 +23,8 @@ public LicenseException(string message, Exception innerException) : base(message public enum LicenseType { Free, + FreeIndividual, + FreeOpenSource, Indie, Business, Enterprise, @@ -210,6 +213,12 @@ public static void RegisterLicense(string licenseKeyText) Thread.CurrentThread.CurrentCulture = CultureInfo.InvariantCulture; try { + if (IsFreeLicenseKey(licenseKeyText)) + { + ValidateFreeLicenseKey(licenseKeyText); + return; + } + var parts = licenseKeyText.SplitOnFirst('-'); subId = parts[0]; @@ -227,7 +236,7 @@ public static void RegisterLicense(string licenseKeyText) catch (Exception ex) { //bubble unrelated project Exceptions - if (ex is FileNotFoundException || ex is FileLoadException || ex is BadImageFormatException) + if (ex is FileNotFoundException || ex is FileLoadException || ex is BadImageFormatException || ex is NotSupportedException) throw; if (ex is LicenseException) @@ -277,6 +286,162 @@ private static void ValidateLicenseKey(LicenseKey key) if (LicenseWarningMessage != null) Console.WriteLine(LicenseWarningMessage); } + + private const string IndividualPrefix = "Individual (c) "; + private const string OpenSourcePrefix = "OSS "; + + private static bool IsFreeLicenseKey(string licenseText) => + licenseText.StartsWith(IndividualPrefix) || licenseText.StartsWith(OpenSourcePrefix); + + private static void ValidateFreeLicenseKey(string licenseText) + { + if (!IsFreeLicenseKey(licenseText)) + throw new NotSupportedException("Not a free License Key"); + + var envKey = Environment.GetEnvironmentVariable("SERVICESTACK_LICENSE"); + if (envKey == licenseText) + throw new LicenseException("Cannot use SERVICESTACK_LICENSE Environment variable with free License Keys, " + + "please use Licensing.RegisterLicense() in source code."); + + LicenseKey key = null; + if (licenseText.StartsWith(IndividualPrefix)) + { + key = VerifyIndividualLicense(licenseText); + if (key == null) + throw new LicenseException("Individual License Key is invalid."); + } + else if (licenseText.StartsWith(OpenSourcePrefix)) + { + key = VerifyOpenSourceLicense(licenseText); + if (key == null) + throw new LicenseException("Open Source License Key is invalid."); + } + else throw new NotSupportedException("Not a free License Key"); + + var releaseDate = Env.GetReleaseDate(); + if (releaseDate > key.Expiry) + throw new LicenseException($"This license has expired on {key.Expiry:d} and is not valid for use with this release.\n" + + "Check https://servicestack.net/free for eligible renewals.").Trace(); + + __activatedLicense = new __ActivatedLicense(key); + } + + internal static string Info => __activatedLicense?.LicenseKey == null + ? "NO" + : __activatedLicense.LicenseKey.Type switch { + LicenseType.Free => "FR", + LicenseType.FreeIndividual => "FI", + LicenseType.FreeOpenSource => "FO", + LicenseType.Indie => "IN", + LicenseType.Business => "BU", + LicenseType.Enterprise => "EN", + LicenseType.TextIndie => "TI", + LicenseType.TextBusiness => "TB", + LicenseType.OrmLiteIndie => "OI", + LicenseType.OrmLiteBusiness => "OB", + LicenseType.RedisIndie => "RI", + LicenseType.RedisBusiness => "RB", + LicenseType.AwsIndie => "AI", + LicenseType.AwsBusiness => "AB", + LicenseType.Trial => "TR", + LicenseType.Site => "SI", + LicenseType.TextSite => "TS", + LicenseType.RedisSite => "RS", + LicenseType.OrmLiteSite => "OS", + _ => "UN", + }; + + private static LicenseKey VerifyIndividualLicense(string licenseKey) + { + if (licenseKey == null) + return null; + if (licenseKey.Length < 100) + return null; + if (!licenseKey.StartsWith(IndividualPrefix)) + return null; + var keyText = licenseKey.LastLeftPart(' '); + var keySign = licenseKey.LastRightPart(' '); + if (keySign.Length < 48) + return null; + + try + { + var rsa = System.Security.Cryptography.RSA.Create(); + rsa.FromXml(LicensePublicKey); + +#if !NETCORE + var verified = ((System.Security.Cryptography.RSACryptoServiceProvider)rsa) + .VerifyData(keyText.ToUtf8Bytes(), "SHA256", Convert.FromBase64String(keySign)); +#else + var verified = rsa.VerifyData(keyText.ToUtf8Bytes(), + Convert.FromBase64String(keySign), + System.Security.Cryptography.HashAlgorithmName.SHA256, + System.Security.Cryptography.RSASignaturePadding.Pkcs1); +#endif + if (verified) + { + var yearStr = keyText.Substring(IndividualPrefix.Length).LeftPart(' '); + if (yearStr.Length == 4 && int.TryParse(yearStr, out var year)) + { + return new LicenseKey { + Expiry = new DateTime(year + 1, 1, 1, 0, 0, 0, DateTimeKind.Utc), + Hash = keySign, + Name = keyText, + Type = LicenseType.FreeIndividual, + }; + } + } + } + catch { } + + return null; + } + + private static LicenseKey VerifyOpenSourceLicense(string licenseKey) + { + if (licenseKey == null) + return null; + if (licenseKey.Length < 100) + return null; + if (!licenseKey.StartsWith(OpenSourcePrefix)) + return null; + var keyText = licenseKey.LastLeftPart(' '); + var keySign = licenseKey.LastRightPart(' '); + if (keySign.Length < 48) + return null; + + try + { + var rsa = System.Security.Cryptography.RSA.Create(); + rsa.FromXml(LicensePublicKey); + +#if !NETCORE + var verified = ((System.Security.Cryptography.RSACryptoServiceProvider)rsa) + .VerifyData(keyText.ToUtf8Bytes(), "SHA256", Convert.FromBase64String(keySign)); +#else + var verified = rsa.VerifyData(keyText.ToUtf8Bytes(), + Convert.FromBase64String(keySign), + System.Security.Cryptography.HashAlgorithmName.SHA256, + System.Security.Cryptography.RSASignaturePadding.Pkcs1); +#endif + if (verified) + { + var yearStr = keyText.Substring(OpenSourcePrefix.Length).RightPart(' ').LeftPart(' '); + if (yearStr.Length == 4 && int.TryParse(yearStr, out var year)) + { + return new LicenseKey { + Expiry = new DateTime(year + 1, 1, 1, 0, 0, 0, DateTimeKind.Utc), + Hash = keySign, + Name = keyText, + Type = LicenseType.FreeOpenSource, + }; + } + } + } + catch { } + + return null; + } public static void RemoveLicense() { @@ -382,6 +547,8 @@ public static LicenseFeature GetLicensedFeatures(this LicenseKey key) case LicenseType.Free: return LicenseFeature.Free; + case LicenseType.FreeIndividual: + case LicenseType.FreeOpenSource: case LicenseType.Indie: case LicenseType.Business: case LicenseType.Enterprise: From 1c868c7ec0e82180c799bcccfd67729d32303957 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Sat, 1 Jan 2022 08:25:02 +0800 Subject: [PATCH 20/75] Add scope overloads to ToJson/ToJsv/ToCsv ext methods --- src/ServiceStack.Text/StringExtensions.cs | 30 +++++++++++++++++++ .../ServiceStack.Text.Tests/JsConfigTests.cs | 11 +++++++ 2 files changed, 41 insertions(+) diff --git a/src/ServiceStack.Text/StringExtensions.cs b/src/ServiceStack.Text/StringExtensions.cs index 2066ff4f9..79206d0b0 100644 --- a/src/ServiceStack.Text/StringExtensions.cs +++ b/src/ServiceStack.Text/StringExtensions.cs @@ -517,6 +517,16 @@ public static string ToJsv(this T obj) return TypeSerializer.SerializeToString(obj); } + public static string ToJsv(this T obj, Action configure) + { + var config = new Config(); + configure(config); + using (JsConfig.With(config)) + { + return ToJsv(obj); + } + } + public static string ToSafeJsv(this T obj) { return TypeSerializer.HasCircularReferences(obj) @@ -533,6 +543,16 @@ public static T FromJsvSpan(this ReadOnlySpan jsv) { return TypeSerializer.DeserializeFromSpan(jsv); } + + public static string ToJson(this T obj, Action configure) + { + var config = new Config(); + configure(config); + using (JsConfig.With(config)) + { + return ToJson(obj); + } + } public static string ToJson(this T obj) { @@ -563,6 +583,16 @@ public static string ToCsv(this T obj) return CsvSerializer.SerializeToString(obj); } + public static string ToCsv(this T obj, Action configure) + { + var config = new Config(); + configure(config); + using (JsConfig.With(config)) + { + return ToCsv(obj); + } + } + public static T FromCsv(this string csv) { return CsvSerializer.DeserializeFromString(csv); diff --git a/tests/ServiceStack.Text.Tests/JsConfigTests.cs b/tests/ServiceStack.Text.Tests/JsConfigTests.cs index b42d3d3b1..40868914b 100644 --- a/tests/ServiceStack.Text.Tests/JsConfigTests.cs +++ b/tests/ServiceStack.Text.Tests/JsConfigTests.cs @@ -75,6 +75,17 @@ public void TestJsonDataWithJsConfigScope() AssertObjectJson(); } + [Test] + public void TestJsonDataWithJsConfigScope_ext() + { + Assert.That(CreateObject().ToJson(config => config.TextCase = TextCase.SnakeCase), + Is.EqualTo("{\"id\":1,\"root_id\":100,\"display_name\":\"Test object\"}")); + Assert.That(CreateObject().ToJson(config => config.TextCase = TextCase.CamelCase), + Is.EqualTo("{\"id\":1,\"rootId\":100,\"displayName\":\"Test object\"}")); + Assert.That(CreateObject().ToJson(config => config.TextCase = TextCase.PascalCase), + Is.EqualTo("{\"Id\":1,\"RootId\":100,\"DisplayName\":\"Test object\"}")); + } + [Test] public void TestCloneObjectWithJsConfigScope() { From 11006a9e93d374fb600d5abbb0ea4feb0d9257a4 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 3 Jan 2022 01:45:02 +0800 Subject: [PATCH 21/75] Imply lenient deserialization when using snake_case --- .../Common/DeserializeTypeRefJson.cs | 3 ++- .../Common/DeserializeTypeRefJsv.cs | 2 +- src/ServiceStack.Text/TypeConfig.cs | 13 ++++++++++++- tests/ServiceStack.Text.Tests/JsConfigTests.cs | 1 - 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs b/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs index bd13ffa51..6960ddc2f 100644 --- a/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs +++ b/src/ServiceStack.Text/Common/DeserializeTypeRefJson.cs @@ -35,7 +35,8 @@ internal static object StringToType(ReadOnlySpan strType, var typeAttr = config.TypeAttrMemory; object instance = null; - var lenient = config.PropertyConvention == PropertyConvention.Lenient; + var textCase = typeConfig.TextCase.GetValueOrDefault(config.TextCase); + var lenient = config.PropertyConvention == PropertyConvention.Lenient || textCase == TextCase.SnakeCase; for (; index < strTypeLength; index++) { if (!JsonUtils.IsWhiteSpace(buffer[index])) break; } //Whitespace inline diff --git a/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs b/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs index aef14a9e5..2f6c57379 100644 --- a/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs +++ b/src/ServiceStack.Text/Common/DeserializeTypeRefJsv.cs @@ -32,7 +32,7 @@ internal static object StringToType(ReadOnlySpan strType, var config = JsConfig.GetConfig(); object instance = null; - var lenient = config.PropertyConvention == PropertyConvention.Lenient; + var lenient = config.PropertyConvention == PropertyConvention.Lenient || config.TextCase == TextCase.SnakeCase; var strTypeLength = strType.Length; while (index < strTypeLength) diff --git a/src/ServiceStack.Text/TypeConfig.cs b/src/ServiceStack.Text/TypeConfig.cs index d3a405a84..57585be3e 100644 --- a/src/ServiceStack.Text/TypeConfig.cs +++ b/src/ServiceStack.Text/TypeConfig.cs @@ -14,6 +14,15 @@ internal class TypeConfig internal FieldInfo[] Fields; internal Func OnDeserializing; internal bool IsUserType { get; set; } + internal Func TextCaseResolver; + internal TextCase? TextCase + { + get + { + var result = TextCaseResolver?.Invoke(); + return result is null or Text.TextCase.Default ? null : result; + } + } internal TypeConfig(Type type) { @@ -77,7 +86,9 @@ public static Func OnDeserializing static TypeConfig Create() { - config = new TypeConfig(typeof(T)); + config = new TypeConfig(typeof(T)) { + TextCaseResolver = () => JsConfig.TextCase + }; var excludedProperties = JsConfig.ExcludePropertyNames ?? TypeConstants.EmptyStringArray; diff --git a/tests/ServiceStack.Text.Tests/JsConfigTests.cs b/tests/ServiceStack.Text.Tests/JsConfigTests.cs index 40868914b..38c298e73 100644 --- a/tests/ServiceStack.Text.Tests/JsConfigTests.cs +++ b/tests/ServiceStack.Text.Tests/JsConfigTests.cs @@ -131,7 +131,6 @@ public void TestCloneObjectWithJsConfigLocal() { JsConfig.TextCase = TextCase.Default; JsConfig.TextCase = TextCase.SnakeCase; - JsConfig.PropertyConvention = PropertyConvention.Lenient; AssertObject(); From eef6e8f70081c4d5b68a8a909185960866ae443f Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Sat, 15 Jan 2022 17:49:30 +0800 Subject: [PATCH 22/75] Optimize MD5 Hash for .net6 --- src/ServiceStack.Text/StreamExtensions.cs | 28 +++++++++----------- tests/ServiceStack.Text.Tests/StreamTests.cs | 10 +++++++ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/ServiceStack.Text/StreamExtensions.cs b/src/ServiceStack.Text/StreamExtensions.cs index b622fadbd..6ea626c7d 100644 --- a/src/ServiceStack.Text/StreamExtensions.cs +++ b/src/ServiceStack.Text/StreamExtensions.cs @@ -446,27 +446,23 @@ public static Task WriteAsync(this Stream stream, byte[] bytes, CancellationToke public static Task WriteAsync(this Stream stream, string text, CancellationToken token = default) => MemoryProvider.Instance.WriteAsync(stream, text.AsSpan(), token); - public static string ToMd5Hash(this Stream stream) + public static byte[] ToMd5Bytes(this Stream stream) { - var hash = System.Security.Cryptography.MD5.Create().ComputeHash(stream); - var sb = StringBuilderCache.Allocate(); - foreach (byte b in hash) +#if NET6_0_OR_GREATER + if (stream is MemoryStream ms) + return System.Security.Cryptography.MD5.HashData(ms.GetBufferAsSpan()); +#endif + if (stream.CanSeek) { - sb.Append(b.ToString("x2")); + stream.Position = 0; } - return StringBuilderCache.ReturnAndFree(sb); + return System.Security.Cryptography.MD5.Create().ComputeHash(stream); } - public static string ToMd5Hash(this byte[] bytes) - { - var hash = System.Security.Cryptography.MD5.Create().ComputeHash(bytes); - var sb = StringBuilderCache.Allocate(); - foreach (byte b in hash) - { - sb.Append(b.ToString("x2")); - } - return StringBuilderCache.ReturnAndFree(sb); - } + public static string ToMd5Hash(this Stream stream) => ToMd5Bytes(stream).ToHex(); + + public static string ToMd5Hash(this byte[] bytes) => + System.Security.Cryptography.MD5.Create().ComputeHash(bytes).ToHex(); /// /// Returns bytes in publiclyVisible MemoryStream diff --git a/tests/ServiceStack.Text.Tests/StreamTests.cs b/tests/ServiceStack.Text.Tests/StreamTests.cs index 63567c8b7..006e0c138 100644 --- a/tests/ServiceStack.Text.Tests/StreamTests.cs +++ b/tests/ServiceStack.Text.Tests/StreamTests.cs @@ -52,5 +52,15 @@ public void Does_escape_string_when_serializing_to_Stream() Assert.That(ssString, Is.EqualTo(expected)); } } + + [Test] + public void Can_create_MD5_hashes_from_Stream() + { + var md5Hash = "35f184b0e35d7f5629e79cb4bc802893"; + var utf8Bytes = nameof(Can_create_MD5_hashes_from_Stream).ToUtf8Bytes(); + var ms = new MemoryStream(utf8Bytes); + Assert.That(utf8Bytes.ToMd5Hash(), Is.EqualTo(md5Hash)); + Assert.That(ms.ToMd5Bytes().ToHex(), Is.EqualTo(md5Hash)); + } } } \ No newline at end of file From 9b60998db844b7d49425bcef0384a0648b763adc Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Sun, 16 Jan 2022 10:25:07 +0800 Subject: [PATCH 23/75] Add Brotli file compression --- src/ServiceStack.Text/HttpUtils.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.cs b/src/ServiceStack.Text/HttpUtils.cs index 39ce5bca6..de6546aa3 100644 --- a/src/ServiceStack.Text/HttpUtils.cs +++ b/src/ServiceStack.Text/HttpUtils.cs @@ -2006,15 +2006,16 @@ public static class HttpMethods public static class CompressionTypes { - public static readonly string[] AllCompressionTypes = new[] { Deflate, GZip }; + public static readonly string[] AllCompressionTypes = { Deflate, GZip }; public const string Default = Deflate; + public const string Brotli = "br"; public const string Deflate = "deflate"; public const string GZip = "gzip"; public static bool IsValid(string compressionType) { - return compressionType == Deflate || compressionType == GZip; + return compressionType is Deflate or GZip or Brotli; } public static void AssertIsValid(string compressionType) @@ -2022,7 +2023,7 @@ public static void AssertIsValid(string compressionType) if (!IsValid(compressionType)) { throw new NotSupportedException(compressionType - + " is not a supported compression type. Valid types: gzip, deflate."); + + " is not a supported compression type. Valid types: gzip, deflate, br."); } } @@ -2030,6 +2031,7 @@ public static string GetExtension(string compressionType) { switch (compressionType) { + case Brotli: case Deflate: case GZip: return "." + compressionType; From 1994e93a61a3ab6b814952ccf53604df8564423e Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Sun, 16 Jan 2022 10:28:42 +0800 Subject: [PATCH 24/75] Only export brotli compression in net6 --- src/ServiceStack.Text/HttpUtils.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.cs b/src/ServiceStack.Text/HttpUtils.cs index de6546aa3..3b58c003e 100644 --- a/src/ServiceStack.Text/HttpUtils.cs +++ b/src/ServiceStack.Text/HttpUtils.cs @@ -2006,16 +2006,29 @@ public static class HttpMethods public static class CompressionTypes { - public static readonly string[] AllCompressionTypes = { Deflate, GZip }; + public static readonly string[] AllCompressionTypes = + { +#if NET6_0_OR_GREATER + Brotli, +#endif + Deflate, + GZip, + }; public const string Default = Deflate; +#if NET6_0_OR_GREATER public const string Brotli = "br"; +#endif public const string Deflate = "deflate"; public const string GZip = "gzip"; public static bool IsValid(string compressionType) { - return compressionType is Deflate or GZip or Brotli; + return compressionType is Deflate or GZip +#if NET6_0_OR_GREATER + or Brotli +#endif + ; } public static void AssertIsValid(string compressionType) @@ -2031,7 +2044,9 @@ public static string GetExtension(string compressionType) { switch (compressionType) { +#if NET6_0_OR_GREATER case Brotli: +#endif case Deflate: case GZip: return "." + compressionType; From 11fb0d3c8c94ddf7d5ebfe7dbbaffe7e58ffbea8 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Sun, 16 Jan 2022 10:30:24 +0800 Subject: [PATCH 25/75] ok to all Brotli symbols --- src/ServiceStack.Text/HttpUtils.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.cs b/src/ServiceStack.Text/HttpUtils.cs index 3b58c003e..b672b575a 100644 --- a/src/ServiceStack.Text/HttpUtils.cs +++ b/src/ServiceStack.Text/HttpUtils.cs @@ -2016,9 +2016,7 @@ public static class CompressionTypes }; public const string Default = Deflate; -#if NET6_0_OR_GREATER public const string Brotli = "br"; -#endif public const string Deflate = "deflate"; public const string GZip = "gzip"; @@ -2044,9 +2042,7 @@ public static string GetExtension(string compressionType) { switch (compressionType) { -#if NET6_0_OR_GREATER case Brotli: -#endif case Deflate: case GZip: return "." + compressionType; From 729f90b26d6c3c49a73756ccc300bcac0f083552 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Sun, 16 Jan 2022 12:05:51 +0800 Subject: [PATCH 26/75] Update HttpUtils.cs --- src/ServiceStack.Text/HttpUtils.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.cs b/src/ServiceStack.Text/HttpUtils.cs index b672b575a..c5c81e394 100644 --- a/src/ServiceStack.Text/HttpUtils.cs +++ b/src/ServiceStack.Text/HttpUtils.cs @@ -1375,7 +1375,7 @@ namespace ServiceStack { public static class MimeTypes { - public static Dictionary ExtensionMimeTypes = new Dictionary(); + public static Dictionary ExtensionMimeTypes = new(); public const string Utf8Suffix = "; charset=utf-8"; public const string Html = "text/html"; @@ -1957,7 +1957,7 @@ public static class HttpHeaders public const string Host = "Host"; public const string UserAgent = "User-Agent"; - public static HashSet RestrictedHeaders = new HashSet(StringComparer.OrdinalIgnoreCase) + public static HashSet RestrictedHeaders = new(StringComparer.OrdinalIgnoreCase) { Accept, Connection, @@ -1990,7 +1990,7 @@ public static class HttpMethods "POLL", "SUBSCRIBE", "UNSUBSCRIBE" //MS Exchange WebDav: http://msdn.microsoft.com/en-us/library/aa142917.aspx }; - public static HashSet AllVerbs = new HashSet(allVerbs); + public static HashSet AllVerbs = new(allVerbs); public static bool Exists(string httpMethod) => AllVerbs.Contains(httpMethod.ToUpper()); public static bool HasVerb(string httpVerb) => Exists(httpVerb); @@ -2034,7 +2034,7 @@ public static void AssertIsValid(string compressionType) if (!IsValid(compressionType)) { throw new NotSupportedException(compressionType - + " is not a supported compression type. Valid types: gzip, deflate, br."); + + " is not a supported compression type. Valid types: " + string.Join(", ", AllCompressionTypes)); } } @@ -2057,7 +2057,7 @@ public static class HttpStatus { public static string GetStatusDescription(int statusCode) { - if (statusCode >= 100 && statusCode < 600) + if (statusCode is >= 100 and < 600) { int i = statusCode / 100; int j = statusCode % 100; @@ -2069,7 +2069,7 @@ public static string GetStatusDescription(int statusCode) return string.Empty; } - private static readonly string[][] Descriptions = new string[][] + private static readonly string[][] Descriptions = { null, new[] From a17d6ca95e0f4fc50a9525df4c4c241a7c0b260e Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Sun, 16 Jan 2022 12:35:47 +0800 Subject: [PATCH 27/75] Change default Compression Type to Brotli if net6 --- src/ServiceStack.Text/HttpUtils.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.cs b/src/ServiceStack.Text/HttpUtils.cs index c5c81e394..2a0baac9a 100644 --- a/src/ServiceStack.Text/HttpUtils.cs +++ b/src/ServiceStack.Text/HttpUtils.cs @@ -2015,18 +2015,23 @@ public static class CompressionTypes GZip, }; +#if NET6_0_OR_GREATER + public const string Default = Brotli; +#else public const string Default = Deflate; +#endif + public const string Brotli = "br"; public const string Deflate = "deflate"; public const string GZip = "gzip"; public static bool IsValid(string compressionType) { - return compressionType is Deflate or GZip #if NET6_0_OR_GREATER - or Brotli + return compressionType is Deflate or GZip or Brotli; +#else + return compressionType is Deflate or GZip; #endif - ; } public static void AssertIsValid(string compressionType) From 0bab92cedfca97e0201c0f3159341348fafc99ab Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Sun, 23 Jan 2022 10:21:35 +0800 Subject: [PATCH 28/75] Improve MimeTypes.GetExtension() --- src/ServiceStack.Text/HttpUtils.cs | 17 ++++++++++------- tests/ServiceStack.Text.Tests/HttpUtilsTests.cs | 9 +++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.cs b/src/ServiceStack.Text/HttpUtils.cs index 2a0baac9a..786b94dbd 100644 --- a/src/ServiceStack.Text/HttpUtils.cs +++ b/src/ServiceStack.Text/HttpUtils.cs @@ -12,7 +12,12 @@ namespace ServiceStack { - public static class HttpUtils + public static partial class HttpUtils + { + + } + + public static partial class HttpUtils { public static string UserAgent = "ServiceStack.Text"; @@ -781,10 +786,8 @@ public static Stream SendStreamToUrl(this string url, string method = null, if (requestBody != null) { - using (var req = PclExport.Instance.GetRequestStream(webReq)) - { - requestBody.CopyTo(req); - } + using var req = PclExport.Instance.GetRequestStream(webReq); + requestBody.CopyTo(req); } var webRes = PclExport.Instance.GetResponse(webReq); @@ -1431,8 +1434,8 @@ public static string GetExtension(string mimeType) } var parts = mimeType.Split('/'); - if (parts.Length == 1) return "." + parts[0]; - if (parts.Length == 2) return "." + parts[1]; + if (parts.Length == 1) return "." + parts[0].LeftPart('+').LeftPart(';'); + if (parts.Length == 2) return "." + parts[1].LeftPart('+').LeftPart(';'); throw new NotSupportedException("Unknown mimeType: " + mimeType); } diff --git a/tests/ServiceStack.Text.Tests/HttpUtilsTests.cs b/tests/ServiceStack.Text.Tests/HttpUtilsTests.cs index 8e7f640e1..bf8e243fe 100644 --- a/tests/ServiceStack.Text.Tests/HttpUtilsTests.cs +++ b/tests/ServiceStack.Text.Tests/HttpUtilsTests.cs @@ -64,5 +64,14 @@ public void Can_SetHashParam() Assert.That("http://example.com#s=rf/f=1".SetHashParam("f", "2"), Is.EqualTo("http://example.com#s=rf/f=2")); Assert.That("http://example.com#ab=0".SetHashParam("a", "1"), Is.EqualTo("http://example.com#ab=0/a=1")); } + + [Test] + public void Can_get_MimeType_file_extension() + { + Assert.That(MimeTypes.GetExtension(MimeTypes.Html), Is.EqualTo(".html")); + Assert.That(MimeTypes.GetExtension(MimeTypes.HtmlUtf8), Is.EqualTo(".html")); + Assert.That(MimeTypes.GetExtension(MimeTypes.ImagePng), Is.EqualTo(".png")); + Assert.That(MimeTypes.GetExtension(MimeTypes.ImageSvg), Is.EqualTo(".svg")); + } } } \ No newline at end of file From e3f1f6fd092c28b9c5e0806f35ca30599dba52d4 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Sun, 23 Jan 2022 10:31:45 +0800 Subject: [PATCH 29/75] remove partial class --- src/ServiceStack.Text/HttpUtils.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.cs b/src/ServiceStack.Text/HttpUtils.cs index 786b94dbd..a5551f4a3 100644 --- a/src/ServiceStack.Text/HttpUtils.cs +++ b/src/ServiceStack.Text/HttpUtils.cs @@ -12,11 +12,6 @@ namespace ServiceStack { - public static partial class HttpUtils - { - - } - public static partial class HttpUtils { public static string UserAgent = "ServiceStack.Text"; From 6e2d35f3c3a6bade1bd1b0ac8e96b5c2133d9aec Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 24 Jan 2022 03:03:56 +0800 Subject: [PATCH 30/75] Upgrade to v15.4.0 --- src/Directory.Build.props | 2 +- src/ServiceStack.Text/Env.cs | 2 +- tests/Directory.Build.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a0f23e7be..a8a35f07b 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - 5.13.3 + 5.14.0 ServiceStack ServiceStack, Inc. © 2008-2022 ServiceStack, Inc diff --git a/src/ServiceStack.Text/Env.cs b/src/ServiceStack.Text/Env.cs index b65466d03..8ab386aa3 100644 --- a/src/ServiceStack.Text/Env.cs +++ b/src/ServiceStack.Text/Env.cs @@ -117,7 +117,7 @@ static Env() public static string VersionString { get; set; } - public static decimal ServiceStackVersion = 5.133m; + public static decimal ServiceStackVersion = 5.14m; public static bool IsLinux { get; set; } diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index bbeaaa709..02591eb88 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -1,7 +1,7 @@ - 5.13.3 + 5.14.0 latest false From ac60a4c0840c9d268de805f88de025e86703c8b8 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 24 Jan 2022 20:11:19 +0800 Subject: [PATCH 31/75] Remove legacy projects --- .../JsonSerializationBenchmarks.cs | 182 ---------- .../ParseBuiltinsBenchmarks.cs | 85 ----- .../ServiceStack.Text.Benchmarks/Program.cs | 24 -- .../ServiceStack.Text.Benchmarks.csproj | 30 -- .../JsonDeserializationBenchmarks.cs | 90 ----- .../MiniProfiler/ClientTiming.cs | 42 --- .../MiniProfiler/ClientTimings.cs | 172 --------- .../MiniProfiler/CustomTiming.cs | 127 ------- .../MiniProfiler/FlowData.cs | 52 --- .../MiniProfiler/MiniProfiler.cs | 341 ------------------ .../MiniProfiler/MiniProfilerExtensions.cs | 142 -------- .../MiniProfiler/StackTraceSnippet.cs | 78 ---- .../MiniProfiler/Timing.cs | 286 --------------- .../ModelWithCommonTypes.cs | 59 --- .../ParseBuiltinsBenchmarks.cs | 45 --- .../Program.cs | 35 -- ...t.VersionCompareBenchmarks.BaseLine.csproj | 36 -- ...Stack.Text.VersionCompareBenchmarks.csproj | 45 --- benchmarks/run-benchmarks.bat | 23 -- .../ServiceStack.Memory.csproj | 19 - 20 files changed, 1913 deletions(-) delete mode 100644 benchmarks/ServiceStack.Text.Benchmarks/JsonSerializationBenchmarks.cs delete mode 100644 benchmarks/ServiceStack.Text.Benchmarks/ParseBuiltinsBenchmarks.cs delete mode 100644 benchmarks/ServiceStack.Text.Benchmarks/Program.cs delete mode 100644 benchmarks/ServiceStack.Text.Benchmarks/ServiceStack.Text.Benchmarks.csproj delete mode 100644 benchmarks/ServiceStack.Text.VersionCompareBenchmarks/JsonDeserializationBenchmarks.cs delete mode 100644 benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/ClientTiming.cs delete mode 100644 benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/ClientTimings.cs delete mode 100644 benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/CustomTiming.cs delete mode 100644 benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/FlowData.cs delete mode 100644 benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/MiniProfiler.cs delete mode 100644 benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/MiniProfilerExtensions.cs delete mode 100644 benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/StackTraceSnippet.cs delete mode 100644 benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/Timing.cs delete mode 100644 benchmarks/ServiceStack.Text.VersionCompareBenchmarks/ModelWithCommonTypes.cs delete mode 100644 benchmarks/ServiceStack.Text.VersionCompareBenchmarks/ParseBuiltinsBenchmarks.cs delete mode 100644 benchmarks/ServiceStack.Text.VersionCompareBenchmarks/Program.cs delete mode 100644 benchmarks/ServiceStack.Text.VersionCompareBenchmarks/ServiceStack.Text.VersionCompareBenchmarks.BaseLine.csproj delete mode 100644 benchmarks/ServiceStack.Text.VersionCompareBenchmarks/ServiceStack.Text.VersionCompareBenchmarks.csproj delete mode 100644 benchmarks/run-benchmarks.bat delete mode 100644 src/ServiceStack.Memory/ServiceStack.Memory.csproj diff --git a/benchmarks/ServiceStack.Text.Benchmarks/JsonSerializationBenchmarks.cs b/benchmarks/ServiceStack.Text.Benchmarks/JsonSerializationBenchmarks.cs deleted file mode 100644 index 1bd650e82..000000000 --- a/benchmarks/ServiceStack.Text.Benchmarks/JsonSerializationBenchmarks.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System; -using System.IO; -using System.Text; -using BenchmarkDotNet.Attributes; -using ServiceStack.Text; -using ServiceStack.Text.Tests.DynamicModels; -using ServiceStack.Text.Json; - -namespace ServiceStack.Text.Benchmarks -{ - public class ModelWithCommonTypes - { - public char CharValue { get; set; } - - public byte ByteValue { get; set; } - - public sbyte SByteValue { get; set; } - - public short ShortValue { get; set; } - - public ushort UShortValue { get; set; } - - public int IntValue { get; set; } - - public uint UIntValue { get; set; } - - public long LongValue { get; set; } - - public ulong ULongValue { get; set; } - - public float FloatValue { get; set; } - - public double DoubleValue { get; set; } - - public decimal DecimalValue { get; set; } - - public DateTime DateTimeValue { get; set; } - - public TimeSpan TimeSpanValue { get; set; } - - public Guid GuidValue { get; set; } - - public static ModelWithCommonTypes Create(byte i) - { - return new ModelWithCommonTypes - { - ByteValue = i, - CharValue = (char)i, - DateTimeValue = new DateTime(2000, 1, 1 + i), - DecimalValue = i, - DoubleValue = i, - FloatValue = i, - IntValue = i, - LongValue = i, - SByteValue = (sbyte)i, - ShortValue = i, - TimeSpanValue = new TimeSpan(i), - UIntValue = i, - ULongValue = i, - UShortValue = i, - GuidValue = Guid.NewGuid(), - }; - } - } - - public class JsonSerializationBenchmarks - { - static ModelWithAllTypes allTypesModel = ModelWithAllTypes.Create(3); - static ModelWithCommonTypes commonTypesModel = ModelWithCommonTypes.Create(3); - static MemoryStream stream = new MemoryStream(32768); - const string serializedString = "this is the test string"; - readonly string serializedString256 = new string('t', 256); - readonly string serializedString512 = new string('t', 512); - readonly string serializedString4096 = new string('t', 4096); - - [Benchmark] - public void SerializeJsonAllTypes() - { - string result = JsonSerializer.SerializeToString(allTypesModel); - } - - [Benchmark] - public void SerializeJsonCommonTypes() - { - string result = JsonSerializer.SerializeToString(commonTypesModel); - } - - [Benchmark] - public void SerializeJsonString() - { - string result = JsonSerializer.SerializeToString(serializedString); - } - - [Benchmark] - public void SerializeJsonStringToStream() - { - stream.Position = 0; - JsonSerializer.SerializeToStream(serializedString, stream); - } - - [Benchmark] - public void SerializeJsonString256ToStream() - { - stream.Position = 0; - JsonSerializer.SerializeToStream(serializedString256, stream); - } - - [Benchmark] - public void SerializeJsonString512ToStream() - { - stream.Position = 0; - JsonSerializer.SerializeToStream(serializedString512, stream); - } - - [Benchmark] - public void SerializeJsonString4096ToStream() - { - stream.Position = 0; - JsonSerializer.SerializeToStream(serializedString4096, stream); - } - - [Benchmark] - public void SerializeJsonStringToStreamDirectly() - { - stream.Position = 0; - string tmp = JsonSerializer.SerializeToString(serializedString); - byte[] arr = Encoding.UTF8.GetBytes(tmp); - stream.Write(arr, 0, arr.Length); - } - - - [Benchmark] - public void SerializeJsonAllTypesToStream() - { - stream.Position = 0; - JsonSerializer.SerializeToStream(allTypesModel, stream); - } - - [Benchmark] - public void SerializeJsonCommonTypesToStream() - { - stream.Position = 0; - JsonSerializer.SerializeToStream(commonTypesModel, stream); - } - - [Benchmark] - public void SerializeJsonStringToStreamUsingDirectStreamWriter() - { - stream.Position = 0; - var writer = new DirectStreamWriter(stream, JsonSerializer.UTF8Encoding); - JsonWriter.WriteRootObject(writer, serializedString); - writer.Flush(); - } - - [Benchmark] - public void SerializeJsonString256ToStreamUsingDirectStreamWriter() - { - stream.Position = 0; - var writer = new DirectStreamWriter(stream, JsonSerializer.UTF8Encoding); - JsonWriter.WriteRootObject(writer, serializedString256); - writer.Flush(); - } - - [Benchmark] - public void SerializeJsonString512ToStreamUsingDirectStreamWriter() - { - stream.Position = 0; - var writer = new DirectStreamWriter(stream, JsonSerializer.UTF8Encoding); - JsonWriter.WriteRootObject(writer, serializedString512); - writer.Flush(); - } - - [Benchmark] - public void SerializeJsonString4096ToStreamUsingDirectStreamWriter() - { - stream.Position = 0; - var writer = new DirectStreamWriter(stream, JsonSerializer.UTF8Encoding); - JsonWriter.WriteRootObject(writer, serializedString4096); - writer.Flush(); - } - } -} diff --git a/benchmarks/ServiceStack.Text.Benchmarks/ParseBuiltinsBenchmarks.cs b/benchmarks/ServiceStack.Text.Benchmarks/ParseBuiltinsBenchmarks.cs deleted file mode 100644 index 1e736969e..000000000 --- a/benchmarks/ServiceStack.Text.Benchmarks/ParseBuiltinsBenchmarks.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System; -using System.Globalization; -using BenchmarkDotNet.Attributes; -using ServiceStack.Text; -#if NETCOREAPP1_1 -using Microsoft.Extensions.Primitives; -#endif -using ServiceStack.Text.Support; - -namespace ServiceStack.Text.Benchmarks -{ - public class ParseBuiltinBenchmarks - { - const string int32_1 = "1234"; - const string int32_2 = "-1234"; - const string decimal_1 = "1234.5678"; - const string decimal_2 = "-1234.5678"; - const string decimal_3 = "1234.5678901234567890"; - const string decimal_4 = "-1234.5678901234567890"; - const string guid_1 = "{b6170a18-3dd7-4a9b-b5d6-21033b5ad162}"; - - readonly StringSegment ss_int32_1 = new StringSegment(int32_1); - readonly StringSegment ss_int32_2 = new StringSegment(int32_2); - readonly StringSegment ss_decimal_1 = new StringSegment(decimal_1); - readonly StringSegment ss_decimal_2 = new StringSegment(decimal_2); - readonly StringSegment ss_decimal_3 = new StringSegment(decimal_3); - readonly StringSegment ss_decimal_4 = new StringSegment(decimal_4); - readonly StringSegment ss_guid_1 = new StringSegment(guid_1); - - [Benchmark] - public void Int32Parse() - { - var res1 = int.Parse(int32_1, CultureInfo.InvariantCulture); - var res2 = int.Parse(int32_2, CultureInfo.InvariantCulture); - } - - [Benchmark] - public void StringSegment_Int32Parse() - { - var res1 = ss_int32_1.ParseInt32(); - var res2 = ss_int32_2.ParseInt32(); - } - - [Benchmark] - public void DecimalParse() - { - var res1 = decimal.Parse(decimal_1, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture); - var res2 = decimal.Parse(decimal_2, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture); - } - - [Benchmark] - public void BigDecimalParse() - { - var res1 = decimal.Parse(decimal_3, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture); - var res2 = decimal.Parse(decimal_4, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture); - } - - - [Benchmark] - public void StringSegment_DecimalParse() - { - var res1 = ss_decimal_1.ParseDecimal(true); - var res2 = ss_decimal_2.ParseDecimal(true); - } - - [Benchmark] - public void StringSegment_BigDecimalParse() - { - var res1 = ss_decimal_3.ParseDecimal(true); - var res2 = ss_decimal_4.ParseDecimal(true); - } - - [Benchmark] - public void GuidParse() - { - var res1 = Guid.Parse(guid_1); - } - - [Benchmark] - public void StringSegment_GuidParse() - { - var res1 = ss_guid_1.ParseGuid(); - } - } -} \ No newline at end of file diff --git a/benchmarks/ServiceStack.Text.Benchmarks/Program.cs b/benchmarks/ServiceStack.Text.Benchmarks/Program.cs deleted file mode 100644 index 2ca6bcabe..000000000 --- a/benchmarks/ServiceStack.Text.Benchmarks/Program.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Running; -using BenchmarkDotNet.Validators; - -namespace ServiceStack.Text.Benchmarks -{ - public class Program - { - public static void Main(string[] args) - { - //BenchmarkRunner.Run( - BenchmarkRunner.Run( - ManualConfig - .Create(DefaultConfig.Instance) - //.With(Job.RyuJitX64) - .With(Job.Core) - .With(new BenchmarkDotNet.Diagnosers.CompositeDiagnoser()) - .With(ExecutionValidator.FailOnError) - ); - } - } -} diff --git a/benchmarks/ServiceStack.Text.Benchmarks/ServiceStack.Text.Benchmarks.csproj b/benchmarks/ServiceStack.Text.Benchmarks/ServiceStack.Text.Benchmarks.csproj deleted file mode 100644 index 56c274987..000000000 --- a/benchmarks/ServiceStack.Text.Benchmarks/ServiceStack.Text.Benchmarks.csproj +++ /dev/null @@ -1,30 +0,0 @@ - - - - net45;netcoreapp1.1 - ServiceStack.Text.Benchmarks - Exe - ServiceStack.Text.Benchmarks - $(PackageTargetFallback);dnxcore50 - 1.1.1 - - - - true - - - - portable - - - - - - - - - - - - - diff --git a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/JsonDeserializationBenchmarks.cs b/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/JsonDeserializationBenchmarks.cs deleted file mode 100644 index f3c2ce861..000000000 --- a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/JsonDeserializationBenchmarks.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.IO; -using System.Text; -using BenchmarkDotNet.Attributes; -using ServiceStack.Text; -using ServiceStack.Text.Json; -using StackExchange.Profiling; - -namespace ServiceStack.Text.Benchmarks -{ - public class StringType - { - public string Value1 { get; set; } - public string Value2 { get; set; } - public string Value3 { get; set; } - public string Value4 { get; set; } - public string Value5 { get; set; } - public string Value6 { get; set; } - public string Value7 { get; set; } - - public static StringType Create() - { - var st = new StringType(); - st.Value1 = st.Value2 = st.Value3 = st.Value4 = st.Value5 = st.Value6 = st.Value7 = "Hello, world"; - return st; - } - } - - [MemoryDiagnoser] - public class JsonDeserializationBenchmarks - { - static ModelWithCommonTypes commonTypesModel = ModelWithCommonTypes.Create(3); - static MemoryStream stream = new MemoryStream(32768); - const string serializedString = "this is the test string"; - readonly string serializedString256 = new string('t', 256); - readonly string serializedString512 = new string('t', 512); - readonly string serializedString4096 = new string('t', 4096); - - static string commonTypesModelJson; - static string stringTypeJson; - - static JsonDeserializationBenchmarks() - { - commonTypesModelJson = JsonSerializer.SerializeToString(commonTypesModel); - stringTypeJson = JsonSerializer.SerializeToString(StringType.Create()); - } - - [Benchmark(Description = "Deserialize Json: class with builtin types")] - public void DeserializeJsonCommonTypes() - { - var result = JsonSerializer.DeserializeFromString(commonTypesModelJson); - } - - [Benchmark(Description = "Deserialize Json: class with 10 string properties")] - public void DeserializeStringType() - { - var result = JsonSerializer.DeserializeFromString(stringTypeJson); - } - - [Benchmark(Description = "Deserialize Json: Complex MiniProfiler")] - public MiniProfiler ComplexDeserializeServiceStack() => ServiceStack.Text.JsonSerializer.DeserializeFromString(_complexProfilerJson); - - private static readonly MiniProfiler _complexProfiler = GetComplexProfiler(); - private static readonly string _complexProfilerJson = _complexProfiler.ToJson(); - - private static MiniProfiler GetComplexProfiler() - { - var mp = new MiniProfiler("Complex"); - for (var i = 0; i < 50; i++) - { - using (mp.Step("Step " + i)) - { - for (var j = 0; j < 50; j++) - { - using (mp.Step("SubStep " + j)) - { - for (var k = 0; k < 50; k++) - { - using (mp.CustomTiming("Custom " + k, "YOLO!")) - { - } - } - } - } - } - } - return mp; - } - } -} diff --git a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/ClientTiming.cs b/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/ClientTiming.cs deleted file mode 100644 index 6ecc24636..000000000 --- a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/ClientTiming.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Runtime.Serialization; - -namespace StackExchange.Profiling -{ - /// - /// A client timing probe - /// - [DataContract] - public class ClientTiming - { - /// - /// Gets or sets the name. - /// - [DataMember(Order = 1)] - public string Name { get; set; } - - /// - /// Gets or sets the start. - /// - [DataMember(Order = 2)] - public decimal Start { get; set; } - - /// - /// Gets or sets the duration. - /// - [DataMember(Order = 3)] - public decimal Duration { get; set; } - - /// - /// Unique Identifier used for sql storage. - /// - /// Not set unless storing in Sql - public Guid Id { get; set; } - - /// - /// Used for sql storage - /// - /// Not set unless storing in Sql - public Guid MiniProfilerId { get; set; } - } -} diff --git a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/ClientTimings.cs b/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/ClientTimings.cs deleted file mode 100644 index 0e3dbae35..000000000 --- a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/ClientTimings.cs +++ /dev/null @@ -1,172 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; - -namespace StackExchange.Profiling -{ - /// - /// Times collected from the client - /// - [DataContract] - public class ClientTimings - { - private const string TimingPrefix = "clientPerformance[timing]["; - private const string ProbesPrefix = "clientProbes["; - - /// - /// Gets or sets the list of client side timings - /// - [DataMember(Order = 2)] - public List Timings { get; set; } - - /// - /// Gets or sets the redirect count. - /// - [DataMember(Order = 1)] - public int RedirectCount { get; set; } - - /// - /// Returns null if there is not client timing stuff - /// - /// The form to transform to a . - public static ClientTimings FromForm(IDictionary form) - { - ClientTimings timing = null; - // AJAX requests won't have client timings - if (!form.ContainsKey(TimingPrefix + "navigationStart]")) return timing; - long.TryParse(form[TimingPrefix + "navigationStart]"], out long navigationStart); - if (navigationStart > 0) - { - var timings = new List(); - timing = new ClientTimings(); - int.TryParse(form["clientPerformance[navigation][redirectCount]"], out int redirectCount); - timing.RedirectCount = redirectCount; - - var clientPerf = new Dictionary(); - var clientProbes = new Dictionary(); - - foreach (string key in - form.Keys.OrderBy(i => i.IndexOf("Start]", StringComparison.Ordinal) > 0 ? "_" + i : i)) - { - if (key.StartsWith(TimingPrefix, StringComparison.Ordinal)) - { - long.TryParse(form[key], out long val); - val -= navigationStart; - - string parsedName = key.Substring( - TimingPrefix.Length, (key.Length - 1) - TimingPrefix.Length); - - // just ignore stuff that is negative ... not relevant - if (val > 0) - { - if (parsedName.EndsWith("Start", StringComparison.Ordinal)) - { - var shortName = parsedName.Substring(0, parsedName.Length - 5); - clientPerf[shortName] = new ClientTiming - { - Duration = -1, - Name = parsedName, - Start = val - }; - } - else if (parsedName.EndsWith("End", StringComparison.Ordinal)) - { - var shortName = parsedName.Substring(0, parsedName.Length - 3); - if (clientPerf.TryGetValue(shortName, out var t)) - { - t.Duration = val - t.Start; - t.Name = shortName; - } - } - else - { - clientPerf[parsedName] = new ClientTiming { Name = parsedName, Start = val, Duration = -1 }; - } - } - } - - if (key.StartsWith(ProbesPrefix, StringComparison.Ordinal)) - { - int endBracketIndex = key.IndexOf(']'); - if (endBracketIndex > 0 && int.TryParse(key.Substring(ProbesPrefix.Length, endBracketIndex - ProbesPrefix.Length), out int probeId)) - { - if (!clientProbes.TryGetValue(probeId, out var t)) - { - t = new ClientTiming(); - clientProbes.Add(probeId, t); - } - - if (key.EndsWith("[n]", StringComparison.Ordinal)) - { - t.Name = form[key]; - } - - if (key.EndsWith("[d]", StringComparison.Ordinal)) - { - long.TryParse(form[key], out long val); - if (val > 0) - { - t.Start = val - navigationStart; - } - } - } - } - } - - foreach (var group in clientProbes - .Values.OrderBy(p => p.Name) - .GroupBy(p => p.Name)) - { - ClientTiming current = null; - foreach (var item in group) - { - if (current == null) - { - current = item; - } - else - { - current.Duration = item.Start - current.Start; - timings.Add(current); - current = null; - } - } - } - - foreach (var item in clientPerf.Values) - { - item.Name = SentenceCase(item.Name); - } - - timings.AddRange(clientPerf.Values); - timing.Timings = timings.OrderBy(t => t.Start).ToList(); - } - - return timing; - } - - private static string SentenceCase(string value) - { - var sb = new StringBuilder(); - for (int i = 0; i < value.Length; i++) - { - if (i == 0) - { - sb.Append(char.ToUpper(value[0])); - continue; - } - - if (value[i] == char.ToUpper(value[i])) - { - sb.Append(' '); - } - - sb.Append(value[i]); - } - - return sb.ToString(); - } - } -} diff --git a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/CustomTiming.cs b/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/CustomTiming.cs deleted file mode 100644 index 545a7c09f..000000000 --- a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/CustomTiming.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Runtime.Serialization; - -namespace StackExchange.Profiling -{ - /// - /// A custom timing that usually represents a Remote Procedure Call, allowing better - /// visibility into these longer-running calls. - /// - [DataContract] - public class CustomTiming : IDisposable - { - private readonly decimal? _minSaveMs; - - /// - /// Don't use this. - /// - [Obsolete("Used for serialization")] - public CustomTiming() { /* serialization only */ } - - /// - /// Returns a new CustomTiming, also initializing its and, optionally, its . - /// - /// The to attach the timing so. - /// The descriptive command string for this timing, e.g. a URL or database command. - /// (Optional) The minimum time required to actually save this timing (e.g. do we care?). - public CustomTiming(MiniProfiler profiler, string commandString, decimal? minSaveMs = null) - { - _profiler = profiler; - _startTicks = profiler.ElapsedTicks; - _minSaveMs = minSaveMs; - CommandString = commandString; - - Id = Guid.NewGuid(); - StartMilliseconds = profiler.GetRoundedMilliseconds(profiler.ElapsedTicks); - - if (true/*!MiniProfiler.Settings.ExcludeStackTraceSnippetFromCustomTimings*/) - { - StackTraceSnippet = Helpers.StackTraceSnippet.Get(); - } - } - - private readonly MiniProfiler _profiler; - private readonly long _startTicks; - - /// - /// Unique identifier for this . - /// - [DataMember(Order = 1)] - public Guid Id { get; set; } - - /// - /// Gets or sets the command that was executed, e.g. "select * from Table" or "INCR my:redis:key" - /// - [DataMember(Order = 2)] - public string CommandString { get; set; } - - // TODO: should this just match the key that the List is stored under in Timing.CustomTimings? - /// - /// Short name describing what kind of custom timing this is, e.g. "Get", "Query", "Fetch". - /// - [DataMember(Order = 3)] - public string ExecuteType { get; set; } - - /// - /// Gets or sets roughly where in the calling code that this custom timing was executed. - /// - [DataMember(Order = 4)] - public string StackTraceSnippet { get; set; } - - /// - /// Gets or sets the offset from main MiniProfiler start that this custom command began. - /// - [DataMember(Order = 5)] - public decimal StartMilliseconds { get; set; } - - /// - /// Gets or sets how long this custom command statement took to execute. - /// - [DataMember(Order = 6)] - public decimal? DurationMilliseconds { get; set; } - - /// - /// OPTIONAL - how long this tim took to come back initially from the remote server, - /// before all data is fetched and command is completed. - /// - [DataMember(Order = 7)] - public decimal? FirstFetchDurationMilliseconds { get; set; } - - /// - /// Whether this operation errored. - /// - [DataMember(Order = 8)] - public bool Errored { get; set; } - - internal string Category { get; set; } - - /// - /// OPTIONAL - call after receiving the first response from your Remote Procedure Call to - /// properly set . - /// - public void FirstFetchCompleted() - { - FirstFetchDurationMilliseconds = FirstFetchDurationMilliseconds ?? _profiler.GetDurationMilliseconds(_startTicks); - } - - /// - /// Stops this timing, setting . - /// - public void Stop() - { - DurationMilliseconds = DurationMilliseconds ?? _profiler.GetDurationMilliseconds(_startTicks); - - if (_minSaveMs.HasValue && _minSaveMs.Value > 0 && DurationMilliseconds < _minSaveMs.Value) - { - _profiler.Head.RemoveCustomTiming(Category, this); - } - } - - void IDisposable.Dispose() => Stop(); - - /// - /// Returns for debugging. - /// - public override string ToString() => CommandString; - } -} diff --git a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/FlowData.cs b/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/FlowData.cs deleted file mode 100644 index 2455dc292..000000000 --- a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/FlowData.cs +++ /dev/null @@ -1,52 +0,0 @@ -#if NET45 -using System.Runtime.Remoting; -using System.Runtime.Remoting.Messaging; -#else -using System.Threading; -#endif - -namespace StackExchange.Profiling.Internal -{ - /// - /// Internal MiniProfiler extensions, not meant for consumption. - /// This can and probably will break without warning. Don't use the .Internal namespace directly. - /// - /// Shim class covering for AsyncLocal{T} in pre-.NET 4.6 which didn't have it. - /// - /// The type of data to store. - public class FlowData - { -#if NET45 - // Key specific to this type. -#pragma warning disable RCS1158 // Avoid static members in generic types. - private static readonly string _key = typeof(FlowData).FullName; -#pragma warning restore RCS1158 // Avoid static members in generic types. - - /// - /// Gets or sets the value of the ambient data. - /// - public T Value - { - get - { - var handle = CallContext.LogicalGetData(_key) as ObjectHandle; - return handle != null - ? (T)handle.Unwrap() - : default(T); - } - set { CallContext.LogicalSetData(_key, new ObjectHandle(value)); } - } -#else - private readonly AsyncLocal _backing = new AsyncLocal(); - - /// - /// Gets or sets the value of the ambient data. - /// - public T Value - { - get => _backing.Value; - set => _backing.Value = value; - } -#endif - } -} \ No newline at end of file diff --git a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/MiniProfiler.cs b/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/MiniProfiler.cs deleted file mode 100644 index 3d68b60c6..000000000 --- a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/MiniProfiler.cs +++ /dev/null @@ -1,341 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.Serialization; -using System.Threading.Tasks; -using System.Diagnostics; -using StackExchange.Profiling.Internal; - -namespace StackExchange.Profiling -{ - /// - /// A single MiniProfiler can be used to represent any number of steps/levels in a call-graph, via Step() - /// - /// Totally baller. - [DataContract] - public partial class MiniProfiler - { - /// - /// Initialises a new instance of the class. - /// Obsolete - used for serialization. - /// - [Obsolete("Used for serialization")] - public MiniProfiler() { /* serialization only */ } - - /// - /// Initialises a new instance of the class. Creates and starts a new MiniProfiler - /// for the root . - /// - /// The name of this , typically a URL. - public MiniProfiler(string name) - { - Id = Guid.NewGuid(); - MachineName = Environment.MachineName; - Started = DateTime.UtcNow; - - // stopwatch must start before any child Timings are instantiated - Stopwatch = Stopwatch.StartNew(); - Root = new Timing(this, null, name); - } - - /// - /// Whether the profiler is currently profiling - /// - internal bool IsActive { get; set; } - - /// - /// Gets or sets the profiler id. - /// Identifies this Profiler so it may be stored/cached. - /// - [DataMember(Order = 1)] - public Guid Id { get; set; } - - /// - /// Gets or sets a display name for this profiling session. - /// - [DataMember(Order = 2)] - public string Name { get; set; } - - /// - /// Gets or sets when this profiler was instantiated, in UTC time. - /// - [DataMember(Order = 3)] - public DateTime Started { get; set; } - - /// - /// Gets the milliseconds, to one decimal place, that this MiniProfiler ran. - /// - [DataMember(Order = 4)] - public decimal DurationMilliseconds { get; set; } - - /// - /// Gets or sets where this profiler was run. - /// - [DataMember(Order = 5)] - public string MachineName { get; set; } - - /// - /// Keys are names, values are URLs, allowing additional links to be added to a profiler result, e.g. perhaps a deeper - /// diagnostic page for the current request. - /// - /// - /// Use to easily add a name/url pair to this dictionary. - /// - [DataMember(Order = 6)] - public Dictionary CustomLinks { get; set; } - - /// - /// Json used to store Custom Links. Do not touch. - /// - /*public string CustomLinksJson - { - get => CustomLinks?.ToJson(); - set - { - if (value.HasValue()) - { - CustomLinks = value.FromJson>(); - } - } - } */ - - private Timing _root; - /// - /// Gets or sets the root timing. - /// The first that is created and started when this profiler is instantiated. - /// All other s will be children of this one. - /// - [DataMember(Order = 7)] - public Timing Root - { - get => _root; - set - { - _root = value; - RootTimingId = value.Id; - - // TODO: Add serialization tests, then move Timing.Children to get/set - // and for() { .ParentTiming = this; } over in the local setter set there - // let serialization take care of the tree instead. - // I bet the current method doesn't even work, since it depends on .Root being set - // last in deserialization order - // TL;DR: I bet no one ever checked this, or possible no one cares about parent after deserialization. - - // when being deserialized, we need to go through and set all child timings' parents - if (_root.HasChildren) - { - var timings = new Stack(); - timings.Push(_root); - while (timings.Count > 0) - { - var timing = timings.Pop(); - - if (timing.HasChildren) - { - var children = timing.Children; - - for (int i = children.Count - 1; i >= 0; i--) - { - children[i].ParentTiming = timing; - timings.Push(children[i]); // FLORIDA! TODO: refactor this and other stack creation methods into one - } - } - } - } - } - } - - /// - /// Id of Root Timing. Used for Sql Storage purposes. - /// - public Guid? RootTimingId { get; set; } - - /// - /// Gets or sets timings collected from the client - /// - [DataMember(Order = 8)] - public ClientTimings ClientTimings { get; set; } - - /// - /// RedirectCount in ClientTimings. Used for sql storage. - /// - public int? ClientTimingsRedirectCount { get; set; } - - /// - /// Gets or sets a string identifying the user/client that is profiling this request. - /// - /// - /// If this is not set manually at some point, the IUserProvider implementation will be used; - /// by default, this will be the current request's IP address. - /// - [DataMember(Order = 9)] - public string User { get; set; } - - /// - /// Returns true when this MiniProfiler has been viewed by the that recorded it. - /// - /// - /// Allows POSTs that result in a redirect to be profiled. implementation - /// will keep a list of all profilers that haven't been fetched down. - /// - [DataMember(Order = 10)] - public bool HasUserViewed { get; set; } - - // Allows async to properly track the attachment point - private readonly FlowData _head = new FlowData(); - - /// - /// Gets or sets points to the currently executing Timing. - /// - public Timing Head - { - get => _head.Value; - set => _head.Value = value; - } - - /// - /// Gets the ticks since this MiniProfiler was started. - /// - internal long ElapsedTicks => Stopwatch.ElapsedTicks; - - /// - /// Gets the timer, for unit testing, returns the timer. - /// - internal Stopwatch Stopwatch { get; set; } - - /// - /// Starts a new MiniProfiler based on the current . This new profiler can be accessed by - /// . - /// - /// - /// Allows explicit naming of the new profiling session; when null, an appropriate default will be used, e.g. for - /// a web request, the url will be used for the overall session name. - /// - //public static MiniProfiler Start(string sessionName = null) => - // Settings.ProfilerProvider.Start(sessionName); - - /// - /// Ends the current profiling session, if one exists. - /// - /// - /// When true, clears the , allowing profiling to - /// be prematurely stopped and discarded. Useful for when a specific route does not need to be profiled. - /// - //public static void Stop(bool discardResults = false) => - // Settings.ProfilerProvider.Stop(discardResults); - - /// - /// Asynchronously ends the current profiling session, if one exists. - /// This invokves async saving all the way down if th providers support it. - /// - /// - /// When true, clears the , allowing profiling to - /// be prematurely stopped and discarded. Useful for when a specific route does not need to be profiled. - /// - //public static Task StopAsync(bool discardResults = false) => - // Settings.ProfilerProvider.StopAsync(discardResults); - - /// - /// Deserializes the JSON string parameter to a . - /// - /// The string to deserialize int a . - //public static MiniProfiler FromJson(string json) => json.FromJson(); - - /// - /// Returns the 's and this profiler recorded. - /// - /// a string containing the recording information - public override string ToString() => Root != null ? Root.Name + " (" + DurationMilliseconds + " ms)" : string.Empty; - - /// - /// Returns true if Ids match. - /// - /// The to compare to. - public override bool Equals(object obj) => obj is MiniProfiler && Id.Equals(((MiniProfiler)obj).Id); - - /// - /// Returns hash code of Id. - /// - public override int GetHashCode() => Id.GetHashCode(); - - /// - /// Walks the hierarchy contained in this profiler, starting with , and returns each Timing found. - /// - public IEnumerable GetTimingHierarchy() - { - var timings = new Stack(); - - timings.Push(_root); - - while (timings.Count > 0) - { - var timing = timings.Pop(); - - yield return timing; - - if (timing.HasChildren) - { - var children = timing.Children; - for (int i = children.Count - 1; i >= 0; i--) timings.Push(children[i]); - } - } - } - -#if !NETCOREAPP1_1 // TODO: Revisit in .NET Standard 2.0 - /// - /// Create a DEEP clone of this MiniProfiler. - /// - public MiniProfiler Clone() - { - var serializer = new DataContractSerializer(typeof(MiniProfiler), null, int.MaxValue, false, true, null); - using (var ms = new System.IO.MemoryStream()) - { - serializer.WriteObject(ms, this); - ms.Position = 0; - return (MiniProfiler)serializer.ReadObject(ms); - } - } -#endif - - - internal Timing StepImpl(string name, decimal? minSaveMs = null, bool? includeChildrenWithMinSave = false) - { - return new Timing(this, Head, name, minSaveMs, includeChildrenWithMinSave); - } - - //internal IDisposable IgnoreImpl() => new Suppression(this); - - internal bool StopImpl() - { - if (!Stopwatch.IsRunning) - return false; - - Stopwatch.Stop(); - DurationMilliseconds = GetRoundedMilliseconds(ElapsedTicks); - - foreach (var timing in GetTimingHierarchy()) - { - timing.Stop(); - } - - return true; - } - - /// - /// Returns milliseconds based on Stopwatch's Frequency, rounded to one decimal place. - /// - /// The tick count to round. - internal decimal GetRoundedMilliseconds(long ticks) - { - long z = 10000 * ticks; - decimal timesTen = (int)(z / Stopwatch.Frequency); - return timesTen / 10; - } - - /// - /// Returns how many milliseconds have elapsed since was recorded. - /// - /// The start tick count. - internal decimal GetDurationMilliseconds(long startTicks) => - GetRoundedMilliseconds(ElapsedTicks - startTicks); - } -} \ No newline at end of file diff --git a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/MiniProfilerExtensions.cs b/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/MiniProfilerExtensions.cs deleted file mode 100644 index 9c54b3b98..000000000 --- a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/MiniProfilerExtensions.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace StackExchange.Profiling -{ - /// - /// Contains helper methods that ease working with null s. - /// - public static class MiniProfilerExtensions - { - /// - /// Wraps in a call and executes it, returning its result. - /// - /// the type of result. - /// The current profiling session or null. - /// Method to execute and profile. - /// The step name used to label the profiler results. - /// the profiled result. - /// Throws when is null. - public static T Inline(this MiniProfiler profiler, Func selector, string name) - { - if (selector == null) throw new ArgumentNullException(nameof(selector)); - if (profiler == null) return selector(); - using (profiler.StepImpl(name)) - { - return selector(); - } - } - - /// - /// Returns an () that will time the code between its creation and disposal. - /// - /// The current profiling session or null. - /// A descriptive name for the code that is encapsulated by the resulting Timing's lifetime. - /// the profile step - public static Timing Step(this MiniProfiler profiler, string name) => profiler?.StepImpl(name); - - /// - /// Returns an () that will time the code between its creation and disposal. - /// Will only save the if total time taken exceeds . - /// - /// The current profiling session or null. - /// A descriptive name for the code that is encapsulated by the resulting Timing's lifetime. - /// The minimum amount of time that needs to elapse in order for this result to be recorded. - /// Should the amount of time spent in child timings be included when comparing total time - /// profiled with ? If true, will include children. If false will ignore children. - /// - /// If is set to true and a child is removed due to its use of StepIf, then the - /// time spent in that time will also not count for the current StepIf calculation. - public static Timing StepIf(this MiniProfiler profiler, string name, decimal minSaveMs, bool includeChildren = false) - { - return profiler?.StepImpl(name, minSaveMs, includeChildren); - } - - /// - /// Returns a new that will automatically set its - /// and - /// - /// The current profiling session or null. - /// The category under which this timing will be recorded. - /// The command string that will be recorded along with this timing, for display in the MiniProfiler results. - /// Execute Type to be associated with the Custom Timing. Example: Get, Set, Insert, Delete - /// - /// Should be used like the extension method - /// - public static CustomTiming CustomTiming(this MiniProfiler profiler, string category, string commandString, string executeType = null) - { - return CustomTimingIf(profiler, category, commandString, 0, executeType: executeType); - } - - /// - /// Returns a new that will automatically set its - /// and . Will only save the new if the total elapsed time - /// takes more than . - /// - /// The current profiling session or null. - /// The category under which this timing will be recorded. - /// The command string that will be recorded along with this timing, for display in the MiniProfiler results. - /// Execute Type to be associated with the Custom Timing. Example: Get, Set, Insert, Delete - /// The minimum amount of time that needs to elapse in order for this result to be recorded. - /// - /// Should be used like the extension method - /// - public static CustomTiming CustomTimingIf(this MiniProfiler profiler, string category, string commandString, decimal minSaveMs, string executeType = null) - { - if (profiler == null || profiler.Head == null || !profiler.IsActive) return null; - - var result = new CustomTiming(profiler, commandString, minSaveMs) - { - ExecuteType = executeType, - Category = category - }; - - // THREADING: revisit - profiler.Head.AddCustomTiming(category, result); - - return result; - } - - /// - /// Returns an that will ignore profiling between its creation and disposal. - /// - /// - /// This is mainly useful in situations where you want to ignore database profiling for known hot spots, - /// but it is safe to use in a nested step such that you can ignore sub-sections of a profiled step. - /// - /// The current profiling session or null. - /// the profile step - //public static IDisposable Ignore(this MiniProfiler profiler) => profiler?.IgnoreImpl(); - - /// - /// Adds 's hierarchy to this profiler's current Timing step, - /// allowing other threads, remote calls, etc. to be profiled and joined into this profiling session. - /// - /// The to add to. - /// The to append to 's tree. - public static void AddProfilerResults(this MiniProfiler profiler, MiniProfiler externalProfiler) - { - if (profiler?.Head == null || externalProfiler == null) return; - profiler.Head.AddChild(externalProfiler.Root); - } - - /// - /// Adds the and pair to 's - /// dictionary; will be displayed on the client in the bottom of the profiler popup. - /// - /// The to add the link to. - /// The text label for the link. - /// The URL the link goes to. - public static void AddCustomLink(this MiniProfiler profiler, string text, string url) - { - if (profiler == null || !profiler.IsActive) return; - - lock (profiler) - { - profiler.CustomLinks = profiler.CustomLinks ?? new Dictionary(); - } - - profiler.CustomLinks[text] = url; - } - } -} \ No newline at end of file diff --git a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/StackTraceSnippet.cs b/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/StackTraceSnippet.cs deleted file mode 100644 index 70ea9d1fb..000000000 --- a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/StackTraceSnippet.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.Diagnostics; -using System.Reflection; -using System.Text; - -namespace StackExchange.Profiling.Helpers -{ - /// - /// Gets part of a stack trace containing only methods we care about. - /// - public static class StackTraceSnippet - { - // TODO: Uhhhhhhh, this isn't gonna work. Let's come back to this. Oh and async. Dammit. - private const string AspNetEntryPointMethodName = "System.Web.HttpApplication.IExecutionStep.Execute"; - - /// - /// Gets the current formatted and filtered stack trace. - /// - /// Space separated list of methods - public static string Get() - { -#if !NETCOREAPP1_1 - var frames = new StackTrace().GetFrames(); -#else // TODO: Make this work in .NET Standard, true fix isn't until 2.0 via https://github.com/dotnet/corefx/pull/12527 - StackFrame[] frames = null; -#endif - if (frames == null /*|| MiniProfiler.Settings.StackMaxLength <= 0*/) - { - return string.Empty; - } - - var sb = new StringBuilder(); - int stackLength = -1; // Starts on -1 instead of zero to compensate for adding 1 first time - - foreach (StackFrame t in frames) - { - var method = t.GetMethod(); - - // no need to continue up the chain - if (method.Name == AspNetEntryPointMethodName) - break; - - if (stackLength >= 120 /*MiniProfiler.Settings.StackMaxLength*/) - break; - - var assembly = method.Module.Assembly.GetName().Name; - //if (!ShouldExcludeType(method) - // && !MiniProfiler.Settings.AssembliesToExclude.Contains(assembly) - // && !MiniProfiler.Settings.MethodsToExclude.Contains(method.Name)) - { - if (sb.Length > 0) - { - sb.Append(' '); - } - sb.Append(method.Name); - stackLength += method.Name.Length + 1; // 1 added for spaces. - } - } - - return sb.ToString(); - } - - /*private static bool ShouldExcludeType(MethodBase method) - { - var t = method.DeclaringType; - - while (t != null) - { - if (MiniProfiler.Settings.TypesToExclude.Contains(t.Name)) - return true; - - t = t.DeclaringType; - } - - return false; - } - */ - } -} \ No newline at end of file diff --git a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/Timing.cs b/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/Timing.cs deleted file mode 100644 index 5c1b90b13..000000000 --- a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/MiniProfiler/Timing.cs +++ /dev/null @@ -1,286 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; - -namespace StackExchange.Profiling -{ - /// - /// An individual profiling step that can contain child steps. - /// - [DataContract] - public class Timing : IDisposable - { - /// - /// Offset from parent MiniProfiler's creation that this Timing was created. - /// - private readonly long _startTicks; - private readonly decimal? _minSaveMs; - private readonly bool _includeChildrenWithMinSave; - - /// - /// Initialises a new instance of the class. - /// Obsolete - used for serialization. - /// - [Obsolete("Used for serialization")] - public Timing() { /* serialization */ } - - /// - /// Creates a new Timing named 'name' in the 'profiler's session, with 'parent' as this Timing's immediate ancestor. - /// - /// The this belongs to. - /// The this is a child of. - /// The name of this timing. - /// (Optional) The minimum threshold (in milliseconds) for saving this timing. - /// (Optional) Whether the children are included when comparing to the threshold. - public Timing(MiniProfiler profiler, Timing parent, string name, decimal? minSaveMs = null, bool? includeChildrenWithMinSave = false) - { - Id = Guid.NewGuid(); - Profiler = profiler; - Profiler.Head = this; - - // root will have no parent - parent?.AddChild(this); - - Name = name; - - _startTicks = profiler.ElapsedTicks; - _minSaveMs = minSaveMs; - _includeChildrenWithMinSave = includeChildrenWithMinSave == true; - StartMilliseconds = profiler.GetRoundedMilliseconds(_startTicks); - } - - /// - /// Gets or sets Unique identifier for this timing; set during construction. - /// - [DataMember(Order = 1)] - public Guid Id { get; set; } - - /// - /// Gets or sets Text displayed when this Timing is rendered. - /// - [DataMember(Order = 2)] - public string Name { get; set; } - - /// - /// Gets or sets How long this Timing step took in ms; includes any Timings' durations. - /// - [DataMember(Order = 3)] - public decimal? DurationMilliseconds { get; set; } - - /// - /// Gets or sets The offset from the start of profiling. - /// - [DataMember(Order = 4)] - public decimal StartMilliseconds { get; set; } - - /// - /// Gets or sets All sub-steps that occur within this Timing step. Add new children through - /// - [DataMember(Order = 5)] - public List Children { get; set; } - - /// - /// lists keyed by their type, e.g. "sql", "memcache", "redis", "http". - /// - [DataMember(Order = 6)] - public Dictionary> CustomTimings { get; set; } - - /// - /// Returns true when there exists any objects in this . - /// - public bool HasCustomTimings => CustomTimings?.Values.Any(v => v?.Count > 0) ?? false; - - /// - /// Gets or sets Which Timing this Timing is under - the duration that this step takes will be added to its parent's duration. - /// - /// This will be null for the root (initial) Timing. - [IgnoreDataMember] - public Timing ParentTiming { get; set; } - - /// - /// The Unique Identifier identifying the parent timing of this Timing. Used for sql server storage. - /// - [IgnoreDataMember] - public Guid ParentTimingId { get; set; } - - /// - /// Gets the elapsed milliseconds in this step without any children's durations. - /// - [IgnoreDataMember] - public decimal DurationWithoutChildrenMilliseconds - { - get - { - var result = DurationMilliseconds.GetValueOrDefault(); - - if (HasChildren) - { - foreach (var child in Children) - { - result -= child.DurationMilliseconds.GetValueOrDefault(); - } - } - - return Math.Round(result, 1); - } - } - - /// - /// Gets a value indicating whether this is less than the configured - /// , by default 2.0 ms. - /// - [IgnoreDataMember] - public bool IsTrivial => DurationMilliseconds <= 2.0M /*MiniProfiler.Settings.TrivialDurationThresholdMilliseconds*/; - - /// - /// Gets a value indicating whether this Timing has inner Timing steps. - /// - [IgnoreDataMember] - public bool HasChildren => Children?.Count > 0; - - /// - /// Gets a value indicating whether this Timing is the first one created in a MiniProfiler session. - /// - [IgnoreDataMember] - public bool IsRoot => Equals(Profiler.Root); - - /// - /// Gets a value indicating whether how far away this Timing is from the Profiler's Root. - /// - [IgnoreDataMember] - public short Depth - { - get - { - short result = 0; - var parent = ParentTiming; - - while (parent != null) - { - parent = parent.ParentTiming; - result++; - } - - return result; - } - } - - /// - /// Gets a reference to the containing profiler, allowing this Timing to affect the Head and get Stopwatch readings. - /// - internal MiniProfiler Profiler { get; set; } - - /// - /// The unique identifier used to identify the Profiler with which this Timing is associated. Used for sql storage. - /// - [IgnoreDataMember] - public Guid MiniProfilerId { get; set; } - - /// - /// Returns this Timing's Name. - /// - public override string ToString() => Name; - - /// - /// Returns true if Ids match. - /// - /// The to comare to. - public override bool Equals(object obj) - { - return obj is Timing && Id.Equals(((Timing)obj).Id); - } - - /// - /// Returns hash code of Id. - /// - public override int GetHashCode() => Id.GetHashCode(); - - /// - /// Completes this Timing's duration and sets the MiniProfiler's Head up one level. - /// - public void Stop() - { - if (DurationMilliseconds != null) return; - DurationMilliseconds = Profiler.GetDurationMilliseconds(_startTicks); - Profiler.Head = ParentTiming; - - if (_minSaveMs.HasValue && _minSaveMs.Value > 0 && ParentTiming != null) - { - var compareMs = _includeChildrenWithMinSave ? DurationMilliseconds : DurationWithoutChildrenMilliseconds; - if (compareMs < _minSaveMs.Value) - { - ParentTiming.RemoveChild(this); - } - } - } - - /// - /// Stops profiling, allowing the using construct to neatly encapsulate a region to be profiled. - /// - void IDisposable.Dispose() => Stop(); - - /// - /// Add the parameter 'timing' to this Timing's Children collection. - /// - /// The child to add. - /// - /// Used outside this assembly for custom deserialization when creating an implementation. - /// - public void AddChild(Timing timing) - { - Children = Children ?? new List(); - - Children.Add(timing); - timing.Profiler = timing.Profiler ?? Profiler; - timing.ParentTiming = this; - timing.ParentTimingId = Id; - if (Profiler != null) - timing.MiniProfilerId = Profiler.Id; - } - - internal void RemoveChild(Timing timing) => Children?.Remove(timing); - - /// - /// Adds to this step's dictionary of - /// custom timings, . Ensures that is created, - /// as well as the 's list. - /// - /// The kind of custom timing, e.g. "http", "redis", "memcache" - /// Duration and command information - public void AddCustomTiming(string category, CustomTiming customTiming) - { - GetCustomTimingList(category).Add(customTiming); - } - - internal void RemoveCustomTiming(string category, CustomTiming customTiming) - { - GetCustomTimingList(category).Remove(customTiming); - } - - private readonly object _lockObject = new object(); - - /// - /// Returns the list keyed to the , creating any collections when null. - /// - /// The kind of custom timings, e.g. "sql", "redis", "memcache" - private List GetCustomTimingList(string category) - { - lock (_lockObject) - { - CustomTimings = CustomTimings ?? new Dictionary>(); - } - - List result; - lock (CustomTimings) - { - if (!CustomTimings.TryGetValue(category, out result)) - { - result = new List(); - CustomTimings[category] = result; - } - } - return result; - } - } -} \ No newline at end of file diff --git a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/ModelWithCommonTypes.cs b/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/ModelWithCommonTypes.cs deleted file mode 100644 index 0ae5ae3eb..000000000 --- a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/ModelWithCommonTypes.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; - -namespace ServiceStack.Text.Benchmarks -{ - public class ModelWithCommonTypes - { - public char CharValue { get; set; } - - public byte ByteValue { get; set; } - - public sbyte SByteValue { get; set; } - - public short ShortValue { get; set; } - - public ushort UShortValue { get; set; } - - public int IntValue { get; set; } - - public uint UIntValue { get; set; } - - public long LongValue { get; set; } - - public ulong ULongValue { get; set; } - - public float FloatValue { get; set; } - - public double DoubleValue { get; set; } - - public decimal DecimalValue { get; set; } - - public DateTime DateTimeValue { get; set; } - - public TimeSpan TimeSpanValue { get; set; } - - public Guid GuidValue { get; set; } - - public static ModelWithCommonTypes Create(byte i) - { - return new ModelWithCommonTypes - { - ByteValue = i, - CharValue = (char)i, - DateTimeValue = new DateTime(2000, 1, 1 + i), - DecimalValue = i, - DoubleValue = i, - FloatValue = i, - IntValue = i, - LongValue = i, - SByteValue = (sbyte)i, - ShortValue = i, - TimeSpanValue = new TimeSpan(i), - UIntValue = i, - ULongValue = i, - UShortValue = i, - GuidValue = Guid.NewGuid(), - }; - } - } -} \ No newline at end of file diff --git a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/ParseBuiltinsBenchmarks.cs b/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/ParseBuiltinsBenchmarks.cs deleted file mode 100644 index 96a90eae2..000000000 --- a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/ParseBuiltinsBenchmarks.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System; -using System.Globalization; -using BenchmarkDotNet.Attributes; -using ServiceStack.Text; - -namespace ServiceStack.Text.Benchmarks -{ - public class ParseBuiltinBenchmarks - { - const string int32_1 = "1234"; - const string int32_2 = "-1234"; - const string decimal_1 = "1234.5678"; - const string decimal_2 = "-1234.5678"; - const string decimal_3 = "1234.5678901234567890"; - const string decimal_4 = "-1234.5678901234567890"; - const string guid_1 = "{b6170a18-3dd7-4a9b-b5d6-21033b5ad162}"; - - [Benchmark] - public void Int32Parse() - { - var res1 = JsonSerializer.DeserializeFromString(int32_1); - var res2 = JsonSerializer.DeserializeFromString(int32_2); - } - - [Benchmark] - public void DecimalParse() - { - var res1 = JsonSerializer.DeserializeFromString(decimal_1); - var res2 = JsonSerializer.DeserializeFromString(decimal_2); - } - - [Benchmark] - public void BigDecimalParse() - { - var res1 = JsonSerializer.DeserializeFromString(decimal_3); - var res2 = JsonSerializer.DeserializeFromString(decimal_4); - } - - [Benchmark] - public void GuidParse() - { - var res1 = JsonSerializer.DeserializeFromString(guid_1); - } - } -} \ No newline at end of file diff --git a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/Program.cs b/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/Program.cs deleted file mode 100644 index c957937c1..000000000 --- a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/Program.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; -using BenchmarkDotNet.Configs; -using BenchmarkDotNet.Jobs; -using BenchmarkDotNet.Running; -using BenchmarkDotNet.Validators; - -namespace ServiceStack.Text.Benchmarks -{ - public class Program - { - public static void Main(string[] args) - { - var benchmarks = new[] { - typeof(JsonDeserializationBenchmarks), - typeof(ParseBuiltinBenchmarks) - }; - - var switcher = new BenchmarkSwitcher(benchmarks); - - foreach (var benchmark in benchmarks) - { - switcher.Run( - args: new[] {benchmark.Name}, - config: ManualConfig - .Create(DefaultConfig.Instance) - //.With(Job.RyuJitX64) - .With(Job.Core) - .With(Job.Clr) - .With(new BenchmarkDotNet.Diagnosers.CompositeDiagnoser()) - .With(ExecutionValidator.FailOnError) - ); - } - } - } -} diff --git a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/ServiceStack.Text.VersionCompareBenchmarks.BaseLine.csproj b/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/ServiceStack.Text.VersionCompareBenchmarks.BaseLine.csproj deleted file mode 100644 index a0f37384e..000000000 --- a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/ServiceStack.Text.VersionCompareBenchmarks.BaseLine.csproj +++ /dev/null @@ -1,36 +0,0 @@ - - - - netcoreapp1.1;net46; - ServiceStack.Text.VersionCompareBenchmarks.BaseLine - Exe - ServiceStack.Text.VersionCompareBenchmarks - $(PackageTargetFallback);dnxcore50 - 1.1.2 - - - - true - - - - portable - - - - - - - - - - - - - - - - - - - diff --git a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/ServiceStack.Text.VersionCompareBenchmarks.csproj b/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/ServiceStack.Text.VersionCompareBenchmarks.csproj deleted file mode 100644 index 7b5a05480..000000000 --- a/benchmarks/ServiceStack.Text.VersionCompareBenchmarks/ServiceStack.Text.VersionCompareBenchmarks.csproj +++ /dev/null @@ -1,45 +0,0 @@ - - - - netcoreapp1.1;net46; - ServiceStack.Text.VersionCompareBenchmarks - Exe - ServiceStack.Text.VersionCompareBenchmarks - $(PackageTargetFallback);dnxcore50 - 1.1.2 - - - - true - - - - portable - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/benchmarks/run-benchmarks.bat b/benchmarks/run-benchmarks.bat deleted file mode 100644 index 1f47a2c04..000000000 --- a/benchmarks/run-benchmarks.bat +++ /dev/null @@ -1,23 +0,0 @@ -@echo off -set proj=ServiceStack.Text.VersionCompareBenchmarks -set curdate= -for /f "tokens=2-4 delims=/ " %%a in ('date /t') do (set curdate=%%c-%%a-%%b) -for /f "usebackq tokens=1,2 delims==" %%a in (`wmic os get LocalDateTime /VALUE 2^>NUL`) do (if '.%%a.'=='.LocalDateTime.' set ldt=%%b) -set curdate=%ldt:~0,4%-%ldt:~4,2%-%ldt:~6,2% -echo %proj%\ServiceStack.Text.VersionCompareBenchmarks.csproj %curdate% - -mkdir Results - -rmdir /s /q bin -rmdir /s /q obj -dotnet restore %proj%\ServiceStack.Text.VersionCompareBenchmarks.csproj && dotnet build -c Release %proj%\ServiceStack.Text.VersionCompareBenchmarks.csproj -%proj%\bin\Release\net46\ServiceStack.Text.VersionCompareBenchmarks.exe -copy BenchmarkDotNet.Artifacts\results\JsonDeserializationBenchmarks-report-github.md Results\JsonDeserialization-%curdate%.md -copy BenchmarkDotNet.Artifacts\results\ParseBuiltinBenchmarks-report-github.md Results\ParseBuiltin-%curdate%.md - -rmdir /s /q bin -rmdir /s /q obj -dotnet restore %proj%\ServiceStack.Text.VersionCompareBenchmarks.BaseLine.csproj && dotnet build -c Release %proj%\ServiceStack.Text.VersionCompareBenchmarks.BaseLine.csproj -%proj%\bin\Release\net46\ServiceStack.Text.VersionCompareBenchmarks.BaseLine.exe -copy BenchmarkDotNet.Artifacts\results\JsonDeserializationBenchmarks-report-github.md Results\JsonDeserialization-baseline-%curdate%.md -copy BenchmarkDotNet.Artifacts\results\ParseBuiltinBenchmarks-report-github.md Results\ParseBuiltin-baseline-%curdate%.md diff --git a/src/ServiceStack.Memory/ServiceStack.Memory.csproj b/src/ServiceStack.Memory/ServiceStack.Memory.csproj deleted file mode 100644 index d5efec16f..000000000 --- a/src/ServiceStack.Memory/ServiceStack.Memory.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - netcoreapp2.1;net6.0 - - - $(DefineConstants);NETSTANDARD;NETCORE;NETCORE2_1 - - - $(DefineConstants);NETSTANDARD;NETCORE;NET6_0 - - - - - - - NetCoreMemory.cs - - - \ No newline at end of file From 237a371d5abbb70fabfb39e163bda89571733e8d Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 24 Jan 2022 20:11:58 +0800 Subject: [PATCH 32/75] Upgrade to 6.0.0 --- NuGet.Config | 2 +- build/build-core.proj | 3 +-- build/build-deps.proj | 9 +------ build/build.proj | 8 +----- src/Directory.Build.props | 4 +-- src/ServiceStack.Text.sln | 6 ----- src/ServiceStack.Text/Env.cs | 2 +- src/ServiceStack.Text/MemoryProvider.cs | 2 +- src/ServiceStack.Text/NetCoreMemory.cs | 3 +-- .../Properties/AssemblyInfo.cs | 2 +- .../ServiceStack.Text.Core.csproj | 11 +------- .../ServiceStack.Text.csproj | 12 ++------- tests/Directory.Build.props | 20 ++------------ .../Northwind.Common/Northwind.Common.csproj | 15 +++-------- .../MemoryProviderBenchmarks.cs | 1 - .../ServiceStack.Text.Benchmarks.csproj | 4 +-- .../DataContractTests.cs | 27 ------------------- .../ServiceStack.Text.Tests.csproj | 1 - 18 files changed, 21 insertions(+), 111 deletions(-) diff --git a/NuGet.Config b/NuGet.Config index 42daf5f44..f3c213adf 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,7 +1,7 @@ - + \ No newline at end of file diff --git a/build/build-core.proj b/build/build-core.proj index 395a22659..707725c77 100644 --- a/build/build-core.proj +++ b/build/build-core.proj @@ -4,7 +4,7 @@ - 5 + 6 0 $(BUILD_NUMBER) @@ -41,7 +41,6 @@ - diff --git a/build/build-deps.proj b/build/build-deps.proj index ca0f8332f..38e06c69b 100644 --- a/build/build-deps.proj +++ b/build/build-deps.proj @@ -4,7 +4,7 @@ - 5 + 6 0 $(BUILD_NUMBER) @@ -32,14 +32,12 @@ - - @@ -69,11 +67,6 @@ - - - - 5 + 6 0 $(BUILD_NUMBER) @@ -47,7 +47,6 @@ - @@ -77,11 +76,6 @@ - - - - 5.14.0 + 6.0.0 ServiceStack ServiceStack, Inc. © 2008-2022 ServiceStack, Inc @@ -24,7 +24,7 @@ true - + $(DefineConstants);NETFX;NET45 True False diff --git a/src/ServiceStack.Text.sln b/src/ServiceStack.Text.sln index 5b25d9f12..a2c32f26d 100644 --- a/src/ServiceStack.Text.sln +++ b/src/ServiceStack.Text.sln @@ -21,8 +21,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceStack.Text.Tests", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceStack.Text.TestsConsole", "..\tests\ServiceStack.Text.TestsConsole\ServiceStack.Text.TestsConsole.csproj", "{DD3BEB33-2509-423A-8545-CE1A83684530}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceStack.Memory", "ServiceStack.Memory\ServiceStack.Memory.csproj", "{7128E4DB-6C8B-4EFC-A04D-DDDF8A58BB53}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceStack.Text.Benchmarks", "..\tests\ServiceStack.Text.Benchmarks\ServiceStack.Text.Benchmarks.csproj", "{FF1E5617-FD95-496F-B591-17DA6E0B364F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Northwind.Common", "..\tests\Northwind.Common\Northwind.Common.csproj", "{573926E5-F332-462D-A740-31706708A032}" @@ -45,10 +43,6 @@ Global {DD3BEB33-2509-423A-8545-CE1A83684530}.Debug|Any CPU.Build.0 = Debug|Any CPU {DD3BEB33-2509-423A-8545-CE1A83684530}.Release|Any CPU.ActiveCfg = Release|Any CPU {DD3BEB33-2509-423A-8545-CE1A83684530}.Release|Any CPU.Build.0 = Release|Any CPU - {7128E4DB-6C8B-4EFC-A04D-DDDF8A58BB53}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7128E4DB-6C8B-4EFC-A04D-DDDF8A58BB53}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7128E4DB-6C8B-4EFC-A04D-DDDF8A58BB53}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7128E4DB-6C8B-4EFC-A04D-DDDF8A58BB53}.Release|Any CPU.Build.0 = Release|Any CPU {FF1E5617-FD95-496F-B591-17DA6E0B364F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FF1E5617-FD95-496F-B591-17DA6E0B364F}.Debug|Any CPU.Build.0 = Debug|Any CPU {FF1E5617-FD95-496F-B591-17DA6E0B364F}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/ServiceStack.Text/Env.cs b/src/ServiceStack.Text/Env.cs index 8ab386aa3..6ab3e42a5 100644 --- a/src/ServiceStack.Text/Env.cs +++ b/src/ServiceStack.Text/Env.cs @@ -117,7 +117,7 @@ static Env() public static string VersionString { get; set; } - public static decimal ServiceStackVersion = 5.14m; + public static decimal ServiceStackVersion = 6.00m; public static bool IsLinux { get; set; } diff --git a/src/ServiceStack.Text/MemoryProvider.cs b/src/ServiceStack.Text/MemoryProvider.cs index 60e60bad6..4876ebc20 100644 --- a/src/ServiceStack.Text/MemoryProvider.cs +++ b/src/ServiceStack.Text/MemoryProvider.cs @@ -13,7 +13,7 @@ public abstract class MemoryProvider { public static MemoryProvider Instance = #if NETCORE && !NETSTANDARD2_0 - ServiceStack.Memory.NetCoreMemory.Provider; + NetCoreMemory.Provider; #else DefaultMemory.Provider; #endif diff --git a/src/ServiceStack.Text/NetCoreMemory.cs b/src/ServiceStack.Text/NetCoreMemory.cs index 6ecaf62ee..7a2aaed51 100644 --- a/src/ServiceStack.Text/NetCoreMemory.cs +++ b/src/ServiceStack.Text/NetCoreMemory.cs @@ -7,11 +7,10 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -using ServiceStack.Text; using ServiceStack.Text.Common; using ServiceStack.Text.Pools; -namespace ServiceStack.Memory +namespace ServiceStack.Text { public sealed class NetCoreMemory : MemoryProvider { diff --git a/src/ServiceStack.Text/Properties/AssemblyInfo.cs b/src/ServiceStack.Text/Properties/AssemblyInfo.cs index 2136c0240..b64975d01 100644 --- a/src/ServiceStack.Text/Properties/AssemblyInfo.cs +++ b/src/ServiceStack.Text/Properties/AssemblyInfo.cs @@ -2,4 +2,4 @@ [assembly: ComVisible(false)] [assembly: Guid("a352d4d3-df2a-4c78-b646-67181a6333a6")] -[assembly: System.Reflection.AssemblyVersion("5.0.0.0")] +[assembly: System.Reflection.AssemblyVersion("6.0.0.0")] diff --git a/src/ServiceStack.Text/ServiceStack.Text.Core.csproj b/src/ServiceStack.Text/ServiceStack.Text.Core.csproj index f0cd48f9e..675c4cd0c 100644 --- a/src/ServiceStack.Text/ServiceStack.Text.Core.csproj +++ b/src/ServiceStack.Text/ServiceStack.Text.Core.csproj @@ -3,8 +3,7 @@ ServiceStack.Text.Core ServiceStack.Text ServiceStack.Text - - netstandard2.0;netcoreapp2.1;net6.0 + netstandard2.0;net6.0 ServiceStack.Text .NET Standard 2.0 .NET's fastest JSON, JSV and CSV Text Serializers. Fast, Light, Resilient. @@ -14,9 +13,6 @@ JSON;Text;Serializer;CSV;JSV;HTTP;Auto Mapping;Dump;Reflection;JS;Utils;Fast 1591 - - $(DefineConstants);NETCORE;NETCORE2_1 - $(DefineConstants);NETSTANDARD;NET6_0 @@ -28,11 +24,6 @@ - - - - - diff --git a/src/ServiceStack.Text/ServiceStack.Text.csproj b/src/ServiceStack.Text/ServiceStack.Text.csproj index ef46d4470..9b3ef5e7f 100644 --- a/src/ServiceStack.Text/ServiceStack.Text.csproj +++ b/src/ServiceStack.Text/ServiceStack.Text.csproj @@ -2,7 +2,7 @@ ServiceStack.Text ServiceStack.Text - net45;netstandard2.0;netcoreapp2.1;net6.0 + net472;netstandard2.0;net6.0 .NET's fastest JSON Serializer by ServiceStack .NET's fastest JSON, JSV and CSV Text Serializers. Fast, Light, Resilient. @@ -15,9 +15,6 @@ $(DefineConstants);NETCORE;NETSTANDARD;NETSTANDARD2_0 - - $(DefineConstants);NETCORE;NETCORE2_1 - $(DefineConstants);NETCORE;NET6_0;NET6_0_OR_GREATER @@ -25,18 +22,13 @@ - + - - - - - diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 02591eb88..187fbd040 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -1,7 +1,7 @@ - 5.14.0 + 6.0.0 latest false @@ -10,18 +10,6 @@ DEBUG - - $(DefineConstants);NETFX;NET45 - - - - $(DefineConstants);NETFX;NET46 - - - - $(DefineConstants);NETFX;NET461 - - $(DefineConstants);NETFX;NET472 @@ -30,15 +18,11 @@ $(DefineConstants);NETCORE;NETSTANDARD2_0 - - $(DefineConstants);NETSTANDARD;NETSTANDARD2_1 - - $(DefineConstants);NET6_0;NET6_0_OR_GREATER - + $(DefineConstants);NETCORE;NETCORE_SUPPORT diff --git a/tests/Northwind.Common/Northwind.Common.csproj b/tests/Northwind.Common/Northwind.Common.csproj index c575bd4b0..40ac58cdf 100644 --- a/tests/Northwind.Common/Northwind.Common.csproj +++ b/tests/Northwind.Common/Northwind.Common.csproj @@ -1,7 +1,7 @@  - net461;netstandard2.0 + net472;net6.0 Northwind.Common Northwind.Common false @@ -29,25 +29,18 @@ - + $(DefineConstants);NET45 - + - + $(DefineConstants);NETSTANDARD2_0 - - - - - - - \ No newline at end of file diff --git a/tests/ServiceStack.Text.Benchmarks/MemoryProviderBenchmarks.cs b/tests/ServiceStack.Text.Benchmarks/MemoryProviderBenchmarks.cs index 9f479a846..a2a97a6a4 100644 --- a/tests/ServiceStack.Text.Benchmarks/MemoryProviderBenchmarks.cs +++ b/tests/ServiceStack.Text.Benchmarks/MemoryProviderBenchmarks.cs @@ -1,7 +1,6 @@ using System; using System.Globalization; using BenchmarkDotNet.Attributes; -using ServiceStack.Memory; namespace ServiceStack.Text.Benchmarks { diff --git a/tests/ServiceStack.Text.Benchmarks/ServiceStack.Text.Benchmarks.csproj b/tests/ServiceStack.Text.Benchmarks/ServiceStack.Text.Benchmarks.csproj index e238d2b14..32aeb35ff 100644 --- a/tests/ServiceStack.Text.Benchmarks/ServiceStack.Text.Benchmarks.csproj +++ b/tests/ServiceStack.Text.Benchmarks/ServiceStack.Text.Benchmarks.csproj @@ -1,7 +1,7 @@  Exe - netcoreapp2.1 + net6.0 @@ -10,7 +10,7 @@ - + $(DefineConstants);NETSTANDARD;NETCORE;NETCORE2_1 \ No newline at end of file diff --git a/tests/ServiceStack.Text.Tests/DataContractTests.cs b/tests/ServiceStack.Text.Tests/DataContractTests.cs index 0027c1805..c4f689049 100644 --- a/tests/ServiceStack.Text.Tests/DataContractTests.cs +++ b/tests/ServiceStack.Text.Tests/DataContractTests.cs @@ -258,20 +258,6 @@ public void Json_Serialize_Should_Respects_DataContract_Field_Name_With_Bytes_De Assert.IsTrue (t.Bytes.AreEqual (new byte[] { 0x61, 0x62 })); } -#if !NETCORE - [Test] - public void Can_get_weak_DataMember() - { - var dto = new ClassOne(); - var dataMemberAttr = dto.GetType().GetProperty("Id").GetWeakDataMember(); - Assert.That(dataMemberAttr.Name, Is.Null); - - dataMemberAttr = dto.GetType().GetProperty("List").GetWeakDataMember(); - Assert.That(dataMemberAttr.Name, Is.EqualTo("listClassTwo")); - Assert.That(dataMemberAttr.Order, Is.EqualTo(1)); - } -#endif - [DataContract(Name = "my-class", Namespace = "http://schemas.ns.com")] public class MyClass { @@ -279,19 +265,6 @@ public class MyClass public string Title { get; set; } } -#if !NETCORE - [Test] - public void Can_get_weak_DataContract() - { - var mc = new MyClass { Title = "Some random title" }; - - var attr = mc.GetType().GetWeakDataContract(); - - Assert.That(attr.Name, Is.EqualTo("my-class")); - Assert.That(attr.Namespace, Is.EqualTo("http://schemas.ns.com")); - } -#endif - [Test] public void Does_use_DataMember_Name() { diff --git a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj index c1c8de7e1..3ffabe47b 100644 --- a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj +++ b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj @@ -54,7 +54,6 @@ $(DefineConstants);NETCORE;NETSTANDARD2_0 - From a4f579131c542b4505950adcd895bb16704e67a2 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 24 Jan 2022 21:08:49 +0800 Subject: [PATCH 33/75] Change #if NET45 to #if NETFX --- src/Directory.Build.props | 10 +--------- src/ServiceStack.Text/LicenseUtils.cs | 4 ++-- src/ServiceStack.Text/PclExport.Net45.cs | 2 +- src/ServiceStack.Text/PclExport.cs | 2 +- src/ServiceStack.Text/ReflectionOptimizer.Emit.cs | 2 +- src/ServiceStack.Text/ReflectionOptimizer.cs | 2 +- .../MiniProfiler/FlowData.cs | 4 ++-- .../RuntimeSerializationTests.cs | 2 +- 8 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index be1d46624..a118be095 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -25,24 +25,16 @@ - $(DefineConstants);NETFX;NET45 + $(DefineConstants);NETFX;NET45;NET472 True False ../servicestack.snk - - $(DefineConstants);NETFX;NET472 - - $(DefineConstants);NETSTANDARD;NETSTANDARD2_0 - - $(DefineConstants);NETSTANDARD;NETSTANDARD2_1 - - $(DefineConstants);NET6_0;NET6_0_OR_GREATER diff --git a/src/ServiceStack.Text/LicenseUtils.cs b/src/ServiceStack.Text/LicenseUtils.cs index e673e97eb..1a97e3e00 100644 --- a/src/ServiceStack.Text/LicenseUtils.cs +++ b/src/ServiceStack.Text/LicenseUtils.cs @@ -666,7 +666,7 @@ public static bool VerifySignedHash(byte[] DataToVerify, byte[] SignedData, Syst public static LicenseKey VerifyLicenseKeyText(string licenseKeyText) { -#if NET45 || NETCORE +#if NETFX || NETCORE LicenseKey key; try { @@ -686,7 +686,7 @@ public static LicenseKey VerifyLicenseKeyText(string licenseKeyText) private static void FromXml(this System.Security.Cryptography.RSA rsa, string xml) { -#if NET45 +#if NETFX rsa.FromXmlString(xml); #else //throws PlatformNotSupportedException diff --git a/src/ServiceStack.Text/PclExport.Net45.cs b/src/ServiceStack.Text/PclExport.Net45.cs index ffccdee83..73bc305be 100644 --- a/src/ServiceStack.Text/PclExport.Net45.cs +++ b/src/ServiceStack.Text/PclExport.Net45.cs @@ -1,4 +1,4 @@ -#if NET45 +#if NETFX using System; using System.Collections; using System.Collections.Concurrent; diff --git a/src/ServiceStack.Text/PclExport.cs b/src/ServiceStack.Text/PclExport.cs index 1f2a7e06c..c97a8be6b 100644 --- a/src/ServiceStack.Text/PclExport.cs +++ b/src/ServiceStack.Text/PclExport.cs @@ -27,7 +27,7 @@ public static class Platforms } public static PclExport Instance -#if NET45 +#if NETFX = new Net45PclExport() #elif NETSTANDARD2_0 = new NetStandardPclExport() diff --git a/src/ServiceStack.Text/ReflectionOptimizer.Emit.cs b/src/ServiceStack.Text/ReflectionOptimizer.Emit.cs index 6ffdd322a..cac57f78f 100644 --- a/src/ServiceStack.Text/ReflectionOptimizer.Emit.cs +++ b/src/ServiceStack.Text/ReflectionOptimizer.Emit.cs @@ -1,4 +1,4 @@ -#if NET45 || (NETCORE && !NETSTANDARD2_0) +#if NETFX || (NETCORE && !NETSTANDARD2_0) using System; using System.Linq; diff --git a/src/ServiceStack.Text/ReflectionOptimizer.cs b/src/ServiceStack.Text/ReflectionOptimizer.cs index bfac69d1b..0042abcbc 100644 --- a/src/ServiceStack.Text/ReflectionOptimizer.cs +++ b/src/ServiceStack.Text/ReflectionOptimizer.cs @@ -8,7 +8,7 @@ namespace ServiceStack.Text public abstract class ReflectionOptimizer { public static ReflectionOptimizer Instance = -#if NET45 || (NETCORE && !NETSTANDARD2_0) +#if NETFX || (NETCORE && !NETSTANDARD2_0) EmitReflectionOptimizer.Provider #else ExpressionReflectionOptimizer.Provider diff --git a/tests/ServiceStack.Text.Benchmarks/MiniProfiler/FlowData.cs b/tests/ServiceStack.Text.Benchmarks/MiniProfiler/FlowData.cs index 2455dc292..d03b10539 100644 --- a/tests/ServiceStack.Text.Benchmarks/MiniProfiler/FlowData.cs +++ b/tests/ServiceStack.Text.Benchmarks/MiniProfiler/FlowData.cs @@ -1,4 +1,4 @@ -#if NET45 +#if NETFX using System.Runtime.Remoting; using System.Runtime.Remoting.Messaging; #else @@ -16,7 +16,7 @@ namespace StackExchange.Profiling.Internal /// The type of data to store. public class FlowData { -#if NET45 +#if NETFX // Key specific to this type. #pragma warning disable RCS1158 // Avoid static members in generic types. private static readonly string _key = typeof(FlowData).FullName; diff --git a/tests/ServiceStack.Text.Tests/RuntimeSerializationTests.cs b/tests/ServiceStack.Text.Tests/RuntimeSerializationTests.cs index 655894b12..4b5dd3ff5 100644 --- a/tests/ServiceStack.Text.Tests/RuntimeSerializationTests.cs +++ b/tests/ServiceStack.Text.Tests/RuntimeSerializationTests.cs @@ -40,7 +40,7 @@ public class MetaType : IMeta public class RequestDto : IReturn {} -#if NET45 +#if NETFX [Serializable] public class SerialiazableType { } #endif From aa4836205664d12159ac2c308200efe1734133f7 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 24 Jan 2022 21:42:53 +0800 Subject: [PATCH 34/75] Update Env.cs --- src/ServiceStack.Text/Env.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceStack.Text/Env.cs b/src/ServiceStack.Text/Env.cs index 6ab3e42a5..e55876ebb 100644 --- a/src/ServiceStack.Text/Env.cs +++ b/src/ServiceStack.Text/Env.cs @@ -34,7 +34,7 @@ static Env() IsUnix = IsOSX || IsLinux; HasMultiplePlatformTargets = true; IsUWP = IsRunningAsUwp(); -#elif NET45 +#elif NETFX IsNetFramework = true; switch (Environment.OSVersion.Platform) { From 9c8dc8582fe9f4b7dc6fd12addf6727c152b7d4b Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 24 Jan 2022 23:28:15 +0800 Subject: [PATCH 35/75] Remove unnecessary net6.0 deps --- .../ServiceStack.Text.csproj | 3 -- .../ServiceStack.Text.TestsConsole/Program.cs | 30 +++++++++---------- .../ServiceStack.Text.TestsConsole.csproj | 4 +-- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/src/ServiceStack.Text/ServiceStack.Text.csproj b/src/ServiceStack.Text/ServiceStack.Text.csproj index 9b3ef5e7f..df4781b18 100644 --- a/src/ServiceStack.Text/ServiceStack.Text.csproj +++ b/src/ServiceStack.Text/ServiceStack.Text.csproj @@ -30,8 +30,5 @@ - - - \ No newline at end of file diff --git a/tests/ServiceStack.Text.TestsConsole/Program.cs b/tests/ServiceStack.Text.TestsConsole/Program.cs index cf13c08d4..d3836420e 100644 --- a/tests/ServiceStack.Text.TestsConsole/Program.cs +++ b/tests/ServiceStack.Text.TestsConsole/Program.cs @@ -5,7 +5,7 @@ using System.Reflection.Emit; using NUnit.Framework; using ServiceStack.Common.Tests; -using ServiceStack.OrmLite; +//using ServiceStack.OrmLite; using ServiceStack.Reflection; namespace ServiceStack.Text.TestsConsole @@ -14,7 +14,7 @@ class Program { public static void Main(string[] args) { - PrintDumpColumnSchema(); + // PrintDumpColumnSchema(); //var da = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName("dyn"), AssemblyBuilderAccess.Save); @@ -62,19 +62,19 @@ public void Compare_interpolation_vs_string_Concat() public static object SimpleConcat(string text) => "Hi " + text; - public static void PrintDumpColumnSchema() - { - var dbFactory = new OrmLiteConnectionFactory(":memory:", - SqliteDialect.Provider); - - using var db = dbFactory.Open(); - db.CreateTableIfNotExists(); - - ColumnSchema[] columnSchemas = db.GetTableColumns(); - - columnSchemas.Each(x => x.ToString().Print()); - columnSchemas.Each(x => x.PrintDump()); - } + // public static void PrintDumpColumnSchema() + // { + // var dbFactory = new OrmLiteConnectionFactory(":memory:", + // SqliteDialect.Provider); + // + // using var db = dbFactory.Open(); + // db.CreateTableIfNotExists(); + // + // ColumnSchema[] columnSchemas = db.GetTableColumns(); + // + // columnSchemas.Each(x => x.ToString().Print()); + // columnSchemas.Each(x => x.PrintDump()); + // } public class Person { diff --git a/tests/ServiceStack.Text.TestsConsole/ServiceStack.Text.TestsConsole.csproj b/tests/ServiceStack.Text.TestsConsole/ServiceStack.Text.TestsConsole.csproj index 7c573aa4a..ce48a1103 100644 --- a/tests/ServiceStack.Text.TestsConsole/ServiceStack.Text.TestsConsole.csproj +++ b/tests/ServiceStack.Text.TestsConsole/ServiceStack.Text.TestsConsole.csproj @@ -11,7 +11,7 @@ - - + + \ No newline at end of file From 52eddc2a510a2716887dcb47c721ca7430fcecb7 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 24 Jan 2022 23:29:48 +0800 Subject: [PATCH 36/75] Update ServiceStack.Text.Core.csproj --- src/ServiceStack.Text/ServiceStack.Text.Core.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/ServiceStack.Text/ServiceStack.Text.Core.csproj b/src/ServiceStack.Text/ServiceStack.Text.Core.csproj index 675c4cd0c..14f46e2db 100644 --- a/src/ServiceStack.Text/ServiceStack.Text.Core.csproj +++ b/src/ServiceStack.Text/ServiceStack.Text.Core.csproj @@ -25,8 +25,5 @@ - - - \ No newline at end of file From c00dd99c66c8510514937babb5aa984e59d2a8b4 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 24 Jan 2022 23:46:49 +0800 Subject: [PATCH 37/75] Update NuGet.Config --- NuGet.Config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NuGet.Config b/NuGet.Config index f3c213adf..42daf5f44 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,7 +1,7 @@ - + \ No newline at end of file From 477fbd86da5113efb92af945d8a1c42049b464a7 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 24 Jan 2022 23:49:29 +0800 Subject: [PATCH 38/75] Update Northwind.Common.csproj --- tests/Northwind.Common/Northwind.Common.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Northwind.Common/Northwind.Common.csproj b/tests/Northwind.Common/Northwind.Common.csproj index 40ac58cdf..ca01132a2 100644 --- a/tests/Northwind.Common/Northwind.Common.csproj +++ b/tests/Northwind.Common/Northwind.Common.csproj @@ -26,7 +26,7 @@ - + From cf6c381eaf748425d378b8489451f01f957345b0 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 24 Jan 2022 23:50:01 +0800 Subject: [PATCH 39/75] Update ServiceStack.Text.Tests.csproj --- tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj index 3ffabe47b..108ce3a8d 100644 --- a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj +++ b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj @@ -35,7 +35,7 @@ - + $(DefineConstants);NETFX From 2ed1a4f29182c30e559d1e418b2313500c00e62a Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Tue, 25 Jan 2022 20:36:10 +0800 Subject: [PATCH 40/75] Current version doesn't exist in SS.Text test projs --- tests/Northwind.Common/Northwind.Common.csproj | 2 +- tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj | 2 +- .../ServiceStack.Text.TestsConsole.csproj | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Northwind.Common/Northwind.Common.csproj b/tests/Northwind.Common/Northwind.Common.csproj index ca01132a2..3d8f226f4 100644 --- a/tests/Northwind.Common/Northwind.Common.csproj +++ b/tests/Northwind.Common/Northwind.Common.csproj @@ -26,7 +26,7 @@ - + diff --git a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj index 108ce3a8d..00dadf020 100644 --- a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj +++ b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj @@ -35,7 +35,7 @@ - + $(DefineConstants);NETFX diff --git a/tests/ServiceStack.Text.TestsConsole/ServiceStack.Text.TestsConsole.csproj b/tests/ServiceStack.Text.TestsConsole/ServiceStack.Text.TestsConsole.csproj index ce48a1103..2db98f522 100644 --- a/tests/ServiceStack.Text.TestsConsole/ServiceStack.Text.TestsConsole.csproj +++ b/tests/ServiceStack.Text.TestsConsole/ServiceStack.Text.TestsConsole.csproj @@ -11,7 +11,7 @@ - - + + \ No newline at end of file From 296c7a84bdc18f9671834c5806d610b67a9f2faa Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Thu, 27 Jan 2022 19:34:13 +0800 Subject: [PATCH 41/75] Update Env.cs --- src/ServiceStack.Text/Env.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ServiceStack.Text/Env.cs b/src/ServiceStack.Text/Env.cs index e55876ebb..0df2bedf5 100644 --- a/src/ServiceStack.Text/Env.cs +++ b/src/ServiceStack.Text/Env.cs @@ -117,7 +117,7 @@ static Env() public static string VersionString { get; set; } - public static decimal ServiceStackVersion = 6.00m; + public static decimal ServiceStackVersion = 6.01m; public static bool IsLinux { get; set; } From 73fedf1a499bb85afa4c99486cc95e611a6eede6 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Fri, 28 Jan 2022 08:34:58 +0800 Subject: [PATCH 42/75] Refactor HttpUtils into different classes to prepare for alt net6 impl --- src/ServiceStack.Text/Env.cs | 15 +- src/ServiceStack.Text/HttpHeaders.cs | 213 ++ src/ServiceStack.Text/HttpMethods.cs | 32 + src/ServiceStack.Text/HttpUtils.Legacy.cs | 1243 ++++++++++++ src/ServiceStack.Text/HttpUtils.cs | 2227 ++------------------- src/ServiceStack.Text/MimeTypes.cs | 452 +++++ 6 files changed, 2098 insertions(+), 2084 deletions(-) create mode 100644 src/ServiceStack.Text/HttpHeaders.cs create mode 100644 src/ServiceStack.Text/HttpMethods.cs create mode 100644 src/ServiceStack.Text/HttpUtils.Legacy.cs create mode 100644 src/ServiceStack.Text/MimeTypes.cs diff --git a/src/ServiceStack.Text/Env.cs b/src/ServiceStack.Text/Env.cs index 0df2bedf5..edf3a4b2b 100644 --- a/src/ServiceStack.Text/Env.cs +++ b/src/ServiceStack.Text/Env.cs @@ -104,17 +104,16 @@ static Env() VersionString = ServiceStackVersion.ToString(CultureInfo.InvariantCulture); ServerUserAgent = "ServiceStack/" - + VersionString + " " - + PclExport.Instance.PlatformName - + (IsMono ? "/Mono" : "") - + (IsLinux ? "/Linux" : IsOSX ? "/OSX" : IsUnix ? "/Unix" : IsWindows ? "/Windows" : "/UnknownOS") - + (IsIOS ? "/iOS" : IsAndroid ? "/Android" : IsUWP ? "/UWP" : "") - + (IsNet6 ? "/net6" : IsNetStandard20 ? "/std2.0" : IsNetFramework ? "netfx" : "") - + ($"/{LicenseUtils.Info}"); - + + VersionString + " " + + PclExport.Instance.PlatformName + + (IsLinux ? "/Linux" : IsOSX ? "/OSX" : IsUnix ? "/Unix" : IsWindows ? "/Windows" : "/UnknownOS") + (IsIOS ? "/iOS" : IsAndroid ? "/Android" : IsUWP ? "/UWP" : "") + + PlatformModifier + + $"/{LicenseUtils.Info}"; __releaseDate = new DateTime(2001,01,01); } + public static string PlatformModifier => (IsNet6 ? "/net6" : IsNetStandard20 ? "/std2.0" : IsNetFramework ? "/netfx" : "") + (IsMono ? "/Mono" : ""); + public static string VersionString { get; set; } public static decimal ServiceStackVersion = 6.01m; diff --git a/src/ServiceStack.Text/HttpHeaders.cs b/src/ServiceStack.Text/HttpHeaders.cs new file mode 100644 index 000000000..8cdfeace3 --- /dev/null +++ b/src/ServiceStack.Text/HttpHeaders.cs @@ -0,0 +1,213 @@ +using System; +using System.Collections.Generic; + +namespace ServiceStack; + +public static class HttpHeaders +{ + public const string XParamOverridePrefix = "X-Param-Override-"; + + public const string XHttpMethodOverride = "X-Http-Method-Override"; + + public const string XAutoBatchCompleted = "X-AutoBatch-Completed"; // How many requests were completed before first failure + + public const string XTag = "X-Tag"; + + public const string XUserAuthId = "X-UAId"; + + public const string XTrigger = "X-Trigger"; // Trigger Events on UserAgent + + public const string XForwardedFor = "X-Forwarded-For"; // IP Address + + public const string XForwardedPort = "X-Forwarded-Port"; // 80 + + public const string XForwardedProtocol = "X-Forwarded-Proto"; // http or https + + public const string XRealIp = "X-Real-IP"; + + public const string XLocation = "X-Location"; + + public const string XStatus = "X-Status"; + + public const string XPoweredBy = "X-Powered-By"; + + public const string Referer = "Referer"; + + public const string CacheControl = "Cache-Control"; + + public const string IfModifiedSince = "If-Modified-Since"; + + public const string IfUnmodifiedSince = "If-Unmodified-Since"; + + public const string IfNoneMatch = "If-None-Match"; + + public const string IfMatch = "If-Match"; + + public const string LastModified = "Last-Modified"; + + public const string Accept = "Accept"; + + public const string AcceptEncoding = "Accept-Encoding"; + + public const string ContentType = "Content-Type"; + + public const string ContentEncoding = "Content-Encoding"; + + public const string ContentLength = "Content-Length"; + + public const string ContentDisposition = "Content-Disposition"; + + public const string Location = "Location"; + + public const string SetCookie = "Set-Cookie"; + + public const string ETag = "ETag"; + + public const string Age = "Age"; + + public const string Expires = "Expires"; + + public const string Vary = "Vary"; + + public const string Authorization = "Authorization"; + + public const string WwwAuthenticate = "WWW-Authenticate"; + + public const string AllowOrigin = "Access-Control-Allow-Origin"; + + public const string AllowMethods = "Access-Control-Allow-Methods"; + + public const string AllowHeaders = "Access-Control-Allow-Headers"; + + public const string AllowCredentials = "Access-Control-Allow-Credentials"; + + public const string ExposeHeaders = "Access-Control-Expose-Headers"; + + public const string AccessControlMaxAge = "Access-Control-Max-Age"; + + public const string Origin = "Origin"; + + public const string RequestMethod = "Access-Control-Request-Method"; + + public const string RequestHeaders = "Access-Control-Request-Headers"; + + public const string AcceptRanges = "Accept-Ranges"; + + public const string ContentRange = "Content-Range"; + + public const string Range = "Range"; + + public const string SOAPAction = "SOAPAction"; + + public const string Allow = "Allow"; + + public const string AcceptCharset = "Accept-Charset"; + + public const string AcceptLanguage = "Accept-Language"; + + public const string Connection = "Connection"; + + public const string Cookie = "Cookie"; + + public const string ContentLanguage = "Content-Language"; + + public const string Expect = "Expect"; + + public const string Pragma = "Pragma"; + + public const string ProxyAuthenticate = "Proxy-Authenticate"; + + public const string ProxyAuthorization = "Proxy-Authorization"; + + public const string ProxyConnection = "Proxy-Connection"; + + public const string SetCookie2 = "Set-Cookie2"; + + public const string TE = "TE"; + + public const string Trailer = "Trailer"; + + public const string TransferEncoding = "Transfer-Encoding"; + + public const string Upgrade = "Upgrade"; + + public const string Via = "Via"; + + public const string Warning = "Warning"; + + public const string Date = "Date"; + public const string Host = "Host"; + public const string UserAgent = "User-Agent"; + + public static HashSet RestrictedHeaders = new(StringComparer.OrdinalIgnoreCase) + { + Accept, + Connection, + ContentLength, + ContentType, + Date, + Expect, + Host, + IfModifiedSince, + Range, + Referer, + TransferEncoding, + UserAgent, + ProxyConnection, + }; +} + + +public static class CompressionTypes +{ + public static readonly string[] AllCompressionTypes = + { +#if NET6_0_OR_GREATER + Brotli, +#endif + Deflate, + GZip, + }; + +#if NET6_0_OR_GREATER + public const string Default = Brotli; +#else + public const string Default = Deflate; +#endif + + public const string Brotli = "br"; + public const string Deflate = "deflate"; + public const string GZip = "gzip"; + + public static bool IsValid(string compressionType) + { +#if NET6_0_OR_GREATER + return compressionType is Deflate or GZip or Brotli; +#else + return compressionType is Deflate or GZip; +#endif + } + + public static void AssertIsValid(string compressionType) + { + if (!IsValid(compressionType)) + { + throw new NotSupportedException(compressionType + + " is not a supported compression type. Valid types: " + string.Join(", ", AllCompressionTypes)); + } + } + + public static string GetExtension(string compressionType) + { + switch (compressionType) + { + case Brotli: + case Deflate: + case GZip: + return "." + compressionType; + default: + throw new NotSupportedException( + "Unknown compressionType: " + compressionType); + } + } +} \ No newline at end of file diff --git a/src/ServiceStack.Text/HttpMethods.cs b/src/ServiceStack.Text/HttpMethods.cs new file mode 100644 index 000000000..af2427998 --- /dev/null +++ b/src/ServiceStack.Text/HttpMethods.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace ServiceStack; + +public static class HttpMethods +{ + static readonly string[] allVerbs = { + "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", // RFC 2616 + "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", // RFC 2518 + "VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", "UNCHECKOUT", + "MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY", // RFC 3253 + "ORDERPATCH", // RFC 3648 + "ACL", // RFC 3744 + "PATCH", // https://datatracker.ietf.org/doc/draft-dusseault-http-patch/ + "SEARCH", // https://datatracker.ietf.org/doc/draft-reschke-webdav-search/ + "BCOPY", "BDELETE", "BMOVE", "BPROPFIND", "BPROPPATCH", "NOTIFY", + "POLL", "SUBSCRIBE", "UNSUBSCRIBE" //MS Exchange WebDav: http://msdn.microsoft.com/en-us/library/aa142917.aspx + }; + + public static HashSet AllVerbs = new(allVerbs); + + public static bool Exists(string httpMethod) => AllVerbs.Contains(httpMethod.ToUpper()); + public static bool HasVerb(string httpVerb) => Exists(httpVerb); + + public const string Get = "GET"; + public const string Put = "PUT"; + public const string Post = "POST"; + public const string Delete = "DELETE"; + public const string Options = "OPTIONS"; + public const string Head = "HEAD"; + public const string Patch = "PATCH"; +} \ No newline at end of file diff --git a/src/ServiceStack.Text/HttpUtils.Legacy.cs b/src/ServiceStack.Text/HttpUtils.Legacy.cs new file mode 100644 index 000000000..4bb8ba9d9 --- /dev/null +++ b/src/ServiceStack.Text/HttpUtils.Legacy.cs @@ -0,0 +1,1243 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using ServiceStack.Text; + +namespace ServiceStack; + +public static partial class HttpUtils +{ + [ThreadStatic] + public static IHttpResultsFilter ResultsFilter; + + public static string GetJsonFromUrl(this string url, + Action requestFilter = null, Action responseFilter = null) + { + return url.GetStringFromUrl(MimeTypes.Json, requestFilter, responseFilter); + } + + public static Task GetJsonFromUrlAsync(this string url, + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return url.GetStringFromUrlAsync(MimeTypes.Json, requestFilter, responseFilter, token: token); + } + + public static string GetXmlFromUrl(this string url, + Action requestFilter = null, Action responseFilter = null) + { + return url.GetStringFromUrl(MimeTypes.Xml, requestFilter, responseFilter); + } + + public static Task GetXmlFromUrlAsync(this string url, + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return url.GetStringFromUrlAsync(MimeTypes.Xml, requestFilter, responseFilter, token: token); + } + + public static string GetCsvFromUrl(this string url, + Action requestFilter = null, Action responseFilter = null) + { + return url.GetStringFromUrl(MimeTypes.Csv, requestFilter, responseFilter); + } + + public static Task GetCsvFromUrlAsync(this string url, + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return url.GetStringFromUrlAsync(MimeTypes.Csv, requestFilter, responseFilter, token: token); + } + + public static string GetStringFromUrl(this string url, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task GetStringFromUrlAsync(this string url, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, + token: token); + } + + public static string PostStringToUrl(this string url, string requestBody = null, + string contentType = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "POST", + requestBody: requestBody, contentType: contentType, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PostStringToUrlAsync(this string url, string requestBody = null, + string contentType = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "POST", + requestBody: requestBody, contentType: contentType, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PostToUrl(this string url, string formData = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "POST", + contentType: MimeTypes.FormUrlEncoded, requestBody: formData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PostToUrlAsync(this string url, string formData = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "POST", + contentType: MimeTypes.FormUrlEncoded, requestBody: formData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PostToUrl(this string url, object formData = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + string postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; + + return SendStringToUrl(url, method: "POST", + contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PostToUrlAsync(this string url, object formData = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + string postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; + + return SendStringToUrlAsync(url, method: "POST", + contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PostJsonToUrl(this string url, string json, + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "POST", requestBody: json, contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PostJsonToUrlAsync(this string url, string json, + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "POST", requestBody: json, contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PostJsonToUrl(this string url, object data, + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "POST", requestBody: data.ToJson(), contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PostJsonToUrlAsync(this string url, object data, + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "POST", requestBody: data.ToJson(), contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PostXmlToUrl(this string url, string xml, + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "POST", requestBody: xml, contentType: MimeTypes.Xml, accept: MimeTypes.Xml, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PostXmlToUrlAsync(this string url, string xml, + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "POST", requestBody: xml, contentType: MimeTypes.Xml, + accept: MimeTypes.Xml, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PostCsvToUrl(this string url, string csv, + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "POST", requestBody: csv, contentType: MimeTypes.Csv, accept: MimeTypes.Csv, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PostCsvToUrlAsync(this string url, string csv, + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "POST", requestBody: csv, contentType: MimeTypes.Csv, + accept: MimeTypes.Csv, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PutStringToUrl(this string url, string requestBody = null, + string contentType = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "PUT", + requestBody: requestBody, contentType: contentType, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PutStringToUrlAsync(this string url, string requestBody = null, + string contentType = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PUT", + requestBody: requestBody, contentType: contentType, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PutToUrl(this string url, string formData = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "PUT", + contentType: MimeTypes.FormUrlEncoded, requestBody: formData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PutToUrlAsync(this string url, string formData = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PUT", + contentType: MimeTypes.FormUrlEncoded, requestBody: formData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PutToUrl(this string url, object formData = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + string postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; + + return SendStringToUrl(url, method: "PUT", + contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PutToUrlAsync(this string url, object formData = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + string postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; + + return SendStringToUrlAsync(url, method: "PUT", + contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PutJsonToUrl(this string url, string json, + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "PUT", requestBody: json, contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PutJsonToUrlAsync(this string url, string json, + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PUT", requestBody: json, contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PutJsonToUrl(this string url, object data, + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "PUT", requestBody: data.ToJson(), contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PutJsonToUrlAsync(this string url, object data, + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PUT", requestBody: data.ToJson(), contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PutXmlToUrl(this string url, string xml, + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "PUT", requestBody: xml, contentType: MimeTypes.Xml, accept: MimeTypes.Xml, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PutXmlToUrlAsync(this string url, string xml, + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PUT", requestBody: xml, contentType: MimeTypes.Xml, + accept: MimeTypes.Xml, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PutCsvToUrl(this string url, string csv, + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "PUT", requestBody: csv, contentType: MimeTypes.Csv, accept: MimeTypes.Csv, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PutCsvToUrlAsync(this string url, string csv, + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PUT", requestBody: csv, contentType: MimeTypes.Csv, + accept: MimeTypes.Csv, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PatchStringToUrl(this string url, string requestBody = null, + string contentType = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "PATCH", + requestBody: requestBody, contentType: contentType, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PatchStringToUrlAsync(this string url, string requestBody = null, + string contentType = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PATCH", + requestBody: requestBody, contentType: contentType, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PatchToUrl(this string url, string formData = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "PATCH", + contentType: MimeTypes.FormUrlEncoded, requestBody: formData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PatchToUrlAsync(this string url, string formData = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PATCH", + contentType: MimeTypes.FormUrlEncoded, requestBody: formData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PatchToUrl(this string url, object formData = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + string postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; + + return SendStringToUrl(url, method: "PATCH", + contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PatchToUrlAsync(this string url, object formData = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + string postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; + + return SendStringToUrlAsync(url, method: "PATCH", + contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PatchJsonToUrl(this string url, string json, + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "PATCH", requestBody: json, contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PatchJsonToUrlAsync(this string url, string json, + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PATCH", requestBody: json, contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PatchJsonToUrl(this string url, object data, + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "PATCH", requestBody: data.ToJson(), contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PatchJsonToUrlAsync(this string url, object data, + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PATCH", requestBody: data.ToJson(), contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string DeleteFromUrl(this string url, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "DELETE", accept: accept, requestFilter: requestFilter, + responseFilter: responseFilter); + } + + public static Task DeleteFromUrlAsync(this string url, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "DELETE", accept: accept, requestFilter: requestFilter, + responseFilter: responseFilter, token: token); + } + + public static string OptionsFromUrl(this string url, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "OPTIONS", accept: accept, requestFilter: requestFilter, + responseFilter: responseFilter); + } + + public static Task OptionsFromUrlAsync(this string url, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "OPTIONS", accept: accept, requestFilter: requestFilter, + responseFilter: responseFilter, token: token); + } + + public static string HeadFromUrl(this string url, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "HEAD", accept: accept, requestFilter: requestFilter, + responseFilter: responseFilter); + } + + public static Task HeadFromUrlAsync(this string url, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "HEAD", accept: accept, requestFilter: requestFilter, + responseFilter: responseFilter, token: token); + } + + public static string SendStringToUrl(this string url, string method = null, + string requestBody = null, string contentType = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + var webReq = (HttpWebRequest)WebRequest.Create(url); + if (method != null) + webReq.Method = method; + if (contentType != null) + webReq.ContentType = contentType; + + webReq.Accept = accept; + PclExport.Instance.AddCompression(webReq); + + requestFilter?.Invoke(webReq); + + if (ResultsFilter != null) + { + return ResultsFilter.GetString(webReq, requestBody); + } + + if (requestBody != null) + { + using var reqStream = PclExport.Instance.GetRequestStream(webReq); + using var writer = new StreamWriter(reqStream, UseEncoding); + writer.Write(requestBody); + } + else if (method != null && HasRequestBody(method)) + { + webReq.ContentLength = 0; + } + + using var webRes = webReq.GetResponse(); + using var stream = webRes.GetResponseStream(); + responseFilter?.Invoke((HttpWebResponse)webRes); + return stream.ReadToEnd(UseEncoding); + } + + public static async Task SendStringToUrlAsync(this string url, string method = null, + string requestBody = null, + string contentType = null, string accept = "*/*", Action requestFilter = null, + Action responseFilter = null, CancellationToken token = default) + { + var webReq = (HttpWebRequest)WebRequest.Create(url); + if (method != null) + webReq.Method = method; + if (contentType != null) + webReq.ContentType = contentType; + + webReq.Accept = accept; + PclExport.Instance.AddCompression(webReq); + + requestFilter?.Invoke(webReq); + + if (ResultsFilter != null) + { + var result = ResultsFilter.GetString(webReq, requestBody); + return result; + } + + if (requestBody != null) + { + using var reqStream = PclExport.Instance.GetRequestStream(webReq); + using var writer = new StreamWriter(reqStream, UseEncoding); + await writer.WriteAsync(requestBody).ConfigAwait(); + } + else if (method != null && HasRequestBody(method)) + { + webReq.ContentLength = 0; + } + + using var webRes = await webReq.GetResponseAsync().ConfigAwait(); + responseFilter?.Invoke((HttpWebResponse)webRes); + using var stream = webRes.GetResponseStream(); + return await stream.ReadToEndAsync().ConfigAwait(); + } + + public static byte[] GetBytesFromUrl(this string url, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + return url.SendBytesToUrl(accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task GetBytesFromUrlAsync(this string url, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return url.SendBytesToUrlAsync(accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, + token: token); + } + + public static byte[] PostBytesToUrl(this string url, byte[] requestBody = null, string contentType = null, + string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + return SendBytesToUrl(url, method: "POST", + contentType: contentType, requestBody: requestBody, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PostBytesToUrlAsync(this string url, byte[] requestBody = null, + string contentType = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendBytesToUrlAsync(url, method: "POST", + contentType: contentType, requestBody: requestBody, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static byte[] PutBytesToUrl(this string url, byte[] requestBody = null, string contentType = null, + string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + return SendBytesToUrl(url, method: "PUT", + contentType: contentType, requestBody: requestBody, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PutBytesToUrlAsync(this string url, byte[] requestBody = null, string contentType = null, + string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendBytesToUrlAsync(url, method: "PUT", + contentType: contentType, requestBody: requestBody, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static byte[] SendBytesToUrl(this string url, string method = null, + byte[] requestBody = null, string contentType = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + var webReq = (HttpWebRequest)WebRequest.Create(url); + if (method != null) + webReq.Method = method; + + if (contentType != null) + webReq.ContentType = contentType; + + webReq.Accept = accept; + PclExport.Instance.AddCompression(webReq); + + requestFilter?.Invoke(webReq); + + if (ResultsFilter != null) + { + return ResultsFilter.GetBytes(webReq, requestBody); + } + + if (requestBody != null) + { + using var req = PclExport.Instance.GetRequestStream(webReq); + req.Write(requestBody, 0, requestBody.Length); + } + + using var webRes = PclExport.Instance.GetResponse(webReq); + responseFilter?.Invoke((HttpWebResponse)webRes); + + using var stream = webRes.GetResponseStream(); + return stream.ReadFully(); + } + + public static async Task SendBytesToUrlAsync(this string url, string method = null, + byte[] requestBody = null, string contentType = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + var webReq = (HttpWebRequest)WebRequest.Create(url); + if (method != null) + webReq.Method = method; + if (contentType != null) + webReq.ContentType = contentType; + + webReq.Accept = accept; + PclExport.Instance.AddCompression(webReq); + + requestFilter?.Invoke(webReq); + + if (ResultsFilter != null) + { + var result = ResultsFilter.GetBytes(webReq, requestBody); + return result; + } + + if (requestBody != null) + { + using var req = PclExport.Instance.GetRequestStream(webReq); + await req.WriteAsync(requestBody, 0, requestBody.Length, token).ConfigAwait(); + } + + var webRes = await webReq.GetResponseAsync().ConfigAwait(); + responseFilter?.Invoke((HttpWebResponse)webRes); + + using var stream = webRes.GetResponseStream(); + return await stream.ReadFullyAsync(token).ConfigAwait(); + } + + public static Stream GetStreamFromUrl(this string url, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + return url.SendStreamToUrl(accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task GetStreamFromUrlAsync(this string url, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return url.SendStreamToUrlAsync(accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, + token: token); + } + + public static Stream PostStreamToUrl(this string url, Stream requestBody = null, string contentType = null, + string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + return SendStreamToUrl(url, method: "POST", + contentType: contentType, requestBody: requestBody, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PostStreamToUrlAsync(this string url, Stream requestBody = null, + string contentType = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStreamToUrlAsync(url, method: "POST", + contentType: contentType, requestBody: requestBody, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static Stream PutStreamToUrl(this string url, Stream requestBody = null, string contentType = null, + string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + return SendStreamToUrl(url, method: "PUT", + contentType: contentType, requestBody: requestBody, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PutStreamToUrlAsync(this string url, Stream requestBody = null, + string contentType = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + return SendStreamToUrlAsync(url, method: "PUT", + contentType: contentType, requestBody: requestBody, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + /// + /// Returns HttpWebResponse Stream which must be disposed + /// + public static Stream SendStreamToUrl(this string url, string method = null, + Stream requestBody = null, string contentType = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null) + { + var webReq = (HttpWebRequest)WebRequest.Create(url); + if (method != null) + webReq.Method = method; + + if (contentType != null) + webReq.ContentType = contentType; + + webReq.Accept = accept; + PclExport.Instance.AddCompression(webReq); + + requestFilter?.Invoke(webReq); + + if (ResultsFilter != null) + { + return new MemoryStream(ResultsFilter.GetBytes(webReq, requestBody.ReadFully())); + } + + if (requestBody != null) + { + using var req = PclExport.Instance.GetRequestStream(webReq); + requestBody.CopyTo(req); + } + + var webRes = PclExport.Instance.GetResponse(webReq); + responseFilter?.Invoke((HttpWebResponse)webRes); + + var stream = webRes.GetResponseStream(); + return stream; + } + + /// + /// Returns HttpWebResponse Stream which must be disposed + /// + public static async Task SendStreamToUrlAsync(this string url, string method = null, + Stream requestBody = null, string contentType = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + var webReq = (HttpWebRequest)WebRequest.Create(url); + if (method != null) + webReq.Method = method; + if (contentType != null) + webReq.ContentType = contentType; + + webReq.Accept = accept; + PclExport.Instance.AddCompression(webReq); + + requestFilter?.Invoke(webReq); + + if (ResultsFilter != null) + { + return new MemoryStream(ResultsFilter.GetBytes(webReq, + await requestBody.ReadFullyAsync(token).ConfigAwait())); + } + + if (requestBody != null) + { + using var req = PclExport.Instance.GetRequestStream(webReq); + await requestBody.CopyToAsync(req, token).ConfigAwait(); + } + + var webRes = await webReq.GetResponseAsync().ConfigAwait(); + responseFilter?.Invoke((HttpWebResponse)webRes); + + var stream = webRes.GetResponseStream(); + return stream; + } + + public static bool IsAny300(this Exception ex) + { + var status = ex.GetStatus(); + return status >= HttpStatusCode.MultipleChoices && status < HttpStatusCode.BadRequest; + } + + public static bool IsAny400(this Exception ex) + { + var status = ex.GetStatus(); + return status >= HttpStatusCode.BadRequest && status < HttpStatusCode.InternalServerError; + } + + public static bool IsAny500(this Exception ex) + { + var status = ex.GetStatus(); + return status >= HttpStatusCode.InternalServerError && (int)status < 600; + } + + public static bool IsNotModified(this Exception ex) + { + return GetStatus(ex) == HttpStatusCode.NotModified; + } + + public static bool IsBadRequest(this Exception ex) + { + return GetStatus(ex) == HttpStatusCode.BadRequest; + } + + public static bool IsNotFound(this Exception ex) + { + return GetStatus(ex) == HttpStatusCode.NotFound; + } + + public static bool IsUnauthorized(this Exception ex) + { + return GetStatus(ex) == HttpStatusCode.Unauthorized; + } + + public static bool IsForbidden(this Exception ex) + { + return GetStatus(ex) == HttpStatusCode.Forbidden; + } + + public static bool IsInternalServerError(this Exception ex) + { + return GetStatus(ex) == HttpStatusCode.InternalServerError; + } + + public static HttpStatusCode? GetResponseStatus(this string url) + { + try + { + var webReq = (HttpWebRequest)WebRequest.Create(url); + using (var webRes = PclExport.Instance.GetResponse(webReq)) + { + var httpRes = webRes as HttpWebResponse; + return httpRes?.StatusCode; + } + } + catch (Exception ex) + { + return ex.GetStatus(); + } + } + + public static HttpStatusCode? GetStatus(this Exception ex) + { + if (ex == null) + return null; + + if (ex is WebException webEx) + return GetStatus(webEx); + + if (ex is IHasStatusCode hasStatus) + return (HttpStatusCode)hasStatus.StatusCode; + + return null; + } + + public static HttpStatusCode? GetStatus(this WebException webEx) + { + var httpRes = webEx?.Response as HttpWebResponse; + return httpRes?.StatusCode; + } + + public static bool HasStatus(this Exception ex, HttpStatusCode statusCode) + { + return GetStatus(ex) == statusCode; + } + + public static string GetResponseBody(this Exception ex) + { + if (!(ex is WebException webEx) || webEx.Response == null || webEx.Status != WebExceptionStatus.ProtocolError) + return null; + + var errorResponse = (HttpWebResponse)webEx.Response; + using var responseStream = errorResponse.GetResponseStream(); + return responseStream.ReadToEnd(UseEncoding); + } + + public static async Task GetResponseBodyAsync(this Exception ex, CancellationToken token = default) + { + if (!(ex is WebException webEx) || webEx.Response == null || webEx.Status != WebExceptionStatus.ProtocolError) + return null; + + var errorResponse = (HttpWebResponse)webEx.Response; + using var responseStream = errorResponse.GetResponseStream(); + return await responseStream.ReadToEndAsync(UseEncoding).ConfigAwait(); + } + + public static string ReadToEnd(this WebResponse webRes) + { + using var stream = webRes.GetResponseStream(); + return stream.ReadToEnd(UseEncoding); + } + + public static Task ReadToEndAsync(this WebResponse webRes) + { + using var stream = webRes.GetResponseStream(); + return stream.ReadToEndAsync(UseEncoding); + } + + public static IEnumerable ReadLines(this WebResponse webRes) + { + using var stream = webRes.GetResponseStream(); + using var reader = new StreamReader(stream, UseEncoding, true, 1024, leaveOpen: true); + string line; + while ((line = reader.ReadLine()) != null) + { + yield return line; + } + } + + public static HttpWebResponse GetErrorResponse(this string url) + { + try + { + var webReq = WebRequest.Create(url); + using var webRes = PclExport.Instance.GetResponse(webReq); + webRes.ReadToEnd(); + return null; + } + catch (WebException webEx) + { + return (HttpWebResponse)webEx.Response; + } + } + + public static async Task GetErrorResponseAsync(this string url) + { + try + { + var webReq = WebRequest.Create(url); + using var webRes = await webReq.GetResponseAsync().ConfigAwait(); + await webRes.ReadToEndAsync().ConfigAwait(); + return null; + } + catch (WebException webEx) + { + return (HttpWebResponse)webEx.Response; + } + } + + public static void UploadFile(this WebRequest webRequest, Stream fileStream, string fileName, string mimeType, + string accept = null, Action requestFilter = null, string method = "POST", + string field = "file") + { + var httpReq = (HttpWebRequest)webRequest; + httpReq.Method = method; + + if (accept != null) + httpReq.Accept = accept; + + requestFilter?.Invoke(httpReq); + + var boundary = Guid.NewGuid().ToString("N"); + + httpReq.ContentType = "multipart/form-data; boundary=\"" + boundary + "\""; + + var boundaryBytes = ("\r\n--" + boundary + "--\r\n").ToAsciiBytes(); + + var headerBytes = GetHeaderBytes(fileName, mimeType, field, boundary); + + var contentLength = fileStream.Length + headerBytes.Length + boundaryBytes.Length; + PclExport.Instance.InitHttpWebRequest(httpReq, + contentLength: contentLength, allowAutoRedirect: false, keepAlive: false); + + if (ResultsFilter != null) + { + ResultsFilter.UploadStream(httpReq, fileStream, fileName); + return; + } + + using var outputStream = PclExport.Instance.GetRequestStream(httpReq); + outputStream.Write(headerBytes, 0, headerBytes.Length); + fileStream.CopyTo(outputStream, 4096); + outputStream.Write(boundaryBytes, 0, boundaryBytes.Length); + PclExport.Instance.CloseStream(outputStream); + } + + public static async Task UploadFileAsync(this WebRequest webRequest, Stream fileStream, string fileName, + string mimeType, + string accept = null, Action requestFilter = null, string method = "POST", + string field = "file", + CancellationToken token = default) + { + var httpReq = (HttpWebRequest)webRequest; + httpReq.Method = method; + + if (accept != null) + httpReq.Accept = accept; + + requestFilter?.Invoke(httpReq); + + var boundary = Guid.NewGuid().ToString("N"); + + httpReq.ContentType = "multipart/form-data; boundary=\"" + boundary + "\""; + + var boundaryBytes = ("\r\n--" + boundary + "--\r\n").ToAsciiBytes(); + + var headerBytes = GetHeaderBytes(fileName, mimeType, field, boundary); + + var contentLength = fileStream.Length + headerBytes.Length + boundaryBytes.Length; + PclExport.Instance.InitHttpWebRequest(httpReq, + contentLength: contentLength, allowAutoRedirect: false, keepAlive: false); + + if (ResultsFilter != null) + { + ResultsFilter.UploadStream(httpReq, fileStream, fileName); + return; + } + + using var outputStream = PclExport.Instance.GetRequestStream(httpReq); + await outputStream.WriteAsync(headerBytes, 0, headerBytes.Length, token).ConfigAwait(); + await fileStream.CopyToAsync(outputStream, 4096, token).ConfigAwait(); + await outputStream.WriteAsync(boundaryBytes, 0, boundaryBytes.Length, token).ConfigAwait(); + PclExport.Instance.CloseStream(outputStream); + } + + public static void UploadFile(this WebRequest webRequest, Stream fileStream, string fileName) + { + if (fileName == null) + throw new ArgumentNullException(nameof(fileName)); + var mimeType = MimeTypes.GetMimeType(fileName); + if (mimeType == null) + throw new ArgumentException("Mime-type not found for file: " + fileName); + + UploadFile(webRequest, fileStream, fileName, mimeType); + } + + public static async Task UploadFileAsync(this WebRequest webRequest, Stream fileStream, string fileName, + CancellationToken token = default) + { + if (fileName == null) + throw new ArgumentNullException(nameof(fileName)); + var mimeType = MimeTypes.GetMimeType(fileName); + if (mimeType == null) + throw new ArgumentException("Mime-type not found for file: " + fileName); + + await UploadFileAsync(webRequest, fileStream, fileName, mimeType, token: token).ConfigAwait(); + } + + public static string PostXmlToUrl(this string url, object data, + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "POST", requestBody: data.ToXml(), contentType: MimeTypes.Xml, + accept: MimeTypes.Xml, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static string PostCsvToUrl(this string url, object data, + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "POST", requestBody: data.ToCsv(), contentType: MimeTypes.Csv, + accept: MimeTypes.Csv, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static string PutXmlToUrl(this string url, object data, + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "PUT", requestBody: data.ToXml(), contentType: MimeTypes.Xml, + accept: MimeTypes.Xml, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static string PutCsvToUrl(this string url, object data, + Action requestFilter = null, Action responseFilter = null) + { + return SendStringToUrl(url, method: "PUT", requestBody: data.ToCsv(), contentType: MimeTypes.Csv, + accept: MimeTypes.Csv, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static WebResponse PostFileToUrl(this string url, + FileInfo uploadFileInfo, string uploadFileMimeType, + string accept = null, + Action requestFilter = null) + { + var webReq = (HttpWebRequest)WebRequest.Create(url); + using (var fileStream = uploadFileInfo.OpenRead()) + { + var fileName = uploadFileInfo.Name; + + webReq.UploadFile(fileStream, fileName, uploadFileMimeType, accept: accept, requestFilter: requestFilter, + method: "POST"); + } + + if (ResultsFilter != null) + return null; + + return webReq.GetResponse(); + } + + public static async Task PostFileToUrlAsync(this string url, + FileInfo uploadFileInfo, string uploadFileMimeType, + string accept = null, + Action requestFilter = null, CancellationToken token = default) + { + var webReq = (HttpWebRequest)WebRequest.Create(url); + using (var fileStream = uploadFileInfo.OpenRead()) + { + var fileName = uploadFileInfo.Name; + + await webReq.UploadFileAsync(fileStream, fileName, uploadFileMimeType, accept: accept, + requestFilter: requestFilter, method: "POST", token: token).ConfigAwait(); + } + + if (ResultsFilter != null) + return null; + + return await webReq.GetResponseAsync().ConfigAwait(); + } + + public static WebResponse PutFileToUrl(this string url, + FileInfo uploadFileInfo, string uploadFileMimeType, + string accept = null, + Action requestFilter = null) + { + var webReq = (HttpWebRequest)WebRequest.Create(url); + using (var fileStream = uploadFileInfo.OpenRead()) + { + var fileName = uploadFileInfo.Name; + + webReq.UploadFile(fileStream, fileName, uploadFileMimeType, accept: accept, requestFilter: requestFilter, + method: "PUT"); + } + + if (ResultsFilter != null) + return null; + + return webReq.GetResponse(); + } + + public static async Task PutFileToUrlAsync(this string url, + FileInfo uploadFileInfo, string uploadFileMimeType, + string accept = null, + Action requestFilter = null, CancellationToken token = default) + { + var webReq = (HttpWebRequest)WebRequest.Create(url); + using (var fileStream = uploadFileInfo.OpenRead()) + { + var fileName = uploadFileInfo.Name; + + await webReq.UploadFileAsync(fileStream, fileName, uploadFileMimeType, accept: accept, + requestFilter: requestFilter, method: "PUT", token: token).ConfigAwait(); + } + + if (ResultsFilter != null) + return null; + + return await webReq.GetResponseAsync().ConfigAwait(); + } + + public static WebResponse UploadFile(this WebRequest webRequest, + FileInfo uploadFileInfo, string uploadFileMimeType) + { + using (var fileStream = uploadFileInfo.OpenRead()) + { + var fileName = uploadFileInfo.Name; + + webRequest.UploadFile(fileStream, fileName, uploadFileMimeType); + } + + if (ResultsFilter != null) + return null; + + return webRequest.GetResponse(); + } + + public static async Task UploadFileAsync(this WebRequest webRequest, + FileInfo uploadFileInfo, string uploadFileMimeType) + { + using (var fileStream = uploadFileInfo.OpenRead()) + { + var fileName = uploadFileInfo.Name; + + await webRequest.UploadFileAsync(fileStream, fileName, uploadFileMimeType).ConfigAwait(); + } + + if (ResultsFilter != null) + return null; + + return await webRequest.GetResponseAsync().ConfigAwait(); + } + + private static byte[] GetHeaderBytes(string fileName, string mimeType, string field, string boundary) + { + var header = "\r\n--" + boundary + + $"\r\nContent-Disposition: form-data; name=\"{field}\"; filename=\"{fileName}\"\r\nContent-Type: {mimeType}\r\n\r\n"; + + var headerBytes = header.ToAsciiBytes(); + return headerBytes; + } +} + + +public interface IHttpResultsFilter : IDisposable +{ + string GetString(HttpWebRequest webReq, string reqBody); + byte[] GetBytes(HttpWebRequest webReq, byte[] reqBody); + void UploadStream(HttpWebRequest webRequest, Stream fileStream, string fileName); +} + +public class HttpResultsFilter : IHttpResultsFilter +{ + private readonly IHttpResultsFilter previousFilter; + + public string StringResult { get; set; } + public byte[] BytesResult { get; set; } + + public Func StringResultFn { get; set; } + public Func BytesResultFn { get; set; } + public Action UploadFileFn { get; set; } + + public HttpResultsFilter(string stringResult = null, byte[] bytesResult = null) + { + StringResult = stringResult; + BytesResult = bytesResult; + + previousFilter = HttpUtils.ResultsFilter; + HttpUtils.ResultsFilter = this; + } + + public void Dispose() + { + HttpUtils.ResultsFilter = previousFilter; + } + + public string GetString(HttpWebRequest webReq, string reqBody) + { + return StringResultFn != null + ? StringResultFn(webReq, reqBody) + : StringResult; + } + + public byte[] GetBytes(HttpWebRequest webReq, byte[] reqBody) + { + return BytesResultFn != null + ? BytesResultFn(webReq, reqBody) + : BytesResult; + } + + public void UploadStream(HttpWebRequest webRequest, Stream fileStream, string fileName) + { + UploadFileFn?.Invoke(webRequest, fileStream, fileName); + } +} diff --git a/src/ServiceStack.Text/HttpUtils.cs b/src/ServiceStack.Text/HttpUtils.cs index a5551f4a3..ab7c7da07 100644 --- a/src/ServiceStack.Text/HttpUtils.cs +++ b/src/ServiceStack.Text/HttpUtils.cs @@ -2,2146 +2,221 @@ //License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt using System; -using System.Collections.Generic; using System.IO; using System.Net; using System.Text; -using System.Threading; using System.Threading.Tasks; -using ServiceStack.Text; -namespace ServiceStack -{ - public static partial class HttpUtils - { - public static string UserAgent = "ServiceStack.Text"; - - public static Encoding UseEncoding { get; set; } = PclExport.Instance.GetUTF8Encoding(false); - - [ThreadStatic] - public static IHttpResultsFilter ResultsFilter; - - public static string AddQueryParam(this string url, string key, object val, bool encode = true) - { - return url.AddQueryParam(key, val?.ToString(), encode); - } - - public static string AddQueryParam(this string url, object key, string val, bool encode = true) - { - return AddQueryParam(url, key?.ToString(), val, encode); - } - - public static string AddQueryParam(this string url, string key, string val, bool encode = true) - { - if (url == null) - url = ""; - - if (key == null || val == null) - return url; - - var prefix = string.Empty; - if (!url.EndsWith("?") && !url.EndsWith("&")) - { - prefix = url.IndexOf('?') == -1 ? "?" : "&"; - } - return url + prefix + key + "=" + (encode ? val.UrlEncode() : val); - } - - public static string SetQueryParam(this string url, string key, string val) - { - if (url == null) - url = ""; - - if (key == null || val == null) - return url; - - var qsPos = url.IndexOf('?'); - if (qsPos != -1) - { - var existingKeyPos = qsPos + 1 == url.IndexOf(key + "=", qsPos, StringComparison.Ordinal) - ? qsPos - : url.IndexOf("&" + key, qsPos, StringComparison.Ordinal); - - if (existingKeyPos != -1) - { - var endPos = url.IndexOf('&', existingKeyPos + 1); - if (endPos == -1) - endPos = url.Length; - - var newUrl = url.Substring(0, existingKeyPos + key.Length + 1) - + "=" - + val.UrlEncode() - + url.Substring(endPos); - return newUrl; - } - } - var prefix = qsPos == -1 ? "?" : "&"; - return url + prefix + key + "=" + val.UrlEncode(); - } - - public static string AddHashParam(this string url, string key, object val) - { - return url.AddHashParam(key, val?.ToString()); - } - - public static string AddHashParam(this string url, string key, string val) - { - if (url == null) - url = ""; - - if (key == null || val == null) - return url; - - var prefix = url.IndexOf('#') == -1 ? "#" : "/"; - return url + prefix + key + "=" + val.UrlEncode(); - } - - public static string SetHashParam(this string url, string key, string val) - { - if (url == null) - url = ""; - - if (key == null || val == null) - return url; - - var hPos = url.IndexOf('#'); - if (hPos != -1) - { - var existingKeyPos = hPos + 1 == url.IndexOf(key + "=", hPos, PclExport.Instance.InvariantComparison) - ? hPos - : url.IndexOf("/" + key, hPos, PclExport.Instance.InvariantComparison); - - if (existingKeyPos != -1) - { - var endPos = url.IndexOf('/', existingKeyPos + 1); - if (endPos == -1) - endPos = url.Length; - - var newUrl = url.Substring(0, existingKeyPos + key.Length + 1) - + "=" - + val.UrlEncode() - + url.Substring(endPos); - return newUrl; - } - } - var prefix = url.IndexOf('#') == -1 ? "#" : "/"; - return url + prefix + key + "=" + val.UrlEncode(); - } - - public static bool HasRequestBody(string httpMethod) - { - switch (httpMethod) - { - case HttpMethods.Get: - case HttpMethods.Delete: - case HttpMethods.Head: - case HttpMethods.Options: - return false; - } - - return true; - } - - public static string GetJsonFromUrl(this string url, - Action requestFilter = null, Action responseFilter = null) - { - return url.GetStringFromUrl(MimeTypes.Json, requestFilter, responseFilter); - } - - public static Task GetJsonFromUrlAsync(this string url, - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return url.GetStringFromUrlAsync(MimeTypes.Json, requestFilter, responseFilter, token: token); - } - - public static string GetXmlFromUrl(this string url, - Action requestFilter = null, Action responseFilter = null) - { - return url.GetStringFromUrl(MimeTypes.Xml, requestFilter, responseFilter); - } - - public static Task GetXmlFromUrlAsync(this string url, - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return url.GetStringFromUrlAsync(MimeTypes.Xml, requestFilter, responseFilter, token: token); - } - - public static string GetCsvFromUrl(this string url, - Action requestFilter = null, Action responseFilter = null) - { - return url.GetStringFromUrl(MimeTypes.Csv, requestFilter, responseFilter); - } - - public static Task GetCsvFromUrlAsync(this string url, - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return url.GetStringFromUrlAsync(MimeTypes.Csv, requestFilter, responseFilter, token: token); - } - - public static string GetStringFromUrl(this string url, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task GetStringFromUrlAsync(this string url, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string PostStringToUrl(this string url, string requestBody = null, - string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "POST", - requestBody: requestBody, contentType: contentType, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PostStringToUrlAsync(this string url, string requestBody = null, - string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, method: "POST", - requestBody: requestBody, contentType: contentType, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string PostToUrl(this string url, string formData = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "POST", - contentType: MimeTypes.FormUrlEncoded, requestBody: formData, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PostToUrlAsync(this string url, string formData = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, method: "POST", - contentType: MimeTypes.FormUrlEncoded, requestBody: formData, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string PostToUrl(this string url, object formData = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - string postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; - - return SendStringToUrl(url, method: "POST", - contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PostToUrlAsync(this string url, object formData = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - string postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; - - return SendStringToUrlAsync(url, method: "POST", - contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string PostJsonToUrl(this string url, string json, - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "POST", requestBody: json, contentType: MimeTypes.Json, accept: MimeTypes.Json, - requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PostJsonToUrlAsync(this string url, string json, - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, method: "POST", requestBody: json, contentType: MimeTypes.Json, accept: MimeTypes.Json, - requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string PostJsonToUrl(this string url, object data, - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "POST", requestBody: data.ToJson(), contentType: MimeTypes.Json, accept: MimeTypes.Json, - requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PostJsonToUrlAsync(this string url, object data, - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, method: "POST", requestBody: data.ToJson(), contentType: MimeTypes.Json, accept: MimeTypes.Json, - requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string PostXmlToUrl(this string url, string xml, - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "POST", requestBody: xml, contentType: MimeTypes.Xml, accept: MimeTypes.Xml, - requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PostXmlToUrlAsync(this string url, string xml, - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, method: "POST", requestBody: xml, contentType: MimeTypes.Xml, accept: MimeTypes.Xml, - requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string PostCsvToUrl(this string url, string csv, - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "POST", requestBody: csv, contentType: MimeTypes.Csv, accept: MimeTypes.Csv, - requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PostCsvToUrlAsync(this string url, string csv, - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, method: "POST", requestBody: csv, contentType: MimeTypes.Csv, accept: MimeTypes.Csv, - requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string PutStringToUrl(this string url, string requestBody = null, - string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "PUT", - requestBody: requestBody, contentType: contentType, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PutStringToUrlAsync(this string url, string requestBody = null, - string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, method: "PUT", - requestBody: requestBody, contentType: contentType, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string PutToUrl(this string url, string formData = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "PUT", - contentType: MimeTypes.FormUrlEncoded, requestBody: formData, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PutToUrlAsync(this string url, string formData = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, method: "PUT", - contentType: MimeTypes.FormUrlEncoded, requestBody: formData, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string PutToUrl(this string url, object formData = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - string postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; - - return SendStringToUrl(url, method: "PUT", - contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PutToUrlAsync(this string url, object formData = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - string postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; - - return SendStringToUrlAsync(url, method: "PUT", - contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string PutJsonToUrl(this string url, string json, - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "PUT", requestBody: json, contentType: MimeTypes.Json, accept: MimeTypes.Json, - requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PutJsonToUrlAsync(this string url, string json, - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, method: "PUT", requestBody: json, contentType: MimeTypes.Json, accept: MimeTypes.Json, - requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string PutJsonToUrl(this string url, object data, - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "PUT", requestBody: data.ToJson(), contentType: MimeTypes.Json, accept: MimeTypes.Json, - requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PutJsonToUrlAsync(this string url, object data, - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, method: "PUT", requestBody: data.ToJson(), contentType: MimeTypes.Json, accept: MimeTypes.Json, - requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string PutXmlToUrl(this string url, string xml, - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "PUT", requestBody: xml, contentType: MimeTypes.Xml, accept: MimeTypes.Xml, - requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PutXmlToUrlAsync(this string url, string xml, - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, method: "PUT", requestBody: xml, contentType: MimeTypes.Xml, accept: MimeTypes.Xml, - requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string PutCsvToUrl(this string url, string csv, - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "PUT", requestBody: csv, contentType: MimeTypes.Csv, accept: MimeTypes.Csv, - requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PutCsvToUrlAsync(this string url, string csv, - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, method: "PUT", requestBody: csv, contentType: MimeTypes.Csv, accept: MimeTypes.Csv, - requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string PatchStringToUrl(this string url, string requestBody = null, - string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "PATCH", - requestBody: requestBody, contentType: contentType, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PatchStringToUrlAsync(this string url, string requestBody = null, - string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, method: "PATCH", - requestBody: requestBody, contentType: contentType, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string PatchToUrl(this string url, string formData = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "PATCH", - contentType: MimeTypes.FormUrlEncoded, requestBody: formData, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PatchToUrlAsync(this string url, string formData = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, method: "PATCH", - contentType: MimeTypes.FormUrlEncoded, requestBody: formData, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string PatchToUrl(this string url, object formData = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - string postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; - - return SendStringToUrl(url, method: "PATCH", - contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PatchToUrlAsync(this string url, object formData = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - string postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; - - return SendStringToUrlAsync(url, method: "PATCH", - contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string PatchJsonToUrl(this string url, string json, - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "PATCH", requestBody: json, contentType: MimeTypes.Json, accept: MimeTypes.Json, - requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PatchJsonToUrlAsync(this string url, string json, - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, method: "PATCH", requestBody: json, contentType: MimeTypes.Json, accept: MimeTypes.Json, - requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string PatchJsonToUrl(this string url, object data, - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "PATCH", requestBody: data.ToJson(), contentType: MimeTypes.Json, accept: MimeTypes.Json, - requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PatchJsonToUrlAsync(this string url, object data, - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, method: "PATCH", requestBody: data.ToJson(), contentType: MimeTypes.Json, accept: MimeTypes.Json, - requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string DeleteFromUrl(this string url, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "DELETE", accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task DeleteFromUrlAsync(this string url, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, method: "DELETE", accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string OptionsFromUrl(this string url, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "OPTIONS", accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task OptionsFromUrlAsync(this string url, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, method: "OPTIONS", accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string HeadFromUrl(this string url, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "HEAD", accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task HeadFromUrlAsync(this string url, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStringToUrlAsync(url, method: "HEAD", accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static string SendStringToUrl(this string url, string method = null, - string requestBody = null, string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - var webReq = (HttpWebRequest)WebRequest.Create(url); - if (method != null) - webReq.Method = method; - if (contentType != null) - webReq.ContentType = contentType; - - webReq.Accept = accept; - PclExport.Instance.AddCompression(webReq); - - requestFilter?.Invoke(webReq); - - if (ResultsFilter != null) - { - return ResultsFilter.GetString(webReq, requestBody); - } - - if (requestBody != null) - { - using var reqStream = PclExport.Instance.GetRequestStream(webReq); - using var writer = new StreamWriter(reqStream, UseEncoding); - writer.Write(requestBody); - } - else if (method != null && HasRequestBody(method)) - { - webReq.ContentLength = 0; - } - - using var webRes = webReq.GetResponse(); - using var stream = webRes.GetResponseStream(); - responseFilter?.Invoke((HttpWebResponse)webRes); - return stream.ReadToEnd(UseEncoding); - } - - public static async Task SendStringToUrlAsync(this string url, string method = null, string requestBody = null, - string contentType = null, string accept = "*/*", Action requestFilter = null, - Action responseFilter = null, CancellationToken token=default) - { - var webReq = (HttpWebRequest)WebRequest.Create(url); - if (method != null) - webReq.Method = method; - if (contentType != null) - webReq.ContentType = contentType; - - webReq.Accept = accept; - PclExport.Instance.AddCompression(webReq); - - requestFilter?.Invoke(webReq); - - if (ResultsFilter != null) - { - var result = ResultsFilter.GetString(webReq, requestBody); - return result; - } - - if (requestBody != null) - { - using var reqStream = PclExport.Instance.GetRequestStream(webReq); - using var writer = new StreamWriter(reqStream, UseEncoding); - await writer.WriteAsync(requestBody).ConfigAwait(); - } - - using var webRes = await webReq.GetResponseAsync().ConfigAwait(); - responseFilter?.Invoke((HttpWebResponse)webRes); - using var stream = webRes.GetResponseStream(); - return await stream.ReadToEndAsync().ConfigAwait(); - } - - public static byte[] GetBytesFromUrl(this string url, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - return url.SendBytesToUrl(accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task GetBytesFromUrlAsync(this string url, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return url.SendBytesToUrlAsync(accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static byte[] PostBytesToUrl(this string url, byte[] requestBody = null, string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - return SendBytesToUrl(url, method: "POST", - contentType: contentType, requestBody: requestBody, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PostBytesToUrlAsync(this string url, byte[] requestBody = null, string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendBytesToUrlAsync(url, method: "POST", - contentType: contentType, requestBody: requestBody, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static byte[] PutBytesToUrl(this string url, byte[] requestBody = null, string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - return SendBytesToUrl(url, method: "PUT", - contentType: contentType, requestBody: requestBody, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PutBytesToUrlAsync(this string url, byte[] requestBody = null, string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendBytesToUrlAsync(url, method: "PUT", - contentType: contentType, requestBody: requestBody, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static byte[] SendBytesToUrl(this string url, string method = null, - byte[] requestBody = null, string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - var webReq = (HttpWebRequest)WebRequest.Create(url); - if (method != null) - webReq.Method = method; - - if (contentType != null) - webReq.ContentType = contentType; - - webReq.Accept = accept; - PclExport.Instance.AddCompression(webReq); - - requestFilter?.Invoke(webReq); - - if (ResultsFilter != null) - { - return ResultsFilter.GetBytes(webReq, requestBody); - } - - if (requestBody != null) - { - using var req = PclExport.Instance.GetRequestStream(webReq); - req.Write(requestBody, 0, requestBody.Length); - } - - using var webRes = PclExport.Instance.GetResponse(webReq); - responseFilter?.Invoke((HttpWebResponse)webRes); - - using var stream = webRes.GetResponseStream(); - return stream.ReadFully(); - } - - public static async Task SendBytesToUrlAsync(this string url, string method = null, - byte[] requestBody = null, string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - var webReq = (HttpWebRequest)WebRequest.Create(url); - if (method != null) - webReq.Method = method; - if (contentType != null) - webReq.ContentType = contentType; - - webReq.Accept = accept; - PclExport.Instance.AddCompression(webReq); - - requestFilter?.Invoke(webReq); - - if (ResultsFilter != null) - { - var result = ResultsFilter.GetBytes(webReq, requestBody); - return result; - } - - if (requestBody != null) - { - using var req = PclExport.Instance.GetRequestStream(webReq); - await req.WriteAsync(requestBody, 0, requestBody.Length, token).ConfigAwait(); - } - - var webRes = await webReq.GetResponseAsync().ConfigAwait(); - responseFilter?.Invoke((HttpWebResponse)webRes); - - using var stream = webRes.GetResponseStream(); - return await stream.ReadFullyAsync(token).ConfigAwait(); - } - - public static Stream GetStreamFromUrl(this string url, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - return url.SendStreamToUrl(accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task GetStreamFromUrlAsync(this string url, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return url.SendStreamToUrlAsync(accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static Stream PostStreamToUrl(this string url, Stream requestBody = null, string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - return SendStreamToUrl(url, method: "POST", - contentType: contentType, requestBody: requestBody, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PostStreamToUrlAsync(this string url, Stream requestBody = null, string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStreamToUrlAsync(url, method: "POST", - contentType: contentType, requestBody: requestBody, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - public static Stream PutStreamToUrl(this string url, Stream requestBody = null, string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - return SendStreamToUrl(url, method: "PUT", - contentType: contentType, requestBody: requestBody, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static Task PutStreamToUrlAsync(this string url, Stream requestBody = null, string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - return SendStreamToUrlAsync(url, method: "PUT", - contentType: contentType, requestBody: requestBody, - accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); - } - - /// - /// Returns HttpWebResponse Stream which must be disposed - /// - public static Stream SendStreamToUrl(this string url, string method = null, - Stream requestBody = null, string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null) - { - var webReq = (HttpWebRequest)WebRequest.Create(url); - if (method != null) - webReq.Method = method; - - if (contentType != null) - webReq.ContentType = contentType; - - webReq.Accept = accept; - PclExport.Instance.AddCompression(webReq); - - requestFilter?.Invoke(webReq); - - if (ResultsFilter != null) - { - return new MemoryStream(ResultsFilter.GetBytes(webReq, requestBody.ReadFully())); - } - - if (requestBody != null) - { - using var req = PclExport.Instance.GetRequestStream(webReq); - requestBody.CopyTo(req); - } - - var webRes = PclExport.Instance.GetResponse(webReq); - responseFilter?.Invoke((HttpWebResponse)webRes); - - var stream = webRes.GetResponseStream(); - return stream; - } - - /// - /// Returns HttpWebResponse Stream which must be disposed - /// - public static async Task SendStreamToUrlAsync(this string url, string method = null, - Stream requestBody = null, string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, CancellationToken token=default) - { - var webReq = (HttpWebRequest)WebRequest.Create(url); - if (method != null) - webReq.Method = method; - if (contentType != null) - webReq.ContentType = contentType; - - webReq.Accept = accept; - PclExport.Instance.AddCompression(webReq); - - requestFilter?.Invoke(webReq); - - if (ResultsFilter != null) - { - return new MemoryStream(ResultsFilter.GetBytes(webReq, await requestBody.ReadFullyAsync(token).ConfigAwait())); - } - - if (requestBody != null) - { - using var req = PclExport.Instance.GetRequestStream(webReq); - await requestBody.CopyToAsync(req, token).ConfigAwait(); - } - - var webRes = await webReq.GetResponseAsync().ConfigAwait(); - responseFilter?.Invoke((HttpWebResponse)webRes); - - var stream = webRes.GetResponseStream(); - return stream; - } - - public static bool IsAny300(this Exception ex) - { - var status = ex.GetStatus(); - return status >= HttpStatusCode.MultipleChoices && status < HttpStatusCode.BadRequest; - } - - public static bool IsAny400(this Exception ex) - { - var status = ex.GetStatus(); - return status >= HttpStatusCode.BadRequest && status < HttpStatusCode.InternalServerError; - } - - public static bool IsAny500(this Exception ex) - { - var status = ex.GetStatus(); - return status >= HttpStatusCode.InternalServerError && (int)status < 600; - } - - public static bool IsNotModified(this Exception ex) - { - return GetStatus(ex) == HttpStatusCode.NotModified; - } - - public static bool IsBadRequest(this Exception ex) - { - return GetStatus(ex) == HttpStatusCode.BadRequest; - } - - public static bool IsNotFound(this Exception ex) - { - return GetStatus(ex) == HttpStatusCode.NotFound; - } - - public static bool IsUnauthorized(this Exception ex) - { - return GetStatus(ex) == HttpStatusCode.Unauthorized; - } - - public static bool IsForbidden(this Exception ex) - { - return GetStatus(ex) == HttpStatusCode.Forbidden; - } - - public static bool IsInternalServerError(this Exception ex) - { - return GetStatus(ex) == HttpStatusCode.InternalServerError; - } - - public static HttpStatusCode? GetResponseStatus(this string url) - { - try - { - var webReq = (HttpWebRequest)WebRequest.Create(url); - using (var webRes = PclExport.Instance.GetResponse(webReq)) - { - var httpRes = webRes as HttpWebResponse; - return httpRes?.StatusCode; - } - } - catch (Exception ex) - { - return ex.GetStatus(); - } - } - - public static HttpStatusCode? GetStatus(this Exception ex) - { - if (ex == null) - return null; - - if (ex is WebException webEx) - return GetStatus(webEx); - - if (ex is IHasStatusCode hasStatus) - return (HttpStatusCode)hasStatus.StatusCode; - - return null; - } - - public static HttpStatusCode? GetStatus(this WebException webEx) - { - var httpRes = webEx?.Response as HttpWebResponse; - return httpRes?.StatusCode; - } - - public static bool HasStatus(this Exception ex, HttpStatusCode statusCode) - { - return GetStatus(ex) == statusCode; - } - - public static string GetResponseBody(this Exception ex) - { - if (!(ex is WebException webEx) || webEx.Response == null || webEx.Status != WebExceptionStatus.ProtocolError) - return null; - - var errorResponse = (HttpWebResponse)webEx.Response; - using var responseStream = errorResponse.GetResponseStream(); - return responseStream.ReadToEnd(UseEncoding); - } - - public static async Task GetResponseBodyAsync(this Exception ex, CancellationToken token=default) - { - if (!(ex is WebException webEx) || webEx.Response == null || webEx.Status != WebExceptionStatus.ProtocolError) - return null; - - var errorResponse = (HttpWebResponse)webEx.Response; - using var responseStream = errorResponse.GetResponseStream(); - return await responseStream.ReadToEndAsync(UseEncoding).ConfigAwait(); - } - - public static string ReadToEnd(this WebResponse webRes) - { - using var stream = webRes.GetResponseStream(); - return stream.ReadToEnd(UseEncoding); - } - - public static Task ReadToEndAsync(this WebResponse webRes) - { - using var stream = webRes.GetResponseStream(); - return stream.ReadToEndAsync(UseEncoding); - } - - public static IEnumerable ReadLines(this WebResponse webRes) - { - using var stream = webRes.GetResponseStream(); - using var reader = new StreamReader(stream, UseEncoding, true, 1024, leaveOpen:true); - string line; - while ((line = reader.ReadLine()) != null) - { - yield return line; - } - } - - public static HttpWebResponse GetErrorResponse(this string url) - { - try - { - var webReq = WebRequest.Create(url); - using var webRes = PclExport.Instance.GetResponse(webReq); - webRes.ReadToEnd(); - return null; - } - catch (WebException webEx) - { - return (HttpWebResponse)webEx.Response; - } - } - - public static async Task GetErrorResponseAsync(this string url) - { - try - { - var webReq = WebRequest.Create(url); - using var webRes = await webReq.GetResponseAsync().ConfigAwait(); - await webRes.ReadToEndAsync().ConfigAwait(); - return null; - } - catch (WebException webEx) - { - return (HttpWebResponse)webEx.Response; - } - } - - public static Task GetRequestStreamAsync(this WebRequest request) - { - return GetRequestStreamAsync((HttpWebRequest)request); - } - - public static Task GetRequestStreamAsync(this HttpWebRequest request) - { - var tcs = new TaskCompletionSource(); - - try - { - request.BeginGetRequestStream(iar => - { - try - { - var response = request.EndGetRequestStream(iar); - tcs.SetResult(response); - } - catch (Exception exc) - { - tcs.SetException(exc); - } - }, null); - } - catch (Exception exc) - { - tcs.SetException(exc); - } - - return tcs.Task; - } - - public static Task ConvertTo(this Task task) where TDerived : TBase - { - var tcs = new TaskCompletionSource(); - task.ContinueWith(t => tcs.SetResult(t.Result), TaskContinuationOptions.OnlyOnRanToCompletion); - task.ContinueWith(t => tcs.SetException(t.Exception.InnerExceptions), TaskContinuationOptions.OnlyOnFaulted); - task.ContinueWith(t => tcs.SetCanceled(), TaskContinuationOptions.OnlyOnCanceled); - return tcs.Task; - } - - public static Task GetResponseAsync(this WebRequest request) - { - return GetResponseAsync((HttpWebRequest)request).ConvertTo(); - } - - public static Task GetResponseAsync(this HttpWebRequest request) - { - var tcs = new TaskCompletionSource(); - - try - { - request.BeginGetResponse(iar => - { - try - { - var response = (HttpWebResponse)request.EndGetResponse(iar); - tcs.SetResult(response); - } - catch (Exception exc) - { - tcs.SetException(exc); - } - }, null); - } - catch (Exception exc) - { - tcs.SetException(exc); - } - - return tcs.Task; - } - - private static byte[] GetHeaderBytes(string fileName, string mimeType, string field, string boundary) - { - var header = "\r\n--" + boundary + - $"\r\nContent-Disposition: form-data; name=\"{field}\"; filename=\"{fileName}\"\r\nContent-Type: {mimeType}\r\n\r\n"; - - var headerBytes = header.ToAsciiBytes(); - return headerBytes; - } - - public static void UploadFile(this WebRequest webRequest, Stream fileStream, string fileName, string mimeType, - string accept = null, Action requestFilter = null, string method = "POST", string field = "file") - { - var httpReq = (HttpWebRequest)webRequest; - httpReq.Method = method; - - if (accept != null) - httpReq.Accept = accept; - - requestFilter?.Invoke(httpReq); - - var boundary = Guid.NewGuid().ToString("N"); - - httpReq.ContentType = "multipart/form-data; boundary=\"" + boundary + "\""; - - var boundaryBytes = ("\r\n--" + boundary + "--\r\n").ToAsciiBytes(); - - var headerBytes = GetHeaderBytes(fileName, mimeType, field, boundary); - - var contentLength = fileStream.Length + headerBytes.Length + boundaryBytes.Length; - PclExport.Instance.InitHttpWebRequest(httpReq, - contentLength: contentLength, allowAutoRedirect: false, keepAlive: false); - - if (ResultsFilter != null) - { - ResultsFilter.UploadStream(httpReq, fileStream, fileName); - return; - } - - using var outputStream = PclExport.Instance.GetRequestStream(httpReq); - outputStream.Write(headerBytes, 0, headerBytes.Length); - fileStream.CopyTo(outputStream, 4096); - outputStream.Write(boundaryBytes, 0, boundaryBytes.Length); - PclExport.Instance.CloseStream(outputStream); - } - - public static async Task UploadFileAsync(this WebRequest webRequest, Stream fileStream, string fileName, string mimeType, - string accept = null, Action requestFilter = null, string method = "POST", string field = "file", - CancellationToken token=default) - { - var httpReq = (HttpWebRequest)webRequest; - httpReq.Method = method; - - if (accept != null) - httpReq.Accept = accept; - - requestFilter?.Invoke(httpReq); - - var boundary = Guid.NewGuid().ToString("N"); - - httpReq.ContentType = "multipart/form-data; boundary=\"" + boundary + "\""; - - var boundaryBytes = ("\r\n--" + boundary + "--\r\n").ToAsciiBytes(); - - var headerBytes = GetHeaderBytes(fileName, mimeType, field, boundary); - - var contentLength = fileStream.Length + headerBytes.Length + boundaryBytes.Length; - PclExport.Instance.InitHttpWebRequest(httpReq, - contentLength: contentLength, allowAutoRedirect: false, keepAlive: false); - - if (ResultsFilter != null) - { - ResultsFilter.UploadStream(httpReq, fileStream, fileName); - return; - } - - using var outputStream = PclExport.Instance.GetRequestStream(httpReq); - await outputStream.WriteAsync(headerBytes, 0, headerBytes.Length, token).ConfigAwait(); - await fileStream.CopyToAsync(outputStream, 4096, token).ConfigAwait(); - await outputStream.WriteAsync(boundaryBytes, 0, boundaryBytes.Length, token).ConfigAwait(); - PclExport.Instance.CloseStream(outputStream); - } - - public static void UploadFile(this WebRequest webRequest, Stream fileStream, string fileName) - { - if (fileName == null) - throw new ArgumentNullException(nameof(fileName)); - var mimeType = MimeTypes.GetMimeType(fileName); - if (mimeType == null) - throw new ArgumentException("Mime-type not found for file: " + fileName); - - UploadFile(webRequest, fileStream, fileName, mimeType); - } - - public static async Task UploadFileAsync(this WebRequest webRequest, Stream fileStream, string fileName, CancellationToken token=default) - { - if (fileName == null) - throw new ArgumentNullException(nameof(fileName)); - var mimeType = MimeTypes.GetMimeType(fileName); - if (mimeType == null) - throw new ArgumentException("Mime-type not found for file: " + fileName); - - await UploadFileAsync(webRequest, fileStream, fileName, mimeType, token: token).ConfigAwait(); - } - - public static string PostXmlToUrl(this string url, object data, - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "POST", requestBody: data.ToXml(), contentType: MimeTypes.Xml, accept: MimeTypes.Xml, - requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static string PostCsvToUrl(this string url, object data, - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "POST", requestBody: data.ToCsv(), contentType: MimeTypes.Csv, accept: MimeTypes.Csv, - requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static string PutXmlToUrl(this string url, object data, - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "PUT", requestBody: data.ToXml(), contentType: MimeTypes.Xml, accept: MimeTypes.Xml, - requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static string PutCsvToUrl(this string url, object data, - Action requestFilter = null, Action responseFilter = null) - { - return SendStringToUrl(url, method: "PUT", requestBody: data.ToCsv(), contentType: MimeTypes.Csv, accept: MimeTypes.Csv, - requestFilter: requestFilter, responseFilter: responseFilter); - } - - public static WebResponse PostFileToUrl(this string url, - FileInfo uploadFileInfo, string uploadFileMimeType, - string accept = null, - Action requestFilter = null) - { - var webReq = (HttpWebRequest)WebRequest.Create(url); - using (var fileStream = uploadFileInfo.OpenRead()) - { - var fileName = uploadFileInfo.Name; - - webReq.UploadFile(fileStream, fileName, uploadFileMimeType, accept: accept, requestFilter: requestFilter, method: "POST"); - } - - if (ResultsFilter != null) - return null; - - return webReq.GetResponse(); - } - - public static async Task PostFileToUrlAsync(this string url, - FileInfo uploadFileInfo, string uploadFileMimeType, - string accept = null, - Action requestFilter = null, CancellationToken token=default) - { - var webReq = (HttpWebRequest)WebRequest.Create(url); - using (var fileStream = uploadFileInfo.OpenRead()) - { - var fileName = uploadFileInfo.Name; +namespace ServiceStack; - await webReq.UploadFileAsync(fileStream, fileName, uploadFileMimeType, accept: accept, requestFilter: requestFilter, method: "POST", token: token).ConfigAwait(); - } - - if (ResultsFilter != null) - return null; - - return await webReq.GetResponseAsync().ConfigAwait(); - } +public static partial class HttpUtils +{ + public static string UserAgent = "ServiceStack.Text" + Text.Env.PlatformModifier; - public static WebResponse PutFileToUrl(this string url, - FileInfo uploadFileInfo, string uploadFileMimeType, - string accept = null, - Action requestFilter = null) - { - var webReq = (HttpWebRequest)WebRequest.Create(url); - using (var fileStream = uploadFileInfo.OpenRead()) - { - var fileName = uploadFileInfo.Name; + public static Encoding UseEncoding { get; set; } = PclExport.Instance.GetUTF8Encoding(false); - webReq.UploadFile(fileStream, fileName, uploadFileMimeType, accept: accept, requestFilter: requestFilter, method: "PUT"); - } + public static string AddQueryParam(this string url, string key, object val, bool encode = true) + { + return url.AddQueryParam(key, val?.ToString(), encode); + } - if (ResultsFilter != null) - return null; + public static string AddQueryParam(this string url, object key, string val, bool encode = true) + { + return AddQueryParam(url, key?.ToString(), val, encode); + } - return webReq.GetResponse(); - } + public static string AddQueryParam(this string url, string key, string val, bool encode = true) + { + if (url == null) + url = ""; - public static async Task PutFileToUrlAsync(this string url, - FileInfo uploadFileInfo, string uploadFileMimeType, - string accept = null, - Action requestFilter = null, CancellationToken token=default) + if (key == null || val == null) + return url; + + var prefix = string.Empty; + if (!url.EndsWith("?") && !url.EndsWith("&")) { - var webReq = (HttpWebRequest)WebRequest.Create(url); - using (var fileStream = uploadFileInfo.OpenRead()) - { - var fileName = uploadFileInfo.Name; - - await webReq.UploadFileAsync(fileStream, fileName, uploadFileMimeType, accept: accept, requestFilter: requestFilter, method: "PUT", token: token).ConfigAwait(); - } - - if (ResultsFilter != null) - return null; - - return await webReq.GetResponseAsync().ConfigAwait(); + prefix = url.IndexOf('?') == -1 ? "?" : "&"; } + return url + prefix + key + "=" + (encode ? val.UrlEncode() : val); + } - public static WebResponse UploadFile(this WebRequest webRequest, - FileInfo uploadFileInfo, string uploadFileMimeType) + public static string SetQueryParam(this string url, string key, string val) + { + if (url == null) + url = ""; + + if (key == null || val == null) + return url; + + var qsPos = url.IndexOf('?'); + if (qsPos != -1) { - using (var fileStream = uploadFileInfo.OpenRead()) - { - var fileName = uploadFileInfo.Name; - - webRequest.UploadFile(fileStream, fileName, uploadFileMimeType); - } - - if (ResultsFilter != null) - return null; + var existingKeyPos = qsPos + 1 == url.IndexOf(key + "=", qsPos, StringComparison.Ordinal) + ? qsPos + : url.IndexOf("&" + key, qsPos, StringComparison.Ordinal); - return webRequest.GetResponse(); - } - - public static async Task UploadFileAsync(this WebRequest webRequest, - FileInfo uploadFileInfo, string uploadFileMimeType) - { - using (var fileStream = uploadFileInfo.OpenRead()) + if (existingKeyPos != -1) { - var fileName = uploadFileInfo.Name; + var endPos = url.IndexOf('&', existingKeyPos + 1); + if (endPos == -1) + endPos = url.Length; - await webRequest.UploadFileAsync(fileStream, fileName, uploadFileMimeType).ConfigAwait(); + var newUrl = url.Substring(0, existingKeyPos + key.Length + 1) + + "=" + + val.UrlEncode() + + url.Substring(endPos); + return newUrl; } - - if (ResultsFilter != null) - return null; - - return await webRequest.GetResponseAsync().ConfigAwait(); } + var prefix = qsPos == -1 ? "?" : "&"; + return url + prefix + key + "=" + val.UrlEncode(); } - //Allow Exceptions to Customize HTTP StatusCode and StatusDescription returned - public interface IHasStatusCode - { - int StatusCode { get; } - } - - public interface IHasStatusDescription + public static string AddHashParam(this string url, string key, object val) { - string StatusDescription { get; } + return url.AddHashParam(key, val?.ToString()); } - public interface IHttpResultsFilter : IDisposable + public static string AddHashParam(this string url, string key, string val) { - string GetString(HttpWebRequest webReq, string reqBody); - byte[] GetBytes(HttpWebRequest webReq, byte[] reqBody); - void UploadStream(HttpWebRequest webRequest, Stream fileStream, string fileName); + if (url == null) + url = ""; + + if (key == null || val == null) + return url; + + var prefix = url.IndexOf('#') == -1 ? "#" : "/"; + return url + prefix + key + "=" + val.UrlEncode(); } - public class HttpResultsFilter : IHttpResultsFilter + public static string SetHashParam(this string url, string key, string val) { - private readonly IHttpResultsFilter previousFilter; - - public string StringResult { get; set; } - public byte[] BytesResult { get; set; } - - public Func StringResultFn { get; set; } - public Func BytesResultFn { get; set; } - public Action UploadFileFn { get; set; } - - public HttpResultsFilter(string stringResult = null, byte[] bytesResult = null) + if (url == null) + url = ""; + + if (key == null || val == null) + return url; + + var hPos = url.IndexOf('#'); + if (hPos != -1) { - StringResult = stringResult; - BytesResult = bytesResult; - - previousFilter = HttpUtils.ResultsFilter; - HttpUtils.ResultsFilter = this; - } + var existingKeyPos = hPos + 1 == url.IndexOf(key + "=", hPos, PclExport.Instance.InvariantComparison) + ? hPos + : url.IndexOf("/" + key, hPos, PclExport.Instance.InvariantComparison); - public void Dispose() - { - HttpUtils.ResultsFilter = previousFilter; - } + if (existingKeyPos != -1) + { + var endPos = url.IndexOf('/', existingKeyPos + 1); + if (endPos == -1) + endPos = url.Length; - public string GetString(HttpWebRequest webReq, string reqBody) - { - return StringResultFn != null - ? StringResultFn(webReq, reqBody) - : StringResult; + var newUrl = url.Substring(0, existingKeyPos + key.Length + 1) + + "=" + + val.UrlEncode() + + url.Substring(endPos); + return newUrl; + } } + var prefix = url.IndexOf('#') == -1 ? "#" : "/"; + return url + prefix + key + "=" + val.UrlEncode(); + } - public byte[] GetBytes(HttpWebRequest webReq, byte[] reqBody) + public static bool HasRequestBody(string httpMethod) + { + switch (httpMethod) { - return BytesResultFn != null - ? BytesResultFn(webReq, reqBody) - : BytesResult; + case HttpMethods.Get: + case HttpMethods.Delete: + case HttpMethods.Head: + case HttpMethods.Options: + return false; } - public void UploadStream(HttpWebRequest webRequest, Stream fileStream, string fileName) - { - UploadFileFn?.Invoke(webRequest, fileStream, fileName); - } + return true; } -} - -namespace ServiceStack -{ - public static class MimeTypes + + public static Task GetRequestStreamAsync(this WebRequest request) { - public static Dictionary ExtensionMimeTypes = new(); - public const string Utf8Suffix = "; charset=utf-8"; - - public const string Html = "text/html"; - public const string HtmlUtf8 = Html + Utf8Suffix; - public const string Css = "text/css"; - public const string Xml = "application/xml"; - public const string XmlText = "text/xml"; - public const string Json = "application/json"; - public const string ProblemJson = "application/problem+json"; - public const string JsonText = "text/json"; - public const string Jsv = "application/jsv"; - public const string JsvText = "text/jsv"; - public const string Csv = "text/csv"; - public const string ProtoBuf = "application/x-protobuf"; - public const string JavaScript = "text/javascript"; - public const string WebAssembly = "application/wasm"; - public const string Jar = "application/java-archive"; - public const string Dmg = "application/x-apple-diskimage"; - public const string Pkg = "application/x-newton-compatible-pkg"; - - public const string FormUrlEncoded = "application/x-www-form-urlencoded"; - public const string MultiPartFormData = "multipart/form-data"; - public const string JsonReport = "text/jsonreport"; - public const string Soap11 = "text/xml; charset=utf-8"; - public const string Soap12 = "application/soap+xml"; - public const string Yaml = "application/yaml"; - public const string YamlText = "text/yaml"; - public const string PlainText = "text/plain"; - public const string MarkdownText = "text/markdown"; - public const string MsgPack = "application/x-msgpack"; - public const string Wire = "application/x-wire"; - public const string Compressed = "application/x-compressed"; - public const string NetSerializer = "application/x-netserializer"; - public const string Excel = "application/excel"; - public const string MsWord = "application/msword"; - public const string Cert = "application/x-x509-ca-cert"; - - public const string ImagePng = "image/png"; - public const string ImageGif = "image/gif"; - public const string ImageJpg = "image/jpeg"; - public const string ImageSvg = "image/svg+xml"; - - public const string Bson = "application/bson"; - public const string Binary = "application/octet-stream"; - public const string ServerSentEvents = "text/event-stream"; - - public static string GetExtension(string mimeType) - { - switch (mimeType) - { - case ProtoBuf: - return ".pbuf"; - } + return GetRequestStreamAsync((HttpWebRequest)request); + } - var parts = mimeType.Split('/'); - if (parts.Length == 1) return "." + parts[0].LeftPart('+').LeftPart(';'); - if (parts.Length == 2) return "." + parts[1].LeftPart('+').LeftPart(';'); + public static Task GetRequestStreamAsync(this HttpWebRequest request) + { + var tcs = new TaskCompletionSource(); - throw new NotSupportedException("Unknown mimeType: " + mimeType); - } - - //Lower cases and trims left part of content-type prior ';' - public static string GetRealContentType(string contentType) + try { - if (contentType == null) - return null; - - int start = -1, end = -1; - - for(int i=0; i < contentType.Length; i++) + request.BeginGetRequestStream(iar => { - if (!char.IsWhiteSpace(contentType[i])) + try { - if (contentType[i] == ';') - break; - if (start == -1) - { - start = i; - } - end = i; + var response = request.EndGetRequestStream(iar); + tcs.SetResult(response); } - } - - return start != -1 - ? contentType.Substring(start, end - start + 1).ToLowerInvariant() - : null; - } - - //Compares two string from start to ';' char, case-insensitive, - //ignoring (trimming) spaces at start and end - public static bool MatchesContentType(string contentType, string matchesContentType) - { - if (contentType == null || matchesContentType == null) - return false; - - int start = -1, matchStart = -1, matchEnd = -1; - - for (var i=0; i < contentType.Length; i++) - { - if (char.IsWhiteSpace(contentType[i])) - continue; - start = i; - break; - } - - for (var i=0; i < matchesContentType.Length; i++) - { - if (char.IsWhiteSpace(matchesContentType[i])) - continue; - if (matchesContentType[i] == ';') - break; - if (matchStart == -1) - matchStart = i; - matchEnd = i; - } - - return start != -1 && matchStart != -1 && matchEnd != -1 - && string.Compare(contentType, start, - matchesContentType, matchStart, matchEnd - matchStart + 1, - StringComparison.OrdinalIgnoreCase) == 0; + catch (Exception exc) + { + tcs.SetException(exc); + } + }, null); } - - public static Func IsBinaryFilter { get; set; } - - public static bool IsBinary(string contentType) + catch (Exception exc) { - var userFilter = IsBinaryFilter?.Invoke(contentType); - if (userFilter != null) - return userFilter.Value; - - var realContentType = GetRealContentType(contentType); - switch (realContentType) - { - case ProtoBuf: - case MsgPack: - case Binary: - case Bson: - case Wire: - case Cert: - case Excel: - case MsWord: - case Compressed: - case WebAssembly: - case Jar: - case Dmg: - case Pkg: - return true; - } - - // Text format exceptions to below heuristics - switch (realContentType) - { - case ImageSvg: - return false; - } - - var primaryType = realContentType.LeftPart('/'); - var secondaryType = realContentType.RightPart('/'); - switch (primaryType) - { - case "image": - case "audio": - case "video": - return true; - } - - if (secondaryType.StartsWith("pkc") - || secondaryType.StartsWith("x-pkc") - || secondaryType.StartsWith("font") - || secondaryType.StartsWith("vnd.ms-")) - return true; - - return false; + tcs.SetException(exc); } - public static string GetMimeType(string fileNameOrExt) - { - if (string.IsNullOrEmpty(fileNameOrExt)) - throw new ArgumentNullException(nameof(fileNameOrExt)); - - var fileExt = fileNameOrExt.LastRightPart('.').ToLower(); - if (ExtensionMimeTypes.TryGetValue(fileExt, out var mimeType)) - { - return mimeType; - } - - switch (fileExt) - { - case "jpeg": - return "image/jpeg"; - case "gif": - return "image/gif"; - case "png": - return "image/png"; - case "tiff": - return "image/tiff"; - case "bmp": - return "image/bmp"; - case "webp": - return "image/webp"; - - case "jpg": - return "image/jpeg"; - - case "tif": - return "image/tiff"; - - case "svg": - return ImageSvg; - - case "ico": - return "image/x-icon"; - - case "htm": - case "html": - case "shtml": - return "text/html"; - - case "js": - return "text/javascript"; - case "ts": - return "text/typescript"; - case "jsx": - return "text/jsx"; - - case "csv": - return Csv; - case "css": - return Css; - - case "cs": - return "text/x-csharp"; - case "fs": - return "text/x-fsharp"; - case "vb": - return "text/x-vb"; - case "dart": - return "application/dart"; - case "go": - return "text/x-go"; - case "kt": - case "kts": - return "text/x-kotlin"; - case "java": - return "text/x-java"; - case "py": - return "text/x-python"; - case "groovy": - case "gradle": - return "text/x-groovy"; - - case "yml": - case "yaml": - return YamlText; - - case "sh": - return "text/x-sh"; - case "bat": - case "cmd": - return "application/bat"; - - case "xml": - case "csproj": - case "fsproj": - case "vbproj": - return "text/xml"; - - case "txt": - case "ps1": - return "text/plain"; - - case "sgml": - return "text/sgml"; - - case "mp3": - return "audio/mpeg3"; - - case "au": - case "snd": - return "audio/basic"; - - case "aac": - case "ac3": - case "aiff": - case "m4a": - case "m4b": - case "m4p": - case "mid": - case "midi": - case "wav": - return "audio/" + fileExt; - - case "qt": - case "mov": - return "video/quicktime"; - - case "mpg": - return "video/mpeg"; - - case "ogv": - return "video/ogg"; - - case "3gpp": - case "avi": - case "dv": - case "divx": - case "ogg": - case "mp4": - case "webm": - return "video/" + fileExt; - - case "rtf": - return "application/" + fileExt; - - case "xls": - case "xlt": - case "xla": - return Excel; - - case "xlsx": - return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; - case "xltx": - return "application/vnd.openxmlformats-officedocument.spreadsheetml.template"; - - case "doc": - case "dot": - return MsWord; - - case "docx": - return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; - case "dotx": - return "application/vnd.openxmlformats-officedocument.wordprocessingml.template"; - - case "ppt": - case "oit": - case "pps": - case "ppa": - return "application/vnd.ms-powerpoint"; - - case "pptx": - return "application/vnd.openxmlformats-officedocument.presentationml.presentation"; - case "potx": - return "application/vnd.openxmlformats-officedocument.presentationml.template"; - case "ppsx": - return "application/vnd.openxmlformats-officedocument.presentationml.slideshow"; - - case "mdb": - return "application/vnd.ms-access"; - - case "cer": - case "crt": - case "der": - return Cert; - - case "p10": - return "application/pkcs10"; - case "p12": - return "application/x-pkcs12"; - case "p7b": - case "spc": - return "application/x-pkcs7-certificates"; - case "p7c": - case "p7m": - return "application/pkcs7-mime"; - case "p7r": - return "application/x-pkcs7-certreqresp"; - case "p7s": - return "application/pkcs7-signature"; - case "sst": - return "application/vnd.ms-pki.certstore"; - - case "gz": - case "tgz": - case "zip": - case "rar": - case "lzh": - case "z": - return Compressed; - - case "eot": - return "application/vnd.ms-fontobject"; - - case "ttf": - return "application/octet-stream"; - - case "woff": - return "application/font-woff"; - case "woff2": - return "application/font-woff2"; - - case "jar": - return Jar; - - case "aaf": - case "aca": - case "asd": - case "bin": - case "cab": - case "chm": - case "class": - case "cur": - case "db": - case "dat": - case "deploy": - case "dll": - case "dsp": - case "exe": - case "fla": - case "ics": - case "inf": - case "mix": - case "msi": - case "mso": - case "obj": - case "ocx": - case "prm": - case "prx": - case "psd": - case "psp": - case "qxd": - case "sea": - case "snp": - case "so": - case "sqlite": - case "toc": - case "u32": - case "xmp": - case "xsn": - case "xtp": - return Binary; - - case "wasm": - return WebAssembly; - - case "dmg": - return Dmg; - case "pkg": - return Pkg; - - default: - return "application/" + fileExt; - } - } + return tcs.Task; } - public static class HttpHeaders + public static Task ConvertTo(this Task task) where TDerived : TBase { - public const string XParamOverridePrefix = "X-Param-Override-"; - - public const string XHttpMethodOverride = "X-Http-Method-Override"; - - public const string XAutoBatchCompleted = "X-AutoBatch-Completed"; // How many requests were completed before first failure - - public const string XTag = "X-Tag"; - - public const string XUserAuthId = "X-UAId"; - - public const string XTrigger = "X-Trigger"; // Trigger Events on UserAgent - - public const string XForwardedFor = "X-Forwarded-For"; // IP Address - - public const string XForwardedPort = "X-Forwarded-Port"; // 80 - - public const string XForwardedProtocol = "X-Forwarded-Proto"; // http or https - - public const string XRealIp = "X-Real-IP"; - - public const string XLocation = "X-Location"; - - public const string XStatus = "X-Status"; - - public const string XPoweredBy = "X-Powered-By"; - - public const string Referer = "Referer"; - - public const string CacheControl = "Cache-Control"; - - public const string IfModifiedSince = "If-Modified-Since"; - - public const string IfUnmodifiedSince = "If-Unmodified-Since"; - - public const string IfNoneMatch = "If-None-Match"; - - public const string IfMatch = "If-Match"; - - public const string LastModified = "Last-Modified"; - - public const string Accept = "Accept"; - - public const string AcceptEncoding = "Accept-Encoding"; - - public const string ContentType = "Content-Type"; - - public const string ContentEncoding = "Content-Encoding"; - - public const string ContentLength = "Content-Length"; - - public const string ContentDisposition = "Content-Disposition"; - - public const string Location = "Location"; - - public const string SetCookie = "Set-Cookie"; - - public const string ETag = "ETag"; - - public const string Age = "Age"; - - public const string Expires = "Expires"; - - public const string Vary = "Vary"; - - public const string Authorization = "Authorization"; - - public const string WwwAuthenticate = "WWW-Authenticate"; - - public const string AllowOrigin = "Access-Control-Allow-Origin"; - - public const string AllowMethods = "Access-Control-Allow-Methods"; - - public const string AllowHeaders = "Access-Control-Allow-Headers"; - - public const string AllowCredentials = "Access-Control-Allow-Credentials"; - - public const string ExposeHeaders = "Access-Control-Expose-Headers"; - - public const string AccessControlMaxAge = "Access-Control-Max-Age"; - - public const string Origin = "Origin"; - - public const string RequestMethod = "Access-Control-Request-Method"; - - public const string RequestHeaders = "Access-Control-Request-Headers"; - - public const string AcceptRanges = "Accept-Ranges"; - - public const string ContentRange = "Content-Range"; - - public const string Range = "Range"; - - public const string SOAPAction = "SOAPAction"; - - public const string Allow = "Allow"; - - public const string AcceptCharset = "Accept-Charset"; - - public const string AcceptLanguage = "Accept-Language"; - - public const string Connection = "Connection"; - - public const string Cookie = "Cookie"; - - public const string ContentLanguage = "Content-Language"; - - public const string Expect = "Expect"; - - public const string Pragma = "Pragma"; - - public const string ProxyAuthenticate = "Proxy-Authenticate"; - - public const string ProxyAuthorization = "Proxy-Authorization"; - - public const string ProxyConnection = "Proxy-Connection"; - - public const string SetCookie2 = "Set-Cookie2"; - - public const string TE = "TE"; - - public const string Trailer = "Trailer"; - - public const string TransferEncoding = "Transfer-Encoding"; - - public const string Upgrade = "Upgrade"; - - public const string Via = "Via"; - - public const string Warning = "Warning"; - - public const string Date = "Date"; - public const string Host = "Host"; - public const string UserAgent = "User-Agent"; - - public static HashSet RestrictedHeaders = new(StringComparer.OrdinalIgnoreCase) - { - Accept, - Connection, - ContentLength, - ContentType, - Date, - Expect, - Host, - IfModifiedSince, - Range, - Referer, - TransferEncoding, - UserAgent, - ProxyConnection, - }; + var tcs = new TaskCompletionSource(); + task.ContinueWith(t => tcs.SetResult(t.Result), TaskContinuationOptions.OnlyOnRanToCompletion); + task.ContinueWith(t => tcs.SetException(t.Exception.InnerExceptions), TaskContinuationOptions.OnlyOnFaulted); + task.ContinueWith(t => tcs.SetCanceled(), TaskContinuationOptions.OnlyOnCanceled); + return tcs.Task; } - public static class HttpMethods + public static Task GetResponseAsync(this WebRequest request) { - static readonly string[] allVerbs = { - "OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "TRACE", "CONNECT", // RFC 2616 - "PROPFIND", "PROPPATCH", "MKCOL", "COPY", "MOVE", "LOCK", "UNLOCK", // RFC 2518 - "VERSION-CONTROL", "REPORT", "CHECKOUT", "CHECKIN", "UNCHECKOUT", - "MKWORKSPACE", "UPDATE", "LABEL", "MERGE", "BASELINE-CONTROL", "MKACTIVITY", // RFC 3253 - "ORDERPATCH", // RFC 3648 - "ACL", // RFC 3744 - "PATCH", // https://datatracker.ietf.org/doc/draft-dusseault-http-patch/ - "SEARCH", // https://datatracker.ietf.org/doc/draft-reschke-webdav-search/ - "BCOPY", "BDELETE", "BMOVE", "BPROPFIND", "BPROPPATCH", "NOTIFY", - "POLL", "SUBSCRIBE", "UNSUBSCRIBE" //MS Exchange WebDav: http://msdn.microsoft.com/en-us/library/aa142917.aspx - }; - - public static HashSet AllVerbs = new(allVerbs); - - public static bool Exists(string httpMethod) => AllVerbs.Contains(httpMethod.ToUpper()); - public static bool HasVerb(string httpVerb) => Exists(httpVerb); - - public const string Get = "GET"; - public const string Put = "PUT"; - public const string Post = "POST"; - public const string Delete = "DELETE"; - public const string Options = "OPTIONS"; - public const string Head = "HEAD"; - public const string Patch = "PATCH"; + return GetResponseAsync((HttpWebRequest)request).ConvertTo(); } - public static class CompressionTypes + public static Task GetResponseAsync(this HttpWebRequest request) { - public static readonly string[] AllCompressionTypes = - { -#if NET6_0_OR_GREATER - Brotli, -#endif - Deflate, - GZip, - }; - -#if NET6_0_OR_GREATER - public const string Default = Brotli; -#else - public const string Default = Deflate; -#endif - - public const string Brotli = "br"; - public const string Deflate = "deflate"; - public const string GZip = "gzip"; - - public static bool IsValid(string compressionType) - { -#if NET6_0_OR_GREATER - return compressionType is Deflate or GZip or Brotli; -#else - return compressionType is Deflate or GZip; -#endif - } + var tcs = new TaskCompletionSource(); - public static void AssertIsValid(string compressionType) + try { - if (!IsValid(compressionType)) + request.BeginGetResponse(iar => { - throw new NotSupportedException(compressionType - + " is not a supported compression type. Valid types: " + string.Join(", ", AllCompressionTypes)); - } - } - - public static string GetExtension(string compressionType) - { - switch (compressionType) - { - case Brotli: - case Deflate: - case GZip: - return "." + compressionType; - default: - throw new NotSupportedException( - "Unknown compressionType: " + compressionType); - } + try + { + var response = (HttpWebResponse)request.EndGetResponse(iar); + tcs.SetResult(response); + } + catch (Exception exc) + { + tcs.SetException(exc); + } + }, null); } - } - - public static class HttpStatus - { - public static string GetStatusDescription(int statusCode) + catch (Exception exc) { - if (statusCode is >= 100 and < 600) - { - int i = statusCode / 100; - int j = statusCode % 100; - - if (j < Descriptions[i].Length) - return Descriptions[i][j]; - } - - return string.Empty; + tcs.SetException(exc); } - private static readonly string[][] Descriptions = - { - null, - new[] - { - /* 100 */ "Continue", - /* 101 */ "Switching Protocols", - /* 102 */ "Processing" - }, - new[] - { - /* 200 */ "OK", - /* 201 */ "Created", - /* 202 */ "Accepted", - /* 203 */ "Non-Authoritative Information", - /* 204 */ "No Content", - /* 205 */ "Reset Content", - /* 206 */ "Partial Content", - /* 207 */ "Multi-Status" - }, - new[] - { - /* 300 */ "Multiple Choices", - /* 301 */ "Moved Permanently", - /* 302 */ "Found", - /* 303 */ "See Other", - /* 304 */ "Not Modified", - /* 305 */ "Use Proxy", - /* 306 */ string.Empty, - /* 307 */ "Temporary Redirect" - }, - new[] - { - /* 400 */ "Bad Request", - /* 401 */ "Unauthorized", - /* 402 */ "Payment Required", - /* 403 */ "Forbidden", - /* 404 */ "Not Found", - /* 405 */ "Method Not Allowed", - /* 406 */ "Not Acceptable", - /* 407 */ "Proxy Authentication Required", - /* 408 */ "Request Timeout", - /* 409 */ "Conflict", - /* 410 */ "Gone", - /* 411 */ "Length Required", - /* 412 */ "Precondition Failed", - /* 413 */ "Request Entity Too Large", - /* 414 */ "Request-Uri Too Long", - /* 415 */ "Unsupported Media Type", - /* 416 */ "Requested Range Not Satisfiable", - /* 417 */ "Expectation Failed", - /* 418 */ string.Empty, - /* 419 */ string.Empty, - /* 420 */ string.Empty, - /* 421 */ string.Empty, - /* 422 */ "Unprocessable Entity", - /* 423 */ "Locked", - /* 424 */ "Failed Dependency" - }, - new[] - { - /* 500 */ "Internal Server Error", - /* 501 */ "Not Implemented", - /* 502 */ "Bad Gateway", - /* 503 */ "Service Unavailable", - /* 504 */ "Gateway Timeout", - /* 505 */ "Http Version Not Supported", - /* 506 */ string.Empty, - /* 507 */ "Insufficient Storage" - } - }; + return tcs.Task; } } + +//Allow Exceptions to Customize HTTP StatusCode and StatusDescription returned +public interface IHasStatusCode +{ + int StatusCode { get; } +} + +public interface IHasStatusDescription +{ + string StatusDescription { get; } +} \ No newline at end of file diff --git a/src/ServiceStack.Text/MimeTypes.cs b/src/ServiceStack.Text/MimeTypes.cs new file mode 100644 index 000000000..bd9e5430e --- /dev/null +++ b/src/ServiceStack.Text/MimeTypes.cs @@ -0,0 +1,452 @@ +using System; +using System.Collections.Generic; + +namespace ServiceStack; + +public static class MimeTypes +{ + public static Dictionary ExtensionMimeTypes = new(); + public const string Utf8Suffix = "; charset=utf-8"; + + public const string Html = "text/html"; + public const string HtmlUtf8 = Html + Utf8Suffix; + public const string Css = "text/css"; + public const string Xml = "application/xml"; + public const string XmlText = "text/xml"; + public const string Json = "application/json"; + public const string ProblemJson = "application/problem+json"; + public const string JsonText = "text/json"; + public const string Jsv = "application/jsv"; + public const string JsvText = "text/jsv"; + public const string Csv = "text/csv"; + public const string ProtoBuf = "application/x-protobuf"; + public const string JavaScript = "text/javascript"; + public const string WebAssembly = "application/wasm"; + public const string Jar = "application/java-archive"; + public const string Dmg = "application/x-apple-diskimage"; + public const string Pkg = "application/x-newton-compatible-pkg"; + + public const string FormUrlEncoded = "application/x-www-form-urlencoded"; + public const string MultiPartFormData = "multipart/form-data"; + public const string JsonReport = "text/jsonreport"; + public const string Soap11 = "text/xml; charset=utf-8"; + public const string Soap12 = "application/soap+xml"; + public const string Yaml = "application/yaml"; + public const string YamlText = "text/yaml"; + public const string PlainText = "text/plain"; + public const string MarkdownText = "text/markdown"; + public const string MsgPack = "application/x-msgpack"; + public const string Wire = "application/x-wire"; + public const string Compressed = "application/x-compressed"; + public const string NetSerializer = "application/x-netserializer"; + public const string Excel = "application/excel"; + public const string MsWord = "application/msword"; + public const string Cert = "application/x-x509-ca-cert"; + + public const string ImagePng = "image/png"; + public const string ImageGif = "image/gif"; + public const string ImageJpg = "image/jpeg"; + public const string ImageSvg = "image/svg+xml"; + + public const string Bson = "application/bson"; + public const string Binary = "application/octet-stream"; + public const string ServerSentEvents = "text/event-stream"; + + public static string GetExtension(string mimeType) + { + switch (mimeType) + { + case ProtoBuf: + return ".pbuf"; + } + + var parts = mimeType.Split('/'); + if (parts.Length == 1) return "." + parts[0].LeftPart('+').LeftPart(';'); + if (parts.Length == 2) return "." + parts[1].LeftPart('+').LeftPart(';'); + + throw new NotSupportedException("Unknown mimeType: " + mimeType); + } + + //Lower cases and trims left part of content-type prior ';' + public static string GetRealContentType(string contentType) + { + if (contentType == null) + return null; + + int start = -1, end = -1; + + for(int i=0; i < contentType.Length; i++) + { + if (!char.IsWhiteSpace(contentType[i])) + { + if (contentType[i] == ';') + break; + if (start == -1) + { + start = i; + } + end = i; + } + } + + return start != -1 + ? contentType.Substring(start, end - start + 1).ToLowerInvariant() + : null; + } + + //Compares two string from start to ';' char, case-insensitive, + //ignoring (trimming) spaces at start and end + public static bool MatchesContentType(string contentType, string matchesContentType) + { + if (contentType == null || matchesContentType == null) + return false; + + int start = -1, matchStart = -1, matchEnd = -1; + + for (var i=0; i < contentType.Length; i++) + { + if (char.IsWhiteSpace(contentType[i])) + continue; + start = i; + break; + } + + for (var i=0; i < matchesContentType.Length; i++) + { + if (char.IsWhiteSpace(matchesContentType[i])) + continue; + if (matchesContentType[i] == ';') + break; + if (matchStart == -1) + matchStart = i; + matchEnd = i; + } + + return start != -1 && matchStart != -1 && matchEnd != -1 + && string.Compare(contentType, start, + matchesContentType, matchStart, matchEnd - matchStart + 1, + StringComparison.OrdinalIgnoreCase) == 0; + } + + public static Func IsBinaryFilter { get; set; } + + public static bool IsBinary(string contentType) + { + var userFilter = IsBinaryFilter?.Invoke(contentType); + if (userFilter != null) + return userFilter.Value; + + var realContentType = GetRealContentType(contentType); + switch (realContentType) + { + case ProtoBuf: + case MsgPack: + case Binary: + case Bson: + case Wire: + case Cert: + case Excel: + case MsWord: + case Compressed: + case WebAssembly: + case Jar: + case Dmg: + case Pkg: + return true; + } + + // Text format exceptions to below heuristics + switch (realContentType) + { + case ImageSvg: + return false; + } + + var primaryType = realContentType.LeftPart('/'); + var secondaryType = realContentType.RightPart('/'); + switch (primaryType) + { + case "image": + case "audio": + case "video": + return true; + } + + if (secondaryType.StartsWith("pkc") + || secondaryType.StartsWith("x-pkc") + || secondaryType.StartsWith("font") + || secondaryType.StartsWith("vnd.ms-")) + return true; + + return false; + } + + public static string GetMimeType(string fileNameOrExt) + { + if (string.IsNullOrEmpty(fileNameOrExt)) + throw new ArgumentNullException(nameof(fileNameOrExt)); + + var fileExt = fileNameOrExt.LastRightPart('.').ToLower(); + if (ExtensionMimeTypes.TryGetValue(fileExt, out var mimeType)) + { + return mimeType; + } + + switch (fileExt) + { + case "jpeg": + return "image/jpeg"; + case "gif": + return "image/gif"; + case "png": + return "image/png"; + case "tiff": + return "image/tiff"; + case "bmp": + return "image/bmp"; + case "webp": + return "image/webp"; + + case "jpg": + return "image/jpeg"; + + case "tif": + return "image/tiff"; + + case "svg": + return ImageSvg; + + case "ico": + return "image/x-icon"; + + case "htm": + case "html": + case "shtml": + return "text/html"; + + case "js": + return "text/javascript"; + case "ts": + return "text/typescript"; + case "jsx": + return "text/jsx"; + + case "csv": + return Csv; + case "css": + return Css; + + case "cs": + return "text/x-csharp"; + case "fs": + return "text/x-fsharp"; + case "vb": + return "text/x-vb"; + case "dart": + return "application/dart"; + case "go": + return "text/x-go"; + case "kt": + case "kts": + return "text/x-kotlin"; + case "java": + return "text/x-java"; + case "py": + return "text/x-python"; + case "groovy": + case "gradle": + return "text/x-groovy"; + + case "yml": + case "yaml": + return YamlText; + + case "sh": + return "text/x-sh"; + case "bat": + case "cmd": + return "application/bat"; + + case "xml": + case "csproj": + case "fsproj": + case "vbproj": + return "text/xml"; + + case "txt": + case "ps1": + return "text/plain"; + + case "sgml": + return "text/sgml"; + + case "mp3": + return "audio/mpeg3"; + + case "au": + case "snd": + return "audio/basic"; + + case "aac": + case "ac3": + case "aiff": + case "m4a": + case "m4b": + case "m4p": + case "mid": + case "midi": + case "wav": + return "audio/" + fileExt; + + case "qt": + case "mov": + return "video/quicktime"; + + case "mpg": + return "video/mpeg"; + + case "ogv": + return "video/ogg"; + + case "3gpp": + case "avi": + case "dv": + case "divx": + case "ogg": + case "mp4": + case "webm": + return "video/" + fileExt; + + case "rtf": + return "application/" + fileExt; + + case "xls": + case "xlt": + case "xla": + return Excel; + + case "xlsx": + return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + case "xltx": + return "application/vnd.openxmlformats-officedocument.spreadsheetml.template"; + + case "doc": + case "dot": + return MsWord; + + case "docx": + return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + case "dotx": + return "application/vnd.openxmlformats-officedocument.wordprocessingml.template"; + + case "ppt": + case "oit": + case "pps": + case "ppa": + return "application/vnd.ms-powerpoint"; + + case "pptx": + return "application/vnd.openxmlformats-officedocument.presentationml.presentation"; + case "potx": + return "application/vnd.openxmlformats-officedocument.presentationml.template"; + case "ppsx": + return "application/vnd.openxmlformats-officedocument.presentationml.slideshow"; + + case "mdb": + return "application/vnd.ms-access"; + + case "cer": + case "crt": + case "der": + return Cert; + + case "p10": + return "application/pkcs10"; + case "p12": + return "application/x-pkcs12"; + case "p7b": + case "spc": + return "application/x-pkcs7-certificates"; + case "p7c": + case "p7m": + return "application/pkcs7-mime"; + case "p7r": + return "application/x-pkcs7-certreqresp"; + case "p7s": + return "application/pkcs7-signature"; + case "sst": + return "application/vnd.ms-pki.certstore"; + + case "gz": + case "tgz": + case "zip": + case "rar": + case "lzh": + case "z": + return Compressed; + + case "eot": + return "application/vnd.ms-fontobject"; + + case "ttf": + return "application/octet-stream"; + + case "woff": + return "application/font-woff"; + case "woff2": + return "application/font-woff2"; + + case "jar": + return Jar; + + case "aaf": + case "aca": + case "asd": + case "bin": + case "cab": + case "chm": + case "class": + case "cur": + case "db": + case "dat": + case "deploy": + case "dll": + case "dsp": + case "exe": + case "fla": + case "ics": + case "inf": + case "mix": + case "msi": + case "mso": + case "obj": + case "ocx": + case "prm": + case "prx": + case "psd": + case "psp": + case "qxd": + case "sea": + case "snp": + case "so": + case "sqlite": + case "toc": + case "u32": + case "xmp": + case "xsn": + case "xtp": + return Binary; + + case "wasm": + return WebAssembly; + + case "dmg": + return Dmg; + case "pkg": + return Pkg; + + default: + return "application/" + fileExt; + } + } +} \ No newline at end of file From ee06690f035e196fa522bf281a033beaa590c1d7 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Fri, 28 Jan 2022 17:06:10 +0800 Subject: [PATCH 43/75] Reimplement HttpUtils to use HttpClient in net6.0 --- src/ServiceStack.Text/Env.cs | 4 +- src/ServiceStack.Text/HttpUtils.HttpClient.cs | 957 ++++++++++++++++++ ...tils.Legacy.cs => HttpUtils.WebRequest.cs} | 141 +-- src/ServiceStack.Text/HttpUtils.cs | 139 ++- src/ServiceStack.Text/LicenseUtils.cs | 6 +- src/ServiceStack.Text/PclExport.NetCore.cs | 2 +- .../PclExport.NetStandard.cs | 5 +- src/ServiceStack.Text/PclExport.cs | 10 +- .../ServiceStack.Text.csproj | 3 + .../HttpUtilsMockTests.cs | 4 +- .../ServiceStack.Text.Tests.csproj | 1 + .../UseCases/GithubV3ApiTests.cs | 2 +- .../UseCases/StripeGateway.cs | 136 +-- 13 files changed, 1216 insertions(+), 194 deletions(-) create mode 100644 src/ServiceStack.Text/HttpUtils.HttpClient.cs rename src/ServiceStack.Text/{HttpUtils.Legacy.cs => HttpUtils.WebRequest.cs} (93%) diff --git a/src/ServiceStack.Text/Env.cs b/src/ServiceStack.Text/Env.cs index edf3a4b2b..257d5db63 100644 --- a/src/ServiceStack.Text/Env.cs +++ b/src/ServiceStack.Text/Env.cs @@ -107,13 +107,11 @@ static Env() + VersionString + " " + PclExport.Instance.PlatformName + (IsLinux ? "/Linux" : IsOSX ? "/OSX" : IsUnix ? "/Unix" : IsWindows ? "/Windows" : "/UnknownOS") + (IsIOS ? "/iOS" : IsAndroid ? "/Android" : IsUWP ? "/UWP" : "") - + PlatformModifier + + (IsNet6 ? "/net6" : IsNetStandard20 ? "/std2.0" : IsNetFramework ? "/netfx" : "") + (IsMono ? "/Mono" : "") + $"/{LicenseUtils.Info}"; __releaseDate = new DateTime(2001,01,01); } - public static string PlatformModifier => (IsNet6 ? "/net6" : IsNetStandard20 ? "/std2.0" : IsNetFramework ? "/netfx" : "") + (IsMono ? "/Mono" : ""); - public static string VersionString { get; set; } public static decimal ServiceStackVersion = 6.01m; diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs new file mode 100644 index 000000000..dba1d5209 --- /dev/null +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -0,0 +1,957 @@ +#if NET6_0_OR_GREATER +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using ServiceStack.Text; + +namespace ServiceStack; + +public static partial class HttpUtils +{ + public static Func HandlerFactory { get; set; } = + () => new HttpClientHandler { + UseDefaultCredentials = true, + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + }; + + public static Func ClientFactory { get; set; } = + url => new HttpClient(HandlerFactory(), disposeHandler:true); + + public static string GetJsonFromUrl(this string url, + Action? requestFilter = null, Action? responseFilter = null) + { + return url.GetStringFromUrl(MimeTypes.Json, requestFilter, responseFilter); + } + + public static Task GetJsonFromUrlAsync(this string url, + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return url.GetStringFromUrlAsync(MimeTypes.Json, requestFilter, responseFilter, token: token); + } + + public static string GetXmlFromUrl(this string url, + Action? requestFilter = null, Action? responseFilter = null) + { + return url.GetStringFromUrl(MimeTypes.Xml, requestFilter, responseFilter); + } + + public static Task GetXmlFromUrlAsync(this string url, + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return url.GetStringFromUrlAsync(MimeTypes.Xml, requestFilter, responseFilter, token: token); + } + + public static string GetCsvFromUrl(this string url, + Action? requestFilter = null, Action? responseFilter = null) + { + return url.GetStringFromUrl(MimeTypes.Csv, requestFilter, responseFilter); + } + + public static Task GetCsvFromUrlAsync(this string url, + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return url.GetStringFromUrlAsync(MimeTypes.Csv, requestFilter, responseFilter, token: token); + } + + public static string GetStringFromUrl(this string url, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task GetStringFromUrlAsync(this string url, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, + token: token); + } + + public static string PostStringToUrl(this string url, string? requestBody = null, + string? contentType = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "POST", + requestBody: requestBody, contentType: contentType, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PostStringToUrlAsync(this string url, string? requestBody = null, + string? contentType = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "POST", + requestBody: requestBody, contentType: contentType, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PostToUrl(this string url, string? formData = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "POST", + contentType: MimeTypes.FormUrlEncoded, requestBody: formData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PostToUrlAsync(this string url, string? formData = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "POST", + contentType: MimeTypes.FormUrlEncoded, requestBody: formData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PostToUrl(this string url, object? formData = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + string? postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; + + return SendStringToUrl(url, method: "POST", + contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PostToUrlAsync(this string url, object? formData = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + string? postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; + + return SendStringToUrlAsync(url, method: "POST", + contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PostJsonToUrl(this string url, string json, + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "POST", requestBody: json, contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PostJsonToUrlAsync(this string url, string json, + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "POST", requestBody: json, contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PostJsonToUrl(this string url, object data, + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "POST", requestBody: data.ToJson(), contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PostJsonToUrlAsync(this string url, object data, + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "POST", requestBody: data.ToJson(), contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PostXmlToUrl(this string url, string xml, + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "POST", requestBody: xml, contentType: MimeTypes.Xml, accept: MimeTypes.Xml, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PostXmlToUrlAsync(this string url, string xml, + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "POST", requestBody: xml, contentType: MimeTypes.Xml, + accept: MimeTypes.Xml, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PostCsvToUrl(this string url, string csv, + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "POST", requestBody: csv, contentType: MimeTypes.Csv, accept: MimeTypes.Csv, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PostCsvToUrlAsync(this string url, string csv, + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "POST", requestBody: csv, contentType: MimeTypes.Csv, + accept: MimeTypes.Csv, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PutStringToUrl(this string url, string? requestBody = null, + string? contentType = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "PUT", + requestBody: requestBody, contentType: contentType, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PutStringToUrlAsync(this string url, string? requestBody = null, + string? contentType = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PUT", + requestBody: requestBody, contentType: contentType, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PutToUrl(this string url, string? formData = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "PUT", + contentType: MimeTypes.FormUrlEncoded, requestBody: formData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PutToUrlAsync(this string url, string? formData = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PUT", + contentType: MimeTypes.FormUrlEncoded, requestBody: formData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PutToUrl(this string url, object? formData = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + string? postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; + + return SendStringToUrl(url, method: "PUT", + contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PutToUrlAsync(this string url, object? formData = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + string? postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; + + return SendStringToUrlAsync(url, method: "PUT", + contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PutJsonToUrl(this string url, string json, + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "PUT", requestBody: json, contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PutJsonToUrlAsync(this string url, string json, + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PUT", requestBody: json, contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PutJsonToUrl(this string url, object data, + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "PUT", requestBody: data.ToJson(), contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PutJsonToUrlAsync(this string url, object data, + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PUT", requestBody: data.ToJson(), contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PutXmlToUrl(this string url, string xml, + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "PUT", requestBody: xml, contentType: MimeTypes.Xml, accept: MimeTypes.Xml, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PutXmlToUrlAsync(this string url, string xml, + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PUT", requestBody: xml, contentType: MimeTypes.Xml, + accept: MimeTypes.Xml, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PutCsvToUrl(this string url, string csv, + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "PUT", requestBody: csv, contentType: MimeTypes.Csv, accept: MimeTypes.Csv, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PutCsvToUrlAsync(this string url, string csv, + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PUT", requestBody: csv, contentType: MimeTypes.Csv, + accept: MimeTypes.Csv, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PatchStringToUrl(this string url, string? requestBody = null, + string? contentType = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "PATCH", + requestBody: requestBody, contentType: contentType, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PatchStringToUrlAsync(this string url, string? requestBody = null, + string? contentType = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PATCH", + requestBody: requestBody, contentType: contentType, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PatchToUrl(this string url, string? formData = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "PATCH", + contentType: MimeTypes.FormUrlEncoded, requestBody: formData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PatchToUrlAsync(this string url, string? formData = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PATCH", + contentType: MimeTypes.FormUrlEncoded, requestBody: formData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PatchToUrl(this string url, object? formData = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + string? postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; + + return SendStringToUrl(url, method: "PATCH", + contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PatchToUrlAsync(this string url, object? formData = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + string? postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; + + return SendStringToUrlAsync(url, method: "PATCH", + contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PatchJsonToUrl(this string url, string json, + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "PATCH", requestBody: json, contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PatchJsonToUrlAsync(this string url, string json, + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PATCH", requestBody: json, contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string PatchJsonToUrl(this string url, object data, + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "PATCH", requestBody: data.ToJson(), contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PatchJsonToUrlAsync(this string url, object data, + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "PATCH", requestBody: data.ToJson(), contentType: MimeTypes.Json, + accept: MimeTypes.Json, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static string DeleteFromUrl(this string url, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "DELETE", accept: accept, requestFilter: requestFilter, + responseFilter: responseFilter); + } + + public static Task DeleteFromUrlAsync(this string url, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "DELETE", accept: accept, requestFilter: requestFilter, + responseFilter: responseFilter, token: token); + } + + public static string OptionsFromUrl(this string url, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "OPTIONS", accept: accept, requestFilter: requestFilter, + responseFilter: responseFilter); + } + + public static Task OptionsFromUrlAsync(this string url, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "OPTIONS", accept: accept, requestFilter: requestFilter, + responseFilter: responseFilter, token: token); + } + + public static string HeadFromUrl(this string url, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "HEAD", accept: accept, requestFilter: requestFilter, + responseFilter: responseFilter); + } + + public static Task HeadFromUrlAsync(this string url, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStringToUrlAsync(url, method: "HEAD", accept: accept, requestFilter: requestFilter, + responseFilter: responseFilter, token: token); + } + + public static string SendStringToUrl(this string url, string method = HttpMethods.Post, + string? requestBody = null, string? contentType = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + var client = ClientFactory(url); + var httpReq = new HttpRequestMessage(new HttpMethod(method), url); + httpReq.Headers.Add(HttpHeaders.Accept, accept); + + if (requestBody != null) + { + httpReq.Content = new StringContent(requestBody, UseEncoding); + if (contentType != null) + httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); + } + requestFilter?.Invoke(httpReq); + + var httpRes = client.Send(httpReq); + responseFilter?.Invoke(httpRes); + httpRes.EnsureSuccessStatusCode(); + return httpRes.Content.ReadAsStream().ReadToEnd(UseEncoding); + } + + public static async Task SendStringToUrlAsync(this string url, string method = HttpMethods.Post, + string? requestBody = null, + string? contentType = null, string accept = "*/*", Action? requestFilter = null, + Action? responseFilter = null, CancellationToken token = default) + { + var client = ClientFactory(url); + var httpReq = new HttpRequestMessage(new HttpMethod(method), url); + httpReq.Headers.Add(HttpHeaders.Accept, accept); + + if (requestBody != null) + { + httpReq.Content = new StringContent(requestBody, UseEncoding); + if (contentType != null) + httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); + } + requestFilter?.Invoke(httpReq); + + var httpRes = await client.SendAsync(httpReq, token).ConfigAwait(); + responseFilter?.Invoke(httpRes); + httpRes.EnsureSuccessStatusCode(); + return await httpRes.Content.ReadAsStringAsync(token).ConfigAwait(); + } + + public static byte[] GetBytesFromUrl(this string url, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + return url.SendBytesToUrl(accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task GetBytesFromUrlAsync(this string url, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return url.SendBytesToUrlAsync(accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, + token: token); + } + + public static byte[] PostBytesToUrl(this string url, byte[]? requestBody = null, string? contentType = null, + string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + return SendBytesToUrl(url, method: "POST", + contentType: contentType, requestBody: requestBody, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PostBytesToUrlAsync(this string url, byte[]? requestBody = null, + string? contentType = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendBytesToUrlAsync(url, method: "POST", + contentType: contentType, requestBody: requestBody, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static byte[] PutBytesToUrl(this string url, byte[]? requestBody = null, string? contentType = null, + string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + return SendBytesToUrl(url, method: "PUT", + contentType: contentType, requestBody: requestBody, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PutBytesToUrlAsync(this string url, byte[]? requestBody = null, string? contentType = null, + string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendBytesToUrlAsync(url, method: "PUT", + contentType: contentType, requestBody: requestBody, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static byte[] SendBytesToUrl(this string url, string method = HttpMethods.Post, + byte[]? requestBody = null, string? contentType = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + var client = ClientFactory(url); + var httpReq = new HttpRequestMessage(new HttpMethod(method), url); + httpReq.Headers.Add(HttpHeaders.Accept, accept); + + if (requestBody != null) + { + httpReq.Content = new ReadOnlyMemoryContent(requestBody); + if (contentType != null) + httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); + } + requestFilter?.Invoke(httpReq); + + var httpRes = client.Send(httpReq); + responseFilter?.Invoke(httpRes); + httpRes.EnsureSuccessStatusCode(); + return httpRes.Content.ReadAsStream().ReadFully(); + } + + public static async Task SendBytesToUrlAsync(this string url, string method = HttpMethods.Post, + byte[]? requestBody = null, string? contentType = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + var client = ClientFactory(url); + var httpReq = new HttpRequestMessage(new HttpMethod(method), url); + httpReq.Headers.Add(HttpHeaders.Accept, accept); + + if (requestBody != null) + { + httpReq.Content = new ReadOnlyMemoryContent(requestBody); + if (contentType != null) + httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); + } + requestFilter?.Invoke(httpReq); + + var httpRes = await client.SendAsync(httpReq, token).ConfigAwait(); + responseFilter?.Invoke(httpRes); + httpRes.EnsureSuccessStatusCode(); + return await (await httpRes.Content.ReadAsStreamAsync(token).ConfigAwait()).ReadFullyAsync(token).ConfigAwait(); + } + + public static Stream GetStreamFromUrl(this string url, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + return url.SendStreamToUrl(accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task GetStreamFromUrlAsync(this string url, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return url.SendStreamToUrlAsync(accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, + token: token); + } + + public static Stream PostStreamToUrl(this string url, Stream? requestBody = null, string? contentType = null, + string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStreamToUrl(url, method: "POST", + contentType: contentType, requestBody: requestBody, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PostStreamToUrlAsync(this string url, Stream? requestBody = null, + string? contentType = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStreamToUrlAsync(url, method: "POST", + contentType: contentType, requestBody: requestBody, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + public static Stream PutStreamToUrl(this string url, Stream? requestBody = null, string? contentType = null, + string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStreamToUrl(url, method: "PUT", + contentType: contentType, requestBody: requestBody, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static Task PutStreamToUrlAsync(this string url, Stream? requestBody = null, + string? contentType = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return SendStreamToUrlAsync(url, method: "PUT", + contentType: contentType, requestBody: requestBody, + accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); + } + + /// + /// Returns HttpWebResponse Stream which must be disposed + /// + public static Stream SendStreamToUrl(this string url, string method = HttpMethods.Post, + Stream? requestBody = null, string? contentType = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + var client = ClientFactory(url); + var httpReq = new HttpRequestMessage(new HttpMethod(method), url); + httpReq.Headers.Add(HttpHeaders.Accept, accept); + + if (requestBody != null) + { + httpReq.Content = new StreamContent(requestBody); + if (contentType != null) + httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); + } + requestFilter?.Invoke(httpReq); + + var httpRes = client.Send(httpReq); + responseFilter?.Invoke(httpRes); + httpRes.EnsureSuccessStatusCode(); + return httpRes.Content.ReadAsStream(); + } + + /// + /// Returns HttpWebResponse Stream which must be disposed + /// + public static async Task SendStreamToUrlAsync(this string url, string method = HttpMethods.Post, + Stream? requestBody = null, string? contentType = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + var client = ClientFactory(url); + var httpReq = new HttpRequestMessage(new HttpMethod(method), url); + httpReq.Headers.Add(HttpHeaders.Accept, accept); + + if (requestBody != null) + { + httpReq.Content = new StreamContent(requestBody); + if (contentType != null) + httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); + } + requestFilter?.Invoke(httpReq); + + var httpRes = await client.SendAsync(httpReq, token).ConfigAwait(); + responseFilter?.Invoke(httpRes); + httpRes.EnsureSuccessStatusCode(); + return await httpRes.Content.ReadAsStreamAsync(token).ConfigAwait(); + } + + public static HttpStatusCode? GetResponseStatus(this string url) + { + try + { + var client = ClientFactory(url); + var httpReq = new HttpRequestMessage(new HttpMethod(HttpMethods.Get), url); + httpReq.Headers.Add(HttpHeaders.Accept, "*/*"); + var httpRes = client.Send(httpReq); + return httpRes.StatusCode; + } + catch (Exception ex) + { + return ex.GetStatus(); + } + } + + public static HttpResponseMessage? GetErrorResponse(this string url) + { + var client = ClientFactory(url); + var httpReq = new HttpRequestMessage(new HttpMethod(HttpMethods.Get), url); + httpReq.Headers.Add(HttpHeaders.Accept, "*/*"); + var httpRes = client.Send(httpReq); + return httpRes.IsSuccessStatusCode + ? null + : httpRes; + } + + public static async Task GetErrorResponseAsync(this string url, CancellationToken token=default) + { + var client = ClientFactory(url); + var httpReq = new HttpRequestMessage(new HttpMethod(HttpMethods.Get), url); + httpReq.Headers.Add(HttpHeaders.Accept, "*/*"); + var httpRes = await client.SendAsync(httpReq, token).ConfigAwait(); + return httpRes.IsSuccessStatusCode + ? null + : httpRes; + } + + public static string ReadToEnd(this HttpResponseMessage webRes) + { + using var stream = webRes.Content.ReadAsStream(); + return stream.ReadToEnd(UseEncoding); + } + + public static Task ReadToEndAsync(this HttpResponseMessage webRes) + { + using var stream = webRes.Content.ReadAsStream(); + return stream.ReadToEndAsync(UseEncoding); + } + + public static IEnumerable ReadLines(this HttpResponseMessage webRes) + { + using var stream = webRes.Content.ReadAsStream(); + using var reader = new StreamReader(stream, UseEncoding, true, 1024, leaveOpen: true); + string? line; + while ((line = reader.ReadLine()) != null) + { + yield return line; + } + } + + public static HttpResponseMessage UploadFile(this HttpRequestMessage httpReq, Stream fileStream, string fileName, + string? mimeType = null, string accept = "*/*", string method = HttpMethods.Post, string field = "file", + Action? requestFilter = null, Action? responseFilter = null) + { + if (httpReq.RequestUri == null) + throw new ArgumentException(nameof(httpReq.RequestUri)); + + httpReq.Method = new HttpMethod(method); + httpReq.Headers.Add(HttpHeaders.Accept, accept); + requestFilter?.Invoke(httpReq); + + using var content = new MultipartFormDataContent(); + var fileBytes = fileStream.ReadFully(); + using var fileContent = new ByteArrayContent(fileBytes, 0, fileBytes.Length); + fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") + { + Name = "file", + FileName = fileName + }; + fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(mimeType ?? MimeTypes.GetMimeType(fileName)); + content.Add(fileContent, "file", fileName); + + var client = ClientFactory(httpReq.RequestUri!.ToString()); + var httpRes = client.Send(httpReq); + responseFilter?.Invoke(httpRes); + httpRes.EnsureSuccessStatusCode(); + return httpRes; + } + + public static async Task UploadFileAsync(this HttpRequestMessage httpReq, Stream fileStream, string fileName, + string? mimeType = null, string accept = "*/*", string method = HttpMethods.Post, string field = "file", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + if (httpReq.RequestUri == null) + throw new ArgumentException(nameof(httpReq.RequestUri)); + + httpReq.Method = new HttpMethod(method); + httpReq.Headers.Add(HttpHeaders.Accept, accept); + requestFilter?.Invoke(httpReq); + + using var content = new MultipartFormDataContent(); + var fileBytes = await fileStream.ReadFullyAsync(token).ConfigAwait(); + using var fileContent = new ByteArrayContent(fileBytes, 0, fileBytes.Length); + fileContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") + { + Name = "file", + FileName = fileName + }; + fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(mimeType ?? MimeTypes.GetMimeType(fileName)); + content.Add(fileContent, "file", fileName); + + var client = ClientFactory(httpReq.RequestUri!.ToString()); + var httpRes = await client.SendAsync(httpReq, token).ConfigAwait(); + responseFilter?.Invoke(httpRes); + httpRes.EnsureSuccessStatusCode(); + return httpRes; + } + + public static void UploadFile(this HttpRequestMessage httpReq, Stream fileStream, string fileName) + { + if (fileName == null) + throw new ArgumentNullException(nameof(fileName)); + var mimeType = MimeTypes.GetMimeType(fileName); + if (mimeType == null) + throw new ArgumentException("Mime-type not found for file: " + fileName); + + UploadFile(httpReq, fileStream, fileName, mimeType); + } + + public static async Task UploadFileAsync(this HttpRequestMessage webRequest, Stream fileStream, string fileName, + CancellationToken token = default) + { + if (fileName == null) + throw new ArgumentNullException(nameof(fileName)); + var mimeType = MimeTypes.GetMimeType(fileName); + if (mimeType == null) + throw new ArgumentException("Mime-type not found for file: " + fileName); + + await UploadFileAsync(webRequest, fileStream, fileName, mimeType, token: token).ConfigAwait(); + } + + public static string PostXmlToUrl(this string url, object data, + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "POST", requestBody: data.ToXml(), contentType: MimeTypes.Xml, + accept: MimeTypes.Xml, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static string PostCsvToUrl(this string url, object data, + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "POST", requestBody: data.ToCsv(), contentType: MimeTypes.Csv, + accept: MimeTypes.Csv, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static string PutXmlToUrl(this string url, object data, + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "PUT", requestBody: data.ToXml(), contentType: MimeTypes.Xml, + accept: MimeTypes.Xml, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static string PutCsvToUrl(this string url, object data, + Action? requestFilter = null, Action? responseFilter = null) + { + return SendStringToUrl(url, method: "PUT", requestBody: data.ToCsv(), contentType: MimeTypes.Csv, + accept: MimeTypes.Csv, + requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static HttpResponseMessage PostFileToUrl(this string url, + FileInfo uploadFileInfo, string uploadFileMimeType, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + var webReq = new HttpRequestMessage(HttpMethod.Post, url); + using var fileStream = uploadFileInfo.OpenRead(); + var fileName = uploadFileInfo.Name; + + return webReq.UploadFile(fileStream, fileName, uploadFileMimeType, accept: accept, + method: HttpMethods.Post, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static async Task PostFileToUrlAsync(this string url, + FileInfo uploadFileInfo, string uploadFileMimeType, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + var webReq = new HttpRequestMessage(HttpMethod.Post, url); + await using var fileStream = uploadFileInfo.OpenRead(); + var fileName = uploadFileInfo.Name; + + return await webReq.UploadFileAsync(fileStream, fileName, uploadFileMimeType, accept: accept, + method: HttpMethods.Post, requestFilter: requestFilter, responseFilter: responseFilter, token: token).ConfigAwait(); + } + + public static HttpResponseMessage PutFileToUrl(this string url, + FileInfo uploadFileInfo, string uploadFileMimeType, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { + var webReq = new HttpRequestMessage(HttpMethod.Put, url); + using var fileStream = uploadFileInfo.OpenRead(); + var fileName = uploadFileInfo.Name; + + return webReq.UploadFile(fileStream, fileName, uploadFileMimeType, accept: accept, + method: HttpMethods.Post, requestFilter: requestFilter, responseFilter: responseFilter); + } + + public static async Task PutFileToUrlAsync(this string url, + FileInfo uploadFileInfo, string uploadFileMimeType, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + var webReq = new HttpRequestMessage(HttpMethod.Put, url); + await using var fileStream = uploadFileInfo.OpenRead(); + var fileName = uploadFileInfo.Name; + + return await webReq.UploadFileAsync(fileStream, fileName, uploadFileMimeType, accept: accept, + method: HttpMethods.Post, requestFilter: requestFilter, responseFilter: responseFilter, token: token).ConfigAwait(); + } + + public static HttpRequestMessage With(this HttpRequestMessage httpReq, + string? accept = null, + string? userAgent = null, + Dictionary? headers = null) + { + if (accept != null) + httpReq.Headers.Add(HttpHeaders.Accept, accept); + + if (userAgent != null) + httpReq.Headers.UserAgent.Add(new ProductInfoHeaderValue(userAgent)); + + if (headers != null) + { + foreach (var entry in headers) + { + httpReq.Headers.Add(entry.Key, entry.Value); + } + } + + return httpReq; + } + +} + +#endif diff --git a/src/ServiceStack.Text/HttpUtils.Legacy.cs b/src/ServiceStack.Text/HttpUtils.WebRequest.cs similarity index 93% rename from src/ServiceStack.Text/HttpUtils.Legacy.cs rename to src/ServiceStack.Text/HttpUtils.WebRequest.cs index 4bb8ba9d9..d368f4f60 100644 --- a/src/ServiceStack.Text/HttpUtils.Legacy.cs +++ b/src/ServiceStack.Text/HttpUtils.WebRequest.cs @@ -1,3 +1,4 @@ +#if !NET6_0_OR_GREATER using System; using System.Collections.Generic; using System.IO; @@ -773,54 +774,6 @@ public static async Task SendStreamToUrlAsync(this string url, string me return stream; } - public static bool IsAny300(this Exception ex) - { - var status = ex.GetStatus(); - return status >= HttpStatusCode.MultipleChoices && status < HttpStatusCode.BadRequest; - } - - public static bool IsAny400(this Exception ex) - { - var status = ex.GetStatus(); - return status >= HttpStatusCode.BadRequest && status < HttpStatusCode.InternalServerError; - } - - public static bool IsAny500(this Exception ex) - { - var status = ex.GetStatus(); - return status >= HttpStatusCode.InternalServerError && (int)status < 600; - } - - public static bool IsNotModified(this Exception ex) - { - return GetStatus(ex) == HttpStatusCode.NotModified; - } - - public static bool IsBadRequest(this Exception ex) - { - return GetStatus(ex) == HttpStatusCode.BadRequest; - } - - public static bool IsNotFound(this Exception ex) - { - return GetStatus(ex) == HttpStatusCode.NotFound; - } - - public static bool IsUnauthorized(this Exception ex) - { - return GetStatus(ex) == HttpStatusCode.Unauthorized; - } - - public static bool IsForbidden(this Exception ex) - { - return GetStatus(ex) == HttpStatusCode.Forbidden; - } - - public static bool IsInternalServerError(this Exception ex) - { - return GetStatus(ex) == HttpStatusCode.InternalServerError; - } - public static HttpStatusCode? GetResponseStatus(this string url) { try @@ -838,74 +791,6 @@ public static bool IsInternalServerError(this Exception ex) } } - public static HttpStatusCode? GetStatus(this Exception ex) - { - if (ex == null) - return null; - - if (ex is WebException webEx) - return GetStatus(webEx); - - if (ex is IHasStatusCode hasStatus) - return (HttpStatusCode)hasStatus.StatusCode; - - return null; - } - - public static HttpStatusCode? GetStatus(this WebException webEx) - { - var httpRes = webEx?.Response as HttpWebResponse; - return httpRes?.StatusCode; - } - - public static bool HasStatus(this Exception ex, HttpStatusCode statusCode) - { - return GetStatus(ex) == statusCode; - } - - public static string GetResponseBody(this Exception ex) - { - if (!(ex is WebException webEx) || webEx.Response == null || webEx.Status != WebExceptionStatus.ProtocolError) - return null; - - var errorResponse = (HttpWebResponse)webEx.Response; - using var responseStream = errorResponse.GetResponseStream(); - return responseStream.ReadToEnd(UseEncoding); - } - - public static async Task GetResponseBodyAsync(this Exception ex, CancellationToken token = default) - { - if (!(ex is WebException webEx) || webEx.Response == null || webEx.Status != WebExceptionStatus.ProtocolError) - return null; - - var errorResponse = (HttpWebResponse)webEx.Response; - using var responseStream = errorResponse.GetResponseStream(); - return await responseStream.ReadToEndAsync(UseEncoding).ConfigAwait(); - } - - public static string ReadToEnd(this WebResponse webRes) - { - using var stream = webRes.GetResponseStream(); - return stream.ReadToEnd(UseEncoding); - } - - public static Task ReadToEndAsync(this WebResponse webRes) - { - using var stream = webRes.GetResponseStream(); - return stream.ReadToEndAsync(UseEncoding); - } - - public static IEnumerable ReadLines(this WebResponse webRes) - { - using var stream = webRes.GetResponseStream(); - using var reader = new StreamReader(stream, UseEncoding, true, 1024, leaveOpen: true); - string line; - while ((line = reader.ReadLine()) != null) - { - yield return line; - } - } - public static HttpWebResponse GetErrorResponse(this string url) { try @@ -1187,8 +1072,29 @@ private static byte[] GetHeaderBytes(string fileName, string mimeType, string fi var headerBytes = header.ToAsciiBytes(); return headerBytes; } -} + public static HttpWebRequest With(this HttpWebRequest httpReq, + string accept = null, + string userAgent = null, + Dictionary headers = null) + { + if (accept != null) + httpReq.Headers.Add(HttpHeaders.Accept, accept); + + if (userAgent != null) + httpReq.UserAgent = userAgent; + + if (headers != null) + { + foreach (var entry in headers) + { + httpReq.Headers[entry.Key] = entry.Value; + } + } + + return httpReq; + } +} public interface IHttpResultsFilter : IDisposable { @@ -1241,3 +1147,4 @@ public void UploadStream(HttpWebRequest webRequest, Stream fileStream, string fi UploadFileFn?.Invoke(webRequest, fileStream, fileName); } } +#endif diff --git a/src/ServiceStack.Text/HttpUtils.cs b/src/ServiceStack.Text/HttpUtils.cs index ab7c7da07..d96edc574 100644 --- a/src/ServiceStack.Text/HttpUtils.cs +++ b/src/ServiceStack.Text/HttpUtils.cs @@ -2,18 +2,31 @@ //License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt using System; +using System.Collections.Generic; using System.IO; using System.Net; using System.Text; +using System.Threading; using System.Threading.Tasks; +using ServiceStack.Text; namespace ServiceStack; public static partial class HttpUtils { - public static string UserAgent = "ServiceStack.Text" + Text.Env.PlatformModifier; + public static string UserAgent = "ServiceStack.Text" + +#if NET6_0_OR_GREATER + "/net6" +#elif NETSTANDARD2_0 + "/std2.0" +#elif NETFX + "/net472" +#else + "/unknown" +#endif +; - public static Encoding UseEncoding { get; set; } = PclExport.Instance.GetUTF8Encoding(false); + public static Encoding UseEncoding { get; set; } = new UTF8Encoding(false); public static string AddQueryParam(this string url, string key, object val, bool encode = true) { @@ -208,6 +221,128 @@ public static Task GetResponseAsync(this HttpWebRequest request return tcs.Task; } + + public static bool IsAny300(this Exception ex) + { + var status = ex.GetStatus(); + return status is >= HttpStatusCode.MultipleChoices and < HttpStatusCode.BadRequest; + } + + public static bool IsAny400(this Exception ex) + { + var status = ex.GetStatus(); + return status is >= HttpStatusCode.BadRequest and < HttpStatusCode.InternalServerError; + } + + public static bool IsAny500(this Exception ex) + { + var status = ex.GetStatus(); + return status >= HttpStatusCode.InternalServerError && (int)status < 600; + } + + public static bool IsNotModified(this Exception ex) + { + return GetStatus(ex) == HttpStatusCode.NotModified; + } + + public static bool IsBadRequest(this Exception ex) + { + return GetStatus(ex) == HttpStatusCode.BadRequest; + } + + public static bool IsNotFound(this Exception ex) + { + return GetStatus(ex) == HttpStatusCode.NotFound; + } + + public static bool IsUnauthorized(this Exception ex) + { + return GetStatus(ex) == HttpStatusCode.Unauthorized; + } + + public static bool IsForbidden(this Exception ex) + { + return GetStatus(ex) == HttpStatusCode.Forbidden; + } + + public static bool IsInternalServerError(this Exception ex) + { + return GetStatus(ex) == HttpStatusCode.InternalServerError; + } + + public static HttpStatusCode? GetStatus(this Exception ex) + { +#if NET6_0_OR_GREATER + if (ex is System.Net.Http.HttpRequestException httpEx) + return GetStatus(httpEx); +#endif + + if (ex is WebException webEx) + return GetStatus(webEx); + + if (ex is IHasStatusCode hasStatus) + return (HttpStatusCode)hasStatus.StatusCode; + + return null; + } + +#if NET6_0_OR_GREATER + public static HttpStatusCode? GetStatus(this System.Net.Http.HttpRequestException ex) => ex.StatusCode; +#endif + + public static HttpStatusCode? GetStatus(this WebException webEx) + { + var httpRes = webEx?.Response as HttpWebResponse; + return httpRes?.StatusCode; + } + + public static bool HasStatus(this Exception ex, HttpStatusCode statusCode) + { + return GetStatus(ex) == statusCode; + } + + public static string GetResponseBody(this Exception ex) + { + if (!(ex is WebException webEx) || webEx.Response == null || webEx.Status != WebExceptionStatus.ProtocolError) + return null; + + var errorResponse = (HttpWebResponse)webEx.Response; + using var responseStream = errorResponse.GetResponseStream(); + return responseStream.ReadToEnd(UseEncoding); + } + + public static async Task GetResponseBodyAsync(this Exception ex, CancellationToken token = default) + { + if (!(ex is WebException webEx) || webEx.Response == null || webEx.Status != WebExceptionStatus.ProtocolError) + return null; + + var errorResponse = (HttpWebResponse)webEx.Response; + using var responseStream = errorResponse.GetResponseStream(); + return await responseStream.ReadToEndAsync(UseEncoding).ConfigAwait(); + } + + public static string ReadToEnd(this WebResponse webRes) + { + using var stream = webRes.GetResponseStream(); + return stream.ReadToEnd(UseEncoding); + } + + public static Task ReadToEndAsync(this WebResponse webRes) + { + using var stream = webRes.GetResponseStream(); + return stream.ReadToEndAsync(UseEncoding); + } + + public static IEnumerable ReadLines(this WebResponse webRes) + { + using var stream = webRes.GetResponseStream(); + using var reader = new StreamReader(stream, UseEncoding, true, 1024, leaveOpen: true); + string line; + while ((line = reader.ReadLine()) != null) + { + yield return line; + } + } } //Allow Exceptions to Customize HTTP StatusCode and StatusDescription returned diff --git a/src/ServiceStack.Text/LicenseUtils.cs b/src/ServiceStack.Text/LicenseUtils.cs index 1a97e3e00..2fb47aec7 100644 --- a/src/ServiceStack.Text/LicenseUtils.cs +++ b/src/ServiceStack.Text/LicenseUtils.cs @@ -821,10 +821,8 @@ public static bool VerifyLicenseKeyTextFallback(this string licenseKeyText, out public static bool VerifySha1Data(this System.Security.Cryptography.RSACryptoServiceProvider RSAalg, byte[] unsignedData, byte[] encryptedData) { - using (var sha = new System.Security.Cryptography.SHA1CryptoServiceProvider()) - { - return RSAalg.VerifyData(unsignedData, sha, encryptedData); - } + using var sha = System.Security.Cryptography.SHA1.Create(); + return RSAalg.VerifyData(unsignedData, sha, encryptedData); } } } \ No newline at end of file diff --git a/src/ServiceStack.Text/PclExport.NetCore.cs b/src/ServiceStack.Text/PclExport.NetCore.cs index 14d8f89e4..b12bee3be 100644 --- a/src/ServiceStack.Text/PclExport.NetCore.cs +++ b/src/ServiceStack.Text/PclExport.NetCore.cs @@ -1,4 +1,4 @@ -#if NETCORE && !NETSTANDARD2_0 +#if (NETCORE || NET6_0_OR_GREATER) && !NETSTANDARD2_0 using System; using ServiceStack.Text; diff --git a/src/ServiceStack.Text/PclExport.NetStandard.cs b/src/ServiceStack.Text/PclExport.NetStandard.cs index 976e21fe9..e536fec6f 100644 --- a/src/ServiceStack.Text/PclExport.NetStandard.cs +++ b/src/ServiceStack.Text/PclExport.NetStandard.cs @@ -19,10 +19,9 @@ namespace ServiceStack { public class NetStandardPclExport : PclExport { - public static NetStandardPclExport Provider = new NetStandardPclExport(); + public static NetStandardPclExport Provider = new(); - static string[] allDateTimeFormats = new string[] - { + static string[] allDateTimeFormats = { "yyyy-MM-ddTHH:mm:ss.FFFFFFFzzzzzz", "yyyy-MM-ddTHH:mm:ss.FFFFFFF", "yyyy-MM-ddTHH:mm:ss.FFFFFFFZ", diff --git a/src/ServiceStack.Text/PclExport.cs b/src/ServiceStack.Text/PclExport.cs index c97a8be6b..74926ea04 100644 --- a/src/ServiceStack.Text/PclExport.cs +++ b/src/ServiceStack.Text/PclExport.cs @@ -26,13 +26,13 @@ public static class Platforms public const string Net45 = "Net45"; } - public static PclExport Instance + public static PclExport Instance = #if NETFX - = new Net45PclExport() + new Net45PclExport() #elif NETSTANDARD2_0 - = new NetStandardPclExport() -#elif NETCORE - = new NetCorePclExport() + new NetStandardPclExport() +#elif NETCORE || NET6_0_OR_GREATER + new NetCorePclExport() #endif ; diff --git a/src/ServiceStack.Text/ServiceStack.Text.csproj b/src/ServiceStack.Text/ServiceStack.Text.csproj index df4781b18..23980e788 100644 --- a/src/ServiceStack.Text/ServiceStack.Text.csproj +++ b/src/ServiceStack.Text/ServiceStack.Text.csproj @@ -12,6 +12,9 @@ JSON;Text;Serializer;CSV;JSV;HTTP;Auto Mapping;Dump;Reflection;JS;Utils;Fast 1591 + + $(DefineConstants);NETFX;NET472 + $(DefineConstants);NETCORE;NETSTANDARD;NETSTANDARD2_0 diff --git a/tests/ServiceStack.Text.Tests/HttpUtilsMockTests.cs b/tests/ServiceStack.Text.Tests/HttpUtilsMockTests.cs index 88093707f..9870126e4 100644 --- a/tests/ServiceStack.Text.Tests/HttpUtilsMockTests.cs +++ b/tests/ServiceStack.Text.Tests/HttpUtilsMockTests.cs @@ -1,4 +1,5 @@ -// Copyright (c) ServiceStack, Inc. All Rights Reserved. +#if !NET6_0_OR_GREATER +// Copyright (c) ServiceStack, Inc. All Rights Reserved. // License: https://raw.github.com/ServiceStack/ServiceStack/master/license.txt using System.Collections.Generic; @@ -217,3 +218,4 @@ public async Task Can_Mock_BytesFn_Api_responses_Async() } } +#endif diff --git a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj index 00dadf020..d57516471 100644 --- a/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj +++ b/tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj @@ -49,6 +49,7 @@ + $(DefineConstants);NETCORE;NETSTANDARD2_0 diff --git a/tests/ServiceStack.Text.Tests/UseCases/GithubV3ApiTests.cs b/tests/ServiceStack.Text.Tests/UseCases/GithubV3ApiTests.cs index f59da56ce..a08107de5 100644 --- a/tests/ServiceStack.Text.Tests/UseCases/GithubV3ApiTests.cs +++ b/tests/ServiceStack.Text.Tests/UseCases/GithubV3ApiTests.cs @@ -37,7 +37,7 @@ public class GithubV3ApiGateway public T GetJson(string route, params object[] routeArgs) { return GithubApiBaseUrl.AppendPath(route.Fmt(routeArgs)) - .GetJsonFromUrl(requestFilter:x => x.UserAgent = "ServiceStack.Text.Tests") + .GetJsonFromUrl(requestFilter:x => x.With(userAgent:"ServiceStack.Text.Tests")) .FromJson(); } diff --git a/tests/ServiceStack.Text.Tests/UseCases/StripeGateway.cs b/tests/ServiceStack.Text.Tests/UseCases/StripeGateway.cs index d65035b02..7936f6f01 100644 --- a/tests/ServiceStack.Text.Tests/UseCases/StripeGateway.cs +++ b/tests/ServiceStack.Text.Tests/UseCases/StripeGateway.cs @@ -4,11 +4,15 @@ using System; using System.Collections.Generic; using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; using System.Runtime.Serialization; using System.Text; +using System.Threading; using System.Threading.Tasks; using ServiceStack.Stripe.Types; using ServiceStack.Text; +using ServiceStack; namespace ServiceStack.Stripe { @@ -572,6 +576,8 @@ public class StripeGateway : IRestGateway private string publishableKey; public ICredentials Credentials { get; set; } private string UserAgent { get; set; } + + public HttpClient Client { get; set; } public StripeGateway(string apiKey, string publishableKey = null) { @@ -581,28 +587,13 @@ public StripeGateway(string apiKey, string publishableKey = null) Timeout = TimeSpan.FromSeconds(60); UserAgent = "servicestack .net stripe v1"; Currency = Currencies.UnitedStatesDollar; + Client = new HttpClient(new HttpClientHandler { + Credentials = Credentials, + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, + }, disposeHandler:true); JsConfig.InitStatics(); } - - protected virtual void InitRequest(HttpWebRequest req, string method, string idempotencyKey) - { - req.Accept = MimeTypes.Json; - req.Credentials = Credentials; - - if (method == HttpMethods.Post || method == HttpMethods.Put) - req.ContentType = MimeTypes.FormUrlEncoded; - - if (!string.IsNullOrWhiteSpace(idempotencyKey)) - req.Headers["Idempotency-Key"] = idempotencyKey; - - req.Headers["Stripe-Version"] = APIVersion; - - PclExport.Instance.Config(req, - userAgent: UserAgent, - timeout: Timeout, - preAuthenticate: true); - } - + protected virtual void HandleStripeException(WebException ex) { string errorBody = ex.GetResponseBody(); @@ -618,56 +609,87 @@ protected virtual void HandleStripeException(WebException ex) } } - protected virtual string Send(string relativeUrl, string method, string body, string idempotencyKey) + private HttpRequestMessage PrepareRequest(string relativeUrl, string method, string requestBody, string idempotencyKey) { - try - { - var url = BaseUrl.CombineWith(relativeUrl); - var response = url.SendStringToUrl(method: method, requestBody: body, requestFilter: req => - { - InitRequest(req, method, idempotencyKey); - }); + var url = BaseUrl.CombineWith(relativeUrl); - return response; - } - catch (WebException ex) + var httpReq = new HttpRequestMessage(new HttpMethod(method), url); + + httpReq.Headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent)); + httpReq.Headers.Add(HttpHeaders.Accept, MimeTypes.Json); + + if (!string.IsNullOrWhiteSpace(idempotencyKey)) + httpReq.Headers.Add("Idempotency-Key", idempotencyKey); + + httpReq.Headers.Add("Stripe-Version", APIVersion); + + if (requestBody != null) { - string errorBody = ex.GetResponseBody(); - var errorStatus = ex.GetStatus() ?? HttpStatusCode.BadRequest; + httpReq.Content = new StringContent(requestBody, Encoding.UTF8); + if (method is HttpMethods.Post or HttpMethods.Put) + httpReq.Content!.Headers.ContentType = new MediaTypeHeaderValue(MimeTypes.FormUrlEncoded); + } - if (ex.IsAny400()) + return httpReq; + } + + private Exception CreateException(HttpResponseMessage httpRes) + { +#if NET6_0_OR_GREATER + return new HttpRequestException(httpRes.ReasonPhrase, null, httpRes.StatusCode); +#else + return new HttpRequestException(httpRes.ReasonPhrase); +#endif + } + + protected virtual string Send(string relativeUrl, string method, string requestBody, string idempotencyKey) + { + var httpReq = PrepareRequest(relativeUrl, method, requestBody, idempotencyKey); + +#if NET6_0_OR_GREATER + var httpRes = Client.Send(httpReq); + var responseBody = httpRes.Content.ReadAsStream().ReadToEnd(Encoding.UTF8); +#else + var httpRes = Client.SendAsync(httpReq).GetAwaiter().GetResult(); + var responseBody = httpRes.Content.ReadAsStreamAsync().GetAwaiter().GetResult().ReadToEnd(Encoding.UTF8); +#endif + + if (httpRes.IsSuccessStatusCode) + return responseBody; + + if (httpRes.StatusCode is >= HttpStatusCode.BadRequest and < HttpStatusCode.InternalServerError) + { + var result = responseBody.FromJson(); + throw new StripeException(result.Error) { - var result = errorBody.FromJson(); - throw new StripeException(result.Error) - { - StatusCode = errorStatus - }; - } - - throw; + StatusCode = httpRes.StatusCode + }; } + + httpRes.EnsureSuccessStatusCode(); + throw CreateException(httpRes); // should never reach here } - protected virtual async Task SendAsync(string relativeUrl, string method, string body, string idempotencyKey) + protected virtual async Task SendAsync(string relativeUrl, string method, string requestBody, string idempotencyKey, CancellationToken token=default) { - try + var httpReq = PrepareRequest(relativeUrl, method, requestBody, idempotencyKey); + + var httpRes = await Client.SendAsync(httpReq, token).ConfigAwait(); + var responseBody = await (await httpRes.Content.ReadAsStreamAsync()).ReadToEndAsync(Encoding.UTF8).ConfigAwait(); + if (httpRes.IsSuccessStatusCode) + return responseBody; + + if (httpRes.StatusCode is >= HttpStatusCode.BadRequest and < HttpStatusCode.InternalServerError) { - var url = BaseUrl.CombineWith(relativeUrl); - var response = await url.SendStringToUrlAsync(method: method, requestBody: body, requestFilter: req => + var result = responseBody.FromJson(); + throw new StripeException(result.Error) { - InitRequest(req, method, idempotencyKey); - }); - - return response; + StatusCode = httpRes.StatusCode + }; } - catch (Exception ex) - { - var webEx = ex.UnwrapIfSingleException() as WebException; - if (webEx != null) - HandleStripeException(webEx); - throw; - } + httpRes.EnsureSuccessStatusCode(); + throw CreateException(httpRes); // should never reach here } public class ConfigScope : IDisposable From e586113fec6449161672ff3fd858c6c866719434 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Sat, 29 Jan 2022 00:24:50 +0800 Subject: [PATCH 44/75] Add client overloads for .net6 HttpClient --- src/ServiceStack.Text/HttpUtils.HttpClient.cs | 330 ++++++++++++------ src/ServiceStack.Text/HttpUtils.WebRequest.cs | 25 ++ .../UseCases/GithubV3ApiTests.cs | 5 +- 3 files changed, 257 insertions(+), 103 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs index dba1d5209..8f3db9b9e 100644 --- a/src/ServiceStack.Text/HttpUtils.HttpClient.cs +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -4,10 +4,10 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Http.Headers; -using System.Text; using System.Threading; using System.Threading.Tasks; using ServiceStack.Text; @@ -16,14 +16,34 @@ namespace ServiceStack; public static partial class HttpUtils { - public static Func HandlerFactory { get; set; } = - () => new HttpClientHandler { - UseDefaultCredentials = true, - AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate, - }; + private class HttpClientFactory + { + private readonly Lazy lazyHandler; + internal HttpClientFactory(HttpClientHandler handler) => + lazyHandler = new Lazy(() => handler, LazyThreadSafetyMode.ExecutionAndPublication); + public HttpClient CreateClient() => new(lazyHandler.Value, disposeHandler: false); + } + + // Ok to use HttpClientHandler which now uses SocketsHttpHandler + // https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.cs#L16 + public static HttpClientHandler HttpClientHandler { get; set; } = new() { + UseDefaultCredentials = true, + AutomaticDecompression = DecompressionMethods.Brotli | DecompressionMethods.Deflate | DecompressionMethods.GZip, + }; + + // This was the least desirable end to this sadness https://github.com/dotnet/aspnetcore/issues/28385 + // Requires + // public static IHttpClientFactory ClientFactory { get; set; } = new ServiceCollection() + // .AddHttpClient() + // .Configure(options => + // options.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = HandlerFactory)) + // .BuildServiceProvider().GetRequiredService(); + + // Escape & BYO IHttpClientFactory + private static HttpClientFactory clientFactory = new(HttpClientHandler); + public static Func CreateClient { get; set; } = () => clientFactory.CreateClient(); - public static Func ClientFactory { get; set; } = - url => new HttpClient(HandlerFactory(), disposeHandler:true); + public static HttpClient Create() => CreateClient(); public static string GetJsonFromUrl(this string url, Action? requestFilter = null, Action? responseFilter = null) @@ -67,22 +87,23 @@ public static Task GetCsvFromUrlAsync(this string url, public static string GetStringFromUrl(this string url, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + return SendStringToUrl(url, method:HttpMethods.Get, accept: accept, + requestFilter: requestFilter, responseFilter: responseFilter); } public static Task GetStringFromUrlAsync(this string url, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, - token: token); + return SendStringToUrlAsync(url, method:HttpMethods.Get, accept: accept, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); } public static string PostStringToUrl(this string url, string? requestBody = null, string? contentType = null, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "POST", + return SendStringToUrl(url, method:HttpMethods.Post, requestBody: requestBody, contentType: contentType, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -92,7 +113,7 @@ public static Task PostStringToUrlAsync(this string url, string? request Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, method: "POST", + return SendStringToUrlAsync(url, method:HttpMethods.Post, requestBody: requestBody, contentType: contentType, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -100,7 +121,7 @@ public static Task PostStringToUrlAsync(this string url, string? request public static string PostToUrl(this string url, string? formData = null, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "POST", + return SendStringToUrl(url, method:HttpMethods.Post, contentType: MimeTypes.FormUrlEncoded, requestBody: formData, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -109,7 +130,7 @@ public static Task PostToUrlAsync(this string url, string? formData = nu Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, method: "POST", + return SendStringToUrlAsync(url, method:HttpMethods.Post, contentType: MimeTypes.FormUrlEncoded, requestBody: formData, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -119,7 +140,7 @@ public static string PostToUrl(this string url, object? formData = null, string { string? postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; - return SendStringToUrl(url, method: "POST", + return SendStringToUrl(url, method:HttpMethods.Post, contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -130,7 +151,7 @@ public static Task PostToUrlAsync(this string url, object? formData = nu { string? postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; - return SendStringToUrlAsync(url, method: "POST", + return SendStringToUrlAsync(url, method:HttpMethods.Post, contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -138,7 +159,7 @@ public static Task PostToUrlAsync(this string url, object? formData = nu public static string PostJsonToUrl(this string url, string json, Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "POST", requestBody: json, contentType: MimeTypes.Json, + return SendStringToUrl(url, method:HttpMethods.Post, requestBody: json, contentType: MimeTypes.Json, accept: MimeTypes.Json, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -147,7 +168,7 @@ public static Task PostJsonToUrlAsync(this string url, string json, Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, method: "POST", requestBody: json, contentType: MimeTypes.Json, + return SendStringToUrlAsync(url, method:HttpMethods.Post, requestBody: json, contentType: MimeTypes.Json, accept: MimeTypes.Json, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -155,7 +176,7 @@ public static Task PostJsonToUrlAsync(this string url, string json, public static string PostJsonToUrl(this string url, object data, Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "POST", requestBody: data.ToJson(), contentType: MimeTypes.Json, + return SendStringToUrl(url, method:HttpMethods.Post, requestBody: data.ToJson(), contentType: MimeTypes.Json, accept: MimeTypes.Json, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -164,7 +185,7 @@ public static Task PostJsonToUrlAsync(this string url, object data, Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, method: "POST", requestBody: data.ToJson(), contentType: MimeTypes.Json, + return SendStringToUrlAsync(url, method:HttpMethods.Post, requestBody: data.ToJson(), contentType: MimeTypes.Json, accept: MimeTypes.Json, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -172,7 +193,7 @@ public static Task PostJsonToUrlAsync(this string url, object data, public static string PostXmlToUrl(this string url, string xml, Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "POST", requestBody: xml, contentType: MimeTypes.Xml, accept: MimeTypes.Xml, + return SendStringToUrl(url, method:HttpMethods.Post, requestBody: xml, contentType: MimeTypes.Xml, accept: MimeTypes.Xml, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -180,7 +201,7 @@ public static Task PostXmlToUrlAsync(this string url, string xml, Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, method: "POST", requestBody: xml, contentType: MimeTypes.Xml, + return SendStringToUrlAsync(url, method:HttpMethods.Post, requestBody: xml, contentType: MimeTypes.Xml, accept: MimeTypes.Xml, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -188,7 +209,7 @@ public static Task PostXmlToUrlAsync(this string url, string xml, public static string PostCsvToUrl(this string url, string csv, Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "POST", requestBody: csv, contentType: MimeTypes.Csv, accept: MimeTypes.Csv, + return SendStringToUrl(url, method:HttpMethods.Post, requestBody: csv, contentType: MimeTypes.Csv, accept: MimeTypes.Csv, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -196,7 +217,7 @@ public static Task PostCsvToUrlAsync(this string url, string csv, Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, method: "POST", requestBody: csv, contentType: MimeTypes.Csv, + return SendStringToUrlAsync(url, method:HttpMethods.Post, requestBody: csv, contentType: MimeTypes.Csv, accept: MimeTypes.Csv, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -205,7 +226,7 @@ public static string PutStringToUrl(this string url, string? requestBody = null, string? contentType = null, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "PUT", + return SendStringToUrl(url, method:HttpMethods.Put, requestBody: requestBody, contentType: contentType, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -215,7 +236,7 @@ public static Task PutStringToUrlAsync(this string url, string? requestB Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, method: "PUT", + return SendStringToUrlAsync(url, method:HttpMethods.Put, requestBody: requestBody, contentType: contentType, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -223,7 +244,7 @@ public static Task PutStringToUrlAsync(this string url, string? requestB public static string PutToUrl(this string url, string? formData = null, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "PUT", + return SendStringToUrl(url, method:HttpMethods.Put, contentType: MimeTypes.FormUrlEncoded, requestBody: formData, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -232,7 +253,7 @@ public static Task PutToUrlAsync(this string url, string? formData = nul Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, method: "PUT", + return SendStringToUrlAsync(url, method:HttpMethods.Put, contentType: MimeTypes.FormUrlEncoded, requestBody: formData, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -242,7 +263,7 @@ public static string PutToUrl(this string url, object? formData = null, string a { string? postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; - return SendStringToUrl(url, method: "PUT", + return SendStringToUrl(url, method:HttpMethods.Put, contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -253,7 +274,7 @@ public static Task PutToUrlAsync(this string url, object? formData = nul { string? postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; - return SendStringToUrlAsync(url, method: "PUT", + return SendStringToUrlAsync(url, method:HttpMethods.Put, contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -261,7 +282,7 @@ public static Task PutToUrlAsync(this string url, object? formData = nul public static string PutJsonToUrl(this string url, string json, Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "PUT", requestBody: json, contentType: MimeTypes.Json, + return SendStringToUrl(url, method:HttpMethods.Put, requestBody: json, contentType: MimeTypes.Json, accept: MimeTypes.Json, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -270,7 +291,7 @@ public static Task PutJsonToUrlAsync(this string url, string json, Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, method: "PUT", requestBody: json, contentType: MimeTypes.Json, + return SendStringToUrlAsync(url, method:HttpMethods.Put, requestBody: json, contentType: MimeTypes.Json, accept: MimeTypes.Json, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -278,7 +299,7 @@ public static Task PutJsonToUrlAsync(this string url, string json, public static string PutJsonToUrl(this string url, object data, Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "PUT", requestBody: data.ToJson(), contentType: MimeTypes.Json, + return SendStringToUrl(url, method:HttpMethods.Put, requestBody: data.ToJson(), contentType: MimeTypes.Json, accept: MimeTypes.Json, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -287,7 +308,7 @@ public static Task PutJsonToUrlAsync(this string url, object data, Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, method: "PUT", requestBody: data.ToJson(), contentType: MimeTypes.Json, + return SendStringToUrlAsync(url, method:HttpMethods.Put, requestBody: data.ToJson(), contentType: MimeTypes.Json, accept: MimeTypes.Json, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -295,7 +316,7 @@ public static Task PutJsonToUrlAsync(this string url, object data, public static string PutXmlToUrl(this string url, string xml, Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "PUT", requestBody: xml, contentType: MimeTypes.Xml, accept: MimeTypes.Xml, + return SendStringToUrl(url, method:HttpMethods.Put, requestBody: xml, contentType: MimeTypes.Xml, accept: MimeTypes.Xml, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -303,7 +324,7 @@ public static Task PutXmlToUrlAsync(this string url, string xml, Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, method: "PUT", requestBody: xml, contentType: MimeTypes.Xml, + return SendStringToUrlAsync(url, method:HttpMethods.Put, requestBody: xml, contentType: MimeTypes.Xml, accept: MimeTypes.Xml, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -311,7 +332,7 @@ public static Task PutXmlToUrlAsync(this string url, string xml, public static string PutCsvToUrl(this string url, string csv, Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "PUT", requestBody: csv, contentType: MimeTypes.Csv, accept: MimeTypes.Csv, + return SendStringToUrl(url, method:HttpMethods.Put, requestBody: csv, contentType: MimeTypes.Csv, accept: MimeTypes.Csv, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -319,7 +340,7 @@ public static Task PutCsvToUrlAsync(this string url, string csv, Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, method: "PUT", requestBody: csv, contentType: MimeTypes.Csv, + return SendStringToUrlAsync(url, method:HttpMethods.Put, requestBody: csv, contentType: MimeTypes.Csv, accept: MimeTypes.Csv, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -328,7 +349,7 @@ public static string PatchStringToUrl(this string url, string? requestBody = nul string? contentType = null, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "PATCH", + return SendStringToUrl(url, method:HttpMethods.Patch, requestBody: requestBody, contentType: contentType, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -338,7 +359,7 @@ public static Task PatchStringToUrlAsync(this string url, string? reques Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, method: "PATCH", + return SendStringToUrlAsync(url, method:HttpMethods.Patch, requestBody: requestBody, contentType: contentType, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -346,7 +367,7 @@ public static Task PatchStringToUrlAsync(this string url, string? reques public static string PatchToUrl(this string url, string? formData = null, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "PATCH", + return SendStringToUrl(url, method:HttpMethods.Patch, contentType: MimeTypes.FormUrlEncoded, requestBody: formData, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -355,7 +376,7 @@ public static Task PatchToUrlAsync(this string url, string? formData = n Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, method: "PATCH", + return SendStringToUrlAsync(url, method:HttpMethods.Patch, contentType: MimeTypes.FormUrlEncoded, requestBody: formData, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -365,7 +386,7 @@ public static string PatchToUrl(this string url, object? formData = null, string { string? postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; - return SendStringToUrl(url, method: "PATCH", + return SendStringToUrl(url, method:HttpMethods.Patch, contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -376,7 +397,7 @@ public static Task PatchToUrlAsync(this string url, object? formData = n { string? postFormData = formData != null ? QueryStringSerializer.SerializeToString(formData) : null; - return SendStringToUrlAsync(url, method: "PATCH", + return SendStringToUrlAsync(url, method:HttpMethods.Patch, contentType: MimeTypes.FormUrlEncoded, requestBody: postFormData, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -384,7 +405,7 @@ public static Task PatchToUrlAsync(this string url, object? formData = n public static string PatchJsonToUrl(this string url, string json, Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "PATCH", requestBody: json, contentType: MimeTypes.Json, + return SendStringToUrl(url, method:HttpMethods.Patch, requestBody: json, contentType: MimeTypes.Json, accept: MimeTypes.Json, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -393,7 +414,7 @@ public static Task PatchJsonToUrlAsync(this string url, string json, Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, method: "PATCH", requestBody: json, contentType: MimeTypes.Json, + return SendStringToUrlAsync(url, method:HttpMethods.Patch, requestBody: json, contentType: MimeTypes.Json, accept: MimeTypes.Json, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -401,7 +422,7 @@ public static Task PatchJsonToUrlAsync(this string url, string json, public static string PatchJsonToUrl(this string url, object data, Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "PATCH", requestBody: data.ToJson(), contentType: MimeTypes.Json, + return SendStringToUrl(url, method:HttpMethods.Patch, requestBody: data.ToJson(), contentType: MimeTypes.Json, accept: MimeTypes.Json, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -410,7 +431,7 @@ public static Task PatchJsonToUrlAsync(this string url, object data, Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, method: "PATCH", requestBody: data.ToJson(), contentType: MimeTypes.Json, + return SendStringToUrlAsync(url, method:HttpMethods.Patch, requestBody: data.ToJson(), contentType: MimeTypes.Json, accept: MimeTypes.Json, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -418,7 +439,7 @@ public static Task PatchJsonToUrlAsync(this string url, object data, public static string DeleteFromUrl(this string url, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "DELETE", accept: accept, requestFilter: requestFilter, + return SendStringToUrl(url, method:HttpMethods.Delete, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -426,14 +447,14 @@ public static Task DeleteFromUrlAsync(this string url, string accept = " Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, method: "DELETE", accept: accept, requestFilter: requestFilter, + return SendStringToUrlAsync(url, method:HttpMethods.Delete, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } public static string OptionsFromUrl(this string url, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "OPTIONS", accept: accept, requestFilter: requestFilter, + return SendStringToUrl(url, method:HttpMethods.Options, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -441,14 +462,14 @@ public static Task OptionsFromUrlAsync(this string url, string accept = Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, method: "OPTIONS", accept: accept, requestFilter: requestFilter, + return SendStringToUrlAsync(url, method:HttpMethods.Options, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } public static string HeadFromUrl(this string url, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "HEAD", accept: accept, requestFilter: requestFilter, + return SendStringToUrl(url, method:HttpMethods.Head, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -456,15 +477,21 @@ public static Task HeadFromUrlAsync(this string url, string accept = "*/ Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStringToUrlAsync(url, method: "HEAD", accept: accept, requestFilter: requestFilter, + return SendStringToUrlAsync(url, method:HttpMethods.Head, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } - + public static string SendStringToUrl(this string url, string method = HttpMethods.Post, string? requestBody = null, string? contentType = null, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - var client = ClientFactory(url); + return Create().SendStringToUrl(url, method, requestBody, contentType, accept, requestFilter, responseFilter); + } + + public static string SendStringToUrl(this HttpClient client, string url, string method = HttpMethods.Post, + string? requestBody = null, string? contentType = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { var httpReq = new HttpRequestMessage(new HttpMethod(method), url); httpReq.Headers.Add(HttpHeaders.Accept, accept); @@ -482,12 +509,20 @@ public static string SendStringToUrl(this string url, string method = HttpMethod return httpRes.Content.ReadAsStream().ReadToEnd(UseEncoding); } - public static async Task SendStringToUrlAsync(this string url, string method = HttpMethods.Post, + public static Task SendStringToUrlAsync(this string url, + string method = HttpMethods.Post, string? requestBody = null, + string? contentType = null, string accept = "*/*", Action? requestFilter = null, + Action? responseFilter = null, CancellationToken token = default) + { + return Create().SendStringToUrlAsync(url, method, requestBody, contentType, accept, + requestFilter, responseFilter, token); + } + + public static async Task SendStringToUrlAsync(this HttpClient client, string url, string method = HttpMethods.Post, string? requestBody = null, string? contentType = null, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - var client = ClientFactory(url); var httpReq = new HttpRequestMessage(new HttpMethod(method), url); httpReq.Headers.Add(HttpHeaders.Accept, accept); @@ -523,7 +558,7 @@ public static byte[] PostBytesToUrl(this string url, byte[]? requestBody = null, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - return SendBytesToUrl(url, method: "POST", + return SendBytesToUrl(url, method:HttpMethods.Post, contentType: contentType, requestBody: requestBody, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -533,7 +568,7 @@ public static Task PostBytesToUrlAsync(this string url, byte[]? requestB Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendBytesToUrlAsync(url, method: "POST", + return SendBytesToUrlAsync(url, method:HttpMethods.Post, contentType: contentType, requestBody: requestBody, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -542,7 +577,7 @@ public static byte[] PutBytesToUrl(this string url, byte[]? requestBody = null, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - return SendBytesToUrl(url, method: "PUT", + return SendBytesToUrl(url, method:HttpMethods.Put, contentType: contentType, requestBody: requestBody, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -552,7 +587,7 @@ public static Task PutBytesToUrlAsync(this string url, byte[]? requestBo Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendBytesToUrlAsync(url, method: "PUT", + return SendBytesToUrlAsync(url, method:HttpMethods.Put, contentType: contentType, requestBody: requestBody, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -561,7 +596,14 @@ public static byte[] SendBytesToUrl(this string url, string method = HttpMethods byte[]? requestBody = null, string? contentType = null, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - var client = ClientFactory(url); + return Create().SendBytesToUrl(url, method, requestBody, contentType, accept, + requestFilter, responseFilter); + } + + public static byte[] SendBytesToUrl(this HttpClient client, string url, string method = HttpMethods.Post, + byte[]? requestBody = null, string? contentType = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { var httpReq = new HttpRequestMessage(new HttpMethod(method), url); httpReq.Headers.Add(HttpHeaders.Accept, accept); @@ -579,12 +621,20 @@ public static byte[] SendBytesToUrl(this string url, string method = HttpMethods return httpRes.Content.ReadAsStream().ReadFully(); } - public static async Task SendBytesToUrlAsync(this string url, string method = HttpMethods.Post, + public static Task SendBytesToUrlAsync(this string url, string method = HttpMethods.Post, + byte[]? requestBody = null, string? contentType = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return Create().SendBytesToUrlAsync(url, method, requestBody, contentType, accept, + requestFilter, responseFilter, token); + } + + public static async Task SendBytesToUrlAsync(this HttpClient client, string url, string method = HttpMethods.Post, byte[]? requestBody = null, string? contentType = null, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - var client = ClientFactory(url); var httpReq = new HttpRequestMessage(new HttpMethod(method), url); httpReq.Headers.Add(HttpHeaders.Accept, accept); @@ -620,7 +670,7 @@ public static Stream PostStreamToUrl(this string url, Stream? requestBody = null string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - return SendStreamToUrl(url, method: "POST", + return SendStreamToUrl(url, method:HttpMethods.Post, contentType: contentType, requestBody: requestBody, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -630,7 +680,7 @@ public static Task PostStreamToUrlAsync(this string url, Stream? request Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStreamToUrlAsync(url, method: "POST", + return SendStreamToUrlAsync(url, method:HttpMethods.Post, contentType: contentType, requestBody: requestBody, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -639,7 +689,7 @@ public static Stream PutStreamToUrl(this string url, Stream? requestBody = null, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - return SendStreamToUrl(url, method: "PUT", + return SendStreamToUrl(url, method:HttpMethods.Put, contentType: contentType, requestBody: requestBody, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -649,19 +699,23 @@ public static Task PutStreamToUrlAsync(this string url, Stream? requestB Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return SendStreamToUrlAsync(url, method: "PUT", + return SendStreamToUrlAsync(url, method:HttpMethods.Put, contentType: contentType, requestBody: requestBody, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } - /// - /// Returns HttpWebResponse Stream which must be disposed - /// public static Stream SendStreamToUrl(this string url, string method = HttpMethods.Post, Stream? requestBody = null, string? contentType = null, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - var client = ClientFactory(url); + return Create().SendStreamToUrl(url, method, requestBody, contentType, accept, + requestFilter, responseFilter); + } + + public static Stream SendStreamToUrl(this HttpClient client, string url, string method = HttpMethods.Post, + Stream? requestBody = null, string? contentType = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null) + { var httpReq = new HttpRequestMessage(new HttpMethod(method), url); httpReq.Headers.Add(HttpHeaders.Accept, accept); @@ -679,15 +733,20 @@ public static Stream SendStreamToUrl(this string url, string method = HttpMethod return httpRes.Content.ReadAsStream(); } - /// - /// Returns HttpWebResponse Stream which must be disposed - /// - public static async Task SendStreamToUrlAsync(this string url, string method = HttpMethods.Post, + public static Task SendStreamToUrlAsync(this string url, string method = HttpMethods.Post, + Stream? requestBody = null, string? contentType = null, string accept = "*/*", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return Create().SendStreamToUrlAsync(url, method, requestBody, contentType, accept, + requestFilter, responseFilter, token); + } + + public static async Task SendStreamToUrlAsync(this HttpClient client, string url, string method = HttpMethods.Post, Stream? requestBody = null, string? contentType = null, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - var client = ClientFactory(url); var httpReq = new HttpRequestMessage(new HttpMethod(method), url); httpReq.Headers.Add(HttpHeaders.Accept, accept); @@ -709,7 +768,7 @@ public static async Task SendStreamToUrlAsync(this string url, string me { try { - var client = ClientFactory(url); + var client = Create(); var httpReq = new HttpRequestMessage(new HttpMethod(HttpMethods.Get), url); httpReq.Headers.Add(HttpHeaders.Accept, "*/*"); var httpRes = client.Send(httpReq); @@ -723,7 +782,7 @@ public static async Task SendStreamToUrlAsync(this string url, string me public static HttpResponseMessage? GetErrorResponse(this string url) { - var client = ClientFactory(url); + var client = Create(); var httpReq = new HttpRequestMessage(new HttpMethod(HttpMethods.Get), url); httpReq.Headers.Add(HttpHeaders.Accept, "*/*"); var httpRes = client.Send(httpReq); @@ -734,7 +793,7 @@ public static async Task SendStreamToUrlAsync(this string url, string me public static async Task GetErrorResponseAsync(this string url, CancellationToken token=default) { - var client = ClientFactory(url); + var client = Create(); var httpReq = new HttpRequestMessage(new HttpMethod(HttpMethods.Get), url); httpReq.Headers.Add(HttpHeaders.Accept, "*/*"); var httpRes = await client.SendAsync(httpReq, token).ConfigAwait(); @@ -766,8 +825,17 @@ public static IEnumerable ReadLines(this HttpResponseMessage webRes) } } - public static HttpResponseMessage UploadFile(this HttpRequestMessage httpReq, Stream fileStream, string fileName, - string? mimeType = null, string accept = "*/*", string method = HttpMethods.Post, string field = "file", + public static HttpResponseMessage UploadFile(this HttpRequestMessage httpReq, Stream fileStream, + string fileName, string? mimeType = null, string accept = "*/*", string method = HttpMethods.Post, string field = "file", + Action? requestFilter = null, Action? responseFilter = null) + { + return Create().UploadFile(httpReq, fileStream, fileName, mimeType, accept, method, field, + requestFilter, responseFilter); + } + + + public static HttpResponseMessage UploadFile(this HttpClient client, HttpRequestMessage httpReq, Stream fileStream, + string fileName, string? mimeType = null, string accept = "*/*", string method = HttpMethods.Post, string field = "file", Action? requestFilter = null, Action? responseFilter = null) { if (httpReq.RequestUri == null) @@ -788,14 +856,24 @@ public static HttpResponseMessage UploadFile(this HttpRequestMessage httpReq, St fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(mimeType ?? MimeTypes.GetMimeType(fileName)); content.Add(fileContent, "file", fileName); - var client = ClientFactory(httpReq.RequestUri!.ToString()); var httpRes = client.Send(httpReq); responseFilter?.Invoke(httpRes); httpRes.EnsureSuccessStatusCode(); return httpRes; } - public static async Task UploadFileAsync(this HttpRequestMessage httpReq, Stream fileStream, string fileName, + public static Task UploadFileAsync(this HttpRequestMessage httpReq, Stream fileStream, + string fileName, + string? mimeType = null, string accept = "*/*", string method = HttpMethods.Post, string field = "file", + Action? requestFilter = null, Action? responseFilter = null, + CancellationToken token = default) + { + return Create().UploadFileAsync(httpReq, fileStream, fileName, mimeType, accept, method, field, + requestFilter, responseFilter, token); + } + + public static async Task UploadFileAsync(this HttpClient client, + HttpRequestMessage httpReq, Stream fileStream, string fileName, string? mimeType = null, string accept = "*/*", string method = HttpMethods.Post, string field = "file", Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) @@ -818,7 +896,6 @@ public static async Task UploadFileAsync(this HttpRequestMe fileContent.Headers.ContentType = MediaTypeHeaderValue.Parse(mimeType ?? MimeTypes.GetMimeType(fileName)); content.Add(fileContent, "file", fileName); - var client = ClientFactory(httpReq.RequestUri!.ToString()); var httpRes = await client.SendAsync(httpReq, token).ConfigAwait(); responseFilter?.Invoke(httpRes); httpRes.EnsureSuccessStatusCode(); @@ -851,7 +928,7 @@ public static async Task UploadFileAsync(this HttpRequestMessage webRequest, Str public static string PostXmlToUrl(this string url, object data, Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "POST", requestBody: data.ToXml(), contentType: MimeTypes.Xml, + return SendStringToUrl(url, method:HttpMethods.Post, requestBody: data.ToXml(), contentType: MimeTypes.Xml, accept: MimeTypes.Xml, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -859,7 +936,7 @@ public static string PostXmlToUrl(this string url, object data, public static string PostCsvToUrl(this string url, object data, Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "POST", requestBody: data.ToCsv(), contentType: MimeTypes.Csv, + return SendStringToUrl(url, method:HttpMethods.Post, requestBody: data.ToCsv(), contentType: MimeTypes.Csv, accept: MimeTypes.Csv, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -867,7 +944,7 @@ public static string PostCsvToUrl(this string url, object data, public static string PutXmlToUrl(this string url, object data, Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "PUT", requestBody: data.ToXml(), contentType: MimeTypes.Xml, + return SendStringToUrl(url, method:HttpMethods.Put, requestBody: data.ToXml(), contentType: MimeTypes.Xml, accept: MimeTypes.Xml, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -875,7 +952,7 @@ public static string PutXmlToUrl(this string url, object data, public static string PutCsvToUrl(this string url, object data, Action? requestFilter = null, Action? responseFilter = null) { - return SendStringToUrl(url, method: "PUT", requestBody: data.ToCsv(), contentType: MimeTypes.Csv, + return SendStringToUrl(url, method:HttpMethods.Put, requestBody: data.ToCsv(), contentType: MimeTypes.Csv, accept: MimeTypes.Csv, requestFilter: requestFilter, responseFilter: responseFilter); } @@ -930,28 +1007,79 @@ public static async Task PutFileToUrlAsync(this string url, method: HttpMethods.Post, requestFilter: requestFilter, responseFilter: responseFilter, token: token).ConfigAwait(); } + public static string? GetHeader(this HttpRequestMessage res, string name) => + res.Headers.TryGetValues(name, out var values) ? values.FirstOrDefault() : null; + + public static string? GetHeader(this HttpResponseMessage res, string name) => + res.Headers.TryGetValues(name, out var values) ? values.FirstOrDefault() : null; + + public static HttpRequestMessage WithHeader(this HttpRequestMessage httpReq, string name, string value) + { + if (name.Equals(HttpHeaders.Authorization, StringComparison.OrdinalIgnoreCase)) + { + httpReq.Headers.Authorization = + new AuthenticationHeaderValue(value.LeftPart(' '), value.RightPart(' ')); + } + else if (name.Equals(HttpHeaders.UserAgent)) + { + if (value.IndexOf('/') >= 0) + { + var product = value.LastLeftPart('/'); + var version = value.LastRightPart('/'); + httpReq.Headers.UserAgent.Add(new ProductInfoHeaderValue(product, version)); + } + else + { + // Avoid format excetions + var commentFmt = value[0] == '(' && value[^1] == ')' ? value : $"({value})"; + httpReq.Headers.UserAgent.Add(new ProductInfoHeaderValue(commentFmt)); + } + } + else + { + httpReq.Headers.Add(name, value); + } + return httpReq; + } + public static HttpRequestMessage With(this HttpRequestMessage httpReq, string? accept = null, string? userAgent = null, - Dictionary? headers = null) + KeyValuePair? authorization = null, + Dictionary? headers = null) { + headers ??= new Dictionary(); + if (accept != null) - httpReq.Headers.Add(HttpHeaders.Accept, accept); - + headers[HttpHeaders.Accept] = accept; if (userAgent != null) - httpReq.Headers.UserAgent.Add(new ProductInfoHeaderValue(userAgent)); + headers[HttpHeaders.UserAgent] = userAgent; + if (authorization != null) + httpReq.Headers.Authorization = + new AuthenticationHeaderValue(authorization.Value.Key, authorization.Value.Value); - if (headers != null) + foreach (var entry in headers) { - foreach (var entry in headers) - { - httpReq.Headers.Add(entry.Key, entry.Value); - } + httpReq.WithHeader(entry.Key, entry.Value); } return httpReq; } - + + public static void DownloadFileTo(this string downloadUrl, string fileName, + Dictionary? headers = null) + { + var client = Create(); + var httpReq = new HttpRequestMessage(HttpMethod.Get, downloadUrl) + .With(accept:"*/*", headers:headers); + + var httpRes = client.Send(httpReq); + httpRes.EnsureSuccessStatusCode(); + + using var fs = new FileStream(fileName, FileMode.CreateNew); + var bytes = httpRes.Content.ReadAsStream().ReadFully(); + fs.Write(bytes); + } } #endif diff --git a/src/ServiceStack.Text/HttpUtils.WebRequest.cs b/src/ServiceStack.Text/HttpUtils.WebRequest.cs index d368f4f60..eedfd66fa 100644 --- a/src/ServiceStack.Text/HttpUtils.WebRequest.cs +++ b/src/ServiceStack.Text/HttpUtils.WebRequest.cs @@ -1073,9 +1073,29 @@ private static byte[] GetHeaderBytes(string fileName, string mimeType, string fi return headerBytes; } + public static string GetHeader(this HttpWebRequest res, string name) => + res.Headers.Get(name); + public static string GetHeader(this HttpWebResponse res, string name) => + res.Headers.Get(name); + + public static void DownloadFileTo(this string downloadUrl, string fileName, + Dictionary headers = null) + { + var webClient = new WebClient(); + if (headers != null) + { + foreach (var entry in headers) + { + webClient.Headers[entry.Key] = entry.Value; + } + } + webClient.DownloadFile(downloadUrl, fileName); + } + public static HttpWebRequest With(this HttpWebRequest httpReq, string accept = null, string userAgent = null, + KeyValuePair? authorization = null, Dictionary headers = null) { if (accept != null) @@ -1084,6 +1104,11 @@ public static HttpWebRequest With(this HttpWebRequest httpReq, if (userAgent != null) httpReq.UserAgent = userAgent; + if (authorization != null) + { + httpReq.Headers[HttpHeaders.Authorization] = authorization.Value.Key + " " + authorization.Value.Value; + } + if (headers != null) { foreach (var entry in headers) diff --git a/tests/ServiceStack.Text.Tests/UseCases/GithubV3ApiTests.cs b/tests/ServiceStack.Text.Tests/UseCases/GithubV3ApiTests.cs index a08107de5..0c891ca67 100644 --- a/tests/ServiceStack.Text.Tests/UseCases/GithubV3ApiTests.cs +++ b/tests/ServiceStack.Text.Tests/UseCases/GithubV3ApiTests.cs @@ -36,8 +36,9 @@ public class GithubV3ApiGateway public T GetJson(string route, params object[] routeArgs) { - return GithubApiBaseUrl.AppendPath(route.Fmt(routeArgs)) - .GetJsonFromUrl(requestFilter:x => x.With(userAgent:"ServiceStack.Text.Tests")) + var url = GithubApiBaseUrl.AppendUrlPathsRaw(route.Fmt(routeArgs)); + return url + .GetJsonFromUrl(requestFilter:x => x.With(userAgent:$"ServiceStack/{Env.VersionString}")) .FromJson(); } From bd47922689bb6cd29b7004782e12b4ef50e4726c Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Sat, 29 Jan 2022 10:10:58 +0800 Subject: [PATCH 45/75] Replace With to use more flexible + binary resilient HttpRequestConfig --- src/ServiceStack.Text/HttpRequestConfig.cs | 27 ++++++ src/ServiceStack.Text/HttpStatus.cs | 90 +++++++++++++++++++ src/ServiceStack.Text/HttpUtils.HttpClient.cs | 48 ++++++---- src/ServiceStack.Text/HttpUtils.WebRequest.cs | 51 ++++++----- .../UseCases/GithubV3ApiTests.cs | 2 +- 5 files changed, 179 insertions(+), 39 deletions(-) create mode 100644 src/ServiceStack.Text/HttpRequestConfig.cs create mode 100644 src/ServiceStack.Text/HttpStatus.cs diff --git a/src/ServiceStack.Text/HttpRequestConfig.cs b/src/ServiceStack.Text/HttpRequestConfig.cs new file mode 100644 index 000000000..5195519b9 --- /dev/null +++ b/src/ServiceStack.Text/HttpRequestConfig.cs @@ -0,0 +1,27 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Text; + +namespace ServiceStack; + +public class HttpRequestConfig +{ + public string? Accept { get; set; } + public string? UserAgent { get; set; } + public string? ContentType { get; set; } + public KeyValuePair? Authorization { get; set; } + public Dictionary? Headers { get; set; } + + public string AuthBearer + { + set => Authorization = new("Bearer", value); + } + + public KeyValuePair AuthBasic + { + set => Authorization = new("Basic", + Convert.ToBase64String(Encoding.UTF8.GetBytes(value.Key + ":" + value.Value))); + } +} diff --git a/src/ServiceStack.Text/HttpStatus.cs b/src/ServiceStack.Text/HttpStatus.cs new file mode 100644 index 000000000..a60925b96 --- /dev/null +++ b/src/ServiceStack.Text/HttpStatus.cs @@ -0,0 +1,90 @@ +namespace ServiceStack.Text; + +public static class HttpStatus +{ + public static string GetStatusDescription(int statusCode) + { + if (statusCode is >= 100 and < 600) + { + int i = statusCode / 100; + int j = statusCode % 100; + + if (j < Descriptions[i].Length) + return Descriptions[i][j]; + } + + return string.Empty; + } + + private static readonly string[][] Descriptions = + { + null, + new[] + { + /* 100 */ "Continue", + /* 101 */ "Switching Protocols", + /* 102 */ "Processing" + }, + new[] + { + /* 200 */ "OK", + /* 201 */ "Created", + /* 202 */ "Accepted", + /* 203 */ "Non-Authoritative Information", + /* 204 */ "No Content", + /* 205 */ "Reset Content", + /* 206 */ "Partial Content", + /* 207 */ "Multi-Status" + }, + new[] + { + /* 300 */ "Multiple Choices", + /* 301 */ "Moved Permanently", + /* 302 */ "Found", + /* 303 */ "See Other", + /* 304 */ "Not Modified", + /* 305 */ "Use Proxy", + /* 306 */ string.Empty, + /* 307 */ "Temporary Redirect" + }, + new[] + { + /* 400 */ "Bad Request", + /* 401 */ "Unauthorized", + /* 402 */ "Payment Required", + /* 403 */ "Forbidden", + /* 404 */ "Not Found", + /* 405 */ "Method Not Allowed", + /* 406 */ "Not Acceptable", + /* 407 */ "Proxy Authentication Required", + /* 408 */ "Request Timeout", + /* 409 */ "Conflict", + /* 410 */ "Gone", + /* 411 */ "Length Required", + /* 412 */ "Precondition Failed", + /* 413 */ "Request Entity Too Large", + /* 414 */ "Request-Uri Too Long", + /* 415 */ "Unsupported Media Type", + /* 416 */ "Requested Range Not Satisfiable", + /* 417 */ "Expectation Failed", + /* 418 */ string.Empty, + /* 419 */ string.Empty, + /* 420 */ string.Empty, + /* 421 */ string.Empty, + /* 422 */ "Unprocessable Entity", + /* 423 */ "Locked", + /* 424 */ "Failed Dependency" + }, + new[] + { + /* 500 */ "Internal Server Error", + /* 501 */ "Not Implemented", + /* 502 */ "Bad Gateway", + /* 503 */ "Service Unavailable", + /* 504 */ "Gateway Timeout", + /* 505 */ "Http Version Not Supported", + /* 506 */ string.Empty, + /* 507 */ "Insufficient Storage" + } + }; +} \ No newline at end of file diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs index 8f3db9b9e..7038f276f 100644 --- a/src/ServiceStack.Text/HttpUtils.HttpClient.cs +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -1007,6 +1007,9 @@ public static async Task PutFileToUrlAsync(this string url, method: HttpMethods.Post, requestFilter: requestFilter, responseFilter: responseFilter, token: token).ConfigAwait(); } + public static void AddHeader(this HttpRequestMessage res, string name, string value) => + res.WithHeader(name, value); + public static string? GetHeader(this HttpRequestMessage res, string name) => res.Headers.TryGetValues(name, out var values) ? values.FirstOrDefault() : null; @@ -1020,7 +1023,13 @@ public static HttpRequestMessage WithHeader(this HttpRequestMessage httpReq, str httpReq.Headers.Authorization = new AuthenticationHeaderValue(value.LeftPart(' '), value.RightPart(' ')); } - else if (name.Equals(HttpHeaders.UserAgent)) + else if (name.Equals(HttpHeaders.ContentType, StringComparison.OrdinalIgnoreCase)) + { + if (httpReq.Content == null) + throw new NotSupportedException("Can't set ContentType before Content is populated"); + httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue(value); + } + else if (name.Equals(HttpHeaders.UserAgent, StringComparison.OrdinalIgnoreCase)) { if (value.IndexOf('/') >= 0) { @@ -1030,7 +1039,7 @@ public static HttpRequestMessage WithHeader(this HttpRequestMessage httpReq, str } else { - // Avoid format excetions + // Avoid format exceptions var commentFmt = value[0] == '(' && value[^1] == ')' ? value : $"({value})"; httpReq.Headers.UserAgent.Add(new ProductInfoHeaderValue(commentFmt)); } @@ -1042,22 +1051,24 @@ public static HttpRequestMessage WithHeader(this HttpRequestMessage httpReq, str return httpReq; } - public static HttpRequestMessage With(this HttpRequestMessage httpReq, - string? accept = null, - string? userAgent = null, - KeyValuePair? authorization = null, - Dictionary? headers = null) + public static HttpRequestMessage With(this HttpRequestMessage httpReq, Action configure) { - headers ??= new Dictionary(); - - if (accept != null) - headers[HttpHeaders.Accept] = accept; - if (userAgent != null) - headers[HttpHeaders.UserAgent] = userAgent; - if (authorization != null) + var config = new HttpRequestConfig(); + configure(config); + + config.Headers ??= new Dictionary(); + var headers = config.Headers; + + if (config.Accept != null) + headers[HttpHeaders.Accept] = config.Accept; + if (config.UserAgent != null) + headers[HttpHeaders.UserAgent] = config.UserAgent; + if (config.ContentType != null) + headers[HttpHeaders.ContentType] = config.ContentType; + if (config.Authorization != null) httpReq.Headers.Authorization = - new AuthenticationHeaderValue(authorization.Value.Key, authorization.Value.Value); - + new AuthenticationHeaderValue(config.Authorization.Value.Key, config.Authorization.Value.Value); + foreach (var entry in headers) { httpReq.WithHeader(entry.Key, entry.Value); @@ -1071,7 +1082,10 @@ public static void DownloadFileTo(this string downloadUrl, string fileName, { var client = Create(); var httpReq = new HttpRequestMessage(HttpMethod.Get, downloadUrl) - .With(accept:"*/*", headers:headers); + .With(c => { + c.Accept = "*/*"; + c.Headers = headers; + }); var httpRes = client.Send(httpReq); httpRes.EnsureSuccessStatusCode(); diff --git a/src/ServiceStack.Text/HttpUtils.WebRequest.cs b/src/ServiceStack.Text/HttpUtils.WebRequest.cs index eedfd66fa..41685d848 100644 --- a/src/ServiceStack.Text/HttpUtils.WebRequest.cs +++ b/src/ServiceStack.Text/HttpUtils.WebRequest.cs @@ -1073,11 +1073,6 @@ private static byte[] GetHeaderBytes(string fileName, string mimeType, string fi return headerBytes; } - public static string GetHeader(this HttpWebRequest res, string name) => - res.Headers.Get(name); - public static string GetHeader(this HttpWebResponse res, string name) => - res.Headers.Get(name); - public static void DownloadFileTo(this string downloadUrl, string fileName, Dictionary headers = null) { @@ -1091,27 +1086,41 @@ public static void DownloadFileTo(this string downloadUrl, string fileName, } webClient.DownloadFile(downloadUrl, fileName); } - - public static HttpWebRequest With(this HttpWebRequest httpReq, - string accept = null, - string userAgent = null, - KeyValuePair? authorization = null, - Dictionary headers = null) + + public static void AddHeader(this HttpWebRequest res, string name, string value) => + res.Headers[name] = value; + public static string GetHeader(this HttpWebRequest res, string name) => + res.Headers.Get(name); + public static string GetHeader(this HttpWebResponse res, string name) => + res.Headers.Get(name); + + public static HttpWebRequest WithHeader(this HttpWebRequest httpReq, string name, string value) { - if (accept != null) - httpReq.Headers.Add(HttpHeaders.Accept, accept); + httpReq.Headers[name] = value; + return httpReq; + } + + public static HttpWebRequest With(this HttpWebRequest httpReq, Action configure) + { + var config = new HttpRequestConfig(); + configure(config); + + if (config.Accept != null) + httpReq.Headers.Add(HttpHeaders.Accept, config.Accept); - if (userAgent != null) - httpReq.UserAgent = userAgent; + if (config.UserAgent != null) + httpReq.UserAgent = config.UserAgent; - if (authorization != null) - { - httpReq.Headers[HttpHeaders.Authorization] = authorization.Value.Key + " " + authorization.Value.Value; - } + if (config.ContentType != null) + httpReq.ContentType = config.ContentType; - if (headers != null) + if (config.Authorization != null) + httpReq.Headers[HttpHeaders.Authorization] = + config.Authorization.Value.Key + " " + config.Authorization.Value.Value; + + if (config.Headers != null) { - foreach (var entry in headers) + foreach (var entry in config.Headers) { httpReq.Headers[entry.Key] = entry.Value; } diff --git a/tests/ServiceStack.Text.Tests/UseCases/GithubV3ApiTests.cs b/tests/ServiceStack.Text.Tests/UseCases/GithubV3ApiTests.cs index 0c891ca67..5df6bc232 100644 --- a/tests/ServiceStack.Text.Tests/UseCases/GithubV3ApiTests.cs +++ b/tests/ServiceStack.Text.Tests/UseCases/GithubV3ApiTests.cs @@ -38,7 +38,7 @@ public T GetJson(string route, params object[] routeArgs) { var url = GithubApiBaseUrl.AppendUrlPathsRaw(route.Fmt(routeArgs)); return url - .GetJsonFromUrl(requestFilter:x => x.With(userAgent:$"ServiceStack/{Env.VersionString}")) + .GetJsonFromUrl(requestFilter:x => x.With(c => c.UserAgent = $"ServiceStack/{Env.VersionString}")) .FromJson(); } From e89af4c45d8d05722dd1703b8cce7fab96402f81 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 31 Jan 2022 02:23:41 +0800 Subject: [PATCH 46/75] bump to v6.0.2 --- src/Directory.Build.props | 2 +- src/ServiceStack.Text/Env.cs | 2 +- tests/Directory.Build.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a118be095..6bfa3e383 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - 6.0.0 + 6.0.2 ServiceStack ServiceStack, Inc. © 2008-2022 ServiceStack, Inc diff --git a/src/ServiceStack.Text/Env.cs b/src/ServiceStack.Text/Env.cs index 257d5db63..3cc38fea5 100644 --- a/src/ServiceStack.Text/Env.cs +++ b/src/ServiceStack.Text/Env.cs @@ -114,7 +114,7 @@ static Env() public static string VersionString { get; set; } - public static decimal ServiceStackVersion = 6.01m; + public static decimal ServiceStackVersion = 6.02m; public static bool IsLinux { get; set; } diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 187fbd040..046281905 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -1,7 +1,7 @@ - 6.0.0 + 6.0.2 latest false From ccb18764ef55acacd6cd63c7e0238456fce3a1ce Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 31 Jan 2022 03:06:47 +0800 Subject: [PATCH 47/75] Update HttpUtils.HttpClient.cs --- src/ServiceStack.Text/HttpUtils.HttpClient.cs | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs index 7038f276f..e986414c7 100644 --- a/src/ServiceStack.Text/HttpUtils.HttpClient.cs +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -1012,9 +1012,28 @@ public static void AddHeader(this HttpRequestMessage res, string name, string va public static string? GetHeader(this HttpRequestMessage res, string name) => res.Headers.TryGetValues(name, out var values) ? values.FirstOrDefault() : null; - - public static string? GetHeader(this HttpResponseMessage res, string name) => - res.Headers.TryGetValues(name, out var values) ? values.FirstOrDefault() : null; + + public static Dictionary> HeadersResolver { get; set; } = new(StringComparer.OrdinalIgnoreCase) + { + [HttpHeaders.ContentType] = res => res.Content.Headers.ContentType?.MediaType, + [HttpHeaders.Expires] = res => res.Content.Headers.Expires?.ToString(), + [HttpHeaders.ContentDisposition] = res => res.Content.Headers.ContentDisposition?.ToString(), + [HttpHeaders.ContentEncoding] = res => res.Content.Headers.ContentEncoding?.ToString(), + [HttpHeaders.ContentLength] = res => res.Content.Headers.ContentLength?.ToString(), + [HttpHeaders.ETag] = res => res.Headers.ETag?.Tag.ToString(), + [HttpHeaders.Vary] = res => string.Join(',', res.Headers.Vary), + [HttpHeaders.CacheControl] = res => res.Headers.CacheControl?.ToString(), + }; + + public static string? GetHeader(this HttpResponseMessage res, string name) + { + if (HeadersResolver.TryGetValue(name, out var fn)) + return fn(res); + + return res.Headers.TryGetValues(name, out var values) + ? values.FirstOrDefault() + : null; + } public static HttpRequestMessage WithHeader(this HttpRequestMessage httpReq, string name, string value) { From b8ac3c48f09e2739d09530e437cf472b18d97c71 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 31 Jan 2022 10:26:20 +0800 Subject: [PATCH 48/75] Improve GetHeaders + Add MatchesContentType ext --- src/ServiceStack.Text/HttpUtils.HttpClient.cs | 47 +++++++++++-------- src/ServiceStack.Text/HttpUtils.WebRequest.cs | 6 +++ .../UseCases/ServiceStack_Text_UseCase.cs | 19 ++++---- 3 files changed, 45 insertions(+), 27 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs index e986414c7..353d08c46 100644 --- a/src/ServiceStack.Text/HttpUtils.HttpClient.cs +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -1010,29 +1010,32 @@ public static async Task PutFileToUrlAsync(this string url, public static void AddHeader(this HttpRequestMessage res, string name, string value) => res.WithHeader(name, value); - public static string? GetHeader(this HttpRequestMessage res, string name) => - res.Headers.TryGetValues(name, out var values) ? values.FirstOrDefault() : null; - - public static Dictionary> HeadersResolver { get; set; } = new(StringComparer.OrdinalIgnoreCase) - { - [HttpHeaders.ContentType] = res => res.Content.Headers.ContentType?.MediaType, - [HttpHeaders.Expires] = res => res.Content.Headers.Expires?.ToString(), - [HttpHeaders.ContentDisposition] = res => res.Content.Headers.ContentDisposition?.ToString(), - [HttpHeaders.ContentEncoding] = res => res.Content.Headers.ContentEncoding?.ToString(), - [HttpHeaders.ContentLength] = res => res.Content.Headers.ContentLength?.ToString(), - [HttpHeaders.ETag] = res => res.Headers.ETag?.Tag.ToString(), - [HttpHeaders.Vary] = res => string.Join(',', res.Headers.Vary), - [HttpHeaders.CacheControl] = res => res.Headers.CacheControl?.ToString(), - }; + public static string? GetHeader(this HttpRequestMessage req, string name) + { + if (RequestHeadersResolver.TryGetValue(name, out var fn)) + return fn(req); + + return req.Headers.TryGetValues(name, out var headers) + ? headers.FirstOrDefault() + : req.Content?.Headers.TryGetValues(name, out var contentHeaders) == true + ? contentHeaders!.FirstOrDefault() + : null; + } + public static Dictionary> RequestHeadersResolver { get; set; } = new(StringComparer.OrdinalIgnoreCase) { + }; + public static Dictionary> ResponseHeadersResolver { get; set; } = new(StringComparer.OrdinalIgnoreCase) { + }; public static string? GetHeader(this HttpResponseMessage res, string name) { - if (HeadersResolver.TryGetValue(name, out var fn)) + if (ResponseHeadersResolver.TryGetValue(name, out var fn)) return fn(res); - - return res.Headers.TryGetValues(name, out var values) - ? values.FirstOrDefault() - : null; + + return res.Headers.TryGetValues(name, out var headers) + ? headers.FirstOrDefault() + : res.Content?.Headers.TryGetValues(name, out var contentHeaders) == true + ? contentHeaders!.FirstOrDefault() + : null; } public static HttpRequestMessage WithHeader(this HttpRequestMessage httpReq, string name, string value) @@ -1115,4 +1118,10 @@ public static void DownloadFileTo(this string downloadUrl, string fileName, } } +public static class HttpClientExt +{ + public static bool MatchesContentType(this HttpResponseMessage res, string matchesContentType) => + MimeTypes.MatchesContentType(res.GetHeader(HttpHeaders.ContentType), matchesContentType); +} + #endif diff --git a/src/ServiceStack.Text/HttpUtils.WebRequest.cs b/src/ServiceStack.Text/HttpUtils.WebRequest.cs index 41685d848..d504490ff 100644 --- a/src/ServiceStack.Text/HttpUtils.WebRequest.cs +++ b/src/ServiceStack.Text/HttpUtils.WebRequest.cs @@ -1181,4 +1181,10 @@ public void UploadStream(HttpWebRequest webRequest, Stream fileStream, string fi UploadFileFn?.Invoke(webRequest, fileStream, fileName); } } + +public static class HttpClientExt +{ + public static bool MatchesContentType(this HttpWebResponse res, string matchesContentType) => + MimeTypes.MatchesContentType(res.Headers[HttpHeaders.ContentType], matchesContentType); +} #endif diff --git a/tests/ServiceStack.Text.Tests/UseCases/ServiceStack_Text_UseCase.cs b/tests/ServiceStack.Text.Tests/UseCases/ServiceStack_Text_UseCase.cs index d881a8ab2..30a6922fc 100644 --- a/tests/ServiceStack.Text.Tests/UseCases/ServiceStack_Text_UseCase.cs +++ b/tests/ServiceStack.Text.Tests/UseCases/ServiceStack_Text_UseCase.cs @@ -1,5 +1,4 @@ -#if !NETCORE -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using NUnit.Framework; @@ -25,18 +24,22 @@ public void Dump_and_Write_GitHub_Organization_Repos_to_CSV() { var orgName = "ServiceStack"; - var orgRepos = "https://api.github.com/orgs/{0}/repos".Fmt(orgName) - .GetJsonFromUrl(httpReq => httpReq.UserAgent = "ServiceStack.Text") + var orgRepos = $"https://api.github.com/orgs/{orgName}/repos" + .GetJsonFromUrl(req => req.With(c => c.UserAgent = "ServiceStack.Text"), + responseFilter: res => + { + var contentType = res.GetHeader(HttpHeaders.ContentType); + Assert.That(res.MatchesContentType(MimeTypes.Json)); + }) .FromJson>(); - "Writing {0} Github Repositories:".Print(orgName); + $"Writing {orgName} Github Repositories:".Print(); orgRepos.PrintDump(); //recursive, pretty-format dump of any C# POCOs - var csvFilePath = "~/{0}-repos.csv".Fmt(orgName).MapAbsolutePath(); + var csvFilePath = $"~/{orgName}-repos.csv".MapAbsolutePath(); File.WriteAllText(csvFilePath, orgRepos.ToCsv()); - Process.Start(csvFilePath); + if (Env.IsNetFramework) Process.Start(csvFilePath); } } } -#endif \ No newline at end of file From db2e1201b9f25887cf5657095b3759179b020e0a Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 31 Jan 2022 10:45:03 +0800 Subject: [PATCH 49/75] Add GetContentLength() ext method --- src/ServiceStack.Text/HttpUtils.HttpClient.cs | 3 +++ src/ServiceStack.Text/HttpUtils.WebRequest.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs index 353d08c46..74712a740 100644 --- a/src/ServiceStack.Text/HttpUtils.HttpClient.cs +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -1122,6 +1122,9 @@ public static class HttpClientExt { public static bool MatchesContentType(this HttpResponseMessage res, string matchesContentType) => MimeTypes.MatchesContentType(res.GetHeader(HttpHeaders.ContentType), matchesContentType); + + public static long? GetContentLength(this HttpResponseMessage res) => + res.Content.Headers.ContentLength; } #endif diff --git a/src/ServiceStack.Text/HttpUtils.WebRequest.cs b/src/ServiceStack.Text/HttpUtils.WebRequest.cs index d504490ff..aa5268739 100644 --- a/src/ServiceStack.Text/HttpUtils.WebRequest.cs +++ b/src/ServiceStack.Text/HttpUtils.WebRequest.cs @@ -1186,5 +1186,8 @@ public static class HttpClientExt { public static bool MatchesContentType(this HttpWebResponse res, string matchesContentType) => MimeTypes.MatchesContentType(res.Headers[HttpHeaders.ContentType], matchesContentType); + + public static long? GetContentLength(this HttpWebResponse res) => + res.ContentLength == -1 ? null : res.ContentLength; } #endif From 8c6929d8942a8c81e5bd161fe559e5cd80cacf34 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 31 Jan 2022 10:53:29 +0800 Subject: [PATCH 50/75] Add code docs --- src/ServiceStack.Text/HttpUtils.HttpClient.cs | 14 ++++++++++++++ src/ServiceStack.Text/HttpUtils.WebRequest.cs | 13 ++++++++++++- src/ServiceStack.Text/MimeTypes.cs | 5 +++-- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs index 74712a740..504d84200 100644 --- a/src/ServiceStack.Text/HttpUtils.HttpClient.cs +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -1010,6 +1010,9 @@ public static async Task PutFileToUrlAsync(this string url, public static void AddHeader(this HttpRequestMessage res, string name, string value) => res.WithHeader(name, value); + /// + /// Returns first Request Header in HttpRequestMessage Headers and Content.Headers + /// public static string? GetHeader(this HttpRequestMessage req, string name) { if (RequestHeadersResolver.TryGetValue(name, out var fn)) @@ -1026,6 +1029,10 @@ public static void AddHeader(this HttpRequestMessage res, string name, string va }; public static Dictionary> ResponseHeadersResolver { get; set; } = new(StringComparer.OrdinalIgnoreCase) { }; + + /// + /// Returns first Response Header in HttpResponseMessage Headers and Content.Headers + /// public static string? GetHeader(this HttpResponseMessage res, string name) { if (ResponseHeadersResolver.TryGetValue(name, out var fn)) @@ -1073,6 +1080,10 @@ public static HttpRequestMessage WithHeader(this HttpRequestMessage httpReq, str return httpReq; } + /// + /// Populate HttpRequestMessage with a simpler, untyped API + /// Syntax compatible with HttpWebRequest + /// public static HttpRequestMessage With(this HttpRequestMessage httpReq, Action configure) { var config = new HttpRequestConfig(); @@ -1120,6 +1131,9 @@ public static void DownloadFileTo(this string downloadUrl, string fileName, public static class HttpClientExt { + /// + /// Case-insensitive, trimmed compare of two content types from start to ';', i.e. without charset suffix + /// public static bool MatchesContentType(this HttpResponseMessage res, string matchesContentType) => MimeTypes.MatchesContentType(res.GetHeader(HttpHeaders.ContentType), matchesContentType); diff --git a/src/ServiceStack.Text/HttpUtils.WebRequest.cs b/src/ServiceStack.Text/HttpUtils.WebRequest.cs index aa5268739..a56e56c77 100644 --- a/src/ServiceStack.Text/HttpUtils.WebRequest.cs +++ b/src/ServiceStack.Text/HttpUtils.WebRequest.cs @@ -1100,6 +1100,10 @@ public static HttpWebRequest WithHeader(this HttpWebRequest httpReq, string name return httpReq; } + /// + /// Populate HttpRequestMessage with a simpler, untyped API + /// Syntax compatible with HttpClient's HttpRequestMessage + /// public static HttpWebRequest With(this HttpWebRequest httpReq, Action configure) { var config = new HttpRequestConfig(); @@ -1184,9 +1188,16 @@ public void UploadStream(HttpWebRequest webRequest, Stream fileStream, string fi public static class HttpClientExt { + /// + /// Case-insensitive, trimmed compare of two content types from start to ';', i.e. without charset suffix + /// public static bool MatchesContentType(this HttpWebResponse res, string matchesContentType) => MimeTypes.MatchesContentType(res.Headers[HttpHeaders.ContentType], matchesContentType); - + + /// + /// Returns null for unknown Content-length + /// Syntax + Behavior compatible with HttpClient HttpResponseMessage + /// public static long? GetContentLength(this HttpWebResponse res) => res.ContentLength == -1 ? null : res.ContentLength; } diff --git a/src/ServiceStack.Text/MimeTypes.cs b/src/ServiceStack.Text/MimeTypes.cs index bd9e5430e..4dfe3b122 100644 --- a/src/ServiceStack.Text/MimeTypes.cs +++ b/src/ServiceStack.Text/MimeTypes.cs @@ -94,8 +94,9 @@ public static string GetRealContentType(string contentType) : null; } - //Compares two string from start to ';' char, case-insensitive, - //ignoring (trimming) spaces at start and end + /// + /// Case-insensitive, trimmed compare of two content types from start to ';', i.e. without charset suffix + /// public static bool MatchesContentType(string contentType, string matchesContentType) { if (contentType == null || matchesContentType == null) From 2d7eacc6b7c551a656e2e17183643b1b3de24116 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 31 Jan 2022 11:59:32 +0800 Subject: [PATCH 51/75] refactor HttpRequestConfig to use typed records --- src/ServiceStack.Text/HttpRequestConfig.cs | 45 +++++++++++++++++-- src/ServiceStack.Text/HttpUtils.HttpClient.cs | 17 +++---- src/ServiceStack.Text/HttpUtils.WebRequest.cs | 40 +++++++++++++---- src/ServiceStack.Text/PclExport.cs | 5 --- 4 files changed, 81 insertions(+), 26 deletions(-) diff --git a/src/ServiceStack.Text/HttpRequestConfig.cs b/src/ServiceStack.Text/HttpRequestConfig.cs index 5195519b9..516cadd5d 100644 --- a/src/ServiceStack.Text/HttpRequestConfig.cs +++ b/src/ServiceStack.Text/HttpRequestConfig.cs @@ -11,17 +11,54 @@ public class HttpRequestConfig public string? Accept { get; set; } public string? UserAgent { get; set; } public string? ContentType { get; set; } - public KeyValuePair? Authorization { get; set; } - public Dictionary? Headers { get; set; } + public HttpHeader? Authorization { get; set; } + public RangeHeader? Range { get; set; } + public List Headers { get; set; } = new(); public string AuthBearer { set => Authorization = new("Bearer", value); } - public KeyValuePair AuthBasic + public HttpHeader AuthBasic { set => Authorization = new("Basic", - Convert.ToBase64String(Encoding.UTF8.GetBytes(value.Key + ":" + value.Value))); + Convert.ToBase64String(Encoding.UTF8.GetBytes(value.Name + ":" + value.Value))); + } +} + +public record HttpHeader +{ + public HttpHeader(string name, string value) + { + this.Name = name; + this.Value = value; + } + + public string Name { get; } + public string Value { get; } + + public void Deconstruct(out string name, out string value) + { + name = this.Name; + value = this.Value; + } +} + +public record RangeHeader +{ + public RangeHeader(long from, long? to = null) + { + this.From = from; + this.To = to; + } + + public long From { get; } + public long? To { get; } + + public void Deconstruct(out long from, out long? to) + { + from = this.From; + to = this.To; } } diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs index 504d84200..24bfa77c6 100644 --- a/src/ServiceStack.Text/HttpUtils.HttpClient.cs +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -1089,35 +1089,36 @@ public static HttpRequestMessage With(this HttpRequestMessage httpReq, Action(); var headers = config.Headers; if (config.Accept != null) - headers[HttpHeaders.Accept] = config.Accept; + headers.Add(new(HttpHeaders.Accept, config.Accept)); if (config.UserAgent != null) - headers[HttpHeaders.UserAgent] = config.UserAgent; + headers.Add(new(HttpHeaders.UserAgent, config.UserAgent)); if (config.ContentType != null) - headers[HttpHeaders.ContentType] = config.ContentType; + headers.Add(new(HttpHeaders.ContentType, config.ContentType)); if (config.Authorization != null) httpReq.Headers.Authorization = - new AuthenticationHeaderValue(config.Authorization.Value.Key, config.Authorization.Value.Value); + new AuthenticationHeaderValue(config.Authorization.Name, config.Authorization.Value); + if (config.Range != null) + httpReq.Headers.Range = new RangeHeaderValue(config.Range.From, config.Range.To); foreach (var entry in headers) { - httpReq.WithHeader(entry.Key, entry.Value); + httpReq.WithHeader(entry.Name, entry.Value); } return httpReq; } public static void DownloadFileTo(this string downloadUrl, string fileName, - Dictionary? headers = null) + List? headers = null) { var client = Create(); var httpReq = new HttpRequestMessage(HttpMethod.Get, downloadUrl) .With(c => { c.Accept = "*/*"; - c.Headers = headers; + if (headers != null) c.Headers = headers; }); var httpRes = client.Send(httpReq); diff --git a/src/ServiceStack.Text/HttpUtils.WebRequest.cs b/src/ServiceStack.Text/HttpUtils.WebRequest.cs index a56e56c77..1a31b6e8b 100644 --- a/src/ServiceStack.Text/HttpUtils.WebRequest.cs +++ b/src/ServiceStack.Text/HttpUtils.WebRequest.cs @@ -1074,18 +1074,40 @@ private static byte[] GetHeaderBytes(string fileName, string mimeType, string fi } public static void DownloadFileTo(this string downloadUrl, string fileName, - Dictionary headers = null) + List headers = null) { var webClient = new WebClient(); if (headers != null) { - foreach (var entry in headers) + foreach (var header in headers) { - webClient.Headers[entry.Key] = entry.Value; + webClient.Headers[header.Name] = header.Value; } } webClient.DownloadFile(downloadUrl, fileName); } + + public static void SetRange(this HttpWebRequest request, long from, long? to) + { + var rangeSpecifier = "bytes"; + var curRange = request.Headers[HttpRequestHeader.Range]; + + if (string.IsNullOrEmpty(curRange)) + { + curRange = rangeSpecifier + "="; + } + else + { + if (string.Compare(curRange.Substring(0, curRange.IndexOf('=')), rangeSpecifier, StringComparison.OrdinalIgnoreCase) != 0) + throw new NotSupportedException("Invalid Range: " + curRange); + curRange = string.Empty; + } + curRange += from.ToString(); + if (to != null) { + curRange += "-" + to; + } + request.Headers[HttpRequestHeader.Range] = curRange; + } public static void AddHeader(this HttpWebRequest res, string name, string value) => res.Headers[name] = value; @@ -1120,14 +1142,14 @@ public static HttpWebRequest With(this HttpWebRequest httpReq, Action Date: Mon, 31 Jan 2022 12:14:35 +0800 Subject: [PATCH 52/75] Use generic reusable names for Headers --- src/ServiceStack.Text/HttpRequestConfig.cs | 16 ++++++++-------- src/ServiceStack.Text/HttpUtils.HttpClient.cs | 2 +- src/ServiceStack.Text/HttpUtils.WebRequest.cs | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/ServiceStack.Text/HttpRequestConfig.cs b/src/ServiceStack.Text/HttpRequestConfig.cs index 516cadd5d..6c300c184 100644 --- a/src/ServiceStack.Text/HttpRequestConfig.cs +++ b/src/ServiceStack.Text/HttpRequestConfig.cs @@ -11,25 +11,25 @@ public class HttpRequestConfig public string? Accept { get; set; } public string? UserAgent { get; set; } public string? ContentType { get; set; } - public HttpHeader? Authorization { get; set; } - public RangeHeader? Range { get; set; } - public List Headers { get; set; } = new(); + public NameValue? Authorization { get; set; } + public LongRange? Range { get; set; } + public List Headers { get; set; } = new(); public string AuthBearer { set => Authorization = new("Bearer", value); } - public HttpHeader AuthBasic + public NameValue AuthBasic { set => Authorization = new("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes(value.Name + ":" + value.Value))); } } -public record HttpHeader +public record NameValue { - public HttpHeader(string name, string value) + public NameValue(string name, string value) { this.Name = name; this.Value = value; @@ -45,9 +45,9 @@ public void Deconstruct(out string name, out string value) } } -public record RangeHeader +public record LongRange { - public RangeHeader(long from, long? to = null) + public LongRange(long from, long? to = null) { this.From = from; this.To = to; diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs index 24bfa77c6..16402ec12 100644 --- a/src/ServiceStack.Text/HttpUtils.HttpClient.cs +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -1112,7 +1112,7 @@ public static HttpRequestMessage With(this HttpRequestMessage httpReq, Action? headers = null) + List? headers = null) { var client = Create(); var httpReq = new HttpRequestMessage(HttpMethod.Get, downloadUrl) diff --git a/src/ServiceStack.Text/HttpUtils.WebRequest.cs b/src/ServiceStack.Text/HttpUtils.WebRequest.cs index 1a31b6e8b..a9cc03fc9 100644 --- a/src/ServiceStack.Text/HttpUtils.WebRequest.cs +++ b/src/ServiceStack.Text/HttpUtils.WebRequest.cs @@ -1074,7 +1074,7 @@ private static byte[] GetHeaderBytes(string fileName, string mimeType, string fi } public static void DownloadFileTo(this string downloadUrl, string fileName, - List headers = null) + List headers = null) { var webClient = new WebClient(); if (headers != null) From 5dda58d217103bf6caead6ade0f62a51180d655d Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 31 Jan 2022 13:18:27 +0800 Subject: [PATCH 53/75] Replace HttpRequestConfig helpers with methods --- src/ServiceStack.Text/HttpRequestConfig.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/ServiceStack.Text/HttpRequestConfig.cs b/src/ServiceStack.Text/HttpRequestConfig.cs index 6c300c184..f5f408693 100644 --- a/src/ServiceStack.Text/HttpRequestConfig.cs +++ b/src/ServiceStack.Text/HttpRequestConfig.cs @@ -15,16 +15,10 @@ public class HttpRequestConfig public LongRange? Range { get; set; } public List Headers { get; set; } = new(); - public string AuthBearer - { - set => Authorization = new("Bearer", value); - } - - public NameValue AuthBasic - { - set => Authorization = new("Basic", - Convert.ToBase64String(Encoding.UTF8.GetBytes(value.Name + ":" + value.Value))); - } + public void SetAuthBearer(string value) => Authorization = new("Bearer", value); + public void SetAuthBasic(string name, string value) => + Authorization = new("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes(name + ":" + value))); + public void SetRange(long from, long? to = null) => Range = new(from, to); } public record NameValue From da747ae95e03e8217e228f5c8e3d9c34c98f4509 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 31 Jan 2022 13:19:09 +0800 Subject: [PATCH 54/75] Update HttpRequestConfig.cs --- src/ServiceStack.Text/HttpRequestConfig.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/ServiceStack.Text/HttpRequestConfig.cs b/src/ServiceStack.Text/HttpRequestConfig.cs index f5f408693..3158d52e8 100644 --- a/src/ServiceStack.Text/HttpRequestConfig.cs +++ b/src/ServiceStack.Text/HttpRequestConfig.cs @@ -19,6 +19,8 @@ public class HttpRequestConfig public void SetAuthBasic(string name, string value) => Authorization = new("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes(name + ":" + value))); public void SetRange(long from, long? to = null) => Range = new(from, to); + + public void AddHeader(string name, string value) => Headers.Add(new(name, value)); } public record NameValue From 5a4702ad90402899498a4cbd23beb9542e38eea0 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 31 Jan 2022 15:40:45 +0800 Subject: [PATCH 55/75] Update HttpUtils.WebRequest.cs --- src/ServiceStack.Text/HttpUtils.WebRequest.cs | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.WebRequest.cs b/src/ServiceStack.Text/HttpUtils.WebRequest.cs index a9cc03fc9..31d482acf 100644 --- a/src/ServiceStack.Text/HttpUtils.WebRequest.cs +++ b/src/ServiceStack.Text/HttpUtils.WebRequest.cs @@ -1089,24 +1089,10 @@ public static void DownloadFileTo(this string downloadUrl, string fileName, public static void SetRange(this HttpWebRequest request, long from, long? to) { - var rangeSpecifier = "bytes"; - var curRange = request.Headers[HttpRequestHeader.Range]; - - if (string.IsNullOrEmpty(curRange)) - { - curRange = rangeSpecifier + "="; - } + if (to != null) + request.AddRange(from, to.Value); else - { - if (string.Compare(curRange.Substring(0, curRange.IndexOf('=')), rangeSpecifier, StringComparison.OrdinalIgnoreCase) != 0) - throw new NotSupportedException("Invalid Range: " + curRange); - curRange = string.Empty; - } - curRange += from.ToString(); - if (to != null) { - curRange += "-" + to; - } - request.Headers[HttpRequestHeader.Range] = curRange; + request.AddRange(from); } public static void AddHeader(this HttpWebRequest res, string name, string value) => From 616f3bc248b687df4f443133eb2abd00f4614180 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 31 Jan 2022 15:47:45 +0800 Subject: [PATCH 56/75] Add explicit Referer Header --- src/ServiceStack.Text/HttpRequestConfig.cs | 1 + src/ServiceStack.Text/HttpUtils.HttpClient.cs | 6 ++++++ src/ServiceStack.Text/HttpUtils.WebRequest.cs | 3 +++ 3 files changed, 10 insertions(+) diff --git a/src/ServiceStack.Text/HttpRequestConfig.cs b/src/ServiceStack.Text/HttpRequestConfig.cs index 3158d52e8..7eeb1d4fc 100644 --- a/src/ServiceStack.Text/HttpRequestConfig.cs +++ b/src/ServiceStack.Text/HttpRequestConfig.cs @@ -11,6 +11,7 @@ public class HttpRequestConfig public string? Accept { get; set; } public string? UserAgent { get; set; } public string? ContentType { get; set; } + public string? Referer { get; set; } public NameValue? Authorization { get; set; } public LongRange? Range { get; set; } public List Headers { get; set; } = new(); diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs index 16402ec12..16ffb5132 100644 --- a/src/ServiceStack.Text/HttpUtils.HttpClient.cs +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -1058,6 +1058,10 @@ public static HttpRequestMessage WithHeader(this HttpRequestMessage httpReq, str throw new NotSupportedException("Can't set ContentType before Content is populated"); httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue(value); } + else if (name.Equals(HttpHeaders.Referer, StringComparison.OrdinalIgnoreCase)) + { + httpReq.Headers.Referrer = new Uri(value); + } else if (name.Equals(HttpHeaders.UserAgent, StringComparison.OrdinalIgnoreCase)) { if (value.IndexOf('/') >= 0) @@ -1097,6 +1101,8 @@ public static HttpRequestMessage With(this HttpRequestMessage httpReq, Action Date: Mon, 31 Jan 2022 16:19:02 +0800 Subject: [PATCH 57/75] Fix Accept + Add Except --- src/ServiceStack.Text/HttpRequestConfig.cs | 1 + src/ServiceStack.Text/HttpUtils.HttpClient.cs | 2 ++ src/ServiceStack.Text/HttpUtils.WebRequest.cs | 5 ++++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ServiceStack.Text/HttpRequestConfig.cs b/src/ServiceStack.Text/HttpRequestConfig.cs index 7eeb1d4fc..bf7036ff2 100644 --- a/src/ServiceStack.Text/HttpRequestConfig.cs +++ b/src/ServiceStack.Text/HttpRequestConfig.cs @@ -12,6 +12,7 @@ public class HttpRequestConfig public string? UserAgent { get; set; } public string? ContentType { get; set; } public string? Referer { get; set; } + public string? Expect { get; set; } public NameValue? Authorization { get; set; } public LongRange? Range { get; set; } public List Headers { get; set; } = new(); diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs index 16ffb5132..2064e74f9 100644 --- a/src/ServiceStack.Text/HttpUtils.HttpClient.cs +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -1108,6 +1108,8 @@ public static HttpRequestMessage With(this HttpRequestMessage httpReq, Action Date: Mon, 31 Jan 2022 17:12:09 +0800 Subject: [PATCH 58/75] Add TransferEncoding chunked --- src/ServiceStack.Text/HttpRequestConfig.cs | 4 +++- src/ServiceStack.Text/HttpUtils.HttpClient.cs | 10 ++++++++++ src/ServiceStack.Text/HttpUtils.WebRequest.cs | 5 +++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/ServiceStack.Text/HttpRequestConfig.cs b/src/ServiceStack.Text/HttpRequestConfig.cs index bf7036ff2..0f9ec75e9 100644 --- a/src/ServiceStack.Text/HttpRequestConfig.cs +++ b/src/ServiceStack.Text/HttpRequestConfig.cs @@ -12,7 +12,9 @@ public class HttpRequestConfig public string? UserAgent { get; set; } public string? ContentType { get; set; } public string? Referer { get; set; } - public string? Expect { get; set; } + public string? Expect { get; set; } + public string[]? TransferEncoding { get; set; } + public bool? TransferEncodingChunked { get; set; } public NameValue? Authorization { get; set; } public LongRange? Range { get; set; } public List Headers { get; set; } = new(); diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs index 2064e74f9..fde630971 100644 --- a/src/ServiceStack.Text/HttpUtils.HttpClient.cs +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -1111,6 +1111,16 @@ public static HttpRequestMessage With(this HttpRequestMessage httpReq, Action 0) + { + foreach (var enc in config.TransferEncoding) + { + httpReq.Headers.TransferEncoding.Add(new(enc)); + } + } + foreach (var entry in headers) { httpReq.WithHeader(entry.Name, entry.Value); diff --git a/src/ServiceStack.Text/HttpUtils.WebRequest.cs b/src/ServiceStack.Text/HttpUtils.WebRequest.cs index 6f972d2b4..85de06867 100644 --- a/src/ServiceStack.Text/HttpUtils.WebRequest.cs +++ b/src/ServiceStack.Text/HttpUtils.WebRequest.cs @@ -1139,6 +1139,11 @@ public static HttpWebRequest With(this HttpWebRequest httpReq, Action 0) + httpReq.TransferEncoding = string.Join(", ", config.TransferEncoding); + foreach (var entry in config.Headers) { httpReq.Headers[entry.Name] = entry.Value; From 717bc657ab9cc25a2de9b57dcae780cb77652999 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 31 Jan 2022 18:37:58 +0800 Subject: [PATCH 59/75] explicitly specify http methods --- src/ServiceStack.Text/HttpUtils.HttpClient.cs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs index fde630971..fae8e729f 100644 --- a/src/ServiceStack.Text/HttpUtils.HttpClient.cs +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -543,14 +543,14 @@ public static async Task SendStringToUrlAsync(this HttpClient client, st public static byte[] GetBytesFromUrl(this string url, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - return url.SendBytesToUrl(accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + return url.SendBytesToUrl(method:HttpMethods.Get, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); } public static Task GetBytesFromUrlAsync(this string url, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return url.SendBytesToUrlAsync(accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, + return url.SendBytesToUrlAsync(method:HttpMethods.Get, accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, token: token); } @@ -596,8 +596,8 @@ public static byte[] SendBytesToUrl(this string url, string method = HttpMethods byte[]? requestBody = null, string? contentType = null, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - return Create().SendBytesToUrl(url, method, requestBody, contentType, accept, - requestFilter, responseFilter); + return Create().SendBytesToUrl(url, method:method, requestBody:requestBody, contentType:contentType, accept:accept, + requestFilter:requestFilter, responseFilter:responseFilter); } public static byte[] SendBytesToUrl(this HttpClient client, string url, string method = HttpMethods.Post, @@ -655,15 +655,16 @@ public static async Task SendBytesToUrlAsync(this HttpClient client, str public static Stream GetStreamFromUrl(this string url, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - return url.SendStreamToUrl(accept: accept, requestFilter: requestFilter, responseFilter: responseFilter); + return url.SendStreamToUrl(method:HttpMethods.Get, accept: accept, + requestFilter: requestFilter, responseFilter: responseFilter); } public static Task GetStreamFromUrlAsync(this string url, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return url.SendStreamToUrlAsync(accept: accept, requestFilter: requestFilter, responseFilter: responseFilter, - token: token); + return url.SendStreamToUrlAsync(method:HttpMethods.Get, accept: accept, + requestFilter: requestFilter, responseFilter: responseFilter, token: token); } public static Stream PostStreamToUrl(this string url, Stream? requestBody = null, string? contentType = null, @@ -708,8 +709,8 @@ public static Stream SendStreamToUrl(this string url, string method = HttpMethod Stream? requestBody = null, string? contentType = null, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - return Create().SendStreamToUrl(url, method, requestBody, contentType, accept, - requestFilter, responseFilter); + return Create().SendStreamToUrl(url, method:method, requestBody:requestBody, contentType:contentType, accept:accept, + requestFilter:requestFilter, responseFilter:responseFilter); } public static Stream SendStreamToUrl(this HttpClient client, string url, string method = HttpMethods.Post, @@ -738,8 +739,8 @@ public static Task SendStreamToUrlAsync(this string url, string method = Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return Create().SendStreamToUrlAsync(url, method, requestBody, contentType, accept, - requestFilter, responseFilter, token); + return Create().SendStreamToUrlAsync(url, method:method, requestBody:requestBody, contentType:contentType, accept:accept, + requestFilter:requestFilter, responseFilter:responseFilter, token); } public static async Task SendStreamToUrlAsync(this HttpClient client, string url, string method = HttpMethods.Post, @@ -826,7 +827,7 @@ public static IEnumerable ReadLines(this HttpResponseMessage webRes) } public static HttpResponseMessage UploadFile(this HttpRequestMessage httpReq, Stream fileStream, - string fileName, string? mimeType = null, string accept = "*/*", string method = HttpMethods.Post, string field = "file", + string fileName, string mimeType, string accept = "*/*", string method = HttpMethods.Post, string field = "file", Action? requestFilter = null, Action? responseFilter = null) { return Create().UploadFile(httpReq, fileStream, fileName, mimeType, accept, method, field, From 71202487f59f48a1710953b8c87d5fa7f4e82430 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 31 Jan 2022 18:45:20 +0800 Subject: [PATCH 60/75] Add more liberal UserAgent --- src/ServiceStack.Text/HttpUtils.HttpClient.cs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs index fae8e729f..6a1b9061f 100644 --- a/src/ServiceStack.Text/HttpUtils.HttpClient.cs +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -1065,18 +1065,7 @@ public static HttpRequestMessage WithHeader(this HttpRequestMessage httpReq, str } else if (name.Equals(HttpHeaders.UserAgent, StringComparison.OrdinalIgnoreCase)) { - if (value.IndexOf('/') >= 0) - { - var product = value.LastLeftPart('/'); - var version = value.LastRightPart('/'); - httpReq.Headers.UserAgent.Add(new ProductInfoHeaderValue(product, version)); - } - else - { - // Avoid format exceptions - var commentFmt = value[0] == '(' && value[^1] == ')' ? value : $"({value})"; - httpReq.Headers.UserAgent.Add(new ProductInfoHeaderValue(commentFmt)); - } + httpReq.Headers.UserAgent.ParseAdd(value); } else { From 8b06357a36a49e3f38b21e46389cd1e034670f4b Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 31 Jan 2022 18:53:35 +0800 Subject: [PATCH 61/75] override accept or consistent behavior --- src/ServiceStack.Text/HttpUtils.HttpClient.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs index 6a1b9061f..0517d7654 100644 --- a/src/ServiceStack.Text/HttpUtils.HttpClient.cs +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -1086,7 +1086,10 @@ public static HttpRequestMessage With(this HttpRequestMessage httpReq, Action Date: Mon, 31 Jan 2022 18:57:28 +0800 Subject: [PATCH 62/75] Update HttpUtils.HttpClient.cs --- src/ServiceStack.Text/HttpUtils.HttpClient.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs index 0517d7654..d825f1452 100644 --- a/src/ServiceStack.Text/HttpUtils.HttpClient.cs +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -485,7 +485,8 @@ public static string SendStringToUrl(this string url, string method = HttpMethod string? requestBody = null, string? contentType = null, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null) { - return Create().SendStringToUrl(url, method, requestBody, contentType, accept, requestFilter, responseFilter); + return Create().SendStringToUrl(url, method:method, requestBody:requestBody, + contentType:contentType, accept:accept, requestFilter:requestFilter, responseFilter:responseFilter); } public static string SendStringToUrl(this HttpClient client, string url, string method = HttpMethods.Post, @@ -514,8 +515,8 @@ public static Task SendStringToUrlAsync(this string url, string? contentType = null, string accept = "*/*", Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return Create().SendStringToUrlAsync(url, method, requestBody, contentType, accept, - requestFilter, responseFilter, token); + return Create().SendStringToUrlAsync(url, method:method, requestBody:requestBody, contentType:contentType, accept:accept, + requestFilter:requestFilter, responseFilter:responseFilter, token); } public static async Task SendStringToUrlAsync(this HttpClient client, string url, string method = HttpMethods.Post, From e50d0c6e03d8434c29ac22c8d6925a75b21026c3 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 31 Jan 2022 18:59:21 +0800 Subject: [PATCH 63/75] Update HttpUtils.HttpClient.cs --- src/ServiceStack.Text/HttpUtils.HttpClient.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs index d825f1452..c29868704 100644 --- a/src/ServiceStack.Text/HttpUtils.HttpClient.cs +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -48,40 +48,40 @@ internal HttpClientFactory(HttpClientHandler handler) => public static string GetJsonFromUrl(this string url, Action? requestFilter = null, Action? responseFilter = null) { - return url.GetStringFromUrl(MimeTypes.Json, requestFilter, responseFilter); + return url.GetStringFromUrl(accept:MimeTypes.Json, requestFilter, responseFilter); } public static Task GetJsonFromUrlAsync(this string url, Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return url.GetStringFromUrlAsync(MimeTypes.Json, requestFilter, responseFilter, token: token); + return url.GetStringFromUrlAsync(accept:MimeTypes.Json, requestFilter, responseFilter, token: token); } public static string GetXmlFromUrl(this string url, Action? requestFilter = null, Action? responseFilter = null) { - return url.GetStringFromUrl(MimeTypes.Xml, requestFilter, responseFilter); + return url.GetStringFromUrl(accept:MimeTypes.Xml, requestFilter, responseFilter); } public static Task GetXmlFromUrlAsync(this string url, Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return url.GetStringFromUrlAsync(MimeTypes.Xml, requestFilter, responseFilter, token: token); + return url.GetStringFromUrlAsync(accept:MimeTypes.Xml, requestFilter, responseFilter, token: token); } public static string GetCsvFromUrl(this string url, Action? requestFilter = null, Action? responseFilter = null) { - return url.GetStringFromUrl(MimeTypes.Csv, requestFilter, responseFilter); + return url.GetStringFromUrl(accept:MimeTypes.Csv, requestFilter, responseFilter); } public static Task GetCsvFromUrlAsync(this string url, Action? requestFilter = null, Action? responseFilter = null, CancellationToken token = default) { - return url.GetStringFromUrlAsync(MimeTypes.Csv, requestFilter, responseFilter, token: token); + return url.GetStringFromUrlAsync(accept:MimeTypes.Csv, requestFilter, responseFilter, token: token); } public static string GetStringFromUrl(this string url, string accept = "*/*", From 5f0e936c211d3cd836d0db3023fddcc7035b4698 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 31 Jan 2022 19:04:11 +0800 Subject: [PATCH 64/75] Update HttpUtils.HttpClient.cs --- src/ServiceStack.Text/HttpUtils.HttpClient.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs index c29868704..93a3df57c 100644 --- a/src/ServiceStack.Text/HttpUtils.HttpClient.cs +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -1058,7 +1058,10 @@ public static HttpRequestMessage WithHeader(this HttpRequestMessage httpReq, str { if (httpReq.Content == null) throw new NotSupportedException("Can't set ContentType before Content is populated"); - httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue(value); + httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue(value.LeftPart(';')); + var charset = value.RightPart(';'); + if (charset != null && charset.IndexOf("charset", StringComparison.OrdinalIgnoreCase) >= 0) + httpReq.Content.Headers.ContentType.CharSet = charset.RightPart('='); } else if (name.Equals(HttpHeaders.Referer, StringComparison.OrdinalIgnoreCase)) { From 271bb2877cbe951f42c7dc80c9f2114030f86d31 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 31 Jan 2022 20:34:39 +0800 Subject: [PATCH 65/75] extract WebRequest ext methods --- src/ServiceStack.Text/HttpUtils.WebRequest.cs | 59 +++++++++++++------ 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.WebRequest.cs b/src/ServiceStack.Text/HttpUtils.WebRequest.cs index 85de06867..9579d2259 100644 --- a/src/ServiceStack.Text/HttpUtils.WebRequest.cs +++ b/src/ServiceStack.Text/HttpUtils.WebRequest.cs @@ -455,6 +455,21 @@ public static string SendStringToUrl(this string url, string method = null, Action requestFilter = null, Action responseFilter = null) { var webReq = (HttpWebRequest)WebRequest.Create(url); + return SendStringToUrl(webReq, method, requestBody, contentType, accept, requestFilter, responseFilter); + } + + public static async Task SendStringToUrlAsync(this string url, string method = null, + string requestBody = null, + string contentType = null, string accept = "*/*", Action requestFilter = null, + Action responseFilter = null, CancellationToken token = default) + { + var webReq = (HttpWebRequest)WebRequest.Create(url); + return await SendStringToUrlAsync(webReq, method, requestBody, contentType, accept, requestFilter, responseFilter); + } + + public static string SendStringToUrl(this HttpWebRequest webReq, string method, string requestBody, string contentType, + string accept, Action requestFilter, Action responseFilter) + { if (method != null) webReq.Method = method; if (contentType != null) @@ -486,13 +501,11 @@ public static string SendStringToUrl(this string url, string method = null, responseFilter?.Invoke((HttpWebResponse)webRes); return stream.ReadToEnd(UseEncoding); } - - public static async Task SendStringToUrlAsync(this string url, string method = null, - string requestBody = null, - string contentType = null, string accept = "*/*", Action requestFilter = null, - Action responseFilter = null, CancellationToken token = default) + + public static async Task SendStringToUrlAsync(this HttpWebRequest webReq, + string method, string requestBody, string contentType, string accept, + Action requestFilter, Action responseFilter) { - var webReq = (HttpWebRequest)WebRequest.Create(url); if (method != null) webReq.Method = method; if (contentType != null) @@ -583,6 +596,21 @@ public static byte[] SendBytesToUrl(this string url, string method = null, Action requestFilter = null, Action responseFilter = null) { var webReq = (HttpWebRequest)WebRequest.Create(url); + return SendBytesToUrl(webReq, method, requestBody, contentType, accept, requestFilter, responseFilter); + } + + public static async Task SendBytesToUrlAsync(this string url, string method = null, + byte[] requestBody = null, string contentType = null, string accept = "*/*", + Action requestFilter = null, Action responseFilter = null, + CancellationToken token = default) + { + var webReq = (HttpWebRequest)WebRequest.Create(url); + return await SendBytesToUrlAsync(webReq, method, requestBody, contentType, accept, requestFilter, responseFilter, token); + } + + public static byte[] SendBytesToUrl(this HttpWebRequest webReq, string method, byte[] requestBody, string contentType, + string accept, Action requestFilter, Action responseFilter) + { if (method != null) webReq.Method = method; @@ -611,13 +639,10 @@ public static byte[] SendBytesToUrl(this string url, string method = null, using var stream = webRes.GetResponseStream(); return stream.ReadFully(); } - - public static async Task SendBytesToUrlAsync(this string url, string method = null, - byte[] requestBody = null, string contentType = null, string accept = "*/*", - Action requestFilter = null, Action responseFilter = null, - CancellationToken token = default) + + public static async Task SendBytesToUrlAsync(this HttpWebRequest webReq, string method, byte[] requestBody, + string contentType, string accept, Action requestFilter, Action responseFilter, CancellationToken token) { - var webReq = (HttpWebRequest)WebRequest.Create(url); if (method != null) webReq.Method = method; if (contentType != null) @@ -646,7 +671,7 @@ public static async Task SendBytesToUrlAsync(this string url, string met using var stream = webRes.GetResponseStream(); return await stream.ReadFullyAsync(token).ConfigAwait(); } - + public static Stream GetStreamFromUrl(this string url, string accept = "*/*", Action requestFilter = null, Action responseFilter = null) { @@ -779,11 +804,9 @@ public static async Task SendStreamToUrlAsync(this string url, string me try { var webReq = (HttpWebRequest)WebRequest.Create(url); - using (var webRes = PclExport.Instance.GetResponse(webReq)) - { - var httpRes = webRes as HttpWebResponse; - return httpRes?.StatusCode; - } + using var webRes = PclExport.Instance.GetResponse(webReq); + var httpRes = webRes as HttpWebResponse; + return httpRes?.StatusCode; } catch (Exception ex) { From 4a4eea8a857c1c4a86c8f4a0d80ee6099b1aa978 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 31 Jan 2022 20:54:49 +0800 Subject: [PATCH 66/75] Use CreateHttp method --- src/ServiceStack.Text/HttpUtils.WebRequest.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.WebRequest.cs b/src/ServiceStack.Text/HttpUtils.WebRequest.cs index 9579d2259..0f776e07b 100644 --- a/src/ServiceStack.Text/HttpUtils.WebRequest.cs +++ b/src/ServiceStack.Text/HttpUtils.WebRequest.cs @@ -454,7 +454,7 @@ public static string SendStringToUrl(this string url, string method = null, string requestBody = null, string contentType = null, string accept = "*/*", Action requestFilter = null, Action responseFilter = null) { - var webReq = (HttpWebRequest)WebRequest.Create(url); + var webReq = WebRequest.CreateHttp(url); return SendStringToUrl(webReq, method, requestBody, contentType, accept, requestFilter, responseFilter); } @@ -463,7 +463,7 @@ public static async Task SendStringToUrlAsync(this string url, string me string contentType = null, string accept = "*/*", Action requestFilter = null, Action responseFilter = null, CancellationToken token = default) { - var webReq = (HttpWebRequest)WebRequest.Create(url); + var webReq = WebRequest.CreateHttp(url); return await SendStringToUrlAsync(webReq, method, requestBody, contentType, accept, requestFilter, responseFilter); } @@ -595,7 +595,7 @@ public static byte[] SendBytesToUrl(this string url, string method = null, byte[] requestBody = null, string contentType = null, string accept = "*/*", Action requestFilter = null, Action responseFilter = null) { - var webReq = (HttpWebRequest)WebRequest.Create(url); + var webReq = WebRequest.CreateHttp(url); return SendBytesToUrl(webReq, method, requestBody, contentType, accept, requestFilter, responseFilter); } @@ -604,7 +604,7 @@ public static async Task SendBytesToUrlAsync(this string url, string met Action requestFilter = null, Action responseFilter = null, CancellationToken token = default) { - var webReq = (HttpWebRequest)WebRequest.Create(url); + var webReq = WebRequest.CreateHttp(url); return await SendBytesToUrlAsync(webReq, method, requestBody, contentType, accept, requestFilter, responseFilter, token); } @@ -731,7 +731,7 @@ public static Stream SendStreamToUrl(this string url, string method = null, Stream requestBody = null, string contentType = null, string accept = "*/*", Action requestFilter = null, Action responseFilter = null) { - var webReq = (HttpWebRequest)WebRequest.Create(url); + var webReq = WebRequest.CreateHttp(url); if (method != null) webReq.Method = method; @@ -769,7 +769,7 @@ public static async Task SendStreamToUrlAsync(this string url, string me Action requestFilter = null, Action responseFilter = null, CancellationToken token = default) { - var webReq = (HttpWebRequest)WebRequest.Create(url); + var webReq = WebRequest.CreateHttp(url); if (method != null) webReq.Method = method; if (contentType != null) @@ -803,7 +803,7 @@ public static async Task SendStreamToUrlAsync(this string url, string me { try { - var webReq = (HttpWebRequest)WebRequest.Create(url); + var webReq = WebRequest.CreateHttp(url); using var webRes = PclExport.Instance.GetResponse(webReq); var httpRes = webRes as HttpWebResponse; return httpRes?.StatusCode; @@ -980,7 +980,7 @@ public static WebResponse PostFileToUrl(this string url, string accept = null, Action requestFilter = null) { - var webReq = (HttpWebRequest)WebRequest.Create(url); + var webReq = WebRequest.CreateHttp(url); using (var fileStream = uploadFileInfo.OpenRead()) { var fileName = uploadFileInfo.Name; @@ -1000,7 +1000,7 @@ public static async Task PostFileToUrlAsync(this string url, string accept = null, Action requestFilter = null, CancellationToken token = default) { - var webReq = (HttpWebRequest)WebRequest.Create(url); + var webReq = WebRequest.CreateHttp(url); using (var fileStream = uploadFileInfo.OpenRead()) { var fileName = uploadFileInfo.Name; @@ -1020,7 +1020,7 @@ public static WebResponse PutFileToUrl(this string url, string accept = null, Action requestFilter = null) { - var webReq = (HttpWebRequest)WebRequest.Create(url); + var webReq = WebRequest.CreateHttp(url); using (var fileStream = uploadFileInfo.OpenRead()) { var fileName = uploadFileInfo.Name; @@ -1040,7 +1040,7 @@ public static async Task PutFileToUrlAsync(this string url, string accept = null, Action requestFilter = null, CancellationToken token = default) { - var webReq = (HttpWebRequest)WebRequest.Create(url); + var webReq = WebRequest.CreateHttp(url); using (var fileStream = uploadFileInfo.OpenRead()) { var fileName = uploadFileInfo.Name; From b05dfff0fec30193db4680445c19812bec3d33d1 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Mon, 31 Jan 2022 20:55:07 +0800 Subject: [PATCH 67/75] Fix ContentType parsing in HttpClient utils --- src/ServiceStack.Text/HttpUtils.HttpClient.cs | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs index 93a3df57c..b2ed24252 100644 --- a/src/ServiceStack.Text/HttpUtils.HttpClient.cs +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -500,7 +500,7 @@ public static string SendStringToUrl(this HttpClient client, string url, string { httpReq.Content = new StringContent(requestBody, UseEncoding); if (contentType != null) - httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); + httpReq.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); } requestFilter?.Invoke(httpReq); @@ -531,7 +531,7 @@ public static async Task SendStringToUrlAsync(this HttpClient client, st { httpReq.Content = new StringContent(requestBody, UseEncoding); if (contentType != null) - httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); + httpReq.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); } requestFilter?.Invoke(httpReq); @@ -612,7 +612,7 @@ public static byte[] SendBytesToUrl(this HttpClient client, string url, string m { httpReq.Content = new ReadOnlyMemoryContent(requestBody); if (contentType != null) - httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); + httpReq.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); } requestFilter?.Invoke(httpReq); @@ -643,7 +643,7 @@ public static async Task SendBytesToUrlAsync(this HttpClient client, str { httpReq.Content = new ReadOnlyMemoryContent(requestBody); if (contentType != null) - httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); + httpReq.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); } requestFilter?.Invoke(httpReq); @@ -725,7 +725,7 @@ public static Stream SendStreamToUrl(this HttpClient client, string url, string { httpReq.Content = new StreamContent(requestBody); if (contentType != null) - httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); + httpReq.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); } requestFilter?.Invoke(httpReq); @@ -756,7 +756,7 @@ public static async Task SendStreamToUrlAsync(this HttpClient client, st { httpReq.Content = new StreamContent(requestBody); if (contentType != null) - httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType); + httpReq.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType); } requestFilter?.Invoke(httpReq); @@ -1058,10 +1058,7 @@ public static HttpRequestMessage WithHeader(this HttpRequestMessage httpReq, str { if (httpReq.Content == null) throw new NotSupportedException("Can't set ContentType before Content is populated"); - httpReq.Content.Headers.ContentType = new MediaTypeHeaderValue(value.LeftPart(';')); - var charset = value.RightPart(';'); - if (charset != null && charset.IndexOf("charset", StringComparison.OrdinalIgnoreCase) >= 0) - httpReq.Content.Headers.ContentType.CharSet = charset.RightPart('='); + httpReq.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(value); } else if (name.Equals(HttpHeaders.Referer, StringComparison.OrdinalIgnoreCase)) { @@ -1097,7 +1094,11 @@ public static HttpRequestMessage With(this HttpRequestMessage httpReq, Action Date: Tue, 1 Feb 2022 21:22:49 +0800 Subject: [PATCH 68/75] bump to v6.0.3 --- src/Directory.Build.props | 2 +- src/ServiceStack.Text/Env.cs | 2 +- tests/Directory.Build.props | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 6bfa3e383..9a112079b 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - 6.0.2 + 6.0.3 ServiceStack ServiceStack, Inc. © 2008-2022 ServiceStack, Inc diff --git a/src/ServiceStack.Text/Env.cs b/src/ServiceStack.Text/Env.cs index 3cc38fea5..8cdbc2d22 100644 --- a/src/ServiceStack.Text/Env.cs +++ b/src/ServiceStack.Text/Env.cs @@ -114,7 +114,7 @@ static Env() public static string VersionString { get; set; } - public static decimal ServiceStackVersion = 6.02m; + public static decimal ServiceStackVersion = 6.03m; public static bool IsLinux { get; set; } diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 046281905..7eb7717ea 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -1,7 +1,7 @@ - 6.0.2 + 6.0.3 latest false From b1c24737de9abe99a9eb13c562837d8998d76274 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Tue, 1 Feb 2022 22:26:21 +0800 Subject: [PATCH 69/75] Update README.md --- README.md | 861 +----------------------------------------------------- 1 file changed, 2 insertions(+), 859 deletions(-) diff --git a/README.md b/README.md index 5432e6189..33ef5488e 100644 --- a/README.md +++ b/README.md @@ -1,860 +1,3 @@ -Follow [@ServiceStack](https://twitter.com/servicestack), [view the docs](https://docs.servicestack.net), use [StackOverflow](https://stackoverflow.com/questions/ask?tags=servicestack,servicestack.text) or [Customer Forums](https://forums.servicestack.net/) for support. +Follow [@ServiceStack](https://twitter.com/servicestack), [view the docs](https://docs.servicestack.net), use [StackOverflow](https://stackoverflow.com/questions/ask?tags=servicestack,servicestack.redis) or [Customer Forums](https://forums.servicestack.net/) for support. -## [FREE high-perf Text Serializers](https://docs.servicestack.net/releases/v4.0.62#servicestacktext-is-now-free) and Core Utils powering [servicestack.net](https://servicestack.net) - -ServiceStack.Text is an **independent, dependency-free** serialization library containing ServiceStack's core high-performance utils and text processing functionality, including: - - - [JSON](https://github.com/ServiceStack/ServiceStack.Text), - [JSV](https://docs.servicestack.net/jsv-format) and - [CSV](https://docs.servicestack.net/csv-format) Text Serializers - - [AutoMapping Utils](https://docs.servicestack.net/auto-mapping) - - [HTTP Utils](https://docs.servicestack.net/http-utils) - - [Dump Utils](https://docs.servicestack.net/dump-utils) - - [Fast Reflection Utils](https://docs.servicestack.net/reflection-utils) - - Several String Extensions, Collection extensions, Reflection Utils and lots more. - -### [Mobile Apps Support](https://github.com/ServiceStackApps/HelloMobile) - -### Try out [ServiceStack.Text Live](https://gist.cafe/c71b3f0123b3d9d08c1b11c98c2ff379) - -A great way to try out ServiceStack.Text is on [gist.cafe](https://gist.cafe) which lets you immediately -run and explore all ServiceStack.Text features from the comfort of your browser with zero software install: - -[![](https://raw.githubusercontent.com/ServiceStack/docs/master/docs/images/gistcafe/gistcafe-csharp.png)](https://gist.cafe/c71b3f0123b3d9d08c1b11c98c2ff379) - -## Simple API - -Like most of the interfaces in ServiceStack, the API is simple. Methods that you would commonly use include: - -### Convenience Serialization Extension Methods - -```csharp -string ToJson(T) -T FromJson() - -string ToJsv(T) -T FromJsv() - -string ToCsv(T) -T FromCsv() - -string ToXml(T) -T FromXml() -``` - -### Explicit API - -#### JSON - -```csharp -string JsonSerializer.SerializeToString(T value) -void JsonSerializer.SerializeToWriter(T value, TextWriter writer) - -T JsonSerializer.DeserializeFromString(string value) -T JsonSerializer.DeserializeFromReader(TextReader reader) -``` - -#### JSV - -```csharp -string TypeSerializer.SerializeToString(T value) -void TypeSerializer.SerializeToWriter(T value, TextWriter writer) - -T TypeSerializer.DeserializeFromString(string value) -T TypeSerializer.DeserializeFromReader(TextReader reader) -``` - -#### CSV - -```csharp -string CsvSerializer.SerializeToString(T value) -void CsvSerializer.SerializeToWriter(T value, TextWriter writer) - -T CsvSerializer.DeserializeFromString(string value) -T CsvSerializer.DeserializeFromReader(TextReader reader) -``` - -Where *T* can be any .NET POCO type. That's all there is - the API was intentionally left simple :) - -### Dump Utils - -Dump / Diagnostic Extensions: - -```csharp -T Dump() -T Print() -T PrintDump() -string Fmt(args) -``` - -### Dynamic JSON parsing API - -```csharp -JsonObject.Parse() -JsonArrayObjects.Parse() -``` - -### Pretty Print JSON - -You can format JSON into a more readable format with the `IndentJson()` extension method, e.g: - -```csharp -var prettyJson = dto.ToJson().IndentJson(); -``` - -URL Extensions: - -```csharp -string GetStringFromUrl() -string GetJsonFromUrl() -string GetResponseStatus() -string UrlEncode() / UrlDecode() -string HexEscape() / HexUnescape() -string UrlFormat() / AppendPath() / AppendPaths() / WithTrailingSlash() -string AddQueryParam() / SetQueryParam() AddHashParam() / SetHashParam() -string WithoutExtension() / ParentDirectory() / ReadAllText() -``` - -#### Stream Extensions: - -```csharp -Stream WriteTo(Stream) / CopyTo() -Stream ReadLines() -Stream ReadFully() / ReadExactly() -``` - -#### String Utils: - -```csharp -string SplitOnFirst() / SplitOnLast() -string IndexOfAny() -string StripHtml() / ToCamelCase() -string SafeSubstring() -string ToUtf8Bytes() / FromUtf8Bytes() -string LeftPart() / LastLeftPart() / RightPart() / LastRightPart() -``` - -more String, Reflection, List, Dictionary, DateTime extensions... - -### Supports Dynamic JSON - -Although usually used to (de)serialize C#/.NET POCO types, it also includes a flexible API allowing you to deserialize any -JSON payload without it's concrete type, see these real-world examples: - - - [Parsing GitHub's v3 API with typed DTOs](https://github.com/ServiceStack/ServiceStack.Text/blob/master/tests/ServiceStack.Text.Tests/UseCases/GithubV3ApiTests.cs) - - [Parsing GitHub's JSON response](https://github.com/ServiceStack/ServiceStack.Text/blob/master/tests/ServiceStack.Text.Tests/UseCases/GitHubRestTests.cs) - - [Parsing Google Maps JSON Response](https://github.com/ServiceStack/ServiceStack.Text/blob/master/tests/ServiceStack.Text.Tests/UseCases/GMapDirectionsTests.cs) - - [Parsing Centroid](https://github.com/ServiceStack/ServiceStack.Text/blob/master/tests/ServiceStack.Text.Tests/UseCases/CentroidTests.cs) - -Also a thin **.NET 4.0 Dynamic JSON** wrapper around ServiceStack's JSON library is included in the -[ServiceStack.Razor](https://github.com/ServiceStack/ServiceStack.Text/blob/master/src/ServiceStack.Text/Pcl.Dynamic.cs) -project. It provides a dynamic, but more succinct API than the above options. - -### JS Utils - -ServiceStack.Text APIs for deserializing arbitrary JSON requires specifying the Type to deserialize into. An alternative flexible approach to read any arbitrary JavaScript or JSON data structures is to use the high-performance and memory efficient JSON utils in -[#Script](https://sharpscript.net/) implementation of JavaScript. - -```csharp -JSON.parse("1") //= int 1 -JSON.parse("1.1") //= double 1.1 -JSON.parse("'a'") //= string "a" -JSON.parse("{a:1}") //= new Dictionary { {"a", 1 } } -JSON.parse("[{a:1}]") //= new List { new Dictionary { { "a", 1 } } } -``` - -#### Eval - -Since JS Utils is an essential part of [#Script](https://sharpscript.net/) it allows for advanced scenarios like implementing a text DSL or scripting language for executing custom logic or business rules you want to be able to change without having to compile or redeploy your App. It uses [#Script Context](https://sharpscript.net/docs/methods) which lets you evaluate the script within a custom scope that defines what functions -and arguments it has access to, e.g.: - -```csharp -public class CustomMethods : ScriptMethods -{ - public string reverse(string text) => new string(text.Reverse().ToArray()); -} - -var scope = JS.CreateScope( - args: new Dictionary { { "arg", "value"} }, - functions: new CustomMethods()); - -JS.eval("arg", scope) //= "value" -JS.eval("reverse(arg)", scope) //= "eulav" -JS.eval("3.itemsOf(arg.reverse().padRight(8, '_'))", scope) //= ["eulav___", "eulav___", "eulav___"] - -//= { a: ["eulav___", "eulav___", "eulav___"] } -JS.eval("{a: 3.itemsOf(arg.reverse().padRight(8, '_')) }", scope) -``` - -ServiceStack's JS Utils is available in the [ServiceStack.Common](https://www.nuget.org/packages/ServiceStack.Common) NuGet package. - -## Install ServiceStack.Text - - PM> Install-Package ServiceStack.Text - -> From v4.0.62+ [ServiceStack.Text is now free!](https://docs.servicestack.net/releases/v4.0.62#servicestacktext-is-now-free) - -## Copying - -Since September 2013, ServiceStack source code is available under GNU Affero General Public License/FOSS License Exception, see license.txt in the source. Alternative commercial licensing is also available, contact team@servicestack.net for details. - -## Contributing - -Contributors need to approve the [Contributor License Agreement](https://docs.google.com/forms/d/16Op0fmKaqYtxGL4sg7w_g-cXXyCoWjzppgkuqzOeKyk/viewform) before any code will be reviewed, see the [Contributing wiki](https://github.com/ServiceStack/ServiceStack/wiki/Contributing) for more details. - -## ServiceStack.JsonSerializer - -For reasons outlined [in this blog post](https://github.com/ServiceStackV3/mythz_blog/blob/master/pages/344.md) I decided to re-use *TypeSerializer's* text processing-core to create ServiceStack.JsonSerializer - the fastest JSON Serializer for .NET. -Based on the [Northwind Benchmarks](http://mono.servicestack.net/benchmarks/) it's *3.6x* faster than .NET's BCL JsonDataContractSerializer and *3x* faster than the previous fastest JSON serializer benchmarked - [JSON.NET](http://json.codeplex.com/). - -A comprehensive set of other .NET benchmarks are maintained at [servicestack.net/benchmarks](http://mono.servicestack.net/benchmarks/) and [in the wiki](https://github.com/ServiceStack/ServiceStack/wiki/Real-world-performance). - -## ServiceStack.CsvSerializer -As CSV is an important format in many data access and migration scenarios, it became [the latest format included in ServiceStack](https://github.com/ServiceStack/ServiceStack/wiki/CSV-Format) which allows all your existing web services to take advantage of the new format without config or code-changes. As its built using the same tech that makes the JSON and JSV serializers so fast, we expect it to be the fastest POCO CSV Serializer for .NET. - -## ServiceStack.TypeSerializer and the JSV-format -Included in this project is `TypeSerializer` - A fast and compact text-based serializer for .NET. It's a light-weight compact Text Serializer which can be used to serialize .NET data types inc custom POCO's and DataContract's. More info on its JSV Format can be found on the [introductory post](https://github.com/ServiceStackV3/mythz_blog/blob/master/pages/176.md). - -## T.Dump() Extension method -Another useful library to have in your .NET toolbox is the [T.Dump() Extension Method](https://github.com/ServiceStackV3/mythz_blog/blob/master/pages/202.md). Under the hood it uses a *Pretty Print* Output of the JSV Format to recursively dump the contents of any .NET object. Example usage and output: - -```csharp -var model = new TestModel(); -model.PrintDump(); - -//Example Output -{ - Int: 1, - String: One, - DateTime: 2010-04-11, - Guid: c050437f6fcd46be9b2d0806a0860b3e, - EmptyIntList: [], - IntList: - [ - 1, - 2, - 3 - ], - StringList: - [ - one, - two, - three - ], - StringIntMap: - { - a: 1, - b: 2, - c: 3 - } -} -``` - -# ServiceStack's JsonSerializer - -ServiceStack's JsonSerializer is optimized for serializing C# POCO types in and out of JSON as fast, compact and cleanly as possible. In most cases C# objects serializes as you would expect them to without added json extensions or serializer-specific artefacts. - -JsonSerializer provides a simple API that allows you to serialize any .NET generic or runtime type into a string, TextWriter/TextReader or Stream. - -### Serialization API - -```csharp -string SerializeToString(T) -void SerializeToWriter(T, TextWriter) -void SerializeToStream(T, Stream) -string SerializeToString(object, Type) -void SerializeToWriter(object, Type, TextWriter) -void SerializeToStream(object, Type, Stream) -``` - -### Deserialization API - -```csharp -T DeserializeFromString(string) -T DeserializeFromReader(TextReader) -object DeserializeFromString(string, Type) -object DeserializeFromReader(reader, Type) -object DeserializeFromStream(Type, Stream) -T DeserializeFromStream(Stream) -``` - -### Extension methods - -```csharp -string ToJson(this T) -T FromJson(this string) -``` - -Convenient **ToJson/FromJson** extension methods are also included reducing the amount of code required, e.g: - -```csharp -new []{ 1, 2, 3 }.ToJson() //= [1,2,3] -"[1,2,3]".FromJson() //= int []{ 1, 2, 3 } -``` - -## JSON Format - -JSON is a lightweight text serialization format with a spec that's so simple that it fits on one page: [https://www.json.org](https://www.json.org). - -The only valid values in JSON are: - - * string - * number - * object - * array - * true - * false - * null - -Where most allowed values are scalar and the only complex types available are objects and arrays. Although limited, the above set of types make a good fit and can express most programming data structures. - -### number, true, false types - -All C# boolean and numeric data types are stored as-is without quotes. - -### null type - -For the most compact output null values are omitted from the serialized by default. If you want to include null values set the global configuration: - -```csharp -JsConfig.Init(new Config { IncludeNullValues = true }); -``` - -### string type - -All other scalar values are stored as strings that are surrounded with double quotes. - -### C# Structs and Value Types - -Because a C# struct is a value type whose public properties are normally just convenience properties around a single scalar value, they are ignored instead the **TStruct.ToString()** method is used to serialize and either the **static TStruct.Parse()** method or **new TStruct(string)** constructor will be used to deserialize the value type if it exists. - -### array type - -Any List, Queue, Stack, Array, Collection, Enumerables including custom enumerable types are stored in exactly the same way as a JavaScript array literal, i.e: - - [1,2,3,4,5] - -All elements in an array must be of the same type. If a custom type is both an IEnumerable and has properties it will be treated as an array and the extra properties will be ignored. - -### object type - -The JSON object type is the most flexible and is how most complex .NET types are serialized. The JSON object type is a key-value pair JavaScript object literal where the key is always a double-quoted string. - -Any IDictionary is serialized into a standard JSON object, i.e: - - {"A":1,"B":2,"C":3,"D":4} - -Which happens to be the same as C# POCO types (inc. Interfaces) with the values: - -`new MyClass { A=1, B=2, C=3, D=4 }` - - {"A":1,"B":2,"C":3,"D":4} - -Only public properties on reference types are serialized with the C# Property Name used for object key and the Property Value as the value. At the moment it is not possible to customize the Property Name. - -JsonSerializer also supports serialization of anonymous types in much the same way: - -`new { A=1, B=2, C=3, D=4 }` - - {"A":1,"B":2,"C":3,"D":4} - -### Parsing JSON Dates - -The default WCF Date that's returned in ServiceStack.Text can be converted with: - -```js -function todate (s) { - return new Date(parseFloat(/Date\(([^)]+)\)/.exec(s)[1])); -}; -``` - -Which if you're using the [servicestack-client](https://docs.servicestack.net/servicestack-client-umd) npm package can be resolved with: - -```ts -import { todate } from "servicestack-client"; -var date = todate(wcfDateString); -``` - -Or if using [ss-utils.js](https://docs.servicestack.net/ss-utils-js) that's built into ServiceStack: - -```js -var date = $.ss.todate(wcfDateString); -``` - -If you change ServiceStack.Text default serialization of Date to either use the ISO8601 date format: - -```csharp -JsConfig.DateHandler = DateHandler.ISO8601; -``` - -It can be parsed natively with: - -```js -new Date(dateString) -``` - -Likewise when configured to return: - -```csharp -JsConfig.DateHandler = DateHandler.UnixTimeMs; -``` - -It can also be converted natively with: - -```js -new Date(unixTimeMs) -``` - -## Global Default JSON Configuration - -The JSON/JSV and CSV serialization can be customized globally by configuring the `JsConfig` or type-specific `JsConfig` static classes with your preferred defaults. Global static configuration can be configured once on **Startup** using `JsConfig.Init()`, e.g: - -```csharp -JsConfig.Init(new Config { - DateHandler = DateHandler.ISO8601, - AlwaysUseUtc = true, - TextCase = TextCase.CamelCase, - ExcludeDefaultValues = true, -}); -``` - -The following is a list of `bool` options you can use to configure many popular preferences: - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameAlias
IncludeNullValuesinv
IncludeNullValuesInDictionariesinvid
IncludeDefaultEnumside
IncludePublicFieldsipf
IncludeTypeInfoiti
ExcludeTypeInfoeti
ExcludeDefaultValuesedv
ConvertObjectTypesIntoStringDictionarycotisd
TreatEnumAsIntegerteai
TryToParsePrimitiveTypeValuesttpptv
TryToParseNumericTypettpnt
ThrowOnDeserializationErrortode
EscapeUnicodeeu
EscapeHtmlCharsehc
PreferInterfacespi
SkipDateTimeConversionsdtc
AlwaysUseUtcauu
AssumeUtcau
AppendUtcOffsetauo
EscapeHtmlCharsehc
EscapeUnicodeeu
EmitCamelCaseNameseccn
EmitLowercaseUnderscoreNameselun
- -### DateHandler (dh) - - - - - - - - - - -
TimestampOffsetto
DCJSCompatibledcjsc
ISO8601iso8601
ISO8601DateOnlyiso8601do
ISO8601DateTimeiso8601dt
RFC1123rfc1123
UnixTimeut
UnixTimeMsutm
- -### TimeSpanHandler (tsh) - - - - -
DurationFormatdf
StandardFormatsf
- -### TextCase (tc) - - - - - - -
Defaultd
PascalCasepc
CamelCasecc
SnakeCasesc
- -### PropertyConvention (pc) - - - - -
Stricts
Lenientl
- -### Custom Config Scopes - -If you need to use different serialization settings from the global static defaults you can use `JsConfig.With()` to create a scoped configuration -using property initializers: - -```csharp -using (JsConfig.With(new Config { - TextCase == TextCase.CamelCase, - PropertyConvention = PropertyConvention.Lenient -})) -{ - return text.FromJson(); -} -``` - -#### Create Custom Scopes using String config - -You can also create a custom config scope from a string manually using `JsConfig.CreateScope()` where you can use the full config name or their aliases, e.g: - -```csharp -using (JsConfig.CreateScope("IncludeNullValues,EDV,dh:ut")) -{ - var json = dto.ToJson(); -} -``` - -This feature is used to provide a number of different [JSON customizations in ServiceStack Services](https://docs.servicestack.net/customize-json-responses). - -### Type Configuration - -If you can't change the definition of a Type (e.g. because its in the BCL), you can specify a custom serialization / -deserialization routine to use instead. E.g. here's how you can add support for `System.Drawing.Color` and customize how `Guid` and `TimeSpan` Types are serialized: - -```csharp -JsConfig.SerializeFn = c => c.ToString().Replace("Color ","").Replace("[","").Replace("]",""); -JsConfig.DeSerializeFn = System.Drawing.Color.FromName; - -JsConfig.SerializeFn = guid => guid.ToString("D"); -JsConfig.SerializeFn = time => - (time.Ticks < 0 ? "-" : "") + time.ToString("hh':'mm':'ss'.'fffffff"); -``` - -## Strict Parsing - -By default ServiceStack Serializers will try to deserialize as much as possible without error, if you prefer you can opt-in to stricter parsing with: - -```csharp -Env.StrictMode = true; -``` - -Where it will instead fail fast and throw Exceptions on deserialization errors. - -## Custom Serialization - -Although JsonSerializer is optimized for serializing .NET POCO types, it still provides some options to change the convention-based serialization routine. - -### Using Structs to Customize JSON - -This makes it possible to customize the serialization routine and provide an even more compact wire format. - -E.g. Instead of using a JSON object to represent a point - - { Width=20, Height=10 } - -You could use a struct and reduce it to just: - - "20x10" - -By overriding **ToString()** and providing a static **Size ParseJson()** method: - -```csharp -public struct Size -{ - public double Width { get; set; } - public double Height { get; set; } - - public override string ToString() - { - return Width + "x" + Height; - } - - public static Size ParseJson(string json) - { - var size = json.Split('x'); - return new Size { - Width = double.Parse(size[0]), - Height = double.Parse(size[1]) - }; - } -} -``` - -Which would change it to the more compact JSON output: - -```csharp - new Size { Width = 20, Height = 10 }.ToJson() // = "20x10" -``` - -That allows you to deserialize it back in the same way: - -```csharp - var size = "20x10".FromJson(); -``` - -### Using Custom IEnumerable class to serialize a JSON array - -In addition to using a Struct you can optionally use a custom C# IEnumerable type to provide a strong-typed wrapper around a JSON array: - -```csharp -public class Point : IEnumerable -{ - double[] points = new double[2]; - - public double X - { - get { return points[0]; } - set { points[0] = value; } - } - - public double Y - { - get { return points[1]; } - set { points[1] = value; } - } - - public IEnumerator GetEnumerator() - { - foreach (var point in points) - yield return point; - } -} -``` - -Which serializes the Point into a compact JSON array: - -```csharp - new Point { X = 1, Y = 2 }.ToJson() // = [1,2] -``` - -## Custom Deserialization - -Because the same wire format shared between Dictionaries, POCOs and anonymous types, in most cases what you serialize with one type can be deserialized with another, i.e. an Anonymous type can be deserialized back into a Dictionary which can be deserialized into a strong-typed POCO and vice-versa. - -Although the JSON Serializer is best optimized for serializing and deserializing .NET types, it's flexible enough to consume 3rd party JSON apis although this generally requires custom de-serialization to convert it into an idiomatic .NET type. - -[GitHubRestTests.cs](https://github.com/ServiceStack/ServiceStack.Text/blob/master/tests/ServiceStack.Text.Tests/UseCases/GitHubRestTests.cs) - - 1. Using [JsonObject](https://github.com/ServiceStack/ServiceStack.Text/blob/master/src/ServiceStack.Text/JsonObject.cs) - 2. Using Generic .NET Collection classes - 3. Using Customized DTO's in the shape of the 3rd party JSON response - -[CentroidTests](https://github.com/ServiceStack/ServiceStack.Text/blob/master/tests/ServiceStack.Text.Tests/UseCases/CentroidTests.cs) is another example that uses the JsonObject to parse a complex custom JSON response. - -## Late-bound Object and Interface Runtime Types - -In order to be able to deserialize late-bound objects like `object`, `interface` properties or `abstract` classes ServiceStack needs to emit type information -in the JSON payload. By default it uses `__type` property name, but can be customized with: - -```csharp -JsConfig.TypeAttr = "$type"; -``` - -You can also configure what Type Information is emitted with: - -```csharp -JsConfig.TypeWriter = type => type.Name; -``` - -Which will just emit the name of the Type (e.g `Dog`) instead of the full Type Name. - -By default ServiceStack will scan all loaded Assemblies to find the Type, but you can tell it to use your own Type Resolver implementation by overriding `TypeFinder`, e.g: - -```csharp -JsConfig.TypeFinder = typeInfo => => -{ - var regex = new Regex(@"^(?[^:]+):#(?.*)$"); - var match = regex.Match(value); - var typeName = string.Format("{0}.{1}", match.Groups["namespace"].Value, match.Groups["type"].Value.Replace(".", "+")); - return MyFindType(typeName); -}; -``` - -### Runtime Type Whitelist - -ServiceStack only allows you to serialize "known safe Types" in late-bound properties which uses a whitelist that's pre-populated with a safe-list of -popular Data Types, DTOs and Request DTOs with the default configuration below: - -```csharp -// Allow deserializing types with [DataContract] or [RuntimeSerializable] attributes -JsConfig.AllowRuntimeTypeWithAttributesNamed = new HashSet -{ - nameof(DataContractAttribute), - nameof(RuntimeSerializableAttribute), // new in ServiceStack.Text -}; - -// Allow deserializing types implementing any of the interfaces below -JsConfig.AllowRuntimeTypeWithInterfacesNamed = new HashSet -{ - "IConvertible", - "ISerializable", - "IRuntimeSerializable", // new in ServiceStack.Text - "IMeta", - "IReturn`1", - "IReturnVoid", -}; - -// Allow object property in ServiceStack.Messaging MQ classes -JsConfig.AllowRuntimeTypeInTypesWithNamespaces = new HashSet -{ - "ServiceStack.Messaging", -}; -``` - -The above rules can be extended to allow your own conventions. If you just need to allow a specific Type you can instead just implement: - -```csharp -JsConfig.AllowRuntimeType = type => type == typeof(MyType); -``` - -If you’re in a trusted intranet environment this can also be used to disable the whitelist completely by allowing all Types to be deserialized into object properties with: - -```csharp -JsConfig.AllowRuntimeType = _ => true; -``` - -### Custom Enum Serialization - -You can use `[EnumMember]` to change what Enum value gets serialized, e.g: - -```csharp -[DataContract] -public enum Day -{ - [EnumMember(Value = "MON")] - Monday, - [EnumMember(Value = "TUE")] - Tuesday, - [EnumMember(Value = "WED")] - Wednesday, - [EnumMember(Value = "THU")] - Thursday, - [EnumMember(Value = "FRI")] - Friday, - [EnumMember(Value = "SAT")] - Saturday, - [EnumMember(Value = "SUN")] - Sunday, -} - -class EnumMemberDto -{ - public Day Day { get; set; } -} - -var dto = new EnumMemberDto {Day = Day.Sunday}; -var json = dto.ToJson(); //= {"Day":"SUN"} - -var fromDto = json.FromJson(); -fromDto.Day //= Day.Sunday -``` - -## TypeSerializer Details (JSV Format) - -Out of the box .NET provides a fairly quick but verbose Xml DataContractSerializer or a slightly more compact but slower JsonDataContractSerializer. -Both of these options are fragile and likely to break with any significant schema changes. -TypeSerializer addresses these shortcomings by being both smaller and significantly faster than the most popular options. -It's also more resilient, e.g. a strongly-typed POCO object can be deserialized back into a loosely-typed string Dictionary and vice-versa. - -With that in mind, TypeSerializer's main features are: - - - Fastest and most compact text-serializer for .NET - - Human readable and writeable, self-describing text format - - Non-invasive and configuration-free - - Resilient to schema changes - - Serializes / De-serializes any .NET data type (by convention) - + Supports custom, compact serialization of structs by overriding `ToString()` and `static T Parse(string)` methods - + Can serialize inherited, interface or 'late-bound objects' data types - + Respects opt-in DataMember custom serialization for DataContract dto types. - -These characteristics make it ideal for use anywhere you need to store or transport .NET data-types, e.g. for text blobs in a ORM, data in and out of a key-value store or as the text-protocol in .NET to .NET web services. - -As such, it's utilized within ServiceStack's other components: - - OrmLite - to store complex types on table models as text blobs in a database field and - - [ServiceStack.Redis](https://github.com/ServiceStack/ServiceStack.Redis) - to store rich POCO data types into the very fast [redis](http://redis.io) instances. - -You may also be interested in the very useful [T.Dump() extension method](https://github.com/ServiceStackV3/mythz_blog/blob/master/pages/202.md) for recursively viewing the contents of any C# POCO Type. - ---- - -# Performance -Type Serializer is actually the fastest and most compact *text serializer* available for .NET. -Out of all the serializers benchmarked, it is the only one to remain competitive with [protobuf-net's](http://code.google.com/p/protobuf-net/) very fast implementation of [Protocol Buffers](http://code.google.com/apis/protocolbuffers/) - google's high-speed binary protocol. - -Below is a series of benchmarks serialize the different tables in the [Northwind database](https://github.com/ServiceStackV3/ServiceStack.Benchmarks/blob/master/tests/ServiceStack.Northwind.Tests/Support/NorthwindData.cs) (3202 records) with the most popular serializers available for .NET: - -### Combined results for serializing / deserialzing a single row of each table in the Northwind database 1,000,000 times -_[view the detailed benchmarks](http://mono.servicestack.net/benchmarks/)_ - - - - - - - - - - - - - - - - - -
SerializerSizePeformance
Microsoft DataContractSerializer4.68x6.72x
Microsoft JsonDataContractSerializer2.24x10.18x
Microsoft BinaryFormatter5.62x9.06x
NewtonSoft.Json2.30x8.15x
ProtoBuf.net1x1x
ServiceStack TypeSerializer1.78x1.92x
- -_number of times larger in size and slower in performance than the best - lower is better_ - -Microsoft's JavaScriptSerializer was also benchmarked but excluded as it was up to 280x times slower - basically don't use it, ever. - - -# JSV Text Format (JSON + CSV) - -Type Serializer uses a hybrid CSV-style escaping + JavaScript-like text-based format that is optimized for both size and speed. I'm naming this JSV-format (i.e. JSON + CSV) - -In many ways it is similar to JavaScript, e.g. any List, Array, Collection of ints, longs, etc are stored in exactly the same way, i.e: - - [1,2,3,4,5] - -Any IDictionary is serialized like JavaScript, i.e: - - {A:1,B:2,C:3,D:4} - -Which also happens to be the same as C# POCO class with the values - -`new MyClass { A=1, B=2, C=3, D=4 }` - - {A:1,B:2,C:3,D:4} - -JSV is *white-space significant*, which means normal string values can be serialized without quotes, e.g: - -`new MyClass { Foo="Bar", Greet="Hello World!"}` is serialized as: - - {Foo:Bar,Greet:Hello World!} - - -### CSV escaping - -Any string with any of the following characters: `[]{},"` -is escaped using CSV-style escaping where the value is wrapped in double quotes, e.g: - -`new MyClass { Name = "Me, Junior" }` is serialized as: - - {Name:"Me, Junior"} - -A value with a double quote is escaped with another double quote e.g: - -`new MyClass { Size = "2\" x 1\"" }` is serialized as: - - {Size:"2"" x 1"""} - - -## Rich support for resilience and schema versioning -To better illustrate the resilience of `TypeSerializer` and the JSV Format check out a real world example of it when it's used to [Painlessly migrate between old and new types in Redis](https://github.com/ServiceStack/ServiceStack.Redis/wiki/MigrationsUsingSchemalessNoSql). - -Support for dynamic payloads and late-bound objects is explained in the post [Versatility of JSV Late-bound objects](http://www.servicestack.net/mythz_blog/?p=314). - - -# Community Resources - - - [ServiceStack.Text has nice extension method called Dump and has a few friends - web archive](https://web.archive.org/web/20150318193203/http://blogs.lessthandot.com/index.php/desktopdev/mstech/servicestack-text-has-a-nice/) by [@chrissie1](https://twitter.com/chrissie1) - - [JSON.NET vs ServiceStack - web archive](https://web.archive.org/web/20140721081359/http://daniel.wertheim.se/2011/02/07/json-net-vs-servicestack/) - - [GithubSharp with ServiceStack.Text](https://github.com/xamarin/GithubSharp/tree/servicestack) by [@XTZGZoReX](https://twitter.com/XTZGZoReX) +# Read ServiceStack.Text Docs at [docs.servicestack.net/text](https://docs.servicestack.net/text/) From e7535a32eb04455ae74dfdefd1a32a1bbec690ad Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Wed, 2 Feb 2022 14:00:58 +0800 Subject: [PATCH 70/75] Update PCL Platform names --- src/ServiceStack.Text/PclExport.NetCore.cs | 6 +++--- .../{PclExport.Net45.cs => PclExport.NetFx.cs} | 8 ++++---- src/ServiceStack.Text/PclExport.cs | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) rename src/ServiceStack.Text/{PclExport.Net45.cs => PclExport.NetFx.cs} (99%) diff --git a/src/ServiceStack.Text/PclExport.NetCore.cs b/src/ServiceStack.Text/PclExport.NetCore.cs index b12bee3be..7cf1fc8f3 100644 --- a/src/ServiceStack.Text/PclExport.NetCore.cs +++ b/src/ServiceStack.Text/PclExport.NetCore.cs @@ -6,11 +6,11 @@ namespace ServiceStack { - public class NetCorePclExport : NetStandardPclExport + public class Net6PclExport : NetStandardPclExport { - public NetCorePclExport() + public Net6PclExport() { - this.PlatformName = Platforms.NetCore; + this.PlatformName = Platforms.Net6; ReflectionOptimizer.Instance = EmitReflectionOptimizer.Provider; } diff --git a/src/ServiceStack.Text/PclExport.Net45.cs b/src/ServiceStack.Text/PclExport.NetFx.cs similarity index 99% rename from src/ServiceStack.Text/PclExport.Net45.cs rename to src/ServiceStack.Text/PclExport.NetFx.cs index 73bc305be..9df18991b 100644 --- a/src/ServiceStack.Text/PclExport.Net45.cs +++ b/src/ServiceStack.Text/PclExport.NetFx.cs @@ -23,11 +23,11 @@ namespace ServiceStack { - public class Net45PclExport : PclExport + public class NetFxPclExport : PclExport { - public static Net45PclExport Provider = new Net45PclExport(); + public static NetFxPclExport Provider = new NetFxPclExport(); - public Net45PclExport() + public NetFxPclExport() { this.DirSep = Path.DirectorySeparatorChar; this.AltDirSep = Path.DirectorySeparatorChar == '/' ? '\\' : '/'; @@ -37,7 +37,7 @@ public Net45PclExport() this.InvariantComparer = StringComparer.InvariantCulture; this.InvariantComparerIgnoreCase = StringComparer.InvariantCultureIgnoreCase; - this.PlatformName = Platforms.Net45; + this.PlatformName = Platforms.NetFX; ReflectionOptimizer.Instance = EmitReflectionOptimizer.Provider; } diff --git a/src/ServiceStack.Text/PclExport.cs b/src/ServiceStack.Text/PclExport.cs index fb957324f..9ba3c9cd1 100644 --- a/src/ServiceStack.Text/PclExport.cs +++ b/src/ServiceStack.Text/PclExport.cs @@ -21,18 +21,18 @@ public abstract class PclExport { public static class Platforms { - public const string NetStandard = "NETStandard"; - public const string NetCore = "NetCore"; - public const string Net45 = "Net45"; + public const string NetStandard = "NETStd"; + public const string Net6 = "NET6"; + public const string NetFX = "NETFX"; } public static PclExport Instance = #if NETFX - new Net45PclExport() + new NetFxPclExport() #elif NETSTANDARD2_0 new NetStandardPclExport() #elif NETCORE || NET6_0_OR_GREATER - new NetCorePclExport() + new Net6PclExport() #endif ; From f08f789a75a3850bb32a9cea567da8a450132ea2 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Wed, 2 Feb 2022 14:01:22 +0800 Subject: [PATCH 71/75] Make HttpUtils use lazy factories to prevent static init exceptions in Blazor --- src/ServiceStack.Text/HttpUtils.HttpClient.cs | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/ServiceStack.Text/HttpUtils.HttpClient.cs b/src/ServiceStack.Text/HttpUtils.HttpClient.cs index b2ed24252..a90a6d862 100644 --- a/src/ServiceStack.Text/HttpUtils.HttpClient.cs +++ b/src/ServiceStack.Text/HttpUtils.HttpClient.cs @@ -19,14 +19,14 @@ public static partial class HttpUtils private class HttpClientFactory { private readonly Lazy lazyHandler; - internal HttpClientFactory(HttpClientHandler handler) => - lazyHandler = new Lazy(() => handler, LazyThreadSafetyMode.ExecutionAndPublication); + internal HttpClientFactory(Func handler) => + lazyHandler = new Lazy(() => handler(), LazyThreadSafetyMode.ExecutionAndPublication); public HttpClient CreateClient() => new(lazyHandler.Value, disposeHandler: false); } // Ok to use HttpClientHandler which now uses SocketsHttpHandler // https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/HttpClientHandler.cs#L16 - public static HttpClientHandler HttpClientHandler { get; set; } = new() { + public static Func HttpClientHandlerFactory { get; set; } = () => new() { UseDefaultCredentials = true, AutomaticDecompression = DecompressionMethods.Brotli | DecompressionMethods.Deflate | DecompressionMethods.GZip, }; @@ -38,10 +38,21 @@ internal HttpClientFactory(HttpClientHandler handler) => // .Configure(options => // options.HttpMessageHandlerBuilderActions.Add(builder => builder.PrimaryHandler = HandlerFactory)) // .BuildServiceProvider().GetRequiredService(); - + // Escape & BYO IHttpClientFactory - private static HttpClientFactory clientFactory = new(HttpClientHandler); - public static Func CreateClient { get; set; } = () => clientFactory.CreateClient(); + private static HttpClientFactory? clientFactory; + public static Func CreateClient { get; set; } = () => { + try + { + clientFactory ??= new(HttpClientHandlerFactory); + return clientFactory.CreateClient(); + } + catch (Exception ex) + { + Tracer.Instance.WriteError(ex); + return new HttpClient(); + } + }; public static HttpClient Create() => CreateClient(); From 8261d3b965f6c181a8bd504ab0c17617c4c4924e Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Wed, 2 Feb 2022 14:01:28 +0800 Subject: [PATCH 72/75] Update Env.cs --- src/ServiceStack.Text/Env.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ServiceStack.Text/Env.cs b/src/ServiceStack.Text/Env.cs index 8cdbc2d22..003d2d40a 100644 --- a/src/ServiceStack.Text/Env.cs +++ b/src/ServiceStack.Text/Env.cs @@ -106,7 +106,8 @@ static Env() ServerUserAgent = "ServiceStack/" + VersionString + " " + PclExport.Instance.PlatformName - + (IsLinux ? "/Linux" : IsOSX ? "/OSX" : IsUnix ? "/Unix" : IsWindows ? "/Windows" : "/UnknownOS") + (IsIOS ? "/iOS" : IsAndroid ? "/Android" : IsUWP ? "/UWP" : "") + + (IsLinux ? "/Linux" : IsOSX ? "/macOS" : IsUnix ? "/Unix" : IsWindows ? "/Windows" : "/UnknownOS") + + (IsIOS ? "/iOS" : IsAndroid ? "/Android" : IsUWP ? "/UWP" : "") + (IsNet6 ? "/net6" : IsNetStandard20 ? "/std2.0" : IsNetFramework ? "/netfx" : "") + (IsMono ? "/Mono" : "") + $"/{LicenseUtils.Info}"; __releaseDate = new DateTime(2001,01,01); From 0c5b55c0154390b47b7ad0097c959badaf9d34d5 Mon Sep 17 00:00:00 2001 From: Chris Thames Date: Sat, 5 Feb 2022 18:54:59 -0600 Subject: [PATCH 73/75] Update DateTimeExtensions.cs DateTimeOffset (#537) Include DateTimeOffset arg to extension method ToUnixTimeMs. Made the assumption to use 'UtcDateTime' to use original DateTime implementation. Possibly a better way exists. Note 1: Due to `private static ToDateTimeSinceUnixEpoch` and `internal DateTimeSerializer.LocalTimeZone` had to re-implement to get it to work. Note 2: `ToUnixTimeMsAlt` should probably also have a DateTimeOffset implementation, but I did not implement. --- src/ServiceStack.Text/DateTimeExtensions.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/ServiceStack.Text/DateTimeExtensions.cs b/src/ServiceStack.Text/DateTimeExtensions.cs index 0373037f3..4c79c3792 100644 --- a/src/ServiceStack.Text/DateTimeExtensions.cs +++ b/src/ServiceStack.Text/DateTimeExtensions.cs @@ -45,6 +45,12 @@ public static long ToUnixTimeMsAlt(this DateTime dateTime) { return (dateTime.ToStableUniversalTime().Ticks - UnixEpoch) / TimeSpan.TicksPerMillisecond; } + + public static long ToUnixTimeMs(this DateTimeOffset dateTimeOffset) + { + var universal = ToDateTimeSinceUnixEpoch(dateTimeOffset); + return (long)universal.TotalMilliseconds; + } public static long ToUnixTimeMs(this DateTime dateTime) { @@ -56,6 +62,9 @@ public static long ToUnixTime(this DateTime dateTime) { return (dateTime.ToDateTimeSinceUnixEpoch().Ticks) / TimeSpan.TicksPerSecond; } + + private static TimeSpan ToDateTimeSinceUnixEpoch(this DateTimeOffset dateTimeOffset) + => ToDateTimeSinceUnixEpoch(dateTimeOffset.UtcDateTime); private static TimeSpan ToDateTimeSinceUnixEpoch(this DateTime dateTime) { From fb5396b5c8b227f54b9ff2e9dd33116c6f0fbda5 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Sun, 6 Feb 2022 09:07:10 +0800 Subject: [PATCH 74/75] Update DateTimeExtensions.cs collapse DateTimeOffset UnixTimeMs impls --- src/ServiceStack.Text/DateTimeExtensions.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/ServiceStack.Text/DateTimeExtensions.cs b/src/ServiceStack.Text/DateTimeExtensions.cs index 4c79c3792..23910f6ac 100644 --- a/src/ServiceStack.Text/DateTimeExtensions.cs +++ b/src/ServiceStack.Text/DateTimeExtensions.cs @@ -46,11 +46,8 @@ public static long ToUnixTimeMsAlt(this DateTime dateTime) return (dateTime.ToStableUniversalTime().Ticks - UnixEpoch) / TimeSpan.TicksPerMillisecond; } - public static long ToUnixTimeMs(this DateTimeOffset dateTimeOffset) - { - var universal = ToDateTimeSinceUnixEpoch(dateTimeOffset); - return (long)universal.TotalMilliseconds; - } + public static long ToUnixTimeMs(this DateTimeOffset dateTimeOffset) => + (long)ToDateTimeSinceUnixEpoch(dateTimeOffset.UtcDateTime).TotalMilliseconds; public static long ToUnixTimeMs(this DateTime dateTime) { @@ -62,9 +59,6 @@ public static long ToUnixTime(this DateTime dateTime) { return (dateTime.ToDateTimeSinceUnixEpoch().Ticks) / TimeSpan.TicksPerSecond; } - - private static TimeSpan ToDateTimeSinceUnixEpoch(this DateTimeOffset dateTimeOffset) - => ToDateTimeSinceUnixEpoch(dateTimeOffset.UtcDateTime); private static TimeSpan ToDateTimeSinceUnixEpoch(this DateTime dateTime) { From 6426a6f0e0788b3caf239d17b886fefd7c4e4592 Mon Sep 17 00:00:00 2001 From: Demis Bellot Date: Wed, 16 Feb 2022 12:39:17 +0800 Subject: [PATCH 75/75] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 33ef5488e..50e856543 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ Follow [@ServiceStack](https://twitter.com/servicestack), [view the docs](https://docs.servicestack.net), use [StackOverflow](https://stackoverflow.com/questions/ask?tags=servicestack,servicestack.redis) or [Customer Forums](https://forums.servicestack.net/) for support. # Read ServiceStack.Text Docs at [docs.servicestack.net/text](https://docs.servicestack.net/text/) + +### This repository [has moved](https://docs.servicestack.net/mono-repo) to [github.com/ServiceStack/ServiceStack/ServiceStack.Text](https://github.com/ServiceStack/ServiceStack/tree/main/ServiceStack.Text)