關於Entity Framework 查詢的二三事

by vivid 31. 五月 2017 13:29

.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N170518303
出刊日期: 2017/5/31

Entity Framework提供了LINQ to Entities,以讓程式設計師利用LINQ語法查詢資料庫的內容。LINQ to Entities提供了相當多的語法來載入資料,這些語法略有差異,了解這些不同語法的差異有助於撰寫效能更佳的應用程式。這篇文章將介紹一些常用的查詢語法,並了解它們的運用。

本文延續使用《Change Tracking API - 1》一文建立的ADO.NET實體資料模型來說明Entity Framework提供的查詢語法。

列舉DbSet物件查詢資料

每當你列舉DbSet物件中的内容,Entity Framework就會送出一個查詢到資料庫,載入資料庫資料表最新的資料。參考以下範例程式碼,查詢Pubs資料庫stores資料表資料,程式碼使用到兩個foreach方法,印出DbSet中store物件的屬性,這會促使Entity Framework下兩次資料庫查詢。

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                foreach (var store in context.stores)
                {
                    Console.WriteLine(store.stor_name);
                }

                foreach (var store in context.stores)
                {
                    Console.WriteLine(store.stor_id);
                }

            }
        }
    }
}

 

你可以使用Visual Studio Diagnostic Tools來觀察程式的執行,從「Debug」->「Windows」->「Show Diagnostic Tools」,然後再按F5執行程式,執行到foreach方法,Entity Framework就送出以下查詢到資料庫

USE [pubs];

GO

SELECT
    [Extent1].[stor_id] AS [stor_id],
    [Extent1].[stor_name] AS [stor_name],
    [Extent1].[stor_address] AS [stor_address],
    [Extent1].[city] AS [city],
    [Extent1].[state] AS [state],
    [Extent1].[zip] AS [zip]
    FROM [dbo].[stores] AS [Extent1]


 

參考下圖,每一次執行到foreach列舉DbSet物件的內容時,就會攔截到一個送到資料庫的查詢事件:

clip_image002

圖 1:使用Diagnostic Tools監看程式執行。

若要考慮到執行效能,避免重複執行資料庫實體查詢的動作,可以善用LINQ提供的To開頭的方法,將查詢結果複製到記憶體。參考以下範例程式碼,叫用ToList()方法將資料複製到List<store>物件,後續便可以從此集合中找尋資料:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var storeList = context.stores.ToList();
                foreach (var store in storeList)
                {
                    Console.WriteLine(store.stor_name);
                }

                foreach (var store in storeList)
                {
                    Console.WriteLine(store.stor_id);
                }

            }
        }
    }
}

 

參考以下範例程式碼,則是使用ToArray()方法,將資料複製到store[]陣列之中,後續便可以從此陣列找尋資料:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var storeList = context.stores.ToArray();
                foreach (var store in storeList)
                {
                    Console.WriteLine(store.stor_name);
                }

                foreach (var store in storeList)
                {
                    Console.WriteLine(store.stor_id);
                }

            }
        }
    }
}

 

使用DbSet物件ToLookup()方法

因為Entity Framework GroupBy()方法回傳的是IQueryable<IGrouping<TKey, TSource>>泛型介面,IQueryable有延遲查詢(deferred execution)的特性,只要列舉此介面,就會建立並執行一個資料庫的實體查詢。以下這段程式碼,每次執行到foreach都會建立資料庫實際連線(以本例來說執行兩次),查詢資料庫資料。

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var storeGroup = context.stores.GroupBy(s => s.state);
                foreach (var group in storeGroup)
                {
                    Console.WriteLine($"Group : {group.Key}");
                    foreach (store item in group)
                    {
                        Console.WriteLine($"\t{item.stor_name}");
                    }
                }

                foreach (var group in storeGroup)
                {
                    Console.WriteLine($"Group : {group.Key}");
                    foreach (store item in group)
                    {
                        Console.WriteLine($"\t{item.stor_id}");
                    }
                }
            }
        }
    }
}

 

