diff --git a/.gitignore b/.gitignore index 086c1e4..d597805 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,12 @@ node_modules # Mongosh temporary files temp + +# C# Debug files +Debug/ + +# NuGet generated artifact files +obj/ + +# User-specific settings files from JetBrains Rider and ReSharper +*.DotSettings.user diff --git a/usage-examples/csharp/driver/Examples/Aggregation/GroupTotal.cs b/usage-examples/csharp/driver/Examples/Aggregation/GroupTotal.cs new file mode 100644 index 0000000..ee906c1 --- /dev/null +++ b/usage-examples/csharp/driver/Examples/Aggregation/GroupTotal.cs @@ -0,0 +1,132 @@ +// :replace-start: { +// "terms": { +// "_order": "order", +// "_aggDb": "aggDb" +// } +// } + +namespace Examples.Aggregation; + +using MongoDB.Driver; +using MongoDB.Bson; + +public class GroupTotal +{ + private IMongoDatabase? _aggDB; + private IMongoCollection? _orders; + + public void LoadSampleData() + { + var uri = DotNetEnv.Env.GetString("CONNECTION_STRING", "Env variable not found. Verify you have a .env file with a valid connection string."); + // :snippet-start: connection-string + // :uncomment-start: + //var uri = "mongodb+srv://mongodb-example:27017"; + // :uncomment-end: + // :snippet-end: + var client = new MongoClient(uri); + _aggDB = client.GetDatabase("agg_tutorials_db"); + // :snippet-start: add-sample-data + _orders = _aggDB.GetCollection("orders"); + _orders.DeleteMany(Builders.Filter.Empty); + + _orders.InsertMany(new List + { + new Order + { + CustomerId = "elise_smith@myemail.com", + OrderDate = DateTime.Parse("2020-05-30T08:35:52Z"), + Value = 231 + }, + new Order + { + CustomerId = "elise_smith@myemail.com", + OrderDate = DateTime.Parse("2020-01-13T09:32:07Z"), + Value = 99 + }, + new Order + { + CustomerId = "oranieri@warmmail.com", + OrderDate = DateTime.Parse("2020-01-01T08:25:37Z"), + Value = 63 + }, + new Order + { + CustomerId = "tj@wheresmyemail.com", + OrderDate = DateTime.Parse("2019-05-28T19:13:32Z"), + Value = 2 + }, + new Order + { + CustomerId = "tj@wheresmyemail.com", + OrderDate = DateTime.Parse("2020-11-23T22:56:53Z"), + Value = 187 + }, + new Order + { + CustomerId = "tj@wheresmyemail.com", + OrderDate = DateTime.Parse("2020-08-18T23:04:48Z"), + Value = 4 + }, + new Order + { + CustomerId = "elise_smith@myemail.com", + OrderDate = DateTime.Parse("2020-12-26T08:55:46Z"), + Value = 4 + }, + new Order + { + CustomerId = "tj@wheresmyemail.com", + OrderDate = DateTime.Parse("2021-02-28T07:49:32Z"), + Value = 1024 + }, + new Order + { + CustomerId = "elise_smith@myemail.com", + OrderDate = DateTime.Parse("2020-10-03T13:49:44Z"), + Value = 102 + } + }); + // :snippet-end: + } + + public List PerformAggregation() + { + if (_aggDB == null || _orders == null) + { + throw new InvalidOperationException("You must call LoadSampleData before performing aggregation."); + } + + // :snippet-start: match + var results = _orders.Aggregate() + .Match(o => o.OrderDate >= DateTime.Parse("2020-01-01T00:00:00Z") && o.OrderDate < DateTime.Parse("2021-01-01T00:00:00Z")) + // :snippet-end: + // :snippet-start: sort-order-date + .SortBy(o => o.OrderDate) + // :snippet-end: + // :snippet-start: group + .Group( + o => o.CustomerId, + g => new + { + CustomerId = g.Key, + FirstPurchaseDate = g.First().OrderDate, + TotalValue = g.Sum(i => i.Value), + TotalOrders = g.Count(), + Orders = g.Select(i => new { i.OrderDate, i.Value }).ToList() + } + ) + // :snippet-end: + // :snippet-start: sort-first-order + .SortBy(c => c.FirstPurchaseDate) + .As(); + // :snippet-end: + + foreach (var result in results.ToList()) + { + Console.WriteLine(result); + } + + return results.ToList(); + } +} +// :replace-end: \ No newline at end of file diff --git a/usage-examples/csharp/driver/Examples/Aggregation/GroupTotalOutput.txt b/usage-examples/csharp/driver/Examples/Aggregation/GroupTotalOutput.txt new file mode 100644 index 0000000..6670eb1 --- /dev/null +++ b/usage-examples/csharp/driver/Examples/Aggregation/GroupTotalOutput.txt @@ -0,0 +1,3 @@ +{ "CustomerId" : "oranieri@warmmail.com", "FirstPurchaseDate" : { "$date" : "2020-01-01T08:25:37Z" }, "TotalValue" : 63, "TotalOrders" : 1, "Orders" : [{ "OrderDate" : { "$date" : "2020-01-01T08:25:37Z" }, "Value" : 63 }] } +{ "CustomerId" : "elise_smith@myemail.com", "FirstPurchaseDate" : { "$date" : "2020-01-13T09:32:07Z" }, "TotalValue" : 436, "TotalOrders" : 4, "Orders" : [{ "OrderDate" : { "$date" : "2020-01-13T09:32:07Z" }, "Value" : 99 }, { "OrderDate" : { "$date" : "2020-05-30T08:35:52Z" }, "Value" : 231 }, { "OrderDate" : { "$date" : "2020-10-03T13:49:44Z" }, "Value" : 102 }, { "OrderDate" : { "$date" : "2020-12-26T08:55:46Z" }, "Value" : 4 }] } +{ "CustomerId" : "tj@wheresmyemail.com", "FirstPurchaseDate" : { "$date" : "2020-08-18T23:04:48Z" }, "TotalValue" : 191, "TotalOrders" : 2, "Orders" : [{ "OrderDate" : { "$date" : "2020-08-18T23:04:48Z" }, "Value" : 4 }, { "OrderDate" : { "$date" : "2020-11-23T22:56:53Z" }, "Value" : 187 }] } \ No newline at end of file diff --git a/usage-examples/csharp/driver/Examples/Aggregation/Order.cs b/usage-examples/csharp/driver/Examples/Aggregation/Order.cs new file mode 100644 index 0000000..ba55e79 --- /dev/null +++ b/usage-examples/csharp/driver/Examples/Aggregation/Order.cs @@ -0,0 +1,15 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace Examples.Aggregation; + +// :snippet-start: class-declaration +public class Order +{ + [BsonId] + public ObjectId Id { get; set; } + public string CustomerId { get; set; } + public DateTime OrderDate { get; set; } + public int Value { get; set; } +} +// :snippet-end: \ No newline at end of file diff --git a/usage-examples/csharp/driver/Examples/Examples.csproj b/usage-examples/csharp/driver/Examples/Examples.csproj new file mode 100644 index 0000000..c1efe6c --- /dev/null +++ b/usage-examples/csharp/driver/Examples/Examples.csproj @@ -0,0 +1,15 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + diff --git a/usage-examples/csharp/driver/Examples/Program.cs b/usage-examples/csharp/driver/Examples/Program.cs new file mode 100644 index 0000000..5156ea5 --- /dev/null +++ b/usage-examples/csharp/driver/Examples/Program.cs @@ -0,0 +1 @@ +Console.WriteLine("This project is not intended to be run as a console application. Run the tests using the 'Tests' solution."); \ No newline at end of file diff --git a/usage-examples/csharp/driver/README.md b/usage-examples/csharp/driver/README.md new file mode 100644 index 0000000..1d1a13f --- /dev/null +++ b/usage-examples/csharp/driver/README.md @@ -0,0 +1,105 @@ +# C# Driver Code Example Test PoC + +This is a PoC to explore testing C# Driver code examples for MongoDB documentation. + +The structure of this C# project is as follows: + +- `driver.sln`: This solution contains the following projects: + - `Examples`: This project contains example code and output to validate. + - `Tests`: This project contains the test infrastructure to actually run + the tests by invoking the example code. + +## To run the tests locally + +### Create a MongoDB Docker Deployment + +To run these tests locally, you need a MongoDB Docker deployment. Make sure Docker is +running, and then: + +1. Pull the MongoDB image from Docker Hub: + + ```shell + docker pull mongo + ``` +2. Run the container: + + ```shell + docker run --name mongodb-test -d -p 27017:27017 mongo + ``` + +3. Verify the container is running: + + ```shell + docker ps + ``` + + The output resembles: + + ```text + CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES + ef70cce38f26 mongo "/usr/local/bin/runn…" 29 hours ago Up 29 hours (healthy) 127.0.0.1:63201->27017/tcp mongodb-test + ``` + + You may note the actual port is different than `27017`, if something else is already running on + `27017` on your machine. Note the port next to the IP address for running the tests. Alternately, you can get just + the port info for your container using the following command: + + ```shell + docker port mongodb-test + ``` + + The output resembles: + + ```text + 27017/tcp -> 0.0.0.0:27017 + 27017/tcp -> [::]:27017 + ``` + +### Create a .env file + +Create a file named `.env` at the root of the `/driver` directory. +Add the following values to your .env file, similar to the following example: + +``` +CONNECTION_STRING="mongodb://localhost:27017" +SOLUTION_ROOT="/Users/dachary.carey/workspace/docs-code-examples/usage-examples/csharp/driver/" +``` + +- `CONNECTION_STRING`: replace the port with the port where your local deployment is running. +- `SOLUTION_ROOT`: insert the path to the `driver` directory on your filesystem. + +### Install the dependencies + +This test suite requires you to have `.NET` v9.0 installed. If you +do not yet have .NET installed, refer to +[the .NET installation page](https://learn.microsoft.com/en-us/dotnet/core/install/macos) +for details. + +From the root of each project directory, run the following command to install +dependencies: + +``` +dotnet restore +``` + +### Run the tests + +You can run tests from the command line or through your IDE. + +#### Run All Tests from the command line + +From the `/drivers` directory, run: + +``` +dotnet test +``` + +#### Run Individual Tests from the command line + +You can run a single test within a given test suite (file). + +From the `/drivers` directory, run: + +``` +dotnet test --filter "FullyQualifiedName=YourNamespace.YourTestClass.YourTestMethod" +``` diff --git a/usage-examples/csharp/driver/Tests/GroupTotalTest.cs b/usage-examples/csharp/driver/Tests/GroupTotalTest.cs new file mode 100644 index 0000000..6d8f02d --- /dev/null +++ b/usage-examples/csharp/driver/Tests/GroupTotalTest.cs @@ -0,0 +1,31 @@ +using Examples.Aggregation; + +namespace Tests; + +public class GroupTotalTest +{ + private GroupTotal _example; + [SetUp] + public void Setup() + { + _example = new GroupTotal(); + _example.LoadSampleData(); + } + + [Test] + public void TestOutputMatchesDocs() + { + var results = _example.PerformAggregation(); + + var solutionRoot = DotNetEnv.Env.GetString("SOLUTION_ROOT", "Env variable not found. Verify you have a .env file with a valid connection string."); + var outputLocation = "Examples/Aggregation/GroupTotalOutput.txt"; + var fullPath = Path.Combine(solutionRoot, outputLocation); + var fileData = TestUtils.ReadBsonDocumentsFromFile(fullPath); + + Assert.That(results.Count, Is.EqualTo(fileData.Length), $"Result count {results.Count} does not match output example length {fileData.Length}."); + for (var i = 0; i < fileData.Length; i++) + { + Assert.That(fileData[i], Is.EqualTo(results[i]), $"Mismatch at index {i}: expected {fileData[i]}, got {results[i]}."); + } + } +} \ No newline at end of file diff --git a/usage-examples/csharp/driver/Tests/TestSuiteSetup.cs b/usage-examples/csharp/driver/Tests/TestSuiteSetup.cs new file mode 100644 index 0000000..29e4166 --- /dev/null +++ b/usage-examples/csharp/driver/Tests/TestSuiteSetup.cs @@ -0,0 +1,13 @@ +namespace Tests; + +using DotNetEnv; + +[SetUpFixture] +public class TestSuiteSetup +{ + [OneTimeSetUp] + public void GlobalSetup() + { + Env.TraversePath().Load(); + } +} diff --git a/usage-examples/csharp/driver/Tests/TestUtils.cs b/usage-examples/csharp/driver/Tests/TestUtils.cs new file mode 100644 index 0000000..f94bccc --- /dev/null +++ b/usage-examples/csharp/driver/Tests/TestUtils.cs @@ -0,0 +1,20 @@ +using MongoDB.Bson; + +public static class TestUtils +{ + public static BsonDocument[] ReadBsonDocumentsFromFile(string filePath) + { + var bsonDocuments = new List(); + + foreach (var line in File.ReadLines(filePath)) + { + if (!string.IsNullOrWhiteSpace(line)) + { + var bsonDoc = BsonDocument.Parse(line); + bsonDocuments.Add(bsonDoc); + } + } + + return bsonDocuments.ToArray(); + } +} diff --git a/usage-examples/csharp/driver/Tests/Tests.csproj b/usage-examples/csharp/driver/Tests/Tests.csproj new file mode 100644 index 0000000..9e71565 --- /dev/null +++ b/usage-examples/csharp/driver/Tests/Tests.csproj @@ -0,0 +1,27 @@ + + + + net9.0 + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/usage-examples/csharp/driver/driver.sln b/usage-examples/csharp/driver/driver.sln new file mode 100644 index 0000000..8973cbc --- /dev/null +++ b/usage-examples/csharp/driver/driver.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples", "Examples\Examples.csproj", "{D1D4ACA8-520F-4B09-983D-132861CC8AEB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "Tests\Tests.csproj", "{E1F6E308-2D95-4FBB-8F39-214C70889A3D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D1D4ACA8-520F-4B09-983D-132861CC8AEB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D1D4ACA8-520F-4B09-983D-132861CC8AEB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D1D4ACA8-520F-4B09-983D-132861CC8AEB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D1D4ACA8-520F-4B09-983D-132861CC8AEB}.Release|Any CPU.Build.0 = Release|Any CPU + {E1F6E308-2D95-4FBB-8F39-214C70889A3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E1F6E308-2D95-4FBB-8F39-214C70889A3D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E1F6E308-2D95-4FBB-8F39-214C70889A3D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E1F6E308-2D95-4FBB-8F39-214C70889A3D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal