diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 00000000..542428eb
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,14 @@
+# Coding standards, domain knowledge, and preferences that AI should follow
+
+## C# Coding Standards
+
+- Use the csharpier formatter for formatting C# code.
+- Use the .editorconfig file for code style settings.
+- Always use `var` when the type is obvious from the right side of the assignment.
+- Always add braces for `if`, `else`, `for`, `foreach`, `while`, and `do` statements, even if they are single-line statements.
+
+## Testing
+
+- Use xUnit for unit testing.
+- Use FluentAssertions for assertions in tests.
+- Use Moq for mocking dependencies in tests.
diff --git a/.github/git-commit-instructions.md b/.github/git-commit-instructions.md
new file mode 100644
index 00000000..048149c4
--- /dev/null
+++ b/.github/git-commit-instructions.md
@@ -0,0 +1,22 @@
+# Git Commit Instructions
+
+To ensure high-quality and consistent commits, please follow these guidelines:
+
+1. **Format your code**
+ - Run the `csharpier` formatter on all C# files before committing.
+ - Ensure your code adheres to the `.editorconfig` settings.
+
+2. **Write clear commit messages**
+ - Use the present tense ("Add feature" not "Added feature").
+ - Start with a short summary (max 72 characters), followed by a blank line and a detailed description if necessary.
+
+3. **Test your changes**
+ - Run all unit tests before committing.
+ - Add or update xUnit tests as needed.
+ - Use FluentAssertions for assertions and Moq for mocking in tests.
+
+4. **Review your changes**
+ - Double-check for accidental debug code or commented-out code.
+ - Ensure only relevant files are staged.
+
+Thank you for helping maintain code quality!
diff --git a/Speckle.Sdk.slnx b/Speckle.Sdk.slnx
index 722954ee..f5111e37 100644
--- a/Speckle.Sdk.slnx
+++ b/Speckle.Sdk.slnx
@@ -14,6 +14,8 @@
+
+
@@ -35,4 +37,4 @@
-
+
\ No newline at end of file
diff --git a/src/Speckle.Sdk/Common/NotNullExtensions.cs b/src/Speckle.Sdk/Common/NotNullExtensions.cs
index 7d07c704..b98216d5 100644
--- a/src/Speckle.Sdk/Common/NotNullExtensions.cs
+++ b/src/Speckle.Sdk/Common/NotNullExtensions.cs
@@ -95,4 +95,22 @@ public static class NotNullExtensions
}
return obj;
}
+
+ public static string NotNullOrWhiteSpace(
+ [NotNull] this string? value,
+ [CallerArgumentExpression(nameof(value))] string? paramName = null
+ )
+ {
+ if (value is null)
+ {
+ throw new ArgumentNullException(paramName ?? "Value is null");
+ }
+
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ throw new ArgumentException("Value cannot be empty or whitespace.", paramName);
+ }
+
+ return value;
+ }
}
diff --git a/src/Speckle.Sdk/SQLite/SQLiteJsonCacheManager.cs b/src/Speckle.Sdk/SQLite/SQLiteJsonCacheManager.cs
index 58710224..a2a39c4b 100644
--- a/src/Speckle.Sdk/SQLite/SQLiteJsonCacheManager.cs
+++ b/src/Speckle.Sdk/SQLite/SQLiteJsonCacheManager.cs
@@ -1,6 +1,7 @@
using System.Text;
using Microsoft.Data.Sqlite;
using Speckle.InterfaceGenerator;
+using Speckle.Sdk.Common;
using Speckle.Sdk.Dependencies;
namespace Speckle.Sdk.SQLite;
@@ -120,7 +121,10 @@ public sealed class SqLiteJsonCacheManager : ISqLiteJsonCacheManager
);
//This does an insert or ignores if already exists
- public void SaveObject(string id, string json) =>
+ public void SaveObject(string id, string json)
+ {
+ id.NotNullOrWhiteSpace();
+ json.NotNullOrWhiteSpace();
_pool.Use(
CacheOperation.InsertOrIgnore,
command =>
@@ -130,6 +134,7 @@ public sealed class SqLiteJsonCacheManager : ISqLiteJsonCacheManager
command.ExecuteNonQuery();
}
);
+ }
//This does an insert or replaces if already exists
public void UpdateObject(string id, string json) =>
@@ -148,29 +153,45 @@ public sealed class SqLiteJsonCacheManager : ISqLiteJsonCacheManager
CacheOperation.BulkInsertOrIgnore,
cmd =>
{
- CreateBulkInsert(cmd, items);
- return cmd.ExecuteNonQuery();
+ if (CreateBulkInsert(cmd, items))
+ {
+ cmd.ExecuteNonQuery();
+ }
}
);
- private void CreateBulkInsert(SqliteCommand cmd, IEnumerable<(string id, string json)> items)
+ private bool CreateBulkInsert(SqliteCommand cmd, IEnumerable<(string id, string json)> items)
{
StringBuilder sb = Pools.StringBuilders.Get();
- sb.AppendLine(CacheDbCommands.Commands[(int)CacheOperation.BulkInsertOrIgnore]);
- int i = 0;
- foreach (var (id, json) in items)
+ try
{
- sb.Append($"(@key{i}, @value{i}),");
- cmd.Parameters.AddWithValue($"@key{i}", id);
- cmd.Parameters.AddWithValue($"@value{i}", json);
- i++;
- }
- sb.Remove(sb.Length - 1, 1);
- sb.Append(';');
+ sb.AppendLine(CacheDbCommands.Commands[(int)CacheOperation.BulkInsertOrIgnore]);
+ int i = 0;
+ foreach (var (id, json) in items)
+ {
+ sb.Append($"(@key{i}, @value{i}),");
+ cmd.Parameters.AddWithValue($"@key{i}", id);
+ cmd.Parameters.AddWithValue($"@value{i}", json);
+ i++;
+ }
+
+ if (i == 0)
+ {
+ return false;
+ }
+
+ sb.Remove(sb.Length - 1, 1);
+ sb.Append(';');
#pragma warning disable CA2100
- cmd.CommandText = sb.ToString();
+ cmd.CommandText = sb.ToString();
#pragma warning restore CA2100
- Pools.StringBuilders.Return(sb);
+ }
+ finally
+ {
+ Pools.StringBuilders.Return(sb);
+ }
+
+ return true;
}
public bool HasObject(string objectId) =>
diff --git a/src/Speckle.Sdk/Serialisation/V2/Send/ClosureMath.cs b/src/Speckle.Sdk/Serialisation/V2/Send/ClosureMath.cs
new file mode 100644
index 00000000..fd72eaa7
--- /dev/null
+++ b/src/Speckle.Sdk/Serialisation/V2/Send/ClosureMath.cs
@@ -0,0 +1,58 @@
+namespace Speckle.Sdk.Serialisation.V2.Send;
+
+public static class ClosureMath
+{
+ public static void IncrementClosures(this Dictionary current, IEnumerable> child)
+ {
+ foreach (var closure in child)
+ {
+ if (current.TryGetValue(closure.Key, out var count))
+ {
+ current[closure.Key] = Math.Max(closure.Value, count) + 1;
+ }
+ else
+ {
+ current[closure.Key] = closure.Value + 1;
+ }
+ }
+ }
+
+ public static void MergeClosures(this Dictionary current, IEnumerable> child)
+ {
+ foreach (var closure in child)
+ {
+ if (current.TryGetValue(closure.Key, out var count))
+ {
+ current[closure.Key] = Math.Max(closure.Value, count);
+ }
+ else
+ {
+ current[closure.Key] = closure.Value;
+ }
+ }
+ }
+
+ public static void IncrementClosure(this Dictionary current, Id id)
+ {
+ if (current.TryGetValue(id, out var count))
+ {
+ current[id] = count + 1;
+ }
+ else
+ {
+ current[id] = 1;
+ }
+ }
+
+ public static void MergeClosure(this Dictionary current, Id id)
+ {
+ if (current.TryGetValue(id, out var count))
+ {
+ current[id] = count;
+ }
+ else
+ {
+ current[id] = 1;
+ }
+ }
+}
diff --git a/src/Speckle.Sdk/Serialisation/V2/Send/ObjectSerializer.cs b/src/Speckle.Sdk/Serialisation/V2/Send/ObjectSerializer.cs
index 5f85353a..6d3e091d 100644
--- a/src/Speckle.Sdk/Serialisation/V2/Send/ObjectSerializer.cs
+++ b/src/Speckle.Sdk/Serialisation/V2/Send/ObjectSerializer.cs
@@ -25,7 +25,6 @@ public partial interface IObjectSerializer : IDisposable;
public sealed class ObjectSerializer : IObjectSerializer
{
private HashSet