每次列舉IQueryable會讓Entity Framework產生以下查詢讀取資料:

USE [pubs];

GO

SELECT
    [Project2].[C1] AS [C1],
    [Project2].[state] AS [state],
    [Project2].[C2] AS [C2],
    [Project2].[stor_id] AS [stor_id],
    [Project2].[stor_name] AS [stor_name],
    [Project2].[stor_address] AS [stor_address],
    [Project2].[city] AS [city],
    [Project2].[state1] AS [state1],
    [Project2].[zip] AS [zip]
    FROM ( SELECT
        [Distinct1].[state] AS [state],
        1 AS [C1],
        [Extent2].[stor_id] AS [stor_id],
        [Extent2].[stor_name] AS [stor_name],
        [Extent2].[stor_address] AS [stor_address],
        [Extent2].[city] AS [city],
        [Extent2].[state] AS [state1],
        [Extent2].[zip] AS [zip],
        CASE WHEN ([Extent2].[stor_id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C2]
        FROM   (SELECT DISTINCT
            [Extent1].[state] AS [state]
            FROM [dbo].[stores] AS [Extent1] ) AS [Distinct1]
        LEFT OUTER JOIN [dbo].[stores] AS [Extent2] ON ([Distinct1].[state] = [Extent2].[state]) OR (([Distinct1].[state] IS NULL) AND ([Extent2].[state] IS NULL))
    )  AS [Project2]
    ORDER BY [Project2].[state] ASC, [Project2].[C2] ASC

 

為了效能,你可以改用DbSet物件ToLookup()方法,參考以下範例程式碼,只在ToLookup()這行程式執行一次實體資料庫查詢,將查詢結果放到記憶體,後續foreach語法的程式碼便可由記憶體中取得資料。

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var storeGroup = context.stores.ToLookup(s => s.state);
                foreach (var group in storeGroup)
                {
                    Console.WriteLine($"Group : {group.Key}");
                    foreach (store item in group)
                    {
                        Console.WriteLine($"\t{item.stor_name}");
                    }
                }

                foreach (var group in storeGroup)
                {
                    Console.WriteLine($"Group : {group.Key}");
                    foreach (store item in group)
                    {
                        Console.WriteLine($"\t{item.stor_id}");
                    }
                }
            }
        }
    }
}

 

使用DbSet物件Find()方法

若想要找尋一個Entity Framework 放在記憶體中的物件,可以利用DbSet物件提供的Find()方法。Find()方法可以傳入key值當做搜尋的參數,找尋並回傳相符的物件,若找不到key相符的物件,Find()方法就回傳「null」。key值對應到主鍵(Primary)欄位。若key值是一個組合鍵,Find()方法可以按「,」符號區隔,填入組成key值的屬性名稱。

不是每一次叫用Find()方法時,都會從資料庫載入資料,而是按照以下優先順序來搜尋物件:

  • · 從記憶體找,搜尋DbSet物件中是否有包含key相符的Entity物件,並回傳此Entity物件。此Entity物件的資料可以從資料庫載入,或是一個新建立、附加到DbSet屬性,但尚未儲存到資料庫的Entity物件。
  • · 從資料庫載入Entity物件,並回傳此物件。

參考以下範例程式碼,找尋是否有key值為「6380」store資料,範例中建立DbContext物件,然後叫用DbSet的Find()方法找Entity。若使用「Visual Studio Diagnostic Tools」來觀察程式的執行,第一次執行Find()方法時,會建立實體資料庫連線,從資料庫載入資料。但第二次執行Find()方法時,就不會執行資料庫查詢,而是從記憶體將DbSet中key相符的物件回傳:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {

            using (var context = new PubsContext())
            {
                store s1 = context.stores.Find("6380"); // DB Query
                Console.WriteLine(s1.stor_name);

                store s2 = context.stores.Find("6380"); // From memory
                Console.WriteLine(s1.stor_name);

            }
        }
    }
}


 

