From 0666e9fb490d540c5692fa74f56298f7770ce42a Mon Sep 17 00:00:00 2001 From: Francesco Del Re Date: Mon, 16 Mar 2026 23:42:27 +0100 Subject: [PATCH 01/13] chore(deps): update project dependencies --- src/SharpConnector/SharpConnector.csproj | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/SharpConnector/SharpConnector.csproj b/src/SharpConnector/SharpConnector.csproj index ba8acaa..c2ea4c0 100755 --- a/src/SharpConnector/SharpConnector.csproj +++ b/src/SharpConnector/SharpConnector.csproj @@ -19,18 +19,18 @@ - + - - - - + + + + - + From 38a47d0918dcd41c174d250c7eaa6195b5e4a8ac Mon Sep 17 00:00:00 2001 From: Francesco Del Re Date: Thu, 19 Mar 2026 12:31:31 +0100 Subject: [PATCH 02/13] chore(deps): update project dependencies --- src/SharpConnector.Tests/SharpConnector.Tests.csproj | 2 +- src/SharpConnector/SharpConnector.csproj | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SharpConnector.Tests/SharpConnector.Tests.csproj b/src/SharpConnector.Tests/SharpConnector.Tests.csproj index 6e43463..c6cd50d 100644 --- a/src/SharpConnector.Tests/SharpConnector.Tests.csproj +++ b/src/SharpConnector.Tests/SharpConnector.Tests.csproj @@ -29,7 +29,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/SharpConnector/SharpConnector.csproj b/src/SharpConnector/SharpConnector.csproj index c2ea4c0..29eef40 100755 --- a/src/SharpConnector/SharpConnector.csproj +++ b/src/SharpConnector/SharpConnector.csproj @@ -19,17 +19,17 @@ - + - + - + - + From 39ae4150e1f8a6e0329689aa5fa4ae13e5e2c043 Mon Sep 17 00:00:00 2001 From: Francesco Del Re Date: Fri, 27 Mar 2026 11:39:59 +0100 Subject: [PATCH 03/13] chore(deps): update project dependencies --- src/SharpConnector.Api/SharpConnector.Api.csproj | 2 +- src/SharpConnector/SharpConnector.csproj | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SharpConnector.Api/SharpConnector.Api.csproj b/src/SharpConnector.Api/SharpConnector.Api.csproj index 07ff124..1324936 100644 --- a/src/SharpConnector.Api/SharpConnector.Api.csproj +++ b/src/SharpConnector.Api/SharpConnector.Api.csproj @@ -42,7 +42,7 @@ - + diff --git a/src/SharpConnector/SharpConnector.csproj b/src/SharpConnector/SharpConnector.csproj index 29eef40..d898592 100755 --- a/src/SharpConnector/SharpConnector.csproj +++ b/src/SharpConnector/SharpConnector.csproj @@ -19,7 +19,7 @@ - + @@ -30,7 +30,7 @@ - + From 2af13c49cfab15aa8bc5135898c1536576a31bb9 Mon Sep 17 00:00:00 2001 From: Francesco Del Re Date: Tue, 7 Apr 2026 01:17:07 +0200 Subject: [PATCH 04/13] chore(deps): update project dependencies --- src/SharpConnector/SharpConnector.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/SharpConnector/SharpConnector.csproj b/src/SharpConnector/SharpConnector.csproj index d898592..4715d7b 100755 --- a/src/SharpConnector/SharpConnector.csproj +++ b/src/SharpConnector/SharpConnector.csproj @@ -19,7 +19,7 @@ - + @@ -30,7 +30,7 @@ - + From f75bc1fd35bf9bde19251fdeb0c94a3925e6a7ed Mon Sep 17 00:00:00 2001 From: Francesco Del Re Date: Thu, 9 Apr 2026 23:48:48 +0200 Subject: [PATCH 05/13] chore(deps): update project dependencies --- src/SharpConnector.Tests/SharpConnector.Tests.csproj | 6 +++--- src/SharpConnector/SharpConnector.csproj | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SharpConnector.Tests/SharpConnector.Tests.csproj b/src/SharpConnector.Tests/SharpConnector.Tests.csproj index c6cd50d..24e5973 100644 --- a/src/SharpConnector.Tests/SharpConnector.Tests.csproj +++ b/src/SharpConnector.Tests/SharpConnector.Tests.csproj @@ -25,10 +25,10 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/SharpConnector/SharpConnector.csproj b/src/SharpConnector/SharpConnector.csproj index 4715d7b..4e6f686 100755 --- a/src/SharpConnector/SharpConnector.csproj +++ b/src/SharpConnector/SharpConnector.csproj @@ -20,8 +20,8 @@ - - + + From 8cb2619b1da37df765981628c76cd33000f57506 Mon Sep 17 00:00:00 2001 From: Francesco Del Re Date: Tue, 5 May 2026 16:42:09 +0200 Subject: [PATCH 06/13] chore: update dependencies --- .../SharpConnector.Tests.csproj | 8 ++++---- src/SharpConnector/SharpConnector.csproj | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/SharpConnector.Tests/SharpConnector.Tests.csproj b/src/SharpConnector.Tests/SharpConnector.Tests.csproj index 24e5973..3936762 100644 --- a/src/SharpConnector.Tests/SharpConnector.Tests.csproj +++ b/src/SharpConnector.Tests/SharpConnector.Tests.csproj @@ -25,11 +25,11 @@ - + - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/SharpConnector/SharpConnector.csproj b/src/SharpConnector/SharpConnector.csproj index 4e6f686..1e557c6 100755 --- a/src/SharpConnector/SharpConnector.csproj +++ b/src/SharpConnector/SharpConnector.csproj @@ -19,17 +19,17 @@ - + - - - - - + + + + + - + From aef1f9216d1267f3e7f830c0095c957448dffd9f Mon Sep 17 00:00:00 2001 From: Francesco Del Re Date: Tue, 19 May 2026 10:35:43 +0200 Subject: [PATCH 07/13] chore(deps): update project dependencies --- .../SharpConnector.Tests.csproj | 6 +++--- src/SharpConnector/SharpConnector.csproj | 18 +++++++++--------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/SharpConnector.Tests/SharpConnector.Tests.csproj b/src/SharpConnector.Tests/SharpConnector.Tests.csproj index 3936762..920cf21 100644 --- a/src/SharpConnector.Tests/SharpConnector.Tests.csproj +++ b/src/SharpConnector.Tests/SharpConnector.Tests.csproj @@ -27,9 +27,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/SharpConnector/SharpConnector.csproj b/src/SharpConnector/SharpConnector.csproj index 1e557c6..8b92a2d 100755 --- a/src/SharpConnector/SharpConnector.csproj +++ b/src/SharpConnector/SharpConnector.csproj @@ -19,18 +19,18 @@ - - - + + + - - - - - + + + + + - + From b6f58d006df14cbee3e1abae31f36f6e68452038 Mon Sep 17 00:00:00 2001 From: Francesco Del Re Date: Tue, 19 May 2026 17:38:29 +0200 Subject: [PATCH 08/13] chore: general correctness review and verification --- src/SharpConnector.Tests/ConnectorTests.cs | 7 ++- .../Connectors/DynamoDB/DynamoDbWrapper.cs | 11 ++++- .../Connectors/LiteDb/LiteDbWrapper.cs | 11 ++++- .../Connectors/Memcached/MemcachedWrapper.cs | 11 ++++- .../Connectors/MongoDb/MongoDbWrapper.cs | 11 ++++- .../Connectors/RavenDb/RavenDbWrapper.cs | 11 ++++- .../Connectors/Redis/RedisWrapper.cs | 48 ++++++++++++++++--- src/SharpConnector/Interfaces/IOperations.cs | 12 +++-- .../Interfaces/ISharpConnectorClient.cs | 10 ++-- .../Operations/ArangoDbOperations.cs | 15 +++++- .../Operations/CouchbaseOperations.cs | 17 ++++++- .../Operations/DynamoDbOperations.cs | 15 +++++- .../Operations/LiteDbOperations.cs | 19 ++++++-- .../Operations/MemcachedOperations.cs | 15 +++++- .../Operations/MongoDbOperations.cs | 15 +++++- src/SharpConnector/Operations/Operations.cs | 11 ++++- .../Operations/OperationsFactory.cs | 11 ++++- .../Operations/RavenDbOperations.cs | 15 +++++- .../Operations/RedisOperations.cs | 24 ++++++++-- src/SharpConnector/SharpConnectorClient.cs | 11 ++++- src/SharpConnector/Utilities/Converter.cs | 24 +++++++++- src/SharpConnector/Utilities/Serialization.cs | 17 +++++-- 22 files changed, 292 insertions(+), 49 deletions(-) diff --git a/src/SharpConnector.Tests/ConnectorTests.cs b/src/SharpConnector.Tests/ConnectorTests.cs index 99e372b..21bf377 100644 --- a/src/SharpConnector.Tests/ConnectorTests.cs +++ b/src/SharpConnector.Tests/ConnectorTests.cs @@ -84,7 +84,8 @@ public ConnectorTests() _mockClient.Setup(client => client.InsertManyAsync( It.IsAny>(), It.IsAny())) - .ReturnsAsync(true); + .ReturnsAsync((IEnumerable v, CancellationToken _) => + (IReadOnlyCollection)v.Select(_ => Guid.NewGuid().ToString()).ToList().AsReadOnly()); _sharpConnectorClient = _mockClient.Object; } @@ -183,7 +184,9 @@ public async Task InsertManyAsync_Enumerable() { var values = new List { "payload1", "payload2", "payload3" }; var result = await _sharpConnectorClient.InsertManyAsync(values); - Assert.IsTrue(result); + Assert.IsNotNull(result); + Assert.AreEqual(values.Count, result.Count); + Assert.IsTrue(result.All(k => !string.IsNullOrEmpty(k))); } [TestMethod] diff --git a/src/SharpConnector/Connectors/DynamoDB/DynamoDbWrapper.cs b/src/SharpConnector/Connectors/DynamoDB/DynamoDbWrapper.cs index fba5ce0..ba88897 100644 --- a/src/SharpConnector/Connectors/DynamoDB/DynamoDbWrapper.cs +++ b/src/SharpConnector/Connectors/DynamoDB/DynamoDbWrapper.cs @@ -12,7 +12,7 @@ namespace SharpConnector.Connectors.DynamoDb { - public class DynamoDbWrapper + public class DynamoDbWrapper : IDisposable { private readonly DynamoDbAccess _dynamoDbAccess; private readonly Table _table; @@ -261,5 +261,14 @@ public Task> QueryAsync(Func filter var list = Query(filter); return Task.FromResult(list); } + + /// + /// Disposes the underlying DynamoDB client. + /// + public void Dispose() + { + _dynamoDbAccess?.Dispose(); + GC.SuppressFinalize(this); + } } } \ No newline at end of file diff --git a/src/SharpConnector/Connectors/LiteDb/LiteDbWrapper.cs b/src/SharpConnector/Connectors/LiteDb/LiteDbWrapper.cs index 107c26c..76728ff 100755 --- a/src/SharpConnector/Connectors/LiteDb/LiteDbWrapper.cs +++ b/src/SharpConnector/Connectors/LiteDb/LiteDbWrapper.cs @@ -11,7 +11,7 @@ namespace SharpConnector.Connectors.LiteDb { - public class LiteDbWrapper + public class LiteDbWrapper : IDisposable { private readonly LiteDbAccess _liteDbAccess; @@ -24,6 +24,15 @@ public LiteDbWrapper(LiteDbConfig liteDbConfig) _liteDbAccess = new LiteDbAccess(liteDbConfig); } + /// + /// Disposes the underlying LiteDB connection. + /// + public void Dispose() + { + _liteDbAccess?.Dispose(); + GC.SuppressFinalize(this); + } + /// /// Retrieve the value of the specified key. /// diff --git a/src/SharpConnector/Connectors/Memcached/MemcachedWrapper.cs b/src/SharpConnector/Connectors/Memcached/MemcachedWrapper.cs index 64e92a7..5cc59fb 100755 --- a/src/SharpConnector/Connectors/Memcached/MemcachedWrapper.cs +++ b/src/SharpConnector/Connectors/Memcached/MemcachedWrapper.cs @@ -11,7 +11,7 @@ namespace SharpConnector.Connectors.Memcached { - public class MemcachedWrapper + public class MemcachedWrapper : IDisposable { private readonly MemcachedAccess _memcachedAccess; @@ -24,6 +24,15 @@ public MemcachedWrapper(MemcachedConfig memcachedConfig) _memcachedAccess = new MemcachedAccess(memcachedConfig); } + /// + /// Disposes the underlying Memcached client. + /// + public void Dispose() + { + _memcachedAccess?.Dispose(); + GC.SuppressFinalize(this); + } + /// /// Retrieve the value of the specified key. /// diff --git a/src/SharpConnector/Connectors/MongoDb/MongoDbWrapper.cs b/src/SharpConnector/Connectors/MongoDb/MongoDbWrapper.cs index c676821..faa86ec 100755 --- a/src/SharpConnector/Connectors/MongoDb/MongoDbWrapper.cs +++ b/src/SharpConnector/Connectors/MongoDb/MongoDbWrapper.cs @@ -11,7 +11,7 @@ namespace SharpConnector.Connectors.MongoDb { - public class MongoDbWrapper + public class MongoDbWrapper : IDisposable { private readonly MongoDbAccess _mongoDbAccess; @@ -24,6 +24,15 @@ public MongoDbWrapper(MongoDbConfig mongoDbConfig) _mongoDbAccess = new MongoDbAccess(mongoDbConfig); } + /// + /// Disposes the underlying MongoDB client. + /// + public void Dispose() + { + _mongoDbAccess?.Dispose(); + GC.SuppressFinalize(this); + } + /// /// Retrieve the value of the specified key. /// diff --git a/src/SharpConnector/Connectors/RavenDb/RavenDbWrapper.cs b/src/SharpConnector/Connectors/RavenDb/RavenDbWrapper.cs index 9309b26..95aac72 100755 --- a/src/SharpConnector/Connectors/RavenDb/RavenDbWrapper.cs +++ b/src/SharpConnector/Connectors/RavenDb/RavenDbWrapper.cs @@ -11,7 +11,7 @@ namespace SharpConnector.Connectors.RavenDb { - public class RavenDbWrapper + public class RavenDbWrapper : IDisposable { private readonly RavenDbAccess _ravenDbAccess; @@ -24,6 +24,15 @@ public RavenDbWrapper(RavenDbConfig ravenDbConfig) _ravenDbAccess = new RavenDbAccess(ravenDbConfig); } + /// + /// Disposes the underlying RavenDB document store. + /// + public void Dispose() + { + _ravenDbAccess?.Dispose(); + GC.SuppressFinalize(this); + } + /// /// Retrieve the value of the specified key. /// diff --git a/src/SharpConnector/Connectors/Redis/RedisWrapper.cs b/src/SharpConnector/Connectors/Redis/RedisWrapper.cs index 434919b..edf960b 100755 --- a/src/SharpConnector/Connectors/Redis/RedisWrapper.cs +++ b/src/SharpConnector/Connectors/Redis/RedisWrapper.cs @@ -12,7 +12,7 @@ namespace SharpConnector.Connectors.Redis { - public class RedisWrapper + public class RedisWrapper : IDisposable { private readonly RedisAccess _redisAccess; @@ -25,6 +25,15 @@ public RedisWrapper(RedisConfig redisConfig) _redisAccess = new RedisAccess(redisConfig); } + /// + /// Disposes the underlying Redis connection. + /// + public void Dispose() + { + _redisAccess?.Dispose(); + GC.SuppressFinalize(this); + } + /// /// Get a Redis database. /// @@ -84,15 +93,21 @@ public IEnumerable GetAll(int databaseNumber = 0) foreach (var endpoint in connection.GetEndPoints()) { var server = connection.GetServer(endpoint); + // Skip replicas to avoid duplicated entries in clustered/replicated setups. + if (server.IsReplica) + continue; + var keys = server.Keys(databaseNumber); foreach (var key in keys) { var value = database.StringGet(key); - if (value.HasValue) - { - entities.Add(Deserialize(value)); - } + if (!value.HasValue) + continue; + + var entity = TryDeserialize(value); + if (entity != null) + entities.Add(entity); } } return entities; @@ -115,6 +130,10 @@ public async Task> GetAllAsync(int databaseNumber = ct.ThrowIfCancellationRequested(); var server = connection.GetServer(endpoint); + // Skip replicas to avoid duplicated entries in clustered/replicated setups. + if (server.IsReplica) + continue; + var keys = server.Keys(databaseNumber); var tasks = new List>(); @@ -135,7 +154,24 @@ private static async Task GetEntityAsync(IDatabase database, Re { ct.ThrowIfCancellationRequested(); var value = await database.StringGetAsync(key).ConfigureAwait(false); - return value.HasValue ? Deserialize(value) : null; + return value.HasValue ? TryDeserialize(value) : null; + } + + /// + /// Attempts to deserialize a value into a . + /// Returns null when the value is not a valid ConnectorEntity payload. + /// + private static ConnectorEntity TryDeserialize(RedisValue value) + { + try + { + return Deserialize(value); + } + catch (JsonException) + { + // The value was not produced by SharpConnector (or the schema changed); ignore. + return null; + } } /// diff --git a/src/SharpConnector/Interfaces/IOperations.cs b/src/SharpConnector/Interfaces/IOperations.cs index 3b26999..a61aebf 100755 --- a/src/SharpConnector/Interfaces/IOperations.cs +++ b/src/SharpConnector/Interfaces/IOperations.cs @@ -11,7 +11,7 @@ namespace SharpConnector.Interfaces /// Operations interface for handling CRUD operations on a specific payload type. /// /// Payload object type. - public interface IOperations + public interface IOperations : IDisposable { /// /// Retrieves a single item by its key. @@ -111,12 +111,16 @@ public interface IOperations Task InsertManyAsync(Dictionary values, TimeSpan expiration, CancellationToken ct = default); /// - /// Asynchronously inserts multiple items at once. + /// Asynchronously inserts multiple items, generating a unique key for each value. /// /// A collection of payload objects to insert. /// A token to cancel the asynchronous operation. - /// A task that represents the asynchronous operation, with the result being true if all insertions were successful. - Task InsertManyAsync(IEnumerable values, CancellationToken ct = default); + /// + /// A task that represents the asynchronous operation. The task result is a read-only + /// collection containing the keys generated for each inserted value, in the same + /// order as the input sequence. + /// + Task> InsertManyAsync(IEnumerable values, CancellationToken ct = default); /// /// Deletes an item by its key. diff --git a/src/SharpConnector/Interfaces/ISharpConnectorClient.cs b/src/SharpConnector/Interfaces/ISharpConnectorClient.cs index e4bcb83..f6c325a 100755 --- a/src/SharpConnector/Interfaces/ISharpConnectorClient.cs +++ b/src/SharpConnector/Interfaces/ISharpConnectorClient.cs @@ -11,7 +11,7 @@ namespace SharpConnector.Interfaces /// Defines the operations for a SharpConnector client. /// /// The payload type. - public interface ISharpConnectorClient + public interface ISharpConnectorClient : IDisposable { /// /// Retrieves an item by its key. @@ -110,11 +110,15 @@ public interface ISharpConnectorClient Task InsertManyAsync(Dictionary values, TimeSpan expiration); /// - /// Asynchronously inserts multiple items. + /// Asynchronously inserts multiple items, generating a unique key for each value. /// /// A collection of items to insert. /// A token to cancel the asynchronous operation. - Task InsertManyAsync(IEnumerable values, CancellationToken ct = default); + /// + /// A read-only collection containing the keys generated for each inserted value, + /// in the same order as the input sequence. + /// + Task> InsertManyAsync(IEnumerable values, CancellationToken ct = default); /// /// Deletes an item by its key. diff --git a/src/SharpConnector/Operations/ArangoDbOperations.cs b/src/SharpConnector/Operations/ArangoDbOperations.cs index 0da60fb..c8287f1 100644 --- a/src/SharpConnector/Operations/ArangoDbOperations.cs +++ b/src/SharpConnector/Operations/ArangoDbOperations.cs @@ -177,10 +177,14 @@ public override bool InsertMany(Dictionary values, TimeSpan expiratio /// A collection of objects to insert. /// A token to cancel the asynchronous operation. /// A task that represents the asynchronous operation. The task result indicates success. - public override Task InsertManyAsync(IEnumerable values, CancellationToken ct = default) + public override async Task> InsertManyAsync(IEnumerable values, CancellationToken ct = default) { + ArgumentNullException.ThrowIfNull(values); var list = values.Select(v => new ConnectorEntity(Guid.NewGuid().ToString(), v, null)).ToList(); - return _arangoDbWrapper.InsertManyAsync(list, ct); + var success = await _arangoDbWrapper.InsertManyAsync(list, ct).ConfigureAwait(false); + return success + ? list.Select(e => e.Key).ToList().AsReadOnly() + : (IReadOnlyCollection)Array.Empty(); } /// @@ -300,5 +304,12 @@ public override async Task InsertManyAsync(Dictionary values, T .InsertManyAsync(entities, ct) .ConfigureAwait(false); } + + /// + public override void Dispose() + { + _arangoDbWrapper?.Dispose(); + base.Dispose(); + } } } \ No newline at end of file diff --git a/src/SharpConnector/Operations/CouchbaseOperations.cs b/src/SharpConnector/Operations/CouchbaseOperations.cs index 10aade6..0ae7b2a 100644 --- a/src/SharpConnector/Operations/CouchbaseOperations.cs +++ b/src/SharpConnector/Operations/CouchbaseOperations.cs @@ -164,8 +164,9 @@ public override bool InsertMany(Dictionary values, TimeSpan expiratio /// The values to store. /// A token to cancel the asynchronous operation. /// True if all insertions succeeded; otherwise, false. - public override async Task InsertManyAsync(IEnumerable values, CancellationToken ct = default) + public override async Task> InsertManyAsync(IEnumerable values, CancellationToken ct = default) { + ArgumentNullException.ThrowIfNull(values); var entities = values.Select(value => { ct.ThrowIfCancellationRequested(); @@ -173,7 +174,10 @@ public override async Task InsertManyAsync(IEnumerable values, Cancella return new ConnectorEntity(key, value, null); }).ToList(); - return await _couchbaseWrapper.InsertManyAsync(entities, ct).ConfigureAwait(false); + var success = await _couchbaseWrapper.InsertManyAsync(entities, ct).ConfigureAwait(false); + return success + ? entities.Select(e => e.Key).ToList().AsReadOnly() + : (IReadOnlyCollection)Array.Empty(); } /// @@ -329,5 +333,14 @@ public override async Task InsertManyAsync(Dictionary values, T .InsertManyAsync(entities, ct) .ConfigureAwait(false); } + + /// + public override void Dispose() + { + // CouchbaseWrapper implements IAsyncDisposable; dispose synchronously here. + if (_couchbaseWrapper != null) + _couchbaseWrapper.DisposeAsync().AsTask().GetAwaiter().GetResult(); + base.Dispose(); + } } } \ No newline at end of file diff --git a/src/SharpConnector/Operations/DynamoDbOperations.cs b/src/SharpConnector/Operations/DynamoDbOperations.cs index df599ec..cdd2650 100644 --- a/src/SharpConnector/Operations/DynamoDbOperations.cs +++ b/src/SharpConnector/Operations/DynamoDbOperations.cs @@ -132,10 +132,14 @@ public override bool InsertMany(Dictionary values) /// A collection of values to store. /// A token to cancel the asynchronous operation. /// True if all insertions succeeded; otherwise, false. - public override Task InsertManyAsync(IEnumerable values, CancellationToken ct = default) + public override async Task> InsertManyAsync(IEnumerable values, CancellationToken ct = default) { + ArgumentNullException.ThrowIfNull(values); var list = values.Select(v => new ConnectorEntity(Guid.NewGuid().ToString(), v, null)).ToList(); - return _dynamoDbWrapper.InsertManyAsync(list, ct); + var success = await _dynamoDbWrapper.InsertManyAsync(list, ct).ConfigureAwait(false); + return success + ? list.Select(e => e.Key).ToList().AsReadOnly() + : (IReadOnlyCollection)Array.Empty(); } /// @@ -286,5 +290,12 @@ public override async Task InsertManyAsync(Dictionary values, T .InsertManyAsync(entities, ct) .ConfigureAwait(false); } + + /// + public override void Dispose() + { + _dynamoDbWrapper?.Dispose(); + base.Dispose(); + } } } \ No newline at end of file diff --git a/src/SharpConnector/Operations/LiteDbOperations.cs b/src/SharpConnector/Operations/LiteDbOperations.cs index d4117ea..54f76d6 100755 --- a/src/SharpConnector/Operations/LiteDbOperations.cs +++ b/src/SharpConnector/Operations/LiteDbOperations.cs @@ -180,17 +180,21 @@ public override async Task> GetAllAsync(CancellationToken ct = de } /// - /// Asynchronously insert multiple values. + /// Asynchronously insert multiple values, generating a unique key for each value. /// /// The values to store. /// A token to cancel the asynchronous operation. - /// True if the insertion succeeded; otherwise, false. - public override async Task InsertManyAsync(IEnumerable values, CancellationToken ct = default) + /// The keys generated for each inserted value. + public override async Task> InsertManyAsync(IEnumerable values, CancellationToken ct = default) { + ArgumentNullException.ThrowIfNull(values); var list = values.Select(v => new LiteDbConnectorEntity(Guid.NewGuid().ToString(), v, null)).ToList(); - return await _liteDbWrapper + var success = await _liteDbWrapper .InsertManyAsync(list, ct) .ConfigureAwait(false); + return success + ? list.Select(e => e.Key).ToList().AsReadOnly() + : (IReadOnlyCollection)Array.Empty(); } /// @@ -283,5 +287,12 @@ public override async Task InsertManyAsync(Dictionary values, T .InsertManyAsync(entities, ct) .ConfigureAwait(false); } + + /// + public override void Dispose() + { + _liteDbWrapper?.Dispose(); + base.Dispose(); + } } } \ No newline at end of file diff --git a/src/SharpConnector/Operations/MemcachedOperations.cs b/src/SharpConnector/Operations/MemcachedOperations.cs index 4564a16..6539eb0 100755 --- a/src/SharpConnector/Operations/MemcachedOperations.cs +++ b/src/SharpConnector/Operations/MemcachedOperations.cs @@ -188,13 +188,17 @@ public override Task> GetAllAsync(CancellationToken ct = default) /// The values to store. /// A token to cancel the asynchronous operation. /// True if all insertions were successful; otherwise, false. - public override async Task InsertManyAsync(IEnumerable values, CancellationToken ct = default) + public override async Task> InsertManyAsync(IEnumerable values, CancellationToken ct = default) { + ArgumentNullException.ThrowIfNull(values); ct.ThrowIfCancellationRequested(); var list = values.Select(v => new ConnectorEntity(Guid.NewGuid().ToString(), v, null)).ToList(); - return await _memcachedWrapper + var success = await _memcachedWrapper .InsertManyAsync(list, ct) .ConfigureAwait(false); + return success + ? list.Select(e => e.Key).ToList().AsReadOnly() + : (IReadOnlyCollection)Array.Empty(); } /// @@ -290,5 +294,12 @@ public override async Task InsertManyAsync(Dictionary values, T .InsertManyAsync(entities, ct) .ConfigureAwait(false); } + + /// + public override void Dispose() + { + _memcachedWrapper?.Dispose(); + base.Dispose(); + } } } \ No newline at end of file diff --git a/src/SharpConnector/Operations/MongoDbOperations.cs b/src/SharpConnector/Operations/MongoDbOperations.cs index f2fbaad..386d21b 100755 --- a/src/SharpConnector/Operations/MongoDbOperations.cs +++ b/src/SharpConnector/Operations/MongoDbOperations.cs @@ -143,12 +143,16 @@ public override bool InsertMany(Dictionary values, TimeSpan expiratio /// The values to store as an enumerable. /// A token to cancel the asynchronous operation. /// True if the insertion was successful; otherwise, false. - public override async Task InsertManyAsync(IEnumerable values, CancellationToken ct = default) + public override async Task> InsertManyAsync(IEnumerable values, CancellationToken ct = default) { + ArgumentNullException.ThrowIfNull(values); var list = values.Select(v => new MongoConnectorEntity(Guid.NewGuid().ToString(), v, null)).ToList(); - return await _mongoDbWrapper + var success = await _mongoDbWrapper .InsertManyAsync(list, ct) .ConfigureAwait(false); + return success + ? list.Select(e => e.Key).ToList().AsReadOnly() + : (IReadOnlyCollection)Array.Empty(); } /// @@ -286,5 +290,12 @@ public override async Task InsertManyAsync(Dictionary values, T .InsertManyAsync(entities, ct) .ConfigureAwait(false); } + + /// + public override void Dispose() + { + _mongoDbWrapper?.Dispose(); + base.Dispose(); + } } } \ No newline at end of file diff --git a/src/SharpConnector/Operations/Operations.cs b/src/SharpConnector/Operations/Operations.cs index 7d6fcab..7d83957 100755 --- a/src/SharpConnector/Operations/Operations.cs +++ b/src/SharpConnector/Operations/Operations.cs @@ -51,7 +51,7 @@ public abstract class Operations : IOperations public abstract Task InsertManyAsync(Dictionary values, TimeSpan expiration, CancellationToken ct = default); /// - public abstract Task InsertManyAsync(IEnumerable values, CancellationToken ct = default); + public abstract Task> InsertManyAsync(IEnumerable values, CancellationToken ct = default); /// public abstract bool Delete(string key); @@ -76,5 +76,14 @@ public abstract class Operations : IOperations /// public abstract Task> QueryAsync(Func filter, CancellationToken ct = default); + + /// + /// Releases resources held by the underlying connector. + /// Override in derived classes to dispose connector-specific resources. + /// + public virtual void Dispose() + { + GC.SuppressFinalize(this); + } } } \ No newline at end of file diff --git a/src/SharpConnector/Operations/OperationsFactory.cs b/src/SharpConnector/Operations/OperationsFactory.cs index 058f99e..16fa0e5 100755 --- a/src/SharpConnector/Operations/OperationsFactory.cs +++ b/src/SharpConnector/Operations/OperationsFactory.cs @@ -43,8 +43,15 @@ public IOperations GetStrategy() .GetChildren() .FirstOrDefault(s => s.Key.Equals("instance", StringComparison.OrdinalIgnoreCase))?.Value; - if (!Enum.TryParse(dbType, true, out ConnectorTypeEnums connectorType)) - throw new InvalidOperationException("Instance section for SharpConnector was not found."); + if (string.IsNullOrWhiteSpace(dbType)) + throw new InvalidOperationException("Instance section for SharpConnector was not found or is empty."); + + if (!Enum.TryParse(dbType, true, out ConnectorTypeEnums connectorType) || + !Enum.IsDefined(typeof(ConnectorTypeEnums), connectorType)) + { + throw new InvalidOperationException( + $"Unsupported SharpConnector instance type '{dbType}'."); + } var connectorConfig = GetConfigurationStrategy(_section, connectorType); diff --git a/src/SharpConnector/Operations/RavenDbOperations.cs b/src/SharpConnector/Operations/RavenDbOperations.cs index 22678d3..7e035e6 100755 --- a/src/SharpConnector/Operations/RavenDbOperations.cs +++ b/src/SharpConnector/Operations/RavenDbOperations.cs @@ -153,12 +153,16 @@ public override bool InsertMany(Dictionary values, TimeSpan expiratio /// The values to store. /// A token to cancel the asynchronous operation. /// True if all insertions were successful; otherwise, false. - public override async Task InsertManyAsync(IEnumerable values, CancellationToken ct = default) + public override async Task> InsertManyAsync(IEnumerable values, CancellationToken ct = default) { + ArgumentNullException.ThrowIfNull(values); var list = values.Select(v => new ConnectorEntity(Guid.NewGuid().ToString(), v, null)).ToList(); - return await _ravenDbWrapper + var success = await _ravenDbWrapper .InsertManyAsync(list, ct) .ConfigureAwait(false); + return success + ? list.Select(e => e.Key).ToList().AsReadOnly() + : (IReadOnlyCollection)Array.Empty(); } /// @@ -302,5 +306,12 @@ public override async Task InsertManyAsync(Dictionary values, T .InsertManyAsync(entities, ct) .ConfigureAwait(false); } + + /// + public override void Dispose() + { + _ravenDbWrapper?.Dispose(); + base.Dispose(); + } } } \ No newline at end of file diff --git a/src/SharpConnector/Operations/RedisOperations.cs b/src/SharpConnector/Operations/RedisOperations.cs index 66f2bdc..119de64 100755 --- a/src/SharpConnector/Operations/RedisOperations.cs +++ b/src/SharpConnector/Operations/RedisOperations.cs @@ -78,6 +78,8 @@ public override bool Insert(string key, T value) /// The expiration of the key. public override bool Insert(string key, T value, TimeSpan expiration) { + if (expiration <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(expiration), "Expiration must be greater than zero."); var connectorEntity = new ConnectorEntity(key, value, expiration); return _redisWrapper.Insert(connectorEntity); } @@ -105,6 +107,8 @@ public override async Task InsertAsync(string key, T value, CancellationTo /// A token to cancel the asynchronous operation. public override async Task InsertAsync(string key, T value, TimeSpan expiration, CancellationToken ct = default) { + if (expiration <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(expiration), "Expiration must be greater than zero."); var connectorEntity = new ConnectorEntity(key, value, expiration); return await _redisWrapper .InsertAsync(connectorEntity, 0, ct) @@ -131,16 +135,23 @@ public override bool InsertMany(Dictionary values, TimeSpan expiratio } /// - /// Insert multiple key-value pairs into Redis asynchronously. + /// Insert multiple values into Redis asynchronously, generating a unique key for each value. /// /// A collection of values to store. /// A token to cancel the asynchronous operation. - public override async Task InsertManyAsync(IEnumerable values, CancellationToken ct = default) + /// + /// The keys generated for each inserted value, in the same order as . + /// + public override async Task> InsertManyAsync(IEnumerable values, CancellationToken ct = default) { + ArgumentNullException.ThrowIfNull(values); var list = values.Select(v => new ConnectorEntity(Guid.NewGuid().ToString(), v, null)).ToList(); - return await _redisWrapper + var success = await _redisWrapper .InsertManyAsync(list, 0, ct) .ConfigureAwait(false); + return success + ? list.Select(e => e.Key).ToList().AsReadOnly() + : (IReadOnlyCollection)Array.Empty(); } /// @@ -295,5 +306,12 @@ public override async Task InsertManyAsync(Dictionary values, T .InsertManyAsync(entities, 0, ct) .ConfigureAwait(false); } + + /// + public override void Dispose() + { + _redisWrapper?.Dispose(); + base.Dispose(); + } } } \ No newline at end of file diff --git a/src/SharpConnector/SharpConnectorClient.cs b/src/SharpConnector/SharpConnectorClient.cs index 89a834e..9f84027 100755 --- a/src/SharpConnector/SharpConnectorClient.cs +++ b/src/SharpConnector/SharpConnectorClient.cs @@ -174,7 +174,7 @@ public async Task InsertManyAsync(Dictionary values, TimeSpan e } /// - public async Task InsertManyAsync(IEnumerable values, CancellationToken ct = default) + public async Task> InsertManyAsync(IEnumerable values, CancellationToken ct = default) { return await _operations.InsertManyAsync(values, ct); } @@ -202,5 +202,14 @@ public async Task> QueryAsync(Func filter, CancellationT { return await (_operations.QueryAsync(filter, ct)); } + + /// + /// Releases the underlying connector resources (e.g. connections, clients). + /// + public void Dispose() + { + _operations?.Dispose(); + GC.SuppressFinalize(this); + } } } diff --git a/src/SharpConnector/Utilities/Converter.cs b/src/SharpConnector/Utilities/Converter.cs index de897ad..21ea881 100644 --- a/src/SharpConnector/Utilities/Converter.cs +++ b/src/SharpConnector/Utilities/Converter.cs @@ -18,6 +18,8 @@ public static class Converter /// public static T ToPayloadObject(this ConnectorEntity connectorEntity) { + if (connectorEntity == null) + return default; return ConvertPayload(connectorEntity.Payload); } @@ -29,6 +31,8 @@ public static T ToPayloadObject(this ConnectorEntity connectorEntity) /// public static T ToPayloadObject(this LiteDbConnectorEntity liteDbConnectorEntity) { + if (liteDbConnectorEntity == null) + return default; return ConvertPayload(liteDbConnectorEntity.Payload); } @@ -71,13 +75,31 @@ public static IEnumerable ToPayloadList(this IEnumerable private static T ConvertPayload(object payload) { + if (payload is null) + return default; + if (payload is T typed) return typed; if (payload is JToken jToken) return jToken.ToObject(); - return (T)Convert.ChangeType(payload, typeof(T)); + var targetType = typeof(T); + + // Fast path for primitives / IConvertible target types + if (payload is IConvertible && typeof(IConvertible).IsAssignableFrom(targetType)) + { + try + { + return (T)Convert.ChangeType(payload, targetType); + } + catch (InvalidCastException) { /* fallback below */ } + catch (FormatException) { /* fallback below */ } + } + + // Fallback: round-trip via JSON for arbitrary POCOs (e.g. BSON documents from MongoDB) + var json = JsonConvert.SerializeObject(payload); + return JsonConvert.DeserializeObject(json); } /// diff --git a/src/SharpConnector/Utilities/Serialization.cs b/src/SharpConnector/Utilities/Serialization.cs index 1edac13..5f98094 100644 --- a/src/SharpConnector/Utilities/Serialization.cs +++ b/src/SharpConnector/Utilities/Serialization.cs @@ -1,30 +1,37 @@ // (c) 2020 Francesco Del Re // This code is licensed under MIT license (see LICENSE.txt for details) using System; -using System.Text.Json; +using Newtonsoft.Json; namespace SharpConnector.Utilities { public static class Serialization { /// - /// Determines if the given object is serializable. + /// Determines if the given object is serializable using the same + /// serializer used by the connectors (Newtonsoft.Json). /// /// The object to check. /// True if the object can be serialized; otherwise, false. public static bool IsSerializable(this object obj) { + if (obj is null) + return true; + try { - // Attempt to serialize the object - JsonSerializer.Serialize(obj); + JsonConvert.SerializeObject(obj); return true; } catch (JsonException) { return false; } - catch (Exception) + catch (NotSupportedException) + { + return false; + } + catch (ArgumentException) { return false; } From b4e0e2b9603b9e875548a12c53f7b752aa8ed1c2 Mon Sep 17 00:00:00 2001 From: Francesco Del Re Date: Wed, 20 May 2026 17:23:01 +0200 Subject: [PATCH 09/13] feat: integrate Cassandra connector --- README.md | 17 + .../CassandraConfigTests.cs | 123 +++++++ .../CassandraFactoryTests.cs | 51 +++ .../Configuration/CassandraConfig.cs | 55 +++ .../Connectors/Cassandra/CassandraAccess.cs | 103 ++++++ .../Connectors/Cassandra/CassandraWrapper.cs | 322 ++++++++++++++++++ .../Enums/ConnectorTypeEnums.cs | 3 +- .../Operations/CassandraOperations.cs | 278 +++++++++++++++ .../Operations/OperationFactory.cs | 1 + .../Operations/OperationsFactory.cs | 3 +- src/SharpConnector/SharpConnector.csproj | 1 + 11 files changed, 955 insertions(+), 2 deletions(-) create mode 100644 src/SharpConnector.Tests/CassandraConfigTests.cs create mode 100644 src/SharpConnector.Tests/CassandraFactoryTests.cs create mode 100644 src/SharpConnector/Configuration/CassandraConfig.cs create mode 100644 src/SharpConnector/Connectors/Cassandra/CassandraAccess.cs create mode 100644 src/SharpConnector/Connectors/Cassandra/CassandraWrapper.cs create mode 100644 src/SharpConnector/Operations/CassandraOperations.cs diff --git a/README.md b/README.md index 0ec9324..db987d7 100755 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ SharpConnector is a .NET library designed to streamline integration with NoSQL d - Supported stores: - Key–value: Redis, EnyimMemcached, DynamoDb - Document: MongoDB, LiteDB, RavenDB, Couchbase + - Wide-column: Cassandra - Multi-model: ArangoDB - Simple configuration via `appsettings.json` and DI-friendly - Sync and async operations (with CancellationToken support) @@ -57,6 +58,7 @@ Through SharpConnector, you can use a consistent interface to perform Insert, Ge * **Couchbase (document-oriented)** * **DynamoDb (key-value or document-oriented)** * **ArangoDB (multi-model)** +* **Apache Cassandra (wide-column)** SharpConnector thus simplifies the development process, providing flexibility and compatibility across diverse NoSQL paradigms without the need to handle specific database implementations. @@ -159,6 +161,20 @@ Then, add the specif `ConnectorConfig` node within your *appsettings.json* file: } ``` +- Cassandra + ```json + { + "ConnectorConfig": { + "Instance": "Cassandra", + "ConnectionString": "127.0.0.1:9042", + "DatabaseName": "sharpconnector", + "TableName": "items", + "Username": "cassandra", + "Password": "cassandra" + } + } + ``` + Once configured, create a new SharpConnector client, specifying the payload type (e.g., string): ```csharp @@ -226,6 +242,7 @@ Each of these libraries operates under a specific license, which governs its usa * **Couchbase**, the official Couchbase SDK for .NET Core and Full Frameworks, see **license** [here](https://github.com/couchbase/couchbase-net-client/blob/master/LICENSE) * **DynamoDb**, the official AWS SDK for .NET, see **license** [here](https://github.com/aws/aws-sdk-net/blob/main/License.txt) * **ArangoDB**, a consistent, comprehensive, minimal driver for ArangoDB, see **license** [here](https://github.com/ArangoDB-Community/arangodb-net-standard/blob/master/LICENSE) +* **CassandraCSharpDriver**, the DataStax C# driver for Apache Cassandra, see **license** [here](https://github.com/datastax/csharp-driver/blob/master/LICENSE) Each library is included to enhance the functionality of SharpConnector while adhering to its licensing terms. diff --git a/src/SharpConnector.Tests/CassandraConfigTests.cs b/src/SharpConnector.Tests/CassandraConfigTests.cs new file mode 100644 index 0000000..0359705 --- /dev/null +++ b/src/SharpConnector.Tests/CassandraConfigTests.cs @@ -0,0 +1,123 @@ +// (c) 2025 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using Microsoft.Extensions.Configuration; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SharpConnector.Configuration; +using System; +using System.Collections.Generic; + +namespace SharpConnector.Tests +{ + [TestClass] + public class CassandraConfigTests + { + private static IConfigurationSection BuildSection(Dictionary values) + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); + + return configuration.GetSection("ConnectorConfig"); + } + + [TestMethod] + public void Ctor_ValidConfiguration_PopulatesAllProperties() + { + var section = BuildSection(new Dictionary + { + ["ConnectorConfig:connectionstring"] = "127.0.0.1:9042", + ["ConnectorConfig:databasename"] = "sharpconnector", + ["ConnectorConfig:tablename"] = "items", + ["ConnectorConfig:username"] = "cassandra", + ["ConnectorConfig:password"] = "cassandra" + }); + + var config = new CassandraConfig(section); + + Assert.AreEqual("127.0.0.1:9042", config.ConnectionString); + Assert.AreEqual("sharpconnector", config.DatabaseName); + Assert.AreEqual("items", config.TableName); + Assert.AreEqual("cassandra", config.Username); + Assert.AreEqual("cassandra", config.Password); + } + + [TestMethod] + public void Ctor_TrimsWhitespace() + { + var section = BuildSection(new Dictionary + { + ["ConnectorConfig:connectionstring"] = " 127.0.0.1:9042 ", + ["ConnectorConfig:databasename"] = " ks ", + ["ConnectorConfig:tablename"] = " tbl ", + ["ConnectorConfig:username"] = " user ", + ["ConnectorConfig:password"] = " pwd " + }); + + var config = new CassandraConfig(section); + + Assert.AreEqual("127.0.0.1:9042", config.ConnectionString); + Assert.AreEqual("ks", config.DatabaseName); + Assert.AreEqual("tbl", config.TableName); + Assert.AreEqual("user", config.Username); + Assert.AreEqual("pwd", config.Password); + } + + [TestMethod] + public void Ctor_OptionalCredentials_AreAllowedToBeNull() + { + var section = BuildSection(new Dictionary + { + ["ConnectorConfig:connectionstring"] = "127.0.0.1:9042", + ["ConnectorConfig:databasename"] = "sharpconnector", + ["ConnectorConfig:tablename"] = "items" + }); + + var config = new CassandraConfig(section); + + Assert.IsNull(config.Username); + Assert.IsNull(config.Password); + } + + [TestMethod] + public void Ctor_NullConfiguration_Throws() + { + Assert.ThrowsExactly(() => new CassandraConfig(null)); + } + + [TestMethod] + public void Ctor_MissingConnectionString_Throws() + { + var section = BuildSection(new Dictionary + { + ["ConnectorConfig:databasename"] = "sharpconnector", + ["ConnectorConfig:tablename"] = "items" + }); + + Assert.ThrowsExactly(() => new CassandraConfig(section)); + } + + [TestMethod] + public void Ctor_MissingDatabaseName_Throws() + { + var section = BuildSection(new Dictionary + { + ["ConnectorConfig:connectionstring"] = "127.0.0.1:9042", + ["ConnectorConfig:tablename"] = "items" + }); + + Assert.ThrowsExactly(() => new CassandraConfig(section)); + } + + [TestMethod] + public void Ctor_MissingTableName_Throws() + { + var section = BuildSection(new Dictionary + { + ["ConnectorConfig:connectionstring"] = "127.0.0.1:9042", + ["ConnectorConfig:databasename"] = "sharpconnector" + }); + + Assert.ThrowsExactly(() => new CassandraConfig(section)); + } + } +} diff --git a/src/SharpConnector.Tests/CassandraFactoryTests.cs b/src/SharpConnector.Tests/CassandraFactoryTests.cs new file mode 100644 index 0000000..0c19b7d --- /dev/null +++ b/src/SharpConnector.Tests/CassandraFactoryTests.cs @@ -0,0 +1,51 @@ +// (c) 2025 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using Microsoft.Extensions.Configuration; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using SharpConnector.Configuration; +using SharpConnector.Enums; +using SharpConnector.Operations; +using System; +using System.Collections.Generic; + +namespace SharpConnector.Tests +{ + [TestClass] + public class CassandraFactoryTests + { + private static IConfigurationSection BuildSection(Dictionary values) + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(values) + .Build(); + + return configuration.GetSection("ConnectorConfig"); + } + + [TestMethod] + public void ConnectorTypeEnum_ContainsCassandra() + { + var defined = Enum.IsDefined(typeof(ConnectorTypeEnums), ConnectorTypeEnums.Cassandra); + Assert.IsTrue(defined); + } + + [TestMethod] + public void OperationFactory_GetConfigurationStrategy_ReturnsCassandraConfig() + { + var section = BuildSection(new Dictionary + { + ["ConnectorConfig:connectionstring"] = "127.0.0.1:9042", + ["ConnectorConfig:databasename"] = "sharpconnector", + ["ConnectorConfig:tablename"] = "items" + }); + + var factory = new OperationsFactory(section); + var config = factory.GetConfigurationStrategy(section, ConnectorTypeEnums.Cassandra); + + Assert.IsNotNull(config); + Assert.IsInstanceOfType(config, typeof(CassandraConfig)); + Assert.AreEqual("sharpconnector", config.DatabaseName); + Assert.AreEqual("items", config.TableName); + } + } +} diff --git a/src/SharpConnector/Configuration/CassandraConfig.cs b/src/SharpConnector/Configuration/CassandraConfig.cs new file mode 100644 index 0000000..463a746 --- /dev/null +++ b/src/SharpConnector/Configuration/CassandraConfig.cs @@ -0,0 +1,55 @@ +// (c) 2025 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using Microsoft.Extensions.Configuration; +using SharpConnector.Enums; +using SharpConnector.Interfaces; +using System; + +namespace SharpConnector.Configuration +{ + /// + /// Provides configuration settings for Apache Cassandra. + /// + public class CassandraConfig : IConnectorConfig + { + public string ConnectionString { get; } + public string DatabaseName { get; } + public string TableName { get; } + public string Username { get; } + public string Password { get; } + + #region NOT USED + public int DatabaseNumber { get; private set; } + public string CollectionName { get; private set; } + public string BucketName { get; private set; } + public string AccessKey { get; private set; } + public string SecretKey { get; private set; } + public string Region { get; private set; } + public string ServiceUrl { get; private set; } + public bool UseHttp { get; private set; } + #endregion + + public CassandraConfig(IConfiguration configuration) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + ConnectionString = configuration[AppConfigParameterEnums.connectionstring.ToString()]?.Trim(); + if (string.IsNullOrEmpty(ConnectionString)) + throw new ArgumentException("Cassandra ConnectionString (contact points) is required but was not found in configuration."); + + DatabaseName = configuration[AppConfigParameterEnums.databasename.ToString()]?.Trim(); + if (string.IsNullOrEmpty(DatabaseName)) + throw new ArgumentException("Cassandra DatabaseName (keyspace) is required but was not found in configuration."); + + TableName = configuration[AppConfigParameterEnums.tablename.ToString()]?.Trim(); + if (string.IsNullOrEmpty(TableName)) + throw new ArgumentException("Cassandra TableName is required but was not found in configuration."); + + Username = configuration[AppConfigParameterEnums.username.ToString()]?.Trim(); + Password = configuration[AppConfigParameterEnums.password.ToString()]?.Trim(); + } + } +} diff --git a/src/SharpConnector/Connectors/Cassandra/CassandraAccess.cs b/src/SharpConnector/Connectors/Cassandra/CassandraAccess.cs new file mode 100644 index 0000000..da14030 --- /dev/null +++ b/src/SharpConnector/Connectors/Cassandra/CassandraAccess.cs @@ -0,0 +1,103 @@ +// (c) 2025 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using Cassandra; +using SharpConnector.Configuration; +using System; +using System.Linq; + +namespace SharpConnector.Connectors.Cassandra +{ + /// + /// Manages access to an Apache Cassandra cluster and the associated session. + /// + public class CassandraAccess : IDisposable + { + public ICluster Cluster { get; } + public ISession Session { get; } + public string Keyspace { get; } + public string TableName { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The Cassandra configuration. + public CassandraAccess(CassandraConfig cassandraConfig) + { + if (cassandraConfig == null) + throw new ArgumentNullException(nameof(cassandraConfig), "Cassandra configuration cannot be null."); + + Keyspace = cassandraConfig.DatabaseName; + TableName = cassandraConfig.TableName; + + var (contactPoints, port) = ParseContactPoints(cassandraConfig.ConnectionString); + + var builder = global::Cassandra.Cluster.Builder() + .AddContactPoints(contactPoints); + + if (port.HasValue) + builder = builder.WithPort(port.Value); + + if (!string.IsNullOrEmpty(cassandraConfig.Username)) + { + builder = builder.WithCredentials( + cassandraConfig.Username, + cassandraConfig.Password ?? string.Empty); + } + + Cluster = builder.Build(); + + // Ensure keyspace exists, then connect to it. + using (var bootstrapSession = Cluster.Connect()) + { + bootstrapSession.Execute( + $"CREATE KEYSPACE IF NOT EXISTS {Keyspace} " + + "WITH replication = {'class':'SimpleStrategy','replication_factor':1};"); + } + + Session = Cluster.Connect(Keyspace); + + // Ensure the storage table exists. + Session.Execute( + $"CREATE TABLE IF NOT EXISTS {TableName} (" + + "key text PRIMARY KEY, " + + "payload text, " + + "expiration bigint);"); + } + + /// + /// Parses the contact points string in the form "host1,host2:9042". + /// + private static (string[] hosts, int? port) ParseContactPoints(string connectionString) + { + var tokens = connectionString + .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) + .Select(t => t.Trim()) + .Where(t => !string.IsNullOrEmpty(t)) + .ToArray(); + + int? parsedPort = null; + var hosts = tokens.Select(t => + { + var idx = t.LastIndexOf(':'); + if (idx > 0 && int.TryParse(t.Substring(idx + 1), out var p)) + { + parsedPort = p; + return t.Substring(0, idx); + } + return t; + }).ToArray(); + + return (hosts, parsedPort); + } + + /// + /// Disposes the underlying Cassandra session and cluster. + /// + public void Dispose() + { + Session?.Dispose(); + Cluster?.Dispose(); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/SharpConnector/Connectors/Cassandra/CassandraWrapper.cs b/src/SharpConnector/Connectors/Cassandra/CassandraWrapper.cs new file mode 100644 index 0000000..09755ed --- /dev/null +++ b/src/SharpConnector/Connectors/Cassandra/CassandraWrapper.cs @@ -0,0 +1,322 @@ +// (c) 2025 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using Cassandra; +using Newtonsoft.Json; +using SharpConnector.Configuration; +using SharpConnector.Entities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpConnector.Connectors.Cassandra +{ + /// + /// Provides CRUD operations over an Apache Cassandra table using a key/payload schema. + /// + public class CassandraWrapper : IDisposable + { + private readonly CassandraAccess _cassandraAccess; + private readonly PreparedStatement _getStatement; + private readonly PreparedStatement _getAllStatement; + private readonly PreparedStatement _insertStatement; + private readonly PreparedStatement _insertStatementWithTtl; + private readonly PreparedStatement _updateStatement; + private readonly PreparedStatement _deleteStatement; + private readonly PreparedStatement _existsStatement; + + /// + /// Initializes a new instance of the class. + /// + /// The Cassandra configuration. + public CassandraWrapper(CassandraConfig cassandraConfig) + { + _cassandraAccess = new CassandraAccess(cassandraConfig); + + var table = _cassandraAccess.TableName; + _getStatement = _cassandraAccess.Session.Prepare( + $"SELECT key, payload, expiration FROM {table} WHERE key = ?;"); + _getAllStatement = _cassandraAccess.Session.Prepare( + $"SELECT key, payload, expiration FROM {table};"); + _insertStatement = _cassandraAccess.Session.Prepare( + $"INSERT INTO {table} (key, payload, expiration) VALUES (?, ?, ?);"); + _insertStatementWithTtl = _cassandraAccess.Session.Prepare( + $"INSERT INTO {table} (key, payload, expiration) VALUES (?, ?, ?) USING TTL ?;"); + _updateStatement = _cassandraAccess.Session.Prepare( + $"UPDATE {table} SET payload = ?, expiration = ? WHERE key = ?;"); + _deleteStatement = _cassandraAccess.Session.Prepare( + $"DELETE FROM {table} WHERE key = ?;"); + _existsStatement = _cassandraAccess.Session.Prepare( + $"SELECT key FROM {table} WHERE key = ?;"); + } + + /// + /// Disposes the underlying Cassandra access. + /// + public void Dispose() + { + _cassandraAccess?.Dispose(); + GC.SuppressFinalize(this); + } + + /// + /// Retrieve the value of the specified key. + /// + /// The key of the object. + public ConnectorEntity Get(string key) + { + var row = _cassandraAccess.Session + .Execute(_getStatement.Bind(key)) + .FirstOrDefault(); + return MapRow(row); + } + + /// + /// Asynchronously retrieve the value of the specified key. + /// + /// The key of the object. + /// A token to cancel the asynchronous operation. + public async Task GetAsync(string key, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + var rowSet = await _cassandraAccess.Session + .ExecuteAsync(_getStatement.Bind(key)) + .ConfigureAwait(false); + return MapRow(rowSet.FirstOrDefault()); + } + + /// + /// Retrieve all values. + /// + public List GetAll() + { + return _cassandraAccess.Session + .Execute(_getAllStatement.Bind()) + .Select(MapRow) + .Where(e => e != null) + .ToList(); + } + + /// + /// Asynchronously retrieve all values. + /// + /// A token to cancel the asynchronous operation. + public async Task> GetAllAsync(CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + var rowSet = await _cassandraAccess.Session + .ExecuteAsync(_getAllStatement.Bind()) + .ConfigureAwait(false); + return rowSet + .Select(MapRow) + .Where(e => e != null) + .ToList(); + } + + /// + /// Insert or replace the specified entity (Cassandra INSERT is an upsert). + /// + /// The entity to store. + public bool Insert(ConnectorEntity connectorEntity) + { + var statement = BuildInsertStatement(connectorEntity); + _cassandraAccess.Session.Execute(statement); + return true; + } + + /// + /// Asynchronously insert or replace the specified entity. + /// + /// The entity to store. + /// A token to cancel the asynchronous operation. + public async Task InsertAsync(ConnectorEntity connectorEntity, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + var statement = BuildInsertStatement(connectorEntity); + await _cassandraAccess.Session + .ExecuteAsync(statement) + .ConfigureAwait(false); + return true; + } + + /// + /// Insert multiple entities using a logged batch. + /// + /// The entities to store. + public bool InsertMany(List connectorEntities) + { + if (connectorEntities == null || connectorEntities.Count == 0) + return true; + + var batch = new BatchStatement(); + foreach (var entity in connectorEntities) + batch.Add(BuildInsertStatement(entity)); + + _cassandraAccess.Session.Execute(batch); + return true; + } + + /// + /// Asynchronously insert multiple entities using a logged batch. + /// + /// The entities to store. + /// A token to cancel the asynchronous operation. + public async Task InsertManyAsync(List connectorEntities, CancellationToken ct = default) + { + if (connectorEntities == null || connectorEntities.Count == 0) + return true; + + ct.ThrowIfCancellationRequested(); + + var batch = new BatchStatement(); + foreach (var entity in connectorEntities) + batch.Add(BuildInsertStatement(entity)); + + await _cassandraAccess.Session + .ExecuteAsync(batch) + .ConfigureAwait(false); + return true; + } + + /// + /// Update an existing entity. + /// + /// The entity to update. + public bool Update(ConnectorEntity connectorEntity) + { + var payload = SerializePayload(connectorEntity.Payload); + var expirationTicks = connectorEntity.Expiration?.Ticks ?? 0L; + + _cassandraAccess.Session.Execute( + _updateStatement.Bind(payload, expirationTicks, connectorEntity.Key)); + return true; + } + + /// + /// Asynchronously update an existing entity. + /// + /// The entity to update. + /// A token to cancel the asynchronous operation. + public async Task UpdateAsync(ConnectorEntity connectorEntity, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + var payload = SerializePayload(connectorEntity.Payload); + var expirationTicks = connectorEntity.Expiration?.Ticks ?? 0L; + + await _cassandraAccess.Session + .ExecuteAsync(_updateStatement.Bind(payload, expirationTicks, connectorEntity.Key)) + .ConfigureAwait(false); + return true; + } + + /// + /// Remove the specified key. + /// + /// The key of the object. + public bool Delete(string key) + { + _cassandraAccess.Session.Execute(_deleteStatement.Bind(key)); + return true; + } + + /// + /// Asynchronously remove the specified key. + /// + /// The key of the object. + /// A token to cancel the asynchronous operation. + public async Task DeleteAsync(string key, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + await _cassandraAccess.Session + .ExecuteAsync(_deleteStatement.Bind(key)) + .ConfigureAwait(false); + return true; + } + + /// + /// Check whether an item exists by its key. + /// + /// The unique key of the item. + public bool Exists(string key) + { + return _cassandraAccess.Session + .Execute(_existsStatement.Bind(key)) + .Any(); + } + + /// + /// Asynchronously check whether an item exists by its key. + /// + /// The unique key of the item. + /// A token to cancel the asynchronous operation. + public async Task ExistsAsync(string key, CancellationToken ct = default) + { + ct.ThrowIfCancellationRequested(); + var rowSet = await _cassandraAccess.Session + .ExecuteAsync(_existsStatement.Bind(key)) + .ConfigureAwait(false); + return rowSet.Any(); + } + + /// + /// Query items by filtering in memory. + /// + /// Predicate used to filter items. + public List Query(Func filter) + { + return GetAll().Where(filter).ToList(); + } + + /// + /// Asynchronously query items by filtering in memory. + /// + /// Predicate used to filter items. + /// A token to cancel the asynchronous operation. + public async Task> QueryAsync(Func filter, CancellationToken ct = default) + { + var entities = await GetAllAsync(ct).ConfigureAwait(false); + ct.ThrowIfCancellationRequested(); + return entities.Where(filter).ToList(); + } + + private BoundStatement BuildInsertStatement(ConnectorEntity entity) + { + var payload = SerializePayload(entity.Payload); + var expirationTicks = entity.Expiration?.Ticks ?? 0L; + + if (entity.Expiration.HasValue && entity.Expiration.Value > TimeSpan.Zero) + { + var ttlSeconds = (int)Math.Max(1, Math.Min(int.MaxValue, entity.Expiration.Value.TotalSeconds)); + return _insertStatementWithTtl.Bind(entity.Key, payload, expirationTicks, ttlSeconds); + } + + return _insertStatement.Bind(entity.Key, payload, expirationTicks); + } + + private static string SerializePayload(object payload) + { + return payload == null ? null : JsonConvert.SerializeObject(payload); + } + + private static ConnectorEntity MapRow(Row row) + { + if (row == null) + return null; + + var key = row.GetValue("key"); + var payloadJson = row.GetValue("payload"); + var expirationTicks = row.GetValue("expiration"); + + object payload = null; + if (!string.IsNullOrEmpty(payloadJson)) + payload = JsonConvert.DeserializeObject(payloadJson); + + TimeSpan? expiration = expirationTicks > 0 + ? TimeSpan.FromTicks(expirationTicks) + : (TimeSpan?)null; + + return new ConnectorEntity(key, payload ?? string.Empty, expiration); + } + } +} diff --git a/src/SharpConnector/Enums/ConnectorTypeEnums.cs b/src/SharpConnector/Enums/ConnectorTypeEnums.cs index 2254961..e035755 100755 --- a/src/SharpConnector/Enums/ConnectorTypeEnums.cs +++ b/src/SharpConnector/Enums/ConnectorTypeEnums.cs @@ -14,6 +14,7 @@ public enum ConnectorTypeEnums RavenDb, Couchbase, DynamoDb, - ArangoDb + ArangoDb, + Cassandra } } diff --git a/src/SharpConnector/Operations/CassandraOperations.cs b/src/SharpConnector/Operations/CassandraOperations.cs new file mode 100644 index 0000000..8828581 --- /dev/null +++ b/src/SharpConnector/Operations/CassandraOperations.cs @@ -0,0 +1,278 @@ +// (c) 2025 Francesco Del Re +// This code is licensed under MIT license (see LICENSE.txt for details) +using SharpConnector.Configuration; +using SharpConnector.Connectors.Cassandra; +using SharpConnector.Entities; +using SharpConnector.Utilities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace SharpConnector.Operations +{ + /// + /// Provides operations for interacting with an Apache Cassandra table. + /// + /// The type of the payload stored in the table. + public class CassandraOperations : Operations + { + private readonly CassandraWrapper _cassandraWrapper; + + /// + /// Initializes a new instance of the class. + /// + /// The Cassandra connector configuration. + public CassandraOperations(CassandraConfig cassandraConfig) + { + _cassandraWrapper = new CassandraWrapper(cassandraConfig); + } + + /// + /// Retrieve the value of the specified key. + /// + /// The key of the object. + public override T Get(string key) + { + var connectorEntity = _cassandraWrapper.Get(key); + if (connectorEntity != null) + return connectorEntity.ToPayloadObject(); + return default; + } + + /// + /// Asynchronously retrieve the value of the specified key. + /// + /// The key of the object. + /// A token to cancel the asynchronous operation. + public override async Task GetAsync(string key, CancellationToken ct = default) + { + var connectorEntity = await _cassandraWrapper.GetAsync(key, ct).ConfigureAwait(false); + if (connectorEntity != null) + return connectorEntity.ToPayloadObject(); + return default; + } + + /// + /// Retrieve all values. + /// + public override IEnumerable GetAll() + { + var connectorEntities = _cassandraWrapper.GetAll(); + return connectorEntities?.ToPayloadList() ?? []; + } + + /// + /// Asynchronously retrieve all values. + /// + /// A token to cancel the asynchronous operation. + public override async Task> GetAllAsync(CancellationToken ct = default) + { + var connectorEntities = await _cassandraWrapper.GetAllAsync(ct).ConfigureAwait(false); + return connectorEntities?.ToPayloadList() ?? []; + } + + /// + /// Insert (upsert) the specified key and value. + /// + /// The key of the object. + /// The value to store. + public override bool Insert(string key, T value) + { + var connectorEntity = new ConnectorEntity(key, value, null); + return _cassandraWrapper.Insert(connectorEntity); + } + + /// + /// Insert (upsert) the specified key and value with expiration. + /// + /// The key of the object. + /// The value to store. + /// The expiration time for the value. + public override bool Insert(string key, T value, TimeSpan expiration) + { + var connectorEntity = new ConnectorEntity(key, value, expiration); + return _cassandraWrapper.Insert(connectorEntity); + } + + /// + /// Asynchronously insert (upsert) the specified key and value. + /// + /// The key of the object. + /// The value to store. + /// A token to cancel the asynchronous operation. + public override Task InsertAsync(string key, T value, CancellationToken ct = default) + { + var connectorEntity = new ConnectorEntity(key, value, null); + return _cassandraWrapper.InsertAsync(connectorEntity, ct); + } + + /// + /// Asynchronously insert (upsert) the specified key and value with expiration. + /// + /// The key of the object. + /// The value to store. + /// The expiration time for the value. + /// A token to cancel the asynchronous operation. + public override Task InsertAsync(string key, T value, TimeSpan expiration, CancellationToken ct = default) + { + var connectorEntity = new ConnectorEntity(key, value, expiration); + return _cassandraWrapper.InsertAsync(connectorEntity, ct); + } + + /// + /// Insert multiple values. + /// + /// The values to store. + public override bool InsertMany(Dictionary values) + { + return _cassandraWrapper.InsertMany(values.ToConnectorEntityList()); + } + + /// + /// Insert multiple values with expiration. + /// + /// The values to store. + /// The expiration time for the values. + public override bool InsertMany(Dictionary values, TimeSpan expiration) + { + return _cassandraWrapper.InsertMany(values.ToConnectorEntityList(expiration)); + } + + /// + /// Asynchronously insert multiple values. + /// + /// A collection of objects to insert. + /// A token to cancel the asynchronous operation. + public override async Task> InsertManyAsync(IEnumerable values, CancellationToken ct = default) + { + ArgumentNullException.ThrowIfNull(values); + var list = values.Select(v => new ConnectorEntity(Guid.NewGuid().ToString(), v, null)).ToList(); + var success = await _cassandraWrapper.InsertManyAsync(list, ct).ConfigureAwait(false); + return success + ? list.Select(e => e.Key).ToList().AsReadOnly() + : (IReadOnlyCollection)Array.Empty(); + } + + /// + /// Asynchronously insert multiple key/value pairs. + /// + /// The values to store. + /// A token to cancel the asynchronous operation. + public override async Task InsertManyAsync(Dictionary values, CancellationToken ct = default) + { + var entities = values.ToConnectorEntityList(); + return await _cassandraWrapper + .InsertManyAsync(entities, ct) + .ConfigureAwait(false); + } + + /// + /// Asynchronously insert multiple key/value pairs with a common expiration. + /// + /// The values to store. + /// The expiration time for the values. + /// A token to cancel the asynchronous operation. + public override async Task InsertManyAsync(Dictionary values, TimeSpan expiration, CancellationToken ct = default) + { + var entities = values.ToConnectorEntityList(expiration); + return await _cassandraWrapper + .InsertManyAsync(entities, ct) + .ConfigureAwait(false); + } + + /// + /// Remove the specified key. + /// + /// The key of the object. + public override bool Delete(string key) + { + return _cassandraWrapper.Delete(key); + } + + /// + /// Asynchronously remove the specified key. + /// + /// The key of the object. + /// A token to cancel the asynchronous operation. + public override Task DeleteAsync(string key, CancellationToken ct = default) + { + return _cassandraWrapper.DeleteAsync(key, ct); + } + + /// + /// Update the value of the specified key. + /// + /// The key of the object. + /// The value to store. + public override bool Update(string key, T value) + { + var connectorEntity = new ConnectorEntity(key, value, null); + return _cassandraWrapper.Update(connectorEntity); + } + + /// + /// Asynchronously update the value of the specified key. + /// + /// The key of the object. + /// The value to store. + /// A token to cancel the asynchronous operation. + public override Task UpdateAsync(string key, T value, CancellationToken ct = default) + { + var connectorEntity = new ConnectorEntity(key, value, null); + return _cassandraWrapper.UpdateAsync(connectorEntity, ct); + } + + /// + /// Check whether an item exists by its key. + /// + /// The unique key of the item. + public override bool Exists(string key) + { + return _cassandraWrapper.Exists(key); + } + + /// + /// Asynchronously check whether an item exists by its key. + /// + /// The unique key of the item. + /// A token to cancel the asynchronous operation. + public override Task ExistsAsync(string key, CancellationToken ct = default) + { + return _cassandraWrapper.ExistsAsync(key, ct); + } + + /// + /// Execute a filtered query over the items in the table. + /// + /// Predicate that selects items of type T. + public override IEnumerable Query(Func filter) + { + return _cassandraWrapper + .Query(e => filter(e.ToPayloadObject())) + .Select(e => e.ToPayloadObject()); + } + + /// + /// Asynchronously execute a filtered query over the items in the table. + /// + /// Predicate that selects items of type T. + /// A token to cancel the asynchronous operation. + public override async Task> QueryAsync(Func filter, CancellationToken ct = default) + { + var result = await _cassandraWrapper + .QueryAsync(e => filter(e.ToPayloadObject()), ct) + .ConfigureAwait(false); + + return result.Select(e => e.ToPayloadObject()); + } + + /// + public override void Dispose() + { + _cassandraWrapper?.Dispose(); + base.Dispose(); + } + } +} diff --git a/src/SharpConnector/Operations/OperationFactory.cs b/src/SharpConnector/Operations/OperationFactory.cs index 33b2ad3..b1520a5 100755 --- a/src/SharpConnector/Operations/OperationFactory.cs +++ b/src/SharpConnector/Operations/OperationFactory.cs @@ -31,6 +31,7 @@ public virtual IConnectorConfig GetConfigurationStrategy(IConfigurationSection s ConnectorTypeEnums.Couchbase => new CouchbaseConfig(section), ConnectorTypeEnums.DynamoDb => new DynamoDbConfig(section), ConnectorTypeEnums.ArangoDb => new ArangoDbConfig(section), + ConnectorTypeEnums.Cassandra => new CassandraConfig(section), _ => throw new ArgumentOutOfRangeException(nameof(connectorTypes), connectorTypes, "Unsupported connector type.") }; } diff --git a/src/SharpConnector/Operations/OperationsFactory.cs b/src/SharpConnector/Operations/OperationsFactory.cs index 16fa0e5..1fc069f 100755 --- a/src/SharpConnector/Operations/OperationsFactory.cs +++ b/src/SharpConnector/Operations/OperationsFactory.cs @@ -29,7 +29,8 @@ public OperationsFactory(IConfigurationSection section) { ConnectorTypeEnums.RavenDb, config => new RavenDbOperations((RavenDbConfig)config) }, { ConnectorTypeEnums.Couchbase, config => new CouchbaseOperations((CouchbaseConfig)config) }, { ConnectorTypeEnums.DynamoDb, config => new DynamoDbOperations((DynamoDbConfig)config) }, - { ConnectorTypeEnums.ArangoDb, config => new ArangoDbOperations((ArangoDbConfig)config) } + { ConnectorTypeEnums.ArangoDb, config => new ArangoDbOperations((ArangoDbConfig)config) }, + { ConnectorTypeEnums.Cassandra, config => new CassandraOperations((CassandraConfig)config) } }; } diff --git a/src/SharpConnector/SharpConnector.csproj b/src/SharpConnector/SharpConnector.csproj index 8b92a2d..9305f2c 100755 --- a/src/SharpConnector/SharpConnector.csproj +++ b/src/SharpConnector/SharpConnector.csproj @@ -20,6 +20,7 @@ + From 3b0abbb6053cd8286885958bbdf22dd377ddabb2 Mon Sep 17 00:00:00 2001 From: Francesco Del Re Date: Fri, 12 Jun 2026 10:00:13 +0200 Subject: [PATCH 10/13] chore(deps): update project dependencies --- .../SharpConnector.Api.csproj | 2 +- .../appsettings.cassandra.json | 10 ++++++++++ .../SharpConnector.Tests.csproj | 2 +- src/SharpConnector/SharpConnector.csproj | 18 +++++++++--------- 4 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 src/SharpConnector.Api/appsettings.cassandra.json diff --git a/src/SharpConnector.Api/SharpConnector.Api.csproj b/src/SharpConnector.Api/SharpConnector.Api.csproj index 1324936..118ca12 100644 --- a/src/SharpConnector.Api/SharpConnector.Api.csproj +++ b/src/SharpConnector.Api/SharpConnector.Api.csproj @@ -42,7 +42,7 @@ - + diff --git a/src/SharpConnector.Api/appsettings.cassandra.json b/src/SharpConnector.Api/appsettings.cassandra.json new file mode 100644 index 0000000..328eefe --- /dev/null +++ b/src/SharpConnector.Api/appsettings.cassandra.json @@ -0,0 +1,10 @@ +{ + "ConnectorConfig": { + "Instance": "Cassandra", + "ConnectionString": "127.0.0.1:9042", + "DatabaseName": "sharpconnector", + "TableName": "items", + "Username": "cassandra", + "Password": "cassandra" + } +} diff --git a/src/SharpConnector.Tests/SharpConnector.Tests.csproj b/src/SharpConnector.Tests/SharpConnector.Tests.csproj index 920cf21..b8a77a7 100644 --- a/src/SharpConnector.Tests/SharpConnector.Tests.csproj +++ b/src/SharpConnector.Tests/SharpConnector.Tests.csproj @@ -25,7 +25,7 @@ - + diff --git a/src/SharpConnector/SharpConnector.csproj b/src/SharpConnector/SharpConnector.csproj index 9305f2c..347cd2b 100755 --- a/src/SharpConnector/SharpConnector.csproj +++ b/src/SharpConnector/SharpConnector.csproj @@ -19,19 +19,19 @@ - + - + - - - - - + + + + + - - + + From 55559d8a00a80eee21facd2eedc00054f2c531df Mon Sep 17 00:00:00 2001 From: Francesco Del Re Date: Sun, 12 Jul 2026 21:17:01 +0200 Subject: [PATCH 11/13] chore: update project dependencies --- src/SharpConnector.Api/SharpConnector.Api.csproj | 2 +- src/SharpConnector.Tests/SharpConnector.Tests.csproj | 6 +++--- src/SharpConnector/SharpConnector.csproj | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/SharpConnector.Api/SharpConnector.Api.csproj b/src/SharpConnector.Api/SharpConnector.Api.csproj index 118ca12..0c0a2a5 100644 --- a/src/SharpConnector.Api/SharpConnector.Api.csproj +++ b/src/SharpConnector.Api/SharpConnector.Api.csproj @@ -42,7 +42,7 @@ - + diff --git a/src/SharpConnector.Tests/SharpConnector.Tests.csproj b/src/SharpConnector.Tests/SharpConnector.Tests.csproj index b8a77a7..b3874a7 100644 --- a/src/SharpConnector.Tests/SharpConnector.Tests.csproj +++ b/src/SharpConnector.Tests/SharpConnector.Tests.csproj @@ -25,10 +25,10 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/SharpConnector/SharpConnector.csproj b/src/SharpConnector/SharpConnector.csproj index 347cd2b..96a4279 100755 --- a/src/SharpConnector/SharpConnector.csproj +++ b/src/SharpConnector/SharpConnector.csproj @@ -19,7 +19,7 @@ - + @@ -28,10 +28,10 @@ - + - - + + From 22205d3ed68692f49d18372bb7d2c4a4c7b14215 Mon Sep 17 00:00:00 2001 From: Francesco Del Re Date: Fri, 17 Jul 2026 18:17:32 +0200 Subject: [PATCH 12/13] chore(deps): update dependencies --- src/SharpConnector.Tests/SharpConnector.Tests.csproj | 6 +++--- src/SharpConnector/SharpConnector.csproj | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/SharpConnector.Tests/SharpConnector.Tests.csproj b/src/SharpConnector.Tests/SharpConnector.Tests.csproj index b3874a7..9ada903 100644 --- a/src/SharpConnector.Tests/SharpConnector.Tests.csproj +++ b/src/SharpConnector.Tests/SharpConnector.Tests.csproj @@ -25,10 +25,10 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/SharpConnector/SharpConnector.csproj b/src/SharpConnector/SharpConnector.csproj index 96a4279..e3c44d8 100755 --- a/src/SharpConnector/SharpConnector.csproj +++ b/src/SharpConnector/SharpConnector.csproj @@ -19,15 +19,15 @@ - + - + - - - - + + + + From 9778c6dae05683ee81a06cfd23494f05104b142c Mon Sep 17 00:00:00 2001 From: Francesco Del Re Date: Fri, 17 Jul 2026 18:25:48 +0200 Subject: [PATCH 13/13] chore(deps): target .NET 10, bump to 4.0.2 and pin MessagePack 3.1.7 (CVE fix) --- src/SharpConnector.Api/SharpConnector.Api.csproj | 2 +- src/SharpConnector.Tests/SharpConnector.Tests.csproj | 4 ++-- src/SharpConnector/SharpConnector.csproj | 9 +++++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/SharpConnector.Api/SharpConnector.Api.csproj b/src/SharpConnector.Api/SharpConnector.Api.csproj index 0c0a2a5..46db46f 100644 --- a/src/SharpConnector.Api/SharpConnector.Api.csproj +++ b/src/SharpConnector.Api/SharpConnector.Api.csproj @@ -1,7 +1,7 @@ - net9.0 + net9.0;net10.0 enable enable false diff --git a/src/SharpConnector.Tests/SharpConnector.Tests.csproj b/src/SharpConnector.Tests/SharpConnector.Tests.csproj index 9ada903..227dbb0 100644 --- a/src/SharpConnector.Tests/SharpConnector.Tests.csproj +++ b/src/SharpConnector.Tests/SharpConnector.Tests.csproj @@ -1,7 +1,7 @@ - + - net9.0 + net9.0;net10.0 false diff --git a/src/SharpConnector/SharpConnector.csproj b/src/SharpConnector/SharpConnector.csproj index e3c44d8..14387ec 100755 --- a/src/SharpConnector/SharpConnector.csproj +++ b/src/SharpConnector/SharpConnector.csproj @@ -1,7 +1,7 @@  - net9.0 + net9.0;net10.0 LICENSE.txt MIT Francesco Del Re @@ -9,9 +9,9 @@ A flexible solution that accelerates integrations with multiple NoSQL databases https://github.com/engineering87/SharpConnector true - 4.0.1 - 3.3.0 - 3.3.0 + 4.0.2 + 4.0.2 + 4.0.2 logo.png README.md SharpConnector @@ -28,6 +28,7 @@ +