EF6 下的 int.Parse

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的模式。