您现在访问的是微软AZURE全球版技术文档网站,若需要访问由世纪互联运营的MICROSOFT AZURE中国区技术文档网站,请访问 https://docs.azure.cn.

.NET 迁移快速参考.NET migration quick reference

适用于: SDK v4APPLIES TO: SDK v4

BotBuilder .NET SDK v4 引入了多项基本变更,这些变更影响我们创作机器人的方式。The BotBuilder .NET SDK v4 introduces several fundamental changes that affect how bots are authored. 本指南的目的是为用户提供快速参考,重点介绍在 v3 和 v4 SDK 中完成任务有哪些常见差异。The purpose of this guide is to provide a quick reference to highlight common differences between accomplishing tasks in the v3 and v4 SDKs.

  • 在机器人和通道之间传递信息的方式已改变。How information passes between a bot and channels has changed. 在 v3 中,我们使用“聊天”对象 和 SendAsync 方法来处理消息,并广泛使用 Autofac 来加载各种依赖项。In v3, you used the Conversation object and SendAsync method to process a message, and Autofac was used extensively for loading various dependencies. 在 v4 中,我们使用“适配器” 和 TurnContext 对象来处理消息,并可使用所选的依赖项注入库。In v4, you use the Adapter and TurnContext objects to process a message, and you can use the dependency injection library of your choice.

  • 另外,我们已将对话和机器人实例进一步分离。Also, dialogs and bot instances have been further decoupled. 在 v3 中,对话内置到核心 SDK 中,堆栈在内部处理,子对话使用 CallForward 方法进行加载。In v3, dialogs were built into the core SDK and the stack was handled internally, and child dialogs were loaded with the Call and Forward methods. 在 v4 中,现在可以将对话作为参数传递到机器人实例中,以便更灵活地进行组合;可以由开发人员控制对话堆栈;子对话使用 BeginDialogAsyncReplaceDialogAsync 方法进行加载。In v4, you now pass dialogs into bot instances as arguments, providing greater compositional flexibility and developer control of the dialog stack, and child dialogs are loaded with the BeginDialogAsync and ReplaceDialogAsync methods.

  • 另外,v4 还提供了一个 ActivityHandler 类,用于自动处理不同类型的活动,例如消息、聊天更新和事件活动。 Moreover, v4 provides an ActivityHandler class, which helps automate the handling of different types of activities, such as message, conversation update, and event activities.

这些改进导致在 .NET 中开发机器人的语法发生了变化,尤其是在创建机器人对象、定义对话和编码事件处理逻辑方面发生了变化。These improvements result in changes in syntax for developing bots in .NET, especially around creating bot objects, defining dialogs, and coding event handling logic.

本主题的其余部分将 .NET Bot Framework SDK v3 中的构造与 v4 中的相应构造进行了对比。The rest of this topic compares the constructs in the .NET Bot Framework SDK v3 to their equivalent in v4.

处理传入消息To process incoming messages

v3v3

[BotAuthentication]
public class MessagesController : ApiController
{
    public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
    {
        if (activity.GetActivityType() == ActivityTypes.Message)
        {
            await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
        }

        return Request.CreateResponse(HttpStatusCode.OK);
    }
}

v4v4

[Route("api/messages")]
[ApiController]
public class BotController : ControllerBase
{
    private readonly IBotFrameworkHttpAdapter Adapter;
    private readonly IBot Bot;

    public BotController(IBotFrameworkHttpAdapter adapter, IBot bot)
    {
        Adapter = adapter;
        Bot = bot;
    }

    [HttpPost]
    public async Task PostAsync()
    {
        await Adapter.ProcessAsync(Request, Response, Bot);
    }
}

向用户发送消息To send a message to a user

v3v3

await context.PostAsync("Hello and welcome to the help desk bot.");

v4v4

await turnContext.SendActivityAsync("Hello and welcome to the help desk bot.");

加载根对话To load a root dialog

v3v3

await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());

v4v4

