Full example of an ODATA Controller for ActiveDirectory search operations with the LightSwitch HTML Client

Introduction

First, thank you all for your feedback to my first post, regarding the Extending the LightSwitch HTML client with a cascading push menu.

Today I will show you, how to use an ODATA v3 Controller for ActiveDirectory search operations within a LightSwitch application. For this I will use the sample A simple ODATA v3 Controller for ActiveDirectory search operations one of my colleagues presented some time ago.

As he already mentioned, it would be fine way to use this controller with select2 and JayData as we already did in a different scenario described in [NoBrainer] Using JayData with jQuery select2 plugin in LightSwitch HTML.
At this point: many thanks to Igor Vaynberg for this great control!

So lets try to build a search box for selecting a user from an Active Directory …

Let’s start

Ok, first create your Visual Studio solution (I used Visual Studio 2013 Update 2). Of course, it is a LightSwitch HTML solution.

Next, create a table called whatever (I called it ‘ADUser’) with following columns (this is only for example, so you can add other columns):

ADUser table

After this, create a Browse Data Screen and AddEdit Details Screen and link them to your table:

Browse Data Screen

 

Don’t forget to link your AddEdit screen with the Browse screen, in case you are not creating it directly from the Browse screen! ;-)

AddEdit Screen

 

Next, goto ‘Package Manager Console’ of your Visual Studio, select your server project in the dropdown and type

Install-Package Microsoft.AspNet.WebApi.WebHost

Then, create 2 folders in the server project of your LightSwitch solution, called ‘Models’ and ‘Controllers’ (note: there is of course no magic behind these folder (names), you can actually name them any way you want or just leave it at all – but when having more controllers and models it might be helpful to keep the overview).

Add a class named ‘ActiveDirectoryUser’ to the Models folder. Here you can define any Active Directory property you want to use.
I defined the common used properties:

using System;
using System.ComponentModel.DataAnnotations;

namespace LightSwitchApplication.Models
{
    public class ActiveDirectoryUser
    {
        [Key]
        public string cn { get; set; }
        public string description { get; set; }
        public string distinguishedName { get; set; }
        public int groupType { get; set; }
        public string name { get; set; }
        public string sn { get; set; }
        public string givenName { get; set; }
        public string department { get; set; }
        public string displayName { get; set; }
        public string mail { get; set; }
        public string objectGUID { get; set; }
        public string objectSID { get; set; }
        public string sAMAccountName { get; set; }
        public int sAMAccountType { get; set; }
        public Int64 uSNChanged { get; set; }
        public Int64 uSNCreated { get; set; }
        public DateTime whenChanged { get; set; }
        public DateTime whenCreated { get; set; }
    }
}

Next, right-click on the Controllers folder and click Add -> Controller and select the ‘Controller scaffolder’ dialogue:
Add Controller Scaffolder

Call your new controller ‘ActiveDirectoryUsersController’ and select your Model class:
Add Controller and select Model

The scaffolder code is somehow a bit boring, hence the scaffolder. So I did not repeat it here in full epic, but you can download the complete code following the link at the end of the post.

Hint
In our controller class we need some additional settings from our web.config. So, open the web.config and add following to the appSettings (as described in the original article about the AD search controller):

<add key="ldapConnection.Path" value="LDAP://your ldap-path" />
<add key="ldapConnection.AuthenticationType" value="Secure" />
<add key="ldapConnection.Username" value="your user with AD access" />
<add key="ldapConnection.Password" value="very secret password" />

(And yes, you should definitely encrypt the web.config after putting in the password!)

Next, open WebApiConfig.cs in the App_Start folder in your server project where we have to define our service route (my service is called ‘utilities.svc’ but you can choose any other naming …) and add following code:

using System.Web.Http;
using System.Web.Http.OData.Extensions;
using System.Web.Http.OData.Builder;
using System.Web.Http.OData.Batch;
using Microsoft.Data.Edm;
using System.Diagnostics;

