{"id":98,"date":"2014-07-19T04:11:17","date_gmt":"2014-07-19T04:11:17","guid":{"rendered":"http:\/\/looselycoupledlabs.com\/?p=98"},"modified":"2014-07-19T22:26:05","modified_gmt":"2014-07-19T22:26:05","slug":"a-real-world-masstransit-customer-portal-example","status":"publish","type":"post","link":"https:\/\/looselycoupledlabs.com\/2014\/07\/a-real-world-masstransit-customer-portal-example\/","title":{"rendered":"A Real-World MassTransit Customer Portal Example"},"content":{"rendered":"

Now that we\u2019ve seen some<\/a> simple examples<\/a> of how to use MassTransit<\/a> with the Publish\/Subscribe pattern on multiple machines, let\u2019s build something that resembles a more real-world app. In this article, we\u2019ll build an ASP.NET MVC Customer Portal app where a customer can create a new support ticket. The ticket will be published onto the service bus. We\u2019ll create a Windows Service to be the subscriber of these messages and it will handle the tickets, in this example, sending a confirmation email to the customer.<\/p>\n

You can get all the code from this blog post at https:\/\/github.com\/dprothero\/Loosely.CustomerPortal<\/a>.<\/p>\n

This is a big one, so roll up your sleeves\u2026<\/p>\n

The Web App<\/h1>\n

Let\u2019s build a rudimentary front-end application that will be a stand-in for a true customer portal style web site. We\u2019ll build an app that has a single menu option on it\u2019s home page:<\/p>\n

\"image\"<\/a><\/p>\n

When the user clicks \u201cOpen a new support ticket,\u201d they will get a very simple form asking for their email address and a description of their problem or question:<\/p>\n

\"image\"<\/a><\/p>\n

When the user clicks \u201cOpen Ticket,\u201d they will see a confirmation message containing their ticket number:<\/p>\n

\"image\"<\/a><\/p>\n

So let\u2019s dig into the code to build this web app.<\/p>\n

New ASP.NET MVC Project<\/h2>\n

Open Visual Studio and choose File\u2026 New Project. Select the \u201cASP.NET Web Application\u201d project template. Give it the name \u201cLoosely.CustomerPortal.WebApp\u201d and name the solution \u201cLoosely.CustomerPortal.\u201d<\/p>\n

In the \u201cNew ASP.NET Project\u201d select the \u201cEmpty\u201d template and check the \u201cMVC\u201d box under the \u201cAdd folders and core references for” heading.<\/p>\n

Contracts<\/h2>\n

Now we need a place to keep our \u201ccontracts\u201d for our service bus. A contract is an interface that specifies the format of our message type. Add a new class library to the solution and name it \u201cLoosely.Bus.Contracts.\u201d Add a new file to the class library called TicketOpened and define the following interface:<\/p>\n

namespace Loosely.Bus.Contracts\r\n{\r\n  public interface TicketOpened\r\n  {\r\n    string Id { get; }\r\n    string CustomerEmail { get; set; }\r\n    string Message { get; set; }\r\n  }\r\n}\r\n<\/pre>\n

This is the message type we will publish onto the service bus whenever a user of the web application wants to open a new support ticket.<\/p>\n

Configuration<\/h2>\n

Now let\u2019s add another class library named \u201cLoosely.Bus.Configuration\u201d where we\u2019ll keep our common MassTransit configuration code. Add the MassTransit.Log4Net and MassTransit.RabbitMQ NuGet packages to this new class library.<\/p>\n

We\u2019ll put the common service bus initialization code into a class called BusInitializer:<\/p>\n

using MassTransit;\r\nusing MassTransit.BusConfigurators;\r\nusing MassTransit.Log4NetIntegration.Logging;\r\nusing System;\r\n\r\nnamespace Loosely.Bus.Configuration\r\n{\r\n  public class BusInitializer\r\n  {\r\n    public static IServiceBus CreateBus(string queueName, Action<ServiceBusConfigurator> moreInitialization)\r\n    {\r\n      Log4NetLogger.Use();\r\n      var bus = ServiceBusFactory.New(x =>\r\n      {\r\n        x.UseRabbitMq();\r\n        x.ReceiveFrom(\"rabbitmq:\/\/localhost\/Loosely_\" + queueName);\r\n        moreInitialization(x);\r\n      });\r\n\r\n      return bus;\r\n    }\r\n  }\r\n}\r\n<\/pre>\n

You may recall, in a previous post<\/a>, when we did the same thing. In fact, this code is nearly identical. The only thing we\u2019ve changed is the prefix for the queue name. In that earlier post<\/a> I describe what we\u2019re doing here in detail. In summary, we\u2019re setting up a new instance of a MassTransit service bus that will use RabbitMQ for it\u2019s transport mechanism.<\/p>\n

Put ASP.NET on the Bus<\/h2>\n

Returning to the Loosely.CustomerPortal.WebApp in our solution, right-click on References and add project references to the Loosely.Bus.Configuration and Loosely.Bus.Contracts projects. Also, add the MassTransit NuGet package to the project.<\/p>\n

The best place I\u2019ve found to create and configure MassTransit in an ASP.NET app is in the Global.asax\u2019s Application_Start event handler. Open Global.asax.cs and make sure the code looks like this:<\/p>\n

using Loosely.Bus.Configuration;\r\nusing MassTransit;\r\nusing System.Web.Mvc;\r\nusing System.Web.Routing;\r\n\r\nnamespace Loosely.CustomerPortal.WebApp\r\n{\r\n  public class MvcApplication : System.Web.HttpApplication\r\n  {\r\n    public static IServiceBus Bus {get; set;}\r\n\r\n    protected void Application_Start()\r\n    {\r\n      AreaRegistration.RegisterAllAreas();\r\n      RouteConfig.RegisterRoutes(RouteTable.Routes);\r\n\r\n      Bus = BusInitializer.CreateBus(\"CustomerPortal_WebApp\", x => { });\r\n    }\r\n\r\n    protected void Application_End()\r\n    {\r\n      Bus.Dispose();\r\n    }\r\n  }\r\n}\r\n<\/pre>\n

We\u2019re adding a public, static property to the MvcApplication class that we can use elsewhere in our application to get access to our service bus. In the Application_Start event handler, after the routing code added by Visual Studio, we can use our BusInitializer class to create a new bus and assign it to the static property. Later, when we want to use the bus, we\u2019ll simply use the expression MvcApplication.Bus.<\/p>\n

Don\u2019t forget to call Dispose on the bus in the Application_End event.<\/p>\n

Ticket Model<\/h2>\n

Still within the WebApp project, add a new model to the Models folder and call it \u201cTicket.\u201d This will be the model we will bind our support ticket data entry form to. We\u2019ll also have it implement the TicketOpened interface so we can publish it to our service bus.<\/p>\n

using Loosely.Bus.Contracts;\r\nusing System;\r\n\r\nnamespace Loosely.CustomerPortal.WebApp.Models\r\n{\r\n  public class Ticket : TicketOpened\r\n  {\r\n    private string _id;\r\n    \r\n    public string Id { get { return _id; } }\r\n    public string CustomerEmail { get; set; }\r\n    public string Message { get; set; }\r\n\r\n    public Ticket()\r\n    {\r\n      _id = Guid.NewGuid().ToString();\r\n    }\r\n\r\n    public void Save()\r\n    {\r\n      MvcApplication.Bus.Publish<TicketOpened>(this, x => { \r\n        x.SetDeliveryMode(MassTransit.DeliveryMode.Persistent);\r\n      });\r\n    }\r\n  }\r\n}\r\n<\/pre>\n

Because we want to be able to tell the user what their ticket ID is right away, we need some method to generate a statistically unique, random ID. GUIDs work well for this in terms of being easy to implement for a developer, as it\u2019s a single line of code. They aren\u2019t a great user experience, of course, due their length.<\/p>\n

Timeout to Pontificate<\/h3>\n

The point to remember is that we need a way to generate an identifier that we know should be unique in whatever data storage repository we will be storing the tickets in without<\/em> having to consult said data storage repository. Remember, the reason we\u2019re using a service bus is to have this web application loosely coupled to whatever backend is used for our ticketing system.<\/p>\n

When the rubber meets the road, however, you may be integrating with a ticketing system that wants to assign it\u2019s own IDs. In that case, you will have to decide whether the requirement to display the ticket ID immediately to the user is worth a round-trip to the ticketing system to get it. There\u2019s no one right answer. Building systems is a constant series of trade-offs.<\/p>\n

Back to the Model\u2026<\/h3>\n

We also have a Save method that we will call from our controller (coming soon). Instead of what you typically see in a Save method (saving to a database), we\u2019re publishing the Ticket onto the bus. Since the ticket implements the TicketOpened interface from our Contracts assembly, other processes can subscribe to these TicketOpened messages and do something interesting with them.<\/p>\n

The Controllers and Views<\/h2>\n

Now let\u2019s build some UI. Add an empty controller called \u201cHomeController\u201d to the \u201cControllers\u201d folder. Nothing much is needed in this controller \u2013 just return a view that will have our \u201cmenu\u201d of options (a menu of one option, that is):<\/p>\n

using System.Web.Mvc;\r\n\r\nnamespace Loosely.CustomerPortal.WebApp.Controllers\r\n{\r\n  public class HomeController : Controller\r\n  {\r\n    \/\/ GET: Home\r\n    public ActionResult Index()\r\n    {\r\n      return View();\r\n    }\r\n  }\r\n}\r\n<\/pre>\n

Create a view named Index under the Views\/Home folder, leaving the \u201cUse a layout page\u201d option checked. The layout page will give us a basic page template so we don\u2019t have to worry about formatting too much. The view can then simply present our single menu option:<\/p>\n

@{\r\n    ViewBag.Title = \"Index\";\r\n}\r\n\r\n<h2>Customer Portal<\/h2>\r\n\r\n<a href=\"@Url.Content(\"~\/Ticket\/Open\")\">Open a new support ticket<\/a>\r\n<\/pre>\n

As you can see, the link to open a new support ticket is taking us to \/Ticket\/Open, which means we need a TicketController with an Open action. Add this controller to the Controllers folder (choose empty again). Below is the code for this controller:<\/p>\n

using System.Web.Mvc;\r\n\r\nnamespace Loosely.CustomerPortal.WebApp.Controllers\r\n{\r\n  public class TicketController : Controller\r\n  {\r\n    [HttpGet]\r\n    public ActionResult Open()\r\n    {\r\n      var ticket = new Models.Ticket();\r\n      return View(ticket);\r\n    }\r\n\r\n    [HttpPost]\r\n    public ActionResult Open(Models.Ticket ticket)\r\n    {\r\n      ticket.Save();\r\n      return Redirect(\"~\/Ticket\/Opened\/\" + ticket.Id);\r\n    }\r\n\r\n    public ActionResult Opened(string id)\r\n    {\r\n      ViewBag.TicketId = id;\r\n      return View();\r\n    }\r\n  }\r\n}\r\n<\/pre>\n

A few interesting things are going on now. First, we\u2019ve got the Open action that\u2019s tagged with the HttpGet attribute. This action simply creates a new ticket model and binds it to the (soon to be created) view. This view will be the data entry form allowing the user to supply their email and message text.<\/p>\n

The next method is also called Open but is tagged with the HttpPost attribute. This is because when the user submits the form we will still be posting to the \/Ticket\/Open url (sorry if this is review for you MVC vets). The post action takes in a ticket model that should be populated with the data from the user\u2019s form submission.<\/p>\n

We take the ticket the user submits and call the Save method on it (which, as you\u2019ll recall, is what will post the message to the service bus). Following the save, we redirect to the Opened action, passing the ticket ID.<\/p>\n

Finally, we have the Opened action which passes the ticket ID into the view via the ViewBag so it can be displayed to the user.<\/p>\n

Ticket Views<\/h3>\n

We need a couple views for the Ticket controller. Under the Views\/Ticket folder, create an empty view named Open and be sure to select \u201cTicket (Loosely.CustomerPortal.WebApp.Models)\u201d for the model class. This view will contain our data input form that will be bound to the Ticket model:<\/p>\n

@model Loosely.CustomerPortal.WebApp.Models.Ticket\r\n\r\n@{\r\n    ViewBag.Title = \"Open\";\r\n}\r\n\r\n<h2>Open a Ticket<\/h2>\r\n\r\n@using ( Html.BeginForm() )\r\n{\r\n  <fieldset>\r\n    <legend>Ticket Info<\/legend>\r\n    <div>@Html.LabelFor(model => model.CustomerEmail)<\/div>\r\n    <div>@Html.TextBoxFor(model => model.CustomerEmail)<\/div>\r\n\r\n    <div>@Html.LabelFor(model => model.Message)<\/div>\r\n    <div>@Html.TextBoxFor(model => model.Message)<\/div>\r\n\r\n    <input type=\"submit\" value=\"Open Ticket\" \/>\r\n\r\n  <\/fieldset>\r\n}\r\n<\/pre>\n

Add another view named Opened under the same Views\/Ticket folder. This view will simply display the ticket ID:<\/p>\n

@{\r\n    ViewBag.Title = \"Opened\";\r\n}\r\n\r\n<h2>Ticket Opened<\/h2>\r\n\r\n<p>\r\n  Your ticket has been opened.\r\n  Your ticket id is: <strong>@ViewBag.TicketId<\/strong>\r\n<\/p>\r\n\r\n<p>\r\n  <a href=\"@Url.Content(\"~\/\")\">Return Home<\/a>\r\n<\/p>\r\n<\/pre>\n

Checkpoint \u2013 The Web App Works, Now What?<\/h2>\n

If you run the web app now, you\u2019ll be able to create a new ticket, and you\u2019ll even get a new ticket ID assigned each time! If you go into the RabbitMQ management interface (see this post<\/a> for instructions), you will see that there\u2019s an exchange named Loosely.Bus.Contracts:TicketOpened that isn\u2019t connected to any other exchanges or queues. If you\u2019ve been following my blog, you\u2019ll know this is because we don\u2019t have anyone listening for these types of messages yet.<\/p>\n

Creating the Backend Service<\/h1>\n

It\u2019s time to do something with these tickets that are being created by the web app. Let\u2019s create a Windows service that can run in the background on any machine and subscribe to TicketOpened messages that are published to the service bus. We\u2019ll use the open source project TopShelf<\/a> for creating our Windows service. TopShelf is published by the same trio of geniuses that gave us MassTransit and it makes creating Windows services extremely simple.<\/p>\n

Start by adding a new Console application to our solution and name it Loosely.CustomerPortal.Backend. Add project references to Loosely.Bus.Configuration and Loosely.Bus.Contracts, as well as a framework reference to System.Configuration. Finally, add NuGet packages MassTransit and TopShelf to the project.<\/p>\n

Backend Configuration<\/h2>\n

First, let\u2019s setup a little configuration so that TopShelf will log any messages to a log file we can create. We\u2019ll also setup the configuration to log general Trace messages. To send email messages, we\u2019ll use Gmail\u2019s SMTP server, so we also need some place to store our Gmail credentials.<\/p>\n

All this configuration can go in the App.config file in the new console application:<\/p>\n

<?xml version=\"1.0\" encoding=\"utf-8\" ?>\r\n<configuration>\r\n  <startup>\r\n    <supportedRuntime version=\"v4.0\" sku=\".NETFramework,Version=v4.5\" \/>\r\n  <\/startup>\r\n  <system.diagnostics>\r\n    <sharedListeners>\r\n      <add name=\"traceLogListener\" type=\"System.Diagnostics.TextWriterTraceListener\" \r\n           initializeData=\"C:\\Logs\\Loosely.CustomerPortal.Backend.log\" \/>\r\n    <\/sharedListeners>\r\n    <sources>\r\n      <source name=\"Default\">\r\n        <listeners>\r\n          <add name=\"traceLogListener\" \/>\r\n          <remove name=\"Default\" \/>\r\n        <\/listeners>\r\n      <\/source>\r\n    <\/sources>\r\n    <trace autoflush=\"true\" indentsize=\"4\">\r\n      <listeners>\r\n        <add name=\"traceLogListener\" \/>\r\n        <remove name=\"Default\" \/>\r\n      <\/listeners>\r\n    <\/trace>\r\n  <\/system.diagnostics>\r\n\r\n  <appSettings file=\"C:\\Config\\Loosely.CustomerPortal.Backend.config\">\r\n    <add key=\"Gmail.Account\" value=\"youraccount@gmail.com\"\/>\r\n    <add key=\"Gmail.Password\" value=\"yourpassword\"\/>\r\n  <\/appSettings>\r\n  \r\n<\/configuration>\r\n<\/pre>\n

Notice we\u2019re logging messages to C:\\Logs\\Loosely.CustomerPortal.Backend.log, so be sure to create a C:\\Logs folder (or change this path to somewhere else you might prefer).<\/p>\n

Email Helper<\/h2>\n

When a new ticket is opened, we want to send a confirmation to the customer\u2019s email address that they supplied. Create a new EmailHelper class to handle the down and dirty SMTP communication:<\/p>\n

using System.Configuration;\r\n\r\nnamespace Loosely.CustomerPortal.Backend\r\n{\r\n  class EmailHelper\r\n  {\r\n    readonly static string gmailAccount = ConfigurationManager.AppSettings.Get(\"Gmail.Account\");\r\n    readonly static string gmailPassword = ConfigurationManager.AppSettings.Get(\"Gmail.Password\");\r\n\r\n    public static void Send(string customerEmail, string subject, string messageBody)\r\n    {\r\n      var client = new System.Net.Mail.SmtpClient(\"smtp.gmail.com\", 587);\r\n      client.EnableSsl = true;\r\n      client.Credentials = new System.Net.NetworkCredential(gmailAccount, gmailPassword);\r\n      client.Send(gmailAccount, customerEmail, subject, messageBody);\r\n    }\r\n  }\r\n}\r\n<\/pre>\n

Nothing magical here. We just pull the Gmail credentials out of our config file and then open a secure connection to Gmail\u2019s smtp server to send a message. Obviously, in a true production app, this code would be written to connect to the appropriate SMTP server and likely not use Gmail like this. Gmail works well for a simple example, however.<\/p>\n

A Consumer for TicketOpened<\/h2>\n

Now we need a consumer class to which MassTransit can send TicketOpened messages. Add a new class called TicketOpenedConsumer:<\/p>\n

using Loosely.Bus.Contracts;\r\nusing MassTransit;\r\nusing System.Diagnostics;\r\n\r\nnamespace Loosely.CustomerPortal.Backend\r\n{\r\n  class TicketOpenedConsumer : Consumes<TicketOpened>.Context\r\n  {\r\n    public void Consume(IConsumeContext<TicketOpened> envelope)\r\n    {\r\n      \/\/ Here is where you would persist the ticket to a data store of some kind.\r\n      \/\/ For this example, we'll just write it to the trace log.\r\n      Trace.WriteLine(\"=========== NEW TICKET ===========\\r\\n\" +\r\n                      \"Id: \" + envelope.Message.Id + \"\\r\\n\" +\r\n                      \"Email: \" + envelope.Message.CustomerEmail + \"\\r\\n\" + \r\n                      \"Message: \" + envelope.Message.Message);\r\n\r\n      \/\/ Send email confirmation to the customer.\r\n      var messageBody = \"Ticket ID \" + envelope.Message.Id + \" has been opened for you! \" +\r\n                        \"We will respond to your inquiry ASAP.\\n\\n\" + \r\n                        \"Your Message:\\n\" + envelope.Message.Message;\r\n\r\n      EmailHelper.Send(envelope.Message.CustomerEmail, \"Ticket Opened\", messageBody);\r\n    }\r\n  }\r\n}\r\n<\/pre>\n

This is the real meat of the backend service. The Consume method will be called for every TicketOpened message that MassTransit picks up off the bus for us. In this example, we\u2019re simply logging the information from the ticket and then sending the confirmation email to the customer.<\/p>\n

The Service Class<\/h2>\n

Next let\u2019s create a class that we\u2019ll use to host our service. We\u2019ll furnish this class to TopShelf, who will call the Start method when the service is started and the Stop method when the service is stopped. Create a new class called TicketService:<\/p>\n

using Loosely.Bus.Configuration;\r\nusing MassTransit;\r\n\r\nnamespace Loosely.CustomerPortal.Backend\r\n{\r\n  class TicketService\r\n  {\r\n    IServiceBus _bus;\r\n\r\n    public TicketService()  {  }\r\n\r\n    public void Start()\r\n    {\r\n      _bus = BusInitializer.CreateBus(\"CustomerPortal_Backend\", x =>\r\n      {\r\n        x.Subscribe(subs =>\r\n        {\r\n          subs.Consumer<TicketOpenedConsumer>().Permanent();\r\n        });\r\n      });\r\n    }\r\n\r\n    public void Stop()\r\n    {\r\n      _bus.Dispose();\r\n    }\r\n  }\r\n}\r\n<\/pre>\n

The Start method is the perfect place to put our bus initialization code. Notice how we are adding a subscription in MassTransit and providing it our TicketOpenedConsumer class. We\u2019re also making it a permanent subscription.<\/p>\n

The Startup Glue<\/h2>\n

Now we have all the classes we need. Open the Program.cs file and put the following TopShelf configuration code into the Main() function:<\/p>\n

using System.Diagnostics;\r\nusing Topshelf;\r\n\r\nnamespace Loosely.CustomerPortal.Backend\r\n{\r\n  class Program\r\n  {\r\n    static void Main(string[] args)\r\n    {\r\n      HostFactory.Run(x =>\r\n      {\r\n        x.Service<TicketService>(s =>\r\n        {\r\n          s.ConstructUsing(name => new TicketService());\r\n          s.WhenStarted(ts => ts.Start());\r\n          s.WhenStopped(ts => ts.Stop());\r\n        });\r\n        x.RunAsLocalSystem();\r\n\r\n        x.SetDescription(\"Loosely Coupled Labs Customer Portal Backend\");\r\n        x.SetDisplayName(\"Loosely.CustomerPortal.Backend\");\r\n        x.SetServiceName(\"Loosely.CustomerPortal.Backend\");\r\n      });\r\n    }\r\n  }\r\n}\r\n<\/pre>\n

I\u2019ll refer you to the TopShelf documentation<\/a> for details on how TopShelf works. For this example, you just need to know that this code wires up our TicketService class to the TopShelf framework. The great thing about TopShelf is you can just run the executable and it will run your program as a standard console app. If you run it with the \u201cinstall\u201d command line parameter, it will install it as a Windows service.<\/p>\n

Let\u2019s Do This<\/h2>\n

Right-click on the solution and choose \u201cSet Startup Projects\u2026\u201d and make both Loosely.CustomerPortal.Backend and Loosely.CustomerPortal.WebApp startup apps. Run the solution and you\u2019ll get a web browser with the web app and a console window running your new service.<\/p>\n

Try submitting a few tickets. You should see the emails (assuming you used your own email address to create the ticket) as well as log entries in the C:\\Logs\\Loosely.CustomerPortal.Backend.log file:<\/p>\n

\"image\"<\/a><\/p>\n

\"image\"<\/a><\/p>\n

Install the Service<\/h2>\n

Now, let\u2019s actually make the backend an actual Windows service. Open a command prompt as administrator (as administrator is important). Change to the directory where the Loosely.CustomerPortal.Backend.exe<\/strong> was built from Visual Studio. This will likely be the Loosely.CustomerPortal\\Loosely.CustomerPortal.Backend\\bin<\/strong> Debug folder. Run the following command:<\/p>\n

> Loosely.CustomerPortal.Backend.exe install\r\n<\/pre>\n

Now you should be able to launch the Windows services MMC snap-in (services.msc) and see the new Loosely.CustomerPortal.Backend Windows service and fire it up!<\/p>\n

\"image\"<\/a><\/p>\n

With the service running, you will want to change your Visual Studio solution back to having only the web app as the startup project.<\/p>\n

What\u2019s Next?<\/h1>\n

Now that we\u2019ve built this app, I\u2019d like to refer back to it in future blog posts so we can refine it to be more robust and \u201centerprise-ready\u201d. Let\u2019s use it to look at things like message retry logic, sagas, and multiple subscribers (for scale or for different functions). As always, let me know if there\u2019s anything specific you\u2019d like to see me write about.<\/p>\n","protected":false},"excerpt":{"rendered":"

Now that we\u2019ve seen some simple examples of how to use MassTransit with the Publish\/Subscribe pattern on multiple machines, let\u2019s build something that resembles a more real-world app. In this article, we\u2019ll build an ASP.NET MVC Customer Portal app where a customer can create a new support ticket. The ticket will be published onto the… Continue reading →<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":102,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[6],"tags":[2,3,4,5],"_links":{"self":[{"href":"https:\/\/looselycoupledlabs.com\/wp-json\/wp\/v2\/posts\/98"}],"collection":[{"href":"https:\/\/looselycoupledlabs.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/looselycoupledlabs.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/looselycoupledlabs.com\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/looselycoupledlabs.com\/wp-json\/wp\/v2\/comments?post=98"}],"version-history":[{"count":4,"href":"https:\/\/looselycoupledlabs.com\/wp-json\/wp\/v2\/posts\/98\/revisions"}],"predecessor-version":[{"id":104,"href":"https:\/\/looselycoupledlabs.com\/wp-json\/wp\/v2\/posts\/98\/revisions\/104"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/looselycoupledlabs.com\/wp-json\/wp\/v2\/media\/102"}],"wp:attachment":[{"href":"https:\/\/looselycoupledlabs.com\/wp-json\/wp\/v2\/media?parent=98"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/looselycoupledlabs.com\/wp-json\/wp\/v2\/categories?post=98"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/looselycoupledlabs.com\/wp-json\/wp\/v2\/tags?post=98"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}