diff --git a/.github/workflows/Delete_build_and_test_on_main.yml b/.github/workflows/Delete_build_and_test_on_main.yml index ad724ee..4bc125d 100644 --- a/.github/workflows/Delete_build_and_test_on_main.yml +++ b/.github/workflows/Delete_build_and_test_on_main.yml @@ -2,7 +2,7 @@ name: Delete build main on: push: - branches: + branches: - master paths: - 'Frends.MongoDB.Delete/**' diff --git a/.github/workflows/Index_build_and_test_on_main.yml b/.github/workflows/Index_build_and_test_on_main.yml new file mode 100644 index 0000000..83cc8eb --- /dev/null +++ b/.github/workflows/Index_build_and_test_on_main.yml @@ -0,0 +1,18 @@ +name: Index build main + +on: + push: + branches: + - master + paths: + - 'Frends.MongoDB.Index/**' + workflow_dispatch: + +jobs: + build: + uses: FrendsPlatform/FrendsTasks/.github/workflows/linux_build_main.yml@main + with: + workdir: Frends.MongoDB.Index + prebuild_command: docker-compose -f ./Frends.MongoDB.Index.Tests/Files/docker-compose.yml up -d + secrets: + badge_service_api_key: ${{ secrets.BADGE_SERVICE_API_KEY }} diff --git a/.github/workflows/Index_build_and_test_on_push.yml b/.github/workflows/Index_build_and_test_on_push.yml new file mode 100644 index 0000000..5de0e5e --- /dev/null +++ b/.github/workflows/Index_build_and_test_on_push.yml @@ -0,0 +1,19 @@ +name: Index build test + +on: + push: + branches-ignore: + - master + paths: + - 'Frends.MongoDB.Index/**' + workflow_dispatch: + +jobs: + build: + uses: FrendsPlatform/FrendsTasks/.github/workflows/linux_build_test.yml@main + with: + workdir: Frends.MongoDB.Index + prebuild_command: docker-compose -f ./Frends.MongoDB.Index.Tests/Files/docker-compose.yml up -d + secrets: + badge_service_api_key: ${{ secrets.BADGE_SERVICE_API_KEY }} + test_feed_api_key: ${{ secrets.TASKS_TEST_FEED_API_KEY }} diff --git a/.github/workflows/Index_release.yml b/.github/workflows/Index_release.yml new file mode 100644 index 0000000..2432706 --- /dev/null +++ b/.github/workflows/Index_release.yml @@ -0,0 +1,12 @@ +name: Index release + +on: + workflow_dispatch: + +jobs: + build: + uses: FrendsPlatform/FrendsTasks/.github/workflows/release.yml@main + with: + workdir: Frends.MongoDB.Index + secrets: + feed_api_key: ${{ secrets.TASKS_FEED_API_KEY }} \ No newline at end of file diff --git a/.github/workflows/Insert_build_and_test_on_main.yml b/.github/workflows/Insert_build_and_test_on_main.yml index a0b158f..5b12981 100644 --- a/.github/workflows/Insert_build_and_test_on_main.yml +++ b/.github/workflows/Insert_build_and_test_on_main.yml @@ -2,7 +2,7 @@ name: Insert build main on: push: - branches: + branches: - master paths: - 'Frends.MongoDB.Insert/**' diff --git a/.github/workflows/Query_build_and_test_on_main.yml b/.github/workflows/Query_build_and_test_on_main.yml index 15d8970..c0e8f64 100644 --- a/.github/workflows/Query_build_and_test_on_main.yml +++ b/.github/workflows/Query_build_and_test_on_main.yml @@ -2,7 +2,7 @@ name: Query build main on: push: - branches: + branches: - master paths: - 'Frends.MongoDB.Query/**' diff --git a/Frends.MongoDB.Delete/CHANGELOG.md b/Frends.MongoDB.Delete/CHANGELOG.md index bc0d207..fcf51e4 100644 --- a/Frends.MongoDB.Delete/CHANGELOG.md +++ b/Frends.MongoDB.Delete/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [2.0.0] - 2025-03-12 +### Added +- Adds caching for the MongoClient connection to improve performance. + +### Updated +- Updated MongoDB.Driver to version 3.2.1 + +### Breaking changes +- The MongoDB driver drops support for MongoDB Server v3.6 and earlier. +- The MongoDB driver drops support for .NET Core 2.x and .NET Framework 4.6. +- Read more about MongoDB driver 3.0 breaking changes [here](https://mongodb.github.io/mongo-csharp-driver/3.0/reference/breaking_changes/) + ## [1.0.1] - 2023-11-22 ### Fixed - Fixed dll error when importing the Task to Frends by adding local dll reference to the project file. diff --git a/Frends.MongoDB.Delete/Frends.MongoDB.Delete.Tests/Files/docker-compose.yml b/Frends.MongoDB.Delete/Frends.MongoDB.Delete.Tests/Files/docker-compose.yml index cd14b47..56b9789 100644 --- a/Frends.MongoDB.Delete/Frends.MongoDB.Delete.Tests/Files/docker-compose.yml +++ b/Frends.MongoDB.Delete/Frends.MongoDB.Delete.Tests/Files/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: mongo: - image: mongo:3 + image: mongo:7 environment: - AUTH=yes - MONGO_INITDB_ROOT_USERNAME=admin diff --git a/Frends.MongoDB.Delete/Frends.MongoDB.Delete.Tests/Frends.MongoDB.Delete.Tests.csproj b/Frends.MongoDB.Delete/Frends.MongoDB.Delete.Tests/Frends.MongoDB.Delete.Tests.csproj index ee26df2..262fd45 100644 --- a/Frends.MongoDB.Delete/Frends.MongoDB.Delete.Tests/Frends.MongoDB.Delete.Tests.csproj +++ b/Frends.MongoDB.Delete/Frends.MongoDB.Delete.Tests/Frends.MongoDB.Delete.Tests.csproj @@ -9,16 +9,15 @@ - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - - - diff --git a/Frends.MongoDB.Delete/Frends.MongoDB.Delete.Tests/UnitTests.cs b/Frends.MongoDB.Delete/Frends.MongoDB.Delete.Tests/UnitTests.cs index eeadac0..7d54e44 100644 --- a/Frends.MongoDB.Delete/Frends.MongoDB.Delete.Tests/UnitTests.cs +++ b/Frends.MongoDB.Delete/Frends.MongoDB.Delete.Tests/UnitTests.cs @@ -8,21 +8,23 @@ namespace Frends.MongoDB.Delete.Tests; [TestClass] public class UnitTests { - /// - /// Run command 'docker-compose up -d' in \Frends.MongoDB.Delete.Tests\Files\ - /// - - private static readonly Connection _connection = new() - { - ConnectionString = "mongodb://admin:Salakala@localhost:27017/?authSource=admin", - Database = "testdb", - CollectionName = "testcoll", - }; - - private readonly string _doc1 = "{ \"foo\":\"bar\", \"bar\": \"foo\" }"; - private readonly string _doc2 = "{ \"foo\":\"bar\", \"bar\": \"foo\" }"; - private readonly string _doc3 = "{ \"qwe\":\"rty\", \"asd\": \"fgh\" }"; - + /// + /// Run command 'docker-compose up -d' in \Frends.MongoDB.Delete.Tests\Files\ + /// + + private static readonly Connection _connection = new() + { + ConnectionString = "mongodb://admin:Salakala@localhost:27017/?authSource=admin", + Database = "testdb", + CollectionName = "testcoll", + }; + + private readonly List _documents = new() + { + "{ 'foo':'bar', 'bar': 'foo' }", + "{ 'foo':'bar', 'bar': 'foo' }", + "{ 'qwe':'rty', 'asd': 'fgh' }" + }; [TestInitialize] public void StartUp() @@ -184,38 +186,45 @@ public void Test_InvalidConnectionString() Assert.IsTrue(ex.Result.Message.StartsWith("Delete error: System.Exception: DeleteOperation error: MongoDB.Driver.MongoAuthenticationException: Unable to authenticate using sasl protocol mechanism SCRAM-SHA-1.")); } - private void InsertTestData() - { - try - { - var collection = GetMongoCollection(_connection.ConnectionString, _connection.Database, _connection.CollectionName); - - var doc1 = BsonDocument.Parse(_doc1); - var doc2 = BsonDocument.Parse(_doc2); - var doc3 = BsonDocument.Parse(_doc3); - - collection.InsertOne(doc1); - collection.InsertOne(doc2); - collection.InsertOne(doc3); - } - catch (Exception ex) - { - throw new Exception(ex.Message); - } - } - private static void DeleteTestData() - { - var collection = GetMongoCollection(_connection.ConnectionString, _connection.Database, _connection.CollectionName); - - var filter1 = "{'bar':'foo'}"; - var filter2 = "{'qwe':'rty'}"; - var filter3 = "{'asd':'fgh'}"; - collection.DeleteMany(filter1); - collection.DeleteMany(filter2); - collection.DeleteMany(filter3); - } - - private static IMongoCollection GetMongoCollection(string connectionString, string database, string collectionName) + private void InsertTestData() + { + try + { + var collection = GetMongoCollection(_connection.ConnectionString, _connection.Database, _connection.CollectionName); + + foreach (var doc in _documents) + { + collection.InsertOne(BsonDocument.Parse(doc)); + } + } + catch (Exception ex) + { + throw new Exception(ex.Message); + } + } + + private static void DeleteTestData() + { + var collection = GetMongoCollection(_connection.ConnectionString, _connection.Database, _connection.CollectionName); + + List filters = new() + { + "{'bar':'foo'}", + "{'qwe':'rty'}", + "{'asd':'fgh'}", + "{foo:'update'}", + "{'foobar':'upsert_create'}", + "{'array':'arr'}" + }; + + foreach (var filter in filters) + { + collection.DeleteMany(filter); + } + } + + + private static IMongoCollection GetMongoCollection(string connectionString, string database, string collectionName) { var dataBase = GetMongoDatabase(connectionString, database); var collection = dataBase.GetCollection(collectionName); diff --git a/Frends.MongoDB.Delete/Frends.MongoDB.Delete/Delete.cs b/Frends.MongoDB.Delete/Frends.MongoDB.Delete/Delete.cs index 2c9ab94..f1fc913 100644 --- a/Frends.MongoDB.Delete/Frends.MongoDB.Delete/Delete.cs +++ b/Frends.MongoDB.Delete/Frends.MongoDB.Delete/Delete.cs @@ -6,6 +6,9 @@ using MongoDB.Driver; using System.IO; using System.Threading.Tasks; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Caching; +using System.Linq; namespace Frends.MongoDB.Delete; @@ -14,15 +17,18 @@ namespace Frends.MongoDB.Delete; /// public class MongoDB { - /// - /// MongoDB delete operation. - /// [Documentation](https://tasks.frends.com/tasks/frends-tasks/Frends.MongoDB.Delete) - /// - /// Input parameters. - /// Connection parameters. - /// Token generated by Frends to stop this task. - /// Object { bool Success, long Count } - public static async Task Delete([PropertyTab] Input input, [PropertyTab] Connection connection, CancellationToken cancellationToken) + internal static readonly ObjectCache ClientCache = MemoryCache.Default; + private static readonly CacheItemPolicy _cachePolicy = new() { SlidingExpiration = TimeSpan.FromHours(1) }; + + /// + /// MongoDB delete operation. + /// [Documentation](https://tasks.frends.com/tasks/frends-tasks/Frends.MongoDB.Delete) + /// + /// Input parameters. + /// Connection parameters. + /// Token generated by Frends to stop this task. + /// Object { bool Success, long Count } + public static async Task Delete([PropertyTab] Input input, [PropertyTab] Connection connection, CancellationToken cancellationToken) { var collection = GetMongoCollection(connection.ConnectionString, connection.Database, connection.CollectionName); @@ -41,7 +47,10 @@ public static async Task Delete([PropertyTab] Input input, [PropertyTab] } case InputType.Filter: - return new Result(true, await DeleteOperation(input, BsonDocument.Parse(input.Filter), collection, cancellationToken)); + if (string.IsNullOrWhiteSpace(input.Filter)) + throw new ArgumentException("Filter string missing."); + + return new Result(true, await DeleteOperation(input, BsonDocument.Parse(input.Filter), collection, cancellationToken)); case InputType.Filters: long count = 0; @@ -80,31 +89,48 @@ private static async Task DeleteOperation(Input input, BsonDocument filter } } - private static IMongoCollection GetMongoCollection(string connectionString, string database, string collectionName) - { - try - { - var dataBase = GetMongoDatabase(connectionString, database); - var collection = dataBase.GetCollection(collectionName); - return collection; - } - catch (Exception ex) - { - throw new Exception($"GetMongoCollection error: {ex}"); - } - } + private static IMongoCollection GetMongoCollection(string connectionString, string database, string collectionName) + { + try + { + var dataBase = GetMongoDatabase(connectionString, database, collectionName); + var collection = dataBase.GetCollection(collectionName); + return collection; + } + catch (Exception ex) + { + throw new Exception($"GetMongoCollection error: {ex}"); + } + } - private static IMongoDatabase GetMongoDatabase(string connectionString, string database) - { - try - { - var mongoClient = new MongoClient(connectionString); - var dataBase = mongoClient.GetDatabase(database); - return dataBase; - } - catch (Exception ex) - { - throw new Exception($"GetMongoDatabase error: {ex}"); - } - } + private static IMongoDatabase GetMongoDatabase(string connectionString, string database, string collectionName) + { + var cacheKey = GetMongoDbCacheKey(connectionString, database, collectionName); + + if (ClientCache.Get(cacheKey) is IMongoDatabase mongoDatabase) + { + return mongoDatabase; + } + + try + { + var mongoClient = new MongoClient(connectionString); + var dataBase = mongoClient.GetDatabase(database); + + ClientCache.Add(cacheKey, dataBase, _cachePolicy); + + return dataBase; + } + catch (Exception ex) + { + throw new Exception($"GetMongoDatabase error: {ex}"); + } + } + + + [ExcludeFromCodeCoverage] + private static string GetMongoDbCacheKey(string connectionString, string database, string collectionName) + { + return $"{connectionString.GetHashCode()}:{database}:{collectionName}"; + } } \ No newline at end of file diff --git a/Frends.MongoDB.Delete/Frends.MongoDB.Delete/Frends.MongoDB.Delete.csproj b/Frends.MongoDB.Delete/Frends.MongoDB.Delete/Frends.MongoDB.Delete.csproj index 8862a2e..c23d6e8 100644 --- a/Frends.MongoDB.Delete/Frends.MongoDB.Delete/Frends.MongoDB.Delete.csproj +++ b/Frends.MongoDB.Delete/Frends.MongoDB.Delete/Frends.MongoDB.Delete.csproj @@ -2,7 +2,7 @@ net6.0 - 1.0.1 + 2.0.0 Frends Frends Frends @@ -19,33 +19,12 @@ PreserveNewest - - true - - - true - - - - - - - - - - - - - + + + + + - - - lib\MongoDB.Driver.dll - - - lib\MongoDB.Driver.Core.dll - - \ No newline at end of file diff --git a/Frends.MongoDB.Delete/Frends.MongoDB.Delete/lib/MongoDB.Driver.Core.dll b/Frends.MongoDB.Delete/Frends.MongoDB.Delete/lib/MongoDB.Driver.Core.dll deleted file mode 100644 index ef4f87f..0000000 Binary files a/Frends.MongoDB.Delete/Frends.MongoDB.Delete/lib/MongoDB.Driver.Core.dll and /dev/null differ diff --git a/Frends.MongoDB.Delete/Frends.MongoDB.Delete/lib/MongoDB.Driver.dll b/Frends.MongoDB.Delete/Frends.MongoDB.Delete/lib/MongoDB.Driver.dll deleted file mode 100644 index a5f429a..0000000 Binary files a/Frends.MongoDB.Delete/Frends.MongoDB.Delete/lib/MongoDB.Driver.dll and /dev/null differ diff --git a/Frends.MongoDB.Delete/README.md b/Frends.MongoDB.Delete/README.md index a02b2d4..63a9483 100644 --- a/Frends.MongoDB.Delete/README.md +++ b/Frends.MongoDB.Delete/README.md @@ -8,7 +8,7 @@ Frends Task for MongoDB delete operation. # Installing -You can install the Task via Frends UI Task View or you can find the NuGet package from the following NuGet feed https://www.myget.org/F/frends-tasks/api/v2. +You can install the Task via Frends UI Task View or you can find the NuGet package from the following NuGet feed [NuGet Package](https://www.myget.org/F/frends-tasks/api/v2) ## Building diff --git a/Frends.MongoDB.Index/CHANGELOG.md b/Frends.MongoDB.Index/CHANGELOG.md new file mode 100644 index 0000000..346f471 --- /dev/null +++ b/Frends.MongoDB.Index/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## [2.0.0] - 2025-03-12 +### Added +- Initial implementation \ No newline at end of file diff --git a/Frends.MongoDB.Index/Frends.MongoDB.Index.Tests/Files/.env b/Frends.MongoDB.Index/Frends.MongoDB.Index.Tests/Files/.env new file mode 100644 index 0000000..1b25ac1 --- /dev/null +++ b/Frends.MongoDB.Index/Frends.MongoDB.Index.Tests/Files/.env @@ -0,0 +1 @@ +MONGO_HOST_DATA=/mongodb \ No newline at end of file diff --git a/Frends.MongoDB.Index/Frends.MongoDB.Index.Tests/Files/docker-compose.yml b/Frends.MongoDB.Index/Frends.MongoDB.Index.Tests/Files/docker-compose.yml new file mode 100644 index 0000000..56b9789 --- /dev/null +++ b/Frends.MongoDB.Index/Frends.MongoDB.Index.Tests/Files/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3' +services: + mongo: + image: mongo:7 + environment: + - AUTH=yes + - MONGO_INITDB_ROOT_USERNAME=admin + - MONGO_INITDB_ROOT_PASSWORD=Salakala + volumes: + - ${MONGO_HOST_DATA}/db:/data/db + ports: + - "27017:27017" \ No newline at end of file diff --git a/Frends.MongoDB.Index/Frends.MongoDB.Index.Tests/Files/testdata.json b/Frends.MongoDB.Index/Frends.MongoDB.Index.Tests/Files/testdata.json new file mode 100644 index 0000000..aa8b20f --- /dev/null +++ b/Frends.MongoDB.Index/Frends.MongoDB.Index.Tests/Files/testdata.json @@ -0,0 +1,4 @@ +{ + "foo": "bar", + "bar": "foo" +} \ No newline at end of file diff --git a/Frends.MongoDB.Index/Frends.MongoDB.Index.Tests/Frends.MongoDB.Index.Tests.csproj b/Frends.MongoDB.Index/Frends.MongoDB.Index.Tests/Frends.MongoDB.Index.Tests.csproj new file mode 100644 index 0000000..7e8d72b --- /dev/null +++ b/Frends.MongoDB.Index/Frends.MongoDB.Index.Tests/Frends.MongoDB.Index.Tests.csproj @@ -0,0 +1,25 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + diff --git a/Frends.MongoDB.Index/Frends.MongoDB.Index.Tests/UnitTests.cs b/Frends.MongoDB.Index/Frends.MongoDB.Index.Tests/UnitTests.cs new file mode 100644 index 0000000..3aa9d0c --- /dev/null +++ b/Frends.MongoDB.Index/Frends.MongoDB.Index.Tests/UnitTests.cs @@ -0,0 +1,207 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Frends.MongoDB.Index.Definitions; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Frends.MongoDB.Index.Tests; + +[TestClass] +public class UnitTests +{ + /// + /// Run command 'docker-compose up -d' in \Frends.MongoDB.Index.Tests\Files\ + /// + + private static readonly Connection _connection = new() + { + ConnectionString = "mongodb://admin:Salakala@localhost:27017/?authSource=admin", + Database = "testdb", + CollectionName = "testcoll", + }; + + [TestCleanup] + public void CleanUp() + { + DeleteTestData(); + } + + [TestMethod] + public async Task Test_Create_Single_Field_Index_Generate_Name() + { + var _input = new Input() + { + IndexAction = IndexAction.Create, + Fields = new FieldNames[] + { + new() { Value = "foo" } + }, + DropExistingIndex = false + }; + + var result = await MongoDB.Index(_input, _connection, default); + Assert.IsTrue(result.Success); + Assert.AreEqual("foo_1", result.IndexName); + } + + [TestMethod] + public async Task Test_Multi_Field_Index_Generate_Name() + { + var _input = new Input() + { + IndexAction = IndexAction.Create, + Fields = new FieldNames[] + { + new() { Value = "foo" }, + new() { Value = "bar", } + }, + DropExistingIndex = false + }; + + var result = await MongoDB.Index(_input, _connection, default); + Assert.IsTrue(result.Success); + Assert.AreEqual("foo_1_bar_1", result.IndexName); + } + + [TestMethod] + public async Task Test_Create_Index_With_Given_Name() + { + var _input = new Input() + { + IndexAction = IndexAction.Create, + Fields = new FieldNames[] + { + new() { Value = "foobar" } + }, + IndexName = "foobar_index", + DropExistingIndex = false + }; + + var result = await MongoDB.Index(_input, _connection, default); + Assert.IsTrue(result.Success); + Assert.AreEqual("foobar_index", result.IndexName); + } + + [TestMethod] + public async Task Test_Try_Create_Index_Without_Fields() + { + var _input = new Input() + { + IndexAction = IndexAction.Create, + IndexName = "nofields", + DropExistingIndex = false + }; + + var ex = await Assert.ThrowsExceptionAsync(async () => await MongoDB.Index(_input, _connection, default)); + Assert.IsTrue(ex.Message.StartsWith("Index error: System.ArgumentException: Field name(s) missing.")); + } + + [TestMethod] + public async Task Test_Try_Create_Index_Where_Already_Exists() + { + var _input = new Input() + { + IndexAction = IndexAction.Create, + Fields = new FieldNames[] + { + new() { Value = "existing" } + }, + DropExistingIndex = false + }; + + await MongoDB.Index(_input, _connection, default); + + _input = new Input() + { + IndexAction = IndexAction.Create, + Fields = new FieldNames[] + { + new() { Value = "existing" } + }, + IndexName = "existing_index", + DropExistingIndex = false + }; + + var ex = await Assert.ThrowsExceptionAsync(async () => await MongoDB.Index(_input, _connection, default)); + Assert.IsTrue(ex.Message.StartsWith("Index error: MongoDB.Driver.MongoCommandException: Command createIndexes failed: Index already exists with a different name: existing_1.")); + } + + [TestMethod] + public async Task Test_Drop_Index_And_Create_With_Same_Name() + { + var _input = new Input() + { + IndexAction = IndexAction.Create, + Fields = new FieldNames[] + { + new() { Value = "dropandcreate" } + }, + IndexName = "dropandcreate", + DropExistingIndex = false + }; + + await MongoDB.Index(_input, _connection, default); + + _input = new Input() + { + IndexAction = IndexAction.Create, + Fields = new FieldNames[] + { + new() { Value = "dropandcreate" }, + new() { Value = "foo" } + }, + IndexName = "dropandcreate", + DropExistingIndex = true + }; + + var result = await MongoDB.Index(_input, _connection, default); + Assert.IsTrue(result.Success); + Assert.AreEqual("dropandcreate", result.IndexName); + } + + [TestMethod] + public async Task Test_Drop_Index() + { + var _input = new Input() + { + IndexAction = IndexAction.Create, + Fields = new FieldNames[] + { + new() { Value = "drop" } + }, + IndexName = "drop", + DropExistingIndex = false + }; + + await MongoDB.Index(_input, _connection, default); + + _input = new Input() + { + IndexAction = IndexAction.Drop, + IndexName = "drop" + }; + + var result = await MongoDB.Index(_input, _connection, default); + Assert.IsTrue(result.Success); + Assert.AreEqual("drop", result.IndexName); + } + + private static void DeleteTestData() + { + var collection = GetMongoCollection(_connection.ConnectionString, _connection.Database, _connection.CollectionName); + collection.Indexes.DropAll(); + } + + private static IMongoCollection GetMongoCollection(string connectionString, string database, string collectionName) + { + var dataBase = GetMongoDatabase(connectionString, database); + var collection = dataBase.GetCollection(collectionName); + return collection; + } + + private static IMongoDatabase GetMongoDatabase(string connectionString, string database) + { + var mongoClient = new MongoClient(connectionString); + var dataBase = mongoClient.GetDatabase(database); + return dataBase; + } +} \ No newline at end of file diff --git a/Frends.MongoDB.Index/Frends.MongoDB.Index.sln b/Frends.MongoDB.Index/Frends.MongoDB.Index.sln new file mode 100644 index 0000000..179a869 --- /dev/null +++ b/Frends.MongoDB.Index/Frends.MongoDB.Index.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35707.178 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Frends.MongoDB.Index", "Frends.MongoDB.Index\Frends.MongoDB.Index.csproj", "{8FB64BB3-C91A-4829-B212-2C967E6830AB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Frends.MongoDB.Index.Tests", "Frends.MongoDB.Index.Tests\Frends.MongoDB.Index.Tests.csproj", "{03BB9C11-A8A1-4E29-A012-F6CC9C862337}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8779CBED-E6FB-4F23-99E2-C6AAC2A85505}" + ProjectSection(SolutionItems) = preProject + CHANGELOG.md = CHANGELOG.md + ..\.github\workflows\Index_build_and_test_on_main.yml = ..\.github\workflows\Index_build_and_test_on_main.yml + ..\.github\workflows\Index_build_and_test_on_push.yml = ..\.github\workflows\Index_build_and_test_on_push.yml + ..\.github\workflows\Index_release.yml = ..\.github\workflows\Index_release.yml + README.md = README.md + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8FB64BB3-C91A-4829-B212-2C967E6830AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8FB64BB3-C91A-4829-B212-2C967E6830AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8FB64BB3-C91A-4829-B212-2C967E6830AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8FB64BB3-C91A-4829-B212-2C967E6830AB}.Release|Any CPU.Build.0 = Release|Any CPU + {03BB9C11-A8A1-4E29-A012-F6CC9C862337}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03BB9C11-A8A1-4E29-A012-F6CC9C862337}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03BB9C11-A8A1-4E29-A012-F6CC9C862337}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03BB9C11-A8A1-4E29-A012-F6CC9C862337}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Frends.MongoDB.Index/Frends.MongoDB.Index/Definitions/Connection.cs b/Frends.MongoDB.Index/Frends.MongoDB.Index/Definitions/Connection.cs new file mode 100644 index 0000000..3981ebd --- /dev/null +++ b/Frends.MongoDB.Index/Frends.MongoDB.Index/Definitions/Connection.cs @@ -0,0 +1,29 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Frends.MongoDB.Index.Definitions; + +/// +/// Connection parameters. +/// +public class Connection +{ + /// + /// Connection string. + /// + /// mongodb://foo:bar@localhost:00000/?authSource=admin + [PasswordPropertyText] + public string ConnectionString { get; set; } + + /// + /// Database. + /// + /// foo + public string Database { get; set; } + + /// + /// Collection name. + /// + /// bar + public string CollectionName { get; set; } +} \ No newline at end of file diff --git a/Frends.MongoDB.Index/Frends.MongoDB.Index/Definitions/Enums.cs b/Frends.MongoDB.Index/Frends.MongoDB.Index/Definitions/Enums.cs new file mode 100644 index 0000000..01475c0 --- /dev/null +++ b/Frends.MongoDB.Index/Frends.MongoDB.Index/Definitions/Enums.cs @@ -0,0 +1,16 @@ +namespace Frends.MongoDB.Index.Definitions; + +/// +/// Index options. +/// +public enum IndexAction +{ + /// + /// Crrates an index. + /// + Create, + /// + /// Deltes an index. + /// + Drop +} \ No newline at end of file diff --git a/Frends.MongoDB.Index/Frends.MongoDB.Index/Definitions/Input.cs b/Frends.MongoDB.Index/Frends.MongoDB.Index/Definitions/Input.cs new file mode 100644 index 0000000..166bb58 --- /dev/null +++ b/Frends.MongoDB.Index/Frends.MongoDB.Index/Definitions/Input.cs @@ -0,0 +1,49 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; +using System.Text.RegularExpressions; + +namespace Frends.MongoDB.Index.Definitions; + +/// +/// Input parameter. +/// +public class Input +{ + /// + /// Creates or deletes an index. + /// + /// IndexAction.Create + [DefaultValue(IndexAction.Create)] + public IndexAction IndexAction { get; set; } + + /// + /// Name of the index, if not specified, MongoDB will generate a name. + /// + [DefaultValue("")] + public string IndexName { get; set; } + + /// + /// An array of field name(s) to be included in the index. + /// + /// name, postalCode + [UIHint(nameof(IndexAction), "", IndexAction.Create)] + public FieldNames[] Fields { get; set; } + + /// + /// Specifies whether to drop the existing index with the same name. + /// + [UIHint(nameof(IndexAction), "", IndexAction.Create)] + public bool DropExistingIndex { get; set; } = false; +} + +/// +/// Input.FieldName values. +/// +public class FieldNames +{ + /// + /// Value. + /// + /// name + public string Value { get; set; } +} \ No newline at end of file diff --git a/Frends.MongoDB.Index/Frends.MongoDB.Index/Definitions/Result.cs b/Frends.MongoDB.Index/Frends.MongoDB.Index/Definitions/Result.cs new file mode 100644 index 0000000..6141821 --- /dev/null +++ b/Frends.MongoDB.Index/Frends.MongoDB.Index/Definitions/Result.cs @@ -0,0 +1,25 @@ +namespace Frends.MongoDB.Index.Definitions; + +/// +/// Task's result. +/// +public class Result +{ + /// + /// Operation completed successfully. + /// + /// true + public bool Success { get; private set; } + + /// + /// Name of the created index. + /// + /// 1 + public string IndexName { get; private set; } + + internal Result(bool success, string indexName) + { + Success = success; + IndexName = indexName; + } +} \ No newline at end of file diff --git a/Frends.MongoDB.Index/Frends.MongoDB.Index/Frends.MongoDB.Index.csproj b/Frends.MongoDB.Index/Frends.MongoDB.Index/Frends.MongoDB.Index.csproj new file mode 100644 index 0000000..8bba96a --- /dev/null +++ b/Frends.MongoDB.Index/Frends.MongoDB.Index/Frends.MongoDB.Index.csproj @@ -0,0 +1,30 @@ + + + + net6.0 + 2.0.0 + Frends + Frends + Frends + Frends + Frends + MIT + true + Frends Task for MongoDB index operations. + https://frends.com/ + https://github.com/FrendsPlatform/Frends.MongoDB + + + + + PreserveNewest + + + + + + + + + + diff --git a/Frends.MongoDB.Index/Frends.MongoDB.Index/FrendsTaskMetadata.json b/Frends.MongoDB.Index/Frends.MongoDB.Index/FrendsTaskMetadata.json new file mode 100644 index 0000000..07fd006 --- /dev/null +++ b/Frends.MongoDB.Index/Frends.MongoDB.Index/FrendsTaskMetadata.json @@ -0,0 +1,7 @@ +{ + "Tasks": [ + { + "TaskMethod": "Frends.MongoDB.Index.MongoDB.Index" + } + ] +} \ No newline at end of file diff --git a/Frends.MongoDB.Index/Frends.MongoDB.Index/Index.cs b/Frends.MongoDB.Index/Frends.MongoDB.Index/Index.cs new file mode 100644 index 0000000..a4a77d5 --- /dev/null +++ b/Frends.MongoDB.Index/Frends.MongoDB.Index/Index.cs @@ -0,0 +1,151 @@ +using Frends.MongoDB.Index.Definitions; +using MongoDB.Bson; +using MongoDB.Driver; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using System.Runtime.Caching; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Frends.MongoDB.Index; + +/// +/// MongoDB Task. +/// +public class MongoDB +{ + internal static readonly ObjectCache ClientCache = MemoryCache.Default; + private static readonly CacheItemPolicy _cachePolicy = new() { SlidingExpiration = TimeSpan.FromHours(1) }; + + /// + /// MongoDB index operation. + /// [Documentation](https://tasks.frends.com/tasks/frends-tasks/Frends.MongoDB.Index) + /// + /// Input parameters. + /// Connection parameters. + /// Token generated by Frends to stop this task. + /// Object { bool Success, long Count } + public static async Task Index([PropertyTab] Input input, [PropertyTab] Connection connection, CancellationToken cancellationToken) + { + var collection = GetMongoCollection(connection.ConnectionString, connection.Database, connection.CollectionName); + + try + { + switch (input.IndexAction) + { + case IndexAction.Create: + if (input.Fields == null || input.Fields?.Length == 0) + throw new ArgumentException("Field name(s) missing."); + + return new Result(true, await CreateIndex(collection, input, cancellationToken)); + + case IndexAction.Drop: + await DropIndex(collection, input, cancellationToken); + return new Result(true, input.IndexName); + + default: + return new Result(false, ""); + }; + } + catch (Exception ex) + { + throw new Exception($"Index error: {ex}"); + } + + + + + + + + //var compoundIndex = new CreateIndexModel(Builders.IndexKeys + // .Ascending(m => m.Type) + // .Ascending(m => m.Rated)); + //collection.Indexes.CreateOne(compoundIndex); + + //var foo = new CreateIndexOptions(); + //foo.Collation = new Collation("en", strength: CollationStrength.Primary); + //var indexModel = new CreateIndexModel(Builders.IndexKeys + //.Ascending(m => m.) + //collection.Indexes.CreateOne(indexModel); + } + + private static async Task CreateIndex(IMongoCollection collection, Input input, CancellationToken cancellationToken) + { + if (input.DropExistingIndex && !string.IsNullOrWhiteSpace(input.IndexName)) + { + await DropIndex(collection, input, cancellationToken); + } + + var indexOptions = new CreateIndexOptions { Name = string.IsNullOrWhiteSpace(input.IndexName) ? null : input.IndexName }; + var indexKeys = Builders.IndexKeys.Combine(input.Fields.Select(field => Builders.IndexKeys.Ascending(field.Value))); + + var indexModel = new CreateIndexModel(indexKeys, indexOptions); + var name = await collection.Indexes.CreateOneAsync(indexModel, cancellationToken: cancellationToken); + return name; + } + + private static async Task DropIndex(IMongoCollection collection, Input input, CancellationToken cancellationToken) + { + await collection.Indexes.DropOneAsync(input.IndexName, cancellationToken); + } + + private static IMongoCollection GetMongoCollection(string connectionString, string database, string collectionName) + { + try + { + var dataBase = GetMongoDatabase(connectionString, database, collectionName); + var collection = dataBase.GetCollection(collectionName); + return collection; + } + catch (Exception ex) + { + throw new Exception($"GetMongoCollection error: {ex}"); + } + } + + private static IMongoDatabase GetMongoDatabase(string connectionString, string database, string collectionName) + { + var cacheKey = GetMongoDbCacheKey(connectionString, database, collectionName); + + if (ClientCache.Get(cacheKey) is IMongoDatabase mongoDatabase) + { + return mongoDatabase; + } + + try + { + var mongoClient = new MongoClient(connectionString); + var dataBase = mongoClient.GetDatabase(database); + + ClientCache.Add(cacheKey, dataBase, _cachePolicy); + + return dataBase; + } + catch (Exception ex) + { + throw new Exception($"GetMongoDatabase error: {ex}"); + } + } + + internal static void ClearClientCache() + { + var cacheKeys = ClientCache.Select(kvp => kvp.Key).ToList(); + foreach (var cacheKey in cacheKeys) + { + ClientCache.Remove(cacheKey); + } + } + + [ExcludeFromCodeCoverage] + private static string GetMongoDbCacheKey(string connectionString, string database, string collectionName) + { + return $"{connectionString.GetHashCode()}:{database}:{collectionName}"; + } +} diff --git a/Frends.MongoDB.Index/Frends.MongoDB.Index/README.md b/Frends.MongoDB.Index/Frends.MongoDB.Index/README.md new file mode 100644 index 0000000..50a5247 --- /dev/null +++ b/Frends.MongoDB.Index/Frends.MongoDB.Index/README.md @@ -0,0 +1,29 @@ +# Frends.MongoDB.Index +Frends Task for MongoDB index operation. + +[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) +[![Build](https://github.com/FrendsPlatform/Frends.MongoDB/actions/workflows/Index_build_and_test_on_main.yml/badge.svg)](https://github.com/FrendsPlatform/Frends.MongoDB/actions) +![MyGet](https://img.shields.io/myget/frends-tasks/v/Frends.MongoDB.Index) +![Coverage](https://app-github-custom-badges.azurewebsites.net/Badge?key=FrendsPlatform/Frends.MongoDB/Frends.MongoDB.Index|main) + +# Installing + +You can install the Task via Frends UI Task View or you can find the NuGet package from the following NuGet feed https://www.myget.org/F/frends-tasks/api/v2. + +## Building + + +Rebuild the project + +`dotnet build` + +Run tests + +Run command `docker-compose up -d` in \Frends.MongoDB.Index.Tests\Files\ + +`dotnet test` + + +Create a NuGet package + +`dotnet pack --configuration Release` \ No newline at end of file diff --git a/Frends.MongoDB.Index/README.md b/Frends.MongoDB.Index/README.md new file mode 100644 index 0000000..be00537 --- /dev/null +++ b/Frends.MongoDB.Index/README.md @@ -0,0 +1,29 @@ +# Frends.MongoDB.Index +Frends Task for MongoDB index operations. + +[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) +[![Build](https://github.com/FrendsPlatform/Frends.MongoDB/actions/workflows/Delete_build_and_test_on_main.yml/badge.svg)](https://github.com/FrendsPlatform/Frends.MongoDB/actions) +![MyGet](https://img.shields.io/myget/frends-tasks/v/Frends.MongoDB.Index) +![Coverage](https://app-github-custom-badges.azurewebsites.net/Badge?key=FrendsPlatform/Frends.MongoDB/Frends.MongoDB.Index|main) + +# Installing + +You can install the Task via Frends UI Task View or you can find the NuGet package from the following NuGet feed [NuGet Package](https://www.myget.org/F/frends-tasks/api/v2) + +## Building + + +Rebuild the project + +`dotnet build` + +Run tests + +Run commands `docker-compose up -d` in \Frends.MongoDB.Index.Tests\Files\ + +`dotnet test` + + +Create a NuGet package + +`dotnet pack --configuration Release` \ No newline at end of file diff --git a/Frends.MongoDB.Insert/CHANGELOG.md b/Frends.MongoDB.Insert/CHANGELOG.md index 4ba4d54..a0d069d 100644 --- a/Frends.MongoDB.Insert/CHANGELOG.md +++ b/Frends.MongoDB.Insert/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## [2.0.0] - 2025-03-12 +### Added +- Adds caching for the MongoClient connection to improve performance. + +### Updated +- Updated MongoDB.Driver to version 3.2.1 + +- ### Breaking changes +- The MongoDB driver drops support for MongoDB Server v3.6 and earlier. +- The MongoDB driver drops support for .NET Core 2.x and .NET Framework 4.6. +- Read more about MongoDB driver 3.0 breaking changes [here](https://mongodb.github.io/mongo-csharp-driver/3.0/reference/breaking_changes/) + ## [1.0.1] - 2023-11-22 ### Fixed - Fixed dll error when importing the Task to Frends by adding local dll reference to the project file. diff --git a/Frends.MongoDB.Insert/Frends.MongoDB.Insert.Tests/Files/docker-compose.yml b/Frends.MongoDB.Insert/Frends.MongoDB.Insert.Tests/Files/docker-compose.yml index cd14b47..56b9789 100644 --- a/Frends.MongoDB.Insert/Frends.MongoDB.Insert.Tests/Files/docker-compose.yml +++ b/Frends.MongoDB.Insert/Frends.MongoDB.Insert.Tests/Files/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: mongo: - image: mongo:3 + image: mongo:7 environment: - AUTH=yes - MONGO_INITDB_ROOT_USERNAME=admin diff --git a/Frends.MongoDB.Insert/Frends.MongoDB.Insert.Tests/Files/testdata.json b/Frends.MongoDB.Insert/Frends.MongoDB.Insert.Tests/Files/testdata.json index aa8b20f..bcf5f58 100644 --- a/Frends.MongoDB.Insert/Frends.MongoDB.Insert.Tests/Files/testdata.json +++ b/Frends.MongoDB.Insert/Frends.MongoDB.Insert.Tests/Files/testdata.json @@ -1,4 +1,4 @@ { - "foo": "bar", + "foo": "bar100", "bar": "foo" } \ No newline at end of file diff --git a/Frends.MongoDB.Insert/Frends.MongoDB.Insert.Tests/Frends.MongoDB.Insert.Tests.csproj b/Frends.MongoDB.Insert/Frends.MongoDB.Insert.Tests/Frends.MongoDB.Insert.Tests.csproj index 661d5d4..d9a458b 100644 --- a/Frends.MongoDB.Insert/Frends.MongoDB.Insert.Tests/Frends.MongoDB.Insert.Tests.csproj +++ b/Frends.MongoDB.Insert/Frends.MongoDB.Insert.Tests/Frends.MongoDB.Insert.Tests.csproj @@ -9,10 +9,13 @@ - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Frends.MongoDB.Insert/Frends.MongoDB.Insert.Tests/UnitTests.cs b/Frends.MongoDB.Insert/Frends.MongoDB.Insert.Tests/UnitTests.cs index 24da58e..c6e4979 100644 --- a/Frends.MongoDB.Insert/Frends.MongoDB.Insert.Tests/UnitTests.cs +++ b/Frends.MongoDB.Insert/Frends.MongoDB.Insert.Tests/UnitTests.cs @@ -1,46 +1,52 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Frends.MongoDB.Insert.Definitions; +using MongoDB.Bson; +using MongoDB.Driver; namespace Frends.MongoDB.Insert.Tests; [TestClass] public class UnitTests { - /* + /* Run command 'docker-compose up -d' in \Frends.MongoDB.Insert.Tests\Files\ */ - private static readonly Connection _connection = new() - { - ConnectionString = "mongodb://admin:Salakala@localhost:27017/?authSource=admin", - Database = "testdb", - CollectionName = "testcoll", - }; - - private readonly string _doc1 = "{ \"foo\":\"bar\", \"bar\": \"foo\" }"; - private readonly string _doc2 = "{ \"foo\":\"bar2\", \"bar\": \"foo2\" }"; + private static readonly Connection _connection = new() + { + ConnectionString = "mongodb://admin:Salakala@localhost:27017/?authSource=admin", + Database = "testdb", + CollectionName = "testcoll", + }; + [TestCleanup] + public void CleanUp() + { + DeleteTestData(); + } - [TestMethod] + [TestMethod] public async Task Test_Insert_Document() { var _input = new Input() { InputType = InputType.Document, - Document = _doc1, + Document = "{ 'foo':'bar1', 'bar': 'foo' }", File = null, }; var result = await MongoDB.Insert(_input, _connection, default); - Assert.IsTrue(result.Success.Equals(true) && result.Id != null); - } + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Id); + Assert.IsNotNull(GetSingleDocuments("{'foo':'bar1'}")); + } [TestMethod] public async Task Test_Insert_Documents() { - var doc1 = new DocumentValues() { Value = _doc1 }; - var doc2 = new DocumentValues() { Value = _doc2 }; + var doc1 = new DocumentValues() { Value = "{ 'foo':'bar2', 'bar': 'foo' }" }; + var doc2 = new DocumentValues() { Value = "{ 'foo':'bar2', 'bar': 'foo' }" }; var _input = new Input() { @@ -50,9 +56,11 @@ public async Task Test_Insert_Documents() File = null, }; - var result = await MongoDB.Insert(_input, _connection, default); - Assert.IsTrue(result.Success.Equals(true) && result.Id != null); - } + var result = await MongoDB.Insert(_input, _connection, default); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Id); + Assert.IsTrue(GetDocuments("bar2")); + } [TestMethod] @@ -65,7 +73,56 @@ public async Task Test_Insert_File() File = "..//..//..//Files//testdata.json", }; - var result = await MongoDB.Insert(_input, _connection, default); - Assert.IsTrue(result.Success.Equals(true) && result.Id != null); - } + var result = await MongoDB.Insert(_input, _connection, default); + Assert.IsTrue(result.Success); + Assert.IsNotNull(result.Id); + Assert.IsTrue(GetDocuments("bar100")); + } + + private static void DeleteTestData() + { + var collection = GetMongoCollection(_connection.ConnectionString, _connection.Database, _connection.CollectionName); + + List filters = new() + { + "{'foo':'bar'}", + "{'foo':'bar1'}", + "{'foo':'bar2'}", + "{'foo':'bar3'}", + "{'foo':'bar100'}" + }; + + foreach (var filter in filters) + { + collection.DeleteMany(filter); + } + } + + private static IMongoCollection GetMongoCollection(string connectionString, string database, string collectionName) + { + var dataBase = GetMongoDatabase(connectionString, database); + var collection = dataBase.GetCollection(collectionName); + return collection; + } + + private static IMongoDatabase GetMongoDatabase(string connectionString, string database) + { + var mongoClient = new MongoClient(connectionString); + var dataBase = mongoClient.GetDatabase(database); + return dataBase; + } + + private static bool GetDocuments(string updated) + { + var collection = GetMongoCollection(_connection.ConnectionString, _connection.Database, _connection.CollectionName); + var documents = collection.Find(new BsonDocument()).ToList(); + var i = documents.Any(x => x.Values.Contains(updated)); + return i; + } + + private static BsonDocument? GetSingleDocuments(string filter) + { + var collection = GetMongoCollection(_connection.ConnectionString, _connection.Database, _connection.CollectionName); + return collection.Find(BsonDocument.Parse(filter)).FirstOrDefault(); + } } \ No newline at end of file diff --git a/Frends.MongoDB.Insert/Frends.MongoDB.Insert/Frends.MongoDB.Insert.csproj b/Frends.MongoDB.Insert/Frends.MongoDB.Insert/Frends.MongoDB.Insert.csproj index c76860c..e69a97a 100644 --- a/Frends.MongoDB.Insert/Frends.MongoDB.Insert/Frends.MongoDB.Insert.csproj +++ b/Frends.MongoDB.Insert/Frends.MongoDB.Insert/Frends.MongoDB.Insert.csproj @@ -2,7 +2,7 @@ net6.0 - 1.0.1 + 2.0.0 Frends Frends Frends @@ -19,33 +19,12 @@ PreserveNewest - - true - - - true - - - - - - - - - - - - - + + + + + - - - lib\MongoDB.Driver.dll - - - lib\MongoDB.Driver.Core.dll - - \ No newline at end of file diff --git a/Frends.MongoDB.Insert/Frends.MongoDB.Insert/Insert.cs b/Frends.MongoDB.Insert/Frends.MongoDB.Insert/Insert.cs index efa3740..ed152de 100644 --- a/Frends.MongoDB.Insert/Frends.MongoDB.Insert/Insert.cs +++ b/Frends.MongoDB.Insert/Frends.MongoDB.Insert/Insert.cs @@ -7,6 +7,8 @@ using MongoDB.Bson; using MongoDB.Driver; using System.Collections.Generic; +using System.Runtime.Caching; +using System.Diagnostics.CodeAnalysis; namespace Frends.MongoDB.Insert; @@ -15,15 +17,19 @@ namespace Frends.MongoDB.Insert; /// public class MongoDB { - /// - /// MongoDB insert operation. - /// [Documentation](https://tasks.frends.com/tasks/frends-tasks/Frends.MongoDB.Insert) - /// - /// Input parameters. - /// Connection parameters. - /// Token generated by Frends to stop this task. - /// Object { bool success, List<string> id } - public static async Task Insert([PropertyTab] Input input, [PropertyTab] Connection connection, CancellationToken cancellationToken) + internal static readonly ObjectCache ClientCache = MemoryCache.Default; + private static readonly CacheItemPolicy _cachePolicy = new() { SlidingExpiration = TimeSpan.FromHours(1) }; + + + /// + /// MongoDB insert operation. + /// [Documentation](https://tasks.frends.com/tasks/frends-tasks/Frends.MongoDB.Insert) + /// + /// Input parameters. + /// Connection parameters. + /// Token generated by Frends to stop this task. + /// Object { bool success, List<string> id } + public static async Task Insert([PropertyTab] Input input, [PropertyTab] Connection connection, CancellationToken cancellationToken) { try { @@ -63,7 +69,8 @@ public static async Task Insert([PropertyTab] Input input, [PropertyTab] } return new Result(true, ids); - default: return new Result(false, null); + default: + return new Result(false, null); }; } catch (Exception ex) @@ -72,31 +79,47 @@ public static async Task Insert([PropertyTab] Input input, [PropertyTab] } } - private static IMongoCollection GetMongoCollection(string connectionString, string database, string collectionName) - { - try - { - var dataBase = GetMongoDatabase(connectionString, database); - var collection = dataBase.GetCollection(collectionName); - return collection; - } - catch (Exception ex) - { - throw new Exception($"GetMongoCollection error: {ex}"); - } - } + private static IMongoCollection GetMongoCollection(string connectionString, string database, string collectionName) + { + try + { + var dataBase = GetMongoDatabase(connectionString, database, collectionName); + var collection = dataBase.GetCollection(collectionName); + return collection; + } + catch (Exception ex) + { + throw new Exception($"GetMongoCollection error: {ex}"); + } + } - private static IMongoDatabase GetMongoDatabase(string connectionString, string database) - { - try - { - var mongoClient = new MongoClient(connectionString); - var dataBase = mongoClient.GetDatabase(database); - return dataBase; - } - catch (Exception ex) - { - throw new Exception($"GetMongoDatabase error: {ex}"); - } - } + private static IMongoDatabase GetMongoDatabase(string connectionString, string database, string collectionName) + { + var cacheKey = GetMongoDbCacheKey(connectionString, database, collectionName); + + if (ClientCache.Get(cacheKey) is IMongoDatabase mongoDatabase) + { + return mongoDatabase; + } + + try + { + var mongoClient = new MongoClient(connectionString); + var dataBase = mongoClient.GetDatabase(database); + + ClientCache.Add(cacheKey, dataBase, _cachePolicy); + + return dataBase; + } + catch (Exception ex) + { + throw new Exception($"GetMongoDatabase error: {ex}"); + } + } + + [ExcludeFromCodeCoverage] + private static string GetMongoDbCacheKey(string connectionString, string database, string collectionName) + { + return $"{connectionString.GetHashCode()}:{database}:{collectionName}"; + } } \ No newline at end of file diff --git a/Frends.MongoDB.Insert/Frends.MongoDB.Insert/lib/MongoDB.Driver.Core.dll b/Frends.MongoDB.Insert/Frends.MongoDB.Insert/lib/MongoDB.Driver.Core.dll deleted file mode 100644 index ef4f87f..0000000 Binary files a/Frends.MongoDB.Insert/Frends.MongoDB.Insert/lib/MongoDB.Driver.Core.dll and /dev/null differ diff --git a/Frends.MongoDB.Insert/Frends.MongoDB.Insert/lib/MongoDB.Driver.dll b/Frends.MongoDB.Insert/Frends.MongoDB.Insert/lib/MongoDB.Driver.dll deleted file mode 100644 index a5f429a..0000000 Binary files a/Frends.MongoDB.Insert/Frends.MongoDB.Insert/lib/MongoDB.Driver.dll and /dev/null differ diff --git a/Frends.MongoDB.Insert/README.md b/Frends.MongoDB.Insert/README.md index b497a31..397d3ac 100644 --- a/Frends.MongoDB.Insert/README.md +++ b/Frends.MongoDB.Insert/README.md @@ -8,7 +8,7 @@ Frends Task for MongoDB insert operation. # Installing -You can install the Task via Frends UI Task View or you can find the NuGet package from the following NuGet feed https://www.myget.org/F/frends-tasks/api/v2. +You can install the Task via Frends UI Task View or you can find the NuGet package from the following NuGet feed [NuGet Package](https://www.myget.org/F/frends-tasks/api/v2) ## Building diff --git a/Frends.MongoDB.Query/CHANGELOG.md b/Frends.MongoDB.Query/CHANGELOG.md index 7905125..372ec7c 100644 --- a/Frends.MongoDB.Query/CHANGELOG.md +++ b/Frends.MongoDB.Query/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [2.0.0] - 2025-03-12 +### Added +- Adds caching for the MongoClient connection to improve performance. +- Option to query single or multiple documents + +### Updated +- Updated MongoDB.Driver to version 3.2.1 + +### Breaking changes +- The MongoDB driver drops support for MongoDB Server v3.6 and earlier. +- The MongoDB driver drops support for .NET Core 2.x and .NET Framework 4.6. +- Read more about MongoDB driver 3.0 breaking changes [here](https://mongodb.github.io/mongo-csharp-driver/3.0/reference/breaking_changes/) + ## [1.0.1] - 2023-11-23 ### Fixed - Fixed dll error when importing the Task to Frends by adding local dll reference to the project file. diff --git a/Frends.MongoDB.Query/Frends.MongoDB.Query.Tests/Files/docker-compose.yml b/Frends.MongoDB.Query/Frends.MongoDB.Query.Tests/Files/docker-compose.yml index cd14b47..56b9789 100644 --- a/Frends.MongoDB.Query/Frends.MongoDB.Query.Tests/Files/docker-compose.yml +++ b/Frends.MongoDB.Query/Frends.MongoDB.Query.Tests/Files/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: mongo: - image: mongo:3 + image: mongo:7 environment: - AUTH=yes - MONGO_INITDB_ROOT_USERNAME=admin diff --git a/Frends.MongoDB.Query/Frends.MongoDB.Query.Tests/Frends.MongoDB.Query.Tests.csproj b/Frends.MongoDB.Query/Frends.MongoDB.Query.Tests/Frends.MongoDB.Query.Tests.csproj index 9de03de..065aa73 100644 --- a/Frends.MongoDB.Query/Frends.MongoDB.Query.Tests/Frends.MongoDB.Query.Tests.csproj +++ b/Frends.MongoDB.Query/Frends.MongoDB.Query.Tests/Frends.MongoDB.Query.Tests.csproj @@ -9,16 +9,15 @@ - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - - - diff --git a/Frends.MongoDB.Query/Frends.MongoDB.Query.Tests/UnitTests.cs b/Frends.MongoDB.Query/Frends.MongoDB.Query.Tests/UnitTests.cs index a67931f..c18891a 100644 --- a/Frends.MongoDB.Query/Frends.MongoDB.Query.Tests/UnitTests.cs +++ b/Frends.MongoDB.Query/Frends.MongoDB.Query.Tests/UnitTests.cs @@ -1,30 +1,34 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Frends.MongoDB.Query.Definitions; -using MongoDB.Bson; using MongoDB.Driver; +using MongoDB.Bson.IO; +using MongoDB.Bson; namespace Frends.MongoDB.Query.Tests; [TestClass] public class UnitTests { - /* + /* Run command 'docker-compose up -d' in \Frends.MongoDB.Query.Tests\Files\ */ - private static readonly Connection _connection = new() - { - ConnectionString = "mongodb://admin:Salakala@localhost:27017/?authSource=admin", - Database = "testdb", - CollectionName = "testcoll", - }; - - private readonly string _doc1 = "{ \"foo\":\"bar\", \"bar\": \"foo\" }"; - private readonly string _doc2 = "{ \"foo\":\"bar\", \"bar\": \"foo\" }"; - private readonly string _doc3 = "{ \"qwe\":\"rty\", \"asd\": \"fgh\" }"; - - - [TestInitialize] + private static readonly Connection _connection = new() + { + ConnectionString = "mongodb://admin:Salakala@localhost:27017/?authSource=admin", + Database = "testdb", + CollectionName = "testcoll", + }; + + private readonly List _documents = new() + { + "{ 'foo':'bar', 'bar': 'foo' }", + "{ 'foo':'bar', 'bar': 'foo' }", + "{ 'foo':99, 'bar': 'foo' }", + "{ 'qwe':'rty', 'asd': 'fgh' }" + }; + + [TestInitialize] public void StartUp() { InsertTestData(); @@ -33,16 +37,17 @@ public void StartUp() [TestCleanup] public void CleanUp() { - DeleteTestData(); - } + DeleteTestData(); + } [TestMethod] public void Test_Query_TwoResults() { var _input = new Input() { - Filter = "{'foo':'bar'}" - }; + Filter = "{'foo':'bar'}", + QueryOptions = QueryOptions.QueryMany + }; var result = MongoDB.Query(_input, _connection, default); Assert.IsTrue(result.Success); @@ -55,14 +60,44 @@ public void Test_Query_OneResults() var _input = new Input() { Filter = "{'qwe':'rty'}" - }; + }; var result = MongoDB.Query(_input, _connection, default); Assert.IsTrue(result.Success); Assert.AreEqual(1, result.Data.Count); } - [TestMethod] + [TestMethod] + public void Test_Query_RelaxedExtendedJson() + { + var _input = new Input() + { + Filter = "{'foo':99}", + JsonOutputMode = Definitions.JsonOutputMode.RelaxedExtendedJson, + }; + + var result = MongoDB.Query(_input, _connection, default); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.Data.Count); + Assert.IsFalse(result.Data[0].Contains("$numberInt")); + } + + [TestMethod] + public void Test_Query_CanonicalExtendedJson() + { + var _input = new Input() + { + Filter = "{'foo':99}", + JsonOutputMode = Definitions.JsonOutputMode.CanonicalExtendedJson, + }; + + var result = MongoDB.Query(_input, _connection, default); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.Data.Count); + Assert.IsTrue(result.Data[0].Contains("$numberInt")); + } + + [TestMethod] public void Test_Query_NotFoundFilter() { var _input = new Input() @@ -78,7 +113,7 @@ public void Test_Query_NotFoundFilter() [TestMethod] public void Test_EmptyQuery() { - var ex = Assert.ThrowsException(() => MongoDB.Query(new Input { Filter = "" }, _connection, default)); + var ex = Assert.ThrowsException(() => MongoDB.Query(new Input { Filter = "" }, _connection, default)); Assert.AreEqual("Query error: Filter can't be null.", ex.Message); } @@ -101,28 +136,44 @@ public void Test_InvalidConnectionString() Assert.IsTrue(ex.Message.StartsWith("Query error: MongoDB.Driver.MongoAuthenticationException: Unable to authenticate using sasl protocol mechanism SCRAM-SHA-1.")); } - private void InsertTestData() - { - try - { - var collection = GetMongoCollection(_connection.ConnectionString, _connection.Database, _connection.CollectionName); - - var doc1 = BsonDocument.Parse(_doc1); - var doc2 = BsonDocument.Parse(_doc2); - var doc3 = BsonDocument.Parse(_doc3); - - collection.InsertOne(doc1); - collection.InsertOne(doc2); - collection.InsertOne(doc3); - } - catch (Exception ex) - { - - throw new Exception(ex.Message); - } - } - - private static IMongoCollection GetMongoCollection(string connectionString, string database, string collectionName) + private void InsertTestData() + { + try + { + var collection = GetMongoCollection(_connection.ConnectionString, _connection.Database, _connection.CollectionName); + + foreach (var doc in _documents) + { + collection.InsertOne(BsonDocument.Parse(doc)); + } + } + catch (Exception ex) + { + throw new Exception(ex.Message); + } + } + + private static void DeleteTestData() + { + var collection = GetMongoCollection(_connection.ConnectionString, _connection.Database, _connection.CollectionName); + + List filters = new() + { + "{'bar':'foo'}", + "{'qwe':'rty'}", + "{'asd':'fgh'}", + "{foo:'update'}", + "{'foobar':'upsert_create'}", + "{'array':'arr'}" + }; + + foreach (var filter in filters) + { + collection.DeleteMany(filter); + } + } + + private static IMongoCollection GetMongoCollection(string connectionString, string database, string collectionName) { var dataBase = GetMongoDatabase(connectionString, database); var collection = dataBase.GetCollection(collectionName); @@ -135,16 +186,4 @@ private static IMongoDatabase GetMongoDatabase(string connectionString, string d var dataBase = mongoClient.GetDatabase(database); return dataBase; } - - private static void DeleteTestData() - { - var collection = GetMongoCollection(_connection.ConnectionString, _connection.Database, _connection.CollectionName); - - var filter1 = "{'bar':'foo'}"; - var filter2 = "{'qwe':'rty'}"; - var filter3 = "{'asd':'fgh'}"; - collection.DeleteMany(filter1); - collection.DeleteMany(filter2); - collection.DeleteMany(filter3); - } } \ No newline at end of file diff --git a/Frends.MongoDB.Query/Frends.MongoDB.Query/Definitions/Enums.cs b/Frends.MongoDB.Query/Frends.MongoDB.Query/Definitions/Enums.cs new file mode 100644 index 0000000..b69d3e6 --- /dev/null +++ b/Frends.MongoDB.Query/Frends.MongoDB.Query/Definitions/Enums.cs @@ -0,0 +1,37 @@ +namespace Frends.MongoDB.Query.Definitions; +/// +/// Query options. +/// +public enum QueryOptions +{ + /// + /// Returns the first document that match a specified filter even though multiple documents may match the specified filter. + /// + QueryOne, + + /// + /// Returns all documents that match a specified filter. + /// + QueryMany, +} + +/// +/// Represents the output mode of a JsonWriter. +/// +public enum JsonOutputMode +{ + /// + /// Use a format that can be pasted in to the MongoDB shell. + /// + Shell, + + /// + /// Output canonical extended JSON. + /// + CanonicalExtendedJson, + + /// + /// Output relaxed extended JSON. + /// + RelaxedExtendedJson, +} \ No newline at end of file diff --git a/Frends.MongoDB.Query/Frends.MongoDB.Query/Definitions/Input.cs b/Frends.MongoDB.Query/Frends.MongoDB.Query/Definitions/Input.cs index c23a5a4..f0495cd 100644 --- a/Frends.MongoDB.Query/Frends.MongoDB.Query/Definitions/Input.cs +++ b/Frends.MongoDB.Query/Frends.MongoDB.Query/Definitions/Input.cs @@ -1,13 +1,30 @@ -namespace Frends.MongoDB.Query.Definitions; +using System.ComponentModel; + +namespace Frends.MongoDB.Query.Definitions; /// /// Input parameter. /// public class Input { - /// - /// Filter document. - /// - /// {'foo':'bar'} - public string Filter { get; set; } + /// + /// Query single or multiple documents. + /// + /// QueryOptions.QueryMany + [DefaultValue(QueryOptions.QueryMany)] + public QueryOptions QueryOptions { get; set; } + + + /// + /// Filter document. + /// + /// {'foo':'bar'} + public string Filter { get; set; } + + /// + /// Json output mode. + /// + /// JsonOutputMode.RelaxedExtendedJson + [DefaultValue(JsonOutputMode.RelaxedExtendedJson)] + public JsonOutputMode JsonOutputMode { get; set; } } \ No newline at end of file diff --git a/Frends.MongoDB.Query/Frends.MongoDB.Query/Frends.MongoDB.Query.csproj b/Frends.MongoDB.Query/Frends.MongoDB.Query/Frends.MongoDB.Query.csproj index 6edb61a..4e0c768 100644 --- a/Frends.MongoDB.Query/Frends.MongoDB.Query/Frends.MongoDB.Query.csproj +++ b/Frends.MongoDB.Query/Frends.MongoDB.Query/Frends.MongoDB.Query.csproj @@ -2,7 +2,7 @@ net6.0 - 1.0.1 + 1.0.3 Frends Frends Frends @@ -19,33 +19,12 @@ PreserveNewest - - true - - - true - - - - - - - - - - - - - + + + + + - - - lib\MongoDB.Driver.dll - - - lib\MongoDB.Driver.Core.dll - - \ No newline at end of file diff --git a/Frends.MongoDB.Query/Frends.MongoDB.Query/FrendsTaskMetadata.json b/Frends.MongoDB.Query/Frends.MongoDB.Query/FrendsTaskMetadata.json index df26bd0..24c71e1 100644 --- a/Frends.MongoDB.Query/Frends.MongoDB.Query/FrendsTaskMetadata.json +++ b/Frends.MongoDB.Query/Frends.MongoDB.Query/FrendsTaskMetadata.json @@ -1,7 +1,7 @@ { - "Tasks": [ - { - "TaskMethod": "Frends.MongoDB.Query.MongoDB.Query" - } - ] + "Tasks": [ + { + "TaskMethod": "Frends.MongoDB.Query.MongoDB.Query" + } + ] } \ No newline at end of file diff --git a/Frends.MongoDB.Query/Frends.MongoDB.Query/Query.cs b/Frends.MongoDB.Query/Frends.MongoDB.Query/Query.cs index 5846f62..4d3e34c 100644 --- a/Frends.MongoDB.Query/Frends.MongoDB.Query/Query.cs +++ b/Frends.MongoDB.Query/Frends.MongoDB.Query/Query.cs @@ -2,10 +2,13 @@ using System.ComponentModel; using System; using System.Threading; +using Mongo = MongoDB; using MongoDB.Bson; using MongoDB.Driver; using MongoDB.Bson.IO; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.Caching; namespace Frends.MongoDB.Query; @@ -14,31 +17,44 @@ namespace Frends.MongoDB.Query; /// public class MongoDB { - /// - /// MongoDB query operation. - /// [Documentation](https://tasks.frends.com/tasks/frends-tasks/Frends.MongoDB.Query) - /// - /// Input parameters. - /// Connection parameters. - /// Token generated by Frends to stop this task. - /// Object { bool Success, List<string> Data } - public static Result Query([PropertyTab] Input input, [PropertyTab] Connection connection, CancellationToken cancellationToken) + internal static readonly ObjectCache ClientCache = MemoryCache.Default; + private static readonly CacheItemPolicy _cachePolicy = new() { SlidingExpiration = TimeSpan.FromHours(1) }; + + + /// + /// MongoDB query operation. + /// [Documentation](https://tasks.frends.com/tasks/frends-tasks/Frends.MongoDB.Query) + /// + /// Input parameters. + /// Connection parameters. + /// Token generated by Frends to stop this task. + /// Object { bool Success, List<string> Data } + public static Result Query([PropertyTab] Input input, [PropertyTab] Connection connection, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(input.Filter)) - throw new Exception("Query error: Filter can't be null."); + throw new ArgumentException("Query error: Filter can't be null."); try { var li = new List(); - var jsonSettings = new JsonWriterSettings { OutputMode = JsonOutputMode.CanonicalExtendedJson }; + var jsonSettings = new JsonWriterSettings { OutputMode = ( Mongo.Bson.IO.JsonOutputMode)(int)input.JsonOutputMode }; var collection = GetMongoCollection(connection.ConnectionString, connection.Database, connection.CollectionName); - var cursor = collection.Find(input.Filter).ToCursor(cancellationToken); - - foreach (var document in cursor.ToEnumerable(cancellationToken: cancellationToken)) - li.Add(document.ToJson(jsonSettings)); + + if (input.QueryOptions == QueryOptions.QueryOne) + { + var document = collection.Find(input.Filter).FirstOrDefault(cancellationToken); + if (document != null) + li.Add(document.ToJson(jsonSettings)); + } + else + { + var cursor = collection.Find(input.Filter).ToCursor(cancellationToken); + foreach (var document in cursor.ToEnumerable(cancellationToken: cancellationToken)) + li.Add(document.ToJson(jsonSettings)); + } + return new Result(true, li); - } catch (Exception ex) { @@ -46,31 +62,47 @@ public static Result Query([PropertyTab] Input input, [PropertyTab] Connection c } } - private static IMongoCollection GetMongoCollection(string connectionString, string database, string collectionName) - { - try - { - var dataBase = GetMongoDatabase(connectionString, database); - var collection = dataBase.GetCollection(collectionName); - return collection; - } - catch (Exception ex) - { - throw new Exception($"GetMongoCollection error: {ex}"); - } - } + private static IMongoCollection GetMongoCollection(string connectionString, string database, string collectionName) + { + try + { + var dataBase = GetMongoDatabase(connectionString, database, collectionName); + var collection = dataBase.GetCollection(collectionName); + return collection; + } + catch (Exception ex) + { + throw new Exception($"GetMongoCollection error: {ex}"); + } + } - private static IMongoDatabase GetMongoDatabase(string connectionString, string database) - { - try - { - var mongoClient = new MongoClient(connectionString); - var dataBase = mongoClient.GetDatabase(database); - return dataBase; - } - catch (Exception ex) - { - throw new Exception($"GetMongoDatabase error: {ex}"); - } - } + private static IMongoDatabase GetMongoDatabase(string connectionString, string database, string collectionName) + { + var cacheKey = GetMongoDbCacheKey(connectionString, database, collectionName); + + if (ClientCache.Get(cacheKey) is IMongoDatabase mongoDatabase) + { + return mongoDatabase; + } + + try + { + var mongoClient = new MongoClient(connectionString); + var dataBase = mongoClient.GetDatabase(database); + + ClientCache.Add(cacheKey, dataBase, _cachePolicy); + + return dataBase; + } + catch (Exception ex) + { + throw new Exception($"GetMongoDatabase error: {ex}"); + } + } + + [ExcludeFromCodeCoverage] + private static string GetMongoDbCacheKey(string connectionString, string database, string collectionName) + { + return $"{connectionString.GetHashCode()}:{database}:{collectionName}"; + } } \ No newline at end of file diff --git a/Frends.MongoDB.Query/Frends.MongoDB.Query/lib/MongoDB.Driver.Core.dll b/Frends.MongoDB.Query/Frends.MongoDB.Query/lib/MongoDB.Driver.Core.dll deleted file mode 100644 index ef4f87f..0000000 Binary files a/Frends.MongoDB.Query/Frends.MongoDB.Query/lib/MongoDB.Driver.Core.dll and /dev/null differ diff --git a/Frends.MongoDB.Query/Frends.MongoDB.Query/lib/MongoDB.Driver.dll b/Frends.MongoDB.Query/Frends.MongoDB.Query/lib/MongoDB.Driver.dll deleted file mode 100644 index a5f429a..0000000 Binary files a/Frends.MongoDB.Query/Frends.MongoDB.Query/lib/MongoDB.Driver.dll and /dev/null differ diff --git a/Frends.MongoDB.Query/README.md b/Frends.MongoDB.Query/README.md index 5c3a39a..6f73be1 100644 --- a/Frends.MongoDB.Query/README.md +++ b/Frends.MongoDB.Query/README.md @@ -8,7 +8,7 @@ Frends Task for MongoDB query operation. # Installing -You can install the Task via Frends UI Task View or you can find the NuGet package from the following NuGet feed https://www.myget.org/F/frends-tasks/api/v2. +You can install the Task via Frends UI Task View or you can find the NuGet package from the following NuGet feed [NuGet Package](https://www.myget.org/F/frends-tasks/api/v2) ## Building diff --git a/Frends.MongoDB.Update/CHANGELOG.md b/Frends.MongoDB.Update/CHANGELOG.md index dc11050..95f5bd8 100644 --- a/Frends.MongoDB.Update/CHANGELOG.md +++ b/Frends.MongoDB.Update/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## [2.0.0] - 2025-03-12 +### Added +- Adds caching for the MongoClient connection to improve performance. +- Adds support for array filter for an update operation. +- Option to use upsert when updating documents. + +### Updated +- Updated MongoDB.Driver to version 3.2.1 + +### Breaking changes +- The MongoDB driver drops support for MongoDB Server v3.6 and earlier. +- The MongoDB driver drops support for .NET Core 2.x and .NET Framework 4.6. +- Read more about MongoDB driver 3.0 breaking changes [here](https://mongodb.github.io/mongo-csharp-driver/3.0/reference/breaking_changes/) + ## [1.0.1] - 2023-11-23 ### Fixed - Fixed dll error when importing the Task to Frends by adding local dll reference to the project file. diff --git a/Frends.MongoDB.Update/Frends.MongoDB.Update.Tests/Files/docker-compose.yml b/Frends.MongoDB.Update/Frends.MongoDB.Update.Tests/Files/docker-compose.yml index cd14b47..56b9789 100644 --- a/Frends.MongoDB.Update/Frends.MongoDB.Update.Tests/Files/docker-compose.yml +++ b/Frends.MongoDB.Update/Frends.MongoDB.Update.Tests/Files/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: mongo: - image: mongo:3 + image: mongo:7 environment: - AUTH=yes - MONGO_INITDB_ROOT_USERNAME=admin diff --git a/Frends.MongoDB.Update/Frends.MongoDB.Update.Tests/Frends.MongoDB.Update.Tests.csproj b/Frends.MongoDB.Update/Frends.MongoDB.Update.Tests/Frends.MongoDB.Update.Tests.csproj index 9e4fec7..d57bb41 100644 --- a/Frends.MongoDB.Update/Frends.MongoDB.Update.Tests/Frends.MongoDB.Update.Tests.csproj +++ b/Frends.MongoDB.Update/Frends.MongoDB.Update.Tests/Frends.MongoDB.Update.Tests.csproj @@ -4,23 +4,19 @@ net6.0 enable enable - false - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - - - - - diff --git a/Frends.MongoDB.Update/Frends.MongoDB.Update.Tests/Properties/launchSettings.json b/Frends.MongoDB.Update/Frends.MongoDB.Update.Tests/Properties/launchSettings.json new file mode 100644 index 0000000..d5af7ad --- /dev/null +++ b/Frends.MongoDB.Update/Frends.MongoDB.Update.Tests/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Frends.MongoDB.Update.Tests": { + "commandName": "Project", + "environmentVariables": { + "MONGODB_TEST_CONNECTION_STRING": "mongodb://admin:Salakala@192.168.10.113:27017/?authSource=admin" + } + } + } +} \ No newline at end of file diff --git a/Frends.MongoDB.Update/Frends.MongoDB.Update.Tests/UnitTests.cs b/Frends.MongoDB.Update/Frends.MongoDB.Update.Tests/UnitTests.cs index b672545..cb5a62a 100644 --- a/Frends.MongoDB.Update/Frends.MongoDB.Update.Tests/UnitTests.cs +++ b/Frends.MongoDB.Update/Frends.MongoDB.Update.Tests/UnitTests.cs @@ -8,23 +8,26 @@ namespace Frends.MongoDB.Update.Tests; [TestClass] public class UnitTests { - /// - /// Run command 'docker-compose up -d' in \Frends.MongoDB.Update.Tests\Files\ - /// - - private static readonly Connection _connection = new() - { - ConnectionString = "mongodb://admin:Salakala@localhost:27017/?authSource=admin", - Database = "testdb", - CollectionName = "testcoll", - }; - - private readonly string _doc1 = "{ 'foo':'bar', 'bar': 'foo' }"; - private readonly string _doc2 = "{ 'foo':'bar', 'bar': 'foo' }"; - private readonly string _doc3 = "{ 'qwe':'rty', 'asd': 'fgh' }"; - - - [TestInitialize] + /// + /// Run command 'docker-compose up -d' in \Frends.MongoDB.Update.Tests\Files\ + /// + + private static readonly Connection _connection = new() + { + ConnectionString = "mongodb://admin:Salakala@localhost:27017/?authSource=admin", + Database = "testdb", + CollectionName = "testcoll", + }; + + private readonly List _documents = new() + { + "{ 'foo':'bar', 'bar': 'foo' }", + "{ 'foo':'bar', 'bar': 'foo' }", + "{ 'qwe':'rty', 'asd': 'fgh' }", + "{ 'array':'arr', 'children': [{'child':1, 'value': 'val1' }, {'child':2, 'value': 'val2' }] }" + }; + + [TestInitialize] public void StartUp() { InsertTestData(); @@ -33,10 +36,10 @@ public void StartUp() [TestCleanup] public void CleanUp() { - UpdateTestData(); - } + DeleteTestData(); + } - [TestMethod] + [TestMethod] public async Task Test_Update_NotFound() { var _input = new Input() @@ -62,10 +65,11 @@ public async Task Test_Update_Single_UpdateOne() { InputType = InputType.Filter, UpdateOptions = Definitions.UpdateOptions.UpdateOne, - Filter = "{'foo':'bar'}", - Filters = null, + Filter = "{'foo':'bar'}", + Filters = null, File = null, - UpdateString = "{$set: {foo:'update'}}" + UpdateString = "{$set: {foo:'update'}}", + Upsert = true }; var result = await MongoDB.Update(_input, _connection, default); @@ -84,8 +88,9 @@ public async Task Test_Update_Single_UpdateMany() Filter = "{'foo':'bar'}", Filters = null, File = null, - UpdateString = "{$set: {foo:'update'}}" - }; + UpdateString = "{$set: {foo:'update'}}", + Upsert = true + }; var result = await MongoDB.Update(_input, _connection, default); Assert.IsTrue(result.Success); @@ -190,8 +195,8 @@ public void Test_InvalidConnectionString() var connection = new Connection { - ConnectionString = "mongodb://admin:Incorrect@localhost:27017/?authSource=invalid", - CollectionName = _connection.CollectionName, + ConnectionString = "mongodb://admin:Incorrect@localhost:27017/?authSource=invalid", + CollectionName = _connection.CollectionName, Database = _connection.Database, }; @@ -199,36 +204,144 @@ public void Test_InvalidConnectionString() Assert.IsTrue(ex.Result.Message.StartsWith("Update error: System.Exception: UpdateOperation error: MongoDB.Driver.MongoAuthenticationException: Unable to authenticate using sasl protocol mechanism SCRAM-SHA-1.")); } - private void InsertTestData() + [TestMethod] + public async Task Test_Upsert_Create_Document() + { + var _input = new Input() + { + InputType = InputType.Filter, + UpdateOptions = Definitions.UpdateOptions.UpdateOne, + Filter = "{'foobar':'upsert_create'}", + Filters = null, + File = null, + UpdateString = "{$set: {foobar:'upsert_create'}}", + Upsert = true + }; + + var result = await MongoDB.Update(_input, _connection, default); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.Count); + Assert.IsTrue(GetDocuments("upsert_create")); + } + + [TestMethod] + public async Task Test_Upsert_Dont_Create_Document() + { + var _input = new Input() + { + InputType = InputType.Filter, + UpdateOptions = Definitions.UpdateOptions.UpdateOne, + Filter = "{'foobar':'upsert_none'}", + Filters = null, + File = null, + UpdateString = "{$set: {foo:'upsert_none'}}", + Upsert = false + }; + + var result = await MongoDB.Update(_input, _connection, default); + Assert.IsTrue(result.Success); + Assert.AreEqual(0, result.Count); + Assert.IsFalse(GetDocuments("upsert_none")); + } + + [TestMethod] + public void Test_Failing_When_No_Filter() + { + var _input = new Input() + { + InputType = InputType.Filter, + UpdateOptions = Definitions.UpdateOptions.UpdateMany, + Filter = "", + Filters = null, + File = null, + UpdateString = "{$set: {foo:'update'}}", + Upsert = true + }; + + var ex = Assert.ThrowsExceptionAsync(async () => await MongoDB.Update(_input, _connection, default)); + Assert.IsTrue(ex.Result.Message.StartsWith("Update error: System.ArgumentException: Filter string missing.")); + } + + [TestMethod] + public async Task Test_Update_Item_In_Child_Array() + { + var _input = new Input() + { + InputType = InputType.Filter, + UpdateOptions = Definitions.UpdateOptions.UpdateOne, + Filter = "{'array':'arr'}", + Filters = null, + File = null, + UpdateString = "{$set: {'children.$[i].value': 'new_value'}}", + ArrayFilter = "{'i.child': 1, 'i.value': 'val1'}", + Upsert = false + }; + + var result = await MongoDB.Update(_input, _connection, default); + Assert.IsTrue(result.Success); + Assert.AreEqual(1, result.Count); + + var document = GetSingleDocuments(_input.Filter); + Assert.IsNotNull(document); + Assert.AreEqual("new_value", document["children"].AsBsonArray[0]["value"].AsString); + } + + [TestMethod] + public async Task Test_Dont_Update_Item_In_Child_Array() + { + var _input = new Input() + { + InputType = InputType.Filter, + UpdateOptions = Definitions.UpdateOptions.UpdateOne, + Filter = "{'array':'arr'}", + Filters = null, + File = null, + UpdateString = "{$set: {'children.$[i].value': 'new_value'}}", + ArrayFilter = "{'i.child': 1, 'i.value': 'val2'}", + Upsert = false + }; + + var result = await MongoDB.Update(_input, _connection, default); + Assert.IsTrue(result.Success); + Assert.AreEqual(0, result.Count); + } + + private void InsertTestData() { try { var collection = GetMongoCollection(_connection.ConnectionString, _connection.Database, _connection.CollectionName); - var doc1 = BsonDocument.Parse(_doc1); - var doc2 = BsonDocument.Parse(_doc2); - var doc3 = BsonDocument.Parse(_doc3); - - collection.InsertOne(doc1); - collection.InsertOne(doc2); - collection.InsertOne(doc3); - } + foreach (var doc in _documents) + { + collection.InsertOne(BsonDocument.Parse(doc)); + } + } catch (Exception ex) { throw new Exception(ex.Message); } } - private static void UpdateTestData() + + private static void DeleteTestData() { var collection = GetMongoCollection(_connection.ConnectionString, _connection.Database, _connection.CollectionName); - var filter1 = "{'bar':'foo'}"; - var filter2 = "{'qwe':'rty'}"; - var filter3 = "{'asd':'fgh'}"; - collection.DeleteMany(filter1); - collection.DeleteMany(filter2); - collection.DeleteMany(filter3); - } + List filters = new() + { + "{'bar':'foo'}", + "{'qwe':'rty'}", + "{'asd':'fgh'}", + "{foo:'update'}", + "{'foobar':'upsert_create'}", + "{'array':'arr'}" + }; + + foreach (var filter in filters) + { + collection.DeleteMany(filter); + } + } private static IMongoCollection GetMongoCollection(string connectionString, string database, string collectionName) { @@ -251,4 +364,10 @@ private static bool GetDocuments(string updated) var i = documents.Any(x => x.Values.Contains(updated)); return i; } + + private static BsonDocument? GetSingleDocuments(string filter) + { + var collection = GetMongoCollection(_connection.ConnectionString, _connection.Database, _connection.CollectionName); + return collection.Find(BsonDocument.Parse(filter)).FirstOrDefault(); + } } \ No newline at end of file diff --git a/Frends.MongoDB.Update/Frends.MongoDB.Update/Definitions/Input.cs b/Frends.MongoDB.Update/Frends.MongoDB.Update/Definitions/Input.cs index 0a0a617..d73ab40 100644 --- a/Frends.MongoDB.Update/Frends.MongoDB.Update/Definitions/Input.cs +++ b/Frends.MongoDB.Update/Frends.MongoDB.Update/Definitions/Input.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; +using System.Text.RegularExpressions; namespace Frends.MongoDB.Update.Definitions; @@ -35,11 +36,11 @@ public class Input [UIHint(nameof(InputType), "", InputType.Filter)] public string Filter { get; set; } - /// - /// An array of filter(s) to be processed one by one. - /// - /// {{'foo':'bar'}, {'bar':'foo'}} - [UIHint(nameof(InputType), "", InputType.Filters)] + /// + /// An array of filter(s) to be processed one by one. + /// + /// {{'foo':'bar'}, {'bar':'foo'}} + [UIHint(nameof(InputType), "", InputType.Filters)] public DocumentValues[] Filters { get; set; } /// @@ -48,6 +49,19 @@ public class Input /// c:\temp\file.json [UIHint(nameof(InputType), "", InputType.File)] public string File { get; set; } + + /// + /// Specifies the array filter for an update operation. + /// In the example below this UpdateString is used: {$set: {'children.$[i].value': 'new_value'}} + /// + /// {'i.child': 1, 'i.value': 'val1'} + public string ArrayFilter { get; set; } + + /// + /// Specifies whether the update operation performs an + /// upsert operation if no documents match the query filter. + /// + public bool Upsert { get; set; } = false; } /// diff --git a/Frends.MongoDB.Update/Frends.MongoDB.Update/Frends.MongoDB.Update.csproj b/Frends.MongoDB.Update/Frends.MongoDB.Update/Frends.MongoDB.Update.csproj index 1d16c54..ba1874a 100644 --- a/Frends.MongoDB.Update/Frends.MongoDB.Update/Frends.MongoDB.Update.csproj +++ b/Frends.MongoDB.Update/Frends.MongoDB.Update/Frends.MongoDB.Update.csproj @@ -2,7 +2,7 @@ net6.0 - 1.0.1 + 2.0.0 Frends Frends Frends @@ -19,33 +19,11 @@ PreserveNewest - - true - - - true - - - - - - - - - - - - - - - - lib\MongoDB.Driver.dll - - - lib\MongoDB.Driver.Core.dll - + + + \ No newline at end of file diff --git a/Frends.MongoDB.Update/Frends.MongoDB.Update/Update.cs b/Frends.MongoDB.Update/Frends.MongoDB.Update/Update.cs index fe2617f..041bb05 100644 --- a/Frends.MongoDB.Update/Frends.MongoDB.Update/Update.cs +++ b/Frends.MongoDB.Update/Frends.MongoDB.Update/Update.cs @@ -6,6 +6,13 @@ using MongoDB.Driver; using System.IO; using System.Threading.Tasks; +using UpdateOptions = MongoDB.Driver.UpdateOptions; +using System.Runtime.Caching; +using System.Linq; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Collections; +using System.Collections.Generic; namespace Frends.MongoDB.Update; @@ -14,18 +21,21 @@ namespace Frends.MongoDB.Update; /// public class MongoDB { - /// - /// MongoDB update operation. - /// [Documentation](https://tasks.frends.com/tasks/frends-tasks/Frends.MongoDB.Update) - /// - /// Input parameters. - /// Connection parameters. - /// Token generated by Frends to stop this task. - /// Object { bool Success, long Count } - public static async Task Update([PropertyTab] Input input, [PropertyTab] Connection connection, CancellationToken cancellationToken) + internal static readonly ObjectCache ClientCache = MemoryCache.Default; + private static readonly CacheItemPolicy _cachePolicy = new() { SlidingExpiration = TimeSpan.FromHours(1) }; + + /// + /// MongoDB update operation. + /// [Documentation](https://tasks.frends.com/tasks/frends-tasks/Frends.MongoDB.Update) + /// + /// Input parameters. + /// Connection parameters. + /// Token generated by Frends to stop this task. + /// Object { bool Success, long Count } + public static async Task Update([PropertyTab] Input input, [PropertyTab] Connection connection, CancellationToken cancellationToken) { - if (string.IsNullOrWhiteSpace(input.UpdateString)) - throw new Exception("Update string missing."); + if (string.IsNullOrWhiteSpace(input.UpdateString)) + throw new ArgumentException("Update string missing."); var collection = GetMongoCollection(connection.ConnectionString, connection.Database, connection.CollectionName); @@ -44,7 +54,10 @@ public static async Task Update([PropertyTab] Input input, [PropertyTab] } case InputType.Filter: - return new Result(true, await UpdateOperation(input, BsonDocument.Parse(input.Filter), collection, cancellationToken)); + if (string.IsNullOrWhiteSpace(input.Filter)) + throw new ArgumentException("Filter string missing."); + + return new Result(true, await UpdateOperation(input, BsonDocument.Parse(input.Filter), collection, cancellationToken)); case InputType.Filters: long count = 0; @@ -64,22 +77,34 @@ public static async Task Update([PropertyTab] Input input, [PropertyTab] private static async Task UpdateOperation(Input input, BsonDocument filter, IMongoCollection collection, CancellationToken cancellationToken) { - try + try { - UpdateDefinition update = input.UpdateString; + UpdateDefinition update = input.UpdateString; + var options = new UpdateOptions + { + IsUpsert = input.Upsert + }; - switch (input.UpdateOptions) - { - case Definitions.UpdateOptions.UpdateOne: - var updateOne = await collection.UpdateOneAsync(filter, update, cancellationToken: cancellationToken); - return updateOne.ModifiedCount; - case Definitions.UpdateOptions.UpdateMany: - var updateMany = await collection.UpdateManyAsync(filter, update, cancellationToken: cancellationToken); - return updateMany.ModifiedCount; - default: - return 0; - } - } + if (!string.IsNullOrWhiteSpace(input.ArrayFilter)) + { +; options.ArrayFilters = new List + { + new BsonDocumentArrayFilterDefinition(BsonDocument.Parse(input.ArrayFilter)) + }; + } + + switch (input.UpdateOptions) + { + case Definitions.UpdateOptions.UpdateOne: + var updateOne = await collection.UpdateOneAsync(filter, update, options, cancellationToken: cancellationToken); + return updateOne.UpsertedId != null ? 1 : updateOne.ModifiedCount; + case Definitions.UpdateOptions.UpdateMany: + var updateMany = await collection.UpdateManyAsync(filter, update, options, cancellationToken: cancellationToken); + return updateMany.UpsertedId != null ? 1 : updateMany.ModifiedCount; + default: + return 0; + } + } catch (Exception ex) { throw new Exception($"UpdateOperation error: {ex}"); @@ -90,7 +115,7 @@ private static IMongoCollection GetMongoCollection(string connecti { try { - var dataBase = GetMongoDatabase(connectionString, database); + var dataBase = GetMongoDatabase(connectionString, database, collectionName); var collection = dataBase.GetCollection(collectionName); return collection; } @@ -100,17 +125,33 @@ private static IMongoCollection GetMongoCollection(string connecti } } - private static IMongoDatabase GetMongoDatabase(string connectionString, string database) + private static IMongoDatabase GetMongoDatabase(string connectionString, string database, string collectionName) { - try + var cacheKey = GetMongoDbCacheKey(connectionString, database, collectionName); + + if (ClientCache.Get(cacheKey) is IMongoDatabase mongoDatabase) + { + return mongoDatabase; + } + + try { var mongoClient = new MongoClient(connectionString); var dataBase = mongoClient.GetDatabase(database); - return dataBase; + + ClientCache.Add(cacheKey, dataBase, _cachePolicy); + + return dataBase; } catch (Exception ex) { throw new Exception($"GetMongoDatabase error: {ex}"); } } + + [ExcludeFromCodeCoverage] + private static string GetMongoDbCacheKey(string connectionString, string database, string collectionName) + { + return $"{connectionString.GetHashCode()}:{database}:{collectionName}"; + } } \ No newline at end of file diff --git a/Frends.MongoDB.Update/Frends.MongoDB.Update/lib/MongoDB.Driver.Core.dll b/Frends.MongoDB.Update/Frends.MongoDB.Update/lib/MongoDB.Driver.Core.dll deleted file mode 100644 index ef4f87f..0000000 Binary files a/Frends.MongoDB.Update/Frends.MongoDB.Update/lib/MongoDB.Driver.Core.dll and /dev/null differ diff --git a/Frends.MongoDB.Update/Frends.MongoDB.Update/lib/MongoDB.Driver.dll b/Frends.MongoDB.Update/Frends.MongoDB.Update/lib/MongoDB.Driver.dll deleted file mode 100644 index a5f429a..0000000 Binary files a/Frends.MongoDB.Update/Frends.MongoDB.Update/lib/MongoDB.Driver.dll and /dev/null differ diff --git a/Frends.MongoDB.Update/README.md b/Frends.MongoDB.Update/README.md index ff42118..be87785 100644 --- a/Frends.MongoDB.Update/README.md +++ b/Frends.MongoDB.Update/README.md @@ -8,11 +8,10 @@ Frends Task for MongoDB update operation. # Installing -You can install the Task via Frends UI Task View or you can find the NuGet package from the following NuGet feed https://www.myget.org/F/frends-tasks/api/v2. +You can install the Task via Frends UI Task View or you can find the NuGet package from the following NuGet feed [NuGet Package](https://www.myget.org/F/frends-tasks/api/v2) ## Building - Rebuild the project `dotnet build` diff --git a/README.md b/README.md index 3752696..aef31ec 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Frends Task for MongoDB related operations. - [Frends.MongoDB.Query](Frends.MongoDB.Query/README.md) - [Frends.MongoDB.Update](Frends.MongoDB.Update/README.md) - [Frends.MongoDB.Delete](Frends.MongoDB.Delete/README.md) +- [Frends.MongoDB.Index](Frends.MongoDB.Index/README.md) # Contributing When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change.