2013年10月31日 星期四

如何 ASP.NET MVC 4 WebApi 跨域傳輸資料

ASP.NET MVC 4 WebApi 專案中,架構上把不同功能的 WebApi 全部都切開,也就是說,將每一塊功能的 Api 都設一個網域,將資料結構分散。因為WebApi 架設在雲端,所以如果某個專案負載過重,也只需要針對一組專案做環境上的調整。

現在問題來了,當我在做跨網域資料存取時,發生無法存取的問題,原因只是因為對 WebApi 存取預設只有自己的網域才可以,所以必須要設置允許網域。

方法 1.


直接修改 web.config ,不過這是針對所有 Action。
<location path="Sample.txt">
    <system.webServer>
      <httpProtocol>
        <customHeaders>
          <add name="Access-Control-Allow-Origin" value="*" />
        </customHeaders>
      </httpProtocol>
    </system.webServer>
</location>

方法 2.


加入一個類別,內容為以下所示:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

using System;
using System.Web.Http.Filters;

namespace Workflow.Filters
{
    public class AllowCrossSiteJsonAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
        {
            if (actionExecutedContext.Response != null)
                actionExecutedContext.Response.Headers.Add("Access-Control-Allow-Origin", "*");

            base.OnActionExecuted(actionExecutedContext);
        }
    }
}

最後你在 Controller 或者是 Action 上面加上屬性,即可允許跨網域傳輸資料:
    [AllowCrossSiteJson]
    public class InstancesController : ApiController
    {
        // ......

    }




2013年10月27日 星期日

ASP.NET MVC 4 中將資料寫入 Windows Azure Table Storage

繼前一篇 ASP.NET MVC 4 WebApi 中使用 ActionFilter 紀錄 Log 提到如何擷取出使用者使用 Action 的狀況,接下來就是將這些狀況紀錄到 Windows Azure Table Storage (儲存體) 內。

1.

首先,先將 Table Storage 的連接字串 (Connection String) 記下來,可以先到 Windows Azure 內找到這個資訊,請照下圖方式找到:

將這組組好的連接字串,放到 ASP.NET MVC 4 專案中的 Windows Azure Web Role 的組態檔內,多設置一組連結字串,照以下圖片設置:

2.

此時你就可以開始寫程式了,可以先將建立 Table Storage 連接:
CloudStorageAccount storageAccount = CloudStorageAccount.Parse(
    CloudConfigurationManager.GetSetting("StorageConnectionString"));

// 建立連接
CloudTableClient tableClient = storageAccount.CreateCloudTableClient();

在 Azure Table 內,每筆資料需要兩個主要的 Key,PartitionKey 和 RowKey,這兩個 Key 值可以組成唯一值。其實要看這兩筆 Key 很簡單,把 PartitionKey 看成是資料表名稱、 RowKey 就看成是主鍵,因為這兩組 Key 如果在 SQL Server 當然很好分辨,但是資料全部轉為 Table 格式,就只能這樣子去看待。

所以要先設定 PartitionKey 名稱,並且建立此表。
private const string LogTableName = "Logs";

...

// 假設表不存在則建立。
tableClient.CreateTableIfNotExist(LogTableName);

// 取得 Table 
TableServiceContext serviceContext = tableClient.GetDataServiceContext();

3.

接著設置一組類別,當作是 Table 的所有欄位,要繼承 TableServiceEntity,必須引用參考 Microsoft.WindowsAzure.StorageClient:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Runtime.Serialization;

using Microsoft.WindowsAzure.StorageClient;

namespace EnterprisePortal.Models
{
    [NotMapped]
    public class Log : TableServiceEntity
    {
        public Log(string partitionKey, string rowKey)
            : base(partitionKey, rowKey)
        {

        }

        public Log() : this("Logs", Guid.NewGuid().ToString())
        {
        }

        public string Location { get; set; }

        public string ServerIP { get; set; }

        public string PublicIP { get; set; }

        public string ErrorMessage { get; set; }

        public Guid Creater { get; set; }

    }
}

4.

可以開始使用這個類別對 Azure Table 做 CRUD 的動作了。
Log _log = new Log();

if (context.Exception == null)
{
    _log.Creater = Guid.NewGuid();
    _log.ErrorMessage = string.Empty;
    _log.Location = context.ActionContext.Request.RequestUri + " | " + context.ActionContext.Request.Method;
    _log.PublicIP = GetPublicIP();
    _log.ServerIP = GetServerIP();
}
else
{
    _log.Creater = Guid.NewGuid();
    _log.ErrorMessage = context.Exception.Message;
    _log.Location = context.Exception.TargetSite.DeclaringType.ToString() + " | " + context.Exception.TargetSite.Name;
    _log.PublicIP = GetPublicIP();
    _log.ServerIP = GetServerIP();
}

/* 刪除 */
List<Log> lstLogs =
(from e in serviceContext.CreateQuery<Log>(LogTableName)
 where e.PartitionKey == "Logs"
 select e).ToList();

foreach (var t_Log in lstLogs)
    serviceContext.DeleteObject(t_Log);

serviceContext.SaveChangesWithRetries();

/* 新增 */

serviceContext.AddObject(LogTableName, _log);

serviceContext.SaveChangesWithRetries();

/* 查詢 */
CloudTableQuery<Log> partitionQuery =
(from e in serviceContext.CreateQuery<Log>(LogTableName)
 where e.PartitionKey == "Logs"
 select e).AsTableServiceQuery<Log>();

 
foreach (Log entity in partitionQuery)
{
    Console.WriteLine("{0}, {1}\t{2}\t{3}", entity.PartitionKey, entity.RowKey,
        entity.PublicIP, entity.ErrorMessage);
}







2013年10月26日 星期六

ASP.NET MVC 4 WebApi 中使用 ActionFilter 紀錄 Log

在 ASP.NET MVC 4 中每個 Controller Action 執行期間,需要紀錄每個使用者的呼叫 Action 的狀態,一方面記錄使用過程,另一方面如果在此當中發生錯誤,紀錄此錯誤狀態訊息,好讓程式人員能夠依照錯誤訊息來改善系統功能。

首先必須要了解整個 Action 在觸發時,會執行哪一些 Filter:
圖片來源:http://www.cnblogs.com/me-sa/archive/2009/06/09/1499414.html

依上圖來看,事件的紀錄,不管是否產生錯誤,都可以在 OnActionExecuted 紀錄。

接下來說明一下在 ASP.NET MVC 4 WebApi 中 設置過程。

1.

新增一個類別檔在 Filters 底下,暫名為 WebApiActionFilterAttribute.cs,內容程式碼為:
namespace EnterprisePortal.Filters
{
    using System;
    using System.Net;
    using System.Net.Http;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;

    using System.Web.Http.Controllers;
    using System.Web.Http.Filters;
    using Microsoft.WindowsAzure;
    using Microsoft.WindowsAzure.StorageClient;
    using EnterprisePortal.Models;

    public class WebApiActionFilterAttribute : ActionFilterAttribute
    {
        private const string LogTableName = "Logs";

        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            
        }

        public override void OnActionExecuted(HttpActionExecutedContext context)
        {
           ... 紀錄 Log,狀態都在 context 可以找到
            
        }
    }
}

2.

最後直接在你的 BaseApiController 中類別上加上 [WebApiActionFilter],或者直接在 Global.asax 註冊,程式碼如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

using System.Web.Http;
using EnterprisePortal.Models;
using EnterprisePortal.Filters;


namespace EnterprisePortal.Controllers
{
    [WebApiActionFilter]
    public class BaseApiController : ApiController
    {
        protected DefaultConnection db = new DefaultConnection();
    }
}

3.

測試吧! 隨便在一個 Action 丟出引數為無效的錯誤:
public IEnumerable Get()
{
    throw new ArgumentException();
    return new string[] { "value1", "value2" };
}

4.

最後在 WebApiActionFilterAttribute.cs 中,OnActionExecuted 設上中斷點,測試是否錯誤訊息有擷取到:


2013年10月20日 星期日

如何切換 Area 的 Web Api 路由 ( Route )

在 ASP.NET MVC 4 的專案中,建立 Areas ( 區域 ) 通常是在系統功能必須做切換的時機下使用,例如:網站前台與後台、或者電子商務系統可能會有店面、產品檢閱、使用者帳戶管理及購買系統 ... 等等。

區域可以有自己的 Model、 View 、Controller 模組,有獨立的網址路由和主版,提供很大的開發彈性。

為此,我選擇將網站中的其中一部分系統功能用 Areas 開發,但是我在 Backend 開了一個 Api Controller,名稱是 Member,處理會員資訊,如下圖。



我本以為這個 WebApi 的路徑會是 Backend/api/MemberApi/,可是實際路徑 api/MemberApi,後來我想如果別的 Areas 也有建一個 MemberApi,會怎麼樣?


會這樣。


最後就是它不知道該走哪一個路由。

此時我就找 BackendAreaRegistration.cs 這個 Areas 的路由檔案如下:

context.MapRoute(
    "Backend_default",
    "Backend/{controller}/{action}/{id}",
    new { action = "Index", id = UrlParameter.Optional }
);

這裡的路由是只對一般的 Controller,後來我 google 了一下解法,參考了這篇 ASP.NET MVC 4 WebAPI. Support Areas in HttpControllerSelector

1.

新增一個類別 AreaHttpControllerSelector.cs,目前是擺在 App_Start 底下。
namespace EnterprisePortal.Infrastructure.Dispatcher
{
    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using System.Net.Http;
    using System.Web.Http;
    using System.Web.Http.Controllers;
    using System.Web.Http.Dispatcher;

    public class AreaHttpControllerSelector : DefaultHttpControllerSelector
    {
        private const string ControllerSuffix = "Controller";
        private const string AreaRouteVariableName = "area";

        private readonly HttpConfiguration _configuration;

        public AreaHttpControllerSelector(HttpConfiguration configuration)
            : base(configuration)
        {
            _configuration = configuration;
        }

        private Dictionary<string, Type> _apiControllerTypes;

        private Dictionary<string, Type> ApiControllerTypes
        {
            get { return _apiControllerTypes ?? (_apiControllerTypes = GetControllerTypes()); }
        }

        private static Dictionary<string, Type> GetControllerTypes()
        {
            var assemblies = AppDomain.CurrentDomain.GetAssemblies();

            var types = assemblies.SelectMany(a => a.GetTypes().Where(t => !t.IsAbstract && t.Name.EndsWith(ControllerSuffix) && typeof(IHttpController).IsAssignableFrom(t)))
                .ToDictionary(t => t.FullName, t => t);

            return types;
        }

        public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
        {
            return GetApiController(request) ?? base.SelectController(request);
        }

        private static string GetAreaName(HttpRequestMessage request)
        {
            var data = request.GetRouteData();

            if (!data.Values.ContainsKey(AreaRouteVariableName))
            {
                return null;
            }

            return data.Values[AreaRouteVariableName].ToString().ToLower();
        }

        private Type GetControllerTypeByArea(string areaName, string controllerName)
        {
            var areaNameToFind = string.Format(".{0}.", areaName.ToLower());
            var controllerNameToFind = string.Format(".{0}{1}", controllerName, ControllerSuffix);

            return ApiControllerTypes.Where(t => t.Key.ToLower().Contains(areaNameToFind) && t.Key.EndsWith(controllerNameToFind, StringComparison.OrdinalIgnoreCase))
                    .Select(t => t.Value).FirstOrDefault();
        }

        private HttpControllerDescriptor GetApiController(HttpRequestMessage request)
        {
            var controllerName = base.GetControllerName(request);

            var areaName = GetAreaName(request);
            if (string.IsNullOrEmpty(areaName))
            {
                return null;
            }

            var type = GetControllerTypeByArea(areaName, controllerName);
            if (type == null)
            {
                return null;
            }

            return new HttpControllerDescriptor(_configuration, controllerName, type);
        }
    }
}

2.

然後在 Global.asax 引用兩個命名空間。
using EnterprisePortal.Infrastructure.Dispatcher;
using System.Web.Http.Dispatcher;

最後在 Application_Start() 加上
GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), new AreaHttpControllerSelector(GlobalConfiguration.Configuration));
            
RouteTable.Routes.MapHttpRoute(
    name: "AreaApi",
    routeTemplate: "api/{area}/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

3.

運行後即可。



2013年10月19日 星期六

Team Foundation Server (TFS) 初次取得線上版本

最近小弟因工作需求必須要使用 Team Foundation Server 上的功能,與 SVN 使用上其實很不一樣,往後會發佈相關教學說明,現在就來介紹初次如何取得線上版本。

1.

首先,必須要先決定線上版本的程式碼要放在本機的哪個目錄中,此範例就先暫訂存在 「D:\網站\開發站\」 底下。

2.

接著開啟 Visual Studio 2012,檢視 > Team Explorer,右側欄出現 Team Explorer 視窗

※ 若對應不到本機端資料夾,照以下方式處理:動作 > 管理工作區

選定 TFS 的資料夾

設定本機資料夾,按下確定

按下是


3.

最後就可以看到 TFS 上的最新版本已經複製到所設定的資料夾內。




將網站部署到 Windows Azure

繼前一篇 如何將 SQL Server 資料庫部署到 SQL Azure,今天就來介紹將網站部署到 Windows Azure。

1.

至 Windows Azure 選擇網站,並決定要部屬的網站名稱。

2.

下載發行設定檔 ( 副檔名為 PublishSettings )

必須妥善保管此檔案

3.

選擇網站要使用的 SQL Azure,並且取得它的連接 字串,其中密碼是不顯示在上面的, 必須修改密碼部分才是完整的 連接字串

4.

針對要上傳的專案點擊「發行」

5.

設置發行設定檔,若在伺服器總管中有設定 Windows Azure 網站, 亦可直接設置。


6.

選定好發行設定檔或者 Windows Azure 網站,按下一個就會帶出網站的設定資訊。可按下驗證連線確定無誤。

7.

接下來要設定資料連接字串,再來就需要複製第 3 步驟的資料庫字串置換密碼後貼到這裡。

8.

最後確認上傳檔案。按下發行。

9.

發行完就可以確認網站是否運行。打完收工



2013年10月18日 星期五

如何將 SQL Server 資料庫部署到 SQL Azure

最近小弟因工作需求必須要使用 Windows Azure 上的功能,今天就來介紹將 SQL Server 資料庫部署到 SQL Azure 的步驟。

1.

在 SQL Server 選定要同步的資料庫,點選右鍵 > 工作 > 將資料庫部署到 SQL Azure

2.

下一步

3.

此步驟要連接資料庫,可設定網域內的 SQL Server 或者是 SQL Azure 的,在此連接字串可從 Windows Azure 取得在 Key-in 到連接資訊中。( 機密資訊已處理 )



4.

確認部署詳細資料

5.

部署中...

6.

請注意,每個資料表必須要有叢集索引 ( Clustering Index ),要不然只要一個資料表失敗就會整個 Rollback。

7.

最後,雲端上就會多出剛剛部署完的資料庫。

8.

打完收工