namespace LightSwitchApplication
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.Routes.MapODataServiceRoute(
                    routeName: &amp;quot;Utilities.svc&amp;quot;
                    ,
                    routePrefix: &amp;quot;Utilities.svc&amp;quot;
                    ,
                    model: GetModel(&amp;quot;Utilities&amp;quot;)
                    ,
                    batchHandler: new DefaultODataBatchHandler(GlobalConfiguration.DefaultServer)
                );
            config.MapHttpAttributeRoutes();
        }
        private static IEdmModel GetModel(string ContainerName)
        {
            ODataConventionModelBuilder builder = new ODataConventionModelBuilder();
            builder.ContainerName = ContainerName;
            Controllers.ActiveDirectoryUsersController.ModelBuilder(builder);
            return builder.GetEdmModel();
        }
    }
}

(Adding BATCH support is certainly optional, but does not cost much …)

Ok, all basics are done. Now, let’s add select2 and JayData to our application. For this, download select2 and JayData at NuGet via Install-Package Select2.js and Install-Package JayData and add following files to your ‘Scripts’ folder in your solution:

Adding select2 and JayData scripts

Now we have to initialize our JayData context.
We use a dynamically generated context as described in [NoBrainer] Changing LightSwitch HTML Cascading choiceLists with JayData. To do this, create a common.js file with following code and add this to your script folder:

var svcAdUrl = msls.application.rootUri + '/utilities.svc/$metadata';
var ctxAdData = '42';
$(document).ready(function () {
    $data.initService(svcAdUrl)
        .then(function (context) {
            ctxAdData = context;
        });
});

Add a reference to this js file to the default.htm.

We use the infamous ubiquitous placeholder to detect proper initialisation of the context.

Create some wire-up code

Add a new file to your ‘Scripts’ folder, called ‘dfchls.js’ and add this code:

var dfchls = {
    addAdSearchBox: function ($element, contentItem, options) {
        try {
            //check if options are set:
            if ((undefined === options) || (null === options)) {
                var options = {}
            }
            checkSelect2Options(options);
            //render custom control:
            var $divMaster = $("<div class='dfchlsMasterDiv' />");
            $element.append($divMaster);
            addDivRow($divMaster, contentItem, options);
        }
        catch (e) {
            console.log(e.message + "; description:" + e.description);
        }
    }
}

function checkSelect2Options(options) {
    if (!options.hasOwnProperty("placeholder")) { options["placeholder"] = 'Search User'; }
    if (!options.hasOwnProperty("quietMillis")) { options["quietMillis"] = 1000; }
    if (!options.hasOwnProperty("minimumInputLength")) { options["minimumInputLength"] = 3; }
    if (!options.hasOwnProperty("dataType")) { options["dataType"] = 'json'; }
    if (!options.hasOwnProperty("page_limit")) { options["page_limit"] = '10'; }
    if (!options.hasOwnProperty("dropdownCssClass")) { options["dropdownCssClass"] = 'bigdrop'; }
    if (!options.hasOwnProperty("width")) { options["width"] = '100%'; }
}

function getActiveDirectoryData(data, searchTerm) {
    //ctxAdData is defined and set in common.js
    var promise = null;
    var tableProperties = { table: 'ActiveDirectoryUsers', id: 'sAMAccountName', name: 'displayName' };
    var idField = tableProperties['id'];
    var displayField = tableProperties['name'];
    data.results = [];
    ctxAdData.prepareRequest = function (r) {
        r[0].requestUri += "('*" + searchTerm + "*')";
    }
    promise = ctxAdData[tableProperties['table']]

    .forEach(function (dataItem) {
        data.results.push({
            id: dataItem[idField],
            text: dataItem[displayField] + " - " + dataItem["department"],
            username: dataItem["sAMAccountName"],
            displayname: dataItem["displayName"],
            email: dataItem["mail"],
            department: dataItem["department"],
            firstname: dataItem["givenName"],
            lastname: dataItem["sn"]
        });
    });
    return promise;
}

