Rebuild Episerver DB to use MongoDB!!! What you think?

I recently doing some small work with MongoDB and just realize Document Based NoSQL DB is really fitted for CMS! So let’s speak about it.

Prons:

  1. Entity stored as BSON (brother of JSON!). That means your content properties will be persisted as the current structure in MongoDB.
  2. MongoDB can Scale Out that means if your site has heaps of content and heaps of traffic, instead of complexity around scaling of SQL Server you can use easily horizontally scale application.
  3. It is FREE!
  4. CMS now days is very tight to Web Analytics and MongoDB is designed to store and process this kind of data! Having said that it doesn’t mean because of just analytics we have to move all of our DB to MongoDB, but if we use SQL Server and we need MongoDB for analytics, it added complexity to project and speed up new developer with the project will become really hard!

Cons:

  1. Currently, Episerver is built on top of SQL Server and I think changing that may need some work!
  2. Many vendors built plugins and they all need to learn and change their packages!
  3. All developers need to learn MongoDB! (To me it is Prons but for business not!)
  4. Technically on my experience “SQL Server” is not a bottleneck for Episerver (thanks for awesome cache)! But in my opinion, next generation of CMS is more dynamic and based on user taste it needs to transform content presentation based on what user is looking for! So very soon it becomes a problem!

 

I really like the idea of you guys! Let me know what you think!

RobotsTxtHandler project – How it works (part 3)

Steps:

  1. Create new “Class Library” project.
  2. Add these NuGet packages to your project: (Please consider you need to have Feed Url of EPiserver NuGet server -> Link):EPiServer.CMS.UI.Core
    Microsoft.AspNet.Mvc
  3. Entry  point: We need to define a “Normal MVC Controller” and decorate that with “GuiPlugIn” attribute:
        [GuiPlugIn(
            Area = EPiServer.PlugIn.PlugInArea.AdminMenu,
            DisplayName = "Robots.txt Handler",
            Description = "Robots.txt Handler",
            Url = Const.ModuleUrlBase + Const.Separator + Const.ModuleAdminController + Const.Separator + Const.ModuleAdminIndexAction,
            RequiredAccess = AccessLevel.Administer)]
        public class AdminController : Controller
    {
    }

    As you can see within attribute properties you can define the name and access level and base Url to use the module to route the URL correctly  into controller. Another important bit is “Area” property! This tells Episerver WHERE to show the  link of the plugin.  You can see full list and description here

  4. When a user clicks on plugin link in “AdminMenu” we want to show a list of available “Robot.txt” handler for each site to a user. To achieve this let’s add “Index”:
           public ActionResult Index()
            {
                var robotTxts = robotsTxtRepository.All().ToList();
    
                var model = new AdminViewModel
                {
                    Sites = GetSitesList(robotTxts.Select(a => a.SiteId)),
                    AvailableRobotTxts = robotTxts.Select(a => new AvailableRobotTxt { Id = a.Id.ExternalId, Name = siteList.Single(s => s.Id.ToString() == a.SiteId).Name })
                };
    
                return PluginPartialView(Const.ModuleAdminIndexAction, model);
            }

    And more than obvious we need to define ViewModel and repository class. You can see them on GitHub

  5. We need to add “Index.cshtml” to the project. Let’s create folder “\View\Admin” and create “Index.cshtml” under “Admin”:
    @model AdminViewModel
    @{  Layout = "Layout.cshtml"; }
    
    @if (Model.AvailableRobotTxts != null && Model.AvailableRobotTxts.Any())
    {
        <div>
            @foreach (var robotTxt in Model.AvailableRobotTxts)
            {
                <div>
                    <span>@robotTxt.Name</span>
                    <span><a href="@Url.Content(string.Format("{0}/{1}/{2}/{3}", Const.ModuleUrlBase,Const.ModuleAdminController,Const.ModuleAdminEditAction, robotTxt.Id))">Edit</a></span>
                    <span><a href="@Url.Content(string.Format("{0}/{1}/{2}/{3}", Const.ModuleUrlBase, Const.ModuleAdminController, Const.ModuleAdminDeleteAction, robotTxt.Id))">Delete</a></span>
                </div>
            }
        </div>
    }
    
    @if (Model.Sites != null && Model.Sites.Any())
    {
        using (Html.BeginForm())
        {
            @Html.DropDownList("SelectedSite", Model.Sites)
            @Html.TextAreaFor(a => a.RobotText, new { @rows = 20 })
    
            <input type="submit" value="Submit" />
        }
    }
    

     

  6. Create “module.config”. This file instructs Episerver how route segment should work and register the plugin DLL:
    <?xml version="1.0" encoding="utf-8"?>
    <module loadFromBin="false" productName="Zanganeh RobotsTxtHandler" >
      <assemblies>
        <add assembly="Zanganeh.RobotsTxtHandler" />
      </assemblies>
      <routes>
        <route url="{moduleArea}/{controller}/{action}/{id}">
          <defaults>
            <add key="moduleArea" value="Zanganeh.RobotsTxtHandler" />
            <add key="controller" value="" />
            <add key="action" value="" />
            <add key="id" value="" />
          </defaults>
        </route>
      </routes>
    </module>
  7. Create web.config  under “View” folder, so Razor template works fine:
    <?xml version="1.0"?>
    <configuration>
      <configSections>
        <sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
          <section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
          <section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
        </sectionGroup>
      </configSections>
    
      <system.web.webPages.razor>
        <host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
        <pages pageBaseType="System.Web.Mvc.WebViewPage">
          <namespaces>
            <add namespace="System.Web.Mvc" />
            <add namespace="System.Web.Mvc.Ajax" />
            <add namespace="System.Web.Mvc.Html" />
            <add namespace="System.Web.Routing" />
            <add namespace="EPiServer.Framework.Web.Mvc.Html" />
            <add namespace="Zanganeh.RobotsTxtHandler" />
            <add namespace="Zanganeh.RobotsTxtHandler.ViewModel" />
          </namespaces>
        </pages>
      </system.web.webPages.razor>
     
      <system.web>
        <compilation>
          <assemblies>
            <add assembly="Zanganeh.RobotsTxtHandler" />
          </assemblies>
        </compilation>
        
      </system.web>
    </configuration>
  8. Create new “nuspec” file to instruct NuGet Package Manager to copy “View” properly into proper area:
    <?xml version="1.0" encoding="utf-8"?>
    <package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
    	<metadata xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
        <id>$id$</id>
        <version>$version$</version>
        <authors>$author$</authors>
        <owners>$author$</owners>
        <requireLicenseAcceptance>false</requireLicenseAcceptance>
        <description>$description$</description>
        <copyright>$copyright$</copyright>
    		<projectUrl>https://github.com/zanganeh/RobotsTxtHandler</projectUrl>
    		<tags>Robots EPiServer</tags>
    	</metadata>
      <files>
        <file src="module.config" target="content\modules\Zanganeh.RobotsTxtHandler\module.config" />
        <file src="Views\web.config" target="content\modules\Zanganeh.RobotsTxtHandler\Views\web.config" />
        <file src="Views\Admin\Index.cshtml" target="content\modules\Zanganeh.RobotsTxtHandler\Views\Admin\Index.cshtml" />
      </files>
    </package>

     

  9. I’m using “MyGet” (can take a look using link) to build my project and generate NuGet file and hosting purpose.

