Saturday, June 30, 2018

Seeding & Extending Users and Roles with ASP.NET Core 2.1 Identity in Visual Studio 2017

In this post I shall describe the steps you need to follow if you want to add more data fields to the standard users & roles database table and seed both users and roles data. The approach being followed is code first development with Entity Framework Core. In order to proceed with this tutorial you need to have the following prerequisites:
  • You are using Visual Studio 2017 running under the Windows 10 operating system
  • You have installed ASP.NET 2.1 Core

Getting Started

In Visual Studio 2017, start a new ASP.NET Core 2.1 application by clicking on File >> New >> Project

Select ASP.NET Core Web Application and name your project with a name like IdentityCore.

Click on the OK button. On the next dialog:
  • select ASP.NET Core 2.1 from the drop-down list
  • select "Web Application (Model-View-Controller)" from the templates
  • click on the "Change Authentication" button and choose "Individual User Accounts"

Click on OK. Run the application by hitting Ctrl + F5 on your keyboard. Click on the Register link on the top-right side of your keyboard to add a new user. 

When you click on the Register button, you may receive a message that looks like this:

Do not be alarmed. The message simple reminds you that the Entity Framework migrations have not been applied yet. Simple click on the blue "Apply Migrations" button then refresh the page in your browser. The home page will display as shown below:


Click on Logout in the top-right corner.

Suppose we want to capture more data about the user, in addition to email and password. Let us assume we want to extend user data with the following properties:

FirstName
LastName
Street
City
Province
PostalCode

An easy way to do this is to create a new class that extends IdentityUser and adds the above properties. In the Models folder, add a new class named ApplicationUser and add to it the following class code:

    public class ApplicationUser : IdentityUser
    {

        public ApplicationUser() : base() { }

        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Street { get; set; }
        public string City { get; set; }
        public string Province { get; set; }
        public string PostalCode { get; set; }
        public string Country { get; set; }
    }

We may also wish to extend the standard roles table with these properties:

Description
CreatedDate

Just like we did with users, we will also create another class for roles that inherits from IdentityRole. In the Models folder, create a new class named ApplicationRole and add to it the following class code:

    public class ApplicationRole : IdentityRole
    {

        public ApplicationRole() : base() { }

        public ApplicationRole(string roleName) : base(roleName) { }

        public ApplicationRole(string roleName, string description,
            DateTime createdDate)
            : base(roleName)
        {
            base.Name = roleName;

            this.Description = description;
            this.CreatedDate = createdDate;
        }

        public string Description { get; set; }
        public DateTime CreatedDate { get; set; }

    }

Edit Data/ApplicationDbContext.cs file and get ApplicationDbContext to inherit from IdentityDbContext<ApplicationUser, ApplicationRole, string>. The ApplicationDbContext class code should look like this:

public class ApplicationDbContext : IdentityDbContext<ApplicationUser, ApplicationRole, string> {
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options) { }
}

Let us now create some sample data for roles and users. Create class named DummyData in Data folder, then add to it the following method:

