2012-03-23 14 views
3

私は、シリアル/ UDP/TCPソースから入力データを読み込むシステムを持っています。入力データは、異なるデータタイプ(たとえば、DateTime、double、int、string)のCSVです。例えば、文字列は次のようになります。C#ユーザー定義CSVからPOCOへのマッピング

2012/03/23 12:00:00,1.000,23,information,1.234 

私は現在、ユーザーはPOCOのどのプロパティに行くCSVリスト内のどの値を指定することができます(未テスト)コードを持っています。

したがって、上記の例では、私はそうのようなオブジェクト必要があります。このクラスでは今

public class InputData 
{ 
public DateTime Timestamp{get;set;} 
public double Distance{get;set;} 
public int Metres{get;set;} 
public string Description{get;set;} 
public double Height{get;set;} 
} 

を、私は、CSV文字列を解析してプロパティを移入する方法があります。この方法では、CSVデータの到着順序を保証するものではないため、「マッピング」情報も必要です。正しい順序を定義するのはユーザーの責任です。

これは私のマッピングクラスである:

//This general class handles mapping CSV to objects 
public class CSVMapping 
{ 
    //A dictionary holding Property Names (Key) and CSV indexes (Value) 
    //0 Based index 
    public IDictionary<string, int> Mapping { get; set; } 
} 

今私の方法ParseCSV():

//use reflection to parse the CSV survey input 
public bool ParseCSV(string pCSV, CSVMapping pMapping) 
{ 
    if (pMapping == null) return false; 
    else 
    { 
     Type t = this.GetType(); 
     IList<PropertyInfo> properties = t.GetProperties(); 
     //Split the CSV values 
     string[] values = pCSV.Split(new char[1] { ',' }); 
     //for each property set its value from the CSV 
     foreach (PropertyInfo prop in properties) 
     { 
      if (pMapping.Mapping.Keys.Contains(prop.Name)) 
      { 
       if (prop.GetType() == typeof(DateTime)) 
       { 
        if (pMapping.Mapping[prop.Name] >= 0 && pMapping.Mapping[prop.Name] < values.Length) 
        { 
         DateTime tmp; 
         DateTime.TryParse(values[pMapping.Mapping[prop.Name]], out tmp); 
         prop.SetValue(this, tmp, null); 
        } 
       } 
       else if (prop.GetType() == typeof(short)) 
       { 
        if (pMapping.Mapping[prop.Name] >= 0 && pMapping.Mapping[prop.Name] < values.Length) 
        { 
         double tmp; 
         double.TryParse(values[pMapping.Mapping[prop.Name]], out tmp); 
         prop.SetValue(this, Convert.ToInt16(tmp), null); 
        } 
       } 
       else if (prop.GetType() == typeof(double)) 
       { 
        if (pMapping.Mapping[prop.Name] >= 0 && pMapping.Mapping[prop.Name] < values.Length) 
        { 
         double tmp; 
         double.TryParse(values[pMapping.Mapping[prop.Name]], out tmp); 
         prop.SetValue(this, tmp, null); 
        } 
       } 
       else if (prop.GetType() == typeof(string)) 
       { 
        if (pMapping.Mapping[prop.Name] >= 0 && pMapping.Mapping[prop.Name] < values.Length) 
        { 
         prop.SetValue(this, values[pMapping.Mapping[prop.Name]], null); 
        } 
       } 
      } 
     } 
     return true; 
    } 
} 

が今私の質問のために:

私は潜在的にこの機能が必要になりますいくつかのクラスを持っています。私のために解析を行うために汎用クラスまたは拡張クラスを実装することは有益でしょうか?私の方法は、CSVデータを解析してオブジェクトをポップするための健全な方法ですか?これを行うには良い方法がありますか?

私はCSVを動的に解析する上での他の質問を読んだことがありますが、実行時にはその順序がわかっていますが、ユーザーは注文を定義する必要があります。

+0

これについての議論は次の投稿にあります。 http:// stackoverflowcom/questions/1941392/are-there-any-csv-reader-writer-libraries-in-c – TGH

+0

TextFieldParserクラスは、コードを少し頑強にします。しかし、私は実際にその問題に取り組まれていないマッピング側にもっと興味を持っています。 – Simon

+0

簡単な質問です。あなたはどのように注文を知らないのですか? –

答えて

2

OleDBはCSVデータの解析時に優れており、リフレクションを使用する必要はありません。ここでのOLE DBクラスとのマッピングのための主なアイデアです:

  1. ユーザー(デリゲート、流れるようなインターフェイスか何かを使用して)マッピングを定義し、それはあなたのマッパークラスで辞書に入りました。
  2. パーサーは、DataTableを作成し、マッパーから列を挿入します。
  3. パーサーは、OleDbConnection、Adapter、Commandを作成し、CSVファイルからdataTableを正しい種類で埋め込みます。
  4. パーサーはDataTableからIDataRecordsを抽出し、MapperはIDataRecordからオブジェクトにマップする必要があります。レコードとオブジェクトのマッピングについては、Dapper.NET、Massive、PetaPocoのようなORMマッパーのソースを読むことをお勧めします。

OLEDB CSV解析:Load csv into oleDB and force all inferred datatypes to string

UPDATE

のみ1つの文字列がありますので、それが最も簡単で、最も単純なアプローチを使用して優れていることは言うまでもないです。したがって、質問の場合:

汎用クラスを実装する - 解析を進める必要がない場合(これ以上の文字列はなく、今後は制約/機能はありません)、オブジェクトを受け取る静的クラスマッピング情報。あなたのものとほぼ同じ外観になっています。ここでは、多少変更されていますバージョン(コンパイルされないかもしれないが、一般的なアイデアを反映すべきである):

public static class CSVParser 
{ 
    public static void FillPOCO(object poco, string csvData, CSVMapping mapping) 
    { 
     PropertyInfo[] relevantProperties = poco.GetType().GetProperties().Where(x => mapping.Mapping.Keys.Contains(x)).ToArray(); 
     string[] dataStrings = csvData.Split(','); 
     foreach (PropertyInfo property in relevantProperties) 
      SetPropertyValue(poco, property, dataStrings[mapping.Mapping[property.Name]]); 
    } 

    private static void SetPropertyValue(object poco, PropertyInfo property, string value) 
    { 
     // .. here goes code to change type to the necessary one .. 
     property.SetValue(poco, value); 
    } 
} 

入力された値への変換文字列について - ほとんどの場合を扱うConvert.ChangeType方法があります。しかし、ブール変数( "false"の代わりに "0"が与えられている)には特別な問題があります。

データ集団については、反射は遅いと言われていますが、単一のオブジェクトやめったに使用されない場合は簡単で簡単なので十分です。ポコ人口の問題に対処するための通常の方法は:実行時変換メソッドの作成(リフレクションを使用して初期化し、他のメソッドと同様にコンパイルして呼び出す) - 通常はDynamicMethod、Expression Treesなどを使用して実装されます - ここにそう。動的オブジェクトの使用法(C#4.0以降で使用可能) - 宣言する必要のない変数の割り当て/取得場所。市場で入手可能なライブラリを使用します(通常、データからオブジェクトへの変換に大きく依存するため、ORMシステムから入手します)。

個人的には、リフレクションが自分のパフォーマンスのニーズに適しているかどうかを測って、問題を早送りします。

+0

情報ありがとうございます。残念ながら、これはちょっと残忍だと思います。私は1行のCSVデータを解析するだけです(テキストファイルではありません)。 CSVから行く - DataTable - 誰かが私にそうでないと確信できない限り、POCOは私の意見では1追加ステップです – Simon

+0

申し訳ありません、私は何とか "一行"事のポイントを逃した。答えを更新しました。 –

+0

+1徹底的な回答ありがとうございました – Simon

2

これまでの数週間で5~10のCSVパーサーを書いたので、これは@ Dimitriyと100%一致します。

編集:(これはPath.GetTempFile()ようなものを使用して一時ファイルにテキストを保存する必要があり、それはあなたが望む柔軟性ができるようになります注意してください)

などの接続のDataTableを使用しての引数は最高だろう文字列が正しく使用されている - Extended Properties='true;FMT=Delimited;HDR=Yes'を使用すると、DataTableに入り、列見出しが保持されます(この場合は役立ちます)。

だから、これはあなたが指定した列見出しでのDataTableを生成

Name,Age,City 
Dominic,29,London 
Bill,20,Seattle 

のようなCSVを書くことができます。それ以外の場合は、以前のように序文を使用してください。

public UserData(DataRow row) 
{ 
    // At this point, the row may be reliable enough for you to 
    // attempt to reference by column names. If not, fall back to indexes 

    this.Name = Convert.ToString(row.Table.Columns.Contains("Name") ? row["Name"] : row[0]); 
    this.Age = Convert.ToInt32(row.Table.Columns.Contains("Age") ? row["Age"] : row[1]); 
    this.City = Convert.ToString(row.Table.Columns.Contains("City") ? row["City"] : row[2]); 
} 

一部は、変換プロセスが実際にはないことを主張するだろう:渡されたときのDataRowがデータを取り除くということ(私はすぐに取得するか、拡張メソッド)コンストラクタを追加し、これを統合すること

UserDataクラスの責任はPOCOであるためです。代わりに、ConverterExtensions.csクラスに拡張メソッドを実装してください。

public static class ConverterExtensions 
{ 
    public static void LoadFromDataRow<UserData>(UserData data, DataRow row) 
    { 
     data.Name = Convert.ToString(row.Table.Columns.Contains("Name") ? row["Name"] : row[0]); 
     data.Age = Convert.ToInt32(row.Table.Columns.Contains("Age") ? row["Age"] : row[1]); 
     data.City = Convert.ToString(row.Table.Columns.Contains("City") ? row["City"] : row[2]); 
    } 
} 

もっと堂々とした方法は、変換を定義するインターフェイスを実装することです。変換プロセスとそのインタフェースを実装し、そのインタフェース参照を内部的に格納します。これはあなたのための変換を行い、マッピングを完全に別々に保ち、あなたのPOCOをきれいに整えます。また、マッパーを「プラグイン」することもできます。

public interface ILoadFromDataRow<T> 
{ 
    void LoadFromDataRow<T>(T object, DataRow dr); 
} 

public class UserLoadFromDataRow : ILoadFromDataRow<UserData> 
{ 
    public void LoadFromDataRow<UserData>(UserData data, DataRow dr) 
    { 
     data.Name = Convert.ToString(row.Table.Columns.Contains("Name") ? row["Name"] : row[0]); 
     data.Age = Convert.ToInt32(row.Table.Columns.Contains("Age") ? row["Age"] : row[1]); 
     data.City = Convert.ToString(row.Table.Columns.Contains("City") ? row["City"] : row[2]); 
    } 
} 

public class UserData 
{ 
    private ILoadFromDataRow<UserData> converter; 

    public UserData(DataRow dr = null, ILoadFromDataRow<UserData> converter = new LoadFromDataRow<UserData>()) 
    { 
     this.converter = (converter == null ? new LoadFromDataRow<UserData>() : converter); 

     if(dr!=null) 
      this.converter.LoadFromDataRow(this,dr); 
    } 

    // POCO as before 
} 

ご使用のシナリオでは、拡張メソッドを使用してください。このインタフェースメソッド(分離と呼ばれる)は、拡張メソッドが登場する前にそれを実装する方法でした。

+0

+2よろしくお願いします。 – Simon

関連する問題