1. EF#1. 要學好 Entity Framework? 請先學好 OOP 跟 C# ...

    這次為了能順利的學好 Entity Framework,花了不少工夫在研究它的作法。不過有一大半不是在 Entity Framework 本身,而是在 C# 的一些特別的語法跟 LINQ 身上...。也因為這樣,我深切的體認到一個 ORM 技術能不能成功,其實都是在 Hosting 這個 Framework 的環境夠不夠成熟...。

    不過在摸索的過程中,找到的資訊都是片斷的,每一篇都是講實作,範例程式,操作步驟... 等等,而當時我最需要的反而是幫助我決定,Entity Framework 到底值不值得我押在上面投資五年開發計劃使用的 ORM 技術? 它跟 NHibernate (考慮中的另一項 ORM framework) 的優缺點為何? 未來發展的優缺點又是什麼? 架構上的差異在那? 另外 Linq to SQL 呢? 這些較偏架構性跟本質的討論及比較資訊,反而少之又少...。

    雖然最後還是研究了些心得出來,不過實在是不想寫那些到處都看的到的實作,就來寫點不一樣的吧。第一篇會先寫寫 ORM 的背景知識,還有 Entity Framework 跟 C# 的語法是如何魚幫水,水幫魚,如何解決了過去 ORM 用起來都不大對勁的問題...。

     

    繼續長篇大論前,先老王賣瓜一下。雖然我碰過的 ORM framework 不多,不過相關的理論跟技術則碰了不少。撇開大學就在研究的 OOP 不談,研究所的指導教授就從 SmallTalk 開使教... 兩年的專題研究都是 OODB (物件導向資料庫),相關論文也看了一堆。出來工作後又有幸用了幾年的 TAMINO (一套 native xml database), 之後又花了很多時間,在 SQL 2000 上面建立起一套 Object <-> XML <-> Database,類似 ORM 的 Framework ...

    不過這麼一路下來,都沒有覺的簡單又可行的方案。除了上面講的是我親自參與過的之外, Microsoft 其實也發表過幾個類似的技術,像是 Typed DataSet 就是個較接近的產物。 Typed DataSet 其實有點接近現在的 Entity Framework 了。DataSet 就等同於 Entity Container / ObjectContext, DataTable 大致就等同於 EntitySet, 而 DataRow 則等同於 Entity, Relation 則大致等同於 Entity Framework 的 Navigation Property.... 不過用起來還是到處都看的到 DataSet 的影子,感覺血統還不夠純正...

    不過現在的 Entity Framework 不一樣了,感覺就已經往實用的領域邁進了一大步! 並不是說 Entity Framework 做的很好 (以成熟度來說, NHibernate 比目前的 Entity Framework 好的多), 而是跟 Entity Framework 搭配的技術都成熟了。一套 ORM 要成功,必要的條件很多啊... 實作上的角度看來,我覺的重要的有這幾項:

    1. 要有優良的 Object / Relationship Mapping 機制、作法、工具等等
    2. Framework 本身的擴充及自訂的能力要夠
    3. 要有效的解決以物件角度思考的查詢 (QUERY) 問題
    4. "物件" 要看起來像 "物件",不是 "資料"
    5. 處理資料庫典型的問題,效能跟便利性不能跟直接操作 DB 差太多
    6. 你的牌子夠不夠響亮... (這是心理因素而已... 哈哈)

    這些是深切的體認。不然的話 ORM 的東西跟本不難啊,以功能來說,Typed DataSet 其實就解決一大半實際的問題了。先來看看物件導向幾個關鍵的核心技術是啥?

    1. 封裝 ( Encapsulation ),抽像化型別 (ADT, Abstract Data Type),資料 (Data) 跟行為 (Behavior) 能夠綁在一起
    2. 繼承 ( Inheritance )
    3. 多型 ( Polymorphism )

    以這樣 "物件" 的關點來看,Microsoft 在 Entity Framework 之前的資料庫技術其實都不合格。先來看看資料庫存取技術,如果能搭配這些物件技術,能有什麼樣的改進?

    [封裝]
    這就沒啥好講的了。物件技術有很好的封裝機制,public / protected / private 等等 scope modifier 就能提供很棒的封裝機制。不過資料庫很難做的好,資料庫的那套頂多叫作安全機制 (security) 或是授權機制 (authorization), 不是封裝 (encapsulation) ... 真正的封裝不是看你是那個帳號決定能不能讀資料? 而是你是那個 SCOPE 的程式,能不能存取封裝起來的內部資訊。DBMS 對於資料的控制能力很有限,不外呼 Key, Constraint, Relationship / Foreign Key 等等。像加解密,正規運算式 (regular expression) 等等,對 DBMS 來說就太複雜了。更複雜的封裝機制單靠 DB 就很不實際... 無奈在沒有 ORM 的前題下,這些問題則是直接曝露在你的程式碼每個地方...。換句話說,如果 ORM 能提供良好的封裝機制,ORM 就能取代掉目前的 Data Access Layer ,成為 APP 存取 DBMS 的主要 API 。

    順便吐個苦水,也因為 DBMS 對於資料的控制能力有限,維護的 APP 總是碰到這種問題,就是錯誤的資料總是有辦法鑽進資料庫裡面。不為什麼,只因為 DBMS 本身擋不下來,而 Data Access Layer 又不夠爭氣到足以扛下這重責大任,最後只能靠 APP 自身的開發人員,靠紀律跟自律,還有良心來作好這層把關的動作... -_- 如果有套像樣的 ORM 能夠卡在這個位置,光是資料內容的把關,就是一大進步了。

     

    [繼承]
    繼承跟資料庫有什麼關係? 其實 ORM 如果能有效的把繼承的功能跟資料庫整合起來,也是很嚇人的。舉例來說,部落格支援文章,相片等等不同的內容,但是它們都要有一致的抽像行為,如新的內容要能夠訂閱 (rss subscription),要能夠有標簽 (tagging) 等等共通的功能,在物件技術我們會很直覺的用繼承來做到。定義 BlogContent 類別,把這些邏輯擺上去。之後再分別衍生出 ArticleContent / PhotoContent 等類別,把差異的實作補上去就完成了。不過同樣的概念別想直接套用在資料庫上,你的腦袋得負責這兩者之間的對應。

    懂的這麼多的工程師很難找啊... 去那裡找這種人來寫 APP ? 其實搞懂這些也不難,C++ / C# 在解決這類問題,只是很簡單的利用到 virtual table 就搞定了。換到 DBMS,就把 virtual table 的資料結構套到 database schema 就可以。不過說來簡單,能夠搞懂這些,還能精確的實作出來的人不多... 真的作出來還會被嫌:

    "它ㄨ的,誰設計的 table schema ? 亂切一通害我 T-SQL query 這麼難寫..."

    嗯,沒事,藉機吐吐苦水。主要要表達的就只有一個,繼承關係要對應到資料庫上面,也是挺麻煩的一件事。Entity Framework / NHibernate 就都提供了三種對應的方法。這三種切割對應的方式,要選那一種? 這又是門學問... Orz, 以後再說。

     

    [多型]
    這個就更玄了。多型是建立在繼承的基礎上,不管你是什麼類別的 instance, 多型的機制可以在父類別的角度,對所有各種衍生類別的物件,一視同仁的操作。而在這統一的前題下,每種類別的物件又可以一國兩制的各自為政... (咳,這不是政治版...)。這樣的抽像程度就是資料庫遠遠所不及的。延序前面講的部落格內容的例子,你能想像這個 store procedure 該怎麼寫嗎?

    "要寫一個 sp_update_blogcontent 的 store procedure, 如果 ID 指向的是 blog, 則要執行更新 HTML 的 code,如果是 photo, 則要更新存放圖檔的 BLOB 欄位..."

    天那,在 DB 這個層次,寫這種 CODE 只能用很醜的 IF  ELSE 一層一層堆起來...,跟物件技術比起來,程式碼的描述及抽像化能力實在差太遠,在這層次能解決的問題複雜度很有限...。你如果是個聰明人,最好還是別在 DBMS 搞這些物件技術,會死人的...。比較好的作法是移到 APP 那層去作比較實際。

    不錯,ORM 存在的原因又多了一個...。

     

    所以,再回頭來看看,ORM真的要發揮它的效益的話,絕對不是只有用 "物件" 來代替 "資料" 而已 (還是老話一句,這樣的話用 Typed DataSet 就夠了)。至少對應出來的 "物件",還能有效的應用到這些物件導向的特性,同時 ORM framework 還能替你維持這些跟資料庫的對應關係,這樣 ORM 技術才能真正發揮它的效益,那些被講到爛的三層式架構才不會在 DBMS 這層就破功了。

    來看看比較具體的部份。這些物件技術的特點,C# 早在 2000 年,JAVA 早在 199X 年就有了,沒什麼了不起。不過當年的 ORM 實在難用的很。當時的 OOPL 就是缺了些東西,ORM 的程式寫起來限制一堆... 對應到資料庫的物件,用起來就是跟一般的物件差很多,這也不能用,那也不能用。

    現在的 C# 就不一樣了,進化到足以解決這些語言的限制。來看看:

    1. reflection, attribute:
      這個讓物件 (Entity) 可以寫的更簡潔,reflection + attribute 可以解決很多過去得繞一大圈才做的到的事。如 class 對應到那個 table, property 對應到那個 column 等等。
    2. partial class:
      ORM 免不了有些 code generator 的搭配。有 partial class 可以讓程式搭配 code generator 更容易一點。
    3. extension methods:
      現有的物件技術很難讓你在現有的 class 上去作擴充,而 extension methods 可以。
    4. Linq
      這可是一大進步。過去 ORM 的目的就是想把 DB 的細節藏起來,無奈碰到 QUERY 的話什麼都藏不住,往往淪落到要嘛都直接用 SQL, 不然就是只剩幾種基本的 API 可以用,無法完全取代直接用 SQL 的查詢。不過 Linq 出現之後這情況就改觀了,雖然像報表那樣複雜大型的 QUERY 還是得直接下 SQL,不過一般 AP 內的查詢都可以直接用 Linq 搞定了

    其它當然還有別的,不過我自己覺的這幾點是關鍵,至少可以讓現在的 Entity Framework 在使用 Entity 時,不會再跟一般的物件有什麼不同。大部份你可以應用在一般物件的技巧,也都能套用在 Entity 身上。第一篇碎碎唸的部份就先寫到這裡。後面會示範一下幾種打造你專屬的 Entity 用到的技巧。想看後面的讀者們請耐心等待 :D

    2009/01/22 系列文章: Entity Framework 與 物件導向設計 .NET C# 技術隨筆 物件導向 Entity Framework

  2. 難搞的 Entity Framework - 跨越 Context 的查詢

    咳,沒錯... 兩個月都沒寫什麼東西出來,就是都在研究 Entity Framework 跟 Enterprise Library... Enterprise Library 倒還好,看看範例,看看 Key Scenario 大概就能入門了,不過 Entity Framework 就沒這麼簡單...

    Microsoft 對 Entity Framework 的 Roadmap 規劃的很大,不過再怎麼樣附在 .NET 3.5 SP1 的也還只是第一版而已。背負著龐大的架構,而現在卻還不一定能拼過其它成熟的 ORM solution, 這就是辛苦的地方。

    先抱怨一下它的設計工具... 好用是蠻好用的 (跟直接寫 XML 檔相比),不過小問題還不少。像是 TABLE 拉進去,刪掉再拉進去,就有機會 build 失敗... VIEW 拉進去沒辦法明確的指定 KEY 是那個欄位,不是結果不對就是 build 失敗,最後忍不住還是得去手動改 .edmx .. 這些事件老實說這兩個月也碰了不少 @_@

    不過撇開工具的問題,Entity Framework 的設計還真是不錯。其它的心得就改天再講,先講困擾我最久的,也是大部份 ORM 的通病 - 大型 database 的問題。

    這裡指的 "大型" 不是指資料筆數很多,是指 schema 很複雜的情況。大型的 AP 用到上百個 table + view 是很常見的,尤其是隨著改版,舊 table 沒刪掉,新的 table 又一直加,那還真是恐佈。所有的 ORM 都需要某種型態的 O / R Mapping, 不是寫設定檔,就是有視覺化的設計工具。不過... 你能想像一張有 500 個 table 的 ER-MODEL 嗎? 

    要避免過大的單一 OR Mapping 設定,就只有做適度的切割了。在 Entity Framework / Visual Studio 2008 ,這點很容易做到,分成多個 .edmx 檔就可以了。不過問題會在後面,分開多個 .edmx 有幾個缺點:

    1. 會有多個 ObjectContext 產生,每邊都有物件要更新時,每個 ObjectContext 都得呼叫 SaveChange..
    2. 不論是 LINQ to Entities 或是 eSQL, 想要 join 橫跨在多個 .edmx 的資料時,會得到無情的錯誤... 不支援跨 context 的操作
    3. AssoicationSet 無法跨越 context 的範圍,意思是跨 .edmx 的 Entity, 不能靠 Navigation Property 來處理。

    解決的方式當然也有,也查了 ADO.NET Team Blog,這兩篇是所有 GOOGLE 到的相關文章裡,講這問題講的最深入的..

    有碰到這問題的,這兩篇一定要看一看。其實文章內探討的問題已經超過我的需求了,我只是要解決我面臨到的問題:

    1. 因應模組化需求,我需要把 .edmx 跟其相關的邏輯封裝在各別的 assembly
    2. 不同的模組間定義的 EntitySet 能夠用 eSQL 做 JOIN 的查詢
    3. 最基本的 LINQ 也不能少
    4. 新增新的模組時,其它模組不需要重新編譯

    老實說這兩篇沒解決到我的問題,只不過瞄到了 Part 2 有這麼一段話:

    9. At runtime, you could create either one Context that works with both the schema sets or two different contexts. To create a single context with both the schema sets, you would use the ObjectContext constructor that takes in an EntityConnectionString. In the Metadata parameter of the connection string, specify the paths to both sets of files.

    哈哈,運氣不錯,關鍵的一段話沒有漏掉... 就這段話解決了我一個多月以來的困擾。Entity Connection 用的連接字串又臭又長,包括了你的 .csdl, .ssdl, .msl 三份定義檔在那裡,還有底層用的 database connection string. 湊起來一大串,像這樣:

    medadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl;............

    而從這兩篇文章,除了各種 split / reuse 這堆設定檔的方法及優缺點之外,就是學到原來 entity connect string 可以併入多組對應檔啊 :D,像這樣:

    medadata=res://*/Model1.csdl|res://*/Model1.ssdl|res://*/Model1.msl|res://*/Model2.csdl|res://*/Model2.ssdl|res://*/Model2.msl;............

    沒錯!! 說穿了不值錢,就是把 entity connection string 這樣改一改就好了,eSQL 就可以透過單一 object context 就能存取兩份 .edmx 內定義的內容, Linq 則要自己用 CreateQuery< >() 方式來產生 EntitySet.. 其它就沒有什麼太大不同了。

    總算搞定這個大問題!! 收工! 其它的應用就改天有空再慢慢貼了 :D 感謝各位在這兩個月枯水期還沒取消訂閱我的 BLOG ... :D

    2009/01/20 .NET C# Entity Framework

  3. RUN!PC 精選文章 - 生產線模式的多執行緒應用

    http://www.runpc.com.tw/content/main_content.aspx?mgo=178&fid=E08

    無意間 search 我自己的名字,才發現這篇文章除了投稿到 RUN! PC 之外,原來還有刊在網站上的精選文章啊...

    哈哈,暗爽一下,順道貼一下 link, 讓沒看到雜誌的網友們也有機會看一看在下的作品...

    2009/01/16 RUN! PC 專欄文章 .NET C# RUN! PC 作品集 多執行緒 專欄 技術隨筆 有的沒的

  4. XmlWellFormedWriter.WriteRaw( ) 的 Bug 後續發展

    一時順手,就按下 Visual Studio 2008 上面的 [Report Bug] 回報上一篇發現的 Bug, 沒想到 Microsoft 真的有回應耶... :D

    反正 Microsoft 在 connect 裡的回應本來就公開的,我就順手貼一下:

     

    https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=386899&wa=wsignin1.0

     

    Hello,
    Originally the WriteRaw method was designed for XmlWriters that are formatting text XML into a file. In those cases the WriteRaw method can be used to write out an XML fragment that is already formatted and checked for well-formedness. It can also be used for writing text nodes with special character that have been already escaped and no further processing of the text is needed.
    However, when we introduced the XmlWriter over XmlDocument/XDocument (accessed via XPathNavigator editing methods), the use of the WriteRaw method on top of XmlDocument became controversial. We had two options:
    1.    Threat it as a text
    2.    Parse it into nodes


    The second option is very difficult (if not possible) to do. The XML fragment can we written out in multiple WriteRaw calls, so we could not assume that a single WriteRaw will contain a fully enclosed fragment. It can also be interleaved with other XmlWriter calls and nested many times – overall a very hard thing to implement properly. So that is why we have decided to treat the WriteRaw content as text, which is what you are seeing.
    If you have an XML fragment in a string and you want to append it to XmlDocument, you can do it like this:


                    XmlDocument doc = new XmlDocument();
                    XmlElement rootElement = doc.CreateElement("root");
                    rootElement.InnerXml = "<a/><a/><a/><a/><a/>";
                    doc.AppendChild(rootElement);


    Or if you really want to use the XmlWriter from XPathNavigator.AppendChild(), you can create an XmlReader over your fragment and use the WriteNode method on the XmlWriter:


                    XmlDocument doc = new XmlDocument();
                    XmlWriter writer = doc.CreateNavigator().AppendChild();
                    writer.WriteStartElement("root");
                    using (XmlReader r = XmlReader.Create(new StringReader("<a/><a/><a/><a/><a/>"), new XmlReaderSettings { ConformanceLevel = ConformanceLevel.Fragment } ) ) {
                        writer.WriteNode(r, false);
                    }
                    writer.WriteEndElement();
                    writer.Close();
                    doc.Save(Console.Out);


    I hope this helps.
    Thank you,
    -Helena Kotas, System.Xml Developer

     

    看來 Microsoft 認為這是權宜之計,不算 BUG,要 USER 就直接避掉了,只是他建議的解法剛好就是我上一篇用的,用 XmlReader, 搭上 ConformanceLevel.Fragment 的設定解決掉...

    只不過,這種情況,不是應該丟出 NotSupportException 比較好嘛? 幹嘛拿個不適當的實作填進來?

     

    提外話,現在 WEB 2.0 的時代,Microsoft 工程師除了寫 CODE 之外,也要負責回客戶的問題了? 真辛苦...

    2008/12/10 .NET C# MSDN Tips 技術隨筆 有的沒的

  5. 原來 System.Xml.XmlWellFormedWriter 有 Bug ..

    果然沒啥人知道的 code, bug 也會比較慢被抓出來 ... 兩個小時前我才貼了找到 XmlNodeWriter 的替代品,用了一下就被我挖到一個 BUG ... @_@

    先來看看我的 Sample Code:

    XmlTextWriter v.s. XmlWellFormedWriter[copy code]
       1:  // test xml text writer, correct result
       2:  // output: <?xml version="1.0" encoding="big5"?><root><a/><a/><a/><a/><a/></root>
       3:  {
       4:      Console.WriteLine("Using XmlTextWriter:");
       5:      XmlWriter writer = XmlWriter.Create(Console.Out);
       6:      writer.WriteStartElement("root");
       7:      writer.WriteRaw("<a/><a/><a/><a/><a/>");
       8:      writer.WriteEndElement();
       9:      writer.Flush();
      10:      Console.WriteLine();
      11:      Console.WriteLine();
      12:  }
      13:  // test xml node writer, wrong result
      14:  // output: <?xml version="1.0" encoding="big5"?><root>&lt;a/&gt;&lt;a/&gt;&lt;a/&gt;&lt;a/&gt;&lt;a/&gt;</root>
      15:  {
      16:      Console.WriteLine("Using XmlWellFormedWriter:");
      17:      XmlDocument xmldoc = new XmlDocument();
      18:      XmlWriter writer = xmldoc.CreateNavigator().AppendChild();
      19:      writer.WriteStartElement("root");
      20:      writer.WriteRaw("<a/><a/><a/><a/><a/>");
      21:      writer.WriteEndElement();
      22:      writer.Close();
      23:      xmldoc.Save(Console.Out);
      24:      Console.WriteLine();
      25:      Console.WriteLine();
      26:  }

     

    而這是程式的輸出畫面:

    image

     

    兩段 code 除了拿到的 XmlWriter 來源不同之外,用它寫 XML DATA 的方式是一致的,不過寫出來的 XML 則完全不同。看來兩種 XmlWriter 對於 WriteRaw(...) 的實作不大相同。而照 MSDN 上的說明來說,XmlTextWriter的行為是對的,XmlWellFormedWriter 則太雞婆了,沒事多作一次編碼...

     

    該說運氣好嘛? 哈哈... 繼上次撈到一個 SmtpMail 的 Bug 之後,這次又撈到一個... 要用的人注意一下,不過即使有這個 Bug, 也不會影響它的地位啦,這 Writer 解決了我很大的困擾,動搖國本也要用下去... (咳... 不過是避開一個 API ...)

     

    最後我改了用法,一方面 API 有 BUG 是一回事,另一方面直接用這 API 也很危險,因為 MSDN 說它不會去做內容的驗證,也就是說透過 WriteRaw( ) 寫進不合法的資料,會讓你整份輸出都毀了... 第二個原因比較重要,因此我換了一個替代作法, 類似 Pipe 一樣,把 XmlReader 讀到的東西都寫到 XmlWriter:

    XmlCopyPipe 實作[copy code]
       1:  /// <summary>
       2:   /// 從 XmlReader 複製到 XmlWriter
       3:   /// </summary>
       4:   /// <param name="reader"></param>
       5:   /// <param name="writer"></param>
       6:   private static void XmlCopyPipe(XmlReader reader, XmlWriter writer)
       7:   {
       8:       if (reader == null)
       9:       {
      10:           throw new ArgumentNullException("reader");
      11:       }
      12:       if (writer == null)
      13:       {
      14:           throw new ArgumentNullException("writer");
      15:       }
      16:       while (reader.Read() == true)
      17:       {
      18:           switch (reader.NodeType)
      19:           {
      20:               case XmlNodeType.Element:
      21:                   writer.WriteStartElement(reader.Prefix, reader.LocalName, reader.NamespaceURI);
      22:                   writer.WriteAttributes(reader, true);
      23:                   if (reader.IsEmptyElement)
      24:                   {
      25:                       writer.WriteEndElement();
      26:                   }
      27:                   break;
      28:               case XmlNodeType.Text:
      29:                   writer.WriteString(reader.Value);
      30:                   break;
      31:               case XmlNodeType.Whitespace:
      32:               case XmlNodeType.SignificantWhitespace:
      33:                   writer.WriteWhitespace(reader.Value);
      34:                   break;
      35:               case XmlNodeType.CDATA:
      36:                   writer.WriteCData(reader.Value);
      37:                   break;
      38:               case XmlNodeType.EntityReference:
      39:                   writer.WriteEntityRef(reader.Name);
      40:                   break;
      41:               case XmlNodeType.XmlDeclaration:
      42:               case XmlNodeType.ProcessingInstruction:
      43:                   writer.WriteProcessingInstruction(reader.Name, reader.Value);
      44:                   break;
      45:               case XmlNodeType.DocumentType:
      46:                   writer.WriteDocType(reader.Name, reader.GetAttribute("PUBLIC"), reader.GetAttribute("SYSTEM"), reader.Value);
      47:                   break;
      48:               case XmlNodeType.Comment:
      49:                   writer.WriteComment(reader.Value);
      50:                   break;
      51:               case XmlNodeType.EndElement:
      52:                   writer.WriteFullEndElement();
      53:                   break;
      54:           }
      55:       }
      56:   }

     

    很好用的作法,就像過去需要 COPY XML 資料,最常見的就是把來源跟目的都用 XmlDocument 載入,直接用 ImportNode( ) 把 XML 片段資料搬到另一個 XmlDocument 再儲存。跟上一篇的原因一樣,看起來很蠢... 就想到這個作法,透過 XmlReader, 拿到的是已經 parsing 過的資料,直接寫到 XmlWriter。而我用的 Writer 正好又可避開重複作 parsing 動作的優點,正好這樣效能跟可用性都兼顧了... 經過 parsing, 至少寫出來的東西會安心一點...

     

    把最後我的程式搭配這個 XmlPipeCopy 改一改:

    用 XmlCopyPipe 取代 WriteRaw( )[copy code]
       1:  XmlDocument xmldoc = new XmlDocument();
       2:  XmlWriter writer = xmldoc.CreateNavigator().AppendChild();
       3:  writer.WriteStartElement("root");
       4:  XmlReaderSettings settings = new XmlReaderSettings();
       5:  settings.ConformanceLevel = ConformanceLevel.Fragment;
       6:  XmlReader reader = XmlReader.Create(
       7:      new StringReader("<a/><a/><a/><a/><a/>"),
       8:      settings);
       9:  XmlCopyPipe(reader, writer);
      10:  writer.WriteEndElement();
      11:  writer.Close();
      12:  xmldoc.Save(Console.Out);

     

    試了一下,果然如預期的執行了 :D,結果也沒錯,還好 XmlWellFormedWriter 的 Bug 只存在於 WriteRaw... 閃開就沒事了:

    image

     

     

    其中有個陷阱,就是如何用 XmlReader 讀取 XmlFragment (可以有多個 ROOT 的 XML DATA)。其實這個解法跟程式碼,大部份都是這篇看來的,只不過在裡面加了個 LOOP 跟改了名字,各位覺的好用的話記得去謝原作者 Mark Fussell, 別謝錯人了 :D

    2008/12/08 .NET C# MSDN Tips 有的沒的