You can fork GitHub and use push it to your own MyGet to see how it works. Valdis Iljuconoks mentioned good point why not extending Link. I just started working on this project for learning purpose and one of our client was asking for this feature and because it is simple I just picked it up to try the Episerver Plugins.

Azure Cognitive Text Analytics key phraser used to Tag an Episerver content

I wanted to play with “Cognetive Services” and thought best to use Episerver for  this reason. I wanted to have below functionality:

  1. Mark “Content” properties I want as “Tageable” content.
  2. Using Azure Congetice Text Analytics key  phraser to detect tag a content
  3. Save key phrses into “Tag” property of the content

Quite simple. To achieve this need to:

  1. Signup for “Free”  Azure Cognitive as below: (got from here)
    1. Navigate to Cognitive Services in the Azure Portal and ensure Text Analytics is selected as the ‘API type’.
    2. Select a plan. You may select the free tier for 5,000 transactions/month. As is a free plan, you will not be charged for using the service. You will need to login to your Azure subscription.
    3. Complete the other fields and create your account.
    4. After you sign up for Text Analytics, find your API Key. Copy the primary key, as you will need it when using the API services.
  2. Write a code to gather all contents on publishing as below
    string apiUrl = "https://westus.api.cognitive.microsoft.com/text/analytics/v2.0/keyPhrases";
    string apiSubscriptionID = "value from Azure Cognetive API";
    string language = "en";
    IEnumerable<string> contents = new string[] { "first content to detect", "second  content to detect" };
    
     using (var webClient = new WebClient())
                {
                    webClient.Headers.Add("Ocp-Apim-Subscription-Key", apiSubscriptionID);
                    webClient.Headers.Add(HttpRequestHeader.ContentType, "application/json");
                    webClient.Headers.Add(HttpRequestHeader.Accept, "application/json");
    
                    var document = new Request { Documents = contents.Select((item, index) => new Content { Id = index, Language = language, Text = item }).ToArray() };
                    var requestContent = JsonConvert.SerializeObject(document);
    
                    var result = webClient.UploadString(apiUrl, requestContent);
    
                    return JsonConvert.DeserializeObject<Response>(result).Documents.SelectMany(a => a.keyPhrases);
                }

    As you can see you need “API Subscription ID” which  you can get it from Azure Portal same API Url.

    As you  can see the concept is quite simple, you pass your “Content(s)” and it returns back key phrases.

  3. Now what you need to  do is to change your Page, Block, .. (Content) to inherit “ITageablePage” and apply “[TextAnalysisRequire]” attribute to any content type property which you want to consider as a part of text analysis. Sample as below:
        [SiteContentType(
            GUID = "17583DCD-3C11-49DD-A66D-0DEF0DD601FC",
            GroupName = Global.GroupNames.Products)]
        public class ProductPage : PageData, ITageablePage
        {
            [Display(
        GroupName = SystemTabNames.Content,
        Order = 310)]
            [CultureSpecific]
            [TextAnalysisRequire]
            public virtual XhtmlString MainBody { get; set; }
    
            [Editable(false)]
            [UIHint(UIHint.Textarea)]
            public virtual string Tags { get; set; }
        }

