Custom Build Triggers in VSTS

In my previous posts, I’ve shown people how to use VSTS (formerly known as VSO) to trigger continuous testing using builds and release management. I was able to utilize new reporting capabilities in build, particularly, test reports. I created reports that shows pass/fail trends for tests in my build definitions.

PassFailTrend

There are still limitations (or in this case features I wish Microsoft would consider such as customizing test reports from builds as well as showing pass/fail trends past 10 builds). My biggest disappointment thus far is “NOT” able to schedule build (with tests) using re-occurring pattern/s. As of writing this post, you can schedule builds in VSTS however, you have to “manually” keep adding scheduled times.

Scheduled

Imagine a scenario where you need to run a build every hour (or half hour), you have to manually add new times every hour, in this case, 24 times. Very inconvenient.

Fortunately, VSTS has public API’s that allows us to access build execution and trigger. With the public API’s I was able to write a very simple console app and use Windows’ built in “Task Scheduler” functionality. One would say, why not create a windows services? Yes, that’s option but I would make a point back to say: “Why develop a windows service further complicating the process where Windows has ‘Task Scheduler’ that’s been tested and used more broadly?”

Below is the code:

NOTE: You need to refer to the following Nuget Packages:

  • Microsoft.TeamFoundationServer.ExtendedClient
  • Microsoft.TeamFoundationServer.Client
  • Microsoft.VisualStudio.Services.Client
  • Microsoft.VisualStudio.Services.InteractiveClient
static class Program
    {
        static void Main(string[] args)
        {
            var buildoutputmodel = SetupBuildOutputModel();
            var vssconnection = new VssConnection(
                new Uri(buildoutputmodel.VsoUrl),
                new VssBasicCredential(buildoutputmodel.UserName, buildoutputmodel.Password)
                );
            var buildHttpClient = vssconnection.GetClient<BuildHttpClient>();
            //Below is my implementation of triggering multiple builds. I simply used the app.config to specify the build's ID, split each entry and validate. 
            ConfigurationManager.AppSettings["builddefinitionids"].Split(',').ToList().ForEach(
                buildid =>
                {
                    string stringoutput;
                    try
                    {
                        var id = buildid.ValidateBuildId();
                        DefinitionReference definitionReference = new DefinitionReference
                        {
                            Id = id,
                            Project = new TeamProjectReference
                            {
                                Name = buildoutputmodel.TeamProjectName
                            }
                        };
                        var build = new Build { Definition = definitionReference };
                        //This is where you trigger the build
                        var buildnumber = buildHttpClient.QueueBuildAsync(build,
                            buildoutputmodel.TeamProjectName).Result;
                        stringoutput = $"Build Triggered... \nBuild Number: {buildnumber} \nBuild Definition ID: {definitionReference.Id} \nTeam Project: {definitionReference.Project.Name}\n";
                        Console.WriteLine(stringoutput);
                        AsLogger.Info(stringoutput);
                    }
                    catch (Exception ex)
                    {
                        stringoutput = $"Exception Occurred: \n{ex.Message} \n{ex.InnerException}\n";
                        Console.WriteLine(stringoutput);
                        AsLogger.Error(stringoutput);
                    }
                });
        }

        private static BuildOutputModel SetupBuildOutputModel()
        {
            return new BuildOutputModel
            {
                UserName = ConfigurationManager.AppSettings["username"],
                Password = ConfigurationManager.AppSettings["password"],
                VsoUrl = ConfigurationManager.AppSettings["vsourl"],
                TeamProjectName = ConfigurationManager.AppSettings["teamproject"],
                BuilDefinitionName = ConfigurationManager.AppSettings["builddefinition"],
                GitRepo = ConfigurationManager.AppSettings["gitrepo"]
            };
        }
    }

Once you compile the code (.exe), simply create a scheduled task using Windows’ Task Scheduler:

TaskScheduler

Then the execution:

VSTSQueue

Advertisement

Accelerate Release Cycle: Continuous Delivery Automation…

How you can apply Automation to accelerate release cycles, improve quality, safety and governance?

Last Tuesday I participated in an online panel on the subject of CD Automation, as part of Continuous Discussions (#c9d9), a series of community panels about Agile, Continuous Delivery and DevOps. Watch a recording of the panel:

https://www.youtube.com/watch?v=2-_bb9Q-rtw

Continuous Discussions is a community initiative by Electric Cloud, which powers Continuous Delivery at businesses like SpaceX, Cisco, GE and E*TRADE by automating their build, test and deployment processes.

Below are a few insights from my contribution to the panel:

Automation != Orchestration

Automation. There’s really a big opportunity, if I want to start talking about automation, people think about many things that are in automation, but in this context, for Continuous Delivery, it is the process of taking your application, getting it deployed faster, with the right set of quality gates, with the right set of people who will help you automate that process seamlessly. So in turn, it’s more about making sure your application is being deployed more frequently, with the right quality gates in it.

Fundamentally, the process, what you look at is this: you take your application and you build it, then you test it, make sure that the build is good. When you build it, then you deploy to a set of environments – what you think your test or your dev or your staging, your environments, then you test it again. So it’s that balance of how you build your application and test it.

And being a test architect, I’m obviously an advocate of quality, and I think there’s three things people have to figure out when they start thinking about Continuous Delivery: one, the velocity of things – how fast you deploy, two, the quality side of things – what set of quality gates you need to put in place when you build and deploy an application, and the last thing you have to figure out is after you have velocity, and you have your quality, what is the cost of doing things? What set of tool sets you have to use to make all of these things work together?

We have many teams here, and there could be many things and many people, but in terms of our orchestration, what you think about is, what is the current process that prevents us from deploying and what are those manual things that we can avoid. It’s a blend of how long does it take for us to build and deploy a current application, what are the manual processes involved in doing that? Now let’s try to dissect each one of them, and make things easier, because let’s be honest, if it takes you a long time to build your application, that’s going to be a big bottleneck for you to have this Continuous Delivery pipeline.

And on top of that, is, being in the airline industry, one other thing we have to think about when we start thinking about this CD process is the idea of having the right set of guidelines for it, or standards behind it. So if it takes your application, let’s say, minutes to build, hopefully not an hour, just a couple of minutes. Are there any current tool sets, or are there any current technologies we can utilize to make that process much faster, avoid the manual effort of doing it? I’ll give you a perfect example:

Before, I think it was pretty typical for us to deploy application to utilize scripts, some PowerShell scripts, and some batch files, ” x-copy this, copy that…” And certainly now in the world of technologies you really don’t have to do that because we have tools, there’s software that allows you to build and take those bits and automatically deploy them. You just need to utilize the right task. So those are the small things in terms of orchestration that you need to start thinking about. Then on top of that, OK, then what would I need for us to make sure that this current application when we deploy it has the right set of quality gates behind it?

So, looking at those different processes, orchestration, flow, helps a lot. I would strongly suggest taking a look at what your application does and how long does it take to build, and if there’s any way that can you minimize the amount of time to build and manually do it – automate it. There are many tools out there that can help you do it. (Too many…)

We’re using the same thing. You actually have the same orchestration processes, the only thing you have to think about differently is – don’t create a process where it’s dependent on each environment, think about your application when you deploy it, it targets any environment. You can be deploying, you can be using the same processes on a test environment, or a QA environment, but you shouldn’t be developing a process that is different from one environment to the other. Your process orchestration should target any environment, so anyone can take that same process, spin up a new environment, and follow through the same processes over again, utilizing the same quality gates in between.

Also, when you start talking about testing, there’s the traditional way of testing applications where you do a lot of the manual stuff, and sometimes you have a hundred, two hundred, three hundred test cases. Sometimes you have to think smartly when you start doing CD processes. For example, which test make sense to deploy this build, and does it cover the right set of test families? And a good way to tell that is this, and if you have an analytics in your organization, think about which of those scenarios are being utilized more by your customers.

Sometimes you create test cases and automate them just for the sake of automating, but what value do you get out from those automated test cases, if some of those test cases are not even used a lot? So optimize your test, take a look at the data, what you have for your app, and try to dissect that, and think about ways to make your tests smarter, and make that part of your process.

Challenges and Checkpoints of CD Pipelines

as.com, our main website, it’s a great group of people by the way – the way we approach software is – what is the primary reason we’re doing CD? It’s because we’re doing agile development, taking one step at a time, and based on the experience we have in continuing to observe and evaluate, we have a process where you make things easier for people to integrate with each other.

For example, the tools – pick a tool that can easily be used by people, and it works well for the development, the testing and the PM team. Because, one of the challenges is, for example, (and before you even talk about the tools, I want to go one step back), it’s understanding the culture. I think that is the big thing there, to understand the culture change, what people need to do and embrace that, changes are inevitable.

I’ll give you a good example: we’re all use to doing waterfall. Back in the old days, where you hand it off to someone, and hand it off to someone… Now when you start something – everyone’s engaged at that point of view, at that point of time. When I use the set of tools, there are things you have to sacrifice in between, that you’re not accustomed to, for example, for testing, traditionally, you probably still see it from time to time, but some test teams will still create test plans, and the superior views, when you try to do CD, you try to waste some of that time away, and, so, for doing web automation, tackle it at a good point, try to do less – there is a great article out there that talks about the testing pyramid. Where at the very bottom of the base you talk about the coverage of unit testing, in the middle layer you talk about services, then the top layer would be UX automation. Less emphasis on UX automation, more thorough test in the unit test, that way you get more coverage.

So my point is there are certain teams that use different tool sets for testing, while dev and PM use a different set of for managed work, so when the time comes to integrate and do this agile CD approach, what happens is no one has time to learn these tools. A developer will not have time to understand what tool that tester is using to automate stuff, and likewise for PM and other folks, and DevOps, and infrastructure. So think about a tool that will work well for the team, as that is one of the things that made it challenging for us, and even now we’re still evaluating some of these tools. There are many tools, and given that we’re in a world with a lot of open source, you can certainly take advantage of that, and take a look at what people are using out there: tools, culture, the process, also think about, when you start, when you’re doing Continuous Delivery, sometimes you think moving so fast, velocity, orchestration – but think about the small things that really help improve your app – security, performance testing. So my suggestions is, one of key challenges there, is, make sure you have a good monitoring system in place, when you start doing your CD process. Continue to monitor what your application is doing, even though you deploy it to a test environment, because I guarantee you there are certain things that will get exposed, when you actually have the right monitoring system in place.

I’ll take it as a startup company, where you got the walk, crawl, run phase – the walk phase is like this “Let’s do it!” Let’s get the right tools sets, let’s get it working, let’s go with it, yes, you’re delivering faster, you’re learning caveats, you’re learning some of these points where, “Oh we need to go back…” but the issue is, as you move forward, you start seeing more deficiencies, and a lot more people not standardizing the things. People do things differently, in fact, since we’re talking about Continuous Delivery, I should say people are delivering software differently from one team to the other. It becomes very challenging to standardize some things, not only in the tool sets but simply on the process as well.

Because one person can do the flow differently, then you have a different set of quality gates, then you have a different set of tools. We’re still learning, but at the end of the day – let’s be honest, you will not satisfy everybody. You’re not going to satisfy every team out there, but at least try to get a consensus of small things, standard things that worked well with everybody.

We still have teams that use different tools sets, but it’s not it’s not like one team is different from the other – now we have a big organization using the same tools, and that’s a good start, and we have another organization using the same tools, that’s a good start, then we have another organization using the same tool sets – now we have a lot of people talking with each other, just take it to the next level. Because the other thing to consider is while you’re standardizing the things, the big benefit of doing that as well is cost, you’re able to save the company money by standardizing certain things that worked well for you. So now you’re hitting two birds with one stone – obviously the next thing is, people will get more enticed to collaborate, share best practices, and provide governance around certain things that worked well for us.

Being in quality, quality is my forte, I can talk about web automation the whole day – unit testing, that kind of stuff, but certain people need to realize that when you do test automation there are certain things that you use to automate things that may not work with people. But at least try to establish a standard that will work well in terms of governance of the process, what you do about it, so making things easier for tracking and auditing is the other thing you have to consider.

Tools: Tips and Tricks

I’ll just make this straight – if I had one tip, when talking about Continuous Delivery, we know we want to be Agile, we want to release faster with quality. So have everybody be accountable of quality. If you think about quality from the get go, when you start doing Continuous Delivery, you’ll start expanding of all these tool sets, you’ll be able to define a process, you’ll be able to define your orchestration. In fact, it will actually even help determine what tools you will need to use, if you start thinking about quality at the get go, as part of your process, it’s mandatory.

I can’t stress this enough, when you talk about Continuous Delivery it’s all about release, release, release… features, features, features… at the end, you’ll spend more time thinking about fixing defects or bugs.

And even tools, sometimes tools don’t necessarily have the right set of features that help you deal with quality. So you keep everybody accountable of quality, because this is very important when you start thinking about Continuous Delivery out to your production servers. You’ll be able to write down the list of things, when you start thinking about quality: monitoring, automation, tools sets, orchestration, security, performance. If I had one big tip, it’s to have everybody be accountable for quality, it’s a shared practice by everybody, not by just the test team.

Working with VSTS Rest APIs

I’ve been working with VSTS for quite some time now and wanted to share some of the sample code I’ve written to work with VSTS data. As I work with many teams, there have been requests such as getting specific metadata during and/or after build. Examples would be people wanting to get specific data from associated work-items during builds or collection level licensing information for your users. Here are I’ll tap in to specific areas:

VSTS Builds, VSTS Work-Items, VSTS GIT (Commits), VSTS User License Information

You’ll need to the following Nuget Packages:

  • Microsoft.TeamFoundationServer.ExtendedClient
  • Microsoft.TeamFoundationServer.Client
  • Microsoft.VisualStudio.Services.Client
  • Microsoft.VisualStudio.Services.InteractiveClient

Sample Code to retrieve all Builds from a given VSTS Team Project Build Definition containing associated commits and work-items:

using System;
using System.Configuration;
using System.Linq;
using System.Text;
using AAG.Test.Core.Logger;
using Microsoft.TeamFoundation.Build.WebApi;
using Microsoft.TeamFoundation.SourceControl.WebApi;
using Microsoft.VisualStudio.Services.Client;
using VSTSApi.Entities;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.TeamFoundation.WorkItemTracking.WebApi;

namespace VSTSApi
{
    class Program
    {
        private static BuildOutputModel _buildoutputmodel;

        private static string VssAccountUrl { get; set; } = ConfigurationManager.AppSettings["VssAccountUrl"];

        static void Main(string[] args)
        {
            try
            {
                StringBuilder outputStringBuilder = new StringBuilder();
                var buildoutputmodel = SetBuildOutputModel(args);
                var creds = new VssClientCredentials(false);
                creds.PromptType = CredentialPromptType.PromptIfNeeded;
                var vssConnection = new VssConnection(new Uri(buildoutputmodel.VSOUrl + "/defaultcollection"), new VssBasicCredential(buildoutputmodel.UserName, buildoutputmodel.Password));

                var buildserver = vssConnection.GetClient<BuildHttpClient>();
                var workitems = vssConnection.GetClient<WorkItemTrackingHttpClient>();
                var commititems = vssConnection.GetClient<GitHttpClient>();
                var builds = buildserver.GetBuildsAsync(_buildoutputmodel.TeamProjectName).Result;
                var targetbuilds = builds.Where(definition => definition.Definition.Name.Contains(buildoutputmodel.BuilDefinitionName));
                foreach (var build in targetbuilds)
                {
                    outputStringBuilder.AppendLine($"Name: {build.Definition.Name} : BuildID: {build.Id}");
                    var associatedcommits = buildserver.GetBuildCommitsAsync(build.Definition.Project.Name,
                        build.Id).Result;
                    if (associatedcommits.Any())
                        outputStringBuilder.AppendLine($"All Commits Made for this Build:  {Environment.NewLine} ========= {Environment.NewLine} ");
                    associatedcommits.ForEach(commit =>
                    {
                        var user = commititems.GetCommitAsync(buildoutputmodel.TeamProjectName, commit.Id, buildoutputmodel.GitRepo).Result.Author;
                        outputStringBuilder.AppendLine($"ID: {commit.Id} Committed By: {user.Name}  E-mail: {user.Email} {Environment.NewLine} Description: {commit.Message} {Environment.NewLine}");
                    });
                    var commits = associatedcommits.Select(change => change.Id);
                    var associatedworkitems = buildserver.GetBuildWorkItemsRefsAsync(commits,
                        build.Definition.Project.Name, build.Id).Result;
                    if (associatedworkitems.Any())
                        outputStringBuilder.AppendLine($"All Associated Workitems for this Build:  {Environment.NewLine} ========= {Environment.NewLine} ");
                    foreach (var wi in associatedworkitems)
                    {
                        outputStringBuilder.AppendLine($"ID : {wi.Id} : URL: {wi.Url}");
                        var workitem = workitems.GetWorkItemAsync(int.Parse(wi.Id)).Result;
                        outputStringBuilder.AppendLine($"Title : {workitem.Fields["Title"]} : Description: {workitem.Fields["Description"]}");
                    }
                    outputStringBuilder.AppendLine($"{Environment.NewLine} ========= {Environment.NewLine} ");
                }
                //}

                DumpData(outputStringBuilder.ToString(), Console.WriteLine);
                DumpData(outputStringBuilder.ToString(), print => AsLogger.Info(print));
                Console.WriteLine("Press Any Key to Continue...");
                Console.ReadKey();
            }
            catch (Exception exception)
            {
                throw new Exception($"Error with Application: {exception.Message}", exception.InnerException);
            }
        }

        static void DumpData(string stringoutput, Action<string> print)
        {
            print(stringoutput);
        }

        static BuildOutputModel SetBuildOutputModel(string[] args)
        {
            _buildoutputmodel = new BuildOutputModel
            {
                UserName = ConfigurationManager.AppSettings["username"],
                Password = ConfigurationManager.AppSettings["password"],
                VSOUrl = ConfigurationManager.AppSettings["vsourl"],
                TeamProjectName = ConfigurationManager.AppSettings["teamproject"],
                BuilDefinitionName = ConfigurationManager.AppSettings["builddefinition"],
                GitRepo = ConfigurationManager.AppSettings["gitrepo"]
            };
            return _buildoutputmodel;
        }
    }
}

Entity

namespace VSTSApi.Entities
{
    public class BuildOutputModel
    {
        public string UserName { get; set; }

        public string Password { get; set; }

        public string BuilDefinitionName { get; set; }

        public string TeamProjectName { get; set; }

        public string VSOUrl { get; set; }

        public string GitRepo { get; set; }
    }
}

Sample Code for Getting User Information/Licenses:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using VSTSAccountAdmin.Model;
using Microsoft.VisualStudio.Services.Client;
using Microsoft.VisualStudio.Services.Common;
using Microsoft.VisualStudio.Services.Identity.Client;
using Microsoft.VisualStudio.Services.Licensing;
using Microsoft.VisualStudio.Services.Licensing.Client;

namespace VSTSAccountAdmin
{
    public class Program
    {
        private static string VssAccountUrl { get; set; } = ConfigurationManager.AppSettings["VssAccountUrl"];
        private static string VssAccountName { get; set; }
        private static License VssLicense { get; set; }

        private static List<VSOUserInfo> _vsousers;



        public static void Main(string[] args)
        {
            try
            {
                _vsousers = new List<VSOUserInfo>();

                // Create a connection to the specified account.
                // If you change the false to true, your credentials will be saved.
                var creds = new VssClientCredentials(false);
                creds.PromptType = CredentialPromptType.PromptIfNeeded;
                var vssConnection = new VssConnection(new Uri(VssAccountUrl), creds);

                // We need the clients for tw4o services: Licensing and Identity
                var licensingClient = vssConnection.GetClient<LicensingHttpClient>();
                var identityClient = vssConnection.GetClient<IdentityHttpClient>();

                var entitlements = licensingClient.GetAccountEntitlementsAsync().Result;
                IEnumerable<AccountEntitlement> accountEntitlements = entitlements as IList<AccountEntitlement> ??
                                                                      entitlements.ToList();
                var userIds = accountEntitlements.Select(entitlement => entitlement.UserId).ToList();
                var users = identityClient.ReadIdentitiesAsync(userIds).Result.ToDictionary(item => item.Id);
                foreach (var entitlement in accountEntitlements)
                {
                    var user = users[entitlement.UserId];
                    _vsousers.Add(new VSOUserInfo()
                    {
                        DisplayName = user.DisplayName,
                        LastAccessDate = entitlement.LastAccessedDate,
                        License = entitlement.License.ToString().ToLowerInvariant(),
                        UserID = entitlement.UserId
                    });
                    var stringoutput =
                        $"{Environment.NewLine}Name: {user.DisplayName}, UserId: {entitlement.UserId}, License: {entitlement.License}.";
                    Console.WriteLine(stringoutput);
                }
            }
            catch (Exception ex)
            {
                throw new ArgumentException(ex.Message, ex.InnerException);
            }

        }
    }
}

Entity:

namespace VSTSAccountAdmin.Model
{
    public class VSOUserInfo
    {
        public string DisplayName { get; set; }

        public Guid UserID { get; set; }

        public string License { get; set; }

        public DateTimeOffset LastAccessDate { get; set; }

    }
}

MSTEST TIP: Extension Method/s for TestContext while working on Data Driven Tests.

A very common scenario that developers/testers encounter when writing data driven tests is to print input parameters from data sources. Data driven test can encompass many data sources (sql, .csv, excel, .xml, etc…) and Yes, I was even able to extend MSTests functionality by passing my own custom collection:

MSTEST: EXTENDING DATA DRIVEN TESTS TO USE IENUMERABLE<OBJECT> AS THE DATA SOURCE

https://dondeetan.com/2016/02/18/mstest-extending-data-driven-tests-to-use-ienumerableobject-as-the-data-source/

While there are many implementations to solve this, I wanted to provide (in my opinion) the most easiest and efficient way possible for developers and testers to print out input parameters without leaving the context during test development. Thus, extension methods. What are extension methods? In a high level, extension methods allows you to “extend” or “add” methods to existing types (even types built in the .Net framework) without creating a new class from the derived type. Click here on Extension Methods (C# Programming Guide) more information.

Now that we have good understanding of extension methods, then why not extend the TestContext class which contains all information during test execution? TestContext also contains information around the datarow and datacolumn passed during data driven testing. It’s a simple as calling the datarow during runtime.

TestContext.DataRow["Description"].ToString();

TestConext.DataRow has a property “Table” which you can get all columns during the instance of test execution. At this point, all you need to do is extend TestContext to print input parameters.

public static class Helper
    {
        public static void PrintInput(this TestContext testContext)
        {
            Console.WriteLine($"Total Input Fields: {testContext.DataRow.Table.Columns.Count}");
            var columns = testContext.DataRow.Table.Columns;
            for (var i = 0; i < columns.Count; i++)
            {
                Console.WriteLine($"Field: {columns[i].ColumnName} Value: {testContext.DataRow[i]}");
            }
        }
    }

In your Test Method, all you have to do is just call one line of code:

TestContext.PrintInput();

And the results from that instance test execution:

image

Great way to show input parameters for debugging purposes!

MSTEST: Extending Data Driven Tests to use IENUMERABLE<Object> as the Data Source

One great feature that I like in NUnit is the capability to use collection types for data driven tests. Meaning, you don’t have to open up an external data source connection, pull data and use it to drive parameters for your tests. With a simple attribute in NUnit, you can drive tests as indicated here:

TestCaseSourceAttribute

http://www.nunit.org/index.php?p=testCaseSource&r=2.5.3

NUnit implementation allows you to enumerate from a collection to data drive your tests. MSTest has the same extensibility and is outlined in the following blog:

Extending the Visual Studio Unit Test Type

http://blogs.msdn.com/b/vstsqualitytools/archive/2009/09/04/extending-the-visual-studio-unit-test-type-part-1.aspx

From this blog, I was able to go through the steps and process how MSTest invokes and passes objects in a test method. When MSTest executes, the flow goes through:

  1. TestClassExtensionAttribute calls : GetExecution()
  2. TestExtensionExecution calls : CreateTestMethodInvoker(TestMethodInvokerContext context)
  3. ITestMethodInvoker calls : Invoke(params object[] parameters)

image

Throughout this process, you can use custom attributes and utilize attribute properties for passing in test data. Its best that you use custom attributes in the ITestMethodInvoker.Invoke()

In my solution, I want to develop a fast way of invoking IEnumerable<object> as my test data. In this case, I’ll consume custom attributes to provide a classname and methodname to return test data through reflection. I’ll then use that in ITestMethodInvoker.Invoke() to enumerate objects for my tests.

  • ClassName: class holding the method to generate test data
  • DataSourceName: method within the class that generates any test data

Project Setup:

Make sure that you have the following references in your project:

  • Microsoft.VisualStudio.QualityTools.Common.dll
  • Microsoft.VisualStudio.QualityTools.UnitTestFramework
  • Microsoft.VisualStudio.QualityTools.Vsip.dll

These assemblies are included as part of the .Net framework. Simply browse in the references section in your project

The Custom Attribute:

    [global::System.AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
    public class EnumurableDataSourceAttribute : Attribute
    {
        public string DataSourceName { get; set; }
        public string ClassName { get; set; }


        public EnumurableDataSourceAttribute(string className, string dataSourceName)
        {
            this.DataSourceName = dataSourceName;
            this.ClassName = className;
        }
    } 

Test Class Implementation:

[Serializable]
    public class TestClassCollectionAttribute : TestClassExtensionAttribute
    {
        public override Uri ExtensionId => new Uri("urn:TestClassAttribute");

        public override object GetClientSide()
        {
            return base.GetClientSide();
        }

        public override TestExtensionExecution GetExecution()
        {
            return new TestExtension();
        }
    }

Test Extension:

public class TestExtension : TestExtensionExecution
    {
        public override void Initialize(TestExecution execution)
        {
            
        }

        public override ITestMethodInvoker CreateTestMethodInvoker(TestMethodInvokerContext context)
        {
            return new TestInvokerMethodCollection(context);
        }

        public override void Dispose()
        {
            
        }
    }

Test Method Invoker:

public class TestInvokerMethodCollection : ITestMethodInvoker
    {
        private readonly TestMethodInvokerContext _context;
        
        public TestInvokerMethodCollection(TestMethodInvokerContext context)
        {
            Debug.Assert(context != null);
            _context = context;
        }
        public TestMethodInvokerResult Invoke(params object[] parameters)
        {
            Trace.WriteLine($"Begin Invoke:Test Method Name: {_context.TestMethodInfo.Name}");
            Assembly testMethodAssembly = _context.TestMethodInfo.DeclaringType.Assembly;
            object[] datasourceattributes = _context.TestMethodInfo.GetCustomAttributes(typeof (EnumurableDataSourceAttribute), false);
            Type getclasstype = testMethodAssembly.GetType(((EnumurableDataSourceAttribute)datasourceattributes[0]).ClassName);
            MethodInfo getmethodforobjects = getclasstype.GetMethod(((EnumurableDataSourceAttribute) datasourceattributes[0]).DataSourceName);
            /*
            Use the line below if there are parameters that needs to be passed to the method. 
            ParameterInfo[] methodparameters = getmethodforobjects.GetParameters();
            To instantiate a new concreate class
            object classInstance = Activator.CreateInstance(getclasstype, null);
            Invoke(null,null) = The first null parameter specifies whether it's a static class or not. For static, leave it null
            IEnumerable<object> enmeruableobjects = getmethodforobjects.Invoke(classInstance, null) as IEnumerable<object>;
            */
            IEnumerable<object> enmeruableobjects = getmethodforobjects.Invoke(null, null) as IEnumerable<object>;
            var testresults = new TestResults();
            //This is where each object will be enumarated for the test method. 
            foreach (var obj in enmeruableobjects)
            {
                testresults.AddTestResult(_context.InnerInvoker.Invoke(obj), new object[1] { obj });
            }           
            var output = testresults.GetAllResults();
            _context.TestContext.WriteLine(output.ExtensionResult.ToString());
            return output;
        }
    }

Test Project Setup:

Once, you’ve successfully build your assembly project (custom TestClass attribute), you need to register the custom extension class in your local machine. This is a custom test assembly/adapter so, we’ll need to:

  • Make changes to the registry
  • Add the compiled assembly in the install directory for your VS version. In my case, I’m using Visual Studio 2015 so your custom assemblies will be copied to:  C:\Program Files (x86)\Microsoft Visual Studio 14.0\Common7\IDE\PrivateAssemblies

Luckily, there’s a batch script that lets you do all of these steps. The only thing you need to do is:

  • Change the version of VS to your working VS edition
  • Change the assembly namespace and class reference

The deployment script: (You can also download the deployment script from this blog. Scroll at the bottom of the blog post)

http://blogs.msdn.com/b/qingsongyao/archive/2012/03/28/examples-of-mstest-extension.aspx?CommentPosted=true

@echo off
::------------------------------------------------
:: Install a MSTest unit test type extension
:: which defines a new test class attribute
:: and how to execute its test methods and
:: interpret results.
::
:: NOTE: Only VS needs this and the registration done; the xcopyable mstest uses
:: TestTools.xml virtualized registry file updated which we already have done in sd
::------------------------------------------------

setlocal

:: All the files we need to copy or register are realtive to this script folder
set extdir=%~dp0

:: Get 32 or 64-bit OS
set win64=0
if not "%ProgramFiles(x86)%" == "" set win64=1
if %win64% == 1 (
    set vs14Key=HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\14.0
) else (
    set vs14Key=HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\14.0
)

:: Get the VS installaton path from the Registry
for /f "tokens=2*" %%i in ('reg.exe query %vs14Key% /v InstallDir') do set vsinstalldir=%%j


:: Display some info
echo.
echo =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-==-=-=-=-=-=-=-=-=-=-
echo Please ensure that you are running with adminstrator privileges
echo to copy into the Visual Studio installation folder add keys to the Registry.
echo Any access denied messages probably means you are not.
echo =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-==-=-=-=-=-=-=-=-=-=-
echo.
echo 64-bit OS: %win64%
echo Visual Studio 14.0 regkey:   %vs14Key%
echo Visual Studio 14.0 IDE dir:  %vsinstalldir%

::
:: Copy the SSM test type extension assembly to the VS private assemblies folder
::

set extdll=AAG.Test.Core.CustomTestExtenstions.dll
set vsprivate=%vsinstalldir%PrivateAssemblies
echo Copying to VS PrivateAssemblies: %vsprivate%\%extdll%
copy /Y %extdir%%extdll% "%vsprivate%\%extdll%"

::
:: Register the extension with mstest as a known test type
:: (SSM has two currently, both are in the same assembly)
::

echo Registering the unit test types extensions for use in VS' MSTest

:: Keys Only for 64-bit
if %win64% == 1 (
    set vs14ExtKey64=HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\14.0\EnterpriseTools\QualityTools\TestTypes\{13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b}\TestTypeExtensions
    set vs14_configExtKey64=HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\14.0_Config\EnterpriseTools\QualityTools\TestTypes\{13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b}\TestTypeExtensions
)

:: Keys for both 32 and 64-bit
set vs14ExtKey=HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\14.0\EnterpriseTools\QualityTools\TestTypes\{13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b}\TestTypeExtensions
set vs14_configExtKey=HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\14.0_Config\EnterpriseTools\QualityTools\TestTypes\{13cdc9d9-ddb5-4fa4-a97d-d965ccfc6d4b}\TestTypeExtensions

:: Register the TestClassCollectionAttribute
set regAttrName=TestClassCollectionAttribute
set regProvider="AAG.Test.Core.CustomTestExtenstions.TestClassCollectionAttribute, AAG.Test.Core.CustomTestExtenstions"
if  %win64% == 1 (
    reg add %vs14ExtKey64%\%regAttrName%        /f /v AttributeProvider /d %regProvider%
    reg add %vs14_ConfigExtKey64%\%regAttrName% /f /v AttributeProvider /d %regProvider%
)
reg add %vs14ExtKey%\%regAttrName%        /f /v AttributeProvider /d %regProvider%
reg add %vs14_ConfigExtKey%\%regAttrName% /f /v AttributeProvider /d %regProvider%

:eof
endlocal
exit /b %errorlevel%

Creating the tests in VS:

In your test project, add a reference to the custom MSTest Assemblies

image

In your tests, make sure to use the custom test class and enumerator attribute that we previously defined. Here’s a sample of these test methods that uses different IEnumerable objects.

[TestClassCollection]
    public class MethodCollectionTests
    {

        public TestContext TestContext { get; set; }

        [TestInitialize()]
        public void TestInit()
        {
        }

        [TestMethod]
        [EnumurableDataSourceAttribute("CustomTestExtenstions.Tests.Helper.Helper", "Get5Employees")]
        public void Verify5Employees(Employee employee)
        {
            Assert.IsFalse(String.IsNullOrEmpty(employee.Displayname));
            Console.WriteLine($"Employee FirstName: {employee.Displayname}");
            TestContext.WriteLine($"Test Case Passed for {TestContext.TestName} with Data: {employee.Displayname}");
        }

        [TestMethod]
        [EnumurableDataSourceAttribute("CustomTestExtenstions.Tests.Helper.Helper", "Get20Employees")]
        public void Verify20Employees(Employee employee)
        {
            Assert.IsFalse(String.IsNullOrEmpty(employee.Displayname));
            Console.WriteLine($"Employee FirstName: {employee.Displayname}");
            TestContext.WriteLine($"Test Case Passed for {TestContext.TestName} with Data: {employee.Displayname}");
        }

        [TestMethod]
        [EnumurableDataSourceAttribute("CustomTestExtenstions.Tests.Helper.Helper", "Get5Cars")]
        public void Verify5Cars(Car car)
        {
            Assert.IsNotNull(car);
            Assert.IsFalse(String.IsNullOrEmpty(car.Description));
            Console.WriteLine($"Car Info: Type: {car.CarType} Cost: {car.Cost.ToString("C")}");
            TestContext.WriteLine($"Test Case Passed for {TestContext.TestName} with Data: {car.CarType} with Id: {car.Id}");
        }

        [TestMethod]
        [EnumurableDataSourceAttribute("CustomTestExtenstions.Tests.Helper.Helper", "Get13Cars")]
        public void Verify13Cars(Car car)
        {
            Assert.IsNotNull(car);
            Assert.IsFalse(String.IsNullOrEmpty(car.Description));
            Console.WriteLine($"Car Info: Type: {car.CarType} Cost: {car.Cost.ToString("C")}");
            TestContext.WriteLine($"Test Case Passed for {TestContext.TestName} with Data: {car.CarType} with Id: {car.Id}");
        }

        [TestMethod]
        [EnumurableDataSourceAttribute("CustomTestExtenstions.Tests.Helper.Helper", "Get5EmployeesWithCars")]
        public void Verify5EmployeesWithCars(EmployeeWithCar employeeWithCar)
        {
            Assert.IsNotNull(employeeWithCar);
            Assert.IsFalse(String.IsNullOrEmpty(employeeWithCar.Id));
            Assert.IsNotNull(employeeWithCar.Car);
            Assert.IsNotNull(employeeWithCar.Employee);
            TestContext.WriteLine($"Test Case Passed for {TestContext.TestName} with Data: Name: {employeeWithCar.Employee.Displayname} Car: {employeeWithCar.Car.CarType} with Id: {employeeWithCar.Employee.Id}");
        }

        [TestMethod]
        [EnumurableDataSourceAttribute("CustomTestExtenstions.Tests.Helper.Helper", "Get10SequentialInts")]
        public void Verify10SequentialInts(int intcurrent)
        {
            Assert.IsInstanceOfType(intcurrent, typeof(int));
            TestContext.WriteLine($"Current int Value: {intcurrent}");
        }

        [TestMethod]
        [EnumurableDataSourceAttribute("CustomTestExtenstions.Tests.Helper.Helper", "Get5StringObjects")]
        public void VerifyGet5StringObjects(string stringcurrent)
        {
            Assert.IsInstanceOfType(stringcurrent, typeof(string));
            TestContext.WriteLine($"Current string Value: {stringcurrent}");
        }
    }

The Helper class defines the helper methods to generate test data:

public static class Helper
    {
        public static IEnumerable<object> Get5Employees()
        {
            var employees = GenerateData.GetEmployees(5);
            return (IEnumerable<object>) employees;
        }

        public static IEnumerable<object> Get20Employees()
        {
            var employees = GenerateData.GetEmployees(20);
            return (IEnumerable<object>) employees;
        }
        public static IEnumerable<object> Get5Cars()
        {
            var cars = GenerateData.GetCars(5);
            return (IEnumerable<object>) cars;
        }

        public static IEnumerable<object> Get13Cars()
        {
            var cars = GenerateData.GetCars(13);
            return (IEnumerable<object>)cars;
        }

        public static IEnumerable<object> Get5EmployeesWithCars()
        {
            var cars = GenerateData.GetCars(5);
            var employees = GenerateData.GetEmployees(5);
            var employeeswithcars = new List<EmployeeWithCar>();
            for (int i = 0; i < cars.Count; i++)
            {
                var employeewithcar = new EmployeeWithCar
                {
                    Car = cars[i],
                    Employee = employees[i]
                };
                employeeswithcars.Add(employeewithcar);
            }
            return (IEnumerable<object>)employeeswithcars;
        }

        public static IEnumerable<object> Get10SequentialInts()
        {
            var intobjects = new int[] {1, 2, 3, 4, 5,6,7,8,9, 10};
            return (IEnumerable<object>) intobjects.Cast<object>();
        }

        public static IEnumerable<object> Get5StringObjects()
        {
            var stringobjects = new string[] {"String1", "String2", "String3", "String4", "String5" };
            return (IEnumerable<object>)stringobjects.Cast<object>();
        }
    }

The Execution results!

image

And the output for each of the result!

image

Enabling Targeted Environment Testing during Continuous Delivery (Release Management) in VSTS

In my previous post “Continuous Delivery using VSO Release Manager with Selenium Automated Tests on Azure Web Apps (PaaS)”, I’ve walked through the steps on how to enable continuous delivery by releasing builds to multiple environments. One caveat that I didn’t focus on is taking the same tests and running it against those target environments.

In this post, I’ll walk you through the steps on enabling or running the same tests targeting those environments that you have included as part of your continuous delivery pipeline. You’re basically taking the same tests however passing in different parameters such as the target URL and/or browser to run.

In a high level, these are the changes. The solution only requires:

· Create runsettings file and add your parameters.

· Modify your code to call the parameters from the .runsettings file

· Changes to the “Test Steps” in VSTS release or build

Create runsettings file

In Visual Studio, you have the ability to generate a “testsettings” file however not a “runsettings” file. What’s the difference between both file formats? Both can be used as a reference for executing tests. However the “runsettings” file is more optimized to be used for running Unit Tests. In this case our tests are unit tests.

For more information see: Configure unit tests by using a .runsettings file

https://msdn.microsoft.com/en-us/library/jj635153.aspx

Essentially:

1) Create an xml file with the extension of: .runsettings (e.g. ContinousDeliveryTests.runsettings)

2) Replace the contents of the file with the following:

<?xml version="1.0" encoding="utf-8"?>

<RunSettings>

   <TestRunParameters>

      <Parameter name="EnvURL" value="http://XXXXX.azurewebsites.net" />

      <Parameter name="Browser" value="headless" />

   </TestRunParameters>

  <!--<RunConfiguration> We will use this later so we can run multiple threads for tests

    <MaxCpuCount>4</MaxCpuCount>

  </RunConfiguration>-->

</RunSettings>

3) Save the file to your source control repository. In my case GIT in VSTS.