// Create a DialogExtensions class with a Run method.
public static class DialogExtensions
{
    public static async Task Run(
        this Dialog dialog,
        ITurnContext turnContext,
        IStatePropertyAccessor<DialogState> accessor,
        CancellationToken cancellationToken)
    {
        var dialogSet = new DialogSet(accessor);
        dialogSet.Add(dialog);

        var dialogContext = await dialogSet.CreateContextAsync(turnContext, cancellationToken);

        var results = await dialogContext.ContinueDialogAsync(cancellationToken);
        if (results.Status == DialogTurnStatus.Empty)
        {
            await dialogContext.BeginDialogAsync(dialog.Id, null, cancellationToken);
        }
    }
}

// Call it from the ActivityHandler's OnMessageActivityAsync override
protected override async Task OnMessageActivityAsync(
    ITurnContext<IMessageActivity> turnContext,
    CancellationToken cancellationToken)
{
    // Run the Dialog with the new message Activity.
    await Dialog.Run(
        turnContext,
        ConversationState.CreateProperty<DialogState>("DialogState"),
        cancellationToken);
}

启动子对话To start a child dialog

v3v3

context.Call(new NextDialog(), this.ResumeAfterNextDialog);

or

await context.Forward(new NextDialog(), this.ResumeAfterNextDialog, message);

v4v4

dialogContext.BeginDialogAsync("<child-dialog-id>", options);

or

dialogContext.ReplaceDialogAsync("<child-dialog-id>", options);

结束对话:To end a dialog

v3v3

context.Done(ReturnValue);

v4v4

await context.EndDialogAsync(ReturnValue);

提示用户输入To prompt a user for input

v3v3

PromptDialog.Choice(
    context,
    this.OnOptionSelected,
    Options, PromptMessage,
    ErrorMessage,
    3,
    PromptStyle.PerLine);

v4v4

// In the dialog's constructor, register the prompt, and waterfall steps.
AddDialog(new TextPrompt(nameof(TextPrompt)));
AddDialog(new WaterfallDialog(nameof(WaterfallDialog), new WaterfallStep[]
{
    FirstStepAsync,
    SecondStepAsync,
}));

// The initial child Dialog to run.
InitialDialogId = nameof(WaterfallDialog);

// ...

// In the first step, invoke the prompt.
private async Task<DialogTurnResult> FirstStepAsync(
    WaterfallStepContext stepContext,
    CancellationToken cancellationToken)
{
    return await stepContext.PromptAsync(
        nameof(TextPrompt),
        new PromptOptions { Prompt = MessageFactory.Text("Please enter your destination.") },
        cancellationToken);
}

// In the second step, retrieve the Result from the stepContext.
private async Task<DialogTurnResult> SecondStepAsync(
    WaterfallStepContext stepContext,
    CancellationToken cancellationToken)
{
    var destination = (string)stepContext.Result;
}

将信息保存到对话状态To save information to dialog state

v3v3

在 V3 中,所有对话及其字段都会自动序列化。All dialogs, and their fields, were auto-serialized in V3.

v4v4

// StepContext values are auto-serialized in V4, and scoped to the dialog.
stepContext.values.destination = destination;

将状态更改写入持久性层To write changes in state to the persistence layer

v3v3

默认情况下,状态数据在轮次结束时会自动保存。State data is auto-saved by default at the end of the turn.

v4v4

// You now must explicitly save state changes before the end of the turn.
await this.conversationState.saveChanges(context, false);
await this.userState.saveChanges(context, false);

创建并注册状态存储To create and register state storage

v3v3

// Autofac was used internally by the sdk, and state was automatic.
Conversation.UpdateContainer(
builder =>
{
    builder.RegisterModule(new AzureModule(Assembly.GetExecutingAssembly()));
    var store = new InMemoryDataStore();
    builder.Register(c => store)
        .Keyed<IBotDataStore<BotData>>(AzureModule.Key_DataStore)
        .AsSelf()
        .SingleInstance();
});

v4v4

// Create the storage we'll be using for User and Conversation state.
// In-memory storage is great for testing purposes.
services.AddSingleton<IStorage, MemoryStorage>();

// Create the user state (used in this bot's Dialog implementation).
services.AddSingleton<UserState>();

// Create the conversation state (used by the Dialog system itself).
services.AddSingleton<ConversationState>();

