Blog Home  Home Feed your aggregator (RSS 2.0)  
Mayur's Blog - Sunday, September 18, 2016
 
# Sunday, September 18, 2016

We have seen many web applications with multiple pages. In fact, before the advent of the SPA, muti-page applications were only existed. In reality, there are many muti-page applications in development in today's date as well. Especially, when the application is huge and when it servers many different domains and purposes, the designer could decide in favor of doing muti-page application instead of SPA. When a web application is huge, it is also likely that each page within this multi-page application serves a purpose of SPA. This kind of set up gives the best of both the worlds. By doing multi-page application we do not allow a particular page to grow a lot. That improves maintainability. Additionally, if different pages serves totally different business purposes, then it is a good idea to keep them separate to maintain "separation of concern".

Implementing client side development with Angular2 for any muti-page application requires a little different set up. You may not be able to strictly follow the samples available on Angular2 site. Many of those samples are designed for the SPA.

I came across a situation where I needed to implement Angular2 with muti-page web application. I followed the approach as outlined here. I thought to share the same idea here. I hope that this may be helpful to someone else in need of doing the same thing.

Additionally, you may have an older application that is also a multipage application. Here you may be looking forward to implement the client side development using Angular2. In this case you don't have a choice unless you may want to re-write an application to be an SPA. Re-writing is not always possible. So in this case as well the following discussion might be useful.

Here I explain how to set up Angular2 and how to configure SystemJS to use Angular2 for a multi-page client side web application development. I am using Asp.NET MVC Core as my server side development platform. However, we are discussing here about client side development and this discussion is valid for any server side platform/technology.

1) Create app-components folder under wwwroot

2) Add required components for your application here. For example, if there is a page to manage employees, then you may create employee-edit component here

3) You may create another folder within app-components called common-services

4) You may want to add database services here. For example, you may add employee service to retrieve and update employees

5) You may create another folder within common-service named domains

6) You may use this folder to create interfaces used across your Angular2 project. For example, you may use IEmployee so that data retrieved from employee service can be passed across components by casting JSON to IEmployee. The same way data collected from UI using components can be passed to employee service via IEmployee to make any update in the database

Above image 1: Folder structure

Above image 2: How to refer required services, components and domains within a component

Above image 3: Setting up a service

7) In addition to this you may also see common-components in the screen capture created to show folder structure. This folder actually holds components common to other components. For example, there is component to show error messages. This component is being shared across other components. Such type of common components created for re-usability can be stored here.

Above image 4: Common components

8) There is also another folder called common. It holds common objects but not necessarily domains. For example, Enum.

The logic behind this kind of folder structure is to follow the dependency order. For example, components depend on services, services depend on domains etc. By following this folder structure it is easy to refer the items using import statement. Please refer image 1 to see how consistent it becomes to refer a dependency by following this structure.

9) The next important aspect is to set up bootstrappers and module. As our application is muti-page application we need to set up boot-strapper and module for each page. For example, my application has employee management page. I will set up boot-strapper and module as shown below. You may create modules and page-bootstrappers folder under wwwroot

Above image 5: Module and Boot-strapper

Above image 6: How to refer various dependencies in a module

Above image 7: How to refer module in a boot-strapper

10) Now it is time to set up SystemJS configuration as shown below

(function (global) {
    var packages = {
        'rxjs': {
            defaultExtension: 'js'
        },

        'page-bootstrappers/employee': {
            defaultExtension: 'js',
            main: 'employee-management.js'            
        },
        'page-bootstrappers/admin': {
            defaultExtension: 'js',
            main: 'admin-employee-management.js'
        }
    };

    System.defaultJSExtensions = true;

    System.config({
        baseURL: location.protocol + '//' + location.host,
        paths: {
            'npm:':'node_modules/'
        },
        map: {
            //angular bundles
            '@angular/core': 'npm:@angular/core/bundles/core.umd.js',
            '@angular/common': 'npm:@angular/common/bundles/common.umd.js',
            '@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
            '@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
            '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
            '@angular/http': 'npm:@angular/http/bundles/http.umd.js',
            '@angular/router': 'npm:@angular/router/bundles/router.umd.js',
            '@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
            
            //other libraries
            'rxjs': 'npm:rxjs',

            //my application
            '../app-components/': 'app-components/'
        },
        packages: packages        
    });

})(this);

 

11) Please note:

As this is a multi page, SystemJS must be configured to use baseURL as shown above. Our folder structure for Angular2 always assumes a request from the root to load any component.

12) The following shows the employee management cshtml in VIEWS folder

Above image 8: Employee management folder which users Angular2

The following shows how we set up Employee management page to use Angular2

@{
    ViewBag.Title = "Employee Management";
    Layout = "~/Views/Shared/_LayoutBootstrap.cshtml";
}

<script src="/js/shim.min.js"></script>
<script src="/js/zone.js"></script>
<script src="/js/Reflect.js"></script>
<script src="/js/system.src.js"></script>
<script src="/systemjs.config.js"></script>
<script src="/js/ej.angular2.min.js"></script>
<script>
    System.import('page-bootstrappers/employee').then(function () { }, console.error.bind(console));
</script>
<section>
    <employee-profile>
        <span class="fa fa-spinner fa-pulse fa-3x"></span>&nbsp;&nbsp;&nbsp; Please wait... loading web components and data
    </employee-profile>
</section>

Please note required java script libraries. "js/ej.angular2.min.js" is a third party component library offered by Syncfusion. You may not need it or you may replace it with whatever third party components you are using.

I have created this article based on Angular2 - version 2.0.0. At the time of writing this article, this was the latest version. In the future, this set up may change as per changes applied to Angurlar2 framework.

I hope that this may be useful. If you have any other suggestion to improve it, then please let me know.

Thank you and happy coding!

Sunday, September 18, 2016 7:12:51 PM UTC  #       | 
# Wednesday, July 13, 2016