Find()方法會讓Entity Framework產生以下查詢來取回資料,使用了「Select Top (2)」語法,因為Find()方法的參數對應到資料表主鍵欄位,不應該找回重複的兩筆資料,藉由「Select Top (2)」語法可以進行資料驗證的動作:

USE [pubs];

GO

--Type and value data was not available for the following variables. Their values have been set to defaults.

DECLARE @p0 AS SQL_VARIANT;

SET @p0 = NULL;

SELECT TOP (2)

[Extent1].[stor_id] AS [stor_id],

[Extent1].[stor_name] AS [stor_name],

[Extent1].[stor_address] AS [stor_address],

[Extent1].[city] AS [city],

[Extent1].[state] AS [state],

[Extent1].[zip] AS [zip]

FROM [dbo].[stores] AS [Extent1]

WHERE [Extent1].[stor_id] = @p0

我們看另一個例子,參考以下範例程式碼, 建立一個store物件,並將它加到context.stores之中,資料庫目前並沒有key值為「9999」的資料,因此兩個Find()方法實際上是直接搜尋記憶體,將新建立的store物件回傳,並沒有執行資料庫查詢。

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {

            using (var context = new PubsContext())
            {
                store s = new store() { stor_id = "9999", stor_name = "9999 store" };
                context.stores.Add(s);
                store s1 = context.stores.Find("9999"); // From memory
                Console.WriteLine(s1.stor_name);

                store s2 = context.stores.Find("9999"); // From memory
                Console.WriteLine(s1.stor_name);

            }

        }
    }
}

 

使用DbSet物件SingleOrDefault()方法

假設每此找尋單一物件時,都要查詢資料庫的資料,則可以改用DbSet物件的Single()或SingleOrDefault()方法。Single()或SingleOrDefault()方法的差異是:Single()方法找不到條件相符的資料會產生例外錯誤;而SingleOrDefault()方法找不到條件相符的資料時不會產生例外錯誤,而是回傳「null」。

參考以下範例程式碼,叫用SingleOrDefault()方法找尋city等於「Seattle」的商店資訊,範例中叫用DbSet物件SingleOrDefault()方法兩次,使用Visual Studio Diagnostic Tools來觀察程式的執行,你將發現每次叫用時,都會執行資料庫查詢:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var q = from s in context.stores
                        where s.city == "Seattle"
                        select s;

                var r = q.SingleOrDefault(); //DB Query

                r = q.SingleOrDefault(); //DB Query

                Console.WriteLine(r.stor_name);
            }

        }
    }
}

 

SingleOrDefault()方法會讓Entity Framework產生以下SQL語法來查詢資料庫資料:

--The data may be truncated and may not represent the query that was run on the server

USE [pubs];

GO

SELECT TOP (2)

[Extent1].[stor_id] AS [stor_id],

[Extent1].[stor_name] AS [stor_name],

[Extent1].[stor_address] AS [stor_address],

[Extent1].[city] AS [city],

[Extent1].[state] AS [state],

[Extent1].[zip] AS [zip]

FROM [dbo].[stores] AS [Extent1]

WHERE 'Tapipei' = [Extent1].[city]

特別注意,Entity Framework使用了「Select Top (2)」語法來查詢資料,若查詢回傳兩筆紀錄,則SingleOrDefault()方法便會觸發System.InvalidOperationException例外錯誤,錯誤訊息為「Sequence contains more than one element」,請參考下圖所示:

clip_image004

圖 2:觸發System.InvalidOperationException例外錯誤。

只要叫用SingleOrDefault()方法,就會執行資料庫查詢,而不找尋新增到DbSet物件但尚未寫入資料庫的資料。參考以下範例程式碼,新建立一個Store物件,並將之加入DbSet物件中,但程式執行到SingleOrDefault()方法會回傳「null」,因此最後一行程式將會觸發NullReferenceException例外錯誤。

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                store s = new store() { stor_id = "9999", stor_name = "9999 store" };
                context.stores.Add(s);
                var r = context.stores.SingleOrDefault(o => o.stor_name == "9999 store"); //DB Query
                Console.WriteLine(r.stor_name); // System.NullReferenceException'
            }
        }
    }
}

 

