none
ClickOnce-WebService間のデータやり取りでXMLエラーが出ます RRS feed

  • 質問

  • Visual Studio 2010 C#を用いてClickOnceアプリケーションを作成しています。

    ClickOnceからWebServiceを呼び出し、データベースのデータを取得し、型指定DataTableの形でClickOnce側に返却しているのですが

    その際、データ内に「垂直タブ(0x0b)」があると下記のエラーが発生します。

    {System.Web.Services.Protocols.SoapException: サーバーは要求を処理できませんでした。 ---> System.InvalidOperationException: XML ドキュメント (327,28) でエラーが発生しました。 ---> System.Xml.XmlException: '' (16 進数値 0x0B) は無効な文字です。 行 327、位置 28 です。
       場所 System.Xml.XmlTextReaderImpl.Throw(Exception e)
       場所 System.Xml.XmlTextReaderImpl.ThrowInvalidChar(Int32 pos, Char invChar)
       場所 System.Xml.XmlTextReaderImpl.ParseNumericCharRefInline(Int32 startPos, Boolean expand, BufferBuilder internalSubsetBuilder, Int32& charCount, EntityType& entityType)
       場所 System.Xml.XmlTextReaderImpl.ParseCharRefInline(Int32 startPos, Int32& charCount, EntityType& entityType)
       場所 System.Xml.XmlTextReaderImpl.ParseText(Int32& startPos, Int32& endPos, Int32& outOrChars)
       場所 System.Xml.XmlTextReaderImpl.ParseText()
       場所 System.Xml.XmlTextReaderImpl.ParseElementContent()
       場所 System.Data.DataTextReader.Read()
       場所 System.Data.XmlDataLoader.LoadData(XmlReader reader)
       場所 System.Data.DataTable.ReadXmlDiffgram(XmlReader reader)
       場所 System.Data.DataTable.ReadXml(XmlReader reader, XmlReadMode mode, Boolean denyResolving)
       場所 System.Data.DataTable.ReadXmlSerializable(XmlReader reader)
       場所 System.Data.DataTable.System.Xml.Serialization.IXmlSerializable.ReadXml(XmlReader reader)
       場所 System.Xml.Serialization.XmlSerializationReader.ReadSerializable(IXmlSerializable serializable, Boolean wrappedAny)
       場所 Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReaderSAMPLEDataTable.Read1_SAMPLEDataTable()
       --- 内部例外スタック トレースの終わり ---
       場所 System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle, XmlDeserializationEvents events)
       場所 System.Xml.Serialization.XmlSerializer.Deserialize(TextReader textReader)
       場所 ClickOnce.GetDataTable()  
       --- 内部例外スタック トレースの終わり ---}

    XMLで「垂直タブ(0x0b)」が制御文字として扱われることは調べたのですが、

    WebServiceからデータを返す際、

    ・型指定DataSetの形で返却する場合はこのエラーは発生せず、ClickOnceはデータを受け取ることができる。

    ・型指定DataTableの形で返却する場合のみ、このエラーが発生する。

    という違いがあることがわかりました。

    そこで、

    ・型指定DataSetと型指定DataTableでXML WebServiceの変換処理が異なるのか?

    ・型指定DataSetを返却するように実装すればこの問題は起きないのか?

    が、わからず困っています。

    ご助言いただけると幸いです。

    2018年7月13日 9:56

回答

  • .NetFramework2.0のころのWeb参照技術を使っているとエラーになって、3.0以降の技術(WCF)を使っているとエラーにならないです。

    2.0の頃はSystem.Web.Services.Protocols.SoapHttpClientProtocolを継承したクラスでアクセスし、3.0でSystem.ServiceModel.ClientBase<T>を継承したクラスでアクセスする違いがあります。

    DataSetやDataTableにはSystem.Xml.Serialization.IXmlSerializableが実装されており、独自のシリアライズ/デシリアライズが行われています。
    ダウンロードされてデシリアライズ処理でIXmlSerializable.ReadXml(XmlReader reader)が呼ばれることになりますが、この引数のreaderが2.0のだとSystem.Xml.XmlTextReaderが、3.0のだとSystem.Xml.XmlUTF8TextReaderというのが渡されます。
    おそらくXmlUTF8TextReaderでは制御文字をそのまま通しているので3.0のではエラーにならないのでしょう。

    2.0のでDataSetとDataTableとで違いがあるのは、ReadXmlがそれぞれ別に実装されているからです。
    そしてXmlTextReaderはNormalizationプロパティがtrueだと制御文字でエラーになります。
    このNormalizationプロパティがなぜかDataSetの時はfalseにセットされるが、DataTableの時はtrueのまま読み出そうとしています。
    このため、DataTableの場合のみエラーになってしまうことになります。

    2.0のままで回避するには上記のNormalizationプロパティをfalseにしてやればよさそうなので、SoapHttpClientProtocolを継承しているクラスにpartialでGetReaderForMessageをoverrideしてXmlTextReaderのNormalizationプロパティをfalseにしてやるといけるかもしれません。

    namespace ConsoleApplication1.ServiceReference1
    {
        using System.Web.Services.Protocols;
        using System.Xml;
    
        public partial class WebService1 : SoapHttpClientProtocol
        {
             protected override XmlReader GetReaderForMessage(SoapClientMessage message, int bufferSize)
             {
                  XmlReader reader = base.GetReaderForMessage(message, bufferSize);
                  XmlTextReader textReader = reader as XmlTextReader;
                  if (textReader != null)
                  {
                       textReader.Normalization = false;
                  }
                  return reader;
             }
        }
    }

    #これでも元のDataTableがDataSetに所属しているかしてないかで結果が変わる...


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    2018年7月16日 8:41
  • > データベースのデータを取得し、型指定DataTableの形でClickOnce側に返却しているのですが
    > その際、データ内に「垂直タブ(0x0b)」があると下記のエラーが発生します。

    ASP.NET Web サービス単独の問題としては自分の環境(Windows 10 Pro 64-bit, ASP.NET Web Forms アプリ、.NET 4.6.1, ローカル IIS10, Visual Studio Community 2015 Update 3)では上記の問題は再現できず、期待通りの結果が得られます。

    質問者さんとのやり方の違い・環境の違いはあるでしょうが、問題は ClickOnce 側にあるような気がします。

    もちろん気がするだけで確証はありませんが・・・ だから質問者さんに切り分けを行うようお願いしています。

    検証に使ったコードをアップしておきます。

    ASP.NET Web Service

    以下のコードで GetLong メソッドは関係ないので無視してください。GetDataTable メソッドに注目。Northwind の Products テーブルから作成した型付 DataSet (ProductsDataSet) / 型付 DataTable (ProductsDataTable) を使用。

    <%@ WebService Language="C#" Class="NorthwindProductsWebService" %>
    
    using System;
    using System.Web;
    using System.Web.Services;
    using System.Web.Services.Protocols;
    using NorthwindProducts;
    using NorthwindProducts.ProductsDataSetTableAdapters;
    
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    public class NorthwindProductsWebService  : System.Web.Services.WebService
    {
        [WebMethod]
        public long? GetLong()
        {
            return null;
        }
    
        [WebMethod]
        public ProductsDataSet.ProductsDataTable GetDataTable()
        {        
            ProductsDataSet.ProductsDataTable datatable = new ProductsDataSet.ProductsDataTable();
            ProductsTableAdapter adapter = new ProductsTableAdapter();
            adapter.Fill(datatable);
            return datatable;
        }
    }

    SSMS を使って Products テーブルの最初のレコードの ProductName にある 'Chai' の 'Ch' と 'ai' の間に垂直タブ NCHAR(11) を入れてみます。



    クライアント側のコンソールアプリ

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Data;
    
    namespace ConsoleAppWebService
    {
        class Program
        {
            static void Main(string[] args)
            {
                ServiceReference1.NorthwindProductsWebServiceSoapClient client = 
                    new ServiceReference1.NorthwindProductsWebServiceSoapClient();
    
                long? value = client.GetLong();
                Console.WriteLine("Result of GetLong: {0}", (value == null) ? "N/A" : value.ToString());
    
                ServiceReference1.ProductsDataSet.ProductsDataTable table = client.GetDataTable();
                for (int i = 0; i < 5; i++)
                {
                    string categoryId = (table[i][table.CategoryIDColumn] is DBNull) ? 
                        "N/A" : table[i][table.CategoryIDColumn].ToString();
    
                    Console.WriteLine("Name: {0}, SupplierID: {1}, CategoryID: {2}",
                        table[i].ProductName, table[i].SupplierID, categoryId);
                }
            }
        }
    }

    結果は以下の通り。



    バイナリエディタで見ると 0b になってます。



    ちなみに、'Chai' 全体を NCHAR(11) で置き換えても期待通りの結果が得られます。

    2018年7月15日 4:52

すべての返信

  • ASP.NET Web サービス単独の問題なのか、そうではなくて ClickOnce が絡むと出る問題なのか切り分けられているのでしょうか?

    前者であれば、問題を再現できる必要最低限のごく簡単なサンプルを書けませんか? (後者だと自分はお手上げですけど)
    2018年7月15日 1:36
  • ・型指定DataSetの形で返却する場合はこのエラーは発生せず、ClickOnceはデータを受け取ることができる。

    ・型指定DataTableの形で返却する場合のみ、このエラーが発生する。

    うろ覚えなのですが、型付DataSet + TableAdapter を利用していた時に、下記の 3 パターンで受信側の動作が変わった記憶があって、DataTable だと受け渡しがうまくいかなかったので、それ以降Ⓒで統一するようにしています。

    SOAP メッセージの内容までは追跡していないので、何故失敗したのかまでは未調査ですし、「 」"\u000b" ("\v") の⭿垂直タブ文字␋に対しても当てはまるのかは判りませんが、一応経験則ということで。 

    // WebMethodⒶ  [DataSet を生成して DataTable を返却]
    var ds = new HogeDataSet();
    using (var adp = new HogeDataSetTableAdapters.ExampleTableAdapter())
    {
        adp.Fill(ds.Example);
    }
    return ds.Example;
    // WebMethodⒷ [DataTable を生成して DataTable を返却]
    var table = new HogeDataSet.ExampleDataTable();
    using (var adp = new HogeDataSetTableAdapters.ExampleTableAdapter())
    {
        adp.Fill(table);
    }
    return table;
    // WebMethodⒸ [DataSet を生成して DataSet を返却]
    var ds = new HogeDataSet();
    using (var adp = new HogeDataSetTableAdapters.ExampleTableAdapter())
    {
        adp.Fill(ds.Example);
    }
    return ds;

    2018年7月15日 2:53
  • > データベースのデータを取得し、型指定DataTableの形でClickOnce側に返却しているのですが
    > その際、データ内に「垂直タブ(0x0b)」があると下記のエラーが発生します。

    ASP.NET Web サービス単独の問題としては自分の環境(Windows 10 Pro 64-bit, ASP.NET Web Forms アプリ、.NET 4.6.1, ローカル IIS10, Visual Studio Community 2015 Update 3)では上記の問題は再現できず、期待通りの結果が得られます。

    質問者さんとのやり方の違い・環境の違いはあるでしょうが、問題は ClickOnce 側にあるような気がします。

    もちろん気がするだけで確証はありませんが・・・ だから質問者さんに切り分けを行うようお願いしています。

    検証に使ったコードをアップしておきます。

    ASP.NET Web Service

    以下のコードで GetLong メソッドは関係ないので無視してください。GetDataTable メソッドに注目。Northwind の Products テーブルから作成した型付 DataSet (ProductsDataSet) / 型付 DataTable (ProductsDataTable) を使用。

    <%@ WebService Language="C#" Class="NorthwindProductsWebService" %>
    
    using System;
    using System.Web;
    using System.Web.Services;
    using System.Web.Services.Protocols;
    using NorthwindProducts;
    using NorthwindProducts.ProductsDataSetTableAdapters;
    
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    public class NorthwindProductsWebService  : System.Web.Services.WebService
    {
        [WebMethod]
        public long? GetLong()
        {
            return null;
        }
    
        [WebMethod]
        public ProductsDataSet.ProductsDataTable GetDataTable()
        {        
            ProductsDataSet.ProductsDataTable datatable = new ProductsDataSet.ProductsDataTable();
            ProductsTableAdapter adapter = new ProductsTableAdapter();
            adapter.Fill(datatable);
            return datatable;
        }
    }

    SSMS を使って Products テーブルの最初のレコードの ProductName にある 'Chai' の 'Ch' と 'ai' の間に垂直タブ NCHAR(11) を入れてみます。



    クライアント側のコンソールアプリ

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Data;
    
    namespace ConsoleAppWebService
    {
        class Program
        {
            static void Main(string[] args)
            {
                ServiceReference1.NorthwindProductsWebServiceSoapClient client = 
                    new ServiceReference1.NorthwindProductsWebServiceSoapClient();
    
                long? value = client.GetLong();
                Console.WriteLine("Result of GetLong: {0}", (value == null) ? "N/A" : value.ToString());
    
                ServiceReference1.ProductsDataSet.ProductsDataTable table = client.GetDataTable();
                for (int i = 0; i < 5; i++)
                {
                    string categoryId = (table[i][table.CategoryIDColumn] is DBNull) ? 
                        "N/A" : table[i][table.CategoryIDColumn].ToString();
    
                    Console.WriteLine("Name: {0}, SupplierID: {1}, CategoryID: {2}",
                        table[i].ProductName, table[i].SupplierID, categoryId);
                }
            }
        }
    }

    結果は以下の通り。



    バイナリエディタで見ると 0b になってます。



    ちなみに、'Chai' 全体を NCHAR(11) で置き換えても期待通りの結果が得られます。

    2018年7月15日 4:52
  • .NetFramework2.0のころのWeb参照技術を使っているとエラーになって、3.0以降の技術(WCF)を使っているとエラーにならないです。

    2.0の頃はSystem.Web.Services.Protocols.SoapHttpClientProtocolを継承したクラスでアクセスし、3.0でSystem.ServiceModel.ClientBase<T>を継承したクラスでアクセスする違いがあります。

    DataSetやDataTableにはSystem.Xml.Serialization.IXmlSerializableが実装されており、独自のシリアライズ/デシリアライズが行われています。
    ダウンロードされてデシリアライズ処理でIXmlSerializable.ReadXml(XmlReader reader)が呼ばれることになりますが、この引数のreaderが2.0のだとSystem.Xml.XmlTextReaderが、3.0のだとSystem.Xml.XmlUTF8TextReaderというのが渡されます。
    おそらくXmlUTF8TextReaderでは制御文字をそのまま通しているので3.0のではエラーにならないのでしょう。

    2.0のでDataSetとDataTableとで違いがあるのは、ReadXmlがそれぞれ別に実装されているからです。
    そしてXmlTextReaderはNormalizationプロパティがtrueだと制御文字でエラーになります。
    このNormalizationプロパティがなぜかDataSetの時はfalseにセットされるが、DataTableの時はtrueのまま読み出そうとしています。
    このため、DataTableの場合のみエラーになってしまうことになります。

    2.0のままで回避するには上記のNormalizationプロパティをfalseにしてやればよさそうなので、SoapHttpClientProtocolを継承しているクラスにpartialでGetReaderForMessageをoverrideしてXmlTextReaderのNormalizationプロパティをfalseにしてやるといけるかもしれません。

    namespace ConsoleApplication1.ServiceReference1
    {
        using System.Web.Services.Protocols;
        using System.Xml;
    
        public partial class WebService1 : SoapHttpClientProtocol
        {
             protected override XmlReader GetReaderForMessage(SoapClientMessage message, int bufferSize)
             {
                  XmlReader reader = base.GetReaderForMessage(message, bufferSize);
                  XmlTextReader textReader = reader as XmlTextReader;
                  if (textReader != null)
                  {
                       textReader.Normalization = false;
                  }
                  return reader;
             }
        }
    }

    #これでも元のDataTableがDataSetに所属しているかしてないかで結果が変わる...


    個別に明示されていない限りgekkaがフォーラムに投稿したコードにはフォーラム使用条件に基づき「MICROSOFT LIMITED PUBLIC LICENSE」が適用されます。(かなり自由に使ってOK!)

    2018年7月16日 8:41
  • SurferOnWww様

    魔界の仮面弁士様

    回答ありがとうございます。いつも参考にさせていただいております。

    SurferOnWww様の方法で試したところ、こちらでも正常にデータを取得することができました。

    調査に進展があった為ご報告します。

    サービス参照とWeb参照とで、エラーになる・ならないの違いがありました。

    ・サービス参照(SurferOnWww様の実装方法) 

    ・Web参照(垂直タブでエラーになった実装方法)

    ※どちらも参照しているWebサービスは同じです。

    ※Web参照は、サービス参照の追加 - 詳細設定 - Web参照の追加で登録できます。

    更に、エラーのトレースにあったXmlSerializerのDeserializeも検証したところ、

    垂直タブの入ったDataSetはシリアル化後にDeserializeできるのですが、DataTableの場合はできませんでした。

    // 垂直タブの入ったDataTable取得
    var dt = this._getDataTable();
    
    // XmlSerializer
    var serializer = new XmlSerializer(dt.GetType());
    
    // Serialize
    var sw = new StringWriter();
    serializer.Serialize(sw, dt);
    
    // Deserialize
    var sr = new StringReader(sw.ToString());
    var des = serializer.Deserialize(sr); // Xml Error!!

    実際の内部の流れまで調べてないのですが、XmlSerializerのDeserializeに問題があるような気もしています。

    ひとまず調査報告まで。


    2018年7月16日 9:13
  • gekka様

    ご回答ありがとうございます。

    これまで調べたことで出た疑問点がいっきに解決しました。

    DataSetとDataTableでそういった違いがあったのですね・・・

    気付けませんでした。

    とてもすっきりしました。ありがとうございます。

    2018年7月16日 9:23
  • 一つ教えていただけませんか?

    以下の MSDN ライブラリによると "Web 参照は、作成するアプリケーションが .NET Framework Version 2.0 を対象としている場合にのみ追加してください。" ということですが、質問者さんのケースでそういう事情があったのでしょうか?

    方法 : Web サービスへの参照を追加する
    https://msdn.microsoft.com/ja-jp/library/bb628649.aspx

    それとも ClickOnce が絡むとそうならざるを得ないという事情があるのでしょうか?
    2018年7月17日 1:17
  • SurferOnWww様

    Web参照でなければならなかったということはありません。

    事情としては、Visual Studio 2005の頃に開発したシステムをVisual Studio 2010にバージョンアップしています。

    その際、Web参照の設定のままで作成しておりました。

    "Web 参照は、作成するアプリケーションが .NET Framework Version 2.0 を対象としている場合にのみ追加してください。"

    というのは知らなかったです。Web参照で実装しない方が良さそうですね。

    2018年7月17日 6:13
  • 了解しました。情報提供をありがとうございました。
    2018年7月17日 8:22