One ASP.NET及完美的開發體驗 - ASP.NET MVC 5

One ASP.NET及完美的開發體驗 - ASP.NET MVC 5

作者:陳傳興 ( Bruce, 微軟最有價值專家 )

 

註 – 本篇說明了 ASP.NET改進及 Visual Studio 2013的應用,使用Web Forms或是 MVC 開發技術者皆可閱讀,部份畫面及內容已更新為 VS 2013 RC版本

 

ASP.NET MVC從2007年CTP方式釋出,2009年正式發行ASP.NET MVC 1.0版,2010年發行ASP.NET MVC 2.0版,2011年發行ASP.NET MVC 3.0版,2012年發行ASP.NET MVC 4.0版,ASP.NET MVC已經發展為一個成熟的Model-View-Controller設計模式的開發框架。

 

ASP.NET MVC 5帶來全新的開發者體驗,One ASP.NET的整合新範本系統,經由單一入口即可完成所有Web範本的選擇,各種Web範本之間也可以靈活組合出新範本,例如,Web Forms + Web API等。MVC 5提供更有彈性的會員認證系統,透過Code First的方式開發人員不在受限於固定的Schema,可自由定義所需的資料Schema。採用Bootstrap設計的全新Web範本介面,Bootstrap提供高度彈性,讓開發人員也能快速擁有架構頁面介面及開發出支援響應式網頁 (Responsive Web)的能力。

One ASP.NET

隨著Visual Studio 2013的發行,開發工具又向前邁出大一步,它正在統一一致的體驗,現在,你能夠實現相同功能集,無論你如何開始建置ASP.NET應用程式。例如,開發人員可以在Web Forms專案中透過支架(Scaffold)加入Web API,開發人員可以自由混合選擇你所想要的技術。One ASP.NET是為了讓開發人員在所愛的ASP.NET裡更容易做想做的事,One ASP.NET應該給開發人員信心,無論他們選擇什麼,他們仍然是在一個可信任基礎框架-ASP.NET-下進行開發。

MVC專案範本無縫地整合成一個全新的One ASP.NET體驗。透過One ASP.NET的專案建立精靈,開發人員可以客製化MVC專案和組態認證。MVC專案現在是標準Web應用程式的一部分且不再使用它們自己的專案GUID。

 

 

圖一

圖一為Visual Studio 2013預設的【Web範本】,其中只有一個範本【ASP.NET Web應用程式】。當我們選擇【ASP.NET Web應用程式】專案範本可見到圖二:

 

 

圖二

 

圖二是一個統一介面,所有的【ASP.NET Web應用程式】都集中於此介面之中,新增ASP.NET專案介面之中提供了以下範本:

«  Empty

«  Web Forms

«  MVC

«  Web API

«  SPA

«  Facebook

 

如果在我們選擇的範本裡想要增加其他框加的參考,可以圖三之處勾選想加入的框加參考,Visual Studio 2013會加入最小所需的額外參考與NuGet套件。

 

 

圖三

 

支架(Scaffolding)

 

«  Visual Studio 2013新的支架UI介面。

«  完整重寫MVC與WebForms支架。

n   現在,從任何Web應用程式使用ASP.NET支架可以建立MVC的Views與Controller。

«  MVC支架支援已存在的非MVC或Web API專案。

n   使一個MVC或Web API的專案安裝所有必要的NuGet套件,加入所有MVC相依的檔案與資料夾,更新Web.Config組態,更新Global.asax。

n   允許單獨使用支架僅僅加入最少或完整的MVC 5依賴關係組態。

u  在極少選項,僅為MVC安裝最少量的NuGet套件。

u  在完整選項,也僅安裝最少量NuGet套件+Layout頁面+錯誤頁面+App_Start組態檔案+Scripts。

 

ASP.NET Identity

 

ASP.NET Identity是一套全新的會員身分系統(membership system)用於建置ASP.NET應用程式。ASP.NET Identity便於整合使用者特定設定檔(profile)資料與應用程式資料。ASP.NET Identity還允許開發人員控制應用程式的保存模型(persistence model),開發人員可選擇將資料儲存在SQL Server資料庫或是其住保存儲存區。

Visual Studio 2013更新MVC 5專案範本使用ASP.NET Identity進行身份認證與身份管理。透過圖二的【設定驗證】可進行驗證相關設定:

 

不驗證

 

 

圖四

 

如果你的應用程式不需要有任何驗證設定,請選取此選項。

 

個別使用者帳戶

 

 

圖五

 

你可以對應用程式中的使用者帳戶有完整控制能力。使用者將能夠向你的應用程式登錄帳戶,方法有在網站上建立使用者名稱和密碼,以及透過Facebook、Google、Microsoft Account、Twitter等社交網站的帳戶登入。

 

Windows驗證

 

 

圖六

 

如果你計劃在本身的區域網路執行你的應用程式,可以使用Windows驗證。使用者將以自己的本機或網域帳戶進行驗證。

 

個別使用者帳戶

 

如果選擇使用「個別使用者帳戶」,應用程式將會使用ASP.NET會員身分系統(membership system)進行使用者驗證。ASP.NET會員身分系統能夠讓使用者在網站上使用名稱與密碼來註冊帳戶,或者,使用社交網站如Facebook、Google、Microsoft Account或Twitter來進行登入。ASP.NET會員身分系統將使用者資訊(profile information)儲存於SQL Server LocalDB資料庫,在正式網站,也選擇發行至SQL Server或Windows Azure SQL Database。

 

就功能而言,Visual Studio 2013與Visual Studio 2012都是一樣的,但底層的ASP.NET會員身分系統的程式碼已經完全重新改寫,新程式碼的優點有:

 

«  新會員身分系統是基於OWIN,而不是ASP.NET表單驗證模組。這意味著開發人員可以使用相同的身分驗證機制,無論是在IIS裡的MVC或Web Forms,或是Self-Host的Web API或SignalR。

«  新會員身分系統資料庫是透過Entity Framework Code First來管理,所有的表格都是實體類別來呈現,開發人員可以修改Code First類別內容。這意味著開發人員可以輕鬆地自訂資料庫結構和設定檔相關的Web使用者介面以適應自己的需要,開發人員可以輕鬆地透過Code First遷移(migration)發行更新。

 

新會員身分系統已在Visual Studio 2013新範本中實作,它也可以在任何.NET Framework 4.5或更高的專案中手動實作。新ASP.NET會員身分系統是建置Internet網站且有外部使用者驗證需求的一個好選擇。

 

IdentityConfig與IdentityModels

 

如果新增一個預設的MVC專案範本(預設使用「個別使用者帳戶」驗證),在App_Start資料夾中可發現一個新的IdentityConfig.[cs | vb]組態檔。

 

using System;

using System.Collections.Generic;

using System.Data.Entity;

using System.Security.Claims;

using System.Security.Principal;

using System.Threading.Tasks;

using System.Web;

using System.Web.Helpers;

using Microsoft.AspNet.Identity;

using Microsoft.AspNet.Identity.EntityFramework;

using WebApplication2;

using WebApplication2.Models;

 

namespace WebApplication2

{

    public static class IdentityConfig

    {

        public const string LocalLoginProvider = "Local";

 

        public static IUserSecretStore Secrets { get; set; }

        public static IUserLoginStore Logins { get; set; }

        public static IUserStore Users { get; set; }

        public static IRoleStore Roles { get; set; }

        public static string RoleClaimType { get; set; }

        public static string UserNameClaimType { get; set; }

        public static string UserIdClaimType { get; set; }

        public static string ClaimsIssuer { get; set; }

 

        public static void ConfigureIdentity()

        {

            var dbContextCreator = new DbContextFactory<IdentityDbContext>();

            Secrets = new EFUserSecretStore<UserSecret>(dbContextCreator);

            Logins = new EFUserLoginStore<UserLogin>(dbContextCreator);

            Users = new EFUserStore<User>(dbContextCreator);

            Roles = new EFRoleStore<Role, UserRole>(dbContextCreator);

            RoleClaimType = ClaimsIdentity.DefaultRoleClaimType;

            UserIdClaimType = "https://schemas.microsoft.com/aspnet/userid";

            UserNameClaimType = "https://schemas.microsoft.com/aspnet/username";

            ClaimsIssuer = ClaimsIdentity.DefaultIssuer;

            AntiForgeryConfig.UniqueClaimTypeIdentifier = IdentityConfig.UserIdClaimType;

        }

 

        public static IList<Claim> RemoveUserIdentityClaims(IEnumerable<Claim> claims)

        {

            List<Claim> filteredClaims = new List<Claim>();

            foreach (var c in claims)

            {

                if (c.Type != ClaimTypes.Name &&

                    c.Type != ClaimTypes.NameIdentifier)

                {

                    filteredClaims.Add(c);

                }

            }

            return filteredClaims;

        }

 

        public static void AddRoleClaims(IEnumerable<string> roles, IList<Claim> claims)

        {

            foreach (string role in roles)

             {

                claims.Add(new Claim(RoleClaimType, role, ClaimsIssuer));

            }

        }

 

        public static void AddUserIdentityClaims(string userId, string displayName, IList<Claim> claims)

        {

            claims.Add(new Claim(ClaimTypes.Name, displayName, ClaimsIssuer));

            claims.Add(new Claim(UserIdClaimType, userId, ClaimsIssuer));

            claims.Add(new Claim(UserNameClaimType, displayName, ClaimsIssuer));

        }

 

        public static void SignIn(HttpContextBase context, IEnumerable<Claim> userClaims, bool isPersistent)

        {

            context.SignIn(userClaims, ClaimTypes.Name, RoleClaimType, isPersistent);

        }

    }

}

 

namespace Microsoft.AspNet.Identity

{

    public static class IdentityExtensions

    {

        public static string GetUserName(this IIdentity identity)

        {

            return identity.Name;

        }

 

        public static string GetUserId(this IIdentity identity)

        {

            ClaimsIdentity ci = identity as ClaimsIdentity;

            if (ci != null)

            {

                return ci.FindFirstValue(WebApplication2.IdentityConfig.UserIdClaimType);

            }

            return String.Empty;

        }

 

        public static string FindFirstValue(this ClaimsIdentity identity, string claimType)

        {

            Claim claim = identity.FindFirst(claimType);

            if (claim != null)

            {

                return claim.Value;

            }

            return null;

        }

    }

}

 

Models資料夾中也有一個新的IdentityModels.[cs | vb]。

 

using System;

using System.Collections.Generic;

using System.ComponentModel.DataAnnotations;

using System.ComponentModel.DataAnnotations.Schema;

using System.Data.Entity;

using System.Data.Entity.Infrastructure;

using System.Data.Entity.Validation;

using System.Linq;

using Microsoft.AspNet.Identity;

using Microsoft.AspNet.Identity.EntityFramework;

 

namespace WebApplication2.Models

{

    public class User : IUser

    {

        public User()

            : this(String.Empty)

        {

        }

 

        public User(string userName)

        {

            UserName = userName;

            Id = Guid.NewGuid().ToString();

        }

 

        [Key]

        public string Id { get; set; }

 

        public string UserName { get; set; }

    }

 

    public class UserLogin : IUserLogin

    {

        [Key, Column(Order = 0)]

        public string LoginProvider { get; set; }

        [Key, Column(Order = 1)]

        public string ProviderKey { get; set; }

 

        public string UserId { get; set; }

 

        public UserLogin() { }

 

        public UserLogin(string userId, string loginProvider, string providerKey)

        {

            LoginProvider = loginProvider;

            ProviderKey = providerKey;

            UserId = userId;

        }

    }

 

    public class UserSecret : IUserSecret

    {

        public UserSecret()

        {

        }

 

        public UserSecret(string userName, string secret)

        {

            UserName = userName;

            Secret = secret;

        }

 

        [Key]

        public string UserName { get; set; }

        public string Secret { get; set; }

    }

 

    public class UserRole : IUserRole

    {

        [Key, Column(Order = 0)]

        public string RoleId { get; set; }

        [Key, Column(Order = 1)]

        public string UserId { get; set; }

    }

 

    public class Role : IRole

    {

        public Role()

            : this(String.Empty)

        {

        }

 

        public Role(string roleName)

        {

            Id = roleName;

        }

 

        [Key]

        public string Id { get; set; }

    }

 

    public class IdentityDbContext : DbContext

    {

        public IdentityDbContext()

            : base("DefaultConnection")

        {

        }

 

        public IdentityDbContext(string nameOrConnectionString)

            : base(nameOrConnectionString)