You are now all done! When you publish a “ProductPage” instant it will automatically read “MainBody” property, strip out HTML and pass “Text” to Azure Text Analytics service, ask for “Key Phrases” and store them into “Tag” property! Cool and easy, yeah!

This can become a game changer for the concept of CMS, so now  you can leverage “Personalization” and “Tagged Content” to present your customer what they are looking for. I’m going to write a roadmap for this and make it work with  new personalization concept.  I will push this as NuGet package!

You  can  find the code here

Please bare in mind the API is still in “Preview” version!

RobotsTxtHandler project – How it works (part 2)

You need to have Episerver CMS project (CMS >= 10.0.2). Use Episerver NuGet (if you want check here) and search for Zanganeh.RobotsTxtHandler or directly running code below in “Package Manager Console”:

PM> Install-Package Zanganeh.RobotsTxtHandler

Then re-build  and run project. Then goto {siteurl}/EPiServer/CMS/Admin/Default.aspx and on left hand menu click on “tools -> Robots.txt Handler”:

capture

You can select current site from “Dropdown”and enter text you want to show as “robots.txt” file for that site.

When you save you can see list:

capture

 

And if you got to http://{siteurl}/robots.txt you can see what you entered in the textbox:

capture

 

You can delete or edit robots.txt value using same list:

capture

The  idea behind the scene is quite simple. For each site we store data of robots.txt into Dynamic Data Store. And having http handler that based on current site extract associated robots.txt and show the content. In next step I will describe Episerver add-on Gui.

IContentRepository.GetItems – how it works under hood!

I’ve been told IContentRepository.GetItems is heaps better. And when I asked  people why? They told me it is built DB load. Just want to clarify this. You pass a list of ContentReference (IEnumerable<ContentReference>) and it loads all IContent for each item. This seems to build DB load but when you take a look into code it does build load from MEMORY CACHE otherwise, it does load individually:

class EPiServer.Core.ContentProvider:

    protected virtual IEnumerable<IContent> LoadContents(IList<ContentReference> contentReferences, ILanguageSelector selector)
    {
      IList<IContent> contentList = (IList<IContent>) new List<IContent>();
      foreach (ContentReference contentReference in (IEnumerable<ContentReference>) contentReferences)
      {
        IContent content = this.Load(contentReference, selector);
        if (content != null)
          contentList.Add(content);
      }
      return (IEnumerable<IContent>) contentList;
    }

So please consider this as  performance consideration.

RobotsTxtHandler project (part 1)

As I wanted to get some  experience with Episerver plugin I started this project. This package allow site admin to add/edit/delete “robots.txt” file for Episerver site(s). Requirements  are as below:

  1. Provide “/robots.txt” handler
  2. Support multi-site
  3. Support multilingual
  4. Support multi-channel (e.g. for mobile channel provides different robots.txt)
  5. Easy admin area
  6. Support “default” robots.txt
  7. For UAT site disallow robots to  crawl the site (using web.config to override all  existing configs)
  8. Give some basic analytics data
  9. Plugin is based on MVC (for my own learning purpose only!)

Based  on above I broke it down to into three phases

Phase 1:

  1. Provide “/robots.txt” handler
  2. Support multi-site
  3. Plugin is based on MVC (for my own learning purpose only!)

