IT技術互動交流平臺

高品質開源工具Chloe.ORM:支持存儲過程與Oracle

作者:我叫So  來源:IT165收集  發布日期:2016-12-07 20:52:26

扯淡

這是一款高質量的.NET C#數據庫訪問框架(ORM)。查詢接口借鑒 Linq。借助 lambda 表達式,可以完全用面向對象的方式就能輕松執行多表連接查詢、分組查詢、聚合查詢、插入數據、批量刪除和更新等操作。

其實,早在兩個月前,Chloe 就已經支持 Oracle 數據庫了,只是LZ個人平時用 Oracle 不多,Oracle 較其他數據庫稍微特別,因此,并沒有及時發布,生怕 bug 連連。經過好幾個月的沉淀,除了支持 Oracle 外,對框架內部代碼結構也做了不少的調整,現在,實體也支持繼承父類或接口,更加可喜可賀的是也支持了存儲過程,包括 output 參數以及 Oracle 的 RefCurcor 返回結果集。與此同時,方便對 Chloe 的感興趣的同學學習框架,官網也正式上線了。

導航

  • Chloe.ORM 事前準備 查詢數據 基本查詢 連接查詢 聚合函數 分組查詢 插入數據 更新數據 刪除數據 存儲過程 支持函數 坎坎坷坷 結語

    Chloe.ORM

    事前準備

    實體:

    public enum Gender
    {
        Man = 1,
        Woman
    }
    
    [Table('Users')]
    public class User
    {
        [Sequence('USERS_AUTOID')]
        public int Id { get; set; }
        public string Name { get; set; }
        public Gender? Gender { get; set; }
        public int? Age { get; set; }
        public int? CityId { get; set; }
        public DateTime? OpTime { get; set; }
    }
    
    public class City
    {
        [Column(IsPrimaryKey = true)]
        public int Id { get; set; }
        public string Name { get; set; }
        public int ProvinceId { get; set; }
    }
    
    public class Province
    {
        [Column(IsPrimaryKey = true)]
        public int Id { get; set; }
        public string Name { get; set; }
    }

    因為框架本身需要與具體的數據庫驅動解耦,所以 OracleContext 構造函數需要一個 IDbConnectionFactory 的參數,IDbConnectionFactory 接口只有一個 CreateConnection() 方法,必須先建個類,實現 CreateConnection 方法:

    public class OracleConnectionFactory : IDbConnectionFactory
    {
        string _connString = null;
        public OracleConnectionFactory(string connString)
        {
            this._connString = connString;
        }
        public IDbConnection CreateConnection()
        {
            OracleConnection oracleConnection = new OracleConnection(this._connString);
            OracleConnectionDecorator conn = new OracleConnectionDecorator(oracleConnection);
            return conn;
        }
    }

    由于我用的是 Oracle.ManagedDataAccess 數據庫驅動,OracleConnection 創建的 DbCommand 默認是以順序方式綁定參數,所以,上述例子使用了裝飾者模式對 OracleConnection 封裝了一遍,主要就是修改 DbCommand 參數綁定方式。OracleConnectionDecorator 的定義在官網API文檔和 Github 上的 demo 中都有,在這就不貼了,不然太占篇幅。

    創建一個 DbContext:

    string connString = 'Your connection string';
    OracleContext context = new OracleContext(new OracleConnectionFactory(connString));

    再創建一個 IQuery<T>:

    IQuery<User> q = context.Query<User>();

    查詢數據

    基本查詢

    IQuery<User> q = context.Query<User>();
    
    q.Where(a => a.Id == 1).FirstOrDefault();
    /*
     * SELECT 'USERS'.'ID' AS 'ID','USERS'.'NAME' AS 'NAME','USERS'.'GENDER' AS 'GENDER','USERS'.'AGE' AS 'AGE','USERS'.'CITYID' AS 'CITYID','USERS'.'OPTIME' AS 'OPTIME' FROM 'USERS' 'USERS' WHERE ('USERS'.'ID' = 1 AND ROWNUM < 2)
     */
    
    //可以選取指定的字段,支持返回匿名類型,也可以返回自定義類型
    q.Where(a => a.Id == 1).Select(a => new { a.Id, a.Name }).FirstOrDefault();
    /*
     * SELECT 'USERS'.'ID' AS 'ID','USERS'.'NAME' AS 'NAME' FROM 'USERS' 'USERS' WHERE ('USERS'.'ID' = 1 AND ROWNUM < 2)
     */
    
    //分頁
    q.Where(a => a.Id > 0).OrderBy(a => a.Age).TakePage(1, 20).ToList();
    /*
     * SELECT 'T'.'ID' AS 'ID','T'.'NAME' AS 'NAME','T'.'GENDER' AS 'GENDER','T'.'AGE' AS 'AGE','T'.'CITYID' AS 'CITYID','T'.'OPTIME' AS 'OPTIME' FROM (SELECT 'TTAKE'.'ID' AS 'ID','TTAKE'.'NAME' AS 'NAME','TTAKE'.'GENDER' AS 'GENDER','TTAKE'.'AGE' AS 'AGE','TTAKE'.'CITYID' AS 'CITYID','TTAKE'.'OPTIME' AS 'OPTIME',ROWNUM AS 'ROW_NUMBER_0' FROM (SELECT 'USERS'.'ID' AS 'ID','USERS'.'NAME' AS 'NAME','USERS'.'GENDER' AS 'GENDER','USERS'.'AGE' AS 'AGE','USERS'.'CITYID' AS 'CITYID','USERS'.'OPTIME' AS 'OPTIME' FROM 'USERS' 'USERS' WHERE 'USERS'.'ID' > 0 ORDER BY 'USERS'.'AGE' ASC) 'TTAKE' WHERE ROWNUM < 21) 'T' WHERE 'T'.'ROW_NUMBER_0' > 0
     */

    連接查詢

    IQuery<User> users = context.Query<User>();
    IQuery<City> cities = context.Query<City>();
    IQuery<Province> provinces = context.Query<Province>();
    
    //建立連接
    IJoiningQuery<User, City> user_city = users.InnerJoin(cities, (user, city) => user.CityId == city.Id);
    IJoiningQuery<User, City, Province> user_city_province = user_city.InnerJoin(provinces, (user, city, province) => city.ProvinceId == province.Id);
    
    //查出一個用戶及其隸屬的城市和省份的所有信息,同樣支持返回匿名類型,也可以返回自定義類型
    var view = user_city_province.Select((user, city, province) => new { User = user, City = city, Province = province }).Where(a => a.User.Id == 1).ToList();
    /*
     * SELECT 'USERS'.'ID' AS 'ID','USERS'.'NAME' AS 'NAME','USERS'.'GENDER' AS 'GENDER','USERS'.'AGE' AS 'AGE','USERS'.'CITYID' AS 'CITYID','USERS'.'OPTIME' AS 'OPTIME','CITY'.'ID' AS 'ID0','CITY'.'NAME' AS 'NAME0','CITY'.'PROVINCEID' AS 'PROVINCEID','PROVINCE'.'ID' AS 'ID1','PROVINCE'.'NAME' AS 'NAME1' FROM 'USERS' 'USERS' INNER JOIN 'CITY' 'CITY' ON 'USERS'.'CITYID' = 'CITY'.'ID' INNER JOIN 'PROVINCE' 'PROVINCE' ON 'CITY'.'PROVINCEID' = 'PROVINCE'.'ID' WHERE 'USERS'.'ID' = 1
     */
    
    //也可以只獲取指定的字段信息:UserId,UserName,CityName,ProvinceName,這時,生成的 sql 只包含指定的字段
    user_city_province.Select((user, city, province) => new { UserId = user.Id, UserName = user.Name, CityName = city.Name, ProvinceName = province.Name }).Where(a => a.UserId == 1).ToList();
    /*
     * SELECT 'USERS'.'ID' AS 'USERID','USERS'.'NAME' AS 'USERNAME','CITY'.'NAME' AS 'CITYNAME','PROVINCE'.'NAME' AS 'PROVINCENAME' FROM 'USERS' 'USERS' INNER JOIN 'CITY' 'CITY' ON 'USERS'.'CITYID' = 'CITY'.'ID' INNER JOIN 'PROVINCE' 'PROVINCE' ON 'CITY'.'PROVINCEID' = 'PROVINCE'.'ID' WHERE 'USERS'.'ID' = 1
     */

    聚合函數

    Chloe 的聚合查詢擁有和 linq 差不多的接口,基本是一看就明白。

    IQuery<User> q = context.Query<User>();
    
    q.Select(a => AggregateFunctions.Count()).First();
    /*
     * SELECT COUNT(1) AS 'C' FROM 'USERS' 'USERS' WHERE ROWNUM < 2
     */
    
    q.Select(a => new { Count = AggregateFunctions.Count(), LongCount = AggregateFunctions.LongCount(), Sum = AggregateFunctions.Sum(a.Age), Max = AggregateFunctions.Max(a.Age), Min = AggregateFunctions.Min(a.Age), Average = AggregateFunctions.Average(a.Age) }).First();
    /*
     * SELECT COUNT(1) AS 'COUNT',COUNT(1) AS 'LONGCOUNT',SUM('USERS'.'AGE') AS 'SUM',MAX('USERS'.'AGE') AS 'MAX',MIN('USERS'.'AGE') AS 'MIN',AVG('USERS'.'AGE') AS 'AVERAGE' FROM 'USERS' 'USERS' WHERE ROWNUM < 2
     */
    
    var count = q.Count();
    /*
     * SELECT COUNT(1) AS 'C' FROM 'USERS' 'USERS'
     */
    
    var longCount = q.LongCount();
    /*
     * SELECT COUNT(1) AS 'C' FROM 'USERS' 'USERS'
     */
    
    var sum = q.Sum(a => a.Age);
    /*
     * SELECT SUM('USERS'.'AGE') AS 'C' FROM 'USERS' 'USERS'
     */
    
    var max = q.Max(a => a.Age);
    /*
     * SELECT MAX('USERS'.'AGE') AS 'C' FROM 'USERS' 'USERS'
     */
    
    var min = q.Min(a => a.Age);
    /*
     * SELECT MIN('USERS'.'AGE') AS 'C' FROM 'USERS' 'USERS'
     */
    
    var avg = q.Average(a => a.Age);
    /*
     * SELECT AVG('USERS'.'AGE') AS 'C' FROM 'USERS' 'USERS'
     */

    分組查詢

    Chloe 的分組查詢功能,可以像寫 sql 一樣支持 Having 和 Select。

    IQuery<User> q = context.Query<User>();
    
    IGroupingQuery<User> g = q.Where(a => a.Id > 0).GroupBy(a => a.Age);
    
    g = g.Having(a => a.Age > 1 && AggregateFunctions.Count() > 0);
    
    g.Select(a => new { a.Age, Count = AggregateFunctions.Count(), Sum = AggregateFunctions.Sum(a.Age), Max = AggregateFunctions.Max(a.Age), Min = AggregateFunctions.Min(a.Age), Avg = AggregateFunctions.Average(a.Age) }).ToList();
    /*
     * SELECT 'USERS'.'AGE' AS 'AGE',COUNT(1) AS 'COUNT',SUM('USERS'.'AGE') AS 'SUM',MAX('USERS'.'AGE') AS 'MAX',MIN('USERS'.'AGE') AS 'MIN',AVG('USERS'.'AGE') AS 'AVG' FROM 'USERS' 'USERS' WHERE 'USERS'.'ID' > 0 GROUP BY 'USERS'.'AGE' HAVING ('USERS'.'AGE' > 1 AND COUNT(1) > 0)
     */

    插入數據

    方式1

    以 lambda 表達式樹的方式插入:

    此種方式插入的好處是,可以指定列插入,就像寫 sql 一樣簡單。
    同時,該方式插入返回表主鍵值。如果實體主鍵是自增列(序列),返回值就會是自增值。

    /* User 實體打了序列標簽,會自動獲取序列值。返回主鍵 Id */
    int id = (int)context.Insert<User>(() => new User() { Name = 'lu', Age = 18, Gender = Gender.Man, CityId = 1, OpTime = DateTime.Now });
    /*
     * SELECT 'USERS_AUTOID'.'NEXTVAL' FROM 'DUAL'
     * Int32 :P_0 = 14;
       INSERT INTO 'USERS'('NAME','AGE','GENDER','CITYID','OPTIME','ID') VALUES(N'lu',18,1,1,SYSTIMESTAMP,:P_0)
     */

    方式2

    以實體的方式插入:

    該方式插入,如果一個實體存在自增列,會自動將自增列設置到相應的屬性上。

    User user = new User();
    user.Name = 'lu';
    user.Age = 18;
    user.Gender = Gender.Man;
    user.CityId = 1;
    user.OpTime = DateTime.Now;
    
    //會自動將自增 Id 設置到 user 的 Id 屬性上
    user = context.Insert(user);
    /*
     * SELECT 'USERS_AUTOID'.'NEXTVAL' FROM 'DUAL'
     * Int32 :P_0 = 15;
       String :P_1 = 'lu';
       Int32 :P_2 = 1;
       Int32 :P_3 = 18;
       DateTime :P_4 = '2016/9/5 9:16:59';
       INSERT INTO 'USERS'('ID','NAME','GENDER','AGE','CITYID','OPTIME') VALUES(:P_0,:P_1,:P_2,:P_3,:P_2,:P_4)
     */

    更新數據

    方式1

    以 lambda 表達式樹的方式更新:

    該方式解決的問題是:1.指定列更新;2.批量更新;3.支持類似 Age=Age + 100 這樣更新字段。

    context.Update<User>(a => a.Id == 1, a => new User() { Name = a.Name, Age = a.Age + 100, Gender = Gender.Man, OpTime = DateTime.Now });
    /*
     * UPDATE 'USERS' SET 'NAME'='USERS'.'NAME','AGE'=('USERS'.'AGE' + 100),'GENDER'=1,'OPTIME'=SYSTIMESTAMP WHERE 'USERS'.'ID' = 1
     */
    
    //批量更新
    //給所有女性年輕 10 歲
    context.Update<User>(a => a.Gender == Gender.Woman, a => new User() { Age = a.Age - 10, OpTime = DateTime.Now });
    /*
     * UPDATE 'USERS' SET 'AGE'=('USERS'.'AGE' - 10),'OPTIME'=SYSTIMESTAMP WHERE 'USERS'.'GENDER' = 2
     */

    方式2

    以實體的方式更新:

    User user = new User();
    user.Id = 1;
    user.Name = 'lu';
    user.Age = 28;
    user.Gender = Gender.Man;
    user.OpTime = DateTime.Now;
    
    context.Update(user); //會更新所有映射的字段
    /*
     * String :P_0 = 'lu';
       Int32 :P_1 = 1;
       Int32 :P_2 = 28;
       Nullable<Int32> :P_3 = NULL;
       DateTime :P_4 = '2016/9/5 9:20:07';
       UPDATE 'USERS' SET 'NAME'=:P_0,'GENDER'=:P_1,'AGE'=:P_2,'CITYID'=:P_3,'OPTIME'=:P_4 WHERE 'USERS'.'ID' = :P_1
     */
    
    
    /*
     * 支持只更新屬性值已變的屬性
     */
    
    context.TrackEntity(user);//在上下文中跟蹤實體
    user.Name = user.Name + '1';
    context.Update(user);//這時只會更新被修改的字段
    /*
     * String :P_0 = 'lu1';
       Int32 :P_1 = 1;
       UPDATE 'USERS' SET 'NAME'=:P_0 WHERE 'USERS'.'ID' = :P_1
     */

    刪除數據

    方式1

    以 lambda 表達式樹的方式刪除:

    context.Delete<User>(a => a.Id == 1);
    /*
     * DELETE FROM 'USERS' WHERE 'USERS'.'ID' = 1
     */
    
    //批量刪除
    //刪除所有不男不女的用戶
    context.Delete<User>(a => a.Gender == null);
    /*
     * DELETE FROM 'USERS' WHERE 'USERS'.'GENDER' IS NULL
     */

    方式2

    以實體的方式刪除:

    User user = new User();
    user.Id = 1;
    context.Delete(user);
    /*
     * Int32 :P_0 = 1;
       DELETE FROM 'USERS' WHERE 'USERS'.'ID' = :P_0
     */

    存儲過程

    通過存儲過程獲取一個 User 信息:

    Oracle 數據庫中,如果一個存儲過程需要返回結果集,需要借助 RefCursor output 參數特性。用法如下:

    /* 必須先自定義 RefCursor 參數 */
    OracleParameter p_cur = new OracleParameter();
    p_cur.ParameterName = 'p_cur';
    p_cur.OracleDbType = OracleDbType.RefCursor;
    p_cur.Direction = ParameterDirection.Output;
    
    DbParam refCursorParam = new DbParam();
    /* 將自定義 RefCursor 參數設置到 DbParam 的 ExplicitParameter 屬性 */
    refCursorParam.ExplicitParameter = p_cur;
    
    DbParam id = new DbParam('id', 1);
    User user = context.SqlQuery<User>('Proc_GetUser', CommandType.StoredProcedure, id,refCursorParam).FirstOrDefault();

    通過存儲過程的 output 參數獲取一個用戶的 name:

    DbParam id = new DbParam('id', 1);
    DbParam outputName = new DbParam('name', null, typeof(string)) { Direction = ParamDirection.Output };
    context.Session.ExecuteNonQuery('Proc_GetUserName', CommandType.StoredProcedure, id, outputName);

    支持函數

    IQuery<User> q = context.Query<User>();
    
    var space = new char[] { ' ' };
    
    DateTime startTime = DateTime.Now;
    DateTime endTime = startTime.AddDays(1);
    var ret = q.Select(a => new
         {
             Id = a.Id,
    
             String_Length = (int?)a.Name.Length,//LENGTH('USERS'.'NAME')
             Substring = a.Name.Substring(0),//SUBSTR('USERS'.'NAME',0 + 1,LENGTH('USERS'.'NAME'))
             Substring1 = a.Name.Substring(1),//SUBSTR('USERS'.'NAME',1 + 1,LENGTH('USERS'.'NAME'))
             Substring1_2 = a.Name.Substring(1, 2),//SUBSTR('USERS'.'NAME',1 + 1,2)
             ToLower = a.Name.ToLower(),//LOWER('USERS'.'NAME')
             ToUpper = a.Name.ToUpper(),//UPPER('USERS'.'NAME')
             IsNullOrEmpty = string.IsNullOrEmpty(a.Name),//too long
             Contains = (bool?)a.Name.Contains('s'),//
             Trim = a.Name.Trim(),//TRIM('USERS'.'NAME')
             TrimStart = a.Name.TrimStart(space),//LTRIM('USERS'.'NAME')
             TrimEnd = a.Name.TrimEnd(space),//RTRIM('USERS'.'NAME')
             StartsWith = (bool?)a.Name.StartsWith('s'),//
             EndsWith = (bool?)a.Name.EndsWith('s'),//
    
             /* oracle is not supported DbFunctions.Diffxx. */
             //DiffYears = DbFunctions.DiffYears(startTime, endTime),//
             //DiffMonths = DbFunctions.DiffMonths(startTime, endTime),//
             //DiffDays = DbFunctions.DiffDays(startTime, endTime),//
             //DiffHours = DbFunctions.DiffHours(startTime, endTime),//
             //DiffMinutes = DbFunctions.DiffMinutes(startTime, endTime),//
             //DiffSeconds = DbFunctions.DiffSeconds(startTime, endTime),//
             //DiffMilliseconds = DbFunctions.DiffMilliseconds(startTime, endTime),//
             //DiffMicroseconds = DbFunctions.DiffMicroseconds(startTime, endTime),//
    
             /* ((CAST(:P_0 AS DATE)-CAST(:P_1 AS DATE)) * 86400000 + CAST(TO_CHAR(CAST(:P_0 AS TIMESTAMP),'ff3') AS NUMBER) - CAST(TO_CHAR(CAST(:P_1 AS TIMESTAMP),'ff3') AS NUMBER)) / 86400000 */
             SubtractTotalDays = endTime.Subtract(startTime).TotalDays,//
             SubtractTotalHours = endTime.Subtract(startTime).TotalHours,//...
             SubtractTotalMinutes = endTime.Subtract(startTime).TotalMinutes,//...
             SubtractTotalSeconds = endTime.Subtract(startTime).TotalSeconds,//...
             SubtractTotalMilliseconds = endTime.Subtract(startTime).TotalMilliseconds,//...
    
             AddYears = startTime.AddYears(1),//ADD_MONTHS(:P_0,12 * 1)
             AddMonths = startTime.AddMonths(1),//ADD_MONTHS(:P_0,1)
             AddDays = startTime.AddDays(1),//(:P_0 + 1)
             AddHours = startTime.AddHours(1),//(:P_0 + NUMTODSINTERVAL(1,'HOUR'))
             AddMinutes = startTime.AddMinutes(2),//(:P_0 + NUMTODSINTERVAL(2,'MINUTE'))
             AddSeconds = startTime.AddSeconds(120),//(:P_0 + NUMTODSINTERVAL(120,'SECOND'))
             //AddMilliseconds = startTime.AddMilliseconds(20000),//不支持
    
             Now = DateTime.Now,//SYSTIMESTAMP
             UtcNow = DateTime.UtcNow,//SYS_EXTRACT_UTC(SYSTIMESTAMP)
             Today = DateTime.Today,//TRUNC(SYSDATE,'DD')
             Date = DateTime.Now.Date,//TRUNC(SYSTIMESTAMP,'DD')
             Year = DateTime.Now.Year,//CAST(TO_CHAR(SYSTIMESTAMP,'yyyy') AS NUMBER)
             Month = DateTime.Now.Month,//CAST(TO_CHAR(SYSTIMESTAMP,'mm') AS NUMBER)
             Day = DateTime.Now.Day,//CAST(TO_CHAR(SYSTIMESTAMP,'dd') AS NUMBER)
             Hour = DateTime.Now.Hour,//CAST(TO_CHAR(SYSTIMESTAMP,'hh44') AS NUMBER)
             Minute = DateTime.Now.Minute,//CAST(TO_CHAR(SYSTIMESTAMP,'mi') AS NUMBER)
             Second = DateTime.Now.Second,//CAST(TO_CHAR(SYSTIMESTAMP,'ss') AS NUMBER)
             Millisecond = DateTime.Now.Millisecond,//CAST(TO_CHAR(SYSTIMESTAMP,'ff3') AS NUMBER)
             DayOfWeek = DateTime.Now.DayOfWeek,//(CAST(TO_CHAR(SYSTIMESTAMP,'D') AS NUMBER) - 1)
    
             Int_Parse = int.Parse('1'),//CAST(N'1' AS NUMBER)
             Int16_Parse = Int16.Parse('11'),//CAST(N'11' AS NUMBER)
             Long_Parse = long.Parse('2'),//CAST(N'2' AS NUMBER)
             Double_Parse = double.Parse('3'),//CAST(N'3' AS BINARY_DOUBLE)
             Float_Parse = float.Parse('4'),//CAST(N'4' AS BINARY_FLOAT)
             Decimal_Parse = decimal.Parse('5'),//CAST(N'5' AS NUMBER)
             //Guid_Parse = Guid.Parse('D544BC4C-739E-4CD3-A3D3-7BF803FCE179'),//不支持
    
             Bool_Parse = bool.Parse('1'),//
             DateTime_Parse = DateTime.Parse('1992-1-16'),//TO_TIMESTAMP(N'1992-1-16','yyyy-mm-dd hh44:mi:ssxff')
    
             B = a.Age == null ? false : a.Age > 1,
         }).ToList();

    坎坎坷坷

    支持 Oracle,一開始我是拒(畏)絕(懼)的,這貨太奇葩了- -。后來想想,反正遲早都得要支持,干脆把它給干了吧,免得“夜長夢多”!不過 Oracle 是真奇葩,煩!比如,Oracle 不能直接在存儲過程里直接執行 Select sql 返回結果集,必須得依賴它那個神馬 RefCurcor 參數,這個我真的萬萬沒想到,后來一位園友提醒了才留意這個特性! 再一個,Oracle 不支持 bool 類型,Oracle.ManagedDataAccess 這個驅動的 DataReader 也不支持 GetBoolean 方法,同時 Oracle.ManagedDataAccess 創建的 DbCommand 默認是是以順序方式綁定參數,因此,又不得不對 DataReader 和 DbCommand 包裝一遍才能用。如果真的要細數起來,Oracle 的糟點連起來估計能繞地球一圈!

    結語

    把 Oracle 給支持了,心中的石頭也終于落下,生活輕松了許多。作為眾多 ORM 中為數不多能支持 Oracle 的一枚成員,感興趣的可以關注一波。或許,Chloe 真能給你帶來不一樣的感覺!更多詳細用法敬請參照官網API文檔。

    技術教程或心得我倒不是很擅長寫,我只想把日常開發的一些干貨分享給大家,您的推薦是我分享的最大動力。如果覺得 Chloe 這個開源項目不錯,望大家給個贊,也可以上 Github 關注或收藏(star)一下,以便能及時收到更新通知。同時,Chloe 官網以及基于 NFine 改造的后臺后續也會放出,有期待的同學可以點個關注,也歡迎廣大C#同胞入群交流,暢談.NET復興大計。最后,感謝大家閱讀至此!

    Chloe.ORM 完全開源,遵循 Apache2.0 協議,托管于 GitHub,地址:https://github.com/shuxinqin/Chloe。

    官網:http://www.52chloe.com
    官網后臺:http://www.52chloe.com:82

Tag標簽: 高品質   過程   工具  
  • 專題推薦

About IT165 - 廣告服務 - 隱私聲明 - 版權申明 - 免責條款 - 網站地圖 - 網友投稿 - 聯系方式
本站內容來自于互聯網,僅供用于網絡技術學習,學習中請遵循相關法律法規
香港最快开奖现场直播结果