diff --git a/Directory.Packages.props b/Directory.Packages.props index 86c51244..6bf24322 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -8,6 +8,8 @@ + + @@ -16,7 +18,6 @@ - @@ -28,7 +29,9 @@ - + + + @@ -39,4 +42,4 @@ - + \ No newline at end of file diff --git a/src/Speckle.Objects/packages.lock.json b/src/Speckle.Objects/packages.lock.json index 17abca99..2c2e04f1 100644 --- a/src/Speckle.Objects/packages.lock.json +++ b/src/Speckle.Objects/packages.lock.json @@ -130,6 +130,19 @@ "resolved": "8.0.0", "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + }, + "Newtonsoft.Json.Bson": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", + "dependencies": { + "Newtonsoft.Json": "12.0.1" + } + }, "SQLitePCLRaw.bundle_e_sqlite3": { "type": "Transitive", "resolved": "2.1.4", @@ -162,8 +175,8 @@ }, "System.Buffers": { "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "AwarXzzoDwX6BgrhjoJsk6tUezZEozOT5Y9QKF94Gl4JK91I4PIIBkBco9068Y9/Dra8Dkbie99kXB8+1BaYKw==" + "resolved": "4.6.1", + "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw==" }, "System.ComponentModel.Annotations": { "type": "Transitive", @@ -172,18 +185,18 @@ }, "System.Memory": { "type": "Transitive", - "resolved": "4.5.3", - "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==", + "resolved": "4.6.3", + "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==", "dependencies": { - "System.Buffers": "4.4.0", - "System.Numerics.Vectors": "4.4.0", - "System.Runtime.CompilerServices.Unsafe": "4.5.2" + "System.Buffers": "4.6.1", + "System.Numerics.Vectors": "4.6.1", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" } }, "System.Numerics.Vectors": { "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ==" + "resolved": "4.6.1", + "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q==" }, "System.Reactive": { "type": "Transitive", @@ -205,8 +218,8 @@ }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", - "resolved": "4.5.3", - "contentHash": "3TIsJhD1EiiT0w2CcDMN/iSSwnNnsrnbzeVHSKkaEgV85txMprmuO+Yq2AdSbeVGcg28pdNDTPK87tJhX7VFHw==" + "resolved": "6.1.2", + "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" }, "System.Runtime.InteropServices.WindowsRuntime": { "type": "Transitive", @@ -216,26 +229,40 @@ "System.Runtime": "4.3.0" } }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.1", + "contentHash": "cVAka0o1rJJ5/De0pjNs7jcaZk5hUGf1HGzUyVmE2MEB1Vf0h/8qsWxImk1zjitCbeD2Avaq2P2+usdvqgbeVQ==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, "System.Threading.Tasks.Extensions": { "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", + "resolved": "4.6.3", + "contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==", "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "4.5.3" + "System.Runtime.CompilerServices.Unsafe": "6.1.2" } }, "speckle.sdk": { "type": "Project", "dependencies": { "GraphQL.Client": "[6.0.0, )", - "Microsoft.Bcl.AsyncInterfaces": "[5.0.0, )", + "Microsoft.AspNet.WebApi.Client": "[6.0.0, )", + "Microsoft.Bcl.AsyncInterfaces": "[10.0.1, )", "Microsoft.CSharp": "[4.7.0, )", "Microsoft.Data.Sqlite": "[7.0.5, )", "Microsoft.Extensions.DependencyInjection.Abstractions": "[2.2.0, )", "Microsoft.Extensions.Logging": "[2.2.0, )", "Speckle.DoubleNumerics": "[4.1.0, )", "Speckle.Newtonsoft.Json": "[13.0.2, )", - "Speckle.Sdk.Dependencies": "[1.0.0, )" + "Speckle.Sdk.Dependencies": "[1.0.0, )", + "System.IO.Pipelines": "[10.0.1, )", + "System.Net.Http.Json": "[10.0.1, )", + "System.Threading.Channels": "[10.0.1, )" } }, "speckle.sdk.dependencies": { @@ -252,13 +279,25 @@ "System.Reactive": "5.0.0" } }, + "Microsoft.AspNet.WebApi.Client": { + "type": "CentralTransitive", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "zXeWP03dTo67AoDHUzR+/urck0KFssdCKOC+dq7Nv1V2YbFh/nIg09L0/3wSvyRONEdwxGB/ssEGmPNIIhAcAw==", + "dependencies": { + "Newtonsoft.Json": "13.0.1", + "Newtonsoft.Json.Bson": "1.0.2", + "System.Memory": "4.5.5", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, "Microsoft.Bcl.AsyncInterfaces": { "type": "CentralTransitive", - "requested": "[5.0.0, )", - "resolved": "5.0.0", - "contentHash": "W8DPQjkMScOMTtJbPwmPyj9c3zYSFGawDW3jwlBOOsnY+EzZFLgNQ/UMkK35JmkNOVPdCyPr2Tw7Vv9N+KA3ZQ==", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "E1HSLkPHXEO30JEij2pWbOuzz1Z5ND4a5l7IP1T2RgQuE0a0NzEIvtO64RNy3Otn6PFezbT80cfm3M/Cgt70PA==", "dependencies": { - "System.Threading.Tasks.Extensions": "4.5.4" + "System.Threading.Tasks.Extensions": "4.6.3" } }, "Microsoft.CSharp": { @@ -306,6 +345,54 @@ "requested": "[13.0.2, )", "resolved": "13.0.2", "contentHash": "g1BejUZwax5PRfL6xHgLEK23sqHWOgOj9hE7RvfRRlN00AGt8GnPYt8HedSK7UB3HiRW8zCA9Pn0iiYxCK24BA==" + }, + "System.IO.Pipelines": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "26LbFXHKd7PmRnWlkjnYgmjd5B6HYVG+1MpTO25BdxTJnx6D0O16JPAC/S4YBqjtt4YpfGj1QO/Ss6SPMGEGQw==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3", + "System.Threading.Tasks.Extensions": "4.6.3" + } + }, + "System.Net.Http.Json": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "XGOWt78ccgO9esyNlCemlS2b8JZPrH85pk/MvdHJxp6KwwUY/GnDaw2fPpJa7lgotiTkWnXnhip+ULNKaP7a8A==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3", + "System.Text.Json": "10.0.1", + "System.Threading.Tasks.Extensions": "4.6.3" + } + }, + "System.Text.Json": { + "type": "CentralTransitive", + "requested": "[8.0.5, )", + "resolved": "10.0.1", + "contentHash": "EsgwDgU1PFqhrFA9l5n+RBu76wFhNGCEwu8ITrBNhjPP3MxLyklroU5GIF8o6JYpYg6T4KD/VICfMdgPAvNp5g==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "10.0.1", + "System.Buffers": "4.6.1", + "System.IO.Pipelines": "10.0.1", + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2", + "System.Text.Encodings.Web": "10.0.1", + "System.Threading.Tasks.Extensions": "4.6.3" + } + }, + "System.Threading.Channels": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "YRqU6Y2Cl6C+HrG5h1ftgKZ5VDTSA7j1wMKs5RtlauPeQ2EZ639Jt5aOFHdX3naP01hDDWFOWPApmNDVKwOpmg==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "10.0.1", + "System.Threading.Tasks.Extensions": "4.6.3" + } } }, "net8.0": { @@ -418,6 +505,19 @@ "resolved": "8.0.0", "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + }, + "Newtonsoft.Json.Bson": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", + "dependencies": { + "Newtonsoft.Json": "12.0.1" + } + }, "SQLitePCLRaw.bundle_e_sqlite3": { "type": "Transitive", "resolved": "2.1.4", @@ -455,8 +555,8 @@ }, "System.Memory": { "type": "Transitive", - "resolved": "4.5.3", - "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" }, "System.Reactive": { "type": "Transitive", @@ -468,16 +568,30 @@ "resolved": "4.5.1", "contentHash": "Zh8t8oqolRaFa9vmOZfdQm/qKejdqz0J9kr7o2Fu0vPeoH3BL1EOXipKWwkWtLT1JPzjByrF19fGuFlNbmPpiw==" }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.1", + "contentHash": "cVAka0o1rJJ5/De0pjNs7jcaZk5hUGf1HGzUyVmE2MEB1Vf0h/8qsWxImk1zjitCbeD2Avaq2P2+usdvqgbeVQ==" + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==" + }, "speckle.sdk": { "type": "Project", "dependencies": { "GraphQL.Client": "[6.0.0, )", + "Microsoft.AspNet.WebApi.Client": "[6.0.0, )", "Microsoft.Data.Sqlite": "[7.0.5, )", "Microsoft.Extensions.DependencyInjection.Abstractions": "[2.2.0, )", "Microsoft.Extensions.Logging": "[2.2.0, )", "Speckle.DoubleNumerics": "[4.1.0, )", "Speckle.Newtonsoft.Json": "[13.0.2, )", - "Speckle.Sdk.Dependencies": "[1.0.0, )" + "Speckle.Sdk.Dependencies": "[1.0.0, )", + "System.IO.Pipelines": "[10.0.1, )", + "System.Net.Http.Json": "[10.0.1, )", + "System.Threading.Channels": "[10.0.1, )" } }, "speckle.sdk.dependencies": { @@ -494,6 +608,18 @@ "System.Reactive": "5.0.0" } }, + "Microsoft.AspNet.WebApi.Client": { + "type": "CentralTransitive", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "zXeWP03dTo67AoDHUzR+/urck0KFssdCKOC+dq7Nv1V2YbFh/nIg09L0/3wSvyRONEdwxGB/ssEGmPNIIhAcAw==", + "dependencies": { + "Newtonsoft.Json": "13.0.1", + "Newtonsoft.Json.Bson": "1.0.2", + "System.Memory": "4.5.5", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, "Microsoft.Data.Sqlite": { "type": "CentralTransitive", "requested": "[7.0.5, )", @@ -533,6 +659,37 @@ "requested": "[13.0.2, )", "resolved": "13.0.2", "contentHash": "g1BejUZwax5PRfL6xHgLEK23sqHWOgOj9hE7RvfRRlN00AGt8GnPYt8HedSK7UB3HiRW8zCA9Pn0iiYxCK24BA==" + }, + "System.IO.Pipelines": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "26LbFXHKd7PmRnWlkjnYgmjd5B6HYVG+1MpTO25BdxTJnx6D0O16JPAC/S4YBqjtt4YpfGj1QO/Ss6SPMGEGQw==" + }, + "System.Net.Http.Json": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "XGOWt78ccgO9esyNlCemlS2b8JZPrH85pk/MvdHJxp6KwwUY/GnDaw2fPpJa7lgotiTkWnXnhip+ULNKaP7a8A==", + "dependencies": { + "System.Text.Json": "10.0.1" + } + }, + "System.Text.Json": { + "type": "CentralTransitive", + "requested": "[8.0.5, )", + "resolved": "10.0.1", + "contentHash": "EsgwDgU1PFqhrFA9l5n+RBu76wFhNGCEwu8ITrBNhjPP3MxLyklroU5GIF8o6JYpYg6T4KD/VICfMdgPAvNp5g==", + "dependencies": { + "System.IO.Pipelines": "10.0.1", + "System.Text.Encodings.Web": "10.0.1" + } + }, + "System.Threading.Channels": { + "type": "CentralTransitive", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "YRqU6Y2Cl6C+HrG5h1ftgKZ5VDTSA7j1wMKs5RtlauPeQ2EZ639Jt5aOFHdX3naP01hDDWFOWPApmNDVKwOpmg==" } } } diff --git a/src/Speckle.Sdk.Dependencies/packages.lock.json b/src/Speckle.Sdk.Dependencies/packages.lock.json index c664417c..1f256577 100644 --- a/src/Speckle.Sdk.Dependencies/packages.lock.json +++ b/src/Speckle.Sdk.Dependencies/packages.lock.json @@ -82,12 +82,12 @@ }, "System.Threading.Channels": { "type": "Direct", - "requested": "[9.0.4, )", - "resolved": "9.0.4", - "contentHash": "4qBn2H6/aXBpE/Pm3wY5yusY/pEvQz99NlWHrTUji0qCmOdbhhjaALcpmbfW2ksxlPM6i6S+QFLkpOQdyfeKYQ==", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "YRqU6Y2Cl6C+HrG5h1ftgKZ5VDTSA7j1wMKs5RtlauPeQ2EZ639Jt5aOFHdX3naP01hDDWFOWPApmNDVKwOpmg==", "dependencies": { - "Microsoft.Bcl.AsyncInterfaces": "9.0.4", - "System.Threading.Tasks.Extensions": "4.5.4" + "Microsoft.Bcl.AsyncInterfaces": "10.0.1", + "System.Threading.Tasks.Extensions": "4.6.3" } }, "ILRepack": { @@ -141,24 +141,24 @@ }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg==" + "resolved": "6.1.2", + "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" }, "System.Threading.Tasks.Extensions": { "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", + "resolved": "4.6.3", + "contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==", "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "4.5.3" + "System.Runtime.CompilerServices.Unsafe": "6.1.2" } }, "Microsoft.Bcl.AsyncInterfaces": { "type": "CentralTransitive", - "requested": "[5.0.0, )", - "resolved": "9.0.4", - "contentHash": "9VGI5kxIvrNG2mqLQZnUR6y/3fcnygD8eNpHR+CqfbnIXvea6nehnYknDKQTxZVPMpzpNca+7DxLBmpdB3q0Bw==", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "E1HSLkPHXEO30JEij2pWbOuzz1Z5ND4a5l7IP1T2RgQuE0a0NzEIvtO64RNy3Otn6PFezbT80cfm3M/Cgt70PA==", "dependencies": { - "System.Threading.Tasks.Extensions": "4.5.4" + "System.Threading.Tasks.Extensions": "4.6.3" } } }, @@ -229,9 +229,9 @@ }, "System.Threading.Channels": { "type": "Direct", - "requested": "[9.0.4, )", - "resolved": "9.0.4", - "contentHash": "4qBn2H6/aXBpE/Pm3wY5yusY/pEvQz99NlWHrTUji0qCmOdbhhjaALcpmbfW2ksxlPM6i6S+QFLkpOQdyfeKYQ==" + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "YRqU6Y2Cl6C+HrG5h1ftgKZ5VDTSA7j1wMKs5RtlauPeQ2EZ639Jt5aOFHdX3naP01hDDWFOWPApmNDVKwOpmg==" }, "ILRepack": { "type": "Transitive", diff --git a/src/Speckle.Sdk/Models/DynamicBaseMemberType.cs b/src/Speckle.Sdk/Models/DynamicBaseMemberType.cs index fc114380..b03c2579 100644 --- a/src/Speckle.Sdk/Models/DynamicBaseMemberType.cs +++ b/src/Speckle.Sdk/Models/DynamicBaseMemberType.cs @@ -17,7 +17,7 @@ public enum DynamicBaseMemberType Dynamic = 2, /// - /// The typed members flagged with attribute. + /// The typed members flagged with ObsoleteAttribute attribute. /// Obsolete = 4, @@ -27,12 +27,12 @@ public enum DynamicBaseMemberType SchemaComputed = 16, /// - /// All the typed members, including ones with attributes. + /// All the typed members, including ones with ObsoleteAttribute attributes. /// InstanceAll = Instance + Obsolete, /// - /// All the members, including dynamic and instance members flagged with attributes + /// All the members, including dynamic and instance members flagged with ObsoleteAttribute attributes /// All = InstanceAll + Dynamic, } diff --git a/src/Speckle.Sdk/Pipelines/ProgressStream.cs b/src/Speckle.Sdk/Pipelines/ProgressStream.cs new file mode 100644 index 00000000..ed62121c --- /dev/null +++ b/src/Speckle.Sdk/Pipelines/ProgressStream.cs @@ -0,0 +1,97 @@ +namespace Speckle.Sdk.Pipelines; + +/// +/// Wraps a stream to report upload progress as bytes are read. +/// +public sealed class ProgressStream : Stream +{ + private readonly Stream _innerStream; + private readonly long _totalBytes; + private readonly IProgress<(long BytesSent, long TotalBytes)>? _progress; + private long _bytesSent; + + public ProgressStream( + Stream innerStream, + long totalBytes, + IProgress<(long BytesSent, long TotalBytes)>? progress = null + ) + { + _innerStream = innerStream; + _totalBytes = totalBytes; + _progress = progress; + _bytesSent = 0; + } + + public override bool CanRead => _innerStream.CanRead; + public override bool CanSeek => _innerStream.CanSeek; + public override bool CanWrite => false; + public override long Length => _innerStream.Length; + + public override long Position + { + get => _innerStream.Position; + set => _innerStream.Position = value; + } + + public override int Read(byte[] buffer, int offset, int count) + { + int bytesRead = _innerStream.Read(buffer, offset, count); + ReportProgress(bytesRead); + return bytesRead; + } + + public override async Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + int bytesRead = await _innerStream +#if NET8_0_OR_GREATER + .ReadAsync(buffer.AsMemory(offset, count), cancellationToken) +#else + .ReadAsync(buffer, offset, count, cancellationToken) +#endif + .ConfigureAwait(false); + ReportProgress(bytesRead); + return bytesRead; + } + +#if NET8_0_OR_GREATER + public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + int bytesRead = await _innerStream.ReadAsync(buffer, cancellationToken).ConfigureAwait(false); + ReportProgress(bytesRead); + return bytesRead; + } +#endif + + private void ReportProgress(int bytesRead) + { + _bytesSent += bytesRead; + _progress?.Report((_bytesSent, _totalBytes)); + } + + public override void Flush() => _innerStream.Flush(); + + public override Task FlushAsync(CancellationToken cancellationToken) => _innerStream.FlushAsync(cancellationToken); + + public override long Seek(long offset, SeekOrigin origin) => _innerStream.Seek(offset, origin); + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _innerStream.Dispose(); + } + base.Dispose(disposing); + } + +#if NET8_0_OR_GREATER + public override async ValueTask DisposeAsync() + { + await _innerStream.DisposeAsync().ConfigureAwait(false); + await base.DisposeAsync().ConfigureAwait(false); + } +#endif +} diff --git a/src/Speckle.Sdk/Pipelines/SendPipeline.cs b/src/Speckle.Sdk/Pipelines/SendPipeline.cs new file mode 100644 index 00000000..4c580163 --- /dev/null +++ b/src/Speckle.Sdk/Pipelines/SendPipeline.cs @@ -0,0 +1,59 @@ +using Speckle.Sdk.Credentials; +using Speckle.Sdk.Models; +using Speckle.Sdk.Serialisation; + +namespace Speckle.Sdk.Pipelines; + +public record UploadItem(string Id, Json Json, string SpeckleType, ObjectReference Reference); + +public sealed class SendPipeline : IDisposable +{ + private readonly CancellationToken _cancellationToken; + private readonly Serializer _serializer = new(); + private readonly Uploader _uploader; + + public SendPipeline( + Account account, + string projectId, + string modelId, + string ingestionId, + CancellationToken cancellationToken + ) + { + _cancellationToken = cancellationToken; + _uploader = new Uploader(projectId, modelId, ingestionId, account.serverInfo.url, account.token, cancellationToken); + } + + private UploadItem _lastItem; + + public async Task Process(Base @base) + { + var results = _serializer.Serialize(@base).ToArray(); + var first = results.First(); + foreach (var item in results) + { + // we're not doing fire and forget here so that we get the backpressure from the uploader + await _uploader.PushAsync(item).ConfigureAwait(false); + } + + // NOTE: this is important to keep track of. When we serialze an object, we get back a list of objects, with the first one being the original root. + // In the case of the commit root object, this means the last object is not necessarily the root; we therefore need to manually track its existance here + // and ensure it's the last one through in the uploader's stream. See WaitForUpload down below. + _lastItem = first; + return first.Reference; + } + + public async Task WaitForUpload() + { + await _uploader.PushAsync(_lastItem).ConfigureAwait(false); + await _uploader.CompleteAsync().ConfigureAwait(false); + } + + public async Task WaitForUploadAndServerProcessing() + { + // TODO: in some way, wait for the server to process the upload and return the actual new version id + return await Task.FromResult("todo").ConfigureAwait(false); + } + + public void Dispose() => _uploader.Dispose(); +} diff --git a/src/Speckle.Sdk/Pipelines/Serializer.cs b/src/Speckle.Sdk/Pipelines/Serializer.cs new file mode 100644 index 00000000..ed072a50 --- /dev/null +++ b/src/Speckle.Sdk/Pipelines/Serializer.cs @@ -0,0 +1,372 @@ +using System.Collections; +using System.Reflection; +using Speckle.DoubleNumerics; +using Speckle.Newtonsoft.Json; +using Speckle.Sdk.Dependencies; +using Speckle.Sdk.Helpers; +using Speckle.Sdk.Models; +using Speckle.Sdk.Serialisation; + +namespace Speckle.Sdk.Pipelines; + +/// +/// Another serializer, cleaner and meaner. Provides methods for serializing Speckle objects into a format suitable for upload or storage. +/// This class handles the conversion of and its derivatives +/// into serialized JSON structures along with associated metadata, closures, and references. +/// Any reference objects coming through are being "passed through" serialized - they do not get double encoded. +/// +public class Serializer +{ + private readonly record struct PropertyInfo(string Name, object? Value, bool IsDetachable); + + public IEnumerable Serialize(Base root) + { + // Special case: if root is already an ObjectReference, serialize it verbatim + if (root is ObjectReference existingRef) + { + var uploadItem = ReferenceToUploadItem(existingRef); + yield return uploadItem; + yield break; + } + + var detachedObjects = new List<(Id, Json, Dictionary, Base, string)>(); + var rootClosures = new Dictionary(); + + var (rootId, rootJson) = SerializeBase(root, false, rootClosures, detachedObjects); + + var rootReference = new ObjectReference + { + referencedId = rootId.Value, + applicationId = root.applicationId, + closure = rootClosures.Count > 0 ? rootClosures : null, + }; + + yield return new UploadItem(rootId.Value, rootJson, root.speckle_type, rootReference); + + foreach (var (id, json, closures, baseObj, speckleType) in detachedObjects) + { + var reference = new ObjectReference + { + referencedId = id.Value, + applicationId = baseObj.applicationId, + closure = closures.Count > 0 ? closures : null, + }; + + yield return new UploadItem(id.Value, json, speckleType, reference); + } + } + + private IEnumerable ExtractProperties(Base baseObj) + { + var typedProperties = baseObj.GetInstanceMembers(); + foreach (var prop in typedProperties) + { + if (prop.Name == "id" || prop.Name.StartsWith("__")) + { + continue; + } + + if (prop.IsDefined(typeof(JsonIgnoreAttribute), false)) + { + continue; + } + + var value = prop.GetValue(baseObj); + var isDetachable = prop.GetCustomAttribute(true)?.Detachable ?? false; + + yield return new PropertyInfo(prop.Name, value, isDetachable); + } + + foreach (var propName in baseObj.DynamicPropertyKeys) + { + if (propName.StartsWith("__")) + { + continue; + } + + var value = baseObj[propName]; + +#pragma warning disable CA1866 + var isDetachable = propName.StartsWith("@"); +#pragma warning restore CA1866 + + yield return new PropertyInfo(propName, value, isDetachable); + } + } + + private (Id, Json) SerializeBase( + Base baseObj, + bool forceDetach, + Dictionary closures, + List<(Id, Json, Dictionary, Base, string)> detachedObjects + ) + { + var childClosures = new Dictionary(); + + var sb = Pools.StringBuilders.Get(); + try + { + using var stringWriter = new StringWriter(sb); + using var jsonWriter = new JsonTextWriter(stringWriter); + using var idWriter = new SerializerIdWriter(jsonWriter); + + idWriter.WriteStartObject(); + + foreach (var prop in ExtractProperties(baseObj)) + { + idWriter.WritePropertyName(prop.Name); + SerializeValue(prop.Value, idWriter, prop.IsDetachable, childClosures, detachedObjects); + } + + var (jsonForId, finalWriter) = idWriter.FinishIdWriter(); + var id = IdGenerator.ComputeId(jsonForId); + + finalWriter.WritePropertyName("id"); + finalWriter.WriteValue(id.Value); + + baseObj.id = id.Value; + + if ((forceDetach || childClosures.Count > 0) && childClosures.Count > 0) + { + finalWriter.WritePropertyName("__closure"); + finalWriter.WriteStartObject(); + foreach (var kvp in childClosures) + { + finalWriter.WritePropertyName(kvp.Key); + finalWriter.WriteValue(kvp.Value); + } + finalWriter.WriteEndObject(); + + foreach (var kvp in childClosures) + { + closures[kvp.Key] = closures.TryGetValue(kvp.Key, out var existing) ? existing + kvp.Value : kvp.Value; + } + } + + finalWriter.WriteEndObject(); + finalWriter.Flush(); + + var json = new Json(stringWriter.ToString()); + return (id, json); + } + finally + { + Pools.StringBuilders.Return(sb); + } + } + + private void SerializeValue( + object? value, + JsonWriter writer, + bool isDetachable, + Dictionary closures, + List<(Id, Json, Dictionary, Base, string)> detachedObjects + ) + { + if (value == null) + { + writer.WriteNull(); + return; + } + + switch (value) + { + case string s: + writer.WriteValue(s); + return; + case int i: + writer.WriteValue(i); + return; + case long l: + writer.WriteValue(l); + return; + case double d: + writer.WriteValue(d); + return; + case float f: + writer.WriteValue(f); + return; + case bool b: + writer.WriteValue(b); + return; + case Enum e: + writer.WriteValue((int)(object)e); + return; + case Guid g: + writer.WriteValue(g.ToString()); + return; + case DateTime dt: + writer.WriteValue(dt.ToString("o")); + return; + + case Matrix4x4 md: + writer.WriteStartArray(); + writer.WriteValue(md.M11); + writer.WriteValue(md.M12); + writer.WriteValue(md.M13); + writer.WriteValue(md.M14); + writer.WriteValue(md.M21); + writer.WriteValue(md.M22); + writer.WriteValue(md.M23); + writer.WriteValue(md.M24); + writer.WriteValue(md.M31); + writer.WriteValue(md.M32); + writer.WriteValue(md.M33); + writer.WriteValue(md.M34); + writer.WriteValue(md.M41); + writer.WriteValue(md.M42); + writer.WriteValue(md.M43); + writer.WriteValue(md.M44); + writer.WriteEndArray(); + return; + } + + // Handle ObjectReference before Base (since ObjectReference extends Base) + // This prevents double-serialization and properly propagates closures + if (value is ObjectReference objRef) + { + writer.WriteStartObject(); + writer.WritePropertyName("speckle_type"); + writer.WriteValue("reference"); + writer.WritePropertyName("referencedId"); + writer.WriteValue(objRef.referencedId); + writer.WriteEndObject(); + + // Propagate closure: add the referenced ID + closures[objRef.referencedId] = closures.TryGetValue(objRef.referencedId, out var existing) ? existing + 1 : 1; + + // Propagate nested closures from the ObjectReference.closure dictionary + if (objRef.closure != null) + { + foreach (var kvp in objRef.closure) + { + closures[kvp.Key] = closures.TryGetValue(kvp.Key, out var existingDepth) + ? existingDepth + kvp.Value + : kvp.Value; + } + } + + return; + } + + if (value is Base baseObj) + { + if (isDetachable) + { + var childClosures = new Dictionary(); + var (childId, childJson) = SerializeBase(baseObj, true, childClosures, detachedObjects); + + detachedObjects.Add((childId, childJson, childClosures, baseObj, baseObj.speckle_type)); + + writer.WriteStartObject(); + writer.WritePropertyName("speckle_type"); + writer.WriteValue("reference"); + writer.WritePropertyName("referencedId"); + writer.WriteValue(childId.Value); + writer.WriteEndObject(); + + closures[childId.Value] = closures.TryGetValue(childId.Value, out var existing) ? existing + 1 : 1; + + foreach (var kvp in childClosures) + { + closures[kvp.Key] = closures.TryGetValue(kvp.Key, out var existingDepth) + ? existingDepth + kvp.Value + : kvp.Value; + } + } + else + { + var inlineClosures = new Dictionary(); + var (_, inlineJson) = SerializeBase(baseObj, false, inlineClosures, detachedObjects); + + writer.WriteRawValue(inlineJson.Value); + + foreach (var kvp in inlineClosures) + { + closures[kvp.Key] = closures.TryGetValue(kvp.Key, out var existingDepth) + ? existingDepth + kvp.Value + : kvp.Value; + } + } + return; + } + + if (value is IDictionary dict) + { + writer.WriteStartObject(); + foreach (DictionaryEntry kvp in dict) + { + if (kvp.Key is not string key) + { + throw new ArgumentException("Dictionary keys must be strings"); + } + + writer.WritePropertyName(key); + SerializeValue(kvp.Value, writer, false, closures, detachedObjects); + } + writer.WriteEndObject(); + return; + } + + if (value is IEnumerable enumerable and not string) + { + writer.WriteStartArray(); + foreach (var item in enumerable) + { + SerializeValue(item, writer, isDetachable, closures, detachedObjects); + } + writer.WriteEndArray(); + return; + } + + writer.WriteValue(value.ToString()); + } + + private UploadItem ReferenceToUploadItem(ObjectReference existingRef) + { + var sb = Pools.StringBuilders.Get(); + try + { + using var stringWriter = new StringWriter(sb); + using var jsonWriter = new JsonTextWriter(stringWriter); + + jsonWriter.WriteStartObject(); + jsonWriter.WritePropertyName("speckle_type"); + jsonWriter.WriteValue("reference"); + jsonWriter.WritePropertyName("referencedId"); + jsonWriter.WriteValue(existingRef.referencedId); + jsonWriter.WritePropertyName("__closure"); + + if (existingRef.closure != null && existingRef.closure.Count > 0) + { + jsonWriter.WriteStartObject(); + foreach (var kvp in existingRef.closure) + { + jsonWriter.WritePropertyName(kvp.Key); + jsonWriter.WriteValue(kvp.Value); + } + jsonWriter.WriteEndObject(); + } + else + { + jsonWriter.WriteNull(); + } + + jsonWriter.WriteEndObject(); + jsonWriter.Flush(); + + var refJson = new Json(stringWriter.ToString()); + + return new UploadItem( + existingRef.referencedId, + refJson, + existingRef.speckle_type, + existingRef // Pass through the original ObjectReference + ); + } + finally + { + Pools.StringBuilders.Return(sb); + } + } +} diff --git a/src/Speckle.Sdk/Pipelines/Uploader.cs b/src/Speckle.Sdk/Pipelines/Uploader.cs new file mode 100644 index 00000000..ec8133a4 --- /dev/null +++ b/src/Speckle.Sdk/Pipelines/Uploader.cs @@ -0,0 +1,175 @@ +using System.IO.Compression; +using System.Net.Http.Headers; +using System.Net.Http.Json; +using System.Threading.Channels; + +namespace Speckle.Sdk.Pipelines; + +public sealed class Uploader : IDisposable +{ + private readonly string _projectId; + private readonly string _modelId; + private readonly string _ingestionId; + private readonly CancellationToken _cancellationToken; + private readonly HttpClient _client; + private readonly Channel _channel; + private readonly Task _sendTask; + + public Uploader( + string projectId, + string modelId, + string ingestionId, + string apiEndpoint, + string authToken, + CancellationToken cancellationToken + ) + { + _projectId = projectId; + _modelId = modelId; + _ingestionId = ingestionId; + _cancellationToken = cancellationToken; + + Uri apiBaseUrl = new(new(apiEndpoint), "/api/v1/"); + _client = new HttpClient { BaseAddress = apiBaseUrl, Timeout = TimeSpan.FromMinutes(30) }; + + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken); + + _channel = Channel.CreateBounded( + new BoundedChannelOptions(1000) { FullMode = BoundedChannelFullMode.Wait } + ); + + _sendTask = SendLoopAsync(); + } + + public ValueTask PushAsync(UploadItem item, CancellationToken ct = default) => _channel.Writer.WriteAsync(item, ct); + + public async Task CompleteAsync() + { + _channel.Writer.Complete(); + var result = await _sendTask.ConfigureAwait(false); + return result.IngestionId; + } + + private async Task SendLoopAsync() + { + // 1. Stream channel to temp file + string tempFilePath = Path.GetTempFileName(); + System.Diagnostics.Debug.WriteLine($"Temp file is at {tempFilePath}"); + try + { + long fileSizeBytes; + { + using var fileStream = new FileStream(tempFilePath, FileMode.Create, FileAccess.Write, FileShare.None); + using var gzip = new GZipStream(fileStream, CompressionLevel.Optimal); + using var writer = new StreamWriter(gzip); + await foreach (var item in _channel.Reader.ReadAllAsync(_cancellationToken).ConfigureAwait(false)) + { + await writer.WriteLineAsync($"{item.Id}\t{item.Json}\t{item.SpeckleType}").ConfigureAwait(false); + } + + await writer.FlushAsync().ConfigureAwait(false); + await gzip.FlushAsync(_cancellationToken).ConfigureAwait(false); + } + // fileStream.Flush(); + // fileStream.Close(); + fileSizeBytes = new FileInfo(tempFilePath).Length; + + // 2. Request presigned URL + var signUri = new Uri($"projects/{_projectId}/models/{_modelId}/uploads/sign", UriKind.Relative); + + var signResponse = await HttpClientExtensions + .PostAsJsonAsync(_client, signUri, _cancellationToken) + .ConfigureAwait(false); + signResponse.EnsureSuccessStatusCode(); + + var presignedUpload = + await signResponse.Content.ReadFromJsonAsync(_cancellationToken).ConfigureAwait(false) + ?? throw new InvalidOperationException("Failed to get presigned upload URL"); + + // 3. Upload to S3 + using var fileStreamUpload = new FileStream(tempFilePath, FileMode.Open, FileAccess.Read, FileShare.Read); + + Stream progressStream = fileStreamUpload; // TODO: wrap with progress stream + + using var streamContent = new StreamContent(progressStream); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); + streamContent.Headers.ContentLength = fileSizeBytes; + + using var uploadRequest = new HttpRequestMessage(HttpMethod.Put, new Uri(presignedUpload.Url, UriKind.Absolute)); + uploadRequest.Content = streamContent; + + using var s3Client = new HttpClient(); // NOTE: using a separate client for S3 as we DO NOT NEED THE AUTH HEADER, presigned url's don't work with it. + using var uploadResponse = await s3Client + .SendAsync(uploadRequest, HttpCompletionOption.ResponseHeadersRead, _cancellationToken) + .ConfigureAwait(false); + + uploadResponse.EnsureSuccessStatusCode(); + + // 4. Trigger processing + var processUri = new Uri($"projects/{_projectId}/models/{_modelId}/uploads/process", UriKind.Relative); + var processRequest = new ProcessUploadRequest { key = presignedUpload.Key, ingestionId = _ingestionId }; + + var processResponse = await HttpClientExtensions + .PostAsJsonAsync(_client, processUri, processRequest, _cancellationToken) + .ConfigureAwait(false); + processResponse.EnsureSuccessStatusCode(); + + var processResult = await processResponse + .Content.ReadFromJsonAsync(_cancellationToken) + .ConfigureAwait(false); + + if (processResult == null) + { + throw new InvalidOperationException("Failed to trigger upload processing"); + } + + return new UploadResult { IngestionId = processResult.ingestionId }; + } + catch (Exception ex) when (!ex.IsFatal()) + { + throw; + } + finally + { + // 5. Clean up temp file + if (File.Exists(tempFilePath)) + { + try + { + //File.Delete(tempFilePath); + } +#pragma warning disable CA1031 + catch +#pragma warning restore CA1031 + { + // Best effort + } + } + } + } + + public void Dispose() => _client.Dispose(); +} + +// DTOs +internal record PresignedUploadResponse +{ + public required string Url { get; init; } + public required string Key { get; init; } +} + +internal record ProcessUploadRequest +{ + public required string key { get; init; } + public required string ingestionId { get; init; } +} + +internal record ProcessUploadResponse +{ + public required string ingestionId { get; init; } +} + +internal record UploadResult +{ + public required string IngestionId { get; init; } +} diff --git a/src/Speckle.Sdk/Pipelines/Uploader_Old.cs b/src/Speckle.Sdk/Pipelines/Uploader_Old.cs new file mode 100644 index 00000000..fa8f73c6 --- /dev/null +++ b/src/Speckle.Sdk/Pipelines/Uploader_Old.cs @@ -0,0 +1,83 @@ +// using System.IO.Compression; +// using System.Net.Http.Headers; +// using System.Threading.Channels; +// +// namespace Speckle.Sdk.Pipeline; +// +// public sealed class UploaderOld : IDisposable +// { +// private readonly string _projectId; +// private readonly string _modelId; +// private readonly HttpClient _client; +// private readonly Channel _channel; +// private readonly Task _sendTask; +// +// public UploaderOld(string projectId, string modelId, string? authToken, string? apiEndpoint) +// { +// _projectId = projectId; +// _modelId = modelId; +// +// Uri apiBaseUrl = !string.IsNullOrEmpty(apiEndpoint) +// ? new Uri(apiEndpoint) +// // : new Uri("http://dimitries-macbook-pro.mermaid-emperor.ts.net/api/v1/"); +// : new Uri("http://zog.local:3000/api/v1/"); +// _client = new HttpClient { BaseAddress = apiBaseUrl, Timeout = TimeSpan.FromMinutes(10) }; +// +// if (authToken != null) +// { +// _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken); +// } +// +// _channel = Channel.CreateBounded( +// new BoundedChannelOptions(1000) { FullMode = BoundedChannelFullMode.Wait } // if we're not able to write fast enough, we'll block writes +// ); +// +// _sendTask = SendLoopAsync(projectId, "test"); +// } +// +// public ValueTask PushAsync(UploadItem item, CancellationToken ct = default) => _channel.Writer.WriteAsync(item, ct); +// +// public async Task CompleteAsync() +// { +// _channel.Writer.Complete(); +// await _sendTask.ConfigureAwait(false); +// } +// +// private async Task SendLoopAsync(string projectId, string modelId) +// { +// var content = new PushStreamContent( +// async (stream, _, _) => +// { +// var gzip = new GZipStream(stream, CompressionLevel.Optimal); +// var writer = new StreamWriter(gzip); // new StreamWriter(gzip, System.Text.Encoding.UTF8, 20 * 1024 * 1024); // potential lever for controlling memory pressure +// try +// { +// // extra levers for memory pressure in here: we can manually flush every x items or every x bytes +// await foreach (var item in _channel.Reader.ReadAllAsync().ConfigureAwait(false)) +// { +// await writer.WriteLineAsync($"{item.Id}\t{item.Json}\t{item.SpeckleType}").ConfigureAwait(false); +// } +// } +// finally +// { +// await writer.FlushAsync().ConfigureAwait(false); +// await gzip.FlushAsync().ConfigureAwait(false); +// writer.Dispose(); +// gzip.Dispose(); +// } +// }, +// new MediaTypeHeaderValue("application/x-ndjson") +// ); +// +// var uri = new Uri($"projects/{projectId}/models/{modelId}/versions", UriKind.Relative); +// var request = new HttpRequestMessage(HttpMethod.Post, uri) { Content = content }; +// request.Headers.TransferEncodingChunked = true; // NOTE: important for streaming to happen. +// var response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); +// response.EnsureSuccessStatusCode(); +// +// // Consume the response body to fully complete the request +// await response.Content.ReadAsStringAsync().ConfigureAwait(false); +// } +// +// public void Dispose() => _client.Dispose(); +// } diff --git a/src/Speckle.Sdk/Serialisation/V2/Send/BaseItem.cs b/src/Speckle.Sdk/Serialisation/V2/Send/BaseItem.cs index 2a88e3db..9c5b02bf 100644 --- a/src/Speckle.Sdk/Serialisation/V2/Send/BaseItem.cs +++ b/src/Speckle.Sdk/Serialisation/V2/Send/BaseItem.cs @@ -2,7 +2,13 @@ using System.Text; namespace Speckle.Sdk.Serialisation.V2.Send; -public sealed record BaseItem(Id Id, Json Json, bool NeedsStorage, Dictionary? Closures) : IHasByteSize +public sealed record BaseItem( + Id Id, + Json Json, + bool NeedsStorage, + Dictionary? Closures, + bool? IsReference = false +) : IHasByteSize { public int ByteSize { get; } = Encoding.UTF8.GetByteCount(Json.Value); diff --git a/src/Speckle.Sdk/Serialisation/V2/Send/ObjectFlopper.cs b/src/Speckle.Sdk/Serialisation/V2/Send/ObjectFlopper.cs new file mode 100644 index 00000000..2027a6e3 --- /dev/null +++ b/src/Speckle.Sdk/Serialisation/V2/Send/ObjectFlopper.cs @@ -0,0 +1,79 @@ +using System.IO.Compression; +using System.Net.Http.Headers; +using System.Threading.Channels; + +namespace Speckle.Sdk.Serialisation.V2.Send; + +#pragma warning disable CA1001 +public sealed class ObjectFlopper +#pragma warning restore CA1001 +{ + private readonly Uri _url; + private readonly string _streamId; + private readonly HttpClient _client; + private readonly Channel _channel; + private readonly Task _sendTask; + + public ObjectFlopper(Uri _, string streamId, string? authToken) + { + _streamId = streamId; + _url = new Uri("http://zog.local:3000/api/v1/"); + _client = new HttpClient { BaseAddress = _url, Timeout = TimeSpan.FromMinutes(10) }; + + if (authToken != null) + { + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken); + } + + _channel = Channel.CreateBounded( + new BoundedChannelOptions(1000) { FullMode = BoundedChannelFullMode.Wait } + ); + + _sendTask = SendLoopAsync(streamId, "test"); + } + + public ValueTask PushAsync(BaseItem item, CancellationToken ct = default) => _channel.Writer.WriteAsync(item, ct); + + public async Task CompleteAsync() + { + _channel.Writer.Complete(); + await _sendTask.ConfigureAwait(false); + } + + private async Task SendLoopAsync(string projectId, string modelId) + { + var content = new PushStreamContent( + async (stream, _, _) => + { + var gzip = new GZipStream(stream, CompressionLevel.Optimal); + var writer = new StreamWriter(gzip); //new StreamWriter(gzip, System.Text.Encoding.UTF8, 20 * 1024 * 1024); + try + { + await foreach (var item in _channel.Reader.ReadAllAsync().ConfigureAwait(false)) + { + await writer.WriteLineAsync($"{item.Id}\t{item.Json}").ConfigureAwait(false); + } + } + finally + { + await writer.FlushAsync().ConfigureAwait(false); + await gzip.FlushAsync().ConfigureAwait(false); + writer.Dispose(); + gzip.Dispose(); + } + }, + new MediaTypeHeaderValue("application/x-ndjson") + ); + + var uri = new Uri($"projects/{projectId}/models/{modelId}/versions", UriKind.Relative); + var request = new HttpRequestMessage(HttpMethod.Post, uri) { Content = content }; + request.Headers.TransferEncodingChunked = true; // NOTE: important for streaming to happen. + var response = await _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + // Consume the response body to fully complete the request + await response.Content.ReadAsStringAsync().ConfigureAwait(false); + } + + public void Dispose() => _client.Dispose(); +} diff --git a/src/Speckle.Sdk/Serialisation/V2/Send/ObjectFlopperGandalf.cs b/src/Speckle.Sdk/Serialisation/V2/Send/ObjectFlopperGandalf.cs new file mode 100644 index 00000000..abccd3a6 --- /dev/null +++ b/src/Speckle.Sdk/Serialisation/V2/Send/ObjectFlopperGandalf.cs @@ -0,0 +1,90 @@ +using System.IO.Compression; +using System.IO.Pipelines; +using System.Net.Http.Headers; +using System.Threading.Channels; + +namespace Speckle.Sdk.Serialisation.V2.Send; + +#pragma warning disable CA1001 +public sealed class ObjectFlopperGandalf +#pragma warning restore CA1001 +{ + private readonly Uri _url; + private readonly string _streamId; + private readonly HttpClient _client; + private readonly Channel _channel; + private readonly Task _sendTask; + + public ObjectFlopperGandalf(Uri _, string streamId, string? authToken) + { + _streamId = streamId; + _url = new Uri("http://bender-2.local:3000/api/v1/"); + _client = new HttpClient { BaseAddress = _url, Timeout = TimeSpan.FromMinutes(10) }; + + if (authToken != null) + { + _client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", authToken); + } + + _channel = Channel.CreateBounded( + new BoundedChannelOptions(1000) { FullMode = BoundedChannelFullMode.Wait } + ); + + _sendTask = SendLoopAsync(streamId, "test"); + } + + public ValueTask PushAsync(BaseItem item, CancellationToken ct = default) => _channel.Writer.WriteAsync(item, ct); + + public async Task CompleteAsync() + { + _channel.Writer.Complete(); + await _sendTask.ConfigureAwait(false); + } + + private async Task SendLoopAsync(string projectId, string modelId) + { + var pipe = new Pipe(); + + // Start writing to pipe immediately in background + var writeTask = Task.Run(async () => + { + var gzip = new GZipStream(pipe.Writer.AsStream(), CompressionLevel.Optimal); + var writer = new StreamWriter(gzip); + + try + { + await foreach (var item in _channel.Reader.ReadAllAsync().ConfigureAwait(false)) + { + await writer.WriteLineAsync($"{item.Id}\t{item.Json}").ConfigureAwait(false); + await writer.FlushAsync().ConfigureAwait(false); + } + } + finally + { + await writer.FlushAsync().ConfigureAwait(false); + await gzip.FlushAsync().ConfigureAwait(false); + writer.Dispose(); + gzip.Dispose(); + await pipe.Writer.CompleteAsync().ConfigureAwait(false); + } + }); + + // Start HTTP request immediately, reading from pipe + var content = new StreamContent(pipe.Reader.AsStream()); + content.Headers.ContentType = new MediaTypeHeaderValue("application/x-ndjson"); + content.Headers.ContentEncoding.Add("gzip"); + + var uri = new Uri($"projects/{projectId}/models/{modelId}/objects", UriKind.Relative); + var request = new HttpRequestMessage(HttpMethod.Post, uri) { Content = content }; + + var responseTask = _client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead); + + // Wait for both + await Task.WhenAll(writeTask, responseTask).ConfigureAwait(false); + + var response = await responseTask.ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + } + + public void Dispose() => _client.Dispose(); +} diff --git a/src/Speckle.Sdk/Serialisation/V2/Send/SerializeProcess.cs b/src/Speckle.Sdk/Serialisation/V2/Send/SerializeProcess.cs index 6dac6f6f..29965525 100644 --- a/src/Speckle.Sdk/Serialisation/V2/Send/SerializeProcess.cs +++ b/src/Speckle.Sdk/Serialisation/V2/Send/SerializeProcess.cs @@ -113,16 +113,6 @@ public sealed class SerializeProcess( _processSource.Token ); var findTotalObjectsTask = Task.CompletedTask; - if (!options.SkipFindTotalObjects) - { - ThrowIfFailed(); - findTotalObjectsTask = Task.Factory.StartNew( - () => TraverseTotal(root), - _processSource.Token, - TaskCreationOptions.AttachedToParent | TaskCreationOptions.PreferFairness, - _highest - ); - } await Traverse(root).ConfigureAwait(false); ThrowIfFailed(); @@ -133,6 +123,7 @@ public sealed class SerializeProcess( ThrowIfFailed(); await WaitForSchedulerCompletion().ConfigureAwait(false); ThrowIfFailed(); + return new(root.id.NotNull(), baseSerializer.ObjectReferences.Freeze()); } catch (OperationCanceledException) diff --git a/src/Speckle.Sdk/Serialisation/V2/SerializeProcessFactory.cs b/src/Speckle.Sdk/Serialisation/V2/SerializeProcessFactory.cs index b708f4d4..3c56b4af 100644 --- a/src/Speckle.Sdk/Serialisation/V2/SerializeProcessFactory.cs +++ b/src/Speckle.Sdk/Serialisation/V2/SerializeProcessFactory.cs @@ -35,7 +35,8 @@ public class SerializeProcessFactory( IServerObjectManager serverObjectManager, IProgress? progress, CancellationToken cancellationToken, - SerializeProcessOptions? options = null + SerializeProcessOptions? options = null, + ObjectFlopper? objectFlopper = null ) => new SerializeProcess( progress, diff --git a/src/Speckle.Sdk/Speckle.Sdk.csproj b/src/Speckle.Sdk/Speckle.Sdk.csproj index 5dbc5924..664d497a 100644 --- a/src/Speckle.Sdk/Speckle.Sdk.csproj +++ b/src/Speckle.Sdk/Speckle.Sdk.csproj @@ -23,19 +23,23 @@ + + + + + - diff --git a/src/Speckle.Sdk/packages.lock.json b/src/Speckle.Sdk/packages.lock.json index 5ea95453..b7195702 100644 --- a/src/Speckle.Sdk/packages.lock.json +++ b/src/Speckle.Sdk/packages.lock.json @@ -13,13 +13,25 @@ "System.Reactive": "5.0.0" } }, + "Microsoft.AspNet.WebApi.Client": { + "type": "Direct", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "zXeWP03dTo67AoDHUzR+/urck0KFssdCKOC+dq7Nv1V2YbFh/nIg09L0/3wSvyRONEdwxGB/ssEGmPNIIhAcAw==", + "dependencies": { + "Newtonsoft.Json": "13.0.1", + "Newtonsoft.Json.Bson": "1.0.2", + "System.Memory": "4.5.5", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Direct", - "requested": "[5.0.0, )", - "resolved": "5.0.0", - "contentHash": "W8DPQjkMScOMTtJbPwmPyj9c3zYSFGawDW3jwlBOOsnY+EzZFLgNQ/UMkK35JmkNOVPdCyPr2Tw7Vv9N+KA3ZQ==", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "E1HSLkPHXEO30JEij2pWbOuzz1Z5ND4a5l7IP1T2RgQuE0a0NzEIvtO64RNy3Otn6PFezbT80cfm3M/Cgt70PA==", "dependencies": { - "System.Threading.Tasks.Extensions": "4.5.4" + "System.Threading.Tasks.Extensions": "4.6.3" } }, "Microsoft.CSharp": { @@ -99,6 +111,39 @@ "resolved": "13.0.2", "contentHash": "g1BejUZwax5PRfL6xHgLEK23sqHWOgOj9hE7RvfRRlN00AGt8GnPYt8HedSK7UB3HiRW8zCA9Pn0iiYxCK24BA==" }, + "System.IO.Pipelines": { + "type": "Direct", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "26LbFXHKd7PmRnWlkjnYgmjd5B6HYVG+1MpTO25BdxTJnx6D0O16JPAC/S4YBqjtt4YpfGj1QO/Ss6SPMGEGQw==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3", + "System.Threading.Tasks.Extensions": "4.6.3" + } + }, + "System.Net.Http.Json": { + "type": "Direct", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "XGOWt78ccgO9esyNlCemlS2b8JZPrH85pk/MvdHJxp6KwwUY/GnDaw2fPpJa7lgotiTkWnXnhip+ULNKaP7a8A==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3", + "System.Text.Json": "10.0.1", + "System.Threading.Tasks.Extensions": "4.6.3" + } + }, + "System.Threading.Channels": { + "type": "Direct", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "YRqU6Y2Cl6C+HrG5h1ftgKZ5VDTSA7j1wMKs5RtlauPeQ2EZ639Jt5aOFHdX3naP01hDDWFOWPApmNDVKwOpmg==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "10.0.1", + "System.Threading.Tasks.Extensions": "4.6.3" + } + }, "GraphQL.Client.Abstractions": { "type": "Transitive", "resolved": "6.0.0", @@ -196,6 +241,19 @@ "resolved": "8.0.0", "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + }, + "Newtonsoft.Json.Bson": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", + "dependencies": { + "Newtonsoft.Json": "12.0.1" + } + }, "SQLitePCLRaw.bundle_e_sqlite3": { "type": "Transitive", "resolved": "2.1.4", @@ -228,8 +286,8 @@ }, "System.Buffers": { "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "AwarXzzoDwX6BgrhjoJsk6tUezZEozOT5Y9QKF94Gl4JK91I4PIIBkBco9068Y9/Dra8Dkbie99kXB8+1BaYKw==" + "resolved": "4.6.1", + "contentHash": "N8GXpmiLMtljq7gwvyS+1QvKT/W2J8sNAvx+HVg4NGmsG/H+2k/y9QI23auLJRterrzCiDH+IWAw4V/GPwsMlw==" }, "System.ComponentModel.Annotations": { "type": "Transitive", @@ -238,18 +296,18 @@ }, "System.Memory": { "type": "Transitive", - "resolved": "4.5.3", - "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==", + "resolved": "4.6.3", + "contentHash": "qdcDOgnFZY40+Q9876JUHnlHu7bosOHX8XISRoH94fwk6hgaeQGSgfZd8srWRZNt5bV9ZW2TljcegDNxsf+96A==", "dependencies": { - "System.Buffers": "4.4.0", - "System.Numerics.Vectors": "4.4.0", - "System.Runtime.CompilerServices.Unsafe": "4.5.2" + "System.Buffers": "4.6.1", + "System.Numerics.Vectors": "4.6.1", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" } }, "System.Numerics.Vectors": { "type": "Transitive", - "resolved": "4.4.0", - "contentHash": "UiLzLW+Lw6HLed1Hcg+8jSRttrbuXv7DANVj0DkL9g6EnnzbL75EB7EWsw5uRbhxd/4YdG8li5XizGWepmG3PQ==" + "resolved": "4.6.1", + "contentHash": "sQxefTnhagrhoq2ReR0D/6K0zJcr9Hrd6kikeXsA1I8kOCboTavcUC4r7TSfpKFeE163uMuxZcyfO1mGO3EN8Q==" }, "System.Reactive": { "type": "Transitive", @@ -271,8 +329,8 @@ }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", - "resolved": "4.5.3", - "contentHash": "3TIsJhD1EiiT0w2CcDMN/iSSwnNnsrnbzeVHSKkaEgV85txMprmuO+Yq2AdSbeVGcg28pdNDTPK87tJhX7VFHw==" + "resolved": "6.1.2", + "contentHash": "2hBr6zdbIBTDE3EhK7NSVNdX58uTK6iHW/P/Axmm9sl1xoGSLqDvMtpecn226TNwHByFokYwJmt/aQQNlO5CRw==" }, "System.Runtime.InteropServices.WindowsRuntime": { "type": "Transitive", @@ -282,16 +340,75 @@ "System.Runtime": "4.3.0" } }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.1", + "contentHash": "cVAka0o1rJJ5/De0pjNs7jcaZk5hUGf1HGzUyVmE2MEB1Vf0h/8qsWxImk1zjitCbeD2Avaq2P2+usdvqgbeVQ==", + "dependencies": { + "System.Buffers": "4.6.1", + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, "System.Threading.Tasks.Extensions": { "type": "Transitive", - "resolved": "4.5.4", - "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==", + "resolved": "4.6.3", + "contentHash": "7sCiwilJLYbTZELaKnc7RecBBXWXA+xMLQWZKWawBxYjp6DBlSE3v9/UcvKBvr1vv2tTOhipiogM8rRmxlhrVA==", "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "4.5.3" + "System.Runtime.CompilerServices.Unsafe": "6.1.2" + } + }, + "speckle.connectors.common": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "[2.2.0, )", + "Speckle.Connectors.Logging": "[1.0.0, )", + "Speckle.Converters.Common": "[1.0.0, )", + "Speckle.Objects": "[1.0.0, )" + } + }, + "speckle.connectors.logging": { + "type": "Project" + }, + "speckle.converters.common": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", + "Speckle.Objects": "[1.0.0, )" + } + }, + "speckle.objects": { + "type": "Project", + "dependencies": { + "Speckle.Sdk": "[1.0.0, )" } }, "speckle.sdk.dependencies": { "type": "Project" + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "CentralTransitive", + "requested": "[2.2.0, )", + "resolved": "2.2.0", + "contentHash": "MZtBIwfDFork5vfjpJdG5g8wuJFt7d/y3LOSVVtDK/76wlbtz6cjltfKHqLx2TKVqTj5/c41t77m1+h20zqtPA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.2.0" + } + }, + "System.Text.Json": { + "type": "CentralTransitive", + "requested": "[8.0.5, )", + "resolved": "10.0.1", + "contentHash": "EsgwDgU1PFqhrFA9l5n+RBu76wFhNGCEwu8ITrBNhjPP3MxLyklroU5GIF8o6JYpYg6T4KD/VICfMdgPAvNp5g==", + "dependencies": { + "Microsoft.Bcl.AsyncInterfaces": "10.0.1", + "System.Buffers": "4.6.1", + "System.IO.Pipelines": "10.0.1", + "System.Memory": "4.6.3", + "System.Runtime.CompilerServices.Unsafe": "6.1.2", + "System.Text.Encodings.Web": "10.0.1", + "System.Threading.Tasks.Extensions": "4.6.3" + } } }, "net8.0": { @@ -306,6 +423,18 @@ "System.Reactive": "5.0.0" } }, + "Microsoft.AspNet.WebApi.Client": { + "type": "Direct", + "requested": "[6.0.0, )", + "resolved": "6.0.0", + "contentHash": "zXeWP03dTo67AoDHUzR+/urck0KFssdCKOC+dq7Nv1V2YbFh/nIg09L0/3wSvyRONEdwxGB/ssEGmPNIIhAcAw==", + "dependencies": { + "Newtonsoft.Json": "13.0.1", + "Newtonsoft.Json.Bson": "1.0.2", + "System.Memory": "4.5.5", + "System.Threading.Tasks.Extensions": "4.5.4" + } + }, "Microsoft.Data.Sqlite": { "type": "Direct", "requested": "[7.0.5, )", @@ -368,6 +497,27 @@ "resolved": "13.0.2", "contentHash": "g1BejUZwax5PRfL6xHgLEK23sqHWOgOj9hE7RvfRRlN00AGt8GnPYt8HedSK7UB3HiRW8zCA9Pn0iiYxCK24BA==" }, + "System.IO.Pipelines": { + "type": "Direct", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "26LbFXHKd7PmRnWlkjnYgmjd5B6HYVG+1MpTO25BdxTJnx6D0O16JPAC/S4YBqjtt4YpfGj1QO/Ss6SPMGEGQw==" + }, + "System.Net.Http.Json": { + "type": "Direct", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "XGOWt78ccgO9esyNlCemlS2b8JZPrH85pk/MvdHJxp6KwwUY/GnDaw2fPpJa7lgotiTkWnXnhip+ULNKaP7a8A==", + "dependencies": { + "System.Text.Json": "10.0.1" + } + }, + "System.Threading.Channels": { + "type": "Direct", + "requested": "[10.0.1, )", + "resolved": "10.0.1", + "contentHash": "YRqU6Y2Cl6C+HrG5h1ftgKZ5VDTSA7j1wMKs5RtlauPeQ2EZ639Jt5aOFHdX3naP01hDDWFOWPApmNDVKwOpmg==" + }, "GraphQL.Client.Abstractions": { "type": "Transitive", "resolved": "6.0.0", @@ -455,6 +605,19 @@ "resolved": "8.0.0", "contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw==" }, + "Newtonsoft.Json": { + "type": "Transitive", + "resolved": "13.0.1", + "contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==" + }, + "Newtonsoft.Json.Bson": { + "type": "Transitive", + "resolved": "1.0.2", + "contentHash": "QYFyxhaABwmq3p/21VrZNYvCg3DaEoN/wUuw5nmfAf0X3HLjgupwhkEWdgfb9nvGAUIv3osmZoD3kKl4jxEmYQ==", + "dependencies": { + "Newtonsoft.Json": "12.0.1" + } + }, "SQLitePCLRaw.bundle_e_sqlite3": { "type": "Transitive", "resolved": "2.1.4", @@ -492,8 +655,8 @@ }, "System.Memory": { "type": "Transitive", - "resolved": "4.5.3", - "contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA==" + "resolved": "4.5.5", + "contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw==" }, "System.Reactive": { "type": "Transitive", @@ -505,8 +668,62 @@ "resolved": "4.5.1", "contentHash": "Zh8t8oqolRaFa9vmOZfdQm/qKejdqz0J9kr7o2Fu0vPeoH3BL1EOXipKWwkWtLT1JPzjByrF19fGuFlNbmPpiw==" }, + "System.Text.Encodings.Web": { + "type": "Transitive", + "resolved": "10.0.1", + "contentHash": "cVAka0o1rJJ5/De0pjNs7jcaZk5hUGf1HGzUyVmE2MEB1Vf0h/8qsWxImk1zjitCbeD2Avaq2P2+usdvqgbeVQ==" + }, + "System.Threading.Tasks.Extensions": { + "type": "Transitive", + "resolved": "4.5.4", + "contentHash": "zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==" + }, + "speckle.connectors.common": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "[2.2.0, )", + "Speckle.Connectors.Logging": "[1.0.0, )", + "Speckle.Converters.Common": "[1.0.0, )", + "Speckle.Objects": "[1.0.0, )" + } + }, + "speckle.connectors.logging": { + "type": "Project" + }, + "speckle.converters.common": { + "type": "Project", + "dependencies": { + "Microsoft.Extensions.Logging.Abstractions": "[2.2.0, )", + "Speckle.Objects": "[1.0.0, )" + } + }, + "speckle.objects": { + "type": "Project", + "dependencies": { + "Speckle.Sdk": "[1.0.0, )" + } + }, "speckle.sdk.dependencies": { "type": "Project" + }, + "Microsoft.Extensions.DependencyInjection": { + "type": "CentralTransitive", + "requested": "[2.2.0, )", + "resolved": "2.2.0", + "contentHash": "MZtBIwfDFork5vfjpJdG5g8wuJFt7d/y3LOSVVtDK/76wlbtz6cjltfKHqLx2TKVqTj5/c41t77m1+h20zqtPA==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "2.2.0" + } + }, + "System.Text.Json": { + "type": "CentralTransitive", + "requested": "[8.0.5, )", + "resolved": "10.0.1", + "contentHash": "EsgwDgU1PFqhrFA9l5n+RBu76wFhNGCEwu8ITrBNhjPP3MxLyklroU5GIF8o6JYpYg6T4KD/VICfMdgPAvNp5g==", + "dependencies": { + "System.IO.Pipelines": "10.0.1", + "System.Text.Encodings.Web": "10.0.1" + } } } }