Entity Framework是一個相當易用的ORM Framework,但偶而卻無法滿足最簡單的需求,原因是其有其設計的主軸,不該出現的設計所產生的需求,通常不在其考慮的範圍內,在顧問及授課生涯中,常常出現一些需要滿足,卻又不符合常態的需求,例如本文提及的string to int就是常見的問題。
簡單的說,由於資料庫規劃的錯誤也好、特規也好,把一個本來是int的欄位設計成string了,但卻又要在特定情況將其轉成int,在SQL世界中,CONVERT、CAST很簡單可以解決這種詭異需求,當然,是在不考慮其對資料庫的影響情況下。但在Entity Framework中就沒那麼簡單了,直覺上會使用int.Parse,如下所示。
static void Main(string[] args)
{
var context = new TestPerfDBEntities1();
var result = context.TEST_INT.OrderByDescending(a => int.Parse(a.CLevel));
foreach (var item in result)
Console.WriteLine(item.CLevel);
Console.ReadLine();
}
這個程式會很無情的丟出以下錯誤。
也就是說,int.Parse並不在LINQ To Entites的支援範圍內,其實也合理,因為本來就不該這樣設計的。但需求擺在眼前,最快的解法就是走回Raw SQL路。
var result = context.Database.SqlQuery<TEST_INT>("SELECT * FROM TEST_INT Order By Cast(CLevel as int) DESC");
缺點就是走回Raw SQL,跨資料庫以後會變得麻煩些,但以Cast、Convert的普及程度,這倒也還好,充其量就是礙眼而以。
另一種手段可以不那麼礙眼,在使用EDMX的情況下透過修改.EDMX的對應來達到同樣效果,這種方法必須開啟.EDMX,加入Function區段的定義。
<edmx:ConceptualModels>
<Schema Namespace="TestPerfDBModel" Alias="Self" annotation:UseStrongSpatialTypes="false" xmlns:annotation="http://schemas.microsoft.com/ado/2009/02/edm/annotation" xmlns:customannotation="http://schemas.microsoft.com/ado/2013/11/edm/customannotation" xmlns="http://schemas.microsoft.com/ado/2009/11/edm">
……………………….
<Function Name="ParseInt" ReturnType="Edm.Int32">
<Parameter Name="stringvalue" Type="Edm.String" />
<DefiningExpression>
cast(stringvalue as Edm.Int32)
</DefiningExpression>
</Function>
……………………….
</Schema>
</edmx:ConceptualModels>
接著在Model中加入ParseInt函式。
partial class TestPerfDBEntities1
{
[EdmFunction("TestPerfDBModel", "ParseInt")]
public static int ParseInt(string stringvalue)
{
return System.Int32.Parse(stringvalue);
}
}
然後就可以直接使用了。
static void Main(string[] args)
{
var context = new TestPerfDBEntities1();
context.Database.Log = s => Console.WriteLine(s);
var result = context.TEST_INT.OrderByDescending(a => TestPerfDBEntities1.ParseInt(a.CLevel));
foreach (var item in result)
Console.WriteLine(item.CLevel);
Console.ReadLine();
}
但這招有缺點,就是只能用在EDMX模式,如果是Code First,根本沒.EDMX給你改。
架構上,Code First還是架構在原本的EDMX概念上,只是Mapping 的部分改成即時產生而已,所以理論上EDMX能做到的,Code First也能做到,只是資訊可能沒有像EDMX那麼的豐富。要在Code First達到同樣效果,必須添加以下的程式碼。
public class MyFunctionConvertions : IConceptualModelConvention<EdmModel>
{
public void Apply(EdmModel item, DbModel model)
{
var functionParseInt = new EdmFunctionPayload()
{
CommandText = $"CAST(str AS {PrimitiveType.GetEdmPrimitiveType(PrimitiveTypeKind.Int32)})",
Parameters = new[] {
FunctionParameter.Create("str", PrimitiveType.GetEdmPrimitiveType(PrimitiveTypeKind.String), ParameterMode.In),
},
ReturnParameters = new[] {
FunctionParameter.Create("ReturnValue", PrimitiveType.GetEdmPrimitiveType(PrimitiveTypeKind.Int32), ParameterMode.ReturnValue),
},
IsComposable = true
};
var function = EdmFunction.Create("ParseInt", model.ConceptualModel.EntityTypes.First().NamespaceName, DataSpace.CSpace, functionParseInt, null);
model.ConceptualModel.AddItem(function);
}
}
public partial class TEST_INT
{
public int Id { get; set; }
public string CLevel { get; set; }
}
public partial class Model1 : DbContext
{
public Model1()
: base("name=Model1")
{
}
public virtual DbSet<TEST_INT> TEST_INT { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.Conventions.Add(new MyFunctionConvertions());
}
[DbFunction("ConsoleApp47", "ParseInt")]
public static int ParseInt(string value)
{
throw new NotImplementedException();
}
}
接著就可以使用了。
static void Main(string[] args)
{
var context = new Model1();
context.Database.Log = s => Console.WriteLine(s);
var result = context.TEST_INT.OrderByDescending(a => Model1.ParseInt(a.CLevel));
foreach (var item in result)
Console.WriteLine(item.CLevel);
Console.ReadLine();
}
說真的,我比較喜歡Raw SQL的模式。