Getting Started Guide

Creating a New Project

This getting started guide will show you how to create a custom AspNetCore web application and use the agent framework to create connections and send basic messages. We are going to start from scratch. All you need to have installed in Visual Studio and the .NET Core SDK.

Prerequisites

If you haven’t already done so, install Visual Studio community and the .NET Core 2.2.300 SDK

You should be running an instance of Windows or MacOS for this particular demo.


Create an AspNetCore Project

Open Visual Studio and select new project, then choose Web Application (Model-View-Controller):

_images/choose_template.png

Select the .NET Core 2.2, then name your project WebAgent.

_images/configure_agent.png

Installing the Required Packages

Use one of the three methods below to load the AgentFramework.Core packages into your project using nuget.

Package Manager CLI:

Install-Package AgentFramework.Core -Source https://www.myget.org/F/agent-framework/api/v3/index.json -v 4.0.0-preview.662

.NET CLI:

dotnet add package AgentFramework.Core -s https://www.myget.org/F/agent-framework/api/v3/index.json -v 4.0.0-preview.662

NOTE: For these first two CLI options, make sure to install AgentFramework.AspNetCore and AgentFramework.Core.Handlers by replacing the package name above with these two names as well.

Visual Studio Nuget Package Manager:

  • click on your new project WebAgent in the side bar
  • go to the project tab in your menu, select add nuget packages...
  • click on the dropdown that says nuget.org, and select configure sources...
  • click add, and then name the new source Agent Framework Beta and paste the url https://www.myget.org/F/agent-framework/api/v3/index.json into the location field.
  • check the show pre-release packages box on the bottom left
  • choose the Agent Framework Beta source from the dropdown
  • select the AgentFramework.AspNetCore package from the list. Make sure it is on a 4.0.0 version
  • click add.

We will also use one other package from Nuget called Jdenticon. Add that from the nuget.org repository list.


Installing the libindy SDK on your computer

Windows

You can download binaries of libindy and all dependencies from the Sovrin repo. The dependencies are under deps folder and libindy under one of streams (rc, master, stable). There are two options to link the DLLs

  • Unzip all files in a directory and add that to your PATH variable (recommended for development)
  • Or copy all DLL files in the publish directory (recommended for published deployments)

More details at the Indy documentation for setting up Windows environment.

MacOS

Check Setup Indy SDK build environment for MacOS.

Copy libindy.a and libindy.dylib to the /usr/local/lib/ directory.


Configuring your own Agent

In this section, we’ll walk through some sections of the WebAgent sample to understand how the AgentFramework can be used in a running application.

The WebAgent sample was created using the same steps as listed above. It may help to follow along to the steps below in your own new project. However, you may want to also open and try to run the fully working WebAgent sample in Visual Studio first to see if all the dependancies are working as they should. When the project was created, Startup.cs and Program.cs files were built using a template. These control how your webserver starts. We will need to edit them to use the agent framework.


Startup.cs

Our first goal is to edit the Startup file. Copy and paste the below code into your Startup.cs file:

Startup.cs (click to show/hide)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
using System;
using AgentFramework.AspNetCore;
using AgentFramework.Core.Models.Wallets;
using Jdenticon.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using WebAgent.Messages;
using WebAgent.Protocols.BasicMessage;
using WebAgent.Utils;

namespace WebAgent
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddLogging();

            // Register agent framework dependency services and handlers
            services.AddAgentFramework(builder =>
            {
                builder.AddBasicAgent<SimpleWebAgent>(c =>
                {
                    c.OwnerName = Environment.GetEnvironmentVariable("AGENT_NAME") ?? NameGenerator.GetRandomName();
                    c.EndpointUri = new Uri(Environment.GetEnvironmentVariable("ENDPOINT_HOST") ?? Environment.GetEnvironmentVariable("ASPNETCORE_URLS"));
                    c.WalletConfiguration = new WalletConfiguration { Id = "WebAgentWallet" };
                    c.WalletCredentials = new WalletCredentials { Key = "MyWalletKey" };
                });
            });

            // Register custom handlers with DI pipeline
            services.AddSingleton<BasicMessageHandler>();
            services.AddSingleton<TrustPingMessageHandler>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseBrowserLink();
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();

            // Register agent middleware
            app.UseAgentFramework();

            // fun identicons
            app.UseJdenticon();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

In this file, we congigure and add the Agent Framework to the project. If you are building the project from scratch, make sure to comment out lines 44-45 until you have created these services. Line 72 specifies how the API to trigger actions should be called. ————-

Program.cs

Next, we will edit the Program.cs file. Copy and paste this code too:

Program.cs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;

