Migrating from LightSwitch

This guide demonstrates how to migrate a Microsoft LightSwitch application with Radzen.

1. Create а new Microsoft LightSwitch application that displays the Products table from the Northwind database:

2. Enable Forms authentication for application access control:

3. Publish the app to IIS using an admin user as the application administrator:

This will create the ASP.NET membership tables. LightSwitch uses those to store users, passwords, roles, etc:

4. Create a new database as a copy of the existing one used in the LightSwitch application:

5. Create a new Radzen application that displays the Products table:

6. Enable security with email confirmation:

7. Deploy the application to IIS from Radzen:

We now have the ASP.NET Core 2.0 Identity tables in our new database:

and we can use the following SQL script to migrate our users, passwords, roles, etc. to the new database:

-- THIS SCRIPT NEEDS TO RUN FROM THE CONTEXT OF THE MEMBERSHIP DB
BEGIN TRANSACTION MigrateUsersAndRoles
USE Northwind

-- INSERT USERS
INSERT INTO NorthwindNew.dbo.AspNetUsers
          (Id,
           UserName,
           NormalizedUserName,
           PasswordHash,
           SecurityStamp,
           EmailConfirmed,
           PhoneNumber,
           PhoneNumberConfirmed,
           TwoFactorEnabled,
           LockoutEnd,
           LockoutEnabled,
           AccessFailedCount,
           Email,
           NormalizedEmail)
SELECT aspnet_Users.UserId,
     aspnet_Users.UserName,
     -- The NormalizedUserName value is upper case in ASP.NET Core Identity
     UPPER(aspnet_Users.UserName),
     -- Creates an empty password since passwords don't map between the 2 schemas
     '',
     /*
      The SecurityStamp token is used to verify the state of an account and
      is subject to change at any time. It should be initialized as a new ID.
     */
     NewID(),
     /*
      EmailConfirmed is set when a new user is created and confirmed via email.
      Users must have this set during migration to reset passwords.
     */
     1,
     aspnet_Users.MobileAlias,
     CASE
       WHEN aspnet_Users.MobileAlias IS NULL THEN 0
       ELSE 1
     END,
     -- 2FA likely wasn't setup in Membership for users, so setting as false.
     0,
     CASE
       -- Setting lockout date to time in the future (1,000 years)
       WHEN aspnet_Membership.IsLockedOut = 1 THEN Dateadd(year, 1000,
                                                   Sysutcdatetime())
       ELSE NULL
     END,
     aspnet_Membership.IsLockedOut,
     /*
      AccessFailedAccount is used to track failed logins. This is stored in
      Membership in multiple columns. Setting to 0 arbitrarily.
     */
     0,
     aspnet_Membership.Email,
     -- The NormalizedEmail value is upper case in ASP.NET Core Identity
     UPPER(aspnet_Membership.Email)
FROM   aspnet_Users
     LEFT OUTER JOIN aspnet_Membership
                  ON aspnet_Membership.ApplicationId =
                     aspnet_Users.ApplicationId
                     AND aspnet_Users.UserId = aspnet_Membership.UserId
     LEFT OUTER JOIN NorthwindNew.dbo.AspNetUsers
                  ON aspnet_Membership.UserId = AspNetUsers.Id
WHERE  AspNetUsers.Id IS NULL

-- INSERT ROLES
INSERT INTO NorthwindNew.dbo.AspNetRoles(Id, Name)
SELECT RoleId, RoleName
FROM aspnet_Roles;

-- INSERT USER ROLES
INSERT INTO NorthwindNew.dbo.AspNetUserRoles(UserId, RoleId)
SELECT UserId, RoleId
FROM aspnet_UsersInRoles;

IF @@ERROR <> 0
BEGIN
  ROLLBACK TRANSACTION MigrateUsersAndRoles
  RETURN
END

COMMIT TRANSACTION MigrateUsersAndRoles

We are done!

Now we only need to reset the password from the deployed Radzen application. Microsoft LightSwitch apps needed either a desktop app or custom pages for user management. For the purpose of this blog post I’ll take the shortcut and I will enter my admin user email directly in the database:

Now we can reset the password from the deployed Radzen application:

You will receive email with a link to generate new password and you can use it to login:

If you don’t want your users to reset their passwords you can use a custom password hasher which makes ASP.NET Membership (LightSwitch) passwords work with ASP.NET Core Identity (Radzen):

We need first to copy both Password and PasswordSalt from old aspnet_Membership table:

-- THIS SCRIPT NEEDS TO RUN FROM THE CONTEXT OF THE MEMBERSHIP DB
BEGIN TRANSACTION MigrateUsersAndRoles
USE Northwind

-- INSERT USERS
INSERT INTO NorthwindNew.dbo.AspNetUsers
          (Id,
           UserName,
           NormalizedUserName,
           PasswordHash,
           SecurityStamp,
           EmailConfirmed,
           PhoneNumber,
           PhoneNumberConfirmed,
           TwoFactorEnabled,
           LockoutEnd,
           LockoutEnabled,
           AccessFailedCount,
           Email,
           NormalizedEmail)
SELECT aspnet_Users.UserId,
     aspnet_Users.UserName,
     -- The NormalizedUserName value is upper case in ASP.NET Core Identity
     UPPER(aspnet_Users.UserName),
     -- Copy both Password and PasswordSalt
     aspnet_Membership.Password + ':' + aspnet_Membership.PasswordSalt,
     /*
      The SecurityStamp token is used to verify the state of an account and
      is subject to change at any time. It should be initialized as a new ID.
     */
     NewID(),
     /*
      EmailConfirmed is set when a new user is created and confirmed via email.
      Users must have this set during migration to reset passwords.
     */
     1,
     aspnet_Users.MobileAlias,
     CASE
       WHEN aspnet_Users.MobileAlias IS NULL THEN 0
       ELSE 1
     END,
     -- 2FA likely wasn't setup in Membership for users, so setting as false.
     0,
     CASE
       -- Setting lockout date to time in the future (1,000 years)
       WHEN aspnet_Membership.IsLockedOut = 1 THEN Dateadd(year, 1000,
                                                   Sysutcdatetime())
       ELSE NULL
     END,
     aspnet_Membership.IsLockedOut,
     /*
      AccessFailedAccount is used to track failed logins. This is stored in
      Membership in multiple columns. Setting to 0 arbitrarily.
     */
     0,
     aspnet_Membership.Email,
     -- The NormalizedEmail value is upper case in ASP.NET Core Identity
     UPPER(aspnet_Membership.Email)
FROM   aspnet_Users
     LEFT OUTER JOIN aspnet_Membership
                  ON aspnet_Membership.ApplicationId =
                     aspnet_Users.ApplicationId
                     AND aspnet_Users.UserId = aspnet_Membership.UserId
     LEFT OUTER JOIN NorthwindNew.dbo.AspNetUsers
                  ON aspnet_Membership.UserId = AspNetUsers.Id
WHERE  AspNetUsers.Id IS NULL

-- INSERT ROLES
INSERT INTO NorthwindNew.dbo.AspNetRoles(Id, Name)
SELECT RoleId, RoleName
FROM aspnet_Roles;

-- INSERT USER ROLES
INSERT INTO NorthwindNew.dbo.AspNetUserRoles(UserId, RoleId)
SELECT UserId, RoleId
FROM aspnet_UsersInRoles;

IF @@ERROR <> 0
BEGIN
  ROLLBACK TRANSACTION MigrateUsersAndRoles
  RETURN
END

COMMIT TRANSACTION MigrateUsersAndRoles

Just create a server\Startup.Custom.cs file in the Radzen application with the following content:

namespace NorthwindNew
{
    public partial class Startup
    {
        partial void OnConfigureServices(IServiceCollection services)
        {
            services.AddScoped<IPasswordHasher<ApplicationUser>, CustomPasswordHasher>();
        }
    }

    public class CustomPasswordHasher : PasswordHasher<ApplicationUser>
    {
        public static string GenerateHash(string providedPassword, byte[] saltAsByteArray)
        {
            var sha = new System.Security.Cryptography.SHA1CryptoServiceProvider();

            byte[] p1 = saltAsByteArray;
            byte[] p2 = System.Text.Encoding.Unicode.GetBytes(providedPassword);

            byte[] data = new byte[p1.Length + p2.Length];

            p1.CopyTo(data, 0);
            p2.CopyTo(data, p1.Length);

            return Convert.ToBase64String(sha.ComputeHash(data));
        }

        public override PasswordVerificationResult VerifyHashedPassword(ApplicationUser user, string hashedPasswordAndSalt,
           string providedPassword)
        {
            if (hashedPasswordAndSalt.IndexOf(":") != -1)
            {
                var hashedPassword = hashedPasswordAndSalt.Split(':')[0];
                var salt = hashedPasswordAndSalt.Split(':')[1];

                var hashedProvidedPassword = GenerateHash(providedPassword, Convert.FromBase64String(salt));

                return hashedPassword == hashedProvidedPassword
                    ? PasswordVerificationResult.SuccessRehashNeeded
                    : base.VerifyHashedPassword(user, hashedPassword, providedPassword);
            }
            else
            {
                return base.VerifyHashedPassword(user, hashedPasswordAndSalt, providedPassword);
            }
        }
    }
}