diff --git a/Bot.Builder.Community.sln b/Bot.Builder.Community.sln index 0e3583bc..6cec3361 100644 --- a/Bot.Builder.Community.sln +++ b/Bot.Builder.Community.sln @@ -183,6 +183,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bot.Builder.Community.Adapt EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bot.Builder.Community.Adapters.Facebook.Tests", "tests\Bot.Builder.Community.Adapters.Facebook.Tests\Bot.Builder.Community.Adapters.Facebook.Tests.csproj", "{8201DC48-763A-4534-9E51-466E15DF01D8}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bot.Builder.Community.Storage.MongoDB", "libraries\Bot.Builder.Community.Storage.MongoDB\Bot.Builder.Community.Storage.MongoDB.csproj", "{74BE0FA2-3C6D-4807-8C73-CF87E898276C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug - NuGet Packages|Any CPU = Debug - NuGet Packages|Any CPU @@ -805,6 +807,14 @@ Global {8201DC48-763A-4534-9E51-466E15DF01D8}.Documentation|Any CPU.Build.0 = Debug|Any CPU {8201DC48-763A-4534-9E51-466E15DF01D8}.Release|Any CPU.ActiveCfg = Release|Any CPU {8201DC48-763A-4534-9E51-466E15DF01D8}.Release|Any CPU.Build.0 = Release|Any CPU + {74BE0FA2-3C6D-4807-8C73-CF87E898276C}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU + {74BE0FA2-3C6D-4807-8C73-CF87E898276C}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU + {74BE0FA2-3C6D-4807-8C73-CF87E898276C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {74BE0FA2-3C6D-4807-8C73-CF87E898276C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {74BE0FA2-3C6D-4807-8C73-CF87E898276C}.Documentation|Any CPU.ActiveCfg = Debug|Any CPU + {74BE0FA2-3C6D-4807-8C73-CF87E898276C}.Documentation|Any CPU.Build.0 = Debug|Any CPU + {74BE0FA2-3C6D-4807-8C73-CF87E898276C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {74BE0FA2-3C6D-4807-8C73-CF87E898276C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -893,6 +903,7 @@ Global {428AD1B4-DF58-4D21-9C19-AB4AB6001A90} = {840D4038-9AB8-4750-9FFE-365386CE47E2} {3348B9A5-E3CE-4AF8-B059-8B4D7971C25A} = {840D4038-9AB8-4750-9FFE-365386CE47E2} {8201DC48-763A-4534-9E51-466E15DF01D8} = {840D4038-9AB8-4750-9FFE-365386CE47E2} + {74BE0FA2-3C6D-4807-8C73-CF87E898276C} = {DC62D60A-2EA2-4DB1-B1BA-C8F38D3940B3} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {9FE3B75E-BA2B-45BC-BBF0-DDA8BA10C4F0} diff --git a/libraries/Bot.Builder.Community.Storage.MongoDB/Bot.Builder.Community.Storage.MongoDB.csproj b/libraries/Bot.Builder.Community.Storage.MongoDB/Bot.Builder.Community.Storage.MongoDB.csproj new file mode 100644 index 00000000..af2f8177 --- /dev/null +++ b/libraries/Bot.Builder.Community.Storage.MongoDB/Bot.Builder.Community.Storage.MongoDB.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.0 + + + + + + + + diff --git a/libraries/Bot.Builder.Community.Storage.MongoDB/MongoDbStorage.cs b/libraries/Bot.Builder.Community.Storage.MongoDB/MongoDbStorage.cs new file mode 100644 index 00000000..7a654a20 --- /dev/null +++ b/libraries/Bot.Builder.Community.Storage.MongoDB/MongoDbStorage.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.Builder; +using Microsoft.Extensions.Options; +using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Driver; +using Newtonsoft.Json.Linq; + +namespace Bot.Builder.Community.Storage.MongoDB +{ + public class MongoDbStorage: IStorage + { + private readonly MongoDbStorageOptions _options; + private readonly MongoClient _mongoClient; + + + /// + /// Initializes a new instance of the class. + /// + /// MongoDb options class. + public MongoDbStorage(MongoDbStorageOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + if (string.IsNullOrEmpty(options.ConnectionString)) + { + throw new ArgumentNullException(nameof(options.ConnectionString)); + } + + _options = options; + + _mongoClient = new MongoClient(options.ConnectionString); + + } + + /// + /// Reads storage items from storage. + /// + /// keys of the objects to read. + /// A cancellation token that can be used by other objects + /// or threads to receive notice of cancellation. + /// A task that represents the work queued to execute. + /// If the activities are successfully sent, the task result contains + /// the items read, indexed by key. + /// + /// + public async Task> ReadAsync(string[] keys, CancellationToken cancellationToken = new CancellationToken()) + { + var filter = Builders.Filter + .In(o => o.Id, keys); + + var result = await GetCollection().Find(filter).ToListAsync(cancellationToken: cancellationToken); + + return result.ToDictionary(x => x.Id, x => x.Data); + } + + /// + /// Writes storage items to storage. + /// + /// The items to write, indexed by key. + /// A cancellation token that can be used by other objects + /// or threads to receive notice of cancellation. + /// A task that represents the work queued to execute. + /// + /// + public async Task WriteAsync(IDictionary changes, CancellationToken cancellationToken = new CancellationToken()) + { + var collection = GetCollection(); + foreach (var change in changes) + { + var filter = Builders.Filter.Eq(o => o.Id, change.Key); + var update = Builders.Update.Set(o => o.Data, change.Value); + var options = new UpdateOptions { IsUpsert = true }; + await collection.UpdateOneAsync(filter, update, options, cancellationToken); + } + } + + /// + /// Writes storage items to storage. + /// + /// The items to write, indexed by key. + /// A cancellation token that can be used by other objects + /// or threads to receive notice of cancellation. + /// A task that represents the work queued to execute. + /// + /// + public Task DeleteAsync(string[] keys, CancellationToken cancellationToken = new CancellationToken()) + { + + var filter = Builders.Filter + .In(restaurant => restaurant.Id, keys); + + return GetCollection().DeleteManyAsync(filter, cancellationToken); + } + + private IMongoCollection GetCollection() + { + var database = _mongoClient.GetDatabase(_options.DatabaseName); + var collection = database.GetCollection(_options.CollectionName); + return collection; + } + private class StorageEntry + { + public string Id { get; set; } + public object Data { get; set; } + } + } +} \ No newline at end of file diff --git a/libraries/Bot.Builder.Community.Storage.MongoDB/MongoDbStorageOptions.cs b/libraries/Bot.Builder.Community.Storage.MongoDB/MongoDbStorageOptions.cs new file mode 100644 index 00000000..e5ccbccd --- /dev/null +++ b/libraries/Bot.Builder.Community.Storage.MongoDB/MongoDbStorageOptions.cs @@ -0,0 +1,39 @@ +namespace Bot.Builder.Community.Storage.MongoDB +{ + /// + /// Represents the MongoDB storage configuration options. + /// + /// + /// This class is used to configure the settings for connecting to a MongoDB instance, specifying the database and collection to be used for storage. + /// + public class MongoDbStorageOptions + { + /// + /// Gets or sets the connection string for the MongoDB instance. + /// + /// The connection string. + /// + /// The connection string is used to specify the location and authentication details for the MongoDB instance. + /// + public string ConnectionString { get; set; } + + /// + /// Gets or sets the name of the database to be used for storage. + /// + /// The name of the database. + /// + /// The database name is used to determine which database within the MongoDB instance should be used for storing data. + /// + public string DatabaseName { get; set; } + + /// + /// Gets or sets the name of the collection to be used for storage. + /// + /// The name of the collection. Defaults to "StateData". + /// + /// The collection name is used to determine which collection within the specified database should be used for storing data. If not specified, the default value is "StateData". + /// + public string CollectionName { get; set; } = "StateData"; + } + +} \ No newline at end of file diff --git a/libraries/Bot.Builder.Community.Storage.MongoDB/MongoDbStorageSettings.cs b/libraries/Bot.Builder.Community.Storage.MongoDB/MongoDbStorageSettings.cs new file mode 100644 index 00000000..12da8384 --- /dev/null +++ b/libraries/Bot.Builder.Community.Storage.MongoDB/MongoDbStorageSettings.cs @@ -0,0 +1,87 @@ +using System; +using Microsoft.Extensions.Configuration; + +namespace Bot.Builder.Community.Storage.MongoDB +{ + public class MongoDbStorageSettings + { + public Type[] AllowedTypes { get; private set; } + + public MongoDbStorageOptions Options { get; private set; } + + /// + /// Registers the specified types for MongoDB storage. + /// + /// An array of objects to be registered for storage. + /// The current instance, allowing method calls to be chained. + /// + /// This method sets the property with the provided types. It is useful for configuring the MongoDB storage settings with the desired types that need to be stored in the database. + /// + + public MongoDbStorageSettings RegisterTypes(params Type[] types) + { + AllowedTypes = types; + return this; + } + + /// + /// Configures the using the provided . + /// + /// The to bind the options to. + /// The current instance, allowing method calls to be chained. + /// + /// This method creates a new instance of and binds the provided configuration section to it. The resulting options are then used to configure the MongoDB storage settings. + /// + /// + /// ... + /// "ConnectionString": "...", + /// "DatabaseName": "...", + /// "CollectionName": "...", + /// ... + /// + public MongoDbStorageSettings ConfigureOptions(IConfigurationSection configuration) + { + Options = new MongoDbStorageOptions(); + configuration.Bind(Options); + return this; + } + + /// + /// Configures the using the provided instance. + /// + /// The instance to use for configuration. + /// The current instance, allowing method calls to be chained. + /// + /// This method sets the property with the provided instance. It is useful for configuring the MongoDB storage settings using a preconfigured options object. + /// + public MongoDbStorageSettings ConfigureOptions(MongoDbStorageOptions options) + { + Options = options; + return this; + } + + /// + /// Configures the using the provided connection string, database name, and optionally, collection name. + /// + /// The connection string for the MongoDB instance. + /// The name of the database to be used for storage. + /// The optional name of the collection to be used for storage (default is null). + /// The current instance, allowing method calls to be chained. + /// + /// This method creates a new instance of and sets the provided connection string, database name, and collection name (if provided). The resulting options are then used to configure the MongoDB storage settings. + /// + public MongoDbStorageSettings ConfigureOptions( + string connectionString, + string databaseName, + string collectionName = null) + { + Options = new MongoDbStorageOptions + { + ConnectionString = connectionString, + DatabaseName = databaseName, + CollectionName = collectionName + }; + return this; + } + } +} \ No newline at end of file diff --git a/libraries/Bot.Builder.Community.Storage.MongoDB/Readme.md b/libraries/Bot.Builder.Community.Storage.MongoDB/Readme.md new file mode 100644 index 00000000..32cc3f75 --- /dev/null +++ b/libraries/Bot.Builder.Community.Storage.MongoDB/Readme.md @@ -0,0 +1,46 @@ +# MongoDB Storage Integration + +This functionality enables seamless integration of MongoDB storage into your existing project. With this integration, you can easily store and retrieve data using MongoDB as your backend storage system. + +## Features +- Robust and scalable MongoDB storage solution +- Easy configuration and registration of MongoDB storage using extension methods +- Support for custom serialization of allowed types + +## Getting Started + +To get started, follow these steps: + +1. Install the required package `Bot.Builder.Community.Storage.MongoDB` for your project + + +2. Add the following using statements to your project: +```csharp +using Bot.Builder.Community.Storage.MongoDB; +``` + +3. In the ConfigureServices method of your Startup.cs file, add the following code to configure and register MongoDB storage: +```csharp +services.AddMongoDbStorage(settings => +{ + settings.ConfigureOptions(Configuration.GetSection("MongoDb")); + settings.RegisterTypes( + typeof(YourType1), + typeof(YourType2) + ); +}); +``` +Replace YourType with the type(s) you want to store in + +4. Update your appsettings.json file to include the MongoDB configuration settings: + +```json +{ + "MongoDb": { + "ConnectionString": "your_connection_string", + "Database": "your_database_name", + "Collection": "your_collection_name" + } +} +``` +Replace your_connection_string, your_database_name, and your_collection_name with the appropriate values for your MongoDB instance. \ No newline at end of file diff --git a/libraries/Bot.Builder.Community.Storage.MongoDB/ServiceCollectionExtensions.cs b/libraries/Bot.Builder.Community.Storage.MongoDB/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..6f7bf5ee --- /dev/null +++ b/libraries/Bot.Builder.Community.Storage.MongoDB/ServiceCollectionExtensions.cs @@ -0,0 +1,54 @@ +using System; +using System.Linq; +using Microsoft.Bot.Builder; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace Bot.Builder.Community.Storage.MongoDB +{ + public static class ServiceCollectionExtensions + { + /// + /// Adds and configures MongoDB storage support + /// + /// The to add the storage to. + /// A delegate to configure the . + /// The of the storage context (default is ). + /// The same instance so that multiple calls can be chained. + /// Thrown when no types are found to scan or when the connection string or database name is not provided. + /// + /// This extension method configures MongoDB storage support for the specified service collection. It requires the caller to provide at least one type to scan and a valid connection string and database name. + /// + public static IServiceCollection AddMongoDbStorage(this IServiceCollection services, + Action settings, + ServiceLifetime contextLifetime = ServiceLifetime.Singleton) + { + var config = new MongoDbStorageSettings(); + + settings.Invoke(config); + + if (!config.AllowedTypes.Any()) + { + throw new ArgumentException("No types found to scan. Supply at least one type"); + } + + if (config.Options == null + || string.IsNullOrEmpty(config.Options.ConnectionString) + || string.IsNullOrEmpty(config.Options.DatabaseName) + ) + { + throw new ArgumentException("No connection string or database name found."); + } + + var objectSerializer = new ObjectSerializer(type => ObjectSerializer.DefaultAllowedTypes(type) || config.AllowedTypes.Contains(type)); + + BsonSerializer.RegisterSerializer(objectSerializer); + + services.TryAdd(new ServiceDescriptor(typeof(IStorage), provider => new MongoDbStorage(config.Options), contextLifetime)); + + return services; + } + } +} \ No newline at end of file