Skip to content

Add MongoDB storage support #531

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Bot.Builder.Community.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Bot.Builder" Version="4.13.1" />
<PackageReference Include="MongoDB.Driver" Version="2.19.1" />
</ItemGroup>

</Project>
114 changes: 114 additions & 0 deletions libraries/Bot.Builder.Community.Storage.MongoDB/MongoDbStorage.cs
Original file line number Diff line number Diff line change
@@ -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;


/// <summary>
/// Initializes a new instance of the <see cref="MongoDbStorage"/> class.
/// </summary>
/// <param name="options">MongoDb options <see cref="MongoDbStorage"/> class.</param>
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);

}

/// <summary>
/// Reads storage items from storage.
/// </summary>
/// <param name="keys">keys of the <see cref="IStoreItem"/> objects to read.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <remarks>If the activities are successfully sent, the task result contains
/// the items read, indexed by key.</remarks>
/// <seealso cref="DeleteAsync(string[], CancellationToken)"/>
/// <seealso cref="WriteAsync(IDictionary{string, object}, CancellationToken)"/>
public async Task<IDictionary<string, object>> ReadAsync(string[] keys, CancellationToken cancellationToken = new CancellationToken())
{
var filter = Builders<StorageEntry>.Filter
.In(o => o.Id, keys);

var result = await GetCollection().Find(filter).ToListAsync(cancellationToken: cancellationToken);

return result.ToDictionary(x => x.Id, x => x.Data);
}

/// <summary>
/// Writes storage items to storage.
/// </summary>
/// <param name="changes">The items to write, indexed by key.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <seealso cref="DeleteAsync(string[], CancellationToken)"/>
/// <seealso cref="ReadAsync(string[], CancellationToken)"/>
public async Task WriteAsync(IDictionary<string, object> changes, CancellationToken cancellationToken = new CancellationToken())
{
var collection = GetCollection();
foreach (var change in changes)
{
var filter = Builders<StorageEntry>.Filter.Eq(o => o.Id, change.Key);
var update = Builders<StorageEntry>.Update.Set(o => o.Data, change.Value);
var options = new UpdateOptions { IsUpsert = true };
await collection.UpdateOneAsync(filter, update, options, cancellationToken);
}
}

/// <summary>
/// Writes storage items to storage.
/// </summary>
/// <param name="changes">The items to write, indexed by key.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
/// <seealso cref="DeleteAsync(string[], CancellationToken)"/>
/// <seealso cref="ReadAsync(string[], CancellationToken)"/>
public Task DeleteAsync(string[] keys, CancellationToken cancellationToken = new CancellationToken())
{

var filter = Builders<StorageEntry>.Filter
.In(restaurant => restaurant.Id, keys);

return GetCollection().DeleteManyAsync(filter, cancellationToken);
}

private IMongoCollection<StorageEntry> GetCollection()
{
var database = _mongoClient.GetDatabase(_options.DatabaseName);
var collection = database.GetCollection<StorageEntry>(_options.CollectionName);
return collection;
}
private class StorageEntry
{
public string Id { get; set; }
public object Data { get; set; }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace Bot.Builder.Community.Storage.MongoDB
{
/// <summary>
/// Represents the MongoDB storage configuration options.
/// </summary>
/// <remarks>
/// This class is used to configure the settings for connecting to a MongoDB instance, specifying the database and collection to be used for storage.
/// </remarks>
public class MongoDbStorageOptions
{
/// <summary>
/// Gets or sets the connection string for the MongoDB instance.
/// </summary>
/// <value>The connection string.</value>
/// <remarks>
/// The connection string is used to specify the location and authentication details for the MongoDB instance.
/// </remarks>
public string ConnectionString { get; set; }

/// <summary>
/// Gets or sets the name of the database to be used for storage.
/// </summary>
/// <value>The name of the database.</value>
/// <remarks>
/// The database name is used to determine which database within the MongoDB instance should be used for storing data.
/// </remarks>
public string DatabaseName { get; set; }

/// <summary>
/// Gets or sets the name of the collection to be used for storage.
/// </summary>
/// <value>The name of the collection. Defaults to "StateData".</value>
/// <remarks>
/// 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".
/// </remarks>
public string CollectionName { get; set; } = "StateData";
}

}
Original file line number Diff line number Diff line change
@@ -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; }