public class DummyData {
    public static async Task Initialize(ApplicationDbContext context,
                          UserManager<ApplicationUser> userManager,
                          RoleManager<ApplicationRole> roleManager)
    {
        context.Database.EnsureCreated();

        String adminId1 = "";
        String adminId2 = "";

        string role1 = "Admin";
        string desc1 = "This is the administrator role";
        
        string role2 = "Member";
        string desc2 = "This is the members role";

        string password = "P@$$w0rd";

        if (await roleManager.FindByNameAsync(role1) == null) {
            await roleManager.CreateAsync(new ApplicationRole(role1, desc1, DateTime.Now));
        }
        if (await roleManager.FindByNameAsync(role2) == null) {
            await roleManager.CreateAsync(new ApplicationRole(role2, desc2, DateTime.Now));
        }

        if (await userManager.FindByNameAsync("aa@aa.aa") == null) {
            var user = new ApplicationUser {
                UserName = "aa@aa.aa",
                Email = "aa@aa.aa",
                FirstName = "Adam",
                LastName = "Aldridge",
                Street = "Fake St",
                City = "Vancouver",
                Province = "BC",
                PostalCode = "V5U K8I",
                Country = "Canada",
                PhoneNumber = "6902341234"
            };

            var result = await userManager.CreateAsync(user);
            if (result.Succeeded) {
                await userManager.AddPasswordAsync(user, password);
                await userManager.AddToRoleAsync(user, role1);
            }
            adminId1 = user.Id;
        }

        if (await userManager.FindByNameAsync("bb@bb.bb") == null) {
            var user = new ApplicationUser {
                UserName = "bb@bb.bb",
                Email = "bb@bb.bb",
                FirstName = "Bob",
                LastName = "Barker",
                Street = "Vermont St",
                City = "Surrey",
                Province = "BC",
                PostalCode = "V1P I5T",
                Country = "Canada",
                PhoneNumber = "7788951456"
            };

            var result = await userManager.CreateAsync(user);
            if (result.Succeeded) {
                await userManager.AddPasswordAsync(user, password);
                await userManager.AddToRoleAsync(user, role1);
            }
            adminId2 = user.Id;
        }

        if (await userManager.FindByNameAsync("mm@mm.mm") == null) {
            var user = new ApplicationUser {
                UserName = "mm@mm.mm",
                Email = "mm@mm.mm",
                FirstName = "Mike",
                LastName = "Myers",
                Street = "Yew St",
                City = "Vancouver",
                Province = "BC",
                PostalCode = "V3U E2Y",
                Country = "Canada",
                PhoneNumber = "6572136821"
            };

            var result = await userManager.CreateAsync(user);
            if (result.Succeeded) {
                await userManager.AddPasswordAsync(user, password);
                await userManager.AddToRoleAsync(user, role2);
            }
        }

        if (await userManager.FindByNameAsync("dd@dd.dd") == null) {
            var user = new ApplicationUser {
                UserName = "dd@dd.dd",
                Email = "dd@dd.dd",
                FirstName = "Donald",
                LastName = "Duck",
                Street = "Well St",
                City = "Vancouver",
                Province = "BC",
                PostalCode = "V8U R9Y",
                Country = "Canada",
                PhoneNumber = "6041234567"
            };

            var result = await userManager.CreateAsync(user);
            if (result.Succeeded) {
                await userManager.AddPasswordAsync(user, password);
                await userManager.AddToRoleAsync(user, role2);
            }
        }
    }
}

In the Startup class, replace the call to services.AddDefaultIdentity so that it uses the new ApplicationUser. Add the call to AddDefaultUI with the following code replacement:

services.AddIdentity<ApplicationUser, ApplicationRole>(
    options => options.Stores.MaxLengthForKeys = 128)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultUI()
    .AddDefaultTokenProviders();

Edit Views/Shared/_LoginPartial.cshtml and change:

@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager

TO


@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager

We will use the magic of dependency injection to make available to us the ApplicationDbContext, RoleManager and UserManager objects in the Configure() method in Startup.cs. Change the signature of the Configure() method by adding additional arguments (UserManager & RoleManager) so that it looks like this:

public void Configure(IApplicationBuilder app, 
  IHostingEnvironment env,
  ApplicationDbContext context,
  RoleManager<ApplicationRole> roleManager,
  UserManager<ApplicationUser> userManager
)
{ . . . . . }

Add the following to the bottom of the Configure() method in Startup.cs:

DummyData.Initialize(context, userManager, roleManager).Wait();// seed here

Note that in the Data/Migrations folder there are Entity Framework Code First migrations files that were added by the initial Visual Studio project template.


Since we changed the model for both users and roles, we need to add another migration and subsequently update the database. Therefore, execute the following commands from inside the Package Manager Console (Tools >> Nuget Package Manager >> Package Manager Console):

Add-Migration ExtendedUserRole -Context ApplicationDbContext

Update-Database -Context ApplicationDbContext

At this stage the tables are created, however data is not yet seeded. Let us run our application so that the sample roles and users get seeded. Hit CTRL + F5 on your keyboard. When the application starts, Logout if you are already logged in.

To prove that user and role data have been successfully seeded, login with one of the below credentials that were previously seeded:

Email: a@a.a    Password: P@$$w0rd
Email: b@b.b    Password: P@$$w0rd
Email: d@d.d    Password: P@$$w0rd
Email: m@m.m    Password: P@$$w0rd


The next task we need to accomplish is to modify the registration page so that the application can capture extended data such as First Name, Last Name, Street, City, Province, Postal Code and Country. ASP.NET Core 2.1 (and later) provide ASP.NET Core Identity as a Razor Class Library. This means that the registration UI is baked into the assemblies and is surfaced with the .AddDefaultUI() option with the services.AddIdentity() command in the ConfigureServices() method in Startup.cs.

Since we need to modify the registration controller and view, we instruct the scaffolder to generate the code used for registration. To do this, right-click on the project node in the Solution Explorer pane then:  Add >> New Scaffolded Item:


On the next Add Scaffold dialog, click on Identity on the left side, highlight Identity in the middle pane then click on the Add button.

On the Add Identity dialog, enable the "Override all files" checkbox, select the ApplicationDbContext class then click on the Add button.

Many Razor view pages will be generated for you under folder Areas/Identity/Pages/Account.


Edit the code-behind file Areas\Itentity\Pages\Account\Register.cshtml.cs. Add the following properties to the InputModel class:

[Required]
[DataType(DataType.Text)]
[StringLength(50, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 2)]
[Display(Name ="First Name")]
public string FirstName { get; set; }

[Required]
[DataType(DataType.Text)]
[StringLength(50, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 2)]
[Display(Name = "Last Name")]
public string LastName { get; set; }

[DataType(DataType.Text)]
[MaxLength(50)]
public string Street { get; set; }

[DataType(DataType.Text)]
[MaxLength(50)]
public string City { get; set; }

[DataType(DataType.Text)]
[MaxLength(50)]
public string Province { get; set; }

[DataType(DataType.Text)]
[MaxLength(15)]
[Display(Name = "Postal Code")]
public string PostalCode { get; set; }

[DataType(DataType.Text)]
[MaxLength(35)]
public string Country { get; set; }

In the same  \Itentity\Pages\Account\Register.cshtml.cs code-behind file, edit the code in the OnPostAsync() method so that line:

var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };

is changed to:

var user = new ApplicationUser {
  UserName = Input.Email,
  Email = Input.Email,
  FirstName = Input.FirstName,
  LastName = Input.LastName,
  Street = Input.Street,
  City = Input.City,
  Province = Input.Province,
  PostalCode = Input.PostalCode,
  Country = Input.Country
};

Next, let's update the UI. Edit the Razor view page Areas\Itentity\Pages\Account\Register.cshtml. Add the following markup to the form right before the email block:

<div class="form-group">
  <label asp-for="Input.FirstName"></label>
  <input asp-for="Input.FirstName" class="form-control" />
  <span asp-validation-for="Input.FirstName" class="text-danger"></span>
</div>
<div class="form-group">
  <label asp-for="Input.LastName"></label>
  <input asp-for="Input.LastName" class="form-control" />
  <span asp-validation-for="Input.LastName" class="text-danger"></span>
</div>
<div class="form-group">
  <label asp-for="Input.Street"></label>
  <input asp-for="Input.Street" class="form-control" />
  <span asp-validation-for="Input.Street" class="text-danger"></span>
</div>
<div class="form-group">
  <label asp-for="Input.City"></label>
  <input asp-for="Input.City" class="form-control" />
  <span asp-validation-for="Input.City" class="text-danger"></span>
</div>
<div class="form-group">
  <label asp-for="Input.Province"></label>
  <input asp-for="Input.Province" class="form-control" />
  <span asp-validation-for="Input.Province" class="text-danger"></span>
</div>
<div class="form-group">
  <label asp-for="Input.PostalCode"></label>
  <input asp-for="Input.PostalCode" class="form-control" />
  <span asp-validation-for="Input.PostalCode" class="text-danger"></span>
</div>
<div class="form-group">
  <label asp-for="Input.Country"></label>
  <input asp-for="Input.Country" class="form-control" />
  <span asp-validation-for="Input.Country" class="text-danger"></span>
</div>

Run the web application and click on the Register button on the top-right side.

When you click on the register button all the user data is saved in the database. You can verify that data has indeed been saved by viewing data in the AspNetUsers database table using "SQL Server Object Explorer":


We have succeeded in seeding user and role data and subsequently updating the registration page so that additional user data is captured. Thanks for coming this far in the tutorial.

References:

ASP.NET Core 2.1.0-preview1: Introducing Identity UI as a library

Tuesday, June 26, 2018

ASP.NET Core 2.1 MVC Code 1st Development with EF in Visual Studio

This post introduces the reader to developing an ASP.NET Core 2.1 application that uses the Code 1'st approach with SQL Server. Before you proceed with this tutorial, make sure that the following pre-requisites are met:
  • You are using the Windows 10 Operating System
  • You have Visual Studio 2017 installed on your computer
  • ASP.NET Core 2.1 is installed on your computer
The objective is to model NHL (National Hockey League) teams and players as shown below:

Team

TeamName
City
Province
Country

Player

PlayerId
FirstName
LastName
Position

The Visual Studio Project

Start your Visual Studio 2017
File >> New >> Project...
Select ASP.NET Core Web Application
Give your project a name (like MvcEfCore)


On the next dialog, after you click on OK, choose ASP.NET Core 2.1 and Web Application (Model-View-Controller). Click on the Change Authentication button and select Individual User Accounts.


Click on OK.

Let us add two classes (Team & Player) that represent the entities that were mentioned beforehand. Create two class files in the Models folder: Team.cs & Player.cs. Replace the class code with the following:

Team class

public class Team {
    [Key]
    [MaxLength(30)]
    public string TeamName { get; set; }
    public string City { get; set; }
    public string Province { get; set; }
    public string Country { get; set; }

    public List<Player> Players { get; set; }
}

Make sure you resolve the namespaces for the "Key" and "MaxLength" classes.

Player class

public class Player {
    public int PlayerId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Position { get; set; }

    public string TeamName { get; set; }
    public Team Team { get; set; }
}

Next we need to add a Entity Framework context class. Create a class file named NhlContext in the Data folder. Replace the class code with the following:

    public class NhlContext : DbContext
    {
        public NhlContext(DbContextOptions options) : base(options) { }

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
        }

        public DbSet<Team> Teams { get; set; }
        public DbSet<Player> Players { get; set; }
    }

Make sure you resolve the inherited DbContext, Team, and Player classes.

Build your application to ensure that you do not have any compiler errors.
Add the following code to the ConfigureServices() method in Startup.cs:

services.AddDbContext<NhlContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

The above code ensures that we can use the NhlContext class in dependency injection and that it uses the DefaultConnection connection string in appsettings.json.

Open appsettings.json for editing and change the database name so that it is simply NHL and not a long non-sense name. The appropriate connection string setting in appsettings.json will look like this:

"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=NHL;Trusted_Connection=True;MultipleActiveResultSets=true"

Developers prefer having sample data when building data driven applications. Therefore we will create some dummy data to ensure that our application behaves as expected. Create a class file named DummyData in the Data directory and add to it the following code:

public class DummyData {
  public static void Initialize(IApplicationBuilder app) { 
    using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope()) {
      var context = serviceScope.ServiceProvider.GetService<ApplicationDbContext>();
      context.Database.EnsureCreated();
      //context.Database.Migrate();

      // Look for any teams.
      if (context.Teams!=null && context.Teams.Any()) {
          return;   // DB has already been seeded
      }

      var teams = DummyData.GetTeams().ToArray();
      context.Teams.AddRange(teams);
      context.SaveChanges();

      var players = DummyData.GetPlayers(context).ToArray();
      context.Players.AddRange(players);
      context.SaveChanges();
    }
  }

    public static List<Team> GetTeams() {
        List<Team> teams = new List<Team>() {
            new Team() {
                TeamName="Canucks",
                City="Vancouver",
                Province="BC",
                Country="Canada",
            },
            new Team() {
                TeamName="Sharks",
                City="San Jose",
                Province="CA",
                Country="USA",
            },
            new Team() {
                TeamName="Oilers",
                City="Edmonton",
                Province="AB",
                Country="Canada",
            },
            new Team() {
                TeamName="Flames",
                City="Calgary",
                Province="AB",
                Country="Canada",
            },
            new Team() {
                TeamName="Leafs",
                City="Toronto",
                Province="ON",
                Country="Canada",
            },
            new Team() {
                TeamName="Ducks",
                City="Anaheim",
                Province="CA",
                Country="USA",
            },
            new Team() {
                TeamName="Lightening",
                City="Tampa Bay",
                Province="FL",
                Country="USA",
            },
            new Team() {
                TeamName="Blackhawks",
                City="Chicago",
                Province="IL",
                Country="USA",
            },
        };

        return teams;
    }

    public static List<Player> GetPlayers(NhlContext context) {
        List<Player> players = new List<Player>() {
            new Player {
                FirstName = "Sven",
                LastName = "Baertschi",
                TeamName = context.Teams.Find("Canucks").TeamName,
                Position = "Forward"
            },
            new Player {
                FirstName = "Hendrik",
                LastName = "Sedin",
                TeamName = context.Teams.Find("Canucks").TeamName,
                Position = "Left Wing"
            },
            new Player {
                FirstName = "John",
                LastName = "Rooster",
                TeamName = context.Teams.Find("Flames").TeamName,
                Position = "Right Wing"
            },
            new Player {
                FirstName = "Bob",
                LastName = "Plumber",
                TeamName = context.Teams.Find("Oilers").TeamName,
                Position = "Defense"
            },
        };

        return players;
    }
}


Resolve all outstanding namespaces.

Add the following code to seed data to the end of the Configure() method in Startup.cs:

DummyData.Initialize(app);

It is time to run some migration script. This can be done either at the terminal window or in the package manager console.

Option 1: Package Manager Console

To access the package manager console, click on Tools >> NuGet Package Manager >> Package Manager Console. This will open a window in a bottom pane in Visual Studio.

Type the following commands:

Add-Migration InitialCreate -Context NhlContext -o Data/Migrations/NHL
Update-Database -Context NhlContext

The first command produces the code that is needed to create the Players and Teams tables in the database. This code will be found in the Data/Migrations/NHL folder.

The second command will actually create the Teams & Players tables in the database.

Option 2: Terminal window using dotnet CLI

Drop into a terminal window in the project folder that contains the .csproj file.

Type the following dotnet CLI commands:

dotnet ef migrations add InitialCreate -c NhlContext -o Data/Migrations/NHL
dotnet ef database update -c NhlContext

Test Application

We are now ready to run the application. Hit Ctrl+F5 on your keyboard. The application will run and will look like this:


Of course there is no sight of the data that was created. To view the sample data in Visual Studio, click on View >> SQL Server Object Explorer. This opens up a pane in Visual Studio. Expand nodes database server >> Databases >> NHL >> Tables.



Right-click on dbo.Teams then select View Data. You should see Teams sample data in the database.


Likewise, view Players sample data.


Notice that the last column in the Players entity is a foreign key into the Teams entity.

Let us now scaffold the MVC controllers for both of these entities. Back in Solution Explorer, right-click on Controllers then select Add >> Controller. Select "MVC Controller with views, using Entity Framework".


Choose Team for Model class and NhlContext for Data context class.

When you click on the Add button, the controller for Teams is scaffold-ed for you. This includes the action methods for displaying, creating, editing and deleting data.

Just like you created a controller for the Team table, do the same for the Player table.


To view the output of the controllers created, you can run the application by hitting Ctrl + F5 then add either /teams or /players to view the Teams or Players controllers respectively.

The Teams controller




The Players controller


There is one thing we need to fix in the Teams index view. Since team name is a primary key it does not display in the tabls. The team-name is important to us so we need to modify Views/Teams/Index.cshtml. Open the file in Visual Studio and add the following HTML code as the first column title in the table:

<th>
@Html.DisplayNameFor(model => model.TeamName)
</th>

Also, add the following column data to the table with this HTML code:

<td>
@Html.DisplayFor(modelItem => item.TeamName)
</td>

When you run the application with /teams added to the address line, you should see team names.

Let us add menu items on the home page of our application for Team & Player so we do not have to always access these controllers by typing into the address line. To do this, edit Views/Shared/_Layout.cshtml. Add the following HTML code right before the closing </ul> tag:

<li><a asp-action="Index" asp-area="" asp-controller="Teams" href="https://www.blogger.com/null">Team</a></li>
<li><a asp-action="Index" asp-area="" asp-controller="Players" href="https://www.blogger.com/null">Player</a></li>

Now, when you run the application you will see two new menu buttons for Team & Player.


I hope you found this article useful. Meantime, cheers until we meet again.

Tuesday, April 3, 2018

Working with the Azure DevOps project

Download PDF version of this walk-through.

