Click here to Skip to main content
15,900,725 members
Articles / Programming Languages / C#
Article

Handling Fixed-width Flat Files with .NET Custom Attributes

Rate me:
Please Sign up or sign in to vote.
4.78/5 (12 votes)
28 Oct 2006CPOL2 min read 76.5K   1.5K   32   13
Handling Fixed width flat files with .NET custom attributes

Introduction

This article shows how to handle flat files containing fixed width datafields, using .NET custom attributes. It doesn't explain custom attributes, because I think there has already been enough written on that subject.

Background

How many times did you have to exchange data coming from or going to another environment like a mainframe? Many projects I have worked on needed to import or export datafiles in a fixed width format. The code handling these files must be flexible enough to handle changes in the file format, that's why I created the code described in this article.

Using the Code

Suppose your code needs to import a file containing customers with the following layout :

C#
Id        PIC(9)
Code    PIC(1)
Name   PIC(20)
.....

To read the data from the file, you create a customer class that must derive from a provided base class called StringLayoutUtility. This base class has two important methods : Parse() and ToString(). These two methods will iterate over each property of (your) the (derived) class, having a StringLayoutAttribute defined. With the position information it will find in the StringLayoutAttribute, it will know how to parse or build the string. So on each property, you assign a StringLayoutAttribute, together with two arguments: the begin and end position of the property in the input-string as shown below:

C#
[StringLayoutAttribute(0, 8)]
public string Id
{
   get { return _Id; }
   set { _Id = value; }
}

The Id field starts at position 0 and ends at position 8, meaning the Id field is 9 characters long.

  "012345678ACustomerxxx......."
The StringLayoutUtility class also has a virtual method IsValid() that can be used to do some business validation after the parsing.
C#
public override bool IsValid()
{ 
   // add your validation logic here
   return (!String.IsNullOrEmpty(this.Id) && !String.IsNullOrEmpty(this.Name));
}

Usage

The first code sample provided shows how a string is parsed, and is later compared with the result of the ToString() method:

C#
// this simulates input from a FlatFileReader
string fileLineIn = "123456789ADepoorter Stephan WhateverStreet 456AB1000 Brussels" +
    "Belgium";
Customer customer = new Customer();
customer.Parse(fileLineIn);
DisplayCustomer(customer);
string fileLineOut = customer.ToString();
Console.WriteLine("customer.Parse() " + 
                         ((fileLineIn == fileLineOut.TrimEnd()) ? "==" : "!=") + 
                         " customer.ToString() !" );

The second code sample shows the reading of a file customers.txt, using the FlatFileReader:

C#
// Sample using the FlatFileReader:
FlatFileReader file = new FlatFileReader("customers.txt", Encoding.GetEncoding(
    "iso-8859-1"));
while (file.ParseLine())
{
   customer.Parse(file.CurrentLine);
   DisplayCustomer(customer);
}

Base Class

C#
public void Parse(string input)
{
  if(!String.IsNullOrEmpty(input ) )
  {
    foreach (PropertyInfo property in GetType().GetProperties())
    {
      foreach (Attribute attribute in property.GetCustomAttributes(true))
      {
        StringLayoutAttribute stringLayoutAttribute = attribute as StringLayoutAttribute;
        if (null != stringLayoutAttribute)
        {
          string tmp = string.Empty;
          if (stringLayoutAttribute.StartPosition <= input.Length - 1)
          {
            tmp = input.Substring(stringLayoutAttribute.StartPosition, 
                Math.Min((
                stringLayoutAttribute.EndPosition - stringLayoutAttribute.StartPosition +
                1), input.Length - stringLayoutAttribute.StartPosition));
          }
          switch (_trimInput)
          { 
            case TrimInputMode.Trim :
               tmp = tmp.Trim();
               break;
            case TrimInputMode.TrimStart:
               tmp = tmp.TrimStart();
               break;
            case TrimInputMode.TrimEnd:
               tmp = tmp.TrimEnd();
               break;
          }
          property.SetValue(this, tmp, null);
          break;
        }
      }
    }
  }
}

public override string ToString()
{
  string result = string.Empty;
  foreach (PropertyInfo property in GetType().GetProperties())
  {
    foreach (Attribute attribute in property.GetCustomAttributes(false))
    {
      StringLayoutAttribute stringLayoutAttribute = attribute as StringLayoutAttribute;
      if (null != stringLayoutAttribute)
      {
        string propertyValue = (string)property.GetValue(this, null);
        if (stringLayoutAttribute.StartPosition > 0 && result.Length < 
          stringLayoutAttribute.StartPosition)
          result = result.PadRight(stringLayoutAttribute.StartPosition, _paddingChar);
        
        string left = string.Empty;
        string right = string.Empty;
    
        if (stringLayoutAttribute.StartPosition > 0)
          left = result.Substring(0, stringLayoutAttribute.StartPosition);
        if (result.Length > stringLayoutAttribute.EndPosition + 1)
          right = result.Substring(stringLayoutAttribute.EndPosition + 1);
        if (propertyValue.Length < stringLayoutAttribute.EndPosition - 
            stringLayoutAttribute.StartPosition + 1)
        {        
           propertyValue = propertyValue.PadRight(stringLayoutAttribute.EndPosition - 
            stringLayoutAttribute.StartPosition + 1, _paddingChar);
        }
        result = left + propertyValue + right;
      }
      break;
    }
  }
  return result;
}

ClassDiagram

Extensibility

The sample shown was kept very basic on purpose. Often you will need to parse a part of the input data fields, and then depending on what you have read in the first part, parse the rest of the line differently. This can be handled easily, by using base classes to read the first part, and pass these to a factory that will instantiate an appropriate class that will parse the remaining fields of the input line.

Conclusion

I hope to have shown a simple and flexible implementation to read and create files containing fixed width fields, using .NET custom attributes.

History

  • October, 2006: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Web Developer
Belgium Belgium
Stephan works as a .NET solution archtect at Euricom, one of Belgiums leading .NET companies.

Comments and Discussions

 
GeneralGood article Pin
amitraipopli26-May-10 16:49
amitraipopli26-May-10 16:49 
GeneralRe: Good article Pin
Stephan Depoorter26-May-10 21:10
Stephan Depoorter26-May-10 21:10 
GeneralRe: Good article Pin
amitraipopli27-May-10 7:49
amitraipopli27-May-10 7:49 
GeneralGreat article!!! Pin
Member 6632195-Jan-10 5:53
Member 6632195-Jan-10 5:53 
Questionlooking for the code? Pin
mikec7-Dec-07 8:59
mikec7-Dec-07 8:59 
AnswerRe: looking for the code? Pin
Stephan Depoorter22-Apr-08 10:26
Stephan Depoorter22-Apr-08 10:26 
QuestionLooking for the code? Pin
mikec7-Dec-07 8:47
mikec7-Dec-07 8:47 
GeneralExcellent Article, need one now for a comma delimited file! Pin
v1rich12-Jul-07 8:17
v1rich12-Jul-07 8:17 
AnswerRe: Excellent Article, need one now for a comma delimited file! Pin
Stephan Depoorter13-Jul-07 11:53
Stephan Depoorter13-Jul-07 11:53 
GeneralNice Utility Pin
wvh29-Oct-06 22:06
wvh29-Oct-06 22:06 
GeneralNice Article Pin
Marcos Meli29-Oct-06 12:17
Marcos Meli29-Oct-06 12:17 
GeneralGreat Article Pin
Phillip M. Hoff28-Oct-06 20:55
Phillip M. Hoff28-Oct-06 20:55 
This is a great use of custom attributes. I've solved a very similar problem (automatic deserialization of objects from delimited strings) using a similar method. I set things up a little bit differently, though. Rather than requiring data model classes to derive from a base class which contains the file parsing logic (which has implications when dealing with remotable and non-remotable objects or multiple forms of deserialization), I use dedicated parser classes with an optional validation interface. Following your example, it would look something like this:

<code>
interface IValidatable
{
bool Validate();
}

class Customer : IValidatable
{
private string _id;

[StringLayout(0, 8)]
public string Id
{
get { return _id; }
set { _id = value; }
}

public Customer()
{
}

public bool Validate()
{
return ...;
}
}

static class StringParser
{
public static T Read<T>(string input) where T : new()
{
T t = new T();

// Walk through properties of T and extract values from input as directed by attributes.

IValidatable val = t as IValidatable; // See if T supports validation.

if ((val != null) && !val.Validate())
{
throw new ArgumentException("The input is not a valid Customer string.");
}

return t;
}

public static string Write<T>(T obj)
{
StringBuilder builder = new StringBuilder();

// Walk through properties of obj and arrange output as directed by attributes.

return builder.ToString();
}
}

static void Main()
{
Customer customer = StringParser.Read<Customer>(@"SomeStringOfCustomerData");

Console.WriteLine("Customer ID: {0}", customer.Id);
}
</code>

The example assumes the use of .NET 2.0, but the same design works with 1.x, if a little less elegantly. In that case you'd define the StringParser method like:

public static object Read(Type t, string input) { ... }

and use it like:

Customer customer = (Customer) StringParser.Read(typeof(Customer), @"SomeInputData");

It's always interesting to see how two different people come up with solutions to the same problem. Again, great article.

-Phil

GeneralRe: Great Article Pin
Stephan Depoorter29-Oct-06 9:25
Stephan Depoorter29-Oct-06 9:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.