ASCII Schnittstellen mit Hilfe von Custom Attributes komfortabel erstellen – C#

Viele kennen dass Problem, dass Daten zwischen verschiedenen Welten auch Heute noch immer durch Übergabe von Schnittstellendateien, seien es EDI oder auch ASCII Dateien, die einen festen Satzaufbau haben, übergeben werden.

Die Definitionen sehen oft wie folgt aus:

Wobei die nähere Definition etwas komplexer sein kann:

Art: A alphanumerisches Feld N numerisches Feld Länge L Gesamtlänge des Datenfeldes Nachkomma K davon Nachkommastellen Position von von Stelle ... im Datensatz bis bis Stelle ... im Datensatz M/O M Muss-Feld O Kann-Feld (optional) M/O abhängig von anderen Angaben handelt es sich wahlweise um ein Muss- oder ein Kann-Feld

Der Output, also die Übertragungsdatei enthält dann Daten die ähnlich der nachfolgenden Abbildung aussehen können.

image

Eine durchaus übliche Vorgehensweise solche Daten erstellen zu können ist es, sich Klassen zu erstellen, die dem eigentlichen benötigten Satzaufbau entsprechen, diese in einer Füll Routine mit Daten füllt und anschließend die Daten der Klasseneigenschaften mit string.Format Stück für Stück ausgibt.

Wäre es nicht wünschenswert, bereits bei der Definition der Klasse festlegen zu können, welche Ausgabeeigenschaften (Ausgabeformat wie führende Nullen, Links oder Rechtsbündig, Anzahl Nachkommastellen usw.) die jeweilige Eigenschaft besitzt und an welcher Position die Eigenschaft ausgegeben werden soll, anzugeben, damit man anschließend die Daten einfach mit einer einzelnen Methode im richtigen Format ausgeben kann.

Und genau da setze ich mit meiner Lösung an:

Man kann mit sogenannten Custom Attributes beliebig zusätzliche Informationen an jedes beliebige Klassenelement anheften. Hierzu muss man eine Klasse erstellen die von System.Attributes abgeleitet wird.

Siehe Nachfolgendes Beispiel, welches ziemlich selbsterklärend sein sollte.

using System;
    using System.Reflection;

    public enum ExportFieldType
    {
        /// <summary>
        /// Alphanumeric Field
        /// </summary>
        Alpha,

        /// <summary>
        /// Numeric Field
        /// </summary>
        Numeric
    }

    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
    public class ExportFieldAttribute : Attribute
    {
        private readonly int index;

        private readonly int length;

        private readonly ExportFieldType fieldType;

        private readonly int precision;

        private readonly bool optionalField;

        private readonly int fromPos;

        private readonly int toPos;

        /// <summary>
        /// Initializes a new instance of the <see cref="ExportFieldAttribute"/> class.
        /// </summary>
        /// <param name="index">The index.</param>
        /// <param name="fieldType">Type of the field.</param>
        /// <param name="length">The length.</param>
        /// <param name="precision">The precision.</param>
        /// <param name="optionalField">if set to <c>true</c> [optional field].</param>
        /// <param name="fromPos">From pos.</param>
        /// <param name="toPos">To pos.</param>
        public ExportFieldAttribute(int index, ExportFieldType fieldType, int length, int precision, bool optionalField = false, int fromPos = -1, int toPos = -1)
        {
            this.index = index;
            this.fieldType = fieldType;
            this.length = length;
            this.precision = precision;
            this.optionalField = optionalField;
            this.fromPos = fromPos;
            this.toPos = toPos;
        }

        public int Index
        {
            get
            {
                return this.index;
            }
        }

        public ExportFieldType FieldType
        {
            get
            {
                return this.fieldType;
            }
        }

        public int Length
        {
            get
            {
                return this.length;
            }
        }

        public int Precision
        {
            get
            {
                return this.precision;
            }
        }

        public bool OptionalField
        {
            get
            {
                return this.optionalField;
            }
        }

        public int FromPos
        {
            get
            {
                return this.fromPos;
            }
        }

        public int ToPos
        {
            get
            {
                return this.toPos;
            }
        }
    }

Nun kann ich in der Klassendefinition in der ich die Datensatzstruktur der Exportdatei abbilde, dieses Attribut zusätzlich zu den Eigenschaften der Klasse verwenden um die für den korrekten Export benötigen Informationen anzuheften.

Die Klasse könnte so aussehen:

    public class Ksta
    {
        [ExportField(0, ExportFieldType.Alpha, 5, 0)]
        public string Satza
        {
            get
            {
                return "KSTA_";
            }
        }

        [ExportField(1, ExportFieldType.Numeric, 2, 0)]
        public int Gsber
        {
            get
            {
                return 1;
            }
        }

        [ExportField(2, ExportFieldType.Numeric, 8, 0)]
        public int Kvkda { get; set; }

        [ExportField(3, ExportFieldType.Numeric, 8, 0)]
        public int Dtkda { get; set; }

        [ExportField(4, ExportFieldType.Numeric, 5, 0)]
        public int Kvkvn { get; set; }

        [ExportField(5, ExportFieldType.Numeric, 5, 0)]
        public int Ssbpa { get; set; }

        [ExportField(6, ExportFieldType.Numeric, 8, 3)]
        public int Sskda { get; set; }
    }

Wie aber können wir diese zusätzlichen Attribute verwenden?

Ich möchte dies an einem einfachen Beispiel demonstrieren. Hierzu erweitere ich die Klasse ExportFieldAttribute um folgende Methode:

public static string ExportFieldToString(object obj, bool sorted = true)
{
	string buffer = string.Empty;
	PropertyInfo[] pi = obj.GetType().GetProperties();
	if (sorted)
	{
		var propertyInfoSorted = new PropertyInfo[pi.Length];
		foreach (var propertyInfo in pi)
		{
			var attribs = (ExportFieldAttribute[])propertyInfo.GetCustomAttributes(typeof(ExportFieldAttribute), true);
			if (attribs.Length > 0)
			{
				propertyInfoSorted[attribs[0].Index] = propertyInfo;
			}
		}

		pi = propertyInfoSorted;
	}

	foreach (var propertyInfo in pi)
	{
		if (propertyInfo == null)
		{
			continue;
		}

		var attribs = (ExportFieldAttribute[])propertyInfo.GetCustomAttributes(typeof(ExportFieldAttribute), true);

		if (attribs.Length > 0)
		{
			ExportFieldAttribute attrib = attribs[0];
			var o = propertyInfo.GetValue(obj, null);
			if (o == null)
			{
				o = string.Empty;
			}

			if (attrib.FieldType == ExportFieldType.Alpha)
			{
				string t = string.Format(o.ToString().PadRight(attrib.Length));
				buffer += t;
			}

			if (attrib.FieldType == ExportFieldType.Numeric)
			{
				if (attrib.Precision > 0)
				{
					string concat;
					var buf = o.ToString().Split(',');
					if (buf.Length > 1)
					{
						buf[1] = buf[1].PadRight(attrib.Precision, '0').Substring(0, attrib.Precision);
						concat = buf[0] + buf[1];
					}
					else
					{
						concat = buf[0] + string.Empty.PadRight(attrib.Precision, '0');
					}
					string t = string.Format(concat.PadLeft(attrib.Length, '0'));
					buffer += t;
				}
				else
				{
					string t = string.Format(o.ToString().PadLeft(attrib.Length, '0'));
					buffer += t;
				}
			}
		}
	}
	return buffer;
}

Ich möchte an dieser Stelle nun auch keine aufwändige Erklärung der Methode vornehmen, ich denke dass derjenige der sich mit diesem Problem beschäftigt erkennen wird was darin geschieht.

Und um diese Methode zu verwenden, also um den Satzaufbau der Daten so zu erhalten wie ich ihn mit den Attributen definiert habe, überschreibe ich einfach die ToString() Methode der Klasse in welcher ich die Datenstruktur abgebildet habe.

Das sieht dann so aus (In der Klasse Ksta):

        public override string ToString()
        {
            return ExportFieldAttribute.ExportFieldToString(this);
        }

Wenn ich nun eine Instanz der Klasse Ksta erstelle, diese dann mit Daten fülle und anschließen die ToString Methode aufrufe erhalten ich die Daten genau in der definierten Struktur.

Wenn jemand noch Fragen zu dem Thema hat, oder doch noch weiterführende Erläuterungen benötigt, dann einfach per Kommentar die Fragen oder Anregungen stellen.

2 Gedanken zu „ASCII Schnittstellen mit Hilfe von Custom Attributes komfortabel erstellen – C#“

  1. Hallo HP,
    schöner Beitrag. Die Custom Attributes sind schon recht clever für die Aufgabenstellung.

    Wer eine schnelle Lösung zum Ein/Auslesen sucht sollte sich auch mal den TextFieldParser anschauen (Microsoft.VisualBasic.FileIO.TextFieldParser). Den kann man natürlich auch in C# nutzen.

    Using Reader As New Microsoft.VisualBasic.FileIO.TextFieldParser(fileName, System.Text.Encoding.GetEncoding(850))

    http://msdn.microsoft.com/de-de/library/microsoft.visualbasic.fileio.textfieldparser.aspx

    Hast Du schon mal überlegt Deinen Ansatz in ein T4 Template zu giessen?
    http://www.computerworld.ch/businesspraxis/developerworld/artikel/visual-studio-2010-codegenerierung-ueber-templates-52589

    Viele Grüße,
    Thomas Sczyrba

    1. Hallo Thomas,
      in dem konkreten Zusammenhang habe ich noch nicht über T4 nachgedacht.
      Muss gestehen, dass mir aber auch noch nicht langweilig war :-).
      Will damit sagen, dass dafür auch noch keine Zeit übrig war.

      Man kann es ja mal auf die „HättIchGerneMalGemachtListe“ setzen, oder eventuell greift jemand mal das Thema des Beitrags auf und setzt das in einem T4 Template um.

      Würde mich dann auch freuen 🙂 einen Link dazu hier im Beitrag als Kommentar zu finden.

      Beste Grüße
      HP

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.