Modify your code to call the params from the .runsettings file

In your unit tests where you specify the URL, apply the following changes:

var envUrl = TestContext.Properties["EnvURL"].ToString();

At this point, you can refer the envUrl variable in your code for navigating through your tests.

Changes to the “Test Steps” in VSTS release or build

In VSTS Release Manager, modify the test steps to:

NOTE: As part of your build definition, ensure that you upload your runsettings file as part of your artifacts directory. Here’s a snippet in my build definition step:

CD1

Visual Studio Test step in Release Manager:

For each environment that you deploy your application, modify the Visual Studio Test Step to:

– Specify the runsettings file. You can browse through your build artifacts provided you’ve uploaded your runsettings file as part of your build artifacts

– Override the parameters with the correct value for your environment URL

image

One you make the following changes, your tests will execute targeting those environments specified in your continuous delivery pipeline.

Continuous Testing in VSO using Selenium WebDriver and VSO Test Agents (On-Premise)

This post walks you through on how to implement continuous testing using the following technologies:

  • Using Config Transform to configure your tests to run multiple browsers during runtime
  • Selenium WebDriver – Automation UX framework to drive UX Testing
  • VSO Build – ALM (Application Lifecycle Management) tool suite for storing code (via GIT), creating the build definition (Build) and configuring On-Premise machines as Test Agents

