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欄位 (續)

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

在Sql建立Decimal的時候,習慣性會設定大小,但是在使用Entity Framework Code First的時候卻發現2個問題:
1.沒辦法改變大小(預設是18,2),可能欄位太大或是小數位數不夠
2.沒辦法檢查傳入的資料有沒有在限制的範圍內,可能會發生輸入不符合規則

有人會使用float或double,然後配合[RangeAttribute]去限制大小,以及[RegularExpressionAttribute]來檢驗格式;這或許解決了資料驗證的問題,但是總不可能每個欄位都去下這些屬性,對資料庫欄位大小問題也一樣沒有解決。
首先先來解決資料庫decimal大小問題。
網路上找的最快的方式(Set decimal(16, 3) for a column in Code First Approach in EF4.3)
public class MyContext : DbContext
{
  public DbSet<myclass> MyClass;
  protected override void OnModelCreating(DbModelBuilder modelBuilder)
  {
    modelBuilder.Entity<myclass>().Property(x => x.Sum).HasPrecision(5, 1);
    modelBuilder.Entity<myclass>().Property(x => x.Price).HasPrecision(3, 1);
    base.OnModelCreating(modelBuilder);
  }
}
解決了資料庫大小問題


測試下去就會發現,輸入多少都可以過,只是到資料庫會被截掉,要再加個驗證讓ModelState.IsValid可以為我們擋掉。
先建立一個DecimalAttribute繼承ValidationAttribute
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Parameter, AllowMultiple = false)]
public class DecimalAttribute : ValidationAttribute
{
  public int Precision = 18;
  public int Scale = 2;
  public DecimalAttribute(int Precision = 18, int Scale = 2)
  {
    this.Precision = Precision;
    this.Scale = Scale;
  }
  public override bool IsValid(object value)
  {
    if (value == null)
    {
      return true;
    }
    try
    {
      decimal valueAsDecimal = Convert.ToDecimal(value);
      SqlDecimal sqlDecimal = SqlDecimal.ConvertToPrecScale(new SqlDecimal(valueAsDecimal), Precision, Scale);
    }
    catch (Exception ex)
    {
      if (string.IsNullOrEmpty(base.ErrorMessage) == true)
      {
        base.ErrorMessage = ex.Message;
      }
      return false;
    }
    return true;
  }
}
傳入資料測試驗證機制正常運行

延伸閱讀:
Entity Framework Code First依屬性(Attribute)設定SQL欄位
Entity Framework Code First依屬性(Attribute)設定SQL欄位 (續)