使用DbSet物件Single()方法

DbSet物件Single()方法和SingleOrDefault()方法運作方式大致相同,不再贅述,參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var q = from s in context.stores
                        where s.city == "Seattle"
                        select s;

                var r = q.Single(); //DB Query

                r = q.Single(); //DB Query

                Console.WriteLine(r.stor_name);
            }

        }
    }
}

 

使用Single()方法將產生以下查詢:

USE [pubs];

GO

SELECT TOP (2)

[Extent1].[stor_id] AS [stor_id],

[Extent1].[stor_name] AS [stor_name],

[Extent1].[stor_address] AS [stor_address],

[Extent1].[city] AS [city],

[Extent1].[state] AS [state],

[Extent1].[zip] AS [zip]

FROM [dbo].[stores] AS [Extent1]

WHERE 'Seattle' = [Extent1].[city]

 

使用DbSet物件First()與FirstOrDefault ()方法

使用DbSet物件的Single()或SingleOrDefault()方法找詢資料時,若滿足篩選條件的資料有兩筆以上會觸發System.InvalidOperationException例外錯誤,若不在乎回傳資料的筆數,而想取得回傳資料的第一筆,可以改用DbSet物件的First()與FirstOrDefault ()方法。First ()與FirstOrDefault()方法的差異是:First ()方法找不到條件相符的資料會產生例外錯誤;而FirstOrDefault()方法找不到條件相符的資料時不會產生例外錯誤,而是回傳「null」。

只要叫用FirstOrDefault()方法,就會執行資料庫查詢,而不找尋新增到DbSet但尚未寫入資料庫的資料。參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                var q = from s in context.stores
                        where s.city == "Seattle"
                        select s;

                var r = q.FirstOrDefault(); //DB Query

                r = q.FirstOrDefault(); //DB Query

                Console.WriteLine(r.stor_name);
            }
        }
    }
}

 

使用FirstOrDefault()方法將產生以下查詢:

USE [pubs];

GO

SELECT TOP (1)

[Extent1].[stor_id] AS [stor_id],

[Extent1].[stor_name] AS [stor_name],

[Extent1].[stor_address] AS [stor_address],

[Extent1].[city] AS [city],

[Extent1].[state] AS [state],

[Extent1].[zip] AS [zip]

FROM [dbo].[stores] AS [Extent1]

WHERE 'Seattle' = [Extent1].[city]

 

查詢本機資料

DbSet物件包含一個Local屬性,記錄從資料庫查詢回來的所有資料。此外此屬性也會記錄新增到DbSet物件但尚未寫回資料庫的Entity,不過並不會記錄被標示為刪除而實際上還存在於資料庫的Entity物件。

參考以下範例程式碼,一開始context.stores.Local.Count屬性的值是「0」,新增一個Store物件到DbSet物件,則Count屬性的值是「1」;使用FirstOrDefault()方法載入一筆資料,則Count屬性的值是「2」,從DbSet物件移除一個物件,則Count屬性的值是「1」。最後利用迴圈印出Local屬性所有的物件資料:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 0
                store s = new store() { stor_id = "9999", stor_name = "9999 store" };
                context.stores.Add(s);
                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 1


                store storeToDelete = context.stores.FirstOrDefault(o => o.stor_id == "6380");
                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 2

                context.stores.Remove(storeToDelete);

                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 1

                foreach (var store in context.stores.Local)
                {
                    Console.WriteLine($" {store.stor_id} - {store.stor_name} ");
                }
            }
        }
    }
}

 

執行測試程式碼,此範例執行結果如下所示:

clip_image006

圖 3:查詢本機資料。

載入資料到本機

若想要將資料庫資料表所有資料載入到本機,只要列舉DbSet物件就會將資料庫資料載入記憶體,並且轉換成Entity物件放在Local屬性中。參考以下範例程式碼,一開始context.stores.Local.Count屬性的值是「0」,使用foreach列舉DbSet物件store屬性,context.stores.Local.Count屬性的值便變為「6」;最後一段foreach將Local屬性中的物件資料印出:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 0
                foreach (var store in context.stores)
                {
                    Console.WriteLine($" {store.stor_id} - {store.stor_name} ");
                }
                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 6
                Console.WriteLine("=============");
                Console.WriteLine("Local Data :");
                foreach (var store in context.stores.Local)
                {
                    Console.WriteLine($" {store.stor_id} - {store.stor_name} ");
                }
            }
        }
    }
}

 

執行測試程式碼,此範例執行結果如下所示:

clip_image008

圖 4:載入資料到本機。

除了使用列舉DataSet這招來載入資料之外,還有一個Load()方法可以使用`,參考以下範例程式碼:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 0
                context.stores.Load();
                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 6
                Console.WriteLine("=============");
                Console.WriteLine("Local Data :");
                foreach (var store in context.stores.Local)
                {
                    Console.WriteLine($" {store.stor_id} - {store.stor_name} ");
                }
            }
        }
    }
}

 

執行測試程式碼,此範例執行結果如下所示:

clip_image010

圖 5:使用Load()方法載入資料。

若不想一次載入資料表所有資料,Load()方法可以搭配LINQ查詢,參考以下範例程式碼,載入state為「CA」的資料:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 0

                var q = from s in context.stores
                        where s.state == "CA"
                        select s;

                q.Load();

                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 3
                Console.WriteLine("=============");
                Console.WriteLine("Local Data :");
                foreach (var store in context.stores.Local)
                {
                    Console.WriteLine($" {store.stor_id} - {store.stor_name} - {store.state}");
                }
            }
        }
    }
}

 

執行測試程式碼,此範例執行結果如下所示:

clip_image012

圖 6:使用Load()方法載入部分資料表資料。

我們可以叫用Load()方法多次,分批將資料載入,參考以下範例程式碼,第一次Load()方法載入三筆State為「CA」的資料;第二個Load()方法載入筆State為「WA」的資料,第二次叫用Load()方法載入資料時,不會清空Local屬性,新查詢出來的資料會附加到Local屬性:

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Infrastructure;
using System.Data.Entity.Validation;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace PubsDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var context = new PubsContext())
            {
                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 0

                var q = from s in context.stores
                        where s.state == "CA"
                        select s;

                q.Load();

                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 3
                q = from s in context.stores
                        where s.state == "WA"
                        select s;
                q.Load();

                Console.WriteLine($" Local Count : {context.stores.Local.Count}"); // Local Count : 5
                Console.WriteLine("=============");
                Console.WriteLine("Local Data :");
                foreach (var store in context.stores.Local)
                {
                    Console.WriteLine($" {store.stor_id} - {store.stor_name} - {store.state}");
                }
            }
        }
    }
}


執行測試程式碼,此範例執行結果如下所示:

clip_image014

圖 7:使用Load()方法批次載入資料。

Tags:

.NET Magazine國際中文電子雜誌 | Entity Framework | C# | 許薰尹Vivid Hsu

新增評論




  Country flag
biuquote
  • 評論
  • 線上預覽
Loading






NET Magazine國際中文電子雜誌

NET Magazine國際中文電子版雜誌,由恆逸資訊創立於2000,自發刊日起迄今已發行超過500篇.NET相關技術文章,擁有超過40000名註冊讀者群。NET Magazine國際中文電子版雜誌希望藉於電子雜誌與NET Developer達到共同學習與技術新知分享,歡迎每一位對.NET 技術有興趣的朋友們多多支持本雜誌,讓作者群們可以有持續性的動力繼續爬文。<請加入免費訂閱>

月分類Month List