This post is in response to a question I get pretty frequently when talking about Yammer. The question that many folks want to know is "how can I crawl my Yammer content from SharePoint?" Well, unfortunately you can't, and I have no trick up my sleeve to show you otherwise today to say otherwise. However, that doesn't mean that you can't have a nice consolidated set of search results that includes content from both Yammer and SharePoint, and that's the point of today's post. By the way, this code sample is based on a demo that I showed at the 2014 SharePoint Conference, so for those of you that were looking for some of that code, here you go.
As I've mentioned many times when talking to folks about Yammer development, there is a number of REST endpoints for your programming pleasure. After mentioning this in about three different venues I decided it would just be easier to write something up to demonstrate and that's exactly what I did. What we'll walk through today is the specific implementation that I used for this case. So to begin with, let's look at my out of the box search results page when I do a search for products. Because of the incredibly bad way in which this blog site deals with pictures, I'm just going to give you a link to the picture here; CTRL + Click to open it in a new browser tab: https://onedrive.live.com/?cid=96D1F7C6A8655C41&id=96D1F7C6A8655C41%217884&v=3&mkt=en-US#cid=96D1F7C6A8655C41&id=96D1F7C6A8655C41%217885&v=3.
Okay, so I've previously posted about using the Yammer REST endpoints from .NET. For this example I simply built out from that sample code, as I've been suggesting all of you do when you find something else you need to code for that it does not cover. You can find that original post and sample code at http://blogs.technet.com/b/speschka/archive/2013/10/05/using-the-yammer-api-in-a-net-client-application.aspx. With that in hand, I decided to write my own REST endpoint for this example. So why did I decide to do that?
- I wanted to demonstrate the use of the new CORS support in WebAPI 2.1. This allows me to define what hosts can make cross-domain calls into my REST endpoint to query for Yammer data. That gives me an additional layer of security, plus it now lets me make those client-side cross domain calls to my endpoint.
- I wanted to have something that delivered HTML as the result. That allows me to ensure that anyone querying Yammer gets the same exact user experience no matter what application they are using.
- I wanted to have something that could be reused in many different applications - could be other web apps, could be Windows 8 apps, could be mobile apps, whatever - with a simple REST endpoint there's no end to the possible reuse.
- In my scenario, I wanted to be able to issue queries against Yammer even for people that didn't have Yammer accounts. So in this scenario what I did was to use a service account to make all the query requests. By "service account", I just mean I created an account in Yammer whose only purpose in life is to provided me with an access token that I can use to programmatically read or write content with Yammer. For more details about access tokens in Yammer please see my initial post on this topic that I linked to above.
So with that being said, I started out by adding all the custom classes for serializing Yammer JSON data to my project, along with my code for making GET and POST requests to Yammer (as described in the original blog post).
The next thing I did was build a little console app to test out sending in a query to Yammer and getting back a set of search results. I do that to a) get down the process for issuing the query and b) getting the JSON back so I can build out a new class into which I can serialize the set of search results. I've attached some source code to this posting, so you can find the YammerSearchResults class and see how I incorporated that into the project. I'll also look at it in a little more detail later in this post.
Once I had all of that foundational pieces in place, I created my new WebAPI endpoint. There are many places out on the web where you can get your WebAPI info; for me I used the example from Mike Wasson here as my starting point for learning everything I needed for this project: http://www.asp.net/web-api/overview/security/enabling-cross-origin-requests-in-web-api. With my new WebAPI controller class created, I next enabled CORS support in my project by adding this to the WebApiConfig.cs file (in the App_Start folder)
//enable cross site requests
config.EnableCors();
Now that my project supports CORS, I need to flesh out the where and how for supporting it. To do that I added this attribute to the WebAPI controller class:
[EnableCors(origins: "http://localhost:1629,https://saml.vbtoys.com,https://sps2", headers: "*", methods: "*")]
So the main things worth pointing out here are that I added a list of hosts that I trust in the origins attribute. The "localhost:1629" reference is in there because that's the web dev server that Visual Studio created for a test web application project I created. I used that to test the functionality of the REST endpoint in just a standard HTML page before I ever tried to incorporate it into SharePoint. The saml.vbtoys.com and sps2 hosts are ones that I use for SharePoint provider-hosted apps and SharePoint respectively. So that allows me to use it both in my provider-hosted apps as well as SharePoint itself. Finally the headers and methods attributes are configured to allow all headers and methods through.
The next thing I did was create the method in my WebAPI controller that I use to execute the query against Yammer and return results. The method signature for it looks like this:
// GET api/<controller>?search=steve%20peschka
publicHttpResponseMessage Get(string search)
So as you can see, I'm going to take a string as input - my search string - and I'm going to return an HttpResponseMessage. Inside my method, the code should look quite familiar if you have seen my original post on Yammer dev with .NET - I'm just using my MakeGetRequest and GetInstanceFromJson methods:
List<SearchResult> finds = newList<SearchResult>();
string response = YammerREST.MakeGetRequest(searchUrl + "?search=" + search, accessToken);
YammerSearchResults ysr = YammerSearchResults.GetInstanceFromJson(response);
The "searchUrl" is just the standard Yammer REST endpoint for search, and the "accessToken" is the access token I got for my Yammer service account. Okay, so I got my set of search results back, but one of the first things I noticed is that the search results that are returned include very little information about the user - basically just a Yammer User ID. Of course, I not only wanted to show the author information, but I also wanted to show the picture of the Yammer user. This unfortunately does require making another call out to REST to get this information. In my particular scenario, I only wanted to show the top 3 results from Yammer, so what I did was simply enumerate through the results and get that user information for each one.
One of the nice things I was able to do with the YammerSearchResults class that I created for this is I was able to reuse the YammerMessages class that I had created in my original posting. So my search results include a collection of messages (where each message is a message that matches the search criteria), so I can simply use that code from before to enumerate through the results. This is what that looks like:
foreach(YammerMessage ym in ysr.Messages.Messages)
{
//get the Yammer User that posted each message so we can pull in their picture url
string userUrl = oneUserUrl.Replace("[:id]", ym.SenderID);
response = YammerREST.MakeGetRequest(userUrl, accessToken);
YammerUser yu = YammerUser.GetInstanceFromJson(response);
//some other stuff here I'll describe next
}
While I'm enumerating through the messages in the search results, I go ahead and make another call out to get the information I want for each user. Again, I'm able to use the MakeGetRequest and GetInstanceFromJson methods I described in my original post. With that in hand, I can go ahead and create the dataset I'm going to use to generate the HTML of search results. In order to do that, I created a local class definition in my controller and a List<> of that class type. With those pieces in place I can create one record for each search result that includes both the search result information as well as the user information. My List<> is called "finds" and the code for pulling this all together looks like this (and goes in the enumeration loop above, where it says "some other stuff I'll describe next"):
//add a new search results
finds.Add(newSearchResult(yu.FullName, yu.WebUrl, yu.FirstName,
ym.MessageContent.RichText, yu.PhotoUrl, DateTime.Parse(ym.CreatedAt),
ym.WebUrl));
iCount += 1;
if (iCount == 3)
break;
As you can see, I'm plugging in the message from the search result with "ym.MessageContent.RichText", and all of the rest of the fields are information about the user. Now that I have my list of search results, the rest of the WebAPI controller method is kind of boring. I just create a big long string in a StringBuilder instance, I add some style info and then I add HTML for each search result. I then take the results of the big long string and stick it in an HttpResponseMessage to return, like this:
newHttpResponseMessage()
{
Content = newStringContent(results),
StatusCode = System.Net.HttpStatusCode.OK
};
Shaboom, and there you go. Now, perhaps the best part of all of this is on the SharePoint side. What I decided to do there was to create a new Search vertical. All that really means is that I added a new search results page to the Pages library in an Enterprise Search site collection. You then add a new search navigation item in the search settings for the site, and you point that navigation item at the page you added. Then you go and customize the page to return whatever search results you want. I'm not covering this in super detail here obviously because it's not really on point with this topic, but it should be relatively easy to find on the web. If not, say so in the comments and we can always cover that in another blog post.
But...I created my new search vertical and then the changes I made to it were pretty simple. First, I just plugged in the out of the box search results web parts onto it. Then, I added an out of the box script editor web part above those. This is really the beauty of this solution to me - I didn't have to create anything custom in SharePoint at all. Since it's all just client side script and code, I didn't write a custom web part for SharePoint - I just created my script and pasted it into the out of the box web part. To complete this story, I would LOVE to paste in here the HTML and javascript that I use in the script editor web part to make this all work. However, it is completely unusable when pasted into the awesome blog editing tools on this site <sarcasm>. So instead you'll have to get the attachment to see it - just look for the text file called SharePoint Script Part Snippet.txt.
Now, with all that done, you can click here to see what the final results look here:
https://onedrive.live.com/?gologin=1&mkt=en-US#cid=96D1F7C6A8655C41&id=96D1F7C6A8655C41%217884&v=3
Note that in my particular case I chose to only show search results that were from messages. You could have just as easily shown search results for people, files, etc. Also I configured links on the search results so that you can click on any one of them to view the conversation in Yammer. I also included a link with a "more search results" kind of functionality, and when you click on it a new browser tab opens with the search results page in Yammer, using the query terms that were entered on the page. So it lets you slide over very easily into Yammer to get the full search experience that it provides too.
Also - as a side note - someone else at SPC mentioned that they took a similar approach but chose to return the results in an OpenSearch compliant Xml format so that it could be added as a federated search result. That didn't fit my particular scenario, but it's an interesting take on things so I'm including it here for your consideration. Nice work person that made this comment. :-)