Microsoft has added a new project type in Azure that facilitates DevOps. This is the "DevOps Project" and it is currently in preview. In this post I shall show you how it works in its present state. You can expect it to look different when it reaches final release.

Some of the benefits of the DevOps project are:
  • you can be up and running with a new DevOps pipeline very quickly
  • a wide range of languages and frameworks are supported like .NET, Java, PPH, Node and Python
  • you can start fresh or bring in your application from GitHub on external Git
  • it has built-in Application Insights integration for instant analytics and actionable insights
  • CI/CD using Visual Studio Team Services (VSTS)
In this tutorial, we will use a very simple ASP.NET Core 2.0 web application that you can clone from GitHub. 

I assume you already have the following:
  • You have an Azure account at https://portal.azure.com
  • You have a VSTS account at https://www.visualstudio.com/team-services/
  • You have Docker installed on your computer
  • You have Git installed on your computer
  • You have a GitHub account at http://github.com
  • You have some basic knowledge about Docker
The plan is to deploy a sample ASP.NET Core 2.0 application into a Docker container on Azure using the new DevOps project in Azure.

Forking the sample ASP.NET Core 2.0 application

Login into your GitHub account in your favorite browser.

In a different browser tab, go to https://github.com/medhatelmasry/CompoundInterestCalculator.

Fork this repository into your GitHub account by clicking on the Fork button in the top-right corner.




The Sample Web Application

In the CompoundInterestCalculator directory, there are two other folders: Calculator & UnitTests. Go into the Calculator folder with the following terminal command:

cd CompoundInterestCalculator\Calculator

The Dockerfile in the current folder looks like this:

FROM microsoft/aspnetcore-build:2.0 AS build-env
WORKDIR /app

# Copy csproj and restore as distinct layers
COPY *.csproj ./
RUN dotnet restore

# Copy everything else and build
COPY . ./
RUN dotnet publish --framework netcoreapp2.0 --configuration Release --output dist

# Build runtime image
FROM microsoft/aspnetcore:2.0
WORKDIR /app
COPY --from=build-env /app/dist .

# run the application
ENTRYPOINT ["dotnet", "Calculator.dll"]

The Dockerfile contain instructions for creating a Docker image.



OPTIONAL - do this only if you have Docker installed on your computer

Let's create the Docker image and run the application in a Docker container by executing the following terminal window commands while in the Calculator folder:

docker build -t calculator .
docker run -d -p 8080:80 calculator 

View the web application by going to http://localhost:8080. It will look like this:


The DevOps project in Azure

Let us deploy this application using the DevOps project in Azure. Point your browser to https://portal.azure.com and login into your Azure subscription. Click on "All services" on the left-side navigation:


In the blade that appears, enter "devops" in the filter field then choose "Dev Ops Projects PREVIEW".


Click on the blue "Create DevOps Projects" button in the "DevOps Projects" blade.


The next dialog shows us the types of pipelines that can be generated for us, namely: .NET, Java, Static Websites, Node.js, PHP, Python and your own code. This list is sure to change as time goes by.

Click on the bottom square titled "Bring your own coded", then click Next.


You are asked to choose your code repository. You choices are either GitHub or External Git. Choose GitHub. You will next be asked to choose the the repository and branch.

Enter values as shown above then click on Next. Select "ASP.NET Core" from the drop-down list then click Next.

You are next asked to choose an Azure service to deploy the application. At the time of writing there are only three choices. Select "Web App for Containers" then click on Next.

As soon as you click on "Web App for Containers" a dialog slides from the right-side that looks like this:


Simply click on OK in the above dialog without changing anything, then click on Next.

The next blade is so that Azure can create for you an end-to-end pipeline in VSTS.

Below is a table that describes the values that you should choose:

Account Choose "Use existing" radio button if you already have a VSTS account that uses the same credentials as Azure. If you do not already have a VSTS account, then go ahead and choose "Create New".
Project name You can name the VSTS project: "CompoundInterestCalculator".
Subscription Choose a valid Azure subscription
App name Choose an app name that comprises a unique host name when combined with .azurewebsites.net
App Service location Choose a data center that is closest to where you are

After you are satisfied with the values that you have entered, click on Done.

Deployment of the DevOps project gets underway and it could take about 4 minutes. There is much that is happening behind the scenes:

  • A project is being created in VSTS that points to a GitHub repository.
  • Build and Release definitions are created in VSTS
  • The release version of the application is deployed to Azure
  • A number of other resources are automatically created in both Azure and VSTS

When deployment is completed you can view the website by going to https://.[your app name].azurewebsites.net or simply click on the Browse button on the blade that appears.


It takes a few minutes before the web application appears in your browser. If you see a message "Service Unavailable", it may be that you need to be patient and wait a while longer. When it eventually does appear, it should look just like this:


Azure Artifacts

Let us see what artifacts were created in Azure. Click on "Resource groups" in the left-side navigation.


You will find two resource groups that were created for you, one for Azure artifacts and the other for VSTS.


In my case, clicking on the top Resource Group revealed the following four items:


You can see that four resources belong to the resource group: Application Insights, App Service, Container Registry, and an App Service Plan.

The second resource group contained the following items:

 

There are two resources in the second resource group: a Team Services Account and a DevOps project.

VSTS Artifacts

Next, let's have a look at what is going on in VSTS. Login into your VSTS account and have a look around.

NOTE: You can go directly into your project in VSTS from the DevOps project in Azure by clicking on the link shown below in Azure:



You will find that a new project was created for you.

Click on the project that was just created, then click on Build and Release >> Builds. A new build was created and it shows that it successfully ran.


Click on Build and Release >> Releases. You will also discover that a release was created for you and that it has also completed successfully. 

Continue to look around in VSTS by checking the build and release definitions.

Unit Tests

As mentioned before, there are two projects in our Visual Studio 2017 solution. One happens to be a a Unit Testing project that uses the xunit framework. Let us look at the Build tasks in VSTS in order to determine whether or not out test cases are being executed. 

Click on Build and Release >> Builds.

Click on the build definition that was created for you by the DevOps project in Azure as shown below.

Click on Edit in the top-right.
There are two tasks that have already been defined:
  • Build an image
  • Push an image

It is obvious that unit testing tasks are absent. Let us add tasks that will run our unit tests. These must be added before the push & build image tasks. Click on + then add three .NET Core tasks. 
You can drag-and-drop three of these .NET Core tasks and place them before the already existing tasks.
Change the configuration of each of the .NET Core tasks as follows:

First .NET Core task
Display Name: Restore
Command: restore
Path to projects: **/*.csproj

Second .NET Core task
Display Name: Build
Command: build
Path to projects: **/*.csproj
Arguments: --configuration $(BuildConfiguration)

Third .NET Core task
Display Name: Test
Command: test
Path to projects: **/*Tests/*.csproj
Arguments: --configuration $(BuildConfiguration)
Publish Test Results

Below are the screen captures of each of the three .NET Core task configurations:


This is all we need to ensure that our test cases are processed. Let us queue a build and see what happens. Click on Save & queue >> Save & queue on the top right side.
On the next dialog, simply click on the Save & queue button.

To see progress of the build, click on the link on the top-left side.
After the build completes, click on Tests on the left-side and you will see that all eight test cases have successfully passed.

Eventually, when the build is successful, it will automatically trigger a release.

Whenever any application code changes are committed to source control, the build and release processes are automatically triggered. Let's see for ourselves that this is indeed the case. Let's make a change to the code in GitHub. Find the file CompoundInterestCalculator/Calculator/Models/CompoundInterest.cs in GitHub. Edit the document by clicking on the pencil icon on the top-right side:
We will disable a piece of logic that is important to one of the test cases. Comment out statement "throw new Exception("Principal amount cannot be negative.");" on line 19.  then click on the "Preview Changes" tab.



Enter a comment under "Commit changes" then click on the green "Commit changes" button. 


The above edit an commit automatically triggers a VSTS build. Return to VSTS then click on Build and Release >> Builds. Click on the build definition.


You will notice that the last build failed. Click on the failed build to find out the root cause. There is a red X beside Test node. Click on it.

More detail is available regarding what exactly happened as shown below:


It is clear from this short walk-through that the DevOps project in Azure has automatically done many of the steps that would have had to be done manually in VSTS. We only needed to do a minor tweak to the generated build definition by adding three tasks to run our unit tests. I am sure this new DevOps addition to Azure will become much more mature and useful with the passage of time.