Asp.Net MVC Core 1.0 can be configured to read from environment variable. This is useful especially when you have different machines for different environments such as production, stating and development. However, this set up does not work when you have one machine for different environments. For smaller businesses one machine may host both production and staging environment. For a small team of developers working on a prototype there may be only one machine for different environments to save on cost.

In this situation, configuration based on environment variable may not work. Here I will show how you can configure the project to utilize solution configuration (similar to older Asp.NET MVC where we have used various configuration files such as web.debug.config, web.release.config etc.)

1) Add configuration name and compile symbols

As shown below add required configurations in project.json. For each configuration we need to define a compilation symbol 

Image 1

2) Startup.cs

Add the following highlighted code in Startup method of Startup.cs file. As shown here we are using compilation directives to update the type of environment programmatically.

Image 2

Similar to older Asp.Net MVC, here you need to change the configuration (as shown below) before you build or publish the application.

Image 3

Depending on what solution configuration you are building, the compiler directive would emit the required code. This way the code in Starup method will be modified during compile time.

Please look into image 2 once again and notice how we are adding JSON configuration file.

3) appsettings.json

As shown below add required appsettings.json files. Please note the naming of the files. It should match the names specified in Startup method (see image 2) while creating the builder object.

Image 4

4) Build and Test

Build your project and test it. Try publishing your project with different solution configuration and test whether the set up is really working.

5) Side notes for post-compile and pre-compile

If you ever need to run some scripts before or after compilation based on configuration, then you may utilize the following "precompile" (or "postcompile" not shown) directive available within project.json. As shown below I am running a batch file where I am passing a name of solution configuration selected. Please pay attention to %compile.Configuration%. That's how I request MsBuild to provide the name of solution configuration. That value is transferred to a batch file as a parameter.

This batch file can be used for anything. It may copy several files depending on what is being built. In my case I have app.config (shown in Image 6) file where I have database connection string. You may notice that I am using configsource. So now I need to generate connectionstrings.config and copy it to the same level where app.config is available. By this time you might have figured out that I am going to three connectoionString.config file responsible to carry connection strings for my three different solution configurations. Again this is the same approach we followed during traditional .NET development.

(Generally we specify database connection string in appseetings.json. However, I have a few legacy class libraries. It was going to take a lot to refactor them to inject configuration service as suggested in Asp.NET MVC Cor 1.0. So I was left with no option but to add connection string to app.config. This way class libraries can be provided with database connection during runtime. At this point I am not debating on what is right or wrong but the whole point here is to demonstrate how you can utilize the build processes with ASp.NeT MVC Core 1.0 and leverage what you have been doing with an older Asp.Net MVC set up so far)

Image 5

Image 6

As shown in the following image, I have various connectionstring.config file for various solution configurations. The file precompilescript.bat is the file where I have a logic to find required connectionstrings.<%compile.configuration%>.config and copy that as connectionStrings.config. This all happens during pre-build. That means a correct file is copied just before a build begins. During run time app.config references it and provides a correct connection string.

Image 7

Image 8

Asp.NET MVC Core  1.0 is quite new. The way it has been set up is very different than the traditional .NET development. It is likely that we will have to mix and match a traditional approach with a new approach for the next few years until everything we need to build a web application get transferred to this newer approach.

I hope the above mentioned work around may help.

I am also new to this kind of development approach. If you feel that you have better suggestion in context with this blog post, then please feel free to let me know. I really appreciate any opportunity to learn something new and better.

Thanks a lot for reading. Have fun with coding!!

Wednesday, July 13, 2016 9:52:08 PM UTC  #       | 
# Thursday, June 16, 2016

Today I had to use a development machine running under VMWare workstation. I was doing mobile app development for Android. When I launched the debug mode the emulator all of a sudden failed to launch. I received the following error message.

"The Virtual Machine Management Service failed to start the virtual machine 'Emulator.' because one of the Hyper-V components is not running (Virtual machine ID BZCC6546-129T-4683-922F-52EAT972E888)"

This message indicates that the hyper-v was not able to launch the virtual machine. The problem is caused by the way hyper-v creates a virtualization of a virtual processor. Since I was running a virtual machine, the hyper-v tried to create the virtualization of a virtual processor. Obviously  it failed because the virtual processor did not support virtualization of itself. Fortunately, I was running under VMWare workstation 12 as well as the actual physical machine also supported the virtualization set up provided by VMWare.

So I shut down the VM and went to the settings and enabled the following option under processors. That solved the problem. Hyper-v was able to properly run the emulator.

Hopefully this hint may help someone facing the similar problem.

Thursday, June 16, 2016 7:14:14 AM UTC  #       | 
# Sunday, April 10, 2016

There are many useful posts available to resolve this issue. This issue arises due to race condition. It is important that ASYC methods are written carefully. Care should be taken so that all the references within ASYC method should not return VOID but at least a TASK.

I came across a scenario where a method responsible to execute database and smtp operations was running into this problem. As shown below the database operations were using ASYC methods, however, SMTP was a non-async method. It was a VOID method with no return

Database operations were running into AsyncLock to make sure that it is thread safe. Thread safety was needed as unique identifier of a company record was generated by the code.

After database operations are successful, ProcessRegistration calls a method to run an smtp operation. As it is not an ASYC method and does not return, ProcessNewRegistration Task can declare itself completed. However, smtp operation may not be complete as emailing sometimes not quicker depending on how many emails in the queue of SMTP Server. In this case, when the thread running ProcessNewRegistration Task tries to complete the task, it runs into a problem. This is due to pending task of smtp operation might be still not complete.

The resolution of this problem is to have SendRegistrationEmail as ASYNC too. This allows to await this method. Doing this forces the thread to wait before issuing a token to complete ProcessNewRegistration task. 

If you are running such problems, a good examination of the code base is needed. Refactoring may be needed. The rule of thumb is never use VOID methods.

public async Task<Company> ProcessNewRegistration(Company company, decimal registrationCost, string paymentId)
        {
            var login = await loginRepositoryService.GenerateNewLogin(Interfaces.Enum.UserRole.ClientAdmin);
            company = await GenerateCompany(company, login);
            SendRegistrationEmail(company.CompanyCode, company.Contact1Name, company.Contact1Email, registrationCost, paymentId);
            return company;
        }
private void SendRegistrationEmail(string companyCode, string name, string email, decimal amount, string paymentId)
        {
            var body = $@"<strong>Hi {name}</strong>
                        <p>Thank you very much for your registration.</p>
                        <p>Please accept this email as your reciept of payment.</p>
                        <p>Registration details</p>
                        <strong>Company code: </strong>{companyCode}<br />
                        <strong>Amount paid: </strong>${amount}<br />
                        <strong>Pyament id: </strong>${paymentId}<br />
                        <strong>Date of payment: </strong>{DateTime.Now.ToString("yyyy-MM-dd")}<br />
                        <p>We appreciate your business and looking forward to exceed your expectations.</p>
                        <p>&nbsp;</p>
                        <strong>Sincerely,</strong><br />
                        Customer Service Team
                        ";
            UtilityService.SendEmail(email, "admin@mail.ca", "Confirmation: Registration and Payment", "admin@mail.ca", string.Empty, body, null, null);           
        }
 
private async Task<Company> GenerateCompany(Company company, Login login)
        {            
            using (var al = await asyncLocker.LockAsync())
            {
                var latestCompanyId = await companyService.MaxAsync(c => c.CompanyId);
                var latestCompany = await companyService.FindAsync(latestCompanyId);
                var code = 0;
                if (!int.TryParse(latestCompany.CompanyCode, out code))
                {
                    throw new Exception("Error while generating company code");
                }
                company.CompanyCode = (++code).ToString();
                company.Logins.Add(login);
                companyService.Add(company);
                var id = await companyService.FlushChanges(company);                
            }

            return company;            
        }

Corrected methods to run smtp operations.

public async Task<Company> ProcessNewRegistration(Company company, decimal registrationCost, string paymentId)
        {
            var login = await loginRepositoryService.GenerateNewLogin(Interfaces.Enum.UserRole.ClientAdmin);
            company = await GenerateCompany(company, login);
            await SendRegistrationEmail(company.CompanyCode, company.Contact1Name, company.Contact1Email, registrationCost, paymentId);
            return company;
        }
private async Task SendRegistrationEmail(string companyCode, string name, string email, decimal amount, string paymentId)
        {
            var body = $@"<strong>Hi {name}</strong>
                        <p>Thank you very much for your registration.</p>
                        <p>Please accept this email as your reciept of payment.</p>
                        <p>Registration details</p>
                        <strong>Company code: </strong>{companyCode}<br />
                        <strong>Amount paid: </strong>${amount}<br />
                        <strong>Pyament id: </strong>${paymentId}<br />
                        <strong>Date of payment: </strong>{DateTime.Now.ToString("yyyy-MM-dd")}<br />
                        <p>We appreciate your business and looking forward to exceed your expectations.</p>
                        <p>&nbsp;</p>
                        <strong>Sincerely,</strong><br />
                        Customer Service Team
                        ";
            await UtilityService.SendEmail(email, "admin@mail.ca", "Confirmation: Registration and Payment", "admin@mail.ca", string.Empty, body, null, null);           
        }
 
Sunday, April 10, 2016 11:48:03 PM UTC  #       | 
# Tuesday, December 29, 2015

Sometimes things are straight forward but easy to miss. If you are implementing a client to consume RESTful APIs and like to leverage RestSharp library, then the configuration is pretty straight forward to utilize windows authentication.

RestSharp provides "RestRequest" class. An object of "RestRequest" can be instantiated with required values such as API address and type of method (i.e. post or get or put etc.). This object can be used to enable windows authentication as shown below.

var request = new RestRequest(resource, Method.POST);
request.UseDefaultCredentials = true;

Here "UseDefaultCredentials" is set to "true" to enable windows authentication.

This is great; easy and simple. However, this would be repeating the configuration to enable windows authentication each time a new request is created. To fix this problem I would like propose an alternative. RestSharp also provides another class "RestClient". This class is actually an equivalent to "WebClient" in .NET. It exposes a few useful properties such as a base URL for the API and Authenticator. Authenticator is "IAuthenticator" type. Authenticator can be used to configure the authentication mechanism for the client. So we can implement our own Authenticator as shown below to specify windows authentication.

internal class WinAuthenticator : IAuthenticator
    {
        public void Authenticate(IRestClient client, IRestRequest request)
        {
            request.UseDefaultCredentials = true;
        }
    }

Once this is done we can make a request to an API with custom authenticator as shown below.

var client = new RestClient("BaseApiConnection");
client.Authenticator = new WinAuthenticator();
            
With the help of this client we can create a request as shown below
var request = new RestRequest(resource, Method.POST);
            IRestResponse response = null;
            request.AddJsonBody(data);
            response = client.Execute(request);
This way a single client can be shared across all the requests.
Tuesday, December 29, 2015 7:16:30 PM UTC  #       | 
# Tuesday, October 27, 2015

Earlier there were posts regarding how to build AMD modules with RequierJS and MS Build. Changes were needed to MS Build because there was no direct support from Visual Studio. The MS Build triggered NodeJS to complete the build process and copied required files to the output directory or deployment folder.  

However, Asp.Net MVC vNext (6 or later) web template with Visual Studio 2015 supports client side building tools such Grunt and Bower. Therefore, the client side building has become simpler and does not require a customization to MS Build. But what if you have Asp.Net MVC with version 4 or 5. Older version of MVC templates do not come with support of Bower and Grunt to build required JavaScript. Therefore, you are still stuck with MS Build customization even though you have Visual Studio 2015.

Here I will show one of my experiments where I have implemented client side building of JavaScript. I wanted to build RequireJS AMD modules for multi page Asp.Net MVC 5 using Bower and Grunt.

Again please note that my implementation is based on Visual Studio 2015 but I am using Asp.Net MVC version 5. If you are using vNext then everything is built-in. However, at the time of writing this post the vNext was till in Beta and was not everyone's choice of production development. This approach may not be 100% perfect, however, I believe and hope that it could be a good guiding block to whomsoever wants to implemented a client side build environment for the JavaScript.

Why do we need client side build?

Most of client side libraries are written in JavaScritps and maintained in open source environments. These environments may not necessarily compatible with .NET and Visual Studio. As typical .NET developers we are used to with NuGet packages where we maintain .NET based packages. JavaScritp based packages are available in NuGet too but they are just an extension of the original open source packages. Someone has to volunteer to build NuGet packages. Therefore, it is possible that these packages may not be up to date. Wouldn't it be nicer if we could directly interact with those packages directly instead of getting them through NuGet?

Additionally, good practices such as AMD (asynchronous module) requires modules to be created and served in a certain a way. That includes concatenation of certain modules, minification and cleaning of the modules etc. Essentially it is similar to gathering certain C# files available in a project and creating a DLL by putting them altogether. Moreover, it should not minify when it is in debug/development mode but do it when it is a time for a deployment.

JavaScript development has gathered so much important due to its wide spread usage. Therefore, now days, client side development has taken off a lot. Probably the developers now writing as much JavaScript as much of the managed codes at the server side. Therefore, we need something so that we can manage and maintain client side development.

-- My apology for leaving the post incomplete; something has to be tacked on a priority; I will be back to complete the post as soon as possible; please come again after a few days to read the completed post; thank you -- 

 

Tuesday, October 27, 2015 12:26:36 AM UTC  #       | 
# Monday, September 28, 2015

1. Implicit property initialization

public MyProperty {get;set;} = 10;

2. Access "Static" members directly by referring a type in "Using"

Using Console;

static void main(string[] args)

{

   WritleLine("Happy Coding");      //earlier we wrote Console.WriteLine and did use "Using Console"

}

3. Implicitly mark "setter" to private

public string MyReadOnlyProperty {get;};

In C# 6.0 this would assume that "setter" is private. Earlier we had to explicitly declare that "setter" is private, for example

public string MyReadOnlyProperty {get; private set;}

4. "Out" can be declared in the expression itself and can be scoped locally

public bool Validate(object objToValidate, out List<string> errors){ // validation is here for objToValidate }

Earlier we wrote

private List<string> errors = new List<string>();

public bool Validate(object objToValidate, out List<string> errors)

The scope of variable "errors" was valid outside of the method also. In C# 6.0, if variable is declared within expression then the scope is within the method only.

5. Exception with If statement

try{}

catch(Exception ex) if (//condition 1){}

catch(Exception ex) if (//condition 2){}

Now it is possible to filter catch statement by a given condition

6. Lambda like function expression

var age = 10;

public string MyProperty => String.Format("I am {1} years old",age);

public decimal GetRoundedValue (decimal digit) => Decimal.Round(digit,2);

You may define any property, member or function like this.

7. Define a function with IEnumerable type perameter with Param kewords

void MyMethod(param IEnumberable<string> names)

{

   names.ForEach(n => Save(name));

}

MyMethod("John","Johnyy","Jordan","James");

Earlier only arrays allowed with "param" keyword

8. User separator to initialize any numerical variable for better readability

int population = 1_000_000_00;

It is possible to separate digits for better readability. As far as compiler is concerned, it does not make any difference. The compiler would ignore "_" placed after a first digit.

9. "nameof" can be used to extract a string value of name of any variable, type or method

public void TestMethod()

{

   try{}

   catch(exception ex)

   {

      Logger.Log(ex.message, nameof(TestMethod));

   }

}

10. Another "String" manipulation with /{}

private age = 10;

public string GetAgeDescription()

{

   return "I am /{age} year/({age == 1?"":"s"}) old";

   //output

   //if age is 1 - I am 1 year old; if age is 3 - I am 3 years old

}

11. NULL conditional operator "?."

public string GetStreetAddress(Address address)

{

   return address?.StreetAddress;   //if address is NULL then returns NULL otherwise value of StreetAddress property

}

public int NumberofKids(Parent parent)

{

   return parent?.Numberofkids??0;   //0 if parent is NULL otherwise value of NumberofKids property

}

Earlier it was required to add "If" block to check whether address is NULL or not to avoid possible "Object not set to a reference" error.

12. Use "await" in Catch or Finally

public void DoSomething()

{

   try{}

   catch(exception ex)

   {

      await Logger.LoginTextFile(ex.message);

   }

   finally

   {

      await Email.SendToAdmin("Operation completed");

   }

}

13. Initialize a dictionary with []

private Dictionary<int, string> Names = new Dictionary<int, string>

{

   [0] = "Joe",

   [1] = "John",

   [2] = "Johnny",

   [3] = "Jordan"

}

Monday, September 28, 2015 5:24:43 PM UTC  #       | 
# Friday, July 24, 2015

We are thinking to migrate our legacy web application to Asp.Net 5 framework to utilize all the enhancements and good features available.

The biggest road block is Identity system. Our legacy application has its own User and UserRole tables (in database) to Authenticate and Authorize. Asp.Net 5 is based on newer implementation of Identity, which is part of Microsoft.AspNet.Identity and Entityframework.

The out of box implementation using Visual Studio 2015 uses EntityFramework 7 (Beta). We cannot use it and it is still not ready for Production. Our data layer is still EntityFramework but with older version. We don't want to really change this implementation at this moment at least.

It looks like that we need to fully customize Identity framework implemented in Microsoft.AspNet.Identity framework if we want to use it.

Please note that our legacy application does not need anything except log in and log out from Identity system. So basically verifying that the person is authorized and authenticated by given id and password. Additionally, the Identity created after log in should preserve the roles for a given person so that we can use Attributes to decorate our controller and actions to secure it. These attributes will also use Roles for authorization purpose.

The following is an attempt to do this. Actually at this moment I have completed this prototype kind of implementation and was able to  run this successfully. The log in process and log out process worked. The Identity generated (and saved in HttpContext) is capable enough to secure methods using roles.

So here is my implementation goes. It is still kind of an approach to tackle this issue and there is still a room to improve it in terms of both coding and design.

My implementation is based on Asp.Net Beta 5 released with Visual Studio 2015 RTM. It is possible that by the time Asp.NET goes LIVE there may be a few changes and refactoring needed. I don't expect a lot though. Here is my project.json looks like to bring in required references.

Items shown with "Bold and Italic" are required references for Identity framework. 

{
  "webroot": "wwwroot",
  "userSecretsId": "aspnet5-CCClientWeb-8bbc0c70-ab78-4053-a5bf-492342d52f2a",
  "version": "1.0.0-*",

  "dependencies": {
    "Microsoft.AspNet.Authentication.Cookies": "1.0.0-beta5",
    "Microsoft.AspNet.Authentication.Facebook": "1.0.0-beta5",
    "Microsoft.AspNet.Authentication.Google": "1.0.0-beta5",
    "Microsoft.AspNet.Authentication.MicrosoftAccount": "1.0.0-beta5",
    "Microsoft.AspNet.Authentication.Twitter": "1.0.0-beta5",
    "Microsoft.AspNet.Diagnostics": "1.0.0-beta5",
    "Microsoft.AspNet.Diagnostics.Entity": "7.0.0-beta5",
    "Microsoft.AspNet.Identity": "3.0.0-beta5",
    "Microsoft.AspNet.Identity.EntityFramework": "3.0.0-beta5",
    "Microsoft.AspNet.Mvc": "6.0.0-beta5",
    "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-beta5",
    "Microsoft.AspNet.Server.IIS": "1.0.0-beta5",
    "Microsoft.AspNet.Server.WebListener": "1.0.0-beta5",
    "Microsoft.AspNet.StaticFiles": "1.0.0-beta5",
    "Microsoft.AspNet.Tooling.Razor": "1.0.0-beta5",
    "Microsoft.Framework.CodeGenerators.Mvc": "1.0.0-beta5",
    "Microsoft.Framework.Configuration": "1.0.0-beta5",
    "Microsoft.Framework.Configuration.Json": "1.0.0-*",
    "Microsoft.Framework.Logging": "1.0.0-beta5",
    "Microsoft.Framework.Logging.Console": "1.0.0-beta5",    
    "Microsoft.VisualStudio.Web.BrowserLink.Loader": "14.0.0-beta5"
  },

  "commands": {
    "ef": "EntityFramework.Commands",
    "gen": "Microsoft.Framework.CodeGeneration",
    "web": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5000"
  },

  "frameworks": {
    "dnx451": {
      "dependencies": {
        "CCClients.BusinessService": "1.0.0-*",
        "CCClients.Interfaces": "1.0.0-*",
        "CCClients.Repository": "1.0.0-*",
        "Microsoft.AspNet.Http.Core": "1.0.0-beta4"       
      }
    }
  },

  "exclude": [
    "wwwroot",
    "node_modules",
    "bower_components"
  ],
  "publishExclude": [
    "node_modules",
    "bower_components",
    "**.xproj",
    "**.user",
    "**.vspscc"
  ],
  "scripts": {
    "postrestore": [ "npm install", "bower install" ],
    "prepare": [ "gulp copy" ]
  }
}

1) We need to implement custom classes as shown below. CustomSignInManager and CustomRoleManager inherits from base classes and override required methods. These classes running an engine to drive Identity. Please remember that I am implementing only a bare minimum login functionality to support legacy application. So I really do not have to override many methods from bases classes and do not require implementation of all the interfaces. 

Just to demonstrate how far I can do customization, I have even overridden CheckPassword method (see in CustomUserStore.cs), which allows me to check passwords which are not hashed in the database and stored as raw values. I know that it is not recommended at all but many of the folks forced to support legacy apps not designed properly ard forced to follow this practice and look for a way to find work abounds.

Another thing you may notice is an implementation of CustomUserStore in CustomUserStore.cs. You may see that CustomUserStore class implements many interfaces. This user store actually is a driver for database operations. The implementation here follows "O" of SOLID principles. You may implement as many interfaces as many functionalities you want. In may case I implement IUserStore<CustomIdentityUser>, IUserRoleStore<CustomIdentityUser>, IUserPasswordStore<CustomIdentityUser> because I need Roles. IUserStore and IUserPasswordStore will be needed in any case. However, let's say you want to do account Lockout based on invalid login attempts. In that case you need to implement IUserLockoutStore interface. Identity framework comes with many features and you need to implement required interfaces to take advantage of all. However, if not needed then simply ignoring them would be a wise thing to do to implement a clean and understandable code. 

1)

using Microsoft.AspNet.Identity;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;

namespace CCClientWeb.CustomIdentity
{
    public class CustomClaimsPrincipal : IPrincipal
    {
        private CustomIdentity customIdentity;
        private CustomIdentityUser user;
        public CustomClaimsPrincipal(CustomIdentityUser user)
        {
            customIdentity = new CustomIdentity(IdentityOptions.ApplicationCookieAuthenticationType, true, user.UserName);
            this.user = user;
        }

        public IIdentity Identity
        {
            get
            {
                return customIdentity;
            }
        }

        public bool IsInRole(string role)
        {
            return user.CustomRoles.Any(cr => cr.RoleName == role);
        }
    }
    public class CustomUserClaimsPrincipalFactory : IUserClaimsPrincipalFactory<CustomIdentityUser>
    {
        public async Task<ClaimsPrincipal> CreateAsync(CustomIdentityUser user)
        {
            return await Task.FromResult(new ClaimsPrincipal(new CustomClaimsPrincipal(user)));           
        }
    }
}

2)

using System.Security.Principal;

namespace CCClientWeb.CustomIdentity
{
    public class CustomIdentity : IIdentity
    {
        private string authenticationType;
        private bool isAuthenticated = false;
        private string name;

        public CustomIdentity(string authenticationType, bool isAuthenticated, string name)
        {
            this.authenticationType = authenticationType;
            this.isAuthenticated = isAuthenticated;
            this.name = name;
        }

