Dim/flopper experiments (#438)

* Adds object flopper for experimental object uploads

Implements a new mechanism for uploading objects to the server.

* wip

* pipe works

* WIP

* wip - refactors send pipeline
This commit is contained in:
Dimitrie Stefanescu
2026-02-10 10:17:38 +00:00
committed by GitHub
parent 12df19e431
commit 4980796cd6
16 changed files with 1413 additions and 79 deletions
+6 -3
View File
@@ -8,6 +8,8 @@
<PackageVersion Include="Glob" Version="1.1.9" />
<PackageVersion Include="HttpMultipartParser" Version="9.0.0" />
<PackageVersion Include="ILRepack.FullAuto" Version="1.6.0" />
<PackageVersion Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="10.0.1" />
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
<!-- Keep at exactly 7.0.5 for side by side with V2 -->
<PackageVersion Include="Microsoft.Data.Sqlite" Version="[7.0.5,)" />
@@ -16,7 +18,6 @@
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="[2.2.0,)" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="[2.2.0,)" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="[2.2.0,)" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="[5.0.0,)" />
<PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="Newtonsoft.Json.Schema" Version="4.0.1" />
<PackageVersion Include="Open.ChannelExtensions" Version="9.1.0" />
@@ -28,7 +29,9 @@
<PackageVersion Include="Speckle.DoubleNumerics" Version="4.1.0" />
<PackageVersion Include="SimpleExec" Version="12.0.0" />
<PackageVersion Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
<PackageVersion Include="System.Threading.Channels" Version="9.0.4" />
<PackageVersion Include="System.IO.Pipelines" Version="10.0.1" />
<PackageVersion Include="System.Net.Http.Json" Version="10.0.1" />
<PackageVersion Include="System.Threading.Channels" Version="10.0.1" />
<PackageVersion Include="Verify.Quibble" Version="2.1.1" />
<PackageVersion Include="Verify.Xunit" Version="29.4.0" />
<PackageVersion Include="System.Text.Json" Version="8.0.5" />
@@ -39,4 +42,4 @@
<GlobalPackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<GlobalPackageReference Include="Speckle.InterfaceGenerator" Version="0.9.6" />
</ItemGroup>
</Project>
</Project>
+180 -23
View File
@@ -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=="
}
}
}
+17 -17
View File
@@ -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",
@@ -17,7 +17,7 @@ public enum DynamicBaseMemberType
Dynamic = 2,
/// <summary>
/// The typed members flagged with <see cref="ObsoleteAttribute"/> attribute.
/// The typed members flagged with ObsoleteAttribute attribute.
/// </summary>
Obsolete = 4,
@@ -27,12 +27,12 @@ public enum DynamicBaseMemberType
SchemaComputed = 16,
/// <summary>
/// All the typed members, including ones with <see cref="ObsoleteAttribute"/> attributes.
/// All the typed members, including ones with ObsoleteAttribute attributes.
/// </summary>
InstanceAll = Instance + Obsolete,
/// <summary>
/// All the members, including dynamic and instance members flagged with <see cref="ObsoleteAttribute"/> attributes
/// All the members, including dynamic and instance members flagged with ObsoleteAttribute attributes
/// </summary>
All = InstanceAll + Dynamic,
}
@@ -0,0 +1,97 @@
namespace Speckle.Sdk.Pipelines;
/// <summary>
/// Wraps a stream to report upload progress as bytes are read.
/// </summary>
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<int> 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<int> ReadAsync(Memory<byte> 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
}
+59
View File
@@ -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<ObjectReference> 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<string> 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();
}
+372
View File
@@ -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;
/// <summary>
/// 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 <see cref="Speckle.Sdk.Models.Base"/> and its derivatives
/// into serialized JSON structures along with associated metadata, closures, and references.
/// <para>Any reference objects coming through are being "passed through" serialized - they do not get double encoded.</para>
/// </summary>
public class Serializer
{
private readonly record struct PropertyInfo(string Name, object? Value, bool IsDetachable);
public IEnumerable<UploadItem> 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<string, int>, Base, string)>();
var rootClosures = new Dictionary<string, int>();
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<PropertyInfo> 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<DetachPropertyAttribute>(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<string, int> closures,
List<(Id, Json, Dictionary<string, int>, Base, string)> detachedObjects
)
{
var childClosures = new Dictionary<string, int>();
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<string, int> closures,
List<(Id, Json, Dictionary<string, int>, 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<string, int>();
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<string, int>();
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);
}
}
}
+175
View File
@@ -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<UploadItem> _channel;
private readonly Task<UploadResult> _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<UploadItem>(
new BoundedChannelOptions(1000) { FullMode = BoundedChannelFullMode.Wait }
);
_sendTask = SendLoopAsync();
}
public ValueTask PushAsync(UploadItem item, CancellationToken ct = default) => _channel.Writer.WriteAsync(item, ct);
public async Task<string> CompleteAsync()
{
_channel.Writer.Complete();
var result = await _sendTask.ConfigureAwait(false);
return result.IngestionId;
}
private async Task<UploadResult> 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<PresignedUploadResponse>(_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<ProcessUploadResponse>(_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; }
}
+83
View File
@@ -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<UploadItem> _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<UploadItem>(
// 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();
// }
@@ -2,7 +2,13 @@ using System.Text;
namespace Speckle.Sdk.Serialisation.V2.Send;
public sealed record BaseItem(Id Id, Json Json, bool NeedsStorage, Dictionary<Id, int>? Closures) : IHasByteSize
public sealed record BaseItem(
Id Id,
Json Json,
bool NeedsStorage,
Dictionary<Id, int>? Closures,
bool? IsReference = false
) : IHasByteSize
{
public int ByteSize { get; } = Encoding.UTF8.GetByteCount(Json.Value);
@@ -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<BaseItem> _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<BaseItem>(
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();
}
@@ -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<BaseItem> _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<BaseItem>(
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();
}
@@ -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)
@@ -35,7 +35,8 @@ public class SerializeProcessFactory(
IServerObjectManager serverObjectManager,
IProgress<ProgressArgs>? progress,
CancellationToken cancellationToken,
SerializeProcessOptions? options = null
SerializeProcessOptions? options = null,
ObjectFlopper? objectFlopper = null
) =>
new SerializeProcess(
progress,
+5 -1
View File
@@ -23,19 +23,23 @@
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="GraphQL.Client" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" />
<PackageReference Include="Microsoft.Data.Sqlite" />
<PackageReference Include="Speckle.DoubleNumerics" />
<PackageReference Include="Speckle.Newtonsoft.Json" />
<PackageReference Include="System.IO.Pipelines" />
<PackageReference Include="System.Net.Http.Json" />
<PackageReference Include="System.Threading.Channels" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net8.0'">
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" OverrideVersion="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" OverrideVersion="8.0.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
<PackageReference Include="Microsoft.CSharp" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Microsoft.Extensions.Logging" />
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Speckle.Sdk.Dependencies\Speckle.Sdk.Dependencies.csproj" />
+237 -20
View File
@@ -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"
}
}
}
}