function addDivRow($divMaster, contentItem, options) {
    checkSelect2Options(options);
    var $divChild = $('<div class="dfchlsChildDiv" />');
    var $input = $("<input data-role='none' type='text'>");
    $divChild.append($input);
    $divMaster.append($divChild);
    var placeholderText = options["placeholder"];
    $input
        .select2({
            placeholder: placeholderText,
            quietMillis: options["quietMillis"],
            minimumInputLength: options["minimumInputLength"],
            page_limit: options["page_limit"],
            width: options["width"],
            query: function (query) {
                var items = [];
                getActiveDirectoryData(items, query.term)
                    .then(function () {
                        query.callback(items);
                    });
            }
        })
        .on("change", function (e) {
            contentItem.screen.findContentItem("UserName").value = e.added.username;
            contentItem.screen.findContentItem("Email").value = e.added.email;
            contentItem.screen.findContentItem("Department").value = e.added.department;          
        });    
    addAttributeDataRole($divMaster, 'select2-focusser select2-offscreen');
}
//this function is for render and display needs:
function addAttributeDataRole($ctrl, classToFind) {
    try {
        $ctrl.find("input[type=text]").each(function () {
            var $this = $(this);
            //select input boxes with CssClass select2-focusser.
            //These are inputs rendered by select2-Control and are 
            //used for item searching. Adding attribute "data-role=none" prevent input from being wrapped by jQuery-mobile:

            if ($this.hasClass(classToFind)) {
                $this.attr('data-role', 'none');
            }
        });
    } catch (e) {
        console.log(e.message + "; description:" + e.description);
    }
}

Add a reference to this js file to the default.htm.
The default.htm must now have at least the following references:

    <script type="text/javascript" src="scripts/common.js"></script>
    <script type="text/javascript" src="scripts/select2.min.js"></script>
    <script type="text/javascript" src="scripts/dfchls.js"></script>
    <script type="text/javascript" src="scripts/jaydata.min.js"></script>
    <script type="text/javascript" src="scripts/jaydataproviders/odataprovider.min.js"></script>

And for eye-candy, use css these files:

    <link href="content/select2-light.css" rel="stylesheet" />
    <link href="content/select2-remove-arrow.css" rel="stylesheet" />

We have to add our new control to our AddEdit screen. Add a new Data item, select ‘local property’ and call it ‘SearchAdUser’. Drag your new property in the screen designer and select ‘Custom Control’ for this property:
New Data Item

Now, edit the render method for this new custom control:

myapp.AddEditADUser.SearchAdUser_render = function (element, contentItem) {
    // Write code here.
    dfchls.addAdSearchBox($(element), contentItem, null);
};

Done

That’s it!
Now, you can search for any user in your company!

The Search Box

Of course, you can use the Lightswitch dark theme as well. For this, use the select2-dark.css:

The Search Box

Download

You can download the complete code from our GitHub Account at biz.dfch.LS.LdapExample.

That’s all for now, folks!