namespace WebAgent
{
    public class Program
    {
        public static void Main(string[] args)
        {
            BuildWebHost(args).Run();
        }

        public static IWebHost BuildWebHost(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseKestrel()
                .UseStartup<Startup>()
                .Build();
    }
}

Once you have finished with this code, take a moment to look over the changes that we’ve made.


SimpleWebAgent.cs

Now create a file name SimpleWebAgent.cs in the main directory

This file will inherit from the AgentBase class in the AgentFramework, and it extends the IAgent Interface. This interface includes only one function named Task<MessageResponse>ProcessAsync(IAgentContext context, MessageContext messageContext) This will process any message that is sent to the agent’s endpoint.

Copy and paste the below code into the file:

SimpleWebAgent.cs (Click to show)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System;
using AgentFramework.Core.Handlers;
using WebAgent.Messages;
using WebAgent.Protocols.BasicMessage;

namespace WebAgent
{
    public class SimpleWebAgent : AgentBase
    {
        public SimpleWebAgent(IServiceProvider serviceProvider)
            : base(serviceProvider)
        {
        }

        protected override void ConfigureHandlers()
        {
            AddConnectionHandler();
            AddForwardHandler();
            AddHandler<BasicMessageHandler>();
            AddHandler<TrustPingMessageHandler>();
        }
    }
}

bundleconfig.json

Create a bundleconfig.json file in your project root directory, and paste this json array into to it:

bundleconfig.json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Configure bundling and minification for the project.
// More info at https://go.microsoft.com/fwlink/?LinkId=808241
[
  {
    "outputFileName": "wwwroot/css/site.min.css",
    // An array of relative input file paths. Globbing patterns supported
    "inputFiles": [
      "wwwroot/css/site.css"
    ]
  },
  {
    "outputFileName": "wwwroot/js/site.min.js",
    "inputFiles": [
      "wwwroot/js/site.js"
    ],
    // Optionally specify minification options
    "minify": {
      "enabled": true,
      "renameLocals": true
    },
    // Optionally generate .map file
    "sourceMap": false
  }
]

launchSettings.json

Edit the Property/launchSettings.json

launchSettings.json
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "profiles": {
    "WebAgent": {
      "commandName": "Project",
      "launchBrowser": true,
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "applicationUrl": "http://localhost:5000/"
    }
  }
}

Finally, to get this program to run we will need to add a couple of utility files.

First, add a Utils folder. Add these two files to the Utils folder (Open these links in new tabs):

NameGenerator.cs Extensions.cs


Click run, you should see your template home page will appear in your web browser at http://localhost:5000. Congratulations! You’ve successully included the Agent framework into your project. Continue on to see how you might use it in your project.

WebAgent Walkthrough

We will learn how to create a web agent in this section. If you have created your own agent, you should be able to use that here. You may want to download the View files and import them into your project to save time on copy/paste: We’re assuming that you are already familiar with the high level concepts of Indy and Sovrin.

The first thing is to understand about Wallets. Read about wallets here: Insert link/paragraphs about wallet infra based on hipe: Wallet HIPE


Opening a wallet in Agent Framework

We open a wallet in Agent Framework by provisioning an agent. This process of provisioning agents will create and configure an agent wallet and initialize the agent configuration. The framework will generate a random Did and Verkey, unless you specify AgentSeed which is used if you need determinism. Length of seed must be 32 characters.

await _provisioningService.ProvisionAgentAsync(
    new ProvisioningConfiguration
    {
        EndpointUri = "http://localhost:5000",
        OwnerName = "My Agent"
    });

We provision this agent in the Startup.cs file by building it directly with the AgentBuilder

HomeController.cs (Show)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
using System;
using AgentFramework.AspNetCore;
using AgentFramework.Core.Models.Wallets;
using Jdenticon.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using WebAgent.Messages;
using WebAgent.Protocols.BasicMessage;
using WebAgent.Utils;

namespace WebAgent
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc();

            services.AddLogging();

            // Register agent framework dependency services and handlers
            services.AddAgentFramework(builder =>
            {
                builder.AddBasicAgent<SimpleWebAgent>(c =>
                {
                    c.OwnerName = Environment.GetEnvironmentVariable("AGENT_NAME") ?? NameGenerator.GetRandomName();
                    c.EndpointUri = new Uri(Environment.GetEnvironmentVariable("ENDPOINT_HOST") ?? Environment.GetEnvironmentVariable("ASPNETCORE_URLS"));
                    c.WalletConfiguration = new WalletConfiguration { Id = "WebAgentWallet" };
                    c.WalletCredentials = new WalletCredentials { Key = "MyWalletKey" };
                });
            });

            // Register custom handlers with DI pipeline
            services.AddSingleton<BasicMessageHandler>();
            services.AddSingleton<TrustPingMessageHandler>();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseBrowserLink();
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();

            // Register agent middleware
            app.UseAgentFramework();

            // fun identicons
            app.UseJdenticon();
            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }
    }
}

You now have a functioning wallet in your agent, ready to store all your secrets.


The Agent framework abstracts the main workflows of Indy into a state machine. For our agent, we will show how to create and receive invitations with other people. Although simple, it builds the foundation for all other potential agent communications like credentials and proofs.

TODO: Basic message and routing info

Connections

Every connection is a unique relationship with another agent. The Agent Framework represents this relationship with a ConnectionRecord, this entity describes the pairwise relationship with another party. The states for this record are:

  • Invited - initially, when creating invitations to connect, the record will be set to this state.
  • Negotating - set after accepting an invitation and sending a request to connect
  • Connected - set when both parties have acknowledged the connection and have a pairwise record of each others DID’s

For us to send basic messages back and forth, we will first need to establish that protocol. We will walk through how a message is processed in the WebAgent.

First, we need to define the basic message structure in the Protocols folder. These are all child classes of the AgentMessage class that is in the AgentFramework.

AgentMessage.cs (Show)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using AgentFramework.Core.Exceptions;
using AgentFramework.Core.Extensions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace AgentFramework.Core.Messages
{
    /// <summary>
    /// Represents an abstract base class of a content message.
    /// </summary>
    public abstract class AgentMessage
    {
        /// <summary>
        /// Internal JObject representation of an agent message.
        /// </summary>
        [JsonIgnore]
        private IList<JProperty> _decorators = new List<JProperty>();

        /// <summary>
        /// Gets or sets the message id.
        /// </summary>
        /// <value>
        /// The message id.
        /// </value>
        [JsonProperty("@id")]
        public string Id { get; set; }

        /// <summary>
        /// Gets or sets the type.
        /// </summary>
        /// <value>
        /// The type.
        /// </value>
        [JsonProperty("@type")]
        public string Type { get; set; }

        /// <summary>
        /// Gets the decorators on the message.
        /// </summary>
        /// <returns>The decorators as a JObject.</returns>
        public IReadOnlyList<JProperty> GetDecorators() => new ReadOnlyCollection<JProperty>(_decorators);

        /// <summary>
        /// Internal set method for setting the collection of decorators attached to the message.
        /// </summary>
        /// <param name="decorators">JObject of decorators attached to the message.</param>
        internal void SetDecorators(IList<JProperty> decorators) => _decorators = decorators;

        /// <summary>
        /// Gets the decorator.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="name">The name.</param>
        /// <returns></returns>
        /// <exception cref="AgentFrameworkException">ErrorCode.InvalidMessage Cannot deserialize packed message or unable to find decorator on message.</exception>
        public T GetDecorator<T>(string name) where T : class
        {
            try
            {
                var decorator = _decorators.First(_ => _.Name == $"~{name}");
                return decorator.Value.ToObject<T>();
            }
            catch (Exception e)
            {
                throw new AgentFrameworkException(ErrorCode.InvalidMessage, "Failed to extract decorator from message", e);
            }
        }

        /// <summary>
        /// Finds the decorator with the specified name or returns <code>null</code>.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="name">The name.</param>
        /// <returns>The requested decorator or null</returns>
        public T FindDecorator<T>(string name) where T : class
        {
            var decorator = _decorators.FirstOrDefault(_ => _.Name == $"~{name}");
            return decorator?.Value.ToObject<T>();
        }

        /// <summary>
        /// Adds the decorator.
        /// </summary>
        /// <param name="decorator">The decorator.</param>
        /// <param name="name">The decorator name.</param>
        public void AddDecorator<T>(T decorator, string name) where T : class =>
            _decorators.Add(new JProperty($"~{name}", JsonConvert.DeserializeObject<JToken>(decorator.ToJson())));

        /// <summary>
        /// Sets the decorator overriding any existing values
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="decorator">The decorator.</param>
        /// <param name="name">The name.</param>
        public void SetDecorator<T>(T decorator, string name) where T : class
        {
            _decorators.Remove(_decorators.FirstOrDefault(x => x.Name == $"~{name}"));
            AddDecorator(decorator, name);
        }
    }
}

The BasicMessage uses the AgentMessage attributes as shown in the class above but adds “content” and “sent_time” attributes as well. This will allow something like text messages to be sent between agents.

