Picture: Microsoft

Expose OpenSearch Endpoint with an ASP.NET Core Controller Action

OpenSearch

OpenSearch is a standard that has existed for about 15 years and is used by search engines and Browsers.

This standard was developed by Amazon and is used today, for example, for browsers to have a search field as the start page, for a search bar to be directly in the browser or for a search bar to be embedded directly in Google search hits for the respective web page.

Structure

The OpenSearch definition is in XML and must then be linked linked in header of the website.

For example: Stackoverflow

<html class="html__responsive">

    <head>

        <title>Stack Overflow - Where Developers Learn, Share, &amp; Build Careers</title>
        <link rel="shortcut icon" href="..">
        <link rel="apple-touch-icon" href="...">
        <link rel="image_src" href="..."> 

        <link rel="search" type="application/opensearchdescription+xml" title="Stack Overflow" href="/opensearch.xml">
        ..
        ..
    </head>

So here we see the HTML head area and the link entry for the OpenSearch definition XML.

This tells the browser or a search bot that the website supports OpenSearch and the definition for it is available at /opensearch.xml.

<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/" xmlns:moz="http://www.mozilla.org/2006/browser/search/">
    <ShortName>Stack Overflow</ShortName>
    <Description>
        Search Stack Overflow: Q&A for professional and enthusiast programmers
    </Description>
    <InputEncoding>UTF-8</InputEncoding>
    <Image width="16" height="16" type="image/x-icon">
        https://cdn.sstatic.net/Sites/stackoverflow/img/favicon.ico?v=4f32ecc8f43d
    </Image>
    <Url type="text/html" method="get" template="http://stackoverflow.com/search?q={searchTerms}"/>
</OpenSearchDescription>

The definition itself is relatively lean; it contains only the name, the description, an image and a link to the search with a placeholder for the search input.

ASP.NET OpenSearch Endpoint

In principle, you could simply deliver this as a static page, which would have the disadvantage that you would have to save the URLs permanently.

This is less tragic for relative URLs; however, in a non-fixed environment, it is not so easy.
With ASP.NET Core, however, this is relatively easy to implement with the XDocument class.

[Route("opensearch.xml", Route="OpenSearch"]
public IActionResult OpenSearch()
{
    XNamespace nsOs = "http://a9.com/-/spec/opensearch/1.1/";

    XDocument doc = new XDocument(new XDeclaration("1.0", "UTF-8", "yes"),
        new XElement(nsOs + "OpenSearchDescription",
            new XElement(nsOs + "ShortName", "Your ShortName"),
            new XElement(nsOs + "Description", "Your Description"),
            new XElement(nsOs + "InputEncoding", "UTF-8"),
            new XElement(nsOs + "Image",
                new XAttribute("width", 64),
                new XAttribute("height", 64),
                new XAttribute("type", "image/x-icon"),
                $"{Url.Content("~/favicon.ico")}"
            ),
            new XElement(nsOs + "Url",
                new XAttribute("type", "text/html"),
                new XAttribute("method", "get"),
                  new XAttribute("template",  $"{Url.RouteUrl("OpenSearchQueryEndpoint")}?query=") 
            )
        )
    );

    return File(Encoding.UTF8.GetBytes(doc.ToString()), "text/xml");
}

To avoid to generate the XML on every request we can simply use the InMemory Caching. This improves the performance a lot, but we remain flexible.

[Route("opensearch.xml", Route="OpenSearch"]
public IActionResult OpenSearch()
{
    if (!_cache.TryGetValue("OpenSearch", out byte[] data))
    {
        XNamespace nsOs = "http://a9.com/-/spec/opensearch/1.1/";

        XDocument doc = new XDocument(new XDeclaration("1.0", "UTF-8", "yes"),
            new XElement(nsOs + "OpenSearchDescription",
                new XElement(nsOs + "ShortName", "Your ShortName"),
                new XElement(nsOs + "Description", "Your Description"),
                new XElement(nsOs + "InputEncoding", "UTF-8"),
                new XElement(nsOs + "Image",
                    new XAttribute("width", 64),
                    new XAttribute("height", 64),
                    new XAttribute("type", "image/x-icon"),
                    $"{Url.Content("~/favicon.ico")}"
                ),
                new XElement(nsOs + "Url",
                    new XAttribute("type", "text/html"),
                    new XAttribute("method", "get"),
                    new XAttribute("template",  $"{Url.RouteUrl("OpenSearchQueryEndpoint")}?query=") 
                )
            )
        );

        data = Encoding.UTF8.GetBytes(doc.ToString());

        _cache.Set("OpenSearch", data, TimeSpan.FromDays(7));
    }
    return File(data, "text/xml");
}

For the head area only the following is missing

<link rel="search" type="application/opensearchdescription+xml" title="Your Website" href="@(Url.RouteUrl("OpenSearch"))">

Browsers then automatically recognize that the website has an endpoint for OpenSearch.

The same, of course, applies to search engine bots, which means that after a few hours or even days the search bar will appear in Google's hit list.