Comments

  1. Serguei says:

    Looks great! Very helpful.

  2. Serguei says:

    Sorry for disturbing.
    Can you help me with the problem
    When runs locally ODATA controller works OK, but when published ActiveDirectoryUsers fails because key=Application.User.Name does not get value.

    • Ronald Rink says:

      Hi Serguei, I have two questions regarding you comment:
      1. do you have authentication enabled in your application?
      2. and by “running locally” do you mean running with F5 in the debugger?

      The error “Application.User.Name” not having a value seems to me, that you are not authenticated in the application (as not having enabled form-based or integrated authentcation). Could you verify this and get a stack trace where the error exactly happens?
      Ronald

      • Serguei says:

        Hi Roland!
        Thanks for quick reply.
        I’m asking about yours Simple ODATA v3 Controller for ActiveDirectory (https://d-fens.ch/2014/11/06/a-simple-odata-v3-controller-for-activedirectory-search-operations/)
        1. Access control is: Use Windows authentication->Allow any authenticated Windows user
        2. Yes, when F5 in IE Debug and http://localhost:/HTMLClient/ in Google Chrome it works well. But when published at http:///SearchLdapObject/HtmlClient/ it returns “No items”, but OData source with defenite keys in IE like http://ekbap12t/searchldapobject/v1/Utilities.svc/ActiveDirectoryUsers('**‘) returns right json !

        I can’t figure out what’s wrong with application permissions. Should I install desktop client to administer rights

      • Ronald Rink says:

        Hi, this does not seem to be a problem with ApplicationPermissions (as far as I can tell) – especially when you state (if I understood you correctly), that the LDAP query returns the correct data (meaning that the authenticated user has at least read/query permissions to the AD) resulting in the controller to return actual JSON data.
        We have this running at several different customers with IE9, IE10, FF and Chrome (various versions). So I am really not sure what this is – but probably not something related to the controller itself.
        Maybe something related to the browser version? difficult to say from here …
        You write that in IE DEBUG mode the data is displayed correctly? A timing/sync problem should not be the cause as the UI control operates via promises. But then again it has nothing to do with the ODATA controller itself.

  3. Serguei says:

    Hi Ronald, thanks for the comments.
    IE browser version is not a blame, Chrome goes the same way.
    But I found walkaround – I get Application.User.Name with HTTP handle and then call OData method with the key, so it works fine :)

  4. The lookup is painfully slow for me. Is there anyway to speed it up? Do the async call warnings I receive during build (Warning 3 This async method lacks ‘await’ operators and will run synchronously. Consider using the ‘await’ operator to await non-blocking API calls, or ‘await Task.Run(…)’ to do CPU-bound work on a background thread. c:\users\tabowma0\desktop\working projects\arci\arci\arci\arci.server\controllers\activedirectoryuserscontroller.cs 100 46 ARCI.Server
    )

    play into the speed of lookup by chance? Thanks!

    • Ronald Rink says:

      Hi gibbypoo, we are running this code in an AD environment with approx 120’000 user objects without any problem. In my opinion, the lookup will not be faster whether you run this synchronously or not. You could certainly rewrite this to async behaviour if you suspect this to be the case.
      Do you have a search on a specific subtree? or any filter expressions that will filter too ‘broad’? Does it speed up when you adjust ‘requestUri += “(‘*” + searchTerm + “*’)”;’ to a query without leading ‘*’? Or did you adjust the waitTimeout, returned properties or anything? Are calls taking some time as well, when you query them with e.g. the PoSH ActiveDirectory Module? And how long is call actually taking when querying for a user account?
      Regards, Ronald

      • gibbypoo says:

        I added an additional field but that wouldn’t slow it that much, I would think. It’s taking about 30 seconds for each lookup. I removed the leading asterisk on the requestUri and it seemed to run at the same speed. I’m only looking to return my additional field, first name and last name; would it help by removing all extra returned fields or apply some additional filter to help with the search speed?

        Also, thanks for the unbelievable rate of reply!

        Tim

      • Ronald Rink says:

        Hi gibbypoo, regarding the extra field: is this a field that is included in the GC? And I do not think that removing the other fields would help.
        I really have no explanation why this wuld run that extra slow (30s). This is way above normal (whether it is sync or async does not matter). How long would the same query take when using other tools like the PowerShell ActiveDirectory Cmdlets?. How many objects are returned by the query?

        Maybe you are querying the whole LDAP root tree and your attribute is not a unique attribute? Did you try to reduce the query to some smaller subtree (i.e OU=sub,OU=sub,DC=example,DC=com)? Does it run faster then? How long does it take to query for an attribute like username?

        If you wanted a “proof” that the LDAP query is taking that long time you could do the follwoing:
        1. run wireshark on the ODATA Controller and check the time the LDAP query run and compare it to the time the actual HTTP request runs.
        2. check to domain controller, where this query runs against, for high CPU utilisation.

        Regards, Ronald

      • gibbypoo says:

        Also, the lookup DOES work. Excellent job on getting that to work. I’m trying to pull the displayName off of the object and insert it into my added or edited record. How do I go about putting that piece of info from the lookup into the record? Do I need to add it at the Entity_Inserting/Updating level?

        Thanks again!

      • Ronald Rink says:

        I do not understand what you mean by putting the displayName into the edited record. This one should be returned and shown by default. You can check the attributes in the ActiveDirectoryUSer class and its table mappings in the JS code and the ‘tableProperties’.
        Regards, Ronald

      • gibbypoo says:

        I don’t have any subtrees identified (LDAP://dc=main,dc=example,dc=com), just the standard LDAP connector which is I guess querying the whole root tree? Can I change any of the query filters that may speed things up? Maybe only query against displayName in getActiveDirectoryData?

        Again, thanks so much for the help!

      • Ronald Rink says:

        if you know where your users are, then only specify this subtree. I do not think that querying only specific attributes will make much of a difference if they are all also in the GC. But you would reall yhave to test that.

Trackbacks

  1. […] person picker-cotrol anymore. Instead, we uses select2 control (I already wrote something about it: here.) Now, we deployed our new LS application and set authentication method to BASIC. (Of course, this […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: