Implement botbuilder middlware pipeline and default post to connector middleware

This commit is contained in:
Shahin Shayandeh 2017-08-02 14:05:49 -07:00
Родитель 2ae53701ba
Коммит 07b36f00ee
11 изменённых файлов: 642 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Bot.Connector;
namespace Microsoft.Bot.Builder
{
public class ActivityResolver
{
private IActivity activity;
public void Register(IActivity activity)
{
SetField.NotNull(out this.activity, nameof(activity), activity);
}
public IActivity Resolve()
{
return this.activity;
}
}
}

Просмотреть файл

@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Connector;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.Bot.Builder
{
public class Bot
{
private readonly MiddlewareSet middlewareSet;
public Bot(MiddlewareSet middlewareSet)
{
SetField.NotNull(out this.middlewareSet, nameof(middlewareSet), middlewareSet);
}
public Func<BotContext, CancellationToken, Task<bool>> OnReceive = null;
public MiddlewareSet MiddlewareSet => middlewareSet;
public async Task<bool> Receive(BotContext context, CancellationToken token = default(CancellationToken))
{
var done = false;
try
{
await this.middlewareSet.ContextCreated(context, token);
done = await this.middlewareSet.ReceiveActivity(context, token);
if(!done && OnReceive != null)
{
done = await this.OnReceive(context, token);
}
}
finally
{
await this.middlewareSet.ContextDone(context, token);
}
return done;
}
}
public static partial class BotExtensions
{
public static void Use(this Bot bot, params IMiddleware[] middlewares)
{
foreach (var middlerware in middlewares)
{
bot.MiddlewareSet.Middlewares.Add(middlerware);
}
}
public static IServiceCollection UseBotServices(this IServiceCollection services)
{
// BotLogger
services.AddSingleton<IBotLogger, NullLogger>();
// Activity resolver and IActivity
services.AddScoped<ActivityResolver>();
services.AddScoped<IActivity>(provider => provider.GetRequiredService<ActivityResolver>().Resolve());
// Setup botContext
services.AddScoped<IDataContext, NullDataContext>();
services.AddScoped<BotContext>();
services.AddScoped<IBotContext>(provider => provider.GetService<BotContext>());
// add post to connector as the default middleware
services.AddScoped<PostToConnectorMiddleWare>();
services.AddScoped<IMiddleware>(provider => provider.GetService<PostToConnectorMiddleWare>())
.AddScoped<IList<IMiddleware>>(provider => provider.GetServices<IMiddleware>().ToList());
// register middleware set and set it up as IPostToUser
services.AddScoped<MiddlewareSet>()
.AddScoped<IPostToUser>(provider => provider.GetService<MiddlewareSet>());
services.AddScoped<Bot>();
return services;
}
}
public class PostToConnectorMiddleWare : IMiddleware
{
private readonly IConnector connector;
public PostToConnectorMiddleWare(IConnector connector)
{
SetField.NotNull(out this.connector, nameof(connector), connector);
}
public Task ContextCreated(BotContext context, CancellationToken token)
{
return Task.CompletedTask;
}
public async Task<bool> ReceiveActivity(BotContext context, CancellationToken token)
{
return await Task.FromResult(false);
}
public async Task ContextDone(BotContext context, CancellationToken token)
{
await this.FlushResponses(context, token);
}
public async Task PostAsync(BotContext context, IList<IActivity> activities, CancellationToken token)
{
foreach(var activity in activities)
{
context.Responses.Add(activity);
}
await this.FlushResponses(context, token);
}
private async Task FlushResponses(BotContext context, CancellationToken token)
{
await this.connector.Post(context.Responses, token);
context.Responses.Clear();
}
}
}

Просмотреть файл

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Connector;
namespace Microsoft.Bot.Builder
{
public interface IBotContext : IDataContext
{
IActivity Request { get; }
IList<IActivity> Responses { get; set; }
IBotLogger Logger { get; }
}
public static partial class BotContextExtension
{
public static async Task PostAsync(this BotContext context, CancellationToken token)
{
await context.PostAsync(context, new List<IActivity>(), token);
}
public static Task DoneAsync(this IBotContext context, CancellationToken token)
{
throw new NotImplementedException();
}
}
public class BotContext : DynamicContext, IBotContext, IPostToUser
{
private readonly IActivity request;
private IList<IActivity> responses;
private IBotLogger logger;
private IDataContext dataContext;
private IPostToUser postToUser;
public BotContext(IActivity request, IDataContext dataContext, IPostToUser postToUser, IBotLogger logger = null)
{
SetField.NotNull(out this.request, nameof(request), request);
SetField.NotNull(out this.dataContext, nameof(dataContext), dataContext);
SetField.NotNull(out this.postToUser, nameof(postToUser), postToUser);
this.logger = logger;
this.responses = new List<IActivity>();
}
public async Task PostAsync(BotContext context, IList<IActivity> acitivties, CancellationToken token)
{
await this.postToUser.PostAsync(context, acitivties, token);
}
public IActivity Request => request;
public IList<IActivity> Responses { get => responses; set => this.responses = value; }
public IUserContext User => dataContext.User;
public IConversationContext Conversation => dataContext.Conversation;
public IBotContextData Data => dataContext.Data;
public IBotLogger Logger => this.logger;
}
}

Просмотреть файл

@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.Bot.Builder
{
/// <summary>
/// A property bag of bot data.
/// </summary>
public interface IBotDataBag
{
/// <summary>
/// Gets the number of key/value pairs contained in the <see cref="IBotDataBag"/>.
/// </summary>
int Count { get; }
/// <summary>
/// Checks if data bag contains a value with specified key
/// </summary>
/// <param name="key">The key.</param>
bool ContainsKey(string key);
/// <summary>
/// Gets the value associated with the specified key.
/// </summary>
/// <typeparam name="T">The type of the value to set.</typeparam>
/// <param name="key">The key of the value to get.</param>
/// <param name="value">
/// When this method returns, contains the value associated with the specified key, if the key is found;
/// otherwise, the default value for the type of the value parameter. This parameter is passed uninitialized.
/// </param>
/// <returns>true if the <see cref="IBotDataBag"/> contains an element with the specified key; otherwise, false.</returns>
bool TryGetValue<T>(string key, out T value);
/// <summary>
/// Adds the specified key and value to the bot data bag.
/// </summary>
/// <typeparam name="T">The type of the value to get.</typeparam>
/// <param name="key">The key of the element to add.</param>
/// <param name="value">The value of the element to add. The value can be null for reference types.</param>
void SetValue<T>(string key, T value);
/// <summary>
/// Removes the specified key from the bot data bag.
/// </summary>
/// <param name="key">They key of the element to remove</param>
/// <returns>True if removal of the key is successful; otherwise, false</returns>
bool RemoveValue(string key);
/// <summary>
/// Removes all of the values from data bag.
/// </summary>
void Clear();
}
/// <summary>
/// Helper methods.
/// </summary>
public static partial class Extensions
{
[System.Obsolete(@"Use GetValue<T> instead", false)]
public static T Get<T>(this IBotDataBag bag, string key)
{
T value;
if (!bag.TryGetValue(key, out value))
{
throw new KeyNotFoundException(key);
}
return value;
}
/// <summary>
/// Gets the value associated with the specified key.
/// </summary>
/// <typeparam name="T">The type of the value to get.</typeparam>
/// <param name="bag">The bot data bag.</param>
/// <param name="key">The key of the value to get or set.</param>
/// <returns>The value associated with the specified key. If the specified key is not found, a get operation throws a KeyNotFoundException.</returns>
/// <exception cref="KeyNotFoundException"><paramref name="key"/></exception>
public static T GetValue<T>(this IBotDataBag bag, string key)
{
T value;
if (!bag.TryGetValue(key, out value))
{
throw new KeyNotFoundException(key);
}
return value;
}
/// <summary>
/// Gets the value associated with the specified key or a default value if not found.
/// </summary>
/// <typeparam name="T">The type of the value to get.</typeparam>
/// <param name="bag">The bot data bag.</param>
/// <param name="key">The key of the value to get or set.</param>
/// <param name="defaultValue">The value to return if the key is not present</param>
/// <returns>The value associated with the specified key. If the specified key is not found, <paramref name="defaultValue"/>
/// is returned </returns>
public static T GetValueOrDefault<T>(this IBotDataBag bag, string key, T defaultValue = default(T))
{
T value;
if (!bag.TryGetValue(key, out value))
{
value = defaultValue;
}
return value;
}
}
}

Просмотреть файл

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.Bot.Builder
{
public interface IBotLogger
{
}
public class NullLogger : IBotLogger
{
}
}

Просмотреть файл

@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Connector;
using Microsoft.Extensions.Primitives;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
namespace Microsoft.Bot.Builder
{
public interface IConnector
{
Task Receive(IDictionary<string, StringValues> headers, IActivity activity, CancellationToken token);
Task Post(IList<IActivity> activities, CancellationToken token);
}
public abstract class Connector : IConnector
{
protected readonly IServiceProvider serviceProvider;
public Connector(IServiceProvider serviceProvider)
{
SetField.NotNull(out this.serviceProvider, nameof(serviceProvider), serviceProvider);
}
public abstract Task Post(IList<IActivity> activities, CancellationToken token);
public virtual async Task Receive(IDictionary<string, StringValues> headers, IActivity activity, CancellationToken token)
{
var resolver = this.serviceProvider.GetRequiredService<ActivityResolver>();
resolver.Register(activity);
var bot = this.serviceProvider.GetRequiredService<Bot>();
var context = this.serviceProvider.GetRequiredService<BotContext>();
await bot.Receive(context, token);
}
}
public class BotFrameworkConnector : Connector
{
private readonly BotAuthenticator botAuthenticator;
public BotFrameworkConnector(IServiceProvider serviceProvider, BotAuthenticator botAuthenticator)
: base(serviceProvider)
{
SetField.NotNull(out this.botAuthenticator, nameof(botAuthenticator), botAuthenticator);
}
public async override Task Post(IList<IActivity> activities, CancellationToken token)
{
var connectorClient = this.serviceProvider.GetRequiredService<IConnectorClient>();
foreach (Activity activity in activities)
{
await connectorClient.Conversations.SendToConversationAsync(activity, token);
}
}
public async override Task Receive(IDictionary<string, StringValues> headers, IActivity activity, CancellationToken token)
{
if (await botAuthenticator.TryAuthenticateAsync(headers, new[] { activity }, token))
{
await base.Receive(headers, activity, token);
}
else
{
throw new UnauthorizedAccessException();
}
}
}
public class TraceConnector : Connector
{
public TraceConnector(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}
public override Task Post(IList<IActivity> activities, CancellationToken token)
{
foreach (var activity in activities)
{
if (activity.GetActivityType() == ActivityTypes.Message)
{
Trace.WriteLine((activity as IMessageActivity).Text);
}
else
{
Trace.WriteLine((activity as Activity).Type);
}
}
return Task.CompletedTask;
}
}
public static partial class ConnectorExtensions
{
public static IServiceCollection UseTraceConnector(this IServiceCollection services)
{
services.AddScoped<IConnector, TraceConnector>();
return services;
}
public static IServiceCollection UseBotFrameworkConnector(this IServiceCollection services)
{
services.UseBotConnector();
services.AddScoped<IConnector, BotFrameworkConnector>();
services.AddScoped<IConnectorClient>(provider =>
{
var activity = provider.GetRequiredService<IActivity>();
return new ConnectorClient(new Uri(activity.ServiceUrl));
});
services.AddSingleton<BotAuthenticator>(provider =>
{
var config = provider.GetRequiredService<IConfigurationRoot>();
var appId = config.GetSection(MicrosoftAppCredentials.MicrosoftAppIdKey)?.Value;
var passwrod = config.GetSection(MicrosoftAppCredentials.MicrosoftAppPasswordKey)?.Value;
return new BotAuthenticator(appId, passwrod);
});
return services;
}
}
}

Просмотреть файл

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Bot.Connector;
namespace Microsoft.Bot.Builder
{
public interface IUserContext
{
IBotDataBag Data { get; }
}
public interface IConversationContext
{
ConversationReference reference { get; }
IBotDataBag Data { get; }
}
public interface IBotContextData
{
IBotDataBag Data { get; }
}
public interface IDataContext
{
IUserContext User { get; }
IConversationContext Conversation { get; }
IBotContextData Data { get; }
}
public sealed class DataContext : IDataContext
{
private readonly IUserContext user;
private readonly IConversationContext conversation;
private readonly IBotContextData data;
public DataContext(IUserContext user, IConversationContext conversation, IBotContextData data)
{
SetField.NotNull(out this.user, nameof(user), user);
SetField.NotNull(out this.conversation, nameof(conversation), conversation);
SetField.NotNull(out this.data, nameof(data), data);
}
public IUserContext User => this.user;
public IConversationContext Conversation => this.conversation;
public IBotContextData Data => this.data;
}
public sealed class NullDataContext : IDataContext
{
public NullDataContext()
{
}
public IUserContext User => null;
public IConversationContext Conversation => null;
public IBotContextData Data => null;
}
}

Просмотреть файл

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Connector;
namespace Microsoft.Bot.Builder
{
public interface IContextInitializer
{
Task ContextCreated(BotContext context, CancellationToken token);
}
public interface IPostToBot
{
Task<bool> ReceiveActivity(BotContext context, CancellationToken token);
}
public interface IPostToUser
{
Task PostAsync(BotContext context, IList<IActivity> acitivties, CancellationToken token);
}
public interface IContextFinalizer
{
Task ContextDone(BotContext context, CancellationToken token);
}
public interface IMiddleware : IContextInitializer, IPostToBot, IPostToUser, IContextFinalizer
{
}
}

Просмотреть файл

@ -8,4 +8,8 @@
<PackageReference Include="System.Dynamic.Runtime" Version="4.3.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Bot.Connector\Microsoft.Bot.Connector.csproj" />
</ItemGroup>
</Project>

Просмотреть файл

@ -0,0 +1,50 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Bot.Connector;
namespace Microsoft.Bot.Builder
{
public class MiddlewareSet : IMiddleware
{
private readonly IList<IMiddleware> middlewares;
public MiddlewareSet(IList<IMiddleware> middlewares)
{
SetField.NotNull(out this.middlewares, nameof(middlewares), middlewares);
}
public IList<IMiddleware> Middlewares => middlewares;
public virtual async Task ContextCreated(BotContext context, CancellationToken token)
{
foreach (var m in middlewares)
await m.ContextCreated(context, token);
}
public virtual async Task<bool> ReceiveActivity(BotContext context, CancellationToken token)
{
var handled = false;
foreach (var middleware in middlewares)
{
handled = await middleware.ReceiveActivity(context, token);
if (handled) break;
}
return handled;
}
public virtual async Task ContextDone(BotContext context, CancellationToken token)
{
foreach (var m in middlewares.Reverse())
await m.ContextDone(context, token);
}
public virtual async Task PostAsync(BotContext context, IList<IActivity> acitivties, CancellationToken token)
{
foreach (var m in middlewares.Reverse())
await m.PostAsync(context, acitivties, token);
}
}
}

Просмотреть файл

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Microsoft.Bot.Builder
{
public static class SetField
{
public static void NotNull<T>(out T field, string name, T value)
where T : class
{
if (value != null)
{
field = value;
}
else
{
throw new ArgumentNullException(name);
}
}
public static void CheckNull<T>(string name, T value)
where T : class
{
if (value == null)
{
throw new ArgumentNullException(name);
}
}
}
}