/// <summary>
/// Registers the specified types for MongoDB storage.
/// </summary>
/// <param name="types">An array of <see cref="Type"/> objects to be registered for storage.</param>
/// <returns>The current <see cref="MongoDbStorageSettings"/> instance, allowing method calls to be chained.</returns>
/// <remarks>
/// This method sets the <see cref="AllowedTypes"/> 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.
/// </remarks>

public MongoDbStorageSettings RegisterTypes(params Type[] types)
{
AllowedTypes = types;
return this;
}

/// <summary>
/// Configures the <see cref="MongoDbStorageOptions"/> using the provided <see cref="IConfigurationSection"/>.
/// </summary>
/// <param name="configuration">The <see cref="IConfigurationSection"/> to bind the options to.</param>
/// <returns>The current <see cref="MongoDbStorageSettings"/> instance, allowing method calls to be chained.</returns>
/// <remarks>
/// This method creates a new instance of <see cref="MongoDbStorageOptions"/> and binds the provided configuration section to it. The resulting options are then used to configure the MongoDB storage settings.
/// </remarks>
/// <example>
/// ...
/// "ConnectionString": "...",
/// "DatabaseName": "...",
/// "CollectionName": "...",
/// ...
/// </example>
public MongoDbStorageSettings ConfigureOptions(IConfigurationSection configuration)
{
Options = new MongoDbStorageOptions();
configuration.Bind(Options);
return this;
}

/// <summary>
/// Configures the <see cref="MongoDbStorageOptions"/> using the provided <see cref="MongoDbStorageOptions"/> instance.
/// </summary>
/// <param name="options">The <see cref="MongoDbStorageOptions"/> instance to use for configuration.</param>
/// <returns>The current <see cref="MongoDbStorageSettings"/> instance, allowing method calls to be chained.</returns>
/// <remarks>
/// This method sets the <see cref="Options"/> property with the provided <see cref="MongoDbStorageOptions"/> instance. It is useful for configuring the MongoDB storage settings using a preconfigured options object.
/// </remarks>
public MongoDbStorageSettings ConfigureOptions(MongoDbStorageOptions options)
{
Options = options;
return this;
}

/// <summary>
/// Configures the <see cref="MongoDbStorageOptions"/> using the provided connection string, database name, and optionally, collection name.
/// </summary>
/// <param name="connectionString">The connection string for the MongoDB instance.</param>
/// <param name="databaseName">The name of the database to be used for storage.</param>
/// <param name="collectionName">The optional name of the collection to be used for storage (default is null).</param>
/// <returns>The current <see cref="MongoDbStorageSettings"/> instance, allowing method calls to be chained.</returns>
/// <remarks>
/// This method creates a new instance of <see cref="MongoDbStorageOptions"/> 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.
/// </remarks>
public MongoDbStorageSettings ConfigureOptions(
string connectionString,
string databaseName,
string collectionName = null)
{
Options = new MongoDbStorageOptions
{
ConnectionString = connectionString,
DatabaseName = databaseName,
CollectionName = collectionName
};
return this;
}
}
}
46 changes: 46 additions & 0 deletions libraries/Bot.Builder.Community.Storage.MongoDB/Readme.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Adds and configures MongoDB storage support
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/> to add the storage to.</param>
/// <param name="settings">A delegate to configure the <see cref="MongoDbStorageSettings"/>.</param>
/// <param name="contextLifetime">The <see cref="ServiceLifetime"/> of the storage context (default is <see cref="ServiceLifetime.Singleton"/>).</param>
/// <returns>The same <see cref="IServiceCollection"/> instance so that multiple calls can be chained.</returns>
/// <exception cref="ArgumentException">Thrown when no types are found to scan or when the connection string or database name is not provided.</exception>
/// <remarks>
/// 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.
/// </remarks>
public static IServiceCollection AddMongoDbStorage(this IServiceCollection services,
Action<MongoDbStorageSettings> 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;
}
}
}