愛流浪的小風

技術隨手寫

在 Mac 上使用 Asp.Net 5 與 Entity Framework 建立第一個 Blog 網站

| Comments

幾乎學習所有Web開發程式語言後的第一個綜合練習課題就是如何建立一個 Blog ,這也是因為 Blog 的功能剛好滿足了使用程式語言與資料庫連接的基本操作,而在 Asp.Net 5 之後,由於跨平台支援的特性,也代表著我們不需要開 Windows 就可以撰寫 Asp.Net 的應用程式,但離開 Windows 之後,也意味著沒有了 Visual Studio 來幫助我們進行建立專案和開發,所有的開發方法和步驟都是新的嘗試,所以再度拿出這個練習題來熟悉一下再好不過了,以下範例的進行主要是以在 Mac 上使用 Visual Studio Code 和 Console,搭配 Asp.Net 5 和 Entity Framework 7 的 RC1 版來進行操作,練習如何從無到有撰寫一個 Blog 的網站。

Asp.Net 版本確認

在開始進行開發之前,必須要先確認 Asp.Net 5 的版本是否已確實安裝至 Coreclr 的 RC1 版,由於目前版本變動的幅度還比較大,如果之前就已經有在 Mac 上安裝好 Asp.Net 5 環境的話,可能必須要再確認是否有升級到最新版本,特別需要注意不要使用到 Mono 版的 Runtime ,必須要使用 Coreclr 版的 Runtime。

  1. 打開 Terminal ,使用以下指令升級 dnvm


    dnvm update-self

  2. 升級 dnx coreclr 版本的 runtime 至 rc1


    dnvm upgrade -r coreclr

  3. 確認使用的版本為 1.0.0-rc1-final , Runtime 是 Coreclr


    dnvm list

建立 Blog 網站框架

在 Mac 環境上,由於沒有 Visual Studio ,所以微軟基於 yoeman 開發了一套使用 Command 產生開發範本的工具 generator-aspnet ,所有建立網站或是新增 Class 的工作都可以在 Terminal 透過 Command 來進行新增,這一點也會跟原本的操作習慣比較不一樣,但是與其他語言(例如 Ror )的使用方式更貼近,目前 Visual Studio Code 還沒有將新增程式範本的功能整合進去,或許未來會加到 Visual Studio Code 之中,不過如果想要在其他平台上開發或使用 .Net 應用程式的話,還是盡早的熟悉一下 Command 的操作會更好一些。

  1. 打開 Terminal ,使用指令產生 Asp.Net 5的專案程式碼,選擇 Web Application Basic [without Membership and Authorization]


    yo aspnet

  2. 輸入網站名稱 Blogging

  3. 切換至網站目錄,還原套件並執行網站


    cd Blogging
    dnu restore #還原套件
    dnx web

  4. 打開瀏覽器,開啟 http://localhost:5000 ,可以看到網站成功執行

安裝 EntityFramework 7

在 Windows 上使用 EntityFramework 進行網站開發時,我們通常會選擇使用 Localdb 來搭配作為資料庫的測試環境,而在 Mac 上是沒有 SqlServer 可以使用的,所以在這次的範例中,我會像大家介紹如何使用 EntityFramework 與 Sqlite 進行搭配,作為開發環境使用測試。

  1. 打開 Terminal ,安裝 EntityFramework 與 Sqlite 所需要的 Nuget 套件


    dnu install EntityFramework.Commands
    dnu install EntityFramework.Sqlite

  2. 安裝成功之後,可以在 project.json 中,看到這兩項出現在 dependencies

  3. 為了方便之後進行資料庫操作,在 project.json 建立 EntityFramework 操作的捷徑,在 project.json 的 commands 區段新增以下指令


    "ef": "EntityFramework.Commands"

  4. 在 Terminal 測試指令是否正確


    dnx ef

建立 Blog 資料庫與欄位

  1. 打開 Terminal ,建立 Models 資料夾,以及建立 BlogDbContext 作為操作資料庫的媒介


    mkdir Models
    cd Models
    yo aspnet:Class BlogDbContext

    這邊我們也使用了 generator-aspnet 來幫助我們建立 Class ,建立的 Class 會像這樣

  2. 修改 BlogDbContext 繼承 DbContext ,新增 Post Property,記得 using Microsoft.Data.Entity

    ```
    using Microsoft.Data.Entity;

    public class BlogDbContext: DbContext
    {
    public DbSet Posts { get; set; }
    }
    ```

  3. 新增 Post Class,設定所需要的欄位


    yo aspnet:Class Post

    Post 的資料欄位

    ```
    using System.ComponentModel.DataAnnotations;

    public class Post
    {
    [Key]
    public int Id { get; set; }

    [Required]
    public string Title { get; set; }

    public string Body { get; set; }

    public DateTime CreatedAt { get; set; }
    }
    ```

  4. 打開網站根目錄的 Startup.cs ,修改 ConfigureServices 方法,將 BlogDbContext 註冊到 Asp.Net 5 所內建的 DI Framework 中,並設定使用 Sqlite

    ```
    using System.IO;
    using Microsoft.Data.Entity;
    using Microsoft.Extensions.PlatformAbstractions;

    public void ConfigureServices(IServiceCollection services)
    {

    // Add Entity Framework
    var path = PlatformServices.Default.Application.ApplicationBasePath;
    services.AddEntityFramework()
    .AddSqlite()
    .AddDbContext(
    option =>
    option.UseSqlite("Filename=" + Path.Combine(path, "blog.db")));

    // Add framework services.
    services.AddMvc();
    }
    ```

  5. 在網站根目錄打開 Terminal ,建立新的 Migration 並更新資料庫


    dnx ef migrations add InitialDatabase
    dnx ef database update

    操作畫面

  6. 我們可以在網站根目錄下發現建立了 blog.db ,如果在 Mac 上想要瀏覽 sqlite 資料庫的話,也可以使用這套 Open Source 的 sqlite 瀏覽工具 sqlitebrowser

新增及瀏覽 Blog 功能

準備完資料庫以及資料表之後,接下來我們要繼續把 Blog 的新刪修查功能完成,這部分與原本的 Asp.Net Mvc 使用方式幾乎是一模一樣。

  1. 在根目錄打開 Terminal ,新增 PostController


    yo aspnet:MvcController PostController

  2. 打開 PostController ,增加新刪修查的程式碼

    ```
    public class PostController : Controller
    {
    private BlogDbContext blogDbContext;

    public PostController(BlogDbContext dbContext){
        this.blogDbContext = dbContext;
    }
    
    // GET: /<controller>/
    public IActionResult Index()
    {
        var posts = this.blogDbContext.Posts.ToList();
    
        return View(posts);
    }
    
     // GET: Posts/Details/5
    public ActionResult Details(int? id)
    {
        if (id == null)
        {
            return new BadRequestResult();
        }
    
        Post post = this.blogDbContext.Posts.Where(i=> i.Id == id).FirstOrDefault();
        if (post == null)
        {
            return HttpNotFound();
        }
    
        return View(post);
    }
    
    // GET: Posts/Create        
    public IActionResult Create()
    {
        return View();
    }
    
    // POST: Posts/Create
    // 若要免於過量張貼攻擊,請啟用想要繫結的特定屬性,如需
    // 詳細資訊,請參閱 http://go.microsoft.com/fwlink/?LinkId=317598。
    [HttpPost]
    [ValidateAntiForgeryToken]
    public IActionResult Create([Bind("Id,Title,Body,CreatedAt")] Post post)
    {
        if (ModelState.IsValid)
        {
            this.blogDbContext.Posts.Add(post);
            this.blogDbContext.SaveChanges();
            return RedirectToAction("Index");
        }
    
        return View(post);
    }
    
    // GET: Posts/Edit/5
    public ActionResult Edit(int? id)
    {
        if (id == null)
        {
            return new BadRequestResult();
        }
    
        Post post = this.blogDbContext.Posts.Where(i=> i.Id == id).FirstOrDefault();
        if (post == null)
        {
            return HttpNotFound();
        }
    
        return View(post);
    }
    
    // POST: Posts/Edit/5
    // 若要免於過量張貼攻擊,請啟用想要繫結的特定屬性,如需
    // 詳細資訊,請參閱 http://go.microsoft.com/fwlink/?LinkId=317598。
    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Edit([Bind("Id,Title,Body,CreatedAt")] Post post)
    {
        if (ModelState.IsValid)
        {
            this.blogDbContext.Entry(post).State = EntityState.Modified;
            this.blogDbContext.SaveChanges();
            return RedirectToAction("Index");
        }
        return View(post);
    }
    
    // GET: Posts/Delete/5
    public ActionResult Delete(int? id)
    {
        if (id == null)
        {
            return new BadRequestResult();
        }
    
        Post post = this.blogDbContext.Posts.Where(i=> i.Id == id).FirstOrDefault();
        if (post == null)
        {
            return HttpNotFound();
        }
    
        return View(post);
    }
    
    // POST: Posts/Delete/5
    [HttpPost, ActionName("Delete")]
    [ValidateAntiForgeryToken]
    public ActionResult DeleteConfirmed(int id)
    {
        Post post = this.blogDbContext.Posts.Where(i=> i.Id == id).FirstOrDefault();
        this.blogDbContext.Posts.Remove(post);
        this.blogDbContext.SaveChanges();
        return RedirectToAction("Index");
    }
    
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            this.blogDbContext.Dispose();
        }
        base.Dispose(disposing);
    }
    

    }
    ```

  3. 建立新刪修查的View頁面

  4. 執行程式,可以成功的操作 Blog 的新刪修查功能