Phase 2:

  1. Easy admin area
  2. Support “default” robots.txt
  3. For UAT site disallow robots to  crawl the site (using web.config to override all  existing configs)

Phase 3:

  1. Give some basic analytics data
  2. Support multilingual
  3. Support multi-channel (e.g. for mobile channel provides different robots.txt)

I already release the first RC for Phase 1 and in this tutorial, I will try to explain the challenges and what I learned. You  can access the  repo via:

https://github.com/zanganeh/RobotsTxtHandler

More than happy to get feedbacks on githib. Next step I will describe the architecture and base of plugin, nuget  package and MyGet integration!

How to debug Dojo in Episerver

Recently I struggled  with debugging a custom property code. I usually  check Console in chrome  developer tool to see if  I missed something but there was nothing in  there at all! I tried to debug Dojo and Episerver JS code to find out what a problem and all JS is minified! To  resolve this you can add:

<clientResources debug="true" />

to “web.config” -> “episerver.framework” section! With that all “warnings” from  dojo and episerver will be shown to you and JS is not minified! Hope it can save someone else time! Just remember to remove this item on your live site!

 

EPiServer Custom Property in Simple Steps by Step example!

One our fellow in world.episerver ask question about the custom property in EpiServer. This has been discussed many many time in forum and blogs but I couldn’t be able to find simple version to reference. In this post I’m trying to describe this in simple way!

Requirement: We want to get ahtur “First Name” and “Last Name” and store that into object with type named “Author” as below:

    public class Author
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

 

So eventually in AlloySample having something like:

public class ArticlePage : StandardPage
{
	[UIHint(AuthorEditorDescriptor.UIHint)]
	[BackingType(typeof(PropertyAuthor))]
	public virtual Author Author { get; set; }
}

 

Solution:

  1. First create Alloy sample
  2. Definitely we need type so create “Author” under “Model” folder:
    public class Author
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
  3. PropertyDefinitionTypePlugIn: PropertyDefinitionTypePlugIni is used to introduce a “Type” as Episerver predefined “Type”!  The type needs to be based on  “PropertyString (long or short) or ProperttNumber.  So this will tells Episerver which  property “VALUE” (in our example complex type named  ‘Author’) need to be stored as ‘string’ or ‘int’. So what we do is serialize object as JSON string and store it as ‘string’ in DB. I will speak on separate post about performance considerations when you are building these kind of ‘Custom Properties’. So below is descriptor:
        [PropertyDefinitionTypePlugIn]
        public class PropertyAuthor : PropertyLongString
        {
            public override Type PropertyValueType
            {
                get { return typeof(Author); }
            }
    
            public override object Value
            {
                get
                {
                    var value = base.Value as string;
    
                    if (value == null)
                    {
                        return null;
                    }
    
                    return JsonConvert.DeserializeObject<Author>(value);
                }
    
                set
                {
                    if (value is Author)
                    {
                        base.Value = JsonConvert.SerializeObject(value);
                    }
                    else
                    {
                        base.Value = value;
                    }
                }
            }
    
            public override object SaveData(PropertyDataCollection properties)
            {
                return LongString;
            }
        }

     

  4. EditorDescriptor: With EditorDescriptor we instruct episerver UI  how to render the control in admin area. So we in our case we need two text boxes for “First Name” and “Last Name”  and admin can change the value. To achieve this we need to instruct Episerver to use what JS to render our property on admin area:
        [EditorDescriptorRegistration(TargetType = typeof(Author),
            UIHint = AuthorEditorDescriptor.UIHint)]
        public class AuthorEditorDescriptor : EditorDescriptor
        {
            public const string UIHint = "Author";
            private const string AuthorProperty = "alloy/editors/AuthorProperty";
    
            public AuthorEditorDescriptor()
            {
                ClientEditingClass = AuthorProperty;
            }
        }

     

  5. Dojo JS! This bit is most mysterious bit.  Episerver is using Dojo framework on admin area. So rendering will be happen  on client side! To achieve this  we need to create two file. One is HTML which is “template” file use by JS as template for rendering and actual JS file which will be called first to fire the  rendering process! So let’s create JS first.  Create asda in:captureAuthorProperty.js:
    define([
        "dojo/_base/declare",
        "dijit/_Widget",
        "dijit/_TemplatedMixin",
    
        "dojo/text!./templates/AuthorProperty.html",
        "dojo/dom",
        "dojo/domReady!"
    ],
    function (
        declare,
        _Widget,
        _TemplatedMixin,
        template,
        dom
    ) {
        return declare("alloy/editors/AuthorProperty", [
            _Widget,
            _TemplatedMixin], {
                templateString: template,
                _onFirstNameChange: function (event) {
                    if (!this.value)
                    {
                        this.value = { firstName: '', lastName: '' };
                    }
                    this.value.firstName = event.target.value
                    this._set('value', this.value);
                    this.onChange(this.value);
                },
                _onLastNameChange: function (event) {
                    if (!this.value) {
                        this.value = { firstName: '', lastName: '' };
                    }
                    this.value.lastName = event.target.value
                    this._set('value', this.value);
                    this.onChange(this.value);
                },
                _setValueAttr: function (val) {
                    if (val) {
                        this.firstName.value = val.firstName;
                        this.lastName.value = val.lastName;
                        this._set('value', val);
                    }
                },
                isValid: function () {
                    return true;
                }
            }
        );
    });

     

    AuthorProperty.html:

    <div>
        <label for="firstname">First name</label>
        <input type="text" data-dojo-attach-point="firstName" 
               name="firstname"
               data-dojo-attach-event="onchange:_onFirstNameChange" />
    
        <label for="lastName">Last name</label>
        <input type="text" data-dojo-attach-point="lastName" 
               name="lastName" 
               data-dojo-attach-event="onchange:_onLastNameChange" />
    </div>

    Hope it help.

Mobile Visitor Group criteria for EPiServer

We have heaps of requests from our client. There is a good reference here and we little a bit extended this using below approach, to define if the current user is on mobile or not and show, hide content based on this concept. First step is to define model and this will be stored in Dynamic  Data Store (DDS). This will be stored so CMS can use it to apply the criteria:

    public class BrowserModel : CriterionModelBase
    {
        [DojoWidget(
            SelectionFactoryType = typeof (EnumSelectionFactory),
            LabelTranslationKey = "/shell/cms/visitorgroups/criteria/browser/browsertype",
            AdditionalOptions = "{ selectOnClick: true }"),
         Required]
        public BrowserType Browser { get; set; }

        public override ICriterionModel Copy()
        {
            return ShallowCopy();
        }
    }

    public enum BrowserType
    {
        Desktop,
        Mobile
    }

Second step is to define a criteria and the logic which make decision about if the current user is on mobile or not! We are using  current user request UserAgent:

[VisitorGroupCriterion(
        Category = "User Criteria",
        DisplayName = "Browser",
        Description = "Criterion that matches type of the user's browser",
        LanguagePath = "/shell/cms/visitorgroups/criteria/browser")]
    public class BrowserCriterion : CriterionBase<BrowserModel>
    {
        public override bool IsMatch(IPrincipal principal, HttpContextBase httpContext)
        {
            return MatchBrowserType(httpContext.Request.UserAgent);
        }

        protected virtual bool MatchBrowserType(string userAgent)
        {
            var os =
                new Regex(
                    @"(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od|ad)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino",
                    RegexOptions.IgnoreCase | RegexOptions.Multiline);
            var device =
                new Regex(
                    @"1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-",
                    RegexOptions.IgnoreCase | RegexOptions.Multiline);
            var deviceInfo = string.Empty;

            if (os.IsMatch(userAgent))
            {
                deviceInfo = os.Match(userAgent).Groups[0].Value;
            }

            if (device.IsMatch(userAgent.Substring(0, 4)))
            {
                deviceInfo += device.Match(userAgent).Groups[0].Value;
            }

            if (!string.IsNullOrEmpty(deviceInfo))
            {
                return Model.Browser == BrowserType.Mobile;
            }

            return Model.Browser == BrowserType.Desktop;
        }

Hope it can save you some time on writing custom code!

To exclude uploaded EPiServer Form file uploaded (FileUploadElementBlock) from EPiServer Find Index

There is a flues in EPiServer Form which all uploaded file can be indexed. To exclude uploaded file using EPiServer Form -> FileUploadElementBlock from EPiServer Find indexer you can go:

    [ModuleDependency(typeof(InitializationModule))]
    public class EPiServerFindInitialization : IInitializableModule
    {
        public void Initialize(InitializationEngine context)
        {
            ContentIndexer.Instance.Conventions.ForInstancesOf().ShouldIndex(ShouldIndexDocument);
        }

        public void Uninitialize(InitializationEngine context)
        {
        }

        bool ShouldIndexDocument(DocumentFileBase documentFileBase)
        {
            if (contentAssetHelper.Service.GetAssetOwner(documentFileBase.ContentLink) is FileUploadElementBlock)
            {
                IEnumerable result;
                ContentIndexer.Instance.TryDelete(documentFileBase, out result);

                return false;
            }

            return true;
        }

        readonly Injected contentAssetHelper;
    }