What is the Config transform? This will allow you to change the configuration settings for your app or web configuration files. When you use Selenium WebDriver, you have the option to run your tests using either Chrome, Internet Explorer, FireFox even PhantomJS (Headless testing). See the sample C# code below to generate the correct web driver instance through configuration settings. The method takes in a string value with a default browser of Internet Explorer. Note that sample code below uses try catches to log exception using Log4Net. You can ignore this but just re-use the code for creating the proper Selenium WebDriver

NOTE: If you’re not familiar on using Selenium to run automated UX testing, see these samples below:http://docs.seleniumhq.org/docs/03_webdriver.jsp

public static T OpenBrowser<T>(string browser = "iexplore", bool useExistingBrowser = false, bool usebrowserstack=false)
  {
            string error = string.Empty;

            try
            {
                if (!string.IsNullOrEmpty(browser))
                {
                    browser = browser.ToLower();
                    switch (browser)
                    {
                        case "firefox":
                            IsFirefox = true;
                            return (T)Convert.ChangeType(ReturnDriver<FirefoxDriver, FirefoxProfile>(useExistingBrowser, ref _firFoxDriver,
                                Firefoxprofile), typeof(FirefoxDriver));
                        case "chrome":
                            IsChrome = true;
                            return (T)Convert.ChangeType(ReturnDriver<ChromeDriver, ChromeOptions>(useExistingBrowser, ref _chromeDriver,
                                Chromeprofile), typeof(ChromeDriver));
                        case "headless":
                            return (T)Convert.ChangeType(ReturnDriver<PhantomJSDriver, PhantomJSOptions>(useExistingBrowser, ref _phantomjsdriver,
                                phantomjsoptions), typeof(PhantomJSDriver));
                    }
                }

                // ie (internet explorer)
                IEprofile.IgnoreZoomLevel = true;
                IEprofile.EnsureCleanSession = true;
                IsIE = true;
                return (T)Convert.ChangeType(ReturnDriver<InternetExplorerDriver, InternetExplorerOptions>(useExistingBrowser, ref _internetDriver, IEprofile), typeof(InternetExplorerDriver));              
            }
            catch (Exception ex)
            {
                if (string.IsNullOrWhiteSpace(ex.Message))
                    _Testlog.Info(MethodBase.GetCurrentMethod().Name + " Results = Success");
                else
                    _Testlog.Info(MethodBase.GetCurrentMethod().Name + " Results = " + ex.Message);
                throw new ArgumentException(ex.Message, ex.InnerException);
            }
            return default(T);
  }