        {

        }

 

        protected override DbEntityValidationResult ValidateEntity(DbEntityEntry entityEntry, IDictionary<object, object> items)

        {

            if (entityEntry.State == EntityState.Added)

            {

                User user = entityEntry.Entity as User;

                if (user != null && Users.Where(u => u.UserName.ToUpper() == user.UserName.ToUpper()).Count() > 0)

                {

                    var result = new DbEntityValidationResult(entityEntry, new List<DbValidationError>());

                    result.ValidationErrors.Add(new DbValidationError("User", "User name must be unique."));

                    return result;

                }

            }

            return base.ValidateEntity(entityEntry, items);

        }

 

        public DbSet<User> Users { get; set; }

        public DbSet<UserSecret> Secrets { get; set; }

        public DbSet<UserLogin> UserLogins { get; set; }

        public DbSet<Role> Roles { get; set; }

        public DbSet<UserRole> UserRoles { get; set; }

    }

}

 

更詳細的ASP.NET Identity可參考blogs.msdn.com的《Introducing ASP.NET Identity – A membership system for ASP.NET applications》[1]介紹。

 

認證過濾器

 

認證過濾器(Authentication filters)是ASP.NET MVC一個新類型的過濾器,在ASP.NET MVC管線中會優先處理認證過濾器,並且可以在每一Action方法或每一Controller或全域設置至全部Controller的認證邏輯。認證過濾器會在請求中處理使用者憑證(credentials)並提供相對應的主體(principal)。認證過濾器還可以在未經授權的請求中加入驗證挑戰(authentication challenge)至回應裡。

 

過濾器覆寫

 

現在,透過指定覆寫過濾器(override filter),你可以把覆寫過濾器用於給定的Action方法或Controller,覆寫過濾器應該指定一組不應執行的範圍(Action或Controller)的過濾器型別。這樣允許開發人員組態過濾器並套用至全域配置,然後排除特定的全域過濾器套用至特定的Action或Controller上。

Bootstrap

 

Visual Studio 2013更新MVC 5專案範本使用Bootstrap[2]框架重新設計_Layout及Views的版面與規劃,以提供時尚和反應迅速的外觀和感覺。

 

Note

Bootstrap在2013/8/19推出Version 3.0.0[3]正式版。目前Visual Studio 2013 Preview使用的是Bootstrap Version 2.3.1,兩個版本上使用上有差異性。未來Visual Studio 2013 RTM會採用Bootstrap 3.0.0版。

 

圖七

 

MVC範本預設引用Bootstrap相關CSS。

 

 

圖八

 

BundleConfig預設加入Bootstrap相關CSS與JavaScript組態。

 

 

圖九

 

_Layout頁面利用Bootstrap的重新改寫。

 

Tip

Bootstrap的透過設定CSS的class屬性與data-*屬性來產生效果。

 

 

圖十

 

MVC 5預設Layout畫面。

 

 

圖十一

 

利用Bootstrap的頁面擁有響應式能力 (Responsive Web)。

 

結語

 

相較於MVC的前幾個版本,此次MVC 5就MVC本身而言並無太大改變,主要是在安全性方面的提升。支架的使用與前幾版差異較大。另外,首次採用Bootstrap框架,Bootstrap可有效解決開發人員在頁面資料流安排上的弱點,Bootstrap在國外是非常火紅的技術,也有大量技術資源可以取得,例如,基於Bootstrap技術的套面資源、拖拉動態產生Bootstrap版面等。

 

參考資料

 

  1. https://www.asp.net/vnext/overview/latest/release-notes
  2. https://blogs.msdn.com/b/webdev/archive/2013/06/27/introducing-asp-net-identity-membership-system-for-asp-net-applications.aspx
  3. https://getbootstrap.com/2.3.2/ - Bootstrap 2.3.2文件
  4. https://twbs.github.io/bootstrap/ - Bootstrap 3.0.0文件
  5. https://kkbruce.tw - Bootstrap正體中文文件。

 


[1] https://blogs.msdn.com/b/webdev/archive/2013/06/27/introducing-asp-net-identity-membership-system-for-asp-net-applications.aspx

[2] https://getbootstrap.com/2.3.2/

[3] https://twbs.github.io/bootstrap/

 

 

 

 

延伸閱讀