// The dialog that will be run by the bot.
services.AddSingleton<MainDialog>();

// Create the bot as a transient. In this case the ASP.NET controller is expecting an IBot.
services.AddTransient<IBot, DialogBot>();

// In the bot's ActivityHandler implementation, call SaveChangesAsync after the OnTurnAsync completes.
public class DialogBot : ActivityHandler
{
    protected readonly Dialog Dialog;
    protected readonly BotState ConversationState;
    protected readonly BotState UserState;

    public DialogBot(ConversationState conversationState, UserState userState, Dialog dialog)
    {
        ConversationState = conversationState;
        UserState = userState;
    }

    public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
    {
        await base.OnTurnAsync(turnContext, cancellationToken);

        // Save any state changes that might have occurred during the turn.
        await ConversationState.SaveChangesAsync(turnContext, false, cancellationToken);
        await UserState.SaveChangesAsync(turnContext, false, cancellationToken);
    }

    protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
    {
        // Run the dialog, passing in the message activity for this turn.
        await Dialog.Run(turnContext, ConversationState.CreateProperty<DialogState>("DialogState"), cancellationToken);
    }
}

捕获对话引发的错误To catch an error thrown from a dialog

v3v3

// Create a custom IPostToBot implementation to catch exceptions.
public sealed class CustomPostUnhandledExceptionToUser : IPostToBot
{
    private readonly IPostToBot inner;
    private readonly IBotToUser botToUser;
    private readonly ResourceManager resources;
    private readonly System.Diagnostics.TraceListener trace;

    public CustomPostUnhandledExceptionToUser(IPostToBot inner, IBotToUser botToUser, ResourceManager resources, System.Diagnostics.TraceListener trace)
    {
        SetField.NotNull(out this.inner, nameof(inner), inner);
        SetField.NotNull(out this.botToUser, nameof(botToUser), botToUser);
        SetField.NotNull(out this.resources, nameof(resources), resources);
        SetField.NotNull(out this.trace, nameof(trace), trace);
    }

    async Task IPostToBot.PostAsync(IActivity activity, CancellationToken token)
    {
        try
        {
            await this.inner.PostAsync(activity, token);
        }
        catch (Exception ex)
        {
            try
            {
                // Log exception and send custom error message here.
                await this.botToUser.PostAsync("custom error message");
            }
            catch (Exception inner)
            {
                this.trace.WriteLine(inner);
            }

            throw;
        }
    }
}

// Register this using AutoFac, replacing the default PostUnhandledExceptionToUser.
builder
  .RegisterType<CustomPostUnhandledExceptionToUser>()
  .Keyed<IPostToBot>(typeof(PostUnhandledExceptionToUser));

v4v4

// Provide an error handler in your implementation of the BotFrameworkHttpAdapter.
public class AdapterWithErrorHandler : BotFrameworkHttpAdapter
{
    public AdapterWithErrorHandler(
        ICredentialProvider credentialProvider,
        ILogger<BotFrameworkHttpAdapter> logger,
        ConversationState conversationState = null)
        : base(credentialProvider)
    {
        OnTurnError = async (turnContext, exception) =>
        {
            // Log any leaked exception from the application.
            logger.LogError($"Exception caught : {exception.Message}");

            // Send a catch-all apology to the user.
            await turnContext.SendActivityAsync("Sorry, it looks like something went wrong.");

            if (conversationState != null)
            {
                try
                {
                    // Delete the conversation state for the current conversation, to prevent the
                    // bot from getting stuck in a error-loop caused by being in a bad state.
                    // Conversation state is similar to "cookie-state" in a web page.
                    await conversationState.DeleteAsync(turnContext);
                }
                catch (Exception e)
                {
                    logger.LogError(
                        $"Exception caught on attempting to Delete ConversationState : {e.Message}");
                }
            }
        };
    }
}

处理不同的活动类型To process different activity types

v3v3

// Within your MessageController, check the message type.
string messageType = activity.GetActivityType();
if (messageType == ActivityTypes.Message)
{
    await Conversation.SendAsync(activity, () => new Dialogs.RootDialog());
}
else if (messageType == ActivityTypes.DeleteUserData)
{
}
else if (messageType == ActivityTypes.ConversationUpdate)
{
}
else if (messageType == ActivityTypes.ContactRelationUpdate)
{
}
else if (messageType == ActivityTypes.Typing)
{
}

v4v4

// In the bot's ActivityHandler implementation, override relevant methods.

protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
    // Handle message activities here.
}

protected override Task OnConversationUpdateActivityAsync(ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
    // Handle conversation update activities in general here.
}

protected override Task OnEventActivityAsync(ITurnContext<IEventActivity> turnContext, CancellationToken cancellationToken)
{
    // Handle event activities in general here.
}

记录所有活动To log all activities

v3v3

使用了 IActivityLoggerIActivityLogger was used.

builder.RegisterType<ActivityLoggerImplementation>().AsImplementedInterfaces().InstancePerDependency(); 

public class ActivityLoggerImplementation : IActivityLogger
{
    async Task IActivityLogger.LogAsync(IActivity activity)
    {
        // Store the activity.
    }
}

v4v4

使用 ITranscriptLoggerUse ITranscriptLogger.

var transcriptMiddleware = new TranscriptLoggerMiddleware(new TranscriptLoggerImplementation(Configuration.GetSection("StorageConnectionString").Value));
adapter.Use(transcriptMiddleware);

public class TranscriptLoggerImplementation : ITranscriptLogger
{
    async Task ITranscriptLogger.LogActivityAsync(IActivity activity)
    {
        // Store the activity.
    }
}

添加机器人状态存储To add bot state storage

用于存储 用户数据聊天数据专用聊天数据 的接口已更改。The interface for storing user data, conversation data, and private conversation data has changed.

v3v3

状态是使用 IBotDataStore 实现并通过使用 Autofac 将其注入到 SDK 的对话状态系统保存的。State was persisted using an IBotDataStore implementation, and injecting it into the dialog state system of the SDK using Autofac. Microsoft 在 Microsoft.Bot.Builder.Azure 中提供了 MemoryStorageDocumentDbBotDataStoreTableBotDataStoreSqlBotDataStore 类。Microsoft provided MemoryStorage, DocumentDbBotDataStore, TableBotDataStore, and SqlBotDataStore classes in Microsoft.Bot.Builder.Azure.

IBotDataStore 用于保存数据。IBotDataStore was used to persist data.

Task<bool> FlushAsync(IAddress key, CancellationToken cancellationToken);
Task<T> LoadAsync(IAddress key, BotStoreType botStoreType, CancellationToken cancellationToken);
Task SaveAsync(IAddress key, BotStoreType botStoreType, T data, CancellationToken cancellationToken);
var dbPath = ConfigurationManager.AppSettings["DocDbPath"];
var dbKey = ConfigurationManager.AppSettings["DocDbKey"];
var docDbUri = new Uri(dbPath);
var storage = new DocumentDbBotDataStore(docDbUri, dbKey);
builder.Register(c => storage)
                .Keyed<IBotDataStore<BotData>>(AzureModule.Key_DataStore)
                .AsSelf()
                .SingleInstance();

v4v4

在为机器人创建每个状态管理对象(如 UserStateConversationStatePrivateConversationState)时,存储层使用 IStorage 接口,指定存储层对象。The storage layer uses the IStorage interface, specify the storage-layer object when creating each state-management object for your bot, such as UserState, ConversationState, or PrivateConversationState. 状态管理对象向基础存储层提供密钥,并且还充当属性管理器。The state-management object provides keys to the underlying storage layer and also acts as a property manager. 例如,使用 IPropertyManager.CreateProperty<T>(string name) 可创建状态属性访问器。For instance, use IPropertyManager.CreateProperty<T>(string name) to create a state property accessor. 这些属性访问器用于从机器人的基础存储中检索值以及将值存储到其中。These property accessors are used to retrieve and store values into and out of the bot's underlying storage.

使用 IStorage 可保存数据。Use IStorage to persist data.

Task DeleteAsync(string[] keys, CancellationToken cancellationToken = default(CancellationToken));
Task<IDictionary<string, object>> ReadAsync(string[] keys, CancellationToken cancellationToken = default(CancellationToken));
Task WriteAsync(IDictionary<string, object> changes, CancellationToken cancellationToken = default(CancellationToken));
var storageOptions = new CosmosDbPartitionedStorageOptions()
{
    AuthKey = configuration["cosmosKey"],
    ContainerId = configuration["cosmosContainer"],
    CosmosDbEndpoint = configuration["cosmosPath"],
    DatabaseId = configuration["cosmosDatabase"]
};

IStorage dataStore = new CosmosDbPartitionedStorage(storageOptions);
var conversationState = new ConversationState(dataStore);
services.AddSingleton(conversationState);

备注

使用 CosmosDbPartitionedStorage 时,你要负责创建数据库并提供 Cosmos DB 终结点、授权密钥和数据库 ID,如上所示。When using CosmosDbPartitionedStorage, you are responsible for creating a database and providing the Cosmos DB endpoint, authorization key and database ID as show above. 只需指定容器的 ID 即可 - 机器人将自动创建容器,并确保正确配置该容器,使之能够存储机器人状态。You should simply specify an ID for a container - your bot will create it for you, ensuring it is configured correctly for storing bot state. 如果你确实想要自行创建容器,请确保将分区键设置为 /id 并设置 CosmosDbPartitionedStorageOptions.ContainerId 属性。If you do create the container yourself, ensure that the partition key is set to /id and set the CosmosDbPartitionedStorageOptions.ContainerId property.

使用 Form FlowTo use Form Flow

v3v3

Microsoft.Bot.Builder.FormFlow 已包括在核心 Bot Builder SDK 内。Microsoft.Bot.Builder.FormFlow was included within the core Bot Builder SDK.

v4v4

Bot.Builder.Community.Dialogs.FormFlow 现在是一个 Bot Builder Community 库。Bot.Builder.Community.Dialogs.FormFlow is now a Bot Builder Community library. 源可在社区存储库中找到。The source is available on the community repository.

使用 LuisDialogTo use LuisDialog

v3v3

Microsoft.Bot.Builder.Dialogs.LuisDialog 已包括在核心 Bot Builder SDK 内。Microsoft.Bot.Builder.Dialogs.LuisDialog was included within the core Bot Builder SDK.

v4v4

Bot.Builder.Community.Dialogs.Luis 现在是一个 Bot Builder Community 库。Bot.Builder.Community.Dialogs.Luis is now a Bot Builder Community library. 源可在社区存储库中找到。The source is available on the community repository.

使用 QnA MakerTo use QnA Maker

v3v3

[Serializable]
[QnAMaker("QnAEndpointKey", "QnAKnowledgebaseId", <ScoreThreshold>, <TotalResults>, "QnAEndpointHostName")]
public class SimpleQnADialog : QnAMakerDialog
{
}

v4v4

public class QnABot : ActivityHandler
{
  private readonly IConfiguration _configuration;
  private readonly ILogger<QnABot> _logger;
  private readonly IHttpClientFactory _httpClientFactory;

  public QnABot(IConfiguration configuration, ILogger<QnABot> logger, IHttpClientFactory httpClientFactory)
  {
    _configuration = configuration;
    _logger = logger;
    _httpClientFactory = httpClientFactory;
  }

  protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
  {
    var httpClient = _httpClientFactory.CreateClient();

    var qnaMaker = new QnAMaker(new QnAMakerEndpoint
    {
      KnowledgeBaseId = _configuration["QnAKnowledgebaseId"],
      EndpointKey = _configuration["QnAEndpointKey"],
      Host = _configuration["QnAEndpointHostName"]
    },
    null,
    httpClient);

    _logger.LogInformation("Calling QnA Maker");

    // The actual call to the QnA Maker service.
    var response = await qnaMaker.GetAnswersAsync(turnContext);
    if (response != null && response.Length > 0)
    {
      await turnContext.SendActivityAsync(MessageFactory.Text(response[0].Answer), cancellationToken);
    }
    else
    {
      await turnContext.SendActivityAsync(MessageFactory.Text("No QnA Maker answers were found."), cancellationToken);
    }
  }
}