BasicMessage.cs (Show)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
using System;
using AgentFramework.Core.Messages;
using WebAgent.Messages;
using Newtonsoft.Json;

namespace WebAgent.Protocols.BasicMessage
{
    public class BasicMessage : AgentMessage
    {
        public BasicMessage()
        {
            Id = Guid.NewGuid().ToString();
            Type = CustomMessageTypes.BasicMessageType;
        }
        
        [JsonProperty("content")]
        public string Content { get; set; }

        [JsonProperty("sent_time")]
        public string SentTime { get; set; }
    }
}

When your agent receives a message, it gets put directly into the wallet by the basic message handler.

BasicMessageHandler.cs (Show)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using System;
using System.Threading.Tasks;
using AgentFramework.Core.Contracts;
using AgentFramework.Core.Handlers;
using AgentFramework.Core.Messages;

namespace WebAgent.Protocols.BasicMessage
{
    public class BasicMessageHandler : MessageHandlerBase<BasicMessage>
    {
        private readonly IWalletRecordService _recordService;

        public BasicMessageHandler(IWalletRecordService recordService)
        {
            _recordService = recordService;
        }

        protected override async Task<AgentMessage> ProcessAsync(BasicMessage message, IAgentContext agentContext, MessageContext messageContext)
        {
            Console.WriteLine($"Processing message by {messageContext.Connection.Id}");

            await _recordService.AddAsync(agentContext.Wallet, new BasicMessageRecord
            {
                Id = Guid.NewGuid().ToString(),
                ConnectionId = messageContext.Connection.Id,
                Text = message.Content,
                SentTime = DateTime.TryParse(message.SentTime, out var dateTime) ? dateTime : DateTime.UtcNow,
                Direction = MessageDirection.Incoming
            });

            return null;
        }
    }
}

Then when the view is reloaded, the Controller class takes over.

This is following the MVC pattern of ASPNetCore.

Here is the ConnectionsController class that will show a BasicMessage if one is sent or received:

ConnectionsController.cs (Show)
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
using System;
using System.Globalization;
using System.Reactive.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using AgentFramework.Core.Contracts;
using AgentFramework.Core.Extensions;
using AgentFramework.Core.Handlers;
using AgentFramework.Core.Handlers.Agents;
using AgentFramework.Core.Messages.Connections;
using AgentFramework.Core.Models;
using AgentFramework.Core.Models.Connections;
using AgentFramework.Core.Models.Events;
using AgentFramework.Core.Models.Records.Search;
using AgentFramework.Core.Utils;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using WebAgent.Messages;
using WebAgent.Models;
using WebAgent.Protocols.BasicMessage;

namespace WebAgent.Controllers
{
    public class ConnectionsController : Controller
    {
        private readonly IEventAggregator _eventAggregator;
        private readonly IConnectionService _connectionService;
        private readonly IWalletService _walletService;
        private readonly IWalletRecordService _recordService;
        private readonly IProvisioningService _provisioningService;
        private readonly IAgentProvider _agentContextProvider;
        private readonly IMessageService _messageService;
        private readonly WalletOptions _walletOptions;

        public ConnectionsController(
            IEventAggregator eventAggregator,
            IConnectionService connectionService, 
            IWalletService walletService, 
            IWalletRecordService recordService,
            IProvisioningService provisioningService,
            IAgentProvider agentContextProvider,
            IMessageService messageService,
            IOptions<WalletOptions> walletOptions)
        {
            _eventAggregator = eventAggregator;
            _connectionService = connectionService;
            _walletService = walletService;
            _recordService = recordService;
            _provisioningService = provisioningService;
            _agentContextProvider = agentContextProvider;
            _messageService = messageService;
            _walletOptions = walletOptions.Value;
        }

        [HttpGet]
        public async Task<IActionResult> Index()
        {
            var context = await _agentContextProvider.GetContextAsync();

            return View(new ConnectionsViewModel
            {
                Connections = await _connectionService.ListAsync(context)
            });
        }

        [HttpGet]
        public async Task<IActionResult> CreateInvitation()
        {
            var context = await _agentContextProvider.GetContextAsync();

            var (invitation, _) = await _connectionService.CreateInvitationAsync(context, new InviteConfiguration { AutoAcceptConnection = true });
            ViewData["Invitation"] = $"{(await _provisioningService.GetProvisioningAsync(context.Wallet)).Endpoint.Uri}?c_i={EncodeInvitation(invitation)}";
            return View();
        }

        [HttpPost]
        public async Task<IActionResult> AcceptInvitation(AcceptConnectionViewModel model)
        {
            var context = await _agentContextProvider.GetContextAsync();

            var invite = MessageUtils.DecodeMessageFromUrlFormat<ConnectionInvitationMessage>(model.InvitationDetails);
            var (request, record) = await _connectionService.CreateRequestAsync(context, invite);
            await _messageService.SendAsync(context.Wallet, request, record, invite.RecipientKeys[0]);

            return RedirectToAction("Index");
        }

        [HttpPost]
        public IActionResult ViewInvitation(AcceptConnectionViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return Redirect(Request.Headers["Referer"].ToString());
            }

            ViewData["InvitationDetails"] = model.InvitationDetails;

            var invite = MessageUtils.DecodeMessageFromUrlFormat<ConnectionInvitationMessage>(model.InvitationDetails);

            return View(invite);
        }

        [HttpPost]
        public async Task<IActionResult> SendTrustPing(string connectionId)
        {
            var context = await _agentContextProvider.GetContextAsync();
            var connection = await _connectionService.GetAsync(context, connectionId);
            var message = new TrustPingMessage
            {
                ResponseRequested = true,
                Comment = "Hello"
            };

            var slim = new SemaphoreSlim(0, 1);
            var success = false;

            using (var subscription = _eventAggregator.GetEventByType<ServiceMessageProcessingEvent>()
                .Where(_ => _.MessageType == CustomMessageTypes.TrustPingResponseMessageType)
                .Subscribe(_ => { success = true; slim.Release(); }))
            {
                await _messageService.SendAsync(context.Wallet, message, connection);

                await slim.WaitAsync(TimeSpan.FromSeconds(5));

                return RedirectToAction("Details", new { id = connectionId, trustPingSuccess = success });
            }
        }

        [HttpGet]
        public async Task<IActionResult> Details(string id, bool? trustPingSuccess = null)
        {
            var context = new AgentContext
            {
                Wallet = await _walletService.GetWalletAsync(_walletOptions.WalletConfiguration,
                    _walletOptions.WalletCredentials)
            };

            var model = new ConnectionDetailsViewModel
            {
                Connection = await _connectionService.GetAsync(context, id),
                Messages = await _recordService.SearchAsync<BasicMessageRecord>(context.Wallet,
                    SearchQuery.Equal(nameof(BasicMessageRecord.ConnectionId), id), null, 10),
                TrustPingSuccess = trustPingSuccess
            };

            return View(model);
        }

        [HttpPost]
        public async Task<IActionResult> SendMessage(string connectionId, string text)
        {
            if (string.IsNullOrEmpty(text)) return RedirectToAction("Details", new { id = connectionId });

            var context = new AgentContext
            {
                Wallet = await _walletService.GetWalletAsync(_walletOptions.WalletConfiguration,
                    _walletOptions.WalletCredentials)
            };

            var sentTime = DateTime.UtcNow;
            var messageRecord = new BasicMessageRecord
            {
                Id = Guid.NewGuid().ToString(),
                Direction = MessageDirection.Outgoing,
                Text = text,
                SentTime = sentTime,
                ConnectionId = connectionId
            };
            var message = new BasicMessage
            {
                Content = text,
                SentTime = sentTime.ToString("s", CultureInfo.InvariantCulture)
            };
            var connection = await _connectionService.GetAsync(context, connectionId);

            // Save the outgoing message to the local wallet for chat history purposes
            await _recordService.AddAsync(context.Wallet, messageRecord);

            // Send an agent message using the secure connection
            await _messageService.SendAsync(context.Wallet, message, connection);

            return RedirectToAction("Details", new {id = connectionId});
        }

        [HttpPost]
        public IActionResult LaunchApp(LaunchAppViewModel model)
        {
            return Redirect($"{model.UriSchema}{Uri.EscapeDataString(model.InvitationDetails)}");
        }

        /// <summary>
        /// Encodes the invitation to a base64 string which can be presented to the user as QR code or a deep link Url
        /// </summary>
        /// <returns>The invitation.</returns>
        /// <param name="invitation">Invitation.</param>
        public string EncodeInvitation(ConnectionInvitationMessage invitation)
        {
            return invitation.ToJson().ToBase64();
        }

        /// <summary>
        /// Decodes the invitation from base64 to strongly typed object
        /// </summary>
        /// <returns>The invitation.</returns>
        /// <param name="invitation">Invitation.</param>
        public ConnectionInvitationMessage DecodeInvitation(string invitation)
        {
            return JsonConvert.DeserializeObject<ConnectionInvitationMessage>(Encoding.UTF8.GetString(Convert.FromBase64String(invitation)));
        }
    }
}

Read through the rest of the ConnectionsController class to understand how the Agent Framework can be used to implement the other website features.