小結

新版本的 Asp.Net 5 開放了在 Windows 之外的平台也能進行開發的新功能,也因為不同作業系統和平台的關係,在開發上的習慣和操作方式也不一樣,但這也讓我們看到未來 Asp.Net 5 可能在更多不同環境的操作下產生更多應用與發展的可能性,如果對於在非 Windows 平台上開發 Asp.Net 5 的朋友,可以趁早熟悉並習慣一下這些新的開發嘗試與體驗喔!關於今天的內容,歡迎大家一起討論!

參考資料
  1. OmniSharp/generator-aspnet
  2. ASP.NET 5 Application to New Database
  3. Entity Framework, Create a new project

Comments

comments powered by Disqus

建立並使用Docker運行Asp.Net 5 RC 1應用程式

| Comments

在2015年度的Connect()大會之後,Asp.Net 5 來到了 RC1 版,也就是說 Asp.Net 5 的正式版本的到來指日可待。而在 Asp.Net 5 除了整個大翻修之外,最令人期待的特性之一就是可以跨平台的執行,不再被綁定的 Windows 的環境上運行,這也代表未來我們可以選擇自己所希望使用的作業系統來運行 Asp.Net 應用程式,而今天的文章所要介紹的就是如何透過 Docker 來運行 Asp.Net 5 應用程式。

建立 Asp.Net 5 應用程式

在把程式透過 Docker 來運行之前,我們必須要先準備好我們想要運行的專案。要建立 Asp.Net 5 的專案除了透過 Visual Studio 的範本之外,由於要支援跨平台,現在也多出了使用Y oeman Generator 使用 Command Line 的方式來快速的建立專案範本,而今天的範例會以在 Mac 上透過 Yoeman 建立 Asp.Net 5 專案的方式進行。在安裝之前請先確認環境中安裝好 Asp.Net 5 的環境,安裝說明可以參考連結,同時也必須確認電腦上已經設定好 node.js 的執行環境

  1. 打開Terminal,輸入指令,使用node.js安裝Scaffolding所需要的套件

    npm install -g yo generator-aspnet gulp bower   
    

    如果你之前就有安裝 generator-aspnet 的話,記得升級到 RC1 版本

    npm update -g generator-aspnet
    
  2. 使用指令產生Asp.Net 5的專案,並選擇Web Application

    yo aspnet
    

  3. 輸入專案名稱 DockerWithAspNet,就會看到專案自動被建立

  4. 切換到專案資料夾,並嘗試運行 Asp.Net 5 專案

    cd DockerWithAspNet
    dnu restore
    dnx web
    
  5. 打開網頁 http://localhost:5000 ,可以成功看到網頁運行

使用Docker 運行 Asp.Net 5 專案

首先你必須確認電腦上已經設定好 docker 環境,這邊可以參考官方文件進行安裝,要建立並執行 docker 必須要先準備好一個 docker image 的設定檔,而 generator-aspnet 在建立專案的時候很貼心的幫我們準備好了,我們可以在專案目錄下看到 DockerFile 檔案。

這邊有一個小地方我們必須要修改一下,由於官方提供的 image 版本只有 1.0.0-rc1-final ,與 generator-aspnet 產生的 1.0.0-rc1 不一樣,需要做個修正。

  1. 修改 Dockerfile 的 1.0.0-rc1 為 1.0.0-rc1-final

    FROM microsoft/aspnet:1.0.0-rc1-final-final
    COPY . /app
    WORKDIR /app
    RUN ["dnu", "restore"]
    EXPOSE 5000/tcp
    ENTRYPOINT ["dnx", "-p", "project.json", "web"]
    
  2. 打開 Terminal ,切換到專案目錄,使用以下指令建置 image,並將 image 取名為 dockerwithaspnet

    docker build -t dockerwithaspnet .
    
  3. 建立完成後,可以在 docker 中查到我們包好的 image

    docker images
    

  4. 使用以下指令執行 docker image

    docker run -t -d -p 80:5000 dockerwithaspnet    
    
  5. 打開瀏覽器,瀏覽 docker 所在機器的網址 http://dockerhost ,可以看到網頁成功運行

小結

透過 docker 這個工具,我們可以很快速的重新建立測試或生產環境的網站,再加上 docker 能針對不同版本的 image 加上 tag ,執行速度又快,所以我們可以很輕鬆快速運行所有版本的網站,也能夠根據需求從任一歷史版本建立出該版本的網站提供測試,這是未來在 Asp.Net 5 進行開發或部署時可以好好利用的一個特性,在這邊提供給大家參考,關於今天的內容如果有任何問題,也歡迎大家提出來一起討論喔!

參考文章

*. Running ASP.NET 5 applications in Linux Containers with Docker
*. ASP.NET 5 Applications with VS Code
*. ASP.NET 5 Documentation

Comments

comments powered by Disqus

[Tool]使用Specflow以及Visual Studio Online,打造可維護的負載測試

| Comments

如果你的網站也是一個線上服務平台的話,該如何知道自己的網站能不能撐住瞬間巨量的衝擊呢?答案就是負載測試(壓力測試),以往我們要進行負載測試最困擾的就是必須準備大量的機器,同時針對待測網站給予大量的流量壓力,才能完成,而在雲端服務盛行的今天,我們只要透過Visual Studio Online的負載測試功能,讓你連一台機器都不用準備,就可以對你在雲端的網站進行負載測試,甚至更能夠直接使用原本為了網站做自動化測試的Specflow腳本來進行,讓你的負載測試不再是一大堆使用錄製的檔案,而是可以輕鬆讀懂而且好維護的測試案例!

建立網站

我們假設我們所擁有的是一個線上購物網站,想要評估在扣除庫存的程式邏輯是否可以在瞬間大流量的時候也運作正常,而且不會有超賣的情況發生,以下分別撰寫兩種方法來,並透過壓力測試來評估哪一種方法可以經得起大流量的衝擊。(分別是直接使用TransactionScope並使用Serialize鎖定庫存並扣除,和使用ReadCommited搭配樂觀鎖定來扣除庫存)

  1. 建立一個新的Mvc網站

  2. 使用Nuget安裝Entity Framework

  3. 建立庫存的資料結構和DbContext,一種是使用Serialize鎖定,一種是使用樂觀鎖定

    public class Product
    {   
        [Key]
        public int Id { get; set; }
    
        public string Name { get; set; }
    
        public int TotalQty { get; set; }
    
        public int RequestedQty { get; set; }        
    }       
    
    public class ProductWithVersion
    {
        [Key]
        public int Id { get; set; }
    
        public string Name { get; set; }
    
        public int TotalQty { get; set; }
    
        public int RequestedQty { get; set; }
    
        [Timestamp]
        public byte[] TimeStamp { get; set; }
    }
    
    public class ProductDbContext : DbContext
    {
        public DbSet<Product> Products { get; set; }
    
        public DbSet<ProductWithVersion> ProductWithVersions { get; set; }
    }
    
  4. 套件管理主控台執行以下指令,新增一個Migration

    Enable-Migrations
    Add-Migration Init
    

  5. 打開Configuration.cs,修改Seed的內容,給予資料庫初始值

    protected override void Seed(StockRequest.Models.ProductDbContext context)
    {            
        context.Products.AddOrUpdate(
            i => i.Name,
            new Product { Name = "Test", TotalQty = 500, RequestedQty = 0 }
        );
    
        context.ProductWithVersions.AddOrUpdate(
            i => i.Name,
            new ProductWithVersion { Name = "Test", TotalQty = 500, RequestedQty = 0 }
        );
    }   
    
  6. 套件管理主控台執行以下指令,更新資料庫

    Update-Database
    

  7. 新增ProductController,分別提供有鎖定和沒鎖定的購買商品扣除庫存操作頁面

    public class ProductController : Controller
    {
        private ProductDbContext dbContext = new ProductDbContext();
    
        // GET: Stock
        public ActionResult Index(int id)
        {
            var model = this.dbContext.Products
                            .Where(i => i.Id == id)
                            .FirstOrDefault();
    
            return View(model);
        }
    
        [ValidateAntiForgeryToken]
        [HttpPost]
        public ActionResult Index(FormCollection formData)
        {
            var id = int.Parse(formData["Id"]);
    
            using (TransactionScope scope = new TransactionScope())
            {
                var model = this.dbContext.Products
                                    .Where(i => i.Id == id)
                                    .FirstOrDefault();
    
                model.RequestedQty += 1;
                this.dbContext.SaveChanges();
    
                scope.Complete();
            }
    
            return RedirectToAction("Result");
        }
    
        public ActionResult Nolock(int id)
        {
            var model = this.dbContext.ProductWithVersions
                            .Where(i => i.Id == id)
                            .FirstOrDefault();
    
            return View(model);
        }
    
        [ValidateAntiForgeryToken]
        [HttpPost]
        public ActionResult Nolock(FormCollection formData)
        {
            var id = int.Parse(formData["Id"]);
    
            var model = this.dbContext.ProductWithVersions
                                    .Where(i => i.Id == id)
                                    .FirstOrDefault();            
    
            model.RequestedQty += 1;
            this.dbContext.SaveChanges();           
    
            return RedirectToAction("Result");
        }
    
        public ActionResult Result()
        {
            return View();
        }
    }
    
  8. 新增Index.cshtml和NoLock.cshtml

    Index.cshtml

    <h2>Request Qty With Lock</h2>
    
    @using (Html.BeginForm())
    {
        @Html.AntiForgeryToken()
    
        <div class="form-horizontal">
            <h4>Product @Model.Id,Current Qty: @(Model.TotalQty - Model.RequestedQty)</h4>
            <hr />
    
            @Html.HiddenFor(model => model.Id)        
    
            <div class="form-group">
                <div class="col-md-offset-2 col-md-10">
                    <input type="submit" value="Buy" class="btn btn-default" />                
                </div>
            </div>
        </div>
    }
    

    NoLock.cshtml

    <h2>Request Qty Without lock</h2>
    
    @using (Html.BeginForm())
    {
        @Html.AntiForgeryToken()
    
        <div class="form-horizontal">
            <h4>Product @Model.Id,Current Qty: @(Model.TotalQty - Model.RequestedQty)</h4>
            <hr />
    
            @Html.HiddenFor(model => model.Id)
    
            <div class="form-group">
                <div class="col-md-offset-2 col-md-10">
                    <input type="submit" value="Buy" class="btn btn-default" />
                </div>
            </div>
        </div>
    }
    
  9. 完成站台,可成功連上並扣除庫存

參考資料:

撰寫自動化測試

  1. 新增一個單元測試專案

  2. 使用Nuget在專案中安裝以下套件

    Install-Package Specflow.FluentAutomation
    Install-Package FluentAutomation.SeleniumWebDriver
    Install-Package PhantomJS
    
  3. 打開app.config,設定specflow使用mstest執行單元測試

    <specFlow>
        <unitTestProvider name="mstest" />
        <plugins>
            <add name="FluentAutomation" path="plugin" type="Generator" />
        </plugins>
    </specFlow>
    
  4. 新增購買商品.feature

    #language: zh-TW
    功能: 購買商品
        為了提供可以共買商品的頁面
        提供給使用者
        購買想要的商品
    
    場景: 使用會Lock資料的購買商品頁面購買商品
        假設 開啟商品頁 "http://localhost:42505/Product/Index/1"
        當 點選購買按鈕
        那麼 購買成功
    
    場景: 使用不會Lock資料的購買商品頁面購買商品
        假設 開啟商品頁 "http://localhost:42505/Product/Nolock/1"
        當 點選購買按鈕
        那麼 購買成功
    
  5. 新增購買商品步驟.cs

    [Binding]
    public class 購買商品步驟: StepDefinitionBase
    {
        [BeforeFeature]
        public static void StartUp()
        {
            SeleniumWebDriver.Bootstrap(SeleniumWebDriver.Browser.PhantomJs);
        }
    
        [Given(@"開啟商品頁 ""(.*)""")]
        public void 假設開啟商品頁(string url)
        {
            I.Open(url);
        }
    
        [When(@"點選購買按鈕")]
        public void 當點選購買按鈕()
        {
            I.Click(".btn");
        }
    
        [Then(@"購買成功")]
        public void 那麼購買成功()
        {
            I.Assert.Text("Request Qty Success").In("h2");
        }
    }
    
  6. 執行自動化測試,可以看到測試都成功了!

參考資料:

部署網站到Azure

為了讓Visual Studio Online可以進行壓力測試,我們使用Azure來放置我們的網站,當然你也可以使用一個有公開的主機作為測試使用,但必須評估自己的主機是否乘載得住流量,避免產生問題。

  1. 在網頁專案點選滑鼠右鍵,選擇發行,可以看到發行設定視窗,選擇第一個Microsoft Azure Web Apps

  2. 如果已經有Web App的話,可以選擇現有的,如果沒有的話點選新增,並設定Web App所需的資訊

  3. 在設定記得勾選執行Code First移轉

  4. 按下發行,稍待片刻之後,我們就可以在公開網路上看到我們的網站了!

使用Visual Studio Online執行負載測試

注意:剛開始執行負載測試的時候,建議人數和測試的時間不要一下太大量,以免所花費的金額超過預期!Visual Studio Online也很佛心的每月提供20000虛擬使用者分鐘數的單位給大家免費做壓力測試,大家可以多多利用!

  1. 把剛剛自動化測試的網址,改為部署好網站的網址,並在測試專案新增一個負載測試,並按照需求設定條件,在混合測試的地方選擇剛剛寫好的其中一個測試(一次一個,方便比較)

  2. 方案點選滑鼠右鍵,新增一個項目,選擇測試設定

  3. 在設定把測試回合位置切換為Visual Studio Online (記得Visual Studio要先連接VS Online的Team Project)

  4. 因為我們在FluentAutomation為了方便,使用的是Phantom.js,所以在部署的地方也要把Phantomjs.exe加入裡面

  5. 點選剛剛的負載測試,開始連接並設定測試資料

  6. 測試正在進行中

  7. 測試完成之後,可以下載報告回來看

  8. 我們可以依照上面的步驟繼續完成另外一種方法的壓力測試

小結

透過實際的進行壓力測試之後,我們可以輕鬆地找出網站的瓶頸在哪邊,並且加以改善,並且有準確的數據作為網站架構的依據,可以得知自己的服務極限在哪,並在未來做進階的規劃。

而且這些已經寫好的Specflow測試,不但可以使用在自動化測試,還可以直接作為壓力測試的腳本,除了讓測試可以用在多個地方之外,還是使用語意化的方法撰寫測試案例,讓維護起來更加的輕鬆!關於以上的內容,若有任何問題歡迎一起討論喔!

Comments

comments powered by Disqus

[Asp.Net 5]使用Mac建立Asp.Net 5的MVC網站初體驗

| Comments

微軟的Build 2015正如火如荼的進行中,在第一天的Keynote也公布了許多令人振奮的消息,其中特別吸引我注意的就是.Net Core跨平台(Windows, Linux, Mac),以及Visual Studio Code的公布(跨平台版本的Visual Studio),讓我們使用非Windows環境開發.Net程式的日子似乎不遠了!今天就要向大家介紹,如何在Mac上設定Asp.Net 5的開發環境,讓以往只能在Windows上執行的Asp.Net Mvc也能在Mac或Linux上開發囉!

p.s. 目前.Net Core和Visual Studio Code都還沒有正式的Release,在Mac上都還是Preview階段,未來操作步驟也有可能會有小幅度的變更,請大家注意喔!

環境需求

在設定Asp.Net 5的環境之前,為了讓安裝的過程更加的簡單,請大家先確保在Mac電腦上是否已經安裝了以下套件,來讓Asp.Net 5的安裝,和專案產生更加的簡單喔!

  1. Homebrew
  2. Node.js

安裝Asp.Net 5

請打開Terminal.app或你所慣用的終端機程式,按照順序輸入並執行以下指令,進行Asp.Net 5的安裝

    brew tap aspnet/dnx
    brew update
    brew install dnvm

安裝完畢之後,請將以下設定加入你的.bashrc,方便之後執行Asp.Net 5網站時指令的操作(如果你使用的是zsh,就是加入.zshrc)

    source dnvm.sh

如果都有設定成功,在Terminal輸入dnvm,應該可以看到以下畫面

安裝Scaffolding套件

通常在Windows時,我們都是透過Visual Studio來建立一個新的Asp.Net Mvc專案,而目前在Mac中要新增一個Asp.Net 5的Mvc專案的話,可以透過node.js的generator-aspnet(base on yoeman)來產生一個空的Asp.Net Mvc專案,因此我們也要先安裝一些套件來幫助我們快速建立專案的框架,打開Terminal,輸入以下指令安裝generator-aspnet

    npm install -g yo grunt-cli generator-aspnet bower

建立Asp.Net 5的Mvc專案

到這邊為止,我們已經基本準備好了在Mac上開發Asp.Net 5的環境,接下來就要建立並執行我們在Mac上第一個Asp.Net 5的Mvc網站

  1. 打開Terminal,輸入以下指令,建立Mvc網站

    yo aspnet
    
  2. 這時候會看到詢問你要建立哪種專案,我們選擇Web Application

  3. 接著會詢問你專案的名稱,輸入專案的名稱Test (或你想要的專案名稱)

  4. 然後應該可以看到檔案自動被產生

  5. 我們可以使用Visual Studio Code開啟剛剛建立的專案資料夾,開始進行Mvc網站的開發

啟動網站

  1. 打開Terminal,切換到專案所在資料夾(就是你剛剛輸入的專案名稱)執行套件還原,輸入以下指令,可以看到專案所需的套件被還原

    dnu restore
    

  2. 到這邊為止,我們已經完成一個基本的Asp.Net 5的Mvc網站囉!輸入以下指令啟動網站

    dnx . kestrel
    
  3. 開啟瀏覽器,輸入http://localhost:5001,可以看到網站成功啟動囉!

異常排除

如果你開啟網站,看到下圖的錯誤訊息的話,這是由於mono的一個bug所產生的,也會在未來被修正,我們可以先按照幾個步驟來排除它,畢竟是Preview版,多少還會有一點點的小bug

  1. 打開Terminal,切換到專案資料夾,輸入以下指令

    export MONO_MANAGED_WATCHER=false
    
  2. 重新啟動網站

    dnx . kestrel
    
  3. 開啟瀏覽器,輸入http://localhost:5001,可以看到網站的異常排除囉!

參考資料:https://github.com/aspnet/Mvc/issues/2348

小結

在這次Build 2015中,可以看到許多.Net平台在未來更多的可能性,也讓.Net的開發人員更加的躍躍欲試,或許在不久的將來,在各種平台上執行我們所建置的.Net應用程式或網站都不會是難事吧!

Comments

comments powered by Disqus

[Asp.Net MVC] 使用Asp.Net Idetity整合Google登入

| Comments

隨著網路服務越來越多,現在也有許多網站開始提供透過社交網站來進行登入功能(例如:Google、Twitter、Facebook等等),這樣的好處是不必在自己重新撰寫會員系統,而是可以很快速的透過社交網站驗證使用者是本人之後,再導回我們提供服務的網站,用同一個使用者帳號來進行各種操作。而Asp.Net現在也提供了完整整合OAuth2這種驗證方式的Asp.Net Identity,讓我們快速實現使用OAuth2透過社交網站登入的功能,這篇文章就要向大家分享如何使用Asp.Net Identity來透過Google帳號登入自己的網站。

※本篇內容適合只想整合OAuth驗證,但不想要整合其他Asp.Net Identity會員機制(User、Role),或會員登入要使用自製流程的人

註冊Google Api Key

原本我們使用Asp.Net Identity,不一定需要申請Google Api Key,而是可以透過Google OpenId整合登入的方式,但目前Google因為安全性的考量,將要把使用OpenId登入的功能關閉,也不接受新的網域使用Google OpenId來登入(原本有使用OpenID的也將在2015/4/20關閉,建議盡早修改),所以現在如果要使用Google做登入的話,只能使用OAuth的方式囉!

  1. 進入Google Developer Console - https://console.developers.google.com/project

  2. 新增一個專案,或使用已經擁有的專案

  3. 等待專案建立完成後,進入專案詳細頁面,選擇憑證,點選建立新的用戶端ID

  4. 若還沒有設定產品資訊的話,會提示要先設定同意畫面的資料,需要先將其填寫完畢

  5. 在建立畫面,選擇網路應用程式,填寫網址和callback網址(Asp.Net Identity預設註冊route,擁有signin-google這個callback網址)

  6. 建立完成後,可以看到成功的產生密碼資訊,用戶端ID就是ClientID,用戶端密碼就是ClientSecret

  7. 點選左側的API選項,我們必須要開啟Google+ API,才能在登入時取得登入者的資訊(例如Email)

  8. 啟用Google+ API

  9. 啟用成功後,可以再已啟用的API看到,到這邊就完成了Google Api Key的申請

使用Asp.Net Identity整合登入功能

  1. 建立一個新的Asp.Net Mvc站台,因為要使用Google登入,所以在驗證選項這邊先選擇不驗證

  2. 安裝下列Nuget套件,可以從Nuget套件管理員 => 套件管理主控台以指令方式安裝較快

    Install-Package Microsoft.AspNet.Identity.Core -Version 2.1.0
    Install-Package Microsoft.AspNet.Identity.Owin -Version 2.1.0
    Install-Package Microsoft.Owin.Security.Google -Version 3.0.0
    Install-Package Microsoft.Owin.Security.OAuth -Version 3.0.0
    Install-Package Microsoft.Owin.Host.SystemWeb -Version 3.0.0
    
  3. 在App_Start資料夾下新增Startup.Auth.cs檔案,設定登入方式與Google Api的ClientId、ClientSecret

    App_Start\Startup.Auth.cs
    using System;
    using Microsoft.AspNet.Identity;
    using Microsoft.AspNet.Identity.Owin;
    using Microsoft.Owin;
    using Microsoft.Owin.Security.Cookies;
    using Microsoft.Owin.Security.Google;
    using Owin;
    
    namespace WebApplication3
    {
        public partial class Startup
        {
            // 如需設定驗證的詳細資訊,請瀏覽 http://go.microsoft.com/fwlink/?LinkId=301864
                public void ConfigureAuth(IAppBuilder app)
            {
                // 讓應用程式使用 Cookie 儲存已登入使用者的資訊
                    // 並使用 Cookie 暫時儲存使用者利用協力廠商登入提供者登入的相關資訊;
                    // 在 Cookie 中設定簽章
                    app.UseCookieAuthentication(new CookieAuthenticationOptions
                {
                    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                    LoginPath = new PathString("/Account/Login"),             
                });
    
                app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
    
                // TODO: 輸入Client Id和Client Secret
                    app.UseGoogleAuthentication(
                    clientId: "<YOUR_CLIENTID>",
                    clientSecret: "<YOUR_CLIENTSECRET>");            
            }
        }
    }    
    
  4. 在網站下新增Startup.cs,設定OWIN啟動時執行Startup設定

    Startup.cs
    using System;
    using System.Threading.Tasks;
    using Microsoft.Owin;
    using Owin;
    
    [assembly: OwinStartup(typeof(WebApplication3.Startup))]
    
    namespace WebApplication3
    {
        public partial class Startup
        {
            public void Configuration(IAppBuilder app)
            {
                // 如需如何設定應用程式的詳細資訊,請參閱  http://go.microsoft.com/fwlink/?LinkID=316888'
                    ConfigureAuth(app);
            }
        }
    }
    
  5. 在Controllers新增AccountController,設定外部登入相關Action

    Controllers\AccountController.cs
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using Microsoft.AspNet.Identity;
    using Microsoft.AspNet.Identity.Owin;
    using System.Threading.Tasks;
    using System.Web;
    using System.Web.Mvc;
    using Microsoft.Owin.Security;
    using System.Security.Claims;
    
    namespace WebApplication3.Controllers
    {
        public class AccountController : Controller
        {        
            private IAuthenticationManager AuthenticationManager
            {
                get
                {
                    return HttpContext.GetOwinContext().Authentication;
                }
            }
    
            //
                // GET: /Account/Login
                [AllowAnonymous]
            public ActionResult Login(string returnUrl)
            {
                ViewBag.ReturnUrl = returnUrl;
                return View();
            }
    
            public ActionResult Logout()
            {
                this.AuthenticationManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
    
                return RedirectToAction("Login");
            }
    
            [HttpPost]
            [AllowAnonymous]
            [ValidateAntiForgeryToken]
            public ActionResult ExternalLogin(string provider, string returnUrl)
            {
                // 要求重新導向至外部登入提供者
                    return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Account", new         { ReturnUrl = returnUrl }));
            }
    
            //
                // GET: /Account/ExternalLoginCallback
                [AllowAnonymous]
            public async Task<ActionResult> ExternalLoginCallback(string returnUrl)
            {
                var loginInfo = await AuthenticationManager.GetExternalLoginInfoAsync();
                if (loginInfo == null)
                {
                    return RedirectToAction("Login");
                }
    
                // 若使用者已經有登入資料,請使用此外部登入提供者登入使用者
                    if (loginInfo != null)
                {
                    var id = new ClaimsIdentity(loginInfo.ExternalIdentity.Claims,
                                                DefaultAuthenticationTypes.ApplicationCookie);
    
                    //// TODO: 加上你的驗證邏輯,或是註冊會員邏輯                
        
                    AuthenticationManager.SignIn(id);
    
                    return RedirectToLocal(returnUrl);
                }
                else
                {
                    return RedirectToAction("Login");
                }
            }
    
            private ActionResult RedirectToLocal(string returnUrl)
            {
                if (Url.IsLocalUrl(returnUrl))
                {
                    return Redirect(returnUrl);
                }
                else
                {
                    return RedirectToAction("Index", "Home");
                }
            }
    
            // 新增外部登入時用來當做 XSRF 保護
                private const string XsrfKey = "XsrfId";
    
            internal class ChallengeResult : HttpUnauthorizedResult
            {
                public ChallengeResult(string provider, string redirectUri)
                    : this(provider, redirectUri, null)
                {
                }
    
                public ChallengeResult(string provider, string redirectUri, string userId)
                {
                    LoginProvider = provider;
                    RedirectUri = redirectUri;
                    UserId = userId;
                }
    
                public string LoginProvider { get; set; }
                public string RedirectUri { get; set; }
                public string UserId { get; set; }
    
                public override void ExecuteResult(ControllerContext context)
                {
                    var properties = new AuthenticationProperties { RedirectUri = RedirectUri };
                    if (UserId != null)
                    {
                        properties.Dictionary[XsrfKey] = UserId;
                    }
                    context.HttpContext.GetOwinContext().Authentication.Challenge(properties, LoginProvider);
                }
            }
        }
    }
    
  6. 在Views/Account新增Login.cshtml頁面,提供登入畫面

    Views\Account\Login.cshtml
    @{
        ViewBag.Title = "Login";
        Layout = "~/Views/Shared/_Layout.cshtml";
    }
    
    <div class="jumbotron" style="margin-top:15px">
        <h1>Log On</h1>
        <p>Please login to system.</p>
        <p>
            @using (Html.BeginForm("ExternalLogin", "Account", new { ReturnUrl = ViewBag.ReturnUrl }))
            {
                @Html.AntiForgeryToken()
                <button type="submit" class="btn btn-primary" id="Google" name="provider" value="Google" title="Signin with Google account">Signin with Google account</button>
            }
        </p>
    </div>
    
  7. 執行網站,進入/Account/Login,可以使用Google來登入

  8. 點擊後,出現Google的授權畫面

  9. 按下同意,成功的回到首頁

使用NSquared.AspNet.Identity.Google快速整合

除了按照上面的操作說明整合Asp.Net Identity使用Google登入之外,我也將這些步驟包裝為一個Nuget Package,就可以不用手動一個一個新增檔案,而是透過Nuget的方式快速整合Google登入。(使用前請先確認目前網站沒有使用Asp.Net Identity,新增專案時也沒有驗證選項要選擇不驗證,避免要新增的檔案已經存在)

  1. 一樣建立一個新的Asp.Net Mvc站台,一樣選擇不驗證

  2. 安裝Nuget套件,選擇NSquared.AspNet.Identity.Google

    此套件目前為PreRelease版本

  3. 進入App_Start\Startup.Auth.cs,將Google Api的Client Id和Secret更換為剛剛申請的資料

  4. 啟動網站,可以發現我們成功的使用Google登入囉!

  5. 目前這個Nuget Package也是Open Source,歡迎大家發送PullRequest,網址:https://github.com/91mai/NSquared.AspNet.Identity.Google

小結

透過第三方網站提供的OAuth登入整合,提供了一個方便的渠道讓使用者進行註冊或登入,解決使用者需要記憶額外帳號密碼的不方便性,也可以讓使用者透過自己信任的平台來進行登入。Asp.Net Identity的功能相當強大,還有許多的功能可以讓我們對於會員和登入機制的使用更加的方便,未來有機會再陸續分享給大家!

Comments

comments powered by Disqus

[AngularJs]淺談Angular.js的Provider機制

| Comments

Angular.js的核心是由DI(Dependency Injection)所構成,在開發時期我們透過Angular.js所提供的Service、Factory等Provider,將可共用的程式碼包裝註冊到模組中,最後再透過Angular.js注入(Inject)到我們所想要使用的地方。而Service、Factory等功能其實都是Angular.js中$provide服務所提供的方法之一,讓我們可以將常用、可重複使用的功能包裝成元件,但Service、Factory這幾種Provider有什麼不同呢?今天的文章就要向大家介紹其中的差異之處。

$provide所提供的幾種Provider

Constant

Contant代表著常數,從註冊之後就不會再被改變,也沒辦法透過decorator等機制攔截客制化,它可以在任何的地方被註冊使用。Constant的值可以是數字、文字或是物件,我們可以直接透過angular.constant來註冊常數。

angualr.module('app')
    .constant('version', 1);

畫面上的結果為 1

範例程式碼

通常Constant會使用在設定檔,或是不會變的常數上面。

Value

Value其實和Constant非常類似,它可以是任何種類的數字、文字或是物件,甚至可以是一個function,但它和Constant最主要的差別是他可以透過decorator等機制去攔截,在實際使用前做客制化的調整。

angular.module('app')
    .value('version', 1);

畫面上的結果為 1

範例程式碼

透過Decorator做攔截value,並進行客制化

angular.module('app')
    .config(function ($provide) {
        $provide.decorator('version', function ($delegate) {
        return 'Version - ' + $delegate;
    });
});

畫面上的結果為 Version - 1

範例程式碼

Value通常會使用來作為App的初始值、預設值等用途。

※ Constant和Value有一個重大差異是,Constant可以被注入到Config區段,Value不行

Service

Service通常會用在註冊包含某些功能的Function,Angular.js在注入時會幫我們將Service進行初始化的動作(有點像我們寫C#時會先定義好Class,然後DI在注入時會幫我們new class的實體),在Angular.js中,Service都是Singleton,所以我們可以透過Service來跨Controller分享資料。

angular.module('app')
    .service('myService', function () {
        var self = this;
        self.getName = function () {
            return 'John';
        };
    });

angular.module('app')
    .controller('MainCtrl', function ($scope, myService) {
    //// Angular會初始化方法,並回傳給我們

    //// 類似 new myService()

        $scope.name = myService.getName();
    });

畫面上的結果為 John

範例程式碼

我們可以使用Service將$http取資料的程式包裝,或是封裝常用的商業邏輯為Service。

Factory

與Service不同的是,Factory類似Factory Pattern,我們註冊的不會是一個功能的Function,而是一個如何產生物件的方法,與在註冊為Service時不同,Angular.js會幫我們執行這個產生物件的Function,然後回傳這個物件。

angular.module('app')
    .factory('myFactory', function () {
        return {
            name: 'John'
        }
    });

angular.module('app')
    .controller('MainCtrl', function ($scope, myFactory) {
    //// Angular會執行工廠方法,並回傳物件

    //// 類似 myFactory()

        $scope.name = myFactory.name;
    });

畫面上的結果為 John

範例程式碼

Factory可以用來產生共用的Utility,或是存放一些跨區塊使用的物件。

Provider

Provider是最複雜的一種,它是所有Provider功能的基底,像Factory、Service其實都是Provider包裝後提供的一種語法糖,讓我們可以快速實現並註冊元件。Provider提供了彈性的設定方法,也讓我們可以用類似Factory的方法將物件的初始化包裝起來。

假設我們希望在頁面上顯示version,但又希望在不同的頁面可以顯示不同的樣式,我們就可以使用Provider的方法來實作,如果不做任何調整的話就是顯示預設值。

angular.module('app')
    .constant('version', 1);

angular.module('app')
    .provider('versionTitle', function () {
        var self = this;
            
    //// Provider提供可彈性調整的方法,透過angular.config呼叫並修改prefix

        self.versionPrefix = '';
        self.changeVersionPrefix = function (prefix) {
            self.versionPrefix = prefix;
        }
        
    //// Provider實際產生物件的方法,回傳的實體透過此方法產生

    //// 類似Factory,使用$get()取得物件

        self.$get = ["version", function (version) {
            return self.versionPrefix + version;
        }];
});

畫面上的結果為 1

範例程式碼

如果我們希望替version加上不同prefix的話,只需要額外加上config,並呼叫changeVersionPrefix方法修改prefix即可。

angular.module('app')
    .config(function (versionTitleProvider) {
        versionTitleProvider.changeVersionPrefix('VersionTitle - ');
});

畫面上的結果為 VersionTitle - 1

範例程式碼

Provider通常用來做可根據頁面動態調整設定的方法或物件,擁有比較高的客制化彈性。

Decorator

我們如果想要針對已經撰寫好的Provider客制化,增加想要執行的邏輯,可以透過Decorator方法來攔截並擴充,有點類似AOP

假設我們已經撰寫好一個取得姓名的Service

angular.module('app')
    .service('myService', function () {
        var self = this;
        self.getName = function () {
            return 'John';
        };
});

畫面上的結果為 John

但我們希望在特定的頁面顯示大寫的名字,但又不希望修改原本的Service,我們就可以在這頁使用Decorator的方法,將名字透過擴充的方法修改為大寫,卻不影響原本已經擁有的模組。

angular.module('app')
    .config(function ($provide) {
        $provide.decorator('myService', function ($delegate) {
         //// 將原本的物件包裝起來,並回傳大寫的內容

            return {                  
                getName: function(){
                    return $delegate.getName().toUpperCase();
                }
            };
        });
    });

畫面上的結果為 JOHN

範例程式碼

小結

使用Angular.js所提供的Provider機制,可以讓程式碼更有彈性並且容易管理,我們可以將大部分常用的邏輯都封裝為Service或Factory,真的少部分需要客制的部分透過Decorator或是Provider來彈性設定、調整,讓程式碼不會總是因為微調而一直讓程式增加,並更符合物件導向單一職責的概念,保持程式碼的乾淨與清楚!關於以上內容,若有任何問題歡迎大家一起討論喔!

※ 參考文章

Comments

comments powered by Disqus

[Tool] 使用Travis CI自動建置.Net Open Source Project並發佈到Nuget上

| Comments

微軟在Build 2014開發者大會上宣佈釋出包含.NET Compiler平台在內等多項開發技術,同時也將.Net的原始碼在Github上公開(網址:https://github.com/Microsoft/dotnet),所有人都可以替.Net原始碼提供自己的一份心力。隨著Open Source越來越普及,公開自己開發的Library不但可以幫助其他人不需要重複造輪子,甚至可以透過開放社群的力量,共同來讓熟悉的語言擁有更好用的工具,剛好我們公司最近也將陸陸續續的開放一些專案到Github上提供給需要的人員一起使用,在這邊分享我們如何透過Open Source的服務來自動建置、測試並發佈成品到Nuget上供大家使用。

使用Travis CI建置專案

Travis CI是一套專門為開放程式碼打造的Continuous Integration Server,可以幫助我們在有新的Commit或Pull Request時,自動建置並測試專案,確保大家Commit到Github上的程式碼都是正確並通過測試的,避免新的Commit造成專案不能建置或改壞功能的情形發生。(當然如果是考量到企業內部使用,或是需要更完整的.Net建置資源,使用Visual Studio Online是更棒的選擇。)

首先向大家介紹,如何透過Travis CI來建置我們的.Net專案。

  1. 準備好自己的Open Source專案,並Check In到Github上

  2. 進入Travis CI官方網站,由於要需要讀取Github上的程式碼,所以我們必須使用Github帳號來登入

  3. 第一次登入會需要授權,登入成功後可以看到下面的畫面,在右上角選擇Account,如果沒有看到自己在Github的帳號的話,可以手動點選Sync now,成功後可以看到自己在Github上的專案清單

  4. 將自己想要進行建置的專案狀態由off切為on

  5. 回到自己的Open Source專案之中,在根目錄新增Travis CI建置所需要的設定檔.travis.yml如下,由於Travis CI建置的環境其實是Linux base,所以我們在這邊沒辦法直接使用msbuild來建置專案,而是透過Mono來在Linux上建置專案

    install部分的語法是CI初始化時,需安裝建置所需要的環境(包含Mono的安裝)

    script部分是真正建置專案使用的語法,類似msbuild,指定sln的名稱

    language: c
    
    install:
    # 使用mono進行建置
    - sudo add-apt-repository ppa:directhex/monoxide -y && sudo apt-get update
    - sudo apt-get install mono-devel mono-gmcs
    - mozroots --import --sync
    
    script:
    # 修改為自己專案的sln名稱
    - xbuild NSquared.AspNet.Extensions.sln 
    
    env:
    global:
    - EnableNuGetPackageRestore=true
    
  6. 新增完設定檔後,push到github上,回到Travis CI,可以看到我們多了一個待建置的Commit,這代表我們的建置工作已經被排入執行的Queue中,準備開始執行

  7. 等待一小段時間後,可以看到專案成功的建置,透過這樣的設定,我們就可以確保每一次的Commit都會讓Travis CI跑一次建置,確保程式碼是可執行的

  8. Travis CI還提供了開源專案一個建置狀態的小貼紙,方便我們在Github上顯示專案的健康狀態,我們可以透過點擊右上角的build圖案,取得自動產生圖片的網址

  9. 我們可以把這個狀態貼紙加到我們Github專案的Readme上,這樣別人只要一進入我們的專案,就可以看到專案是否能建置通過囉!

進行單元測試

在進行一個OpenSource專案時,單元測試也是很重要的,它可以幫助每一個新參與的開發者,快速的確認他們所增加的功能並沒有影響原本支援的功能,也可以讓人更盡情的發揮自己的想像力,讓這個Library更加的好使用,下面我就要像大家介紹如何在Travis CI上加上單元測試

  1. 首先先替程式碼加上單元測試,在這邊我所使用的是Specflow這套BDD Framework,它可以幫助我們將測試案例用更語意化的文字來說明測試內容

    ※ 更多測試的文章可以參考91的部落格

  2. 在專案中加上BuildScripts/unittest.sh,我在這邊是使用specrun.exe來幫助我進行specflow的單元測試,但由於在Linux系統上,所以透過mono來執行

    #!/bin/bash
    # 使用Nuget安裝Specrun
    mono --runtime=v4.0.30319 .nuget/NuGet.exe install .nuget/packages.config -o packages
    # 使用Specfun執行測試
    mono --runtime=v4.0.30319 packages/SpecRun.Runner.1.2.0/tools/SpecRun.exe run NSquared.MvcExtensions.Test.dll /baseFolder:MvcExtensions.Test/bin/Debug /toolIntegration:vs2010 /reportFile:.\TestReport.html
    
  3. 到.travis.yml中增加執行unittest.sh,讓建置時同時執行單元測試

    language: c
    
    install:
    - sudo add-apt-repository ppa:directhex/monoxide -y && sudo apt-get update
    - sudo apt-get install mono-devel mono-gmcs
    - mozroots --import --sync
    
    script:
    - xbuild NSquared.AspNet.Extensions.sln
    # 進行單元測試
    - bash BuildScripts/unittest.sh  
    
    env:
        global:
        - EnableNuGetPackageRestore=true 
    
  4. 都修改完畢後,一樣push到github上,就可以看到Travis CI建置的同時,也執行單元測試確保程式沒有壞囉!

將Library發佈到Nuget

有了自動建置和單元測試之後,就要進行將專案建置的結果打包並發行到Nuget上,在發行到Nuget之前,我們必須先準備好申請一組Nuget帳號和Api Key,大家可以到Nuget官方網站申請,下面就開始介紹我們打包Nuget的步驟。

  1. 在開始打包Nuget之前,我們必須先準備好一份定義檔(*.nuspec),可以使用Nuget Package Explorer來產生,這個檔案主要是說明專案的Dependency和要打包的內容

    使用Nuget Package Explorer產生的定義檔

    定義檔原始內容

    <?xml version="1.0" encoding="utf-8"?>
    <package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
        <metadata>
            <id>NSquared.MvcExtensions</id>
            <version>1.0.0</version>
            <title>NSquared.MvcExtensions</title>
            <authors>NSquared</authors>
            <projectUrl>https://github.com/91mai/NSquared.AspNet.Extensions</projectUrl>
            <iconUrl>https://avatars0.githubusercontent.com/u/10022788?v=3&amp;s=200</iconUrl>
            <requireLicenseAcceptance>false</requireLicenseAcceptance>
            <description>This is a Asp.Net Extension for Asp.Net Mvc or Asp.Net WebApi. For easily extend by Action Filter or otherwise.</description>
            <language>zh-TW</language>
            <dependencies>
                <group targetFramework=".NETFramework4.5">
                    <dependency id="Microsoft.AspNet.Mvc" version="4.0.40804.0" />
                    <dependency id="Newtonsoft.Json" version="5.0.8" />
                </group>
            </dependencies>
        </metadata>
        <files>/
            <file src="/MvcExtensions/bin/Debug/NSquared.MvcExtensions.dll" target="lib\net45\NSquared.MvcExtensions.dll" />
        </files>
    </package>
    

    這邊需要注意的是,由於Linux系統的檔案路徑和windows不一樣,所以要將檔案路徑的\換成/

  2. 到Nuget的帳號資訊頁面,記下ApiKey

  3. 加密Api Key,並存到.travis CI設定檔中,必須先安裝Travis CI的gem (詳細說明),安裝完成之後,使用Command Line切換至專案的根目錄,並輸入以下指令加密Api Key到.travis.yml中

    travis encrypt -r 91mai/NSquared.AspNet.Extensions NUGET_APIKEY=<token> --add env.global
    
  4. 執行完成後,開啟.travis.yml應該可以看到多了一個加密區段,這樣可以避免我們將專案存在Github上,但某些Api Key也暴露出來,造成安全問題

  5. 新增BuildScripts/deploypackage.sh,使用nuget指令打包到官方nuget repository中,並自動根據Travis CI的Build Number增加版號,同時因為不希望所有的commit都會打包Nuget Package,這邊也增加判斷只有在Master Branch的commit會打包至nuget! (這邊因為是測試版本,所以版號加上-alpha,nuget會自動判斷為prerelease版本)

    #!/bin/bash
    VersionNumber="0.0.${TRAVIS_BUILD_NUMBER}-alpha"
    
    echo "VersionNumber: $VersionNumber"
    
    if [ "$TRAVIS_BRANCH" == "master" ]; then         
        mono --runtime=v4.0.30319 .nuget/NuGet.exe Pack NSquared.MvcExtensions.nuspec -NonInteractive -Version ${VersionNumber}
        mono --runtime=v4.0.30319 .nuget/NuGet.exe Push NSquared.MvcExtensions.${VersionNumber}.nupkg $NUGET_APIKEY -NonInteractive     
    fi
    
  6. 在.travis.yml一樣增加執行打包至nuget的指令

  7. 設定完畢後,一樣push到Github上,等待Travis CI自動執行

  8. 到Nuget官方網站上,也可以看到已經成功發佈Package

小結

我們的Open Source Project使用Github進行版本控制,每當有新的Commit或Pull Request時就會透過Travis CI進行建置,並經過單元測試,讓大家可以放心開發,一旦有錯誤影響功能就會馬上被發現。當我們需要Release新版本時,只需要將develop Branch Merge到master,Travis CI就會自動幫我們發佈最新版本的Package到Nuget上,使用者也就可以馬上更新到最新版囉!

Comments

comments powered by Disqus

使用Jenkins打造.Net CI Server (2) - 為什麼我們需要CI Server

| Comments

在一個軟體開發團隊中,CI Server就像是我們產品的守護神,平常感覺不到它的存在,卻默默地替我們軟體品質把關,一旦有人改壞了程式,馬上會在第一時間通知我們,也可以幫助我們將日常瑣碎的工作自動化,不用在替建置軟體的乾淨環境,或是如何將程式碼更新到伺服器上而煩惱,讓我們可以花更多時間與精力在專注開發軟體上。

核心概念

CI Server是Continuout Integration Server (持續整合伺服器)的縮寫,持續整合代表了我們為了提升軟體的品質,不斷反覆重複進行的過程。透過CI Server的幫助,可以確保產出程式碼的品質,降低部署網站所花費的時間,甚至將我們習慣在開發後進行的工作都交給它,(ex. 複雜度分析、安全性掃瞄...等),並且完全不會佔用我們所開發使用的電腦。CI Server會自動監控我們的版本控制系統(Version Control System),一旦發現有程式碼簽入版本控制系統,就會自動針對最新的Commit進行建置、單元測試,甚至是部署到測試伺服器上等工作,可以確保我們版本控制系統中的的程式碼隨時都處在一個穩定的狀態,也可以透過CI Server所產出的各項報表觀察軟體的品質、穩定度等各項環節是否符合預期。

對我們的影響

對開發人員來說,在導入CI Server之後的開發,和導入之前並沒有任何的不同,一樣的從版本控制系統上取得原始碼,進行開發後簽入最新的程式碼,完全不需要花費任何的時間去適應導入CI Server後的新流程,因為完全一模一樣!甚至可以說讓開發人員花在雜事上的時間變少了,因為不再需要再自己手動進行各種軟體品質的檢查,把這些額外卻固定的工作都交給CI Server來處理。

導入CI Server後不同的是,再也不用擔心從版本控制系統上取得的程式碼可不可以用,是否能夠通過編譯(Compile),配合單元測試,更可以讓修改既有程式碼時不會影響到系統的正常運作,大大的增加了程式碼的可靠度和可重用度,甚至我們可以每天進行針對全系統的整合性測試,增加了開發者對產品穩定度的信心。

對專案的管理者來說,可以透過CI Server提供的報表,輕鬆地掌握目前專案開發的狀況。透過單元測試報表,看出目前測試程式的涵蓋率及通過率,確保工作項目的完成度。透過靜態程式碼分析報表,找出系統潛在的問題,統一成員的程式碼分析風格。加上自動化部署,部署工作不在佔用資深開發人員的時間,讓開發者可以減少Routing Deploy工作浪費的時間,專注在更重要的工作上,也可以確保每一個最新的程式碼,都會即時的被更新到測試伺服器上供其他人員測試或瀏覽。

使用CI Server的優點

  1. 完全的自動化,第一次設定好之後就不需要再管它
  2. Web介面 (Jenkins),只要透過瀏覽器可以隨時操作瀏覽
  3. 不需重新適應流程,完全依照既有的開發步驟,導入不會打亂習慣的節奏
  4. 提高專案開發透明度,軟體成員和管理者可以透過報表輕鬆掌握目前軟體的狀況
  5. 養成良好的開發習慣 (版本控制、單元測試以及統一的程式碼風格)
  6. 提早發現潛在的風險 (程式碼複雜度、安全性)
  7. 保持軟體穩定,有更改就測試,建置異常馬上發出通知給團隊成員,不怕改壞程式

總結

有了CI Server,可以提高大家對於開發出來產品的信心,因為我們可以透過各種報表說服大家產品的內容是完全Feat Goal,並經過測試!也可以讓開發人員養成良好的開發習慣,多花30%的時間撰寫單元測試,卻可以減少日後無數的維護成本。在更改別人開發的程式碼時,只要確認更動的程式碼有通過寫好的測試,就不怕程式碼被改壞!讓所有在版本控制系統中的程式碼都是可用的,不會因為維護的人員離職,就產生年久失修的問題,大大降低重工率。

萬一真的有人不小心改壞程式碼時,也可以透過整合測試找出更改後邊際效應(Side Effect),不會等到系統真的上線才發現有異常,最大的好處是整個過程都是自動化並且持續進行的,每一天都能享受到它帶來的好處,不但改善了軟體的品質,也改變了使用者的生活,再也不怕改壞東西,也讓程式碼的品質不斷的提高,達成一個正向循環。

Comments

comments powered by Disqus

使用Jenkins打造.Net CI Server (1) - 從零開始

| Comments

你曾經發現在版本控制系統上取得原始碼,卻發現他沒辦法使用嗎?

你曾經擔心你更改的一個小功能是否會影響到系統正常運作嗎?

你認為你們的程式碼是凌亂且難以維護的嗎?

你們程式的組件都是統一由特定人士專門建置,少了他就沒辦法換版嗎?

有了Continuous Integration Server,讓我們的工作開發更加的順暢,所有Checking的程式碼都將會自動進行建置,確保版本控制系統上的都是可用的,完整的進行UnitTest,讓你放手重構或修改既有的功能,透過靜態程式碼分析,統一Coding的風格,還可以發現潛藏的安全性漏洞,甚至作為重構優先順序的依據,建置和部屬不在總是需要依賴特定人士,只要按下一個按鍵,輕輕鬆鬆搞定!

Continuous Integration Server

CI Sever全名叫做Continuous Integration Server,在一個軟體開發團隊中, CI Server扮演了一個很重要的角色,它是幫助我們掌握軟體的品質的最佳第六人,而在接下來的30天之中,我將向大家介紹如何使用Jenkins這套軟體,來重頭打造專屬於自己的CI Server,主要會依據以下幾個大方向來介紹

  • 為什麼需要CI Server?

    在這個系列中,將向大家針對CI Server做簡單的整體介紹,
    包括引進CI Server可以為我們帶來甚麼樣的好處,為什麼我們需要它?

    以及介紹CI Server的主要功能,我們可以怎麼樣的利用它

  • 基本安裝篇

    我們要如何安裝Jenkins?

    以及設定CI Server的基本環境變數,基本簡單介紹Jenkins的主要畫面以及使用的方法

  • 自動化建置

    如何設定專案進行自動化單元測試?

    如何顯示我的測試涵蓋率報表?

    如何整合我的Web UI 自動化測試在CI Server上執行?

  • 靜態程式碼分析

    介紹.Net Web Deployment Framework,如何單鍵完成我的部屬工作?

    設定部屬環境的目錄權限,部屬工作所需要的自訂參數設定

  • MS Build介紹

    介紹MS Build基本語法,以及如何使用MS Build來統一建置流程,讓CI Server新增工作更加容易

  • 實用小技巧

    補充介紹好用的小工具或是PlugIn,來讓我們進行各種操作更加的方便順手,或是讓我們得到第一手系統情報及資訊

總結

希望接下來29天的分享,可以讓大家初步的瞭解CI Server,甚至打造自己的第一套CI Server,透過工具,來讓我們開發軟體的品質提升,減少平常花在排除異常和各種不確定風險的時間,讓自己的軟體隨時處在一個高品質的狀態,降低維護成本,希望對大家都能夠有幫助,也歡迎大家一起討論分享心得 :D

參考資料

Comments

comments powered by Disqus

[Tool]使用Grunt,整合Protractor到Visual Studio中進行自動化測試

| Comments

Grunt是一種前端的自動化工具,可以幫助我們將JavaScript、Css檔案進行最小化、打包、程式碼語法檢查等等工作自動化,相較於Asp.Net內建的Bundle工具,Grunt所提供的用途雖然類似,但更具有彈性,甚至連JavaScript的自動化測試也可以透過它來整合進行,具體的角色有點類似在C#專案中的MsBuild(我們所有的csproj檔其實就是MSBuild的語法寫成),算是一種建置流程的語言。

而Protractor則是專門為Angular.js量身打造的自動化測試Framework,當然我們也可以使用它來對任何其他不是用Angular.js所撰寫的網頁進行自動化測試(理論上只要瀏覽器可以開起來的Html網頁,不是Flash、Silverlight都可以測試),Protractor的執行是透過Command輸入指令來觸發,而Grunt可以幫助我們簡化這些步驟。

今天的文章將用簡單的範例向大家介紹,如何透過Grunt,將Protractor的自動化測試整合到Visual Studio的之中。

安裝Grunt

Grunt是基於node.js所撰寫,所以必須先確保電腦中有安裝node.js,可以在以下網址中下載並安裝node.js

node.js

安裝完成後,打開console程式,輸入以下指令安裝grunt的命令列工具

npm install -g grunt-cli  

到這邊我們就完成了安裝Grunt的基本任務

安裝Protractor

接下來進行Protractor的安裝,Protractor一樣是基於node.js所撰寫,所以我們也可以在console輸入以下的指令,透過npm安裝Protractor

npm install -g protractor

安裝完Protractor之後,我們還需要更新一下自動化執行所需要的服務,一樣在console輸入指令

webdriver-manager-update

做完以上的動作,就完成了Protractor的安裝以及更新。

安裝Extension

在目前的Visual Studio中,要整合Grunt到專案之中,必須要透過Extension的方式 (在不久的將來,Visual Studio的新版本會提供內建的整合),首先要將以下兩個Extension安裝到Visual Studio中

  1. Task Runner Explorer - 用來偵測、綁定和執行Grunt設定檔中的任務
  2. Package Intellisense - 提供package.json(npm)和bower.json的intellisense

設定Grunt基本任務

  1. 我們建立一個新的WebApplication應用程式

  2. 選擇空白專案 (因為只需要寫JavaScript)

  3. 新增Specs/spec.js

  4. 在spec.js撰寫測試案例,這邊我們參考官方Tutorial的程式碼

    describe('angularjs homepage', function () {
        it('should add one and two', function () {
            browser.get('http://juliemr.github.io/protractor-demo/');
            element(by.model('first')).sendKeys(1);
            element(by.model('second')).sendKeys(2);
    
            element(by.id('gobutton')).click();
    
            expect(element(by.binding('latest')).getText()).
                toEqual('5'); // This is wrong!
        });
    });
    

    來源網址:http://angular.github.io/protractor/#/tutorial

  5. 在專案新增package.json,這是包含我們protractor和grunt所需要相關套件的node.js設定檔

  6. 在package.json輸入專案資訊以及我們需要的套件

    {
      "name": "GruntProtractorSample",
      "version": "0.0.0",
      "description": "",
      "main": "index.js",
      "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
      },
      "author": "",
      "license": "ISC",
      "devDependencies": {
        "grunt": "^0.4.5",
        "grunt-contrib-connect": "^0.9.0",
        "grunt-protractor-runner": "^1.1.4",    
        "protractor": "^1.4.0"
      }
    }
    
  7. 在專案新增protractor.conf.js,這是自動化測試相關的設定所在

  8. 輸入設定資料,指定自動讀取Specs資料夾下的所有測試案例

    // An example configuration file.
    exports.config = {
        seleniumAddress: 'http://localhost:4444/wd/hub',
    
        // Capabilities to be passed to the webdriver instance.
        capabilities: {
            'browserName': 'chrome'
        },
    
        // Spec patterns are relative to the current working directly when
        // protractor is called.
        specs: [
            'Specs/**/*.js'
        ],
    
        // Options to be passed to Jasmine-node.
        jasmineNodeOpts: {
            showColors: true,
            defaultTimeoutInterval: 30000
        }
    };
    
  9. 新增gruntfile.js,裡面包含了grunt執行的參數和任務

  10. 輸入grunt的設定

    module.exports = function (grunt) {
        grunt.initConfig({
            protractor: {
                e2e: {
                    options: {
                        configFile: 'protractor.conf.js',
                        keepAlive: true
                    }
                }
            },
            protractor_webdriver: {
                start: {
                    options: {
                        path: ''
                    }
                }
            },
        });
    
        //add grunt protractor npm tasks into grunt.    
        grunt.loadNpmTasks('grunt-protractor-webdriver');
        grunt.loadNpmTasks('grunt-protractor-runner');
    
        grunt.registerTask('default', ['protractor_webdriver:start', 'protractor:e2e']);
    }
    
  11. 打開console,切換至package.json所存在的檔案目錄,輸入以下的指令安裝套件

    npm install
    

    安裝成功後應該會看到類似的畫面

  12. 回到Visual Studio,開始Task Runner Explorer(在檢視 > 其他視窗 > Task Runner Explorer),在default使用滑鼠右鍵選擇Run

  13. 成功執行後,應該會看到自動開啟瀏覽器進行測試

    Task Runner Explorer也會出現執行結果,這邊出現錯誤是正常的,因為剛剛的TestCase是一個錯誤的TestCase

  14. 我們修改spec.js,將最後的toEqual修改為3

    describe('angularjs homepage', function () {
        it('should add one and two', function () {
            browser.get('http://juliemr.github.io/protractor-demo/');
            element(by.model('first')).sendKeys(1);
            element(by.model('second')).sendKeys(2);
    
            element(by.id('gobutton')).click();
    
            expect(element(by.binding('latest')).getText()).
                toEqual('3'); // This is correct!
        });
    });     
    
  15. 再次執行grunt default,可以看到成功通過測試

如此一來,我們之後想要執行自動化測試,就不再需要記憶指令,只需要按一下滑鼠就可以啟動測試囉!

本日小結

grunt的出現,讓我們在建置JavaScript(例如最小化)或其他動作時更加的方便有彈性,並且網路上的Open Source Library也非常多,讓我們設計適合自己的JavaScript建置流程時更加的方便,而目前Visual Studio 2013使用grunt還是需要透過PlugIn的方式,但已經大大的簡化我們使用grunt時所需要的操作,今天的介紹可以幫助我們更輕鬆的在Visual Studio中使用grunt來執行Protractor的自動化測試,讓我們可以不再需要記憶繁雜的指令,更專注在開發!關於今天的內容,歡迎大家提出問題一起討論喔 ^_^

參考資料:

Comments

comments powered by Disqus