Не так давно передо мной встала задача перевести проект с ASP.NET MVC 3 на MVC 4, чтобы иметь возможность использовать преимущества обновлённой платформы, такие как Bundles, минификация, улучшения движка Razor и т.п. Так же стояла задача избавиться от старого Membership провайдера и перейти к использованию SimpleMembership, доступного в MVC 4.

Перевод проекта на MVC 4

Переход с MVC 3 на MVC 4 не составил особого труда. Для этого я использовал это пошаговое руководство.

Перевод проекта на SimpleMembership

Переход на новый Membership провайдер оказался немного сложнее. Для этого понадобилось внести изменения как в базе данных, так и в коде проекта.

Изменения в базе данных

Для работы SimpleMembership в базе данных нужно 5 таблиц — 4 системные и одна пользовательская. В пользовательской таблице должны быть 2 обязательных уникальных поля: UserId типа INT и UserName типа NVARCHAR. Названия полей могут быть любыми, ссылки на них указываются из кода приложения при инициализации соединения. Таблица может хранить любые другие поля, содержащие нужную вам информацию. Если в базе данных нет подходящей таблицы, то её необходимо создать. В моём случае в базе данных уже была таблица Members, содержащая информацию о пользователях. Её я и решил использовать. Если первичный ключ в таблице типа INT, то он наилучшим образом может подойти в качестве поля UserId. В моём случае первичный ключ был типа UNIQUEIDENTIFIER, поэтому я добавил новое поле UserId, сделал его IDENTITY и задал на нём уникальный индекс. В выбранной мной таблице уже существовало уникальное поле Login, хранящее имена пользователей. Его я использовал в качестве поля UserName.

Далее я просто написал скрипт, который создаёт необходимые системные таблицы для SimpleMembership, модифицирует таблицу Members и переносит нужные данные. При миграции пользователей я не стал переносить пароли, а для простоты установил их всем по умолчанию.

Полная версия скрипта выглядит следующим образом:

ALTER TABLE Members
	ADD
		UserId INT IDENTITY(1,1) NOT NULL
GO

CREATE UNIQUE NONCLUSTERED INDEX [MemberIndex] ON [dbo].[Members]
(
	[UserId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
GO

CREATE TABLE [dbo].[webpages_Membership](
	[UserId] [int] NOT NULL,
	[CreateDate] [datetime] NULL,
	[ConfirmationToken] [nvarchar](128) NULL,
	[IsConfirmed] [bit] NULL,
	[LastPasswordFailureDate] [datetime] NULL,
	[PasswordFailuresSinceLastSuccess] [int] NOT NULL,
	[Password] [nvarchar](128) NOT NULL,
	[PasswordChangedDate] [datetime] NULL,
	[PasswordSalt] [nvarchar](128) NOT NULL,
	[PasswordVerificationToken] [nvarchar](128) NULL,
	[PasswordVerificationTokenExpirationDate] [datetime] NULL,
PRIMARY KEY CLUSTERED 
(
	[UserId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

ALTER TABLE [dbo].[webpages_Membership] ADD  DEFAULT ((0)) FOR [IsConfirmed]
GO

ALTER TABLE [dbo].[webpages_Membership] ADD  DEFAULT ((0)) FOR [PasswordFailuresSinceLastSuccess]
GO

CREATE TABLE [dbo].[webpages_OAuthMembership](
	[Provider] [nvarchar](30) NOT NULL,
	[ProviderUserId] [nvarchar](100) NOT NULL,
	[UserId] [int] NOT NULL,
PRIMARY KEY CLUSTERED 
(
	[Provider] ASC,
	[ProviderUserId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

CREATE TABLE [dbo].[webpages_Roles](
	[RoleId] [int] IDENTITY(1,1) NOT NULL,
	[RoleName] [nvarchar](256) NOT NULL,
PRIMARY KEY CLUSTERED 
(
	[RoleId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY],
UNIQUE NONCLUSTERED 
(
	[RoleName] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

CREATE TABLE [dbo].[webpages_UsersInRoles](
	[UserId] [int] NOT NULL,
	[RoleId] [int] NOT NULL,
PRIMARY KEY CLUSTERED 
(
	[UserId] ASC,
	[RoleId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

ALTER TABLE [dbo].[webpages_UsersInRoles]  WITH CHECK ADD  CONSTRAINT [fk_RoleId] FOREIGN KEY([RoleId])
REFERENCES [dbo].[webpages_Roles] ([RoleId])
GO

ALTER TABLE [dbo].[webpages_UsersInRoles] CHECK CONSTRAINT [fk_RoleId]
GO

ALTER TABLE [dbo].[webpages_UsersInRoles]  WITH CHECK ADD  CONSTRAINT [fk_UserId] FOREIGN KEY([UserId])
REFERENCES [dbo].[Members] ([UserId])
GO

ALTER TABLE [dbo].[webpages_UsersInRoles] CHECK CONSTRAINT [fk_UserId]
GO

INSERT INTO webpages_Membership 
(
	UserId
	, CreateDate
	, IsConfirmed
	, PasswordFailuresSinceLastSuccess
	, [Password]
	, PasswordChangedDate
	, PasswordSalt
)
SELECT
	UserId
	, GETDATE()
	, 1
	, 0
	, 'AOVA4DmdUVWzcNwA0HlHWSQdb0h1ty4EOVB4eHs43a9qQSsyVCnpK2eCspWZdkqPbA=='
	, GETDATE()
	, ''
FROM Members
GO

INSERT INTO webpages_Roles
SELECT RoleName
FROM aspnet_Roles
GO

DECLARE @user_guid UNIQUEIDENTIFIER;
DECLARE @role_guid UNIQUEIDENTIFIER;

DECLARE @user_id INT;
DECLARE @role_id INT;

DECLARE cur CURSOR FOR SELECT UserId, RoleId FROM aspnet_UsersInRoles

OPEN cur;

FETCH NEXT FROM cur INTO @user_guid, @role_guid;

WHILE @@FETCH_STATUS = 0
BEGIN
	BEGIN TRANSACTION
		SELECT @user_id = UserId FROM Members WHERE [Login] = (SELECT UserName FROM aspnet_Users WHERE UserId = @user_guid);
		SELECT @role_id = RoleId FROM webpages_Roles WHERE RoleName = (SELECT RoleName FROM aspnet_Roles WHERE RoleId = @role_guid);
		INSERT INTO webpages_UsersInRoles
		(
			UserId
			, RoleId
		)
		VALUES
		(
			@user_id
			, @role_id
		);
	COMMIT;
	FETCH NEXT FROM cur INTO @user_guid, @role_guid;
END

CLOSE cur;
DEALLOCATE cur;
GO

Изменения в коде проекта

После внесения изменений в структуру базы данных необходимо изменить соответствующим образом и код проекта.

Для начала необходимо обновить контроллер AccountController, чтобы он мог использовать SimpleMembership. Для этого я создал новое MVC 4 Internet Application и просто скопировал оттуда код контроллера AccountController в своё приложение, удалив атрибут [InitializeSimpleMembership]. Этот атрибут нужен для создания системных таблицы в базе данных, если они не существуют. Так как мы создали всё заранее, он нам не нужен. Если вы раньше изменяли AccountController, то конечно придётся учесть эти изменения и в новом коде.

SimpleMembership использует вместо класса Membership класс WebSecurity, поэтому нужно заменить все обращения к Membership на WebSecurity, если таковые есть.

И далее самое главное: нужно при старте приложения проинициализировать подключение к базе данных для SimpleMembership. Это делается в функции Application_Start файла Global.asax как показано ниже:

protected void Application_Start()
{
    ...
    WebSecurity.InitializeDatabaseConnection
        ("ConnectionName", "Members", "UserId", "Login", false);
}

Здесь ConnectionName — это имя строки соединения в файле Web.config, Members — это таблица, в которой хранится информация о пользователях, UserId и Login — это имена тех полей в таблице, которые будут использоваться как идентификатор и имя пользователя соответственно. Последний булев параметр указывает, нужно ли создавать таблицы, если их ещё не существует в базе данных. Так как мы создали всё вручную скриптом, то указываем false.

В принципе всё готово для использования приложения с новым Membership провайдером. Запустите приложение и попробуйте войти. Если всё было сделано правильно, то система должна вас авторизовать. После того как вы убедились, что всё работает корректно, можно удалить из базы данных все таблицы, представления и хранимые процедуры с префиксом aspnet, относящиеся к старому Membership. Скрипт для удаления приведён ниже:

DROP TABLE aspnet_Paths
GO

DROP TABLE aspnet_Roles
GO

DROP TABLE aspnet_Users
GO

DROP TABLE aspnet_Applications
GO

DROP TABLE aspnet_Membership
GO

DROP TABLE aspnet_PersonalizationAllUsers
GO

DROP TABLE aspnet_PersonalizationPerUser
GO

DROP TABLE aspnet_Profile
GO

DROP TABLE aspnet_SchemaVersions
GO

DROP TABLE aspnet_UsersInRoles
GO

DROP TABLE aspnet_WebEvent_Events
GO

DROP VIEW vw_aspnet_Applications
GO

DROP VIEW vw_aspnet_MembershipUsers
GO

DROP VIEW vw_aspnet_Profiles
GO

DROP VIEW vw_aspnet_Roles
GO

DROP VIEW vw_aspnet_Users
GO

DROP VIEW vw_aspnet_UsersInRoles
GO

DROP VIEW vw_aspnet_WebPartState_Paths
GO

DROP VIEW vw_aspnet_WebPartState_Shared
GO

DROP VIEW vw_aspnet_WebPartState_User
GO

DROP PROC aspnet_AnyDataInTables
GO

DROP PROC aspnet_Applications_CreateApplication
GO

DROP PROC aspnet_CheckSchemaVersion
GO

DROP PROC aspnet_Membership_ChangePasswordQuestionAndAnswer
GO

DROP PROC aspnet_Membership_CreateUser
GO

DROP PROC aspnet_Membership_FindUsersByEmail
GO

DROP PROC aspnet_Membership_FindUsersByName
GO

DROP PROC aspnet_Membership_GetAllUsers
GO

DROP PROC aspnet_Membership_GetNumberOfUsersOnline
GO

DROP PROC aspnet_Membership_GetPassword
GO

DROP PROC aspnet_Membership_GetPasswordWithFormat
GO

DROP PROC aspnet_Membership_GetUserByEmail
GO

DROP PROC aspnet_Membership_GetUserByName
GO

DROP PROC aspnet_Membership_GetUserByUserId
GO

DROP PROC aspnet_Membership_ResetPassword
GO

DROP PROC aspnet_Membership_SetPassword
GO

DROP PROC aspnet_Membership_UnlockUser
GO

DROP PROC aspnet_Membership_UpdateUser
GO

DROP PROC aspnet_Membership_UpdateUserInfo
GO

DROP PROC aspnet_Paths_CreatePath
GO

DROP PROC aspnet_Personalization_GetApplicationId
GO

DROP PROC aspnet_PersonalizationAdministration_DeleteAllState
GO

DROP PROC aspnet_PersonalizationAdministration_FindState
GO

DROP PROC aspnet_PersonalizationAdministration_GetCountOfState
GO

DROP PROC aspnet_PersonalizationAdministration_ResetSharedState
GO

DROP PROC aspnet_PersonalizationAdministration_ResetUserState
GO

DROP PROC aspnet_PersonalizationAllUsers_GetPageSettings
GO

DROP PROC aspnet_PersonalizationAllUsers_ResetPageSettings
GO

DROP PROC aspnet_PersonalizationAllUsers_SetPageSettings
GO

DROP PROC aspnet_PersonalizationPerUser_GetPageSettings
GO

DROP PROC aspnet_PersonalizationPerUser_ResetPageSettings
GO

DROP PROC aspnet_PersonalizationPerUser_SetPageSettings
GO

DROP PROC aspnet_Profile_DeleteInactiveProfiles
GO

DROP PROC aspnet_Profile_DeleteProfiles
GO

DROP PROC aspnet_Profile_GetNumberOfInactiveProfiles
GO

DROP PROC aspnet_Profile_GetProfiles
GO

DROP PROC aspnet_Profile_GetProperties
GO

DROP PROC aspnet_Profile_SetProperties
GO

DROP PROC aspnet_RegisterSchemaVersion
GO

DROP PROC aspnet_Roles_CreateRole
GO

DROP PROC aspnet_Roles_DeleteRole
GO

DROP PROC aspnet_Roles_GetAllRoles
GO

DROP PROC aspnet_Roles_RoleExists
GO

DROP PROC aspnet_Setup_RemoveAllRoleMembers
GO

DROP PROC aspnet_Setup_RestorePermissions
GO

DROP PROC aspnet_UnRegisterSchemaVersion
GO

DROP PROC aspnet_Users_CreateUser
GO

DROP PROC aspnet_Users_DeleteUser
GO

DROP PROC aspnet_UsersInRoles_AddUsersToRoles
GO

DROP PROC aspnet_UsersInRoles_FindUsersInRole
GO

DROP PROC aspnet_UsersInRoles_GetRolesForUser
GO

DROP PROC aspnet_UsersInRoles_GetUsersInRoles
GO

DROP PROC aspnet_UsersInRoles_IsUserInRole
GO

DROP PROC aspnet_UsersInRoles_RemoveUsersFromRoles
GO

DROP PROC aspnet_WebEvent_LogEvent
GO