        public string AuthenticationType
        {
            get
            {
                return authenticationType;
            }
        }

        public bool IsAuthenticated
        {
            get
            {
                return isAuthenticated;
            }
        }

        public string Name
        {
            get
            {
                return name;
            }
        }
    }
}
3)

using Microsoft.AspNet.Identity.EntityFramework;

namespace CCClientWeb.CustomIdentity
{
    public class CustomIdentityRole:IdentityRole<int>
    {
        public override int Id
        {
            get
            {
                return base.Id;
            }

            set
            {
                base.Id = value;
            }
        }

        public override string Name
        {
            get
            {
                return base.Name;
            }

            set
            {
                base.Name = value;
            }
        }  
    }
}

4)

using Microsoft.AspNet.Identity.EntityFramework;
using System.Collections.Generic;
using System.Linq;

namespace CCClientWeb.CustomIdentity
{
    public class CustomIdentityUser:IdentityUser<int>
    {
        public override int Id
        {
            get;
            set;
        }

        public override string UserName
        {
            get;
            set;
        }

        public string Password
        {
            get;
            set;
        }
       
        public List<CustomIdentityUserRole> CustomRoles
        {
            get;
            set;
        }      

        public override string PasswordHash { get; set; }

        public override ICollection<IdentityUserRole<int>> Roles
        {
            get
            {
                return CustomRoles.Cast<IdentityUserRole<int>>().ToList();
            }
        }

        public override ICollection<IdentityUserLogin<int>> Logins
        {
            get
            {
                return base.Logins;
            }
        }

        public override bool TwoFactorEnabled
        {
            get
            {
                return base.TwoFactorEnabled;
            }

            set
            {
                base.TwoFactorEnabled = false;
            }
        }

        public override bool LockoutEnabled
        {
            get
            {
                return base.LockoutEnabled;
            }

            set
            {
                base.LockoutEnabled = false;
            }
        }

    }

    public class CustomIdentityUserRole : IdentityUserRole<int>
    {
        public string RoleName { get; set; }
    }
}

5)

using Microsoft.AspNet.Identity;
using System;
using System.Threading.Tasks;
using System.Threading;

namespace CCClientWeb.CustomIdentity
{
    public class CustomRoleStore : IRoleStore<CustomIdentityRole>
    {
        public Task<IdentityResult> CreateAsync(CustomIdentityRole role, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task<IdentityResult> DeleteAsync(CustomIdentityRole role, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public void Dispose()
        {
            
        }

        public Task<CustomIdentityRole> FindByIdAsync(string roleId, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task<CustomIdentityRole> FindByNameAsync(string normalizedRoleName, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task<string> GetNormalizedRoleNameAsync(CustomIdentityRole role, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task<string> GetRoleIdAsync(CustomIdentityRole role, CancellationToken cancellationToken)
        {
            return new Task<string>(() => { return role.Id.ToString(); });
        }

        public Task<string> GetRoleNameAsync(CustomIdentityRole role, CancellationToken cancellationToken)
        {
            return new Task<string>(() => { return role.Name.ToString(); });
        }

        public Task SetNormalizedRoleNameAsync(CustomIdentityRole role, string normalizedName, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task SetRoleNameAsync(CustomIdentityRole role, string roleName, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task<IdentityResult> UpdateAsync(CustomIdentityRole role, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
    }
}

6)

using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Authentication;
using Microsoft.AspNet.Identity;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
using System.Threading.Tasks;

namespace CCClientWeb.CustomIdentity
{
    public class CustomSignInManager : SignInManager<CustomIdentityUser>
    {
        public CustomSignInManager(CustomUserManager userManager,
            IHttpContextAccessor contextAccessor,
            CustomUserClaimsPrincipalFactory claimsFactory,
            IOptions<IdentityOptions> optionsAccessor,
            ILogger<CustomSignInManager> logger) : base(userManager, contextAccessor, claimsFactory, optionsAccessor, null)
        {
            CustomUserManager = userManager;
            CustomContext = contextAccessor.HttpContext;
            CustomUserClaimsPrincipalFactory = claimsFactory;
        }

        CustomUserManager CustomUserManager;
        HttpContext CustomContext;
        CustomUserClaimsPrincipalFactory CustomUserClaimsPrincipalFactory;

        public override async Task<SignInResult> PasswordSignInAsync(CustomIdentityUser user, string password, bool isPersistent, bool lockoutOnFailure)
        {
            if (user.Password == password)
            {
                await SignInAsync(user, isPersistent, string.Empty);
                return SignInResult.Success;
            }

            return SignInResult.Failed;
        }

        public override async Task SignInAsync(CustomIdentityUser user, bool isPersistent, string authenticationMethod = null)
        {
            var userPrincipal = await CustomUserClaimsPrincipalFactory.CreateAsync(user);
            CustomContext.Authentication.SignIn(IdentityOptions.ApplicationCookieAuthenticationScheme, userPrincipal, new AuthenticationProperties { IsPersistent = isPersistent });
        }

    }
}

7)

using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Identity;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace CCClientWeb.CustomIdentity
{
    public class CustomUserManager : UserManager<CustomIdentityUser>
    {
        public CustomUserManager(IUserStore<CustomIdentityUser> store,
            IOptions<IdentityOptions> optionsAccessor,
            IPasswordHasher<CustomIdentityUser> passwordHasher,
            IEnumerable<IUserValidator<CustomIdentityUser>> userValidators,
            IEnumerable<IPasswordValidator<CustomIdentityUser>> passwordValidators,
            ILookupNormalizer keyNormalizer,
            IdentityErrorDescriber errors,
            IEnumerable<IUserTokenProvider<CustomIdentityUser>> tokenProviders,
            ILogger<UserManager<CustomIdentityUser>> logger,
            IHttpContextAccessor contextAccessor):base(store, optionsAccessor, passwordHasher, 
                userValidators, passwordValidators, keyNormalizer, errors, tokenProviders, 
                logger, contextAccessor)
        {
           
        }

        public override async Task<bool> CheckPasswordAsync(CustomIdentityUser user, string password)
        {
            return await new Task<bool>(() => { return user.Password == password; });            
        }
    }  
}

8)

using Microsoft.AspNet.Identity;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using CC.Interfaces.RepositoryServices;
using System.Threading;

namespace CCClientWeb.CustomIdentity
{
    public partial class CustomUserStore : IUserStore<CustomIdentityUser>, IUserRoleStore<CustomIdentityUser>, IUserPasswordStore<CustomIdentityUser>
    {
        private IdentityService service;
        private IPasswordHasher<CustomIdentityUser> passwordHasher;

        public CustomUserStore(ILoginRepositoryService loginRepositoryService, IPasswordHasher<CustomIdentityUser> passwordHasher)
        {
            service = new IdentityService(loginRepositoryService, passwordHasher);
            this.passwordHasher = passwordHasher;
        }
        public Task<IdentityResult> CreateAsync(CustomIdentityUser user, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task<IdentityResult> DeleteAsync(CustomIdentityUser user, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task<CustomIdentityUser> FindByIdAsync(string userId, CancellationToken cancellationToken)
        {
            return service.GetUserByIdAsync(userId);
        }

        public Task<CustomIdentityUser> FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken)
        {
            return service.GetUserByUserNameAsync(normalizedUserName);
        }

        public Task<string> GetNormalizedUserNameAsync(CustomIdentityUser user, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task<string> GetUserIdAsync(CustomIdentityUser user, CancellationToken cancellationToken)
        {
            return new Task<string>(() => { return user.Id.ToString(); });
        }

        public Task<string> GetUserNameAsync(CustomIdentityUser user, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task SetNormalizedUserNameAsync(CustomIdentityUser user, string normalizedName, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task SetUserNameAsync(CustomIdentityUser user, string userName, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task<IdentityResult> UpdateAsync(CustomIdentityUser user, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        #region IDisposable Support
        private bool disposedValue = false; // To detect redundant calls

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    // TODO: dispose managed state (managed objects).
                }

                // TODO: free unmanaged resources (unmanaged objects) and override a finalizer below.
                // TODO: set large fields to null.

                disposedValue = true;
            }
        }

        // TODO: override a finalizer only if Dispose(bool disposing) above has code to free unmanaged resources.
        // ~CustomUserStore() {
        //   // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
        //   Dispose(false);
        // }

        // This code added to correctly implement the disposable pattern.
        public void Dispose()
        {
            // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
            Dispose(true);
            // TODO: uncomment the following line if the finalizer is overridden above.
            // GC.SuppressFinalize(this);
        }
        #endregion

   
        public Task AddToRoleAsync(CustomIdentityUser user, string roleName, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task<IList<string>> GetRolesAsync(CustomIdentityUser user, CancellationToken cancellationToken)
        {
            return service.GetUserRolesAsync(user);
        }

        public Task<IList<CustomIdentityUser>> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        public Task<bool> IsInRoleAsync(CustomIdentityUser user, string roleName, CancellationToken cancellationToken)
        {
            return service.IsUserInRoleAsync(user, roleName);
        }

        public Task RemoveFromRoleAsync(CustomIdentityUser user, string roleName, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
   
        public Task<string> GetPasswordHashAsync(CustomIdentityUser user, CancellationToken cancellationToken)
        {
            return service.GetPasswordHashAsync(user);
        }

        public Task<bool> HasPasswordAsync(CustomIdentityUser user, CancellationToken cancellationToken)
        {
            return service.HasPasswordAsync(user);
        }

        public Task SetPasswordHashAsync(CustomIdentityUser user, string passwordHash, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }
    }
}

9)

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Identity;
using sc = System.Security.Claims;
using CC.Interfaces.RepositoryServices;

namespace CCClientWeb.CustomIdentity
{
    public class IdentityService
    {
        ILoginRepositoryService _loginRepSrv = null;
        IPasswordHasher<CustomIdentityUser> passwordHasher = null;

        public IdentityService(ILoginRepositoryService loginRepository, IPasswordHasher<CustomIdentityUser> passwordHasher)
        {
            _loginRepSrv = loginRepository;
            this.passwordHasher = passwordHasher;
        }

        public async Task<CustomIdentityUser> GetUserByUserNameAsync(string userName) 
        {
            var login = await _loginRepSrv.FirstOrDefaultAsync(l => l.LoginName == userName);
            if (login == null) return null;
            var userId = login.CompanyId == null ? login.EmployeeId.Value : login.CompanyId.Value;
            return new CustomIdentityUser 
                { Id = userId, 
                    Password = login.LoginPassword, 
                    UserName = login.LoginName, 
                    CustomRoles = login.LoginsRoles.Select(lr => new CustomIdentityUserRole { RoleId = lr.RoleId, UserId = userId, RoleName = lr.Role.RoleName }).ToList()  
                };
        }

        public async Task<CustomIdentityUser> GetUserByIdAsync(string userId)
        {
            var id = int.Parse(userId);
            var login = await _loginRepSrv.FirstOrDefaultAsync(l => (l.EmployeeId == id && l.CompanyId == null) || (l.CompanyId == id && l.EmployeeId == null));
            var roles = login.LoginsRoles.Select(lr => new CustomIdentityUserRole { RoleId = lr.RoleId, UserId = id, RoleName = lr.Role.RoleName }).ToList();

            return new CustomIdentityUser
            {
                Id = login.CompanyId == null ? login.EmployeeId.Value : login.CompanyId.Value,
                Password = login.LoginPassword,
                UserName = login.LoginName,
                CustomRoles = roles
            };
        }

        public async Task<CustomIdentityUser> FindByNameAsync(string userName) 
        {
            return await GetUserByUserNameAsync(userName);
        }

        public async Task<IList<string>> GetUserRolesAsync(CustomIdentityUser user) 
        {
            var login = await _loginRepSrv.FirstOrDefaultAsync(l => (l.EmployeeId == user.Id && l.CompanyId == null) || (l.CompanyId == user.Id && l.EmployeeId == null));
            return login.LoginsRoles.Select(lr => lr.Role.RoleName).ToList();            
        }

        public async Task<bool> IsUserInRoleAsync(CustomIdentityUser user, string role)
        {
            List<string> roles = await GetUserRolesAsync(user) as List<string>;

            if (roles.Contains(role))
            {
                return true;
            }
            else
            {
                return false;
            }

        }

        public Task AddClaimAsync(CustomIdentityUser user, System.Security.Claims.Claim claim) 
        {
            return new Task(() => { 
            
            });
        }

        public Task<IList<sc.Claim>> GetClaimsAsync(CustomIdentityUser user) 
        {
            return new Task<IList<sc.Claim>>(() => {
                List<sc.Claim> claims = new List<sc.Claim>();
                claims.Add(new sc.Claim(user.CustomRoles.FirstOrDefault().RoleName, user.UserName));                
                return claims as IList<sc.Claim>;
            });            
        }

        public Task RemoveClaimAsync(CustomIdentityUser user, System.Security.Claims.Claim claim)
        {
            return new Task(() => {});
        }

        public Task<string> GetSecurityStampAsync(CustomIdentityUser user) 
        {
            return Task.FromResult("GetSecurityStampAsync");      
        }

        public Task SetSecurityStampAsync(CustomIdentityUser user, string stamp) 
        {
            return new Task(() => { });
        }

        public async Task<string> GetPasswordHashAsync(CustomIdentityUser user) 
        {
            return await Task.FromResult(passwordHasher.HashPassword(user, user.Password));
        }

        public Task<bool> HasPasswordAsync(CustomIdentityUser user) 
        {
            return Task.FromResult(true);
        }
        
        
    }
}

Once classes are written, we can prepare start-up pipeline by adding the following values in Startup.cs. I had to made sure that Authorization configuration is added before MVC as shown below.

app.UseIdentity() is also needed in Configure method in Startup.cs

Basically that is pretty much needed to wire up things to customize Identity in Asp.NET 5.

As you may notice that IndentityService.cs is my wrapper to repositories in Data layer project. I have put in reference to this project in Web project and injected services in Startup.cs. You may see that one of them is ILoginServiceRepository. This class has several methods which allows me to search users and roles.

My LogIn action in controller looks like the following.

using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNet.Authorization;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Mvc;
using CCClientWeb.Models;
using CCClientWeb.CustomIdentity;
using CC.Interfaces.RepositoryServices;

namespace CCClientWeb.Controllers
{
    [Authorize]
    public class AccountController : Controller
    {
        public AccountController(CustomUserManager userManager, CustomSignInManager signInManager)
        {
            UserManager = userManager;
            SigninManager = signInManager;            
        }

        public CustomUserManager UserManager { get; private set; }

        public CustomSignInManager SigninManager { get; private set; }

        private ILoginRepositoryService loginRepositoryService;

        private IdentityService identityService;

        //
        // GET: /Account/Login
        [HttpGet]
        [AllowAnonymous]
        public IActionResult Login(string returnUrl = null)
        {            
            ViewBag.ReturnUrl = returnUrl;
            return View();
        }

        //
        // POST: /Account/Login
        [HttpPost]
        [AllowAnonymous]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
        {
            ViewBag.ReturnUrl = returnUrl;
            if (ModelState.IsValid)
            {
                // This doesn't count login failures towards account lockout
                // To enable password failures to trigger account lockout, set shouldLockout: true
                var result = await SigninManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, false);
                if (result.Succeeded)
                {
                    return RedirectToLocal(returnUrl);
                }

                ModelState.AddModelError(string.Empty, "Invalid login attempt.");
                return View(model);
            }

            // If we got this far, something failed, redisplay form
            return View(model);
        }
}

As this implementation of Identity framework in Asp.Net 5 is a brand new, I had to put in some research. Thankfully, Asp.Net is open source now. I was able to download the code from GithHub (https://github.com/aspnet/Identity) and able to look at it to understand to implement this customization.

As I am injecting repositories into my web project, it really does not matter how I implement my repositories. It is a black box to Identity framework. As loon as it has required methods to do the job, it would work. So this way I was also able to reuse my old code.

I hope that this work may be useful to someone who may be looking forward to migrate his/her legacy applications to Asp.NET 5 but still want to re-use most of existing code base.

Friday, July 24, 2015 10:36:34 PM UTC  #       | 

I upgraded VMWare workstation 9 to 11 (build 11.1.2 - 2780323) before a month or so. Since then I was running into performance issues, especially, when I work with 3 monitors. I am not sure it was a hardware issue or software issue or combination of both. My VM host is Windows 8.1 with the latest updates and guest is Windows 10 RC.

I have docking station Toshiba Dynadock, which enables 3 monitors.

Eventually when I turned off 3D Graphics acceleration in VM Ware workstation settings, the performance improved with 3 monitors running.

This post might be helpful to someone running into the same issue and do not have any clue to fix it.

When Windows 10 RTM will be released, I will verify again by enabling 3D graphics acceleration; I guess by that time we may have some updates from VMWare too. Presently Workstation 11 is based on Windows 10 RC and they may get a chance to fix issues (if there are any) once Windows RTM is released.

I am still not sure what is causing the performance issue but at least I am okay right now and able to use 3 monitors comfortably.

 

Friday, July 24, 2015 11:43:35 AM UTC  #       | 
# Sunday, July 12, 2015

It is due to renaming of GlobalImport.cshtml to _ViewImports.cshtml.

You may rename GlobalImport.cshtml available under Views folder to _ViewImports.cshtml as shown below.

You may refer this Announcement for the details. https://github.com/aspnet/Mvc/issues/2489

Sunday, July 12, 2015 5:52:56 PM UTC  #       | 
Copyright © 2020 Mayur Bharodia. All rights reserved.
DasBlog 'Portal' theme by Johnny Hughes.
Pick a theme: