顯示具有 ASP.NET MVC 4 標籤的文章。 顯示所有文章
顯示具有 ASP.NET MVC 4 標籤的文章。 顯示所有文章

2015年5月7日 星期四

Async/Await、Task產生執行緒

之前一直認為,Async/Await跟Task一樣都會建出新的執行緒,這是錯誤的,Await是用主執行緒,遇到真正的Async方法(ex:HttpClient.GetAsync)或手動執行的Task.Run或Task.Factory.StartNew,這樣才會建立出新的執行緒

//主執行緒ID
Console.WriteLine(string.Format("Main:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId));

//Task執行緒ID
Task t = Task.Run(() => { Console.WriteLine(string.Format("Task:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId)); });

//使用Await呼叫方法
await GetAwaitThreadId();

//等候Task完成
t.Wait();

async Task GetAwaitThreadId()
{
  //Await方法執行緒ID
  Console.WriteLine(string.Format("Await:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId));
}



另外,當呼叫非同步方法時,在前半部的程式碼(Await之前),依然是原本的執行緒在執行,遇到Await時,原本的執行緒會回到呼叫端
後半部的程式碼(Await之後),會暫時被保留,等到要等待的工作完成以後,會另外找一條執行緒出來執行後半部程式碼
//呼叫方法
Task t = PrintAwaitThreadId();
for (int i = 0; i < 10; i++)
{
  //列出目前執行緒
  Console.WriteLine(string.Format("Main[{1}]:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId, i));
  //產生新的執行緒做時間延遲
  await Task.Delay(rnd.Next(1, 10));
}
await t;

async Task PrintAwaitThreadId()
{
  for (int i = 0; i < 10; i++)
  {
    //列出目前執行緒
    Console.WriteLine(string.Format("Await[{1}]:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId, i));
    //產生新的執行緒做時間延遲
    await Task.Delay(rnd.Next(1, 10));
  }
}





2015年4月14日 星期二

使用 Windows Azure Redis 快取

有些資料需要大量運算,但是變動機會極低的時候,常會把運算結果存起來,下次直接抓出結果即可。

如果有使用Windows Azure,這邊提供一個快取方式。

參考:如何使用 Azure Redis 快取

首先建立Redis Cache Service

安裝StackExchange.Redis並加入參考

先來看一下Code:
  /// <summary>
  /// Cache Redis 連線字串,來自Web.config > AppSettings > CacheRedisConnectionString
  /// </summary>
  private static string _CacheRedisConnectionString = "{名稱}.redis.cache.windows.net,ssl=true,password={密碼}";
  /// <summary>
  /// Cache Redis 預設保留時間
  /// </summary>
  private static int _CacheRedisTimeOut = 60;
  /// <summary>
  /// Cache Redis 連線(類似SQLConnection)
  /// </summary>
  private static ConnectionMultiplexer CacheRedisConnection;
  /// <summary>
  /// Cache Redis 存取(類似entity framework的DbContext)
  /// </summary>
  public static IDatabase CacheRedis;

  /// <summary>
  /// 設定快取初始連線
  /// </summary>
  public static void Initialize()
  {
      if(CacheRedisConnection == null)
      {
    CacheRedisConnection = ConnectionMultiplexer.Connect(_CacheRedisConnectionString);
    if(CacheRedis == null)
    {
        CacheRedis = CacheRedisConnection.GetDatabase();
    }
      }
  }

  /// <summary>
  /// 序列化物件
  /// </summary>
  /// <param name="value"></param>
  /// <returns></returns>
  private static string Serialize(object value)
  {
      if (value == null)
    return null;
      return JsonConvert.SerializeObject(value);
  }

  /// <summary>
  /// 字串反序列化為物件
  /// </summary>
  /// <typeparam name="T">回傳物件型別</typeparam>
  /// <param name="value">字串資料</param>
  /// <returns>物件</returns>
  private static T Deserialize<T>(string value)
  {
      if (string.IsNullOrWhiteSpace(value)) return default(T);
      return JsonConvert.DeserializeObject<T>(value);
  }

  /// <summary>
  /// 取得指定型別快取物件
  /// </summary>
  /// <typeparam name="T">回傳物件型別</typeparam>
  /// <param name="key">快取名稱</param>
  /// <returns>回傳快取資料</returns>
  public static T GetCache<T>(string key)
  {
      string value = GetCache(string key);
      if(string.IsNullOrWhiteSpace(value) == true) return null;
      return Deserialize<T>(GetCache(string key));
  }

  /// <summary>
  /// 取得快取物件
  /// </summary>
  /// <param name="key">快取名稱</param>
  /// <returns>回傳快取資料</returns>
  public static string GetCache(string key)
  {
      //cache為null 或 key為空 或 Cache無資料,回傳null
      if (CacheRedis == null || string.IsNullOrWhiteSpace(key) == true || CacheRedis.KeyExists(key) == false) return null;
      return CacheRedis.StringGet(key);
  }

  /// <summary>
  /// 設定快取物件,(選用)設定系統預設快取保留時間
  /// </summary>
  /// <param name="key">快取名稱</param>
  /// <param name="value">要快取的資料</param>
  /// <param name="defaultexpiry">是否設定預設快取保留時間</param>
  public static void SetCache(string key, object value, bool defaultexpiry = true)
  {
      SetCache(key, value, 0, defaultexpiry);
  }

  /// <summary>
  /// 設定快取物件,並設定快取保留時間
  /// </summary>
  /// <param name="key">快取名稱</param>
  /// <param name="value">要快取的資料</param>
  /// <param name="expiry">快取保留時間(分鐘)</param>
  public static void SetCache(string key, object value, int expiry)
  {
      if (expiry <= 0) throw new Exception("CacheRedisHelpers.SetCache(string key, object value, int expiry) error : No Set Expiry");
      SetCache(key, value, expiry, false);
  }

  /// <summary>
  /// (private)設定快取物件
  /// </summary>
  /// <param name="key">快取名稱</param>
  /// <param name="value">要快取的資料</param>
  /// <param name="expiry">快取保留時間(分鐘)</param>
  /// <param name="defaultexpiry">是否設定預設快取保留時間</param>
  private static void SetCache(string key, object value, int expiry, bool defaultexpiry)
  {
      //CacheRedis為null 或 key為空,則不處理
      if (CacheRedis != null && string.IsNullOrWhiteSpace(key) == false)
      {
    //value不為null,則設定快取,反之刪除
    if (value != null)
    {
        //物件轉字串
        string strvalue = value as string ?? Serialize(value);
        
        //存入Cache
        if (defaultexpiry) //使用預設快取保留時間
      CacheRedis.StringSet(key, strvalue , TimeSpan.FromMinutes(_CacheRedisTimeOut));
        else if (expiry > 0) //使用傳入快取保留時間
      CacheRedis.StringSet(key, strvalue , TimeSpan.FromMinutes(expiry));
        else //不設定快取保留時間
      CacheRedis.StringSet(key, strvalue );
    }
    else CacheRedis.KeyDelete(key);
      }
  }

在程式中,寫好建立連線的Method(Initialize),
建立共用序列化與反序列化Method,
取得的部分,如果取不到資料,Redis Cache也是回傳null,Method只是添加一些判斷減少跟Service連線,
儲存的部分,建立2種較彈性的設定保留時間Method,一個是使用預先設定的預設快取保留時間,另一個是自行設定快取保留時間,
完成了Redis Cache存取功能,接下來使用方式如下:
  //建立連線
  Initialize();
  
  //儲存資料(預設保留時間)
  SetCache("key", objvalue, true);

  //儲存資料(自訂保留時間)
  SetCache("key", objvalue, iexpiry);

  //取得資料(String)
  GetCache("key");

  //取得資料(指定Type)
  GetCache("key");

另外,在CacheRedisConnection.GetDatabase(),可以指定不同的DB(int)儲存資料(預設為0),可以將資料區分讓開發者依不同情境區分資料存放位置。

2015年1月20日 星期二

Entity Framework Code First依屬性(Attribute)設定SQL欄位 (續)

花了點時間把Entity Framework Code First依屬性(Attribute)設定SQL欄位的程式碼Review過一次,總覺得設定的地方怪怪的,GetMethods()以後,指定第幾個到底是為了什麼?

if (propAttr.prop.PropertyType.IsGenericType && propAttr.prop.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
  methodInfo = entityConfig.GetType().GetMethods().Where(p => p.Name == "Property").ToList()[7];
}
else
{
  methodInfo = entityConfig.GetType().GetMethods().Where(p => p.Name == "Property").ToList()[6];
}
decimalConfig = methodInfo.Invoke(entityConfig, new[] { lambdaExpression }) as DecimalPropertyConfiguration;
decimalConfig.HasPrecision((byte)propAttr.attr.Precision, (byte)propAttr.attr.Scale);

從Debug模式下去看,原來Property有很多,需要指定正確的Type,不然就會發生轉換失敗的錯誤,而部分資料類別又有區分允不允許NULL,所以造就了上面詭異的CODE
判斷有沒有允許NULL後,強制指定對應的資料型別,這個方法在網路上廣為流傳,但這實在是太暴力了,如果哪一天順序被改掉,會有一堆人死在那邊
為了避免這種情況發生,決定嘗試去判斷對應型別有沒有允許NULL,無奈小弟功力不夠深厚,一直找不出解決方法

運氣還不錯的找到了一組原始碼HerrKater/EntityHelper.cs
參考了裡面的ProcessDecimalProperties,在GetMethod取得MethodInfo時,後面傳入的Type[]使用LambdaExpression.GetType()去指定對應型別,這時候就可以找到我們正確的DecimalPropertyConfiguration
MethodInfo methodInfo = entityConfig.GetType().GetMethod("Property", new[] { lambdaExpression.GetType() });
DecimalPropertyConfiguration decimalConfig = methodInfo.Invoke(entityConfig, new[] { lambdaExpression }) as DecimalPropertyConfiguration;
decimalConfig.HasPrecision((byte)propAttr.attr.Precision, (byte)propAttr.attr.Scale);


另外參考了他取得在DbContext內宣告DbSet<>的方法
var contextProperties = typeof (T).GetProperties().Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof (DbSet<>));

決定把原本用命名空間做判斷的方式改掉,但是他是用PropertyInfo,然後在迴圈裡面進行轉換,這樣會變更到原本我們的Code,所以做一些調整
var tmplist = typeof(T).GetProperties().Where(p => p.PropertyType.IsGenericType && p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>)).SelectMany(p => p.PropertyType.GetGenericArguments()).Where(t => t.GetProperties(BindingFlags.Public | BindingFlags.Instance).Any(p => p.GetCustomAttribute<DecimalAttribute>() != null));

備註:
取得PropertyInfo以後,要先取PropertyType找出宣告的Type,但是因為有外層的DbSet<>,所以還需要再使用GetGenericArguments()去找出定義的Model

參考:
Entity Framework Code First建立Decimal,並設定大小
Entity Framework Code First依屬性(Attribute)設定SQL欄位
HerrKater/EntityHelper.cs

2015年1月19日 星期一

Entity Framework Code First依屬性(Attribute)設定SQL欄位

繼上遍Entity Framework Code First建立Decimal,並設定大小,覺得要針對每個Model設定SQL欄位大小,又要設定驗證大小,日後維護要改2個地方,有點給他麻煩,甚至容易忘記

嘗試對已有設定DecimalAttribute的Model在OnModelCreating的時候,把所有的都一次設定完

首先建立一個靜態類別DecimalModelCreating,並建立設定方法OnModelCreating(如下)


public static class DecimalModelCreating
{
  public static void OnModelCreating(DbModelBuilder modelBuilder, string Namespace)
  {
    //取得所有在指定命名空間中,有欄位包含DecimalAttribute的類別
    foreach (Type classType in from t in Assembly.GetAssembly(typeof(DecimalAttribute)).GetTypes() where t.IsClass == true && string.IsNullOrEmpty(t.Namespace) == false && t.Namespace.Equals(Namespace, StringComparison.CurrentCultureIgnoreCase) == true && t.GetProperties(BindingFlags.Public | BindingFlags.Instance).Any(p => p.GetCustomAttribute<DecimalAttribute>() != null) select t)
    {
      //取得類別中,包含DecimalAttribute的欄位
      foreach (var propAttr in classType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.GetCustomAttribute<DecimalAttribute>() != null).Select(p => new { prop = p, attr = p.GetCustomAttribute<DecimalAttribute>(true) }))
      {
        var entityConfig = modelBuilder.GetType().GetMethod("Entity").MakeGenericMethod(classType).Invoke(modelBuilder, null);
        ParameterExpression param = ParameterExpression.Parameter(classType, "c");
        Expression property = Expression.Property(param, propAttr.prop.Name);
        LambdaExpression lambdaExpression = Expression.Lambda(property, true, new ParameterExpression[] { param });
        DecimalPropertyConfiguration decimalConfig;
        MethodInfo methodInfo;
        //依欄位是否允許null,設定對應MethodInfo
        if (propAttr.prop.PropertyType.IsGenericType && propAttr.prop.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
        {
          methodInfo = entityConfig.GetType().GetMethods().Where(p => p.Name == "Property").ToList()[7];
        }
        else
        {
          methodInfo = entityConfig.GetType().GetMethods().Where(p => p.Name == "Property").ToList()[6];
        }
        decimalConfig = methodInfo.Invoke(entityConfig, new[] { lambdaExpression }) as DecimalPropertyConfiguration;
        decimalConfig.HasPrecision((byte)propAttr.attr.Precision, (byte)propAttr.attr.Scale);
      }
    }
  }
}

接著將DbContextOnModelCreating調整為使用DecimalModelCreating進行設定
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
  DecimalModelCreating.OnModelCreating(modelBuilder, "YourNamespance");
  base.OnModelCreating(modelBuilder);
}

參考:
Entity Framework Code First建立Decimal,並設定大小

2015/01/19調整:
近期將部分資料表移到Azure Storage Table的時候,發生了資料庫移轉錯誤
在模型產生期間偵測到一個或多個驗證錯誤:
System.Data.Entity.Edm.EdmEntityType: : EntityType 'myclass' 未定義索引鍵。請定義此 EntityType 的索引鍵。
System.Data.Entity.Edm.EdmEntitySet: EntityType: EntitySet 'myclass' 是以未定義索引鍵的類型 'myclass' 為基礎。

檢查OnModelCreating後發現,在條件中,只要是在指定命名空間(Namespace)的Model都會被設定:
t.Namespace.Equals(Namespace, StringComparison.CurrentCultureIgnoreCase) == true

增加判斷語法,除了命名空間以外,還要有宣告在DbContext裡面:
typeof(MyContext).GetProperties().Where(x => x.PropertyType.IsGenericType && x.PropertyType.Name.StartsWith("DbSet") && x.PropertyType.GenericTypeArguments != null && x.PropertyType.GenericTypeArguments.Count() > 0).SelectMany(x => x.PropertyType.GenericTypeArguments).Select(x => x.Name).Contains(t.Name)

參考:
Entity Framework Code First建立Decimal,並設定大小
延伸閱讀:
Entity Framework Code First依屬性(Attribute)設定SQL欄位 (續)

2014年11月25日 星期二

ASP.NET MVC 解決加入「控制器」( Controller ) 消失的問題

前陣子將專案升級,在 NuGet 安裝 ASP.NET Web API 2.2 for OData v4.0 套件,產生許多 dll 版本錯誤之問題。

修正後,又發現在要加入「控制項」( Controller ) 時,找不到該選項,如下圖:


甚至連加入「區域」( Areas ) 也找不到。

後來找到 visual studio 2013 'add controller' missing 文章解說,照以下方法即可解決:

1.

卸載專案

2.

編輯專案檔

3.

尋找 ProjectTypeGuids 標籤 ( 請先備分 ),將內容改為
<ProjectTypeGuids>{E3E379DF-F4C6-4180-9B81-6769533ABE47};{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>

儲存

4.

重新載入專案,完成。





2014年8月8日 星期五

ASP .NET MVC4 WebApi - 不允許路由(Route)跨區域(Area)找Controller

目前專案設定權限是採用Area進行區分,基本規劃為角色 1..n Area

最近在強化權限控管,發現在Area找不到Controller的時候,竟然會去其他的Area找有沒有符合名稱的Controller(如何切換 Area 的 Web Api 路由)

對規劃的權限區分來說,這一個非常嚴重的問題


舉個例子:
(角色=>Area)
人員管理者=>People
薪資管理者=>Salary

王小明有人員管理者這個角色,所以他可以使用People裡的所有Controller, 碰巧王小明知道了Salary的某隻Api路徑可以取得全公司員工的薪資(api/Salary/AllPeople), 就權限管理機制,王小明沒有Salary的Area權限,所以被判定為無權限(401)。

到這邊都很沒問題,一切都很美好...如果這樣就結束,還要這篇幹嘛

偏偏王小明很有求知的精神,想說只要是People就會被放行,那如果把Salary改成People不知道可不可以?
於是原本的路徑(api/Salary/AllPeople)變成了(api/People/AllPeople)。

這時候權限機制判斷王小明是人員管理者,可以訪問People的Area,所以放行讓他通過, 殊不知在People下沒有AllPeople的Controller,但對權限機制來說並不重要,有沒有是ControllerSelector的問題。

這時候AreaHttpControllerSelector出馬了,經過一連串的檢查,發現People下沒有AllPeople的Controller,沒辦法走下去,那就交給老爸(DefaultHttpControllerSelector)去處理吧。

而老爸是不管這些Area的,只要符合ControllerName就好,所以找到了Salary的AllPeople,然後就跟小弟(ApiControllerActionSelector)說,從Salary的AllPeople找符合的Action來執行吧。

接著就會發現王小明引發美麗錯誤的結果,他拿到不該拿的資料了...其實也還好


這樣是不行的,如果Route有接到area的話,就只能從指定的Area下找Controller,沒找到就丟錯誤出去,所以修改一下AreaHttpControllerSelector
...略...
        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)
            {
                //原本回傳null是為了向老爸求救,今天兒子要獨當一面了
                //return null;
                throw new HttpResponseException(request.CreateErrorResponse(HttpStatusCode.NotFound,
                    string.Format("No route providing a controller name was found to match request URI \"{0}\"", new object[] { request.RequestUri })));
            }

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


參考:
如何切換 Area 的 Web Api 路由
DefaultHttpControllerSelector.cs Source Code
ApiControllerActionSelector.cs Source Code

2014年7月11日 星期五

時區到各瀏覽器顯示時間統一處理方式

今天要討論的是,當日期時間資料送到瀏覽器時,顯示出來的結果往往讓人難以捉摸。

DateTimeDateTimeOffset轉成JSON以後的結果:
{
  "DateTimeOffset": "2014-07-11T00:00:00+08:00",
  "DateTime": "2014-07-11T00:00:00"
}
DateTimeOffset後面多了+08:00,表示這個時間是特定時區的時間,而DateTime是沒有的。

這時候丟到JavaScript的Date裡面:
  new Date(DateTimeOffset);
  new Date(DateTime);
呈現在畫面上就會變成:
Browser DateTimeOffset DateTime
Chrome Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間) Fri Jul 11 2014 08:00:00 GMT+0800 (台北標準時間)
IE11 Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間) Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間)
FireFox Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間) Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間)
當沒有指定時區時,顯示出來的結果因各瀏覽器定義不同,而有不同的結果。

當不想要資料庫所有欄位都記時區的時候,就需要變更一下Json的DateTimeConverter

首先宣告一個UTCDateTimeConverter,並且繼承IsoDateTimeConverter ,然後在轉JSON的時候自動轉換成設定的格式

備註1:希望給瀏覽器自動轉換為Local時間,所以是轉換成UTC(國際標準時間)
備註2:避免日後Newtonsoft.Json對IsoDateTimeConverter更新,所以不重寫WriteJson,使用設定轉換格式並由IsoDateTimeConverter的WriteJson進行轉換。
/// <summary>
/// DateTime/DateTimeOffset 傳換為UTC日期格式 (Ex: 2014-07-11T12:53:00+00:00).
/// </summary>
public class UTCDateTimeConverter : IsoDateTimeConverter
{
  /// <summary>
  /// 日期轉成Json格式
  /// </summary>
  /// <param name="writer" >The JsonWriter to write to.</param>
  /// <param name="value" >The value.</param>
  /// <param name="serializer" >The calling serializer.</param>
  public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
  {
    //DateTimeOffset轉換時會自動加上時區,所以要先把他轉成UTC時間的DateTime
    if (value.GetType() == typeof(DateTimeOffset))
    {
      DateTimeOffset tmpDateTimeOffset = (DateTimeOffset)value;
      value = tmpDateTimeOffset.DateTime.Add(-tmpDateTimeOffset.Offset);
    }
    //設定想要轉換的格式
    base.DateTimeFormat = "yyyy-MM-ddTHH:mm:ss.FFFFFFFK+00:00";
    //使用預設的WriteJson進行轉換
    base.WriteJson(writer: writer, value: value, serializer: serializer);
  }
}
因為希望傳出去時間格式是統一的,而使用的也是WebApi,所以直接在App_Start/WebApiConfig.cs進行設定:
config.Formatters.JsonFormatter.SerializerSettings.Converters.Add(new UTCDateTimeConverter());

轉出來的Json為:
{
  "DateTimeOffset": "2014-07-10T16:00:00+00:00",
  "DateTime": "2014-07-11T00:00:00+00:00"
}
DateTimeOffset已經是被剪了8小時。

呈現在畫面上就會變成:
Browser DateTimeOffset DateTime
Chrome Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間) Fri Jul 11 2014 08:00:00 GMT+0800 (台北標準時間)
IE11 Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間) Fri Jul 11 2014 08:00:00 GMT+0800 (台北標準時間)
FireFox Fri Jul 11 2014 00:00:00 GMT+0800 (台北標準時間) Fri Jul 11 2014 08:00:00 GMT+0800 (台北標準時間)


瀏覽器都統一了^^

參考:
IsoDateTimeConverter Source
Add different Json.NET DateTimeConverters through the JsonFormatter's SerializerSettings
Create a JSON.NET Date to String custom Converter

2014年7月8日 星期二

ASP .NET MVC4 WebApi-CultureInfo-文化特性排序

相信很多人都發生過,本機排序正常,一上到正式主機以後,排序出來的結果卻不一樣。

其實.NET的執行緒就跟SQL Server的資料表一樣有定序問題,所以排序結果會因為設定而有所不同,基本上執行緒的定序是跟著Server預設語系的。

在變更執行緒的語系之前,先看一下CultureInfo類別

MSDN的說明是:提供有關特定文化特性 (Culture) 的資訊 (文化特性在 Unmanaged 程式碼開發中稱為「地區設定」(Locale))。 提供的資訊包括文化特性的名稱、書寫系統、使用的曆法,以及日期和排序字串的格式。

最後面那一句話,排序字串的格式

了解了這個類別的用途,接著來看一下幾個語系排序的規則。

文化特性名稱
文化特性
預設的排序名稱和識別項
替代排序名稱和識別項
zh-TW
中文 (台灣)
筆劃:0x00000404
注音符號:0x00030404
zh-CN
中文 (中華人民共和國)
發音:0x00000804
筆劃:0x00020804
ja-JP
日文 (日本)
預設:0x00000411
Unicode:0x00010411

知道各語系排序規則以後,接著我們只需要在排序前加上一個指定就搞定了。

System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(language);

這樣是不是很簡單呢。(明明為了排序搞了很久)

參考:
CultureInfo與中文字串排序
CultureInfo 類別


2014年5月15日 星期四

Entity Framework 與 LINQ -- 篩選(Where)使用時機

最近發現,使用Entity Framework的話,要注意下Where的時機,因為有可能是會在SQL端篩選,也有可能會在C#端篩選,可能會導致不同的結果,甚至發生不支援的情況。

拿字串A是否包含了字串B作為範例,這時候在SQL端(還沒產生成實體物件)時執行結果 跟 C#端(已從DB撈出資料)執行結果 就會發生不同的狀況。
先來看一Where下在資料還沒產生成實體物件的時候
C#:
var query = db.Table.Where(x => x.Name.Contains("abc")).ToList();
SQL執行語法會是
SELECT[EmployeeId],[Name]
FROM [dbo].[Employee]
WHERE [Name] LIKE '%abc%'
由此可見,資料判斷是在SQL端處理,然後把結果丟回到C#端。

再來是Where下在ToList()後面
C#:
var query = db.Table.ToList().Where(x => x.Name.Contains("abc"));
SQL執行語法會是
SELECT[EmployeeId],[Name]
FROM [dbo].[Employee]
這時候變成了把所有Employee資料撈出來以後,在篩選資料。

一個是在SQL端下LIKE,一個是在C#端執行Contains,都是包含"abc",但是結果就會不一樣囉。

SQL的LIKE是不分辨大小寫的,所以"ABC"、"AbC"、"abc"、"abC"...都是會被認為是OK的資料。
但是在C#的Contains是有區分大小寫的,所以只有"abc"才會被篩選出來。

如果使用IndexOf去指定不區分大小寫,但是這個只能在C#端使用
錯誤的C#語法:
var query = db.Table.Where(x => x.Name.IndexOf("abc", StringComparison.CurrentCultureIgnoreCase) >= 0).ToList();
這樣會報錯,因為LINQ在轉SQL語法的時候會發生不支援的情況,就回歸到一開始提到的Where時機。
所以我們要在取出資料後,由C#去執行篩選
C#語法:
var query = db.Table.ToList().Where(x => x.Name.IndexOf("abc", StringComparison.CurrentCultureIgnoreCase) >= 0);

PS:Contains是使用IndexOf去實做出來的,所以基本上IndexOf效能會略好於Contains



2014年5月9日 星期五

ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount(續2) -- 中繼傳遞OData參數

延續前二篇
(ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount)
(ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount(續) -- C# Model 接取$inlinecount 資料)

範例為建立一個API,把OData參數傳給資料來源Api,然後做簡單處理或直接回傳(資料來源Api開放使用OData)。

建立一個Api
//如果有$inlinecount就會是不同的格式,所以這邊回傳Object
public Object Get()
{
    return "test";
}
取得所有OData參數
List<string> arrParams = Request.GetQueryNameValuePairs() //所有QueryString的Name、Value集合
    .Where(x => x.Key.StartsWith("$")) //只取得開頭為$
    .Select(x => string.Format("&{0}={1}", x.Key, Uri.EscapeDataString(x.Value))) //組回 &Name=Value
    .ToList();
因為範例的資料來源API本身有指定參數,所以這邊會先把 & 加上,方便後面整理所有參數,請視個人情況調整加上的時機。

判斷是否有設定$inlinecount=allpages
有的話就要用C# Model 接取$inlinecount 資料的共用Model去接資料,沒有就用一般方式即可。
if(arrParams.Select(x => x.ToLower()).Contains("&$inlinecount=allpages") == true)
Select出來轉小寫以後判斷是否包含"&$inlinecount=allpages",因為沒實際測試$filter大小寫是否有區別,所以沒有在取得QueryString的時候就轉。
由組回 Name=Value 時前面有沒有加 & 來決定這邊要不要加。

組合資料來源Api的URL
string.Format("DefauleUrl?name1=val1&name2=val2{0}", string.Join("", arrParams));
string.Join要不要加 & 一樣由前面就決定了,串出來的Url請仔細確認參數部分的格式是不是正確的。

下面是實際範例
public Object Get()
{
    List<string> arrParams = Request.GetQueryNameValuePairs().Where(x => x.Key.StartsWith("$")).Select(x => string.Format("&{0}={1}", x.Key, Uri.EscapeDataString(x.Value))).ToList();
    bool IsAllpages = false;
    string strParams = string.Join("", arrParams);
 string strUrl = string.Format("DefauleUrl?name1=val1&name2=val2{0}", string.Join("", arrParams));
    List<ModelName> Models = new List<ModelName>();
    ODataByApi<ModelName> odateModel = new ODataByApi<ModelName>();
    if (arrParams.Select(x => x.ToLower()).Contains("&$inlinecount=allpages"))
    {
        odateModel = GetData<ODataByApi<ModelName>>(strParams);
        Models = odateModel.Items.ToList();
        IsAllpages = true;
    }
    else
    {
        Models = GetData<List<ModelName>>(strParams);
    }
 //資料處理
    if (Models != null)
    {
        //To do....
    }
 //有$inlinecount=allpages,丟回ODataByAp後return
    if (IsAllpages == true)
    {
        odateModel.Items = Models;
        return odateModel;
    }
 //否則return List
    return Models;
}

參考:
(ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount)
(ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount(續) -- C# Model 接取$inlinecount 資料)


2014年5月7日 星期三

ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount(續) -- C# Model 接取$inlinecount 資料

延續前一篇(ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount)

當C#去接API來的資料,且有設定$inlinecount,資料格式會變更成:
{
  "Items": [
    {
      //資料1
    },
    {
      //資料2
    },
    {
      //資料3
    }
  ],
  "NextPageLink": null,
  "Count": 3
}
Items存放資料,NextPageLink存放下一頁網址,Count存放數量

無法使用一般Model去接資料,因此需要準備一個共用Model
public class ODataByApi<T>
{
    public ICollection<T> Items { get; set; }
    public string NextPageLink { get; set; }
    public int Count { get; set; }
}

用共用Model,並指定資料Model去接資料即可
JsonConvert.DeserializeObject<ODataByApi<ModelName>>(strJson);


參考:
(ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount)

2014年5月5日 星期一

ASP .NET MVC4 WebApi -- OData 使用 與 實作$inlinecount

WebApi提供各種平台取得相關資料,為了滿足各種平台不同的需求(排序、分頁、查詢等等),最直覺的方式就是指定參數給他們使用,再處理各個參數,但這樣就變成不夠彈性,不需要查詢也要傳參數的值,同時加重了前、後端開發人員的麻煩。

使用OData讓前端傳入參數,不需要寫死程式,隨心所欲的完成排序、分頁、查詢等功能。
先來看看OData常用參數:
$top傳回前幾筆資料
$skip跳過幾筆資料
$filter
  • 查詢(where)
    eq-等於、gt-大於、lt-小於、ne-不等於
  • 串連
    and、or
  • $orderby排序
    $inlinecount傳回資料、總筆數、下一頁Url
  • allpages
  • none(預設)
  • 使用方式跟一般傳參數一樣,例如:http://{domain}/api/{controller}/{action}?$top=5&$skip=10
    (更多參數說明請參考官方網站)

    在API的部分,需要設定屬性[Queryable],就可以使用OData。
    備註:
    1.許多文章都說需要使用[Queryable]搭配回傳IQuerable、AsQueryable(),經過測試後,回傳IEnumerable也是可以正常使用的。
    2.如果不想開放所有參數、或有一些限制條件,後端人員可以在[Queryable]設定相關參數以達到目的

    這樣就可以依照各種情況由前端開發人員自行決定需要傳遞那些參數,而後端開發人員只需要專心的處理資料以確保資料正確性即可。


    在測試的時候發現$inlinecount一直沒辦法使用(微軟好像不支援一些參數),這樣前端在做分頁時就不知道資料總筆數,所以我們動手實作一個屬性吧

    public class InlineCountQueryableAttribute : QueryableAttribute
        {
            private static MethodInfo _createPageResult =
                typeof(InlineCountQueryableAttribute)
                .GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
                .Single(m => m.Name == "CreatePageResult");
    
            public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
            {
                base.OnActionExecuted(actionExecutedContext);
    
                HttpRequestMessage request = actionExecutedContext.Request;
                HttpResponseMessage response = actionExecutedContext.Response;
    
                IQueryable result;
                if (response.IsSuccessStatusCode
                    && response.TryGetContentValue<IQueryable>(out result))
                {
                    long? inlineCount = request.GetInlineCount();
                    if (inlineCount != null)
                    {
                        actionExecutedContext.Response = _createPageResult.MakeGenericMethod(result.ElementType).Invoke(
                            null, new object[] { request, request.GetInlineCount(), request.GetNextPageLink(), result }) as HttpResponseMessage;
                    }
                }
            }
    
            internal static HttpResponseMessage CreatePageResult<T>(HttpRequestMessage request, long? count, Uri nextpageLink, IEnumerable<T> results)
            {
                return request.CreateResponse(HttpStatusCode.OK, new PageResult<T>(results, nextpageLink, count));
            }
        }
    
    設定屬性由[Queryable]改為[InlineCountQueryable]即可正常使用$inlinecount參數

    參考:
    System.Web.Http.OData 命名空間
    OData官網
    關於IQueryable特性的小實驗
    Web API, OData, $inlinecount and testing


    2014/07/22 KaiYai補充:
    最近無意間發現了Bug,當程式碼出現錯誤產生Exception,Action回前端時依然會進到Attribute內執行OnActionExecuted事件,因為是Exception,所以傳進來的HttpActionExecutedContext.Response會是null,而HttpActionExecutedContext.Exception會是錯誤資訊,如果利用原本程式碼收到的錯誤訊息不會是原本實際發生錯誤的部分,而是會出現OnActionExecuted內的錯誤:
    <Error>
        <Message>發生錯誤。</Message>
        <ExceptionMessage>並未將物件參考設定為物件的執行個體。</ExceptionMessage>
        <ExceptionType>System.NullReferenceException</ExceptionType>
        <StackTrace>
            ...略...
        </StackTrace>
    </Error>
    

    但實際上的錯誤應該是
    <Error>
        <Message>發生錯誤。</Message>
        <ExceptionMessage>輸入字串格式不正確。</ExceptionMessage>
        <ExceptionType>System.FormatException</ExceptionType>
        <StackTrace>
            ...略...
        </StackTrace>
    </Error>
    

    這是因為response已經收到null值,指令沒有判斷到是否為null,就會發生會設定物件的錯誤。
    所以修正條件式加上判斷response != null
    if (response != null && response.IsSuccessStatusCode && response.TryGetContentValue<IQueryable>(out result))

    原本想要直接判斷HttpActionExecutedContext.Exception != null,但想了想,為什麼執行base.OnActionExecuted(actionExecutedContext);沒有發生錯誤呢?所以決定看一下QueryableAttribute的Code,看到下的條件以後,毅然決然的直接照辦,有錯微軟會先被罵
    public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
    {
        ...略...
        HttpResponseMessage response = actionExecutedContext.Response;
    
        if (response != null && response.IsSuccessStatusCode) 
        ...略...
    }
    


    參考:
    QueryableAttribute Source(aspnetwebstack /src/System.Web.Http.OData/QueryableAttribute.cs)

    2014年4月30日 星期三

    .Net WebSite/WebApi 限制連線IP

    開發人員最怕的就是未經授權或非正常管道使用網站/網路服務

    小弟目前專案都是透過WebApi傳遞資料,都是一關卡一關

    避免有人不小心知道使用後端服務的方法,所以後端的服務都必須限制連接IP


    首先先在Web.config(會跟IIS限制IP and DomainName同步)設定禁止所有IP連接
    <configuration>
      <system.webserver>
        <security>
          <ipsecurity allowunlisted="flase">
        </ipsecurity>
        </security>
      </system>
    </configuration>
    

    這樣就禁止了所有IP連線,接下來為了方便本機測試功能是否正常,以及允許連線的IP,所以我們要在ipSecurity裡面去設定開放條件
    <configuration>
      <system.webserver>
        <security>
          <ipsecurity allowunlisted="flase">
            <add allowed="true" ipaddress="127.0.0.1" />
            <add allowed="true" ipaddress="192.168.1.1" />
          </ipsecurity>
        </security>
      </system>
    </configuration>
    


    這時候基本上已經完成了9成

    剩下的一成就是要看IIS有沒有開啟IP 位址及網域限制(IP Address and Domain Restrictions)功能

    前幾天因為Server沒有安裝所以卡在這裡(Windows Azure 遠端桌面設定)

    沒有開啟的話,請到開啟或關閉Windows功能去設定

    以上2個動作就完成限制IP連線了


    參考:
    Web.config ipSecurity
    IP Security <ipsecurity>

    ASP.NET MVC 4 WebApi 與 Extjs 的結合 -- 換頁重新載入資料

    在 Web Form 做 Grid 換頁時,通常會先將資料把所有資料先建入 DataTable,再使用控制項接收這些資料,換頁時,就將這些資料經過處理再顯示。然後再做資料搜尋,會再去要一次資料,如此一來,在每次產生 GridView 時,都會使用同一份資料篩選,等於是網頁上有一份資料在做處理,如果資料量一大,吃的資源也大,處理與顯示時相對的就慢。

    使用 Extjs 做 Grid.Panel 分頁,一般來說會先將所有資料載到頁面,再做分頁,只是 Extjs 接收資料格式必須要在最根層加上總數量,這樣才能知道該頁顯示幾頁,總共有幾頁,所以格式必須符合如下:
    {
        "totalCount": "6679",
        "topics": [
        {
            // ....
        },
        {
            // ....
        },
        {
            // ....
        },
        {
            // ....
        },
        {
            // ....
        }
        // ...
        ]
    }
    

    而符合這種格式必須在 Web Api 段做些許調整,要多寫一個屬性讓 API 能使用,可以參考:Web API, OData, $inlinecount and testing,程式碼如下

    public class InlineCountQueryableAttribute : QueryableAttribute
    {
        private static MethodInfo _createPageResult =
            typeof(InlineCountQueryableAttribute)
            .GetMethods(BindingFlags.Static | BindingFlags.NonPublic)
            .Single(m => m.Name == "CreatePageResult");
    
        public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
        {
            base.OnActionExecuted(actionExecutedContext);
    
            HttpRequestMessage request = actionExecutedContext.Request;
            HttpResponseMessage response = actionExecutedContext.Response;
    
            IQueryable result;
            if (response.IsSuccessStatusCode
                && response.TryGetContentValue<IQueryable>(out result))
            {
                long? inlineCount = request.GetInlineCount();
                if (inlineCount != null)
                {
                    actionExecutedContext.Response = _createPageResult.MakeGenericMethod(result.ElementType).Invoke(
                        null, new object[] { request, request.GetInlineCount(), request.GetNextPageLink(), result }) as HttpResponseMessage;
                }
            }
        }
    
        internal static HttpResponseMessage CreatePageResult<T>(HttpRequestMessage request, long? count, Uri nextpageLink, IEnumerable<T> results)
        {
            return request.CreateResponse(HttpStatusCode.OK, new PageResult<T>(results, nextpageLink, count));
        }
    }
    

    在 API 加上此屬性:
    [InlineCountQueryable]
    public IEnumerable GetEmployees()
    {
        IEnumerable employees = db.Employees.ToList();
    
        foreach (Employees employee in employees)
        {
            employee.Employees1 = null;
            employee.Employees2 = null;
        }
    
        return employees;
    }
    

    執行結果如下:
    { 
        Count: 15,
        Items: [
            { ... }, 
            { ... }, 
            ...
        ],
        NextPageLink: XXX,
    }
    

    這樣輸出結果與 Extjs Grid.Panel 做分頁要接收的格式已經 95% 相似了,而換頁時通常自動會帶 page 、 start 和 limit,page 感覺只是顯示用,而 start 和 limit 的部分可以使用 API 的 Queryable 來解決,它等同於 Queryable 的 $skip 和 $top:

    首先我們先看如何在 grid.Panel 加上分頁,加上一個屬性即可,其中 store 為資料來源:
    bbar: Ext.create('Ext.PagingToolbar', {
        store: store,
        displayInfo: true,
        displayMsg: 'Displaying topics {0} - {1} of {2}',
        emptyMsg: "No topics to display",
    }),
    

    因為每一次換頁就會與 store 要一次資料,所以必須針對來源做修改:
    var store = Ext.create('Ext.data.JsonStore', {
        storeId: 'store',
        model: 'Model',
        pageSize: 3,
        
        proxy: {
            type: 'ajax',
            url: 'http://localhost:8090/api/Control?$inlinecount=allpages',
            reader: {
                type: 'json',
                root: 'Items',
                totalProperty: 'Count',
    
            },
            startParam: '$skip',
            limitParam: '$top'
        },
        autoLoad: true,
        listeners: {
            load: function (store, records, options) {
            }
        },
    
    });
    

    其中 PageSize 為每頁顯示數量,而 startParam、limitParam 則是改變它換頁帶的參數,最後執行就可以看到每換一次頁就會重新與 API 要一次資料,這樣一來頁面負擔不會太重,同時又做到速度與流量兼顧的結果。


    2014年4月7日 星期一

    ASP.NET MVC 4 Controller 使用委派 ( delegate ) 做更進階的篩選

    委派是事件的基礎。

    將委派與具名方法或匿名方法建立關聯,即可具現化 (Instantiated) 委派。 如需詳細資訊,請參閱具名方法匿名方法
    委派必須以具有相容傳回型別和輸入參數的方法或 Lambda 運算式具現化。 如需方法簽章中可允許之變異數等級的詳細資訊,請參閱委派中的變異數 (C# 和 Visual Basic)。 若要搭配匿名方法使用,就要同時宣告委派以及其相關聯的程式碼。

    以上引用:delegate (C# 參考)

    方便處在於可以在類別上宣告後,即可在各個方法函數內使用,可以依照方法不同,而產生結果也不同,但是只用同一委派 ( delegate ) 實現。

    這裡使用北風資料庫來實作委派,沒有資料庫的話可以參考:Visual Studio 2012 安裝 Northwind 資料庫並建立 Entity Framework Database First ( .edmx )

    在 Controller 中撰寫,傳入訂單編號來搜尋訂單下的訂單細項之產品的名稱,單位價格區間。

    先看看資料庫關聯表:

    先宣告委派,並且決定傳入傳出參數,關鍵字、最小價格、最大價格:
    delegate bool SearchProduct(Order_Details order_detail, string keyword, decimal mixprice, decimal minprice);
    

    類別內宣告委派:
    SearchProduct sp;
    

    最後在 Controller 實作並且使用委派:
    [HttpGet]
    public IEnumerable<Order_Details> SearchProduct(int orderid, string keyword, decimal minprice, decimal maxprice)
    {
        sp = (o, k, min, max) =>
        {
    
            return o.Products.ProductName.Contains(k) ||
                   o.Products.UnitPrice >= min &&
                   o.Products.UnitPrice <= max;
        };
    
        var Order_Details = db.Order_Details
            .Include(x => x.Products)
            .Where(x => x.Orders.OrderID == orderid)
            .ToList()
            .Where(x => sp(x, keyword, minprice, maxprice));
    
        foreach (Order_Details _order_detail in Order_Details)
        {
            _order_detail.Products.Order_Details = null;
        }
    
        return Order_Details; 
    }
    

    執行後呼叫 API - http://localhost:8090/api/Product/SearchProduct?orderid=10285&keyword=Ch&maxprice=18&minprice=17,得到結果:
    [
        {
            "OrderID": 10285,
            "ProductID": 1,
            "UnitPrice": 14.4,
            "Quantity": 45,
            "Discount": 0.2,
            "Orders": null,
            "Products": {
                "ProductID": 1,
                "ProductName": "Chai",
                "SupplierID": 1,
                "CategoryID": 1,
                "QuantityPerUnit": "10 boxes x 20 bags",
                "UnitPrice": 18,
                "UnitsInStock": 39,
                "UnitsOnOrder": 0,
                "ReorderLevel": 10,
                "Discontinued": false,
                "Categories": null,
                "Order_Details": null,
                "Suppliers": null
            }
        }
    ]
    

    為何不在 LINQ 內使用條件判斷? 其實我已經實作過了,會出現「運算式樹狀架構可能不含指派運算子」錯誤,且必須要實例化出來才可以做,以下錯誤程式碼:
    [HttpGet]
    public IEnumerable<Order_Details> SearchProduct(int orderid, string keyword, decimal minprice, decimal maxprice)
    {
    
        Products _product;
    
        var Order_Details = db.Order_Details
            .Include(x => x.Products)
            .Where(x => x.Orders.OrderID == orderid &&
                (
                    (_product = x.Products) != null &&
                    _product.ProductName.Contains(keyword) ||
                    _product.UnitPrice >= minprice ||
                    _product.UnitPrice <= maxprice
                )
    
            ).ToList();
    
        return Order_Details; 
    }
    


    2014年4月4日 星期五

    ASP.NET MVC 4 Model 中 Virtual 的作用

    在 View 端專寫時,顯示某一張表的資料,若未使用到相對應的關聯表,則不載入此表資料;若使用,則載入。這個作用就是延遲載入 ( Lazy Load ),使用再載入,很類似非同步的概念,而在 Model 關聯成員加上 Virtual 關鍵字,就可以做到延遲載入的作用。

    寫個例子來表示一下差別,資料庫使用北風資料庫利用 Database first 建置,可以參考 Visual Studio 2012 安裝 Northwind 資料庫並建立 Entity Framework Database First ( .edmx )

    接著建置 CRUD 的介面,我是使用 Products 這張資料表建置,可以參考:

    建置完成後,顯示頁面如下:

    Products Model 內容如下,建置出來就已經有 virtual :
    public partial class Products
    {
    
        public Products()
        {
            this.Order_Details = new HashSet<Order_Details>();
    
        }
    
        public int ProductID { get; set; }
        public string ProductName { get; set; }
        public Nullable<int> SupplierID { get; set; }
        public Nullable<int> CategoryID { get; set; }
        public string QuantityPerUnit { get; set; }
        public Nullable<decimal> UnitPrice { get; set; }
        public Nullable<short> UnitsInStock { get; set; }
        public Nullable<short> UnitsOnOrder { get; set; }
        public Nullable<short> ReorderLevel { get; set; }
        public bool Discontinued { get; set; }
    
        public virtual Categories Categories { get; set; }
        public virtual ICollection<Order_Details> Order_Details { get; set; }
        public virtual Suppliers Suppliers { get; set; }
    
    }
    

    在 ProductsController 內 Get 程式碼如下:
    public ActionResult Index()
    {
        var products = db.Products;
        return View(products.ToList());
    }
    

    接著將 Products Model 內的關聯成員的 virtual 關鍵字拿掉後執行畫面如下:

    看出差別了嗎? CategoryName 不會顯示出來,我們回到 View 端看一下它的程式碼:
    @Html.DisplayNameFor(model => model.Categories.CategoryName)
    

    這就是所謂的延遲載入的差異,如果將 virtual 關鍵字拿掉,就使用到此關聯成員時,就不會載入;而若有 virtual 關鍵字,則會載入。在 Controller 端其實都沒有變,而是 virtual 使它變得更靈活。

    如果真的不加也有另外的做法,就是在資料讀取時,就關聯表的資料 Include 進來:
    public ActionResult Index()
    {
        var products = db.Products.Include(x => x.Categories);
        return View(products.ToList());
    }
    


    2014年4月1日 星期二

    ASP.NET MVC 4 遇到「參數 @objname 模稜兩可或是所宣告的 @objtype (COLUMN) 有誤。」 之解決方案

    今天 MVC 在做 code first 自動移轉時遇到「參數 @objname 模稜兩可或是所宣告的 @objtype (COLUMN) 有誤。」,如下圖


    怪了,平常改動資料欄位,也不會產生錯誤,檢查了一下,我的程式碼只是將一對多關聯改成一對一 ( 以下為示意 ):
    public class A
        {
            [Key]
            [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
            public Guid AId { get; set; }
    
            public string Name { get; set; }
    
            // 修改前
            // public ICollection<B> B { get; set; }
    
            // 修改後
            public B B { get; set; }
        }
    
        public class B
        {
            [Key]
            [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
            public Guid BId { get; set; }
    
            // ...
        }
    

    後來上網搜尋了一下, EF CodeFirst: Either the parameter @objname is ambiguous or the claimed @objtype (COLUMN) is wrong 有說明這是因為欄位在 rename 時發生錯誤,這不太可能吧!我應該只是將 B 的 A_AId 轉移到 A 的 B_BId,好像沒有 rename 的動作。

    接著我將它的移轉檔案叫出來,在「套件管理器主控台」使用指令 Add-Migration Test 將檔案產出,這才發現問題:
    public partial class Test : DbMigration
    {
        public override void Up()
        {
            DropForeignKey("dbo.B", "A_AId", "dbo.A");
            DropIndex("dbo.B", new[] { "A_AId" });
            RenameColumn(table: "dbo.A", name: "A_AId", newName: "B_BId");
            AddForeignKey("dbo.A", "B_BId", "dbo.B", "BId");
            CreateIndex("dbo.A", "B_BId");
        }
        
        public override void Down()
        {
            DropIndex("dbo.A", new[] { "B_BId" });
            DropForeignKey("dbo.A", "B_BId", "dbo.B");
            RenameColumn(table: "dbo.A", name: "B_BId", newName: "A_AId");
            CreateIndex("dbo.B", "A_AId");
            AddForeignKey("dbo.B", "A_AId", "dbo.A", "AId");
        }
    }
    

    Up 在 Migration ( 轉移 ) 時,會執行,而如果發生錯誤則會使用 Down RollBack。

    問題就是發生在 Up 的第三行 - RenameColumn( ... ),在 A 資料表並無 B_BId 欄位,但居然要把 B_BId 命名成為 A_AId ? 所以這一看就是有問題,重新執行後問題依舊存在。

    所以建議先把一對多關連註解掉運行 API,再將一對一關聯加入在運行 API 後就會正常,或者你也可以就移轉檔案改動:
    public override void Up()
    {
        DropForeignKey("dbo.B", "A_AId", "dbo.A");
        DropIndex("dbo.B", new[] { "A_AId" });
        DropColumn("dbo.B", "A_AId");
    
        AddColumn("dbo.A", "B_BId", c => c.Guid());
        AddForeignKey("dbo.A", "B_BId", "dbo.B", "BId");
        CreateIndex("dbo.A", "B_BId");
    }
    

    前者解決方法會比自己修改移轉檔案來得好,因為你還是要看程式碼找出錯誤在哪裡,比較花時間。