The Generic Type to return the driver is:

NOTE: I’ve hardcoded the value of T as IWebDriver instance to maximize the browser. You don’t have to do this but since we’re using Selenium WebDriver, I’ll just embed it on this method. You can also change the T type to any UX automation.

private static T ReturnDriver<T, TT>(bool existing, ref T driver, TT profile)
        {
            string error = string.Empty;
            
            try
            {
                if (driver == null || existing == false)
                {
                    driver = (T)Activator.CreateInstance(typeof(T), profile);
                }
                ((IWebDriver)driver).Manage().Window.Maximize();
            }
            catch (Exception ex)
            {
                if (string.IsNullOrWhiteSpace(ex.Message))
                    _Testlog.Info(MethodBase.GetCurrentMethod().Name + " Results = Success");
                else
                    _Testlog.Info(MethodBase.GetCurrentMethod().Name + " Results = " + ex.Message);
                throw new ArgumentException(ex.Message, ex.InnerException);
            }
            return driver;
        }

This method takes in a string param. You can pass the value from the config file and if you use config transforms, you can change the run behavior of the browser just by passing in the correct app config value.

Almost forgot! Download the config transform here:  Configuration Transform
https://visualstudiogallery.msdn.microsoft.com/579d3a78-3bdd-497c-bc21-aa6e6abbc859

Here’s a look at what the config transform would look like in your project:

clip_image002

The particular key that I used and configured would look very similar to this (this is from, App.Chrome.config):

<!-- valid options for Browsers are: chrome , firefox, iexplore, headless -->
    <add key="Browser" value="chrome"  xdt:Transform="Replace" xdt:Locator="Match(key)" />

Source Control is GIT in VSO

Given that we’re using VSO as our ALM suite, we’ve opted to use GIT as our backend source control system. This make it’s easier for configuring your build definition since all source code is stored in VSO

Configuring On-Premise VSO Test Agents

The next step is to ensure that you have On-Premise Test Agents that VSO can talk to and execute the tests. For this follow the steps on this article to configure your VSO Test Agents. Note that in VSO, build and test agents are now in the same machine. Also, note that this article talk about On-Premise TFS HOWEVER, the same applies in VSO. You have to go to your VSO instance (https://xxxx.visualstudio.com) and configure your agent pools. The rest is shown belowhttps://msdn.microsoft.com/Library/vs/alm/Build/agents/windows

Let’s do a quick check!

  • Automated UX Tests have been developed in and uses configuration settings to drive the browser driver – Check!
  • Installed Configuration Transform and configured my test project with all appropriate config settings – Check!
  • Test Code stored in GIT (VSO) – Check!
  • On-Premise VSO Test Agents Configured – Check!

Once all of these have been verified then the final step would be stitching and putting all of these together via VSO Build.

Configuring VSO Build for Continuous Testing:

Step 1: Configure Build Variables:

clip_image004

 

 

Step 2: Create Visual Studio Build step:

First step is to build the test solution/project. This is pretty straightforward. On the Solution textbox, browse through the .sln file that you’ve checked in source control (in this case GIT)

clip_image006

Step 3: Deploy the Test Agent on the On-Premise Test Machines:

NOTE: Before completing this step, ensure that you’ve properly configured your test machines. Follow the article below. This articles walks you through creating machines groups for your team project:

http://blogs.msdn.com/b/charles_sterling/archive/2015/07/05/integrating-testing-into-the-ci-and-cd-pipelines.aspx

The key here is ensuring that:

· You have 1 test machine group that has all the test agents configured correctly

· The $(TestUserName) must have admin privileges on the test agents

clip_image008

Step 4: Copy The Compiled Test Assemblies To The On-Premise Test Agents:

The key to this step is ensure that you copy the compiled test assemblies to the right location. $(Build.Repository.LocalPath) is the directory from the build server where “Destination Folder” will copy the test assemblies to the target test agent machine.

clip_image010

Step 5: Execute the Tests:

Nothing special here. Just make sure that you’re reference the correct Test Drop Location. Just copy the Destination Folder from the previous step:

clip_image012

If you configured it correctly, you should get a successful build! Now the result of the build depends on the Pass/Fail results of the tests. In this case, I’ve intentionally Failed 1 automated test to trigger a build failure that coincides to a failing test. Passing the fail test in the future will result to a complete build

clip_image014

clip_image016

In your VSO Home or Team (Dashboard) Pages, you can pin the build trending charts to see PASS/FAIL Patterns for your UX Automation

clip_image017

Using OpenID to authenticate in MVC via Azure AD (Manual Steps)

Title says it all, we have some MVC apps using Azure AD via WSFed and want to convert using OpenID auth. While WSFED works well, we wanted to take a simple approach of using OpenID through Azure AD. These are the steps to either convert from WSFED or add OpenID in existing MVC Apps for Authentication.

I assume that you already have an application registered in Azure Active Directory for your website to use for authenticating AD users. If not, the first step is to create an Application in Azure Active Directory for your website to use to authenticate AD users. To do this:

    1. Sign in to the Azure Management Portal (http://azure.microsoft.com).
    2. Click on the Active Directory icon on the left menu, and then click on the desired directory.
    3. On the top menu, click Applications. If no apps have been added to your directory, this page will only show the Add an App link. Click on the link, or alternatively you can click on the Add button on the command bar.
    4. On the What do you want to do page, click on the link to Add an application my organization is developing.
  • On the Tell us about your application page, you must specify a name for your application as well as indicate the type of application you are registering with Azure AD. You can choose from a web application and/or web API (default) or native client application which represents an application that is installed on a device such as a phone or computer. For this guide, make sure to select Web Application and/or Web API
  1. Once finished, click the arrow icon on the bottom-right corner of the page.
  2. On the App properties page, provide the Sign-on URL (URL for your web application) and App ID URI (Unique URI for your application – Usually it’s a combination or your AD domain/application. Example: http://www.domain.com/mywebsite.somedomain.com) for your web application then click the checkbox in the bottom-right hand corner of the page.
  3. Your application has been added, and you will be taken to the Quick Start page for your application.
  4. Click on the “Configure” Tab. Generate a Key for your client access and write down the following information:
      1. CLIENT ID:
      2. KEY (You generate a Key by clicking on the Save Button on the configure tab)
      3. APP ID URI

     

  5. Federation Metadata Document (You can get this information by click on “VIEW ENDPOINTS” at the bottom section of the Configure tab)

 Capture

Enable SSL on your Dev Machines

With OpenID, you need to have your MVC app enabled with SSL. In your development environment, you can set this by going to the properties of the MVC app, select “Web” on the left navigation and type “https” on the project URL box:

SSL

Add OpenID and OWIN nuget packages to your MVC Application:

  • Microsoft.IdentityModel.Protocol.Extensions
  • System.IdentityModel.Tokens.Jwt
  • Microsoft.Owin.Security.OpenIdConnect
  • Microsoft.Owin.Security.Cookies
  • Microsoft.Owin.Host.SystemWeb
  • Active Directory Authentication Library

Create a class Startup.Auth.cs in the App_Start folder

Replace the code from below: Be sure to take the whole class definition!

Namespace references:


using Microsoft.IdentityModel.Clients.ActiveDirectory;

using Microsoft.Owin.Security;

using Microsoft.Owin.Security.Cookies;

using Microsoft.Owin.Security.OpenIdConnect;

using Owin;


public partial class Startup

   {

       //

       // The Client ID is used by the application to uniquely identify itself to Azure AD.

       // The App Key is a credential used to authenticate the application to Azure AD. Azure AD supports password and certificate credentials.

       // The Metadata Address is used by the application to retrieve the signing keys used by Azure AD.

       // The AAD Instance is the instance of Azure, for example public Azure or Azure China.

       // The Authority is the sign-in URL of the tenant.

       // The Post Logout Redirect Uri is the URL where the user will be redirected after they sign out.

       //

       private static string clientId = ConfigurationManager.AppSettings[&quot;ida:ClientId&quot;];

       private static string appKey = ConfigurationManager.AppSettings[&quot;ida:AppKey&quot;];

       private static string aadInstance = ConfigurationManager.AppSettings[&quot;ida:AADInstance&quot;];

       private static string tenant = ConfigurationManager.AppSettings[&quot;ida:Tenant&quot;];

       private static string postLogoutRedirectUri = ConfigurationManager.AppSettings[&quot;ida:PostLogoutRedirectUri&quot;];

       public static readonly string Authority = String.Format(CultureInfo.InvariantCulture, aadInstance, tenant);

       // This is the resource ID of the AAD Graph API. We'll need this to request a token to call the Graph API.

       string graphResourceId = ConfigurationManager.AppSettings[&quot;ida:GraphUrl&quot;];

       public void ConfigureAuth(IAppBuilder app)

       {

           app.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);

           app.UseCookieAuthentication(new CookieAuthenticationOptions());

           app.UseOpenIdConnectAuthentication(

               new OpenIdConnectAuthenticationOptions

               {

                   ClientId = clientId,

                   Authority = Authority,

                   PostLogoutRedirectUri = postLogoutRedirectUri,

                  Notifications = new OpenIdConnectAuthenticationNotifications()

                   {

                       //

                       // If there is a code in the OpenID Connect response, redeem it for an access token and refresh token, and store those away.

                       //

                       AuthorizationCodeReceived = (context) =&gt;

                       {

                           var code = context.Code;

                           ClientCredential credential = new ClientCredential(clientId, appKey);

                           string userObjectID = context.AuthenticationTicket.Identity.FindFirst(

                                   &quot;http://schemas.microsoft.com/identity/claims/objectidentifier&quot;).Value;

                           AuthenticationContext authContext = new AuthenticationContext(Authority, new NaiveSessionCache(userObjectID));

                           AuthenticationResult result = authContext.AcquireTokenByAuthorizationCode(

                              code, new Uri(HttpContext.Current.Request.Url.GetLeftPart(UriPartial.Path)), credential, graphResourceId);

                           AuthenticationHelper.token = result.AccessToken;

                           return Task.FromResult(0);

                       }

                   }

               });

       }

   }

Create Utility classes

In the project, create a new folder called Utils, create a class AuthenticationHelper.cs. Replace the code from below. Be sure to take the whole class definition!

References


using Microsoft.Azure.ActiveDirectory.GraphClient;


internal class AuthenticationHelper

   {

       public static string token;

       /// &lt;summary&gt;

       ///     Async task to acquire token for Application.

       /// &lt;/summary&gt;

       /// &lt;returns&gt;Async Token for application.&lt;/returns&gt;

       public static async Task&lt;string&gt; AcquireTokenAsync()

       {

           if (token == null || token.IsEmpty())

           {

               throw new Exception(&quot;Authorization Required.&quot;);

           }

           return token;

       }

       /// &lt;summary&gt;

       ///     Get Active Directory Client for Application.

       /// &lt;/summary&gt;

       /// &lt;returns&gt;ActiveDirectoryClient for Application.&lt;/returns&gt;

       public static ActiveDirectoryClient GetActiveDirectoryClient()

       {

           Uri baseServiceUri = new Uri(Constants.ResourceUrl);

           ActiveDirectoryClient activeDirectoryClient =

               new ActiveDirectoryClient(new Uri(baseServiceUri, Constants.TenantId),

                   async () =&gt; await AcquireTokenAsync());

           return activeDirectoryClient;

       }

   }

In the Utils folder, create a class Constants.cs. Replace the code from below. Be sure to take the whole class definition!


internal class Constants

   {

       public static string ResourceUrl = ConfigurationManager.AppSettings[&quot;ida:GraphUrl&quot;];

       public static string ClientId = ConfigurationManager.AppSettings[&quot;ida:ClientId&quot;];

       public static string AppKey = ConfigurationManager.AppSettings[&quot;ida:AppKey&quot;];

       public static string TenantId = ConfigurationManager.AppSettings[&quot;ida:TenantId&quot;];

       public static string AuthString = ConfigurationManager.AppSettings[&quot;ida:Auth&quot;] +

                                         ConfigurationManager.AppSettings[&quot;ida:Tenant&quot;];

       public static string ClientSecret = ConfigurationManager.AppSettings[&quot;ida:ClientSecret&quot;];

   }

In the Utils folder, create three new classes called NaiveSessionCache.cs. Replace the code from below. Be sure to take the whole class definition!

References:


using Microsoft.IdentityModel.Clients.ActiveDirectory;


public class NaiveSessionCache : TokenCache

   {

       private static readonly object FileLock = new object();

       private readonly string CacheId = string.Empty;

       private string UserObjectId = string.Empty;

       public NaiveSessionCache(string userId)

       {

           UserObjectId = userId;

           CacheId = UserObjectId + &quot;_TokenCache&quot;;

           AfterAccess = AfterAccessNotification;

           BeforeAccess = BeforeAccessNotification;

           Load();

       }

       public void Load()

       {

           lock (FileLock)

           {

               if (HttpContext.Current != null)

               {

                   Deserialize((byte[])HttpContext.Current.Session[CacheId]);

               }

           }

       }

        public void Persist()

       {

           lock (FileLock)

           {

               // reflect changes in the persistent store

               HttpContext.Current.Session[CacheId] = Serialize();

               // once the write operation took place, restore the HasStateChanged bit to false

               HasStateChanged = false;

           }

       }

       // Empties the persistent store.

       public override void Clear()

       {

           base.Clear();

           HttpContext.Current.Session.Remove(CacheId);

       }

       public override void DeleteItem(TokenCacheItem item)

       {

           base.DeleteItem(item);

           Persist();

       }

       // Triggered right before ADAL needs to access the cache.

       // Reload the cache from the persistent store in case it changed since the last access.

       private void BeforeAccessNotification(TokenCacheNotificationArgs args)

       {

           Load();

       }

       // Triggered right after ADAL accessed the cache.

       private void AfterAccessNotification(TokenCacheNotificationArgs args)

       {

           // if the access operation resulted in a cache update

           if (HasStateChanged)

           {

               Persist();

           }

       }

   }

Add OWIN Startup class 

Right-click on the project, select Add, select “OWIN Startup class”, and name the class “Startup”. If “OWIN Startup Class” doesn’t appear in the menu, instead select “Class”, and in the search box enter “OWIN”. “OWIN Startup class” will appear as a selection; select it, and name the class Startup.cs .

In Startup.cs , replace the code from below. Again, note the definition changes from public class Startup to public partial class Startup .


using System;

using System.Threading.Tasks;

using Microsoft.Owin;

using Owin;

[assembly: OwinStartup(typeof(MVCProject.Startup))]

namespace MVCProject

{

   public partial class Startup

   {

       public void Configuration(IAppBuilder app)

       {

           ConfigureAuth(app);

       }

   }

}

Create UserProfile model

In the Models folder add a new class called UserProfile.cs . Copy the implementation of UserProfile from below:


public class UserProfile

   {

       public string DisplayName { get; set; }

       public string GivenName { get; set; }

       public string Surname { get; set; }

   }

Create new UserProfileController

Add a new empty MVC5 controller UserProfileController to the project. Copy the implementation from below. Remember to include the [Authorize] attribute on the class definition.

References:


using System.Net.Http;

using System.Net.Http.Headers;

using System.Security.Claims;

using System.Threading.Tasks;

using System.Web;

using System.Web.Mvc;

using Microsoft.IdentityModel.Clients.ActiveDirectory;

using Microsoft.Owin.Security.OpenIdConnect;

using Newtonsoft.Json;


[Authorize]

   public class UserProfileController : Controller

   {

       private const string TenantIdClaimType = &quot;http://schemas.microsoft.com/identity/claims/tenantid&quot;;

       private static readonly string clientId = ConfigurationManager.AppSettings[&quot;ida:ClientId&quot;];

       private static readonly string appKey = ConfigurationManager.AppSettings[&quot;ida:AppKey&quot;];

       private readonly string graphResourceId = ConfigurationManager.AppSettings[&quot;ida:GraphUrl&quot;];

       private readonly string graphUserUrl = &quot;https://graph.windows.net/{0}/me?api-version=&quot; +

                                              ConfigurationManager.AppSettings[&quot;ida:GraphApiVersion&quot;];

       //

       // GET: /UserProfile/

       public async Task&lt;ActionResult&gt; Index()

       {

           //

           // Retrieve the user's name, tenantID, and access token since they are parameters used to query the Graph API.

           //

           UserProfile profile;

           string tenantId = ClaimsPrincipal.Current.FindFirst(TenantIdClaimType).Value;

            AuthenticationResult result = null;

           try

           {

               // Get the access token from the cache

               string userObjectID =

                   ClaimsPrincipal.Current.FindFirst(&quot;http://schemas.microsoft.com/identity/claims/objectidentifier&quot;)

                       .Value;

               AuthenticationContext authContext = new AuthenticationContext(Startup.Authority,

                   new NaiveSessionCache(userObjectID));

               ClientCredential credential = new ClientCredential(clientId, appKey);

               result = authContext.AcquireTokenSilent(graphResourceId, credential,

                   new UserIdentifier(userObjectID, UserIdentifierType.UniqueId));

               // Call the Graph API manually and retrieve the user's profile.

               string requestUrl = String.Format(

                   CultureInfo.InvariantCulture,

                   graphUserUrl,

                   HttpUtility.UrlEncode(tenantId));

               HttpClient client = new HttpClient();

               HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, requestUrl);

               request.Headers.Authorization = new AuthenticationHeaderValue(&quot;Bearer&quot;, result.AccessToken);

               HttpResponseMessage response = await client.SendAsync(request);

               // Return the user's profile in the view.

               if (response.IsSuccessStatusCode)

               {

                   string responseString = await response.Content.ReadAsStringAsync();

                   profile = JsonConvert.DeserializeObject&lt;UserProfile&gt;(responseString);

               }

               else

               {

                   // If the call failed, then drop the current access token and show the user an error indicating they might need to sign-in again.

                   authContext.TokenCache.Clear();

                   profile = new UserProfile();

                   profile.DisplayName = &quot; &quot;;

                   profile.GivenName = &quot; &quot;;

                   profile.Surname = &quot; &quot;;

                   ViewBag.ErrorMessage = &quot;UnexpectedError&quot;;

               }

           }

           catch (Exception e)

           {

               if (Request.QueryString[&quot;reauth&quot;] == &quot;True&quot;)

              {

                   //

                   // Send an OpenID Connect sign-in request to get a new set of tokens.

                   // If the user still has a valid session with Azure AD, they will not be prompted for their credentials.

                   // The OpenID Connect middleware will return to this controller after the sign-in response has been handled.

                   //

                   HttpContext.GetOwinContext()

                       .Authentication.Challenge(OpenIdConnectAuthenticationDefaults.AuthenticationType);

               }

               //

               // The user needs to re-authorize. Show them a message to that effect.

               //

               profile = new UserProfile();

              profile.DisplayName = &quot; &quot;;

               profile.GivenName = &quot; &quot;;

               profile.Surname = &quot; &quot;;

               ViewBag.ErrorMessage = &quot;AuthorizationRequired&quot;;

           }

           return View(profile);

       }

   }

Create new AccountController

Add a new empty MVC5 controller AccountController to the project. Copy the implementation from below.

References:


using System.Security.Claims;

using Microsoft.IdentityModel.Clients.ActiveDirectory;

using Microsoft.Owin.Security;

using Microsoft.Owin.Security.Cookies;

using Microsoft.Owin.Security.OpenIdConnect;

using QualityEngineeringSite.Utils;


public class AccountController : Controller

   {

       public void SignIn()

       {

           // Send an OpenID Connect sign-in request.

           if (!Request.IsAuthenticated)

           {

               HttpContext.GetOwinContext()

                   .Authentication.Challenge(new AuthenticationProperties { RedirectUri = &quot;/&quot; },

                       OpenIdConnectAuthenticationDefaults.AuthenticationType);

           }

       }

       public void SignOut()

       {

           // Remove all cache entries for this user and send an OpenID Connect sign-out request.

           string userObjectID =

               ClaimsPrincipal.Current.FindFirst(&quot;http://schemas.microsoft.com/identity/claims/objectidentifier&quot;).Value;

           AuthenticationContext authContext = new AuthenticationContext(Startup.Authority,

               new NaiveSessionCache(userObjectID));

           authContext.TokenCache.Clear();

           AuthenticationHelper.token = null;

           HttpContext.GetOwinContext().Authentication.SignOut(

               OpenIdConnectAuthenticationDefaults.AuthenticationType, CookieAuthenticationDefaults.AuthenticationType);

       }

   }

Create a new partial view _LoginPartial.cshtml 

In the Views –> Shared folder, create a new partial view _LoginPartial.cshtml. Replace the contents of the file from below


@using System

@{

   var user = &quot;Null User&quot;;

   if (!String.IsNullOrEmpty(User.Identity.Name))

   {

       user = User.Identity.Name;

   }

}

@if (Request.IsAuthenticated)

{

   &lt;text&gt;

       &lt;ul class=&quot;nav navbar-nav navbar-right&quot;&gt;

           &lt;li&gt;

               @Html.ActionLink(user, &quot;Index&quot;, &quot;UserProfile&quot;, routeValues: null, htmlAttributes: null)

           &lt;/li&gt;

           &lt;li&gt;

               @Html.ActionLink(&quot;Sign out&quot;, &quot;SignOut&quot;, &quot;Account&quot;)

           &lt;/li&gt;

       &lt;/ul&gt;

   &lt;/text&gt;

}

else

{

   &lt;ul class=&quot;nav navbar-nav navbar-right&quot;&gt;

       &lt;li&gt;@Html.ActionLink(&quot;Sign in&quot;, &quot;Index&quot;, &quot;UserProfile&quot;, routeValues: null, htmlAttributes: new { id = &quot;loginLink&quot; })&lt;/li&gt;

   &lt;/ul&gt;

}

Modify existing _Layout.cshtml

In the Views –> Shared folder, add a single line, @Html.Partial(“_LoginPartial”) , that lights up the previously added _LoginPartial view. See screenshot below

Authenticate Users

If you want the user to be required to sign-in before they can see any page of the app, then in the HomeController, decorate the HomeController class with the [Authorize] attribute. If you leave this out, the user will be able to see the home page of the app without having to sign-in first, and can click the sign-in link on that page to get signed in.

For more information around the AuthorizeAttribute, refer to:

AuthorizeAttribute Class

https://msdn.microsoft.com/en-us/library/system.web.mvc.authorizeattribute(v=vs.118).aspx

Web.Config Settings

In web.config , in <appSettings> , create keys for ida:ClientId , ida:AppKey , ida:AADInstance , ida:Tenant and ida:PostLogoutRedirectUri and set the values accordingly. For the public Azure AD, the value of ida:AADInstance is https://login.windows.net/{0} . See sample below:


&lt;!-- Values for OpenID and Graph API --&gt;

&lt;!-- ClientId is the application ID from your own Azure AD tenant --&gt;

   &lt;add key=&quot;ida:ClientId&quot; value=&quot;XXXXXXX&quot; /&gt;

   &lt;add key=&quot;ida:AppKey&quot; value=&quot;XXXXX&quot; /&gt;

   &lt;add key=&quot;ida:AADInstance&quot; value=&quot;https://login.windows.net/{0}&quot; /&gt;

&lt;!-- Tenant is the Tenant ID from your own Azure AD tenant. This is in a form of GUID.This is the value from your Federation Metadata Document URL' --&gt;

   &lt;add key=&quot;ida:Tenant&quot; value=&quot;XXXXXXXXX&quot; /&gt;

&lt;!-- Tenant is the Tenant ID from your own Azure AD tenant. This is in a form of GUID.This is the value from your Federation Metadata Document URL' --&gt;

   &lt;add key=&quot;ida:TenantId&quot; value=&quot;XXXXXXXX&quot; /&gt;

&lt;!-- PostLogoutRedirectUri is your application endpoint --&gt;

   &lt;add key=&quot;ida:PostLogoutRedirectUri&quot; value=&quot;http://xxxx.azurewebsites.net/&quot; /&gt;

   &lt;add key=&quot;aspnet:UseTaskFriendlySynchronizationContext&quot; value=&quot;true&quot; /&gt;

In web.config add this line in the <system.web> section: <sessionState timeout=”525600″ /> . This increases the ASP.Net session state timeout to its maximum value so that access tokens and refresh tokens cache in session state aren’t cleared after the default timeout of 20 minutes.