using System.Collections.Generic;
public interface IFileChunk
public string? ToHeaderChunk();
public string ToFileChunk();
public abstract record FileChunk : IFileChunk
public string? Header { get; init; } = null;
public string? ToHeaderChunk() => Header;
public virtual string ToFileChunk()
return QuoteIfNecessary(ToString());
public abstract override string ToString();
protected static bool NeedsQuotes(string rawValue)
var needsQuotes = new char[] { ',', '|', '\'', '\"', '\r', '\n' };
return rawValue.Any(c => needsQuotes.Contains(c));
protected static string Quote(string rawValue)
var valueWithExistingQuotesDoubled = rawValue.Replace("\"", "\"\"");
return string.Format("\"{0}\"", valueWithExistingQuotesDoubled);
protected static string QuoteIfNecessary(string rawValue)
return NeedsQuotes(rawValue) ?
public record FileChunk<T>(
public FileChunk(T? val, string? header = null)
public override string ToString() => Value?.ToString() ?? string.Empty;
public record StringChunk(
) : FileChunk<string>(Value)
public StringChunk(string? val, string? header = null)
public record CurrencyChunk(
) : FileChunk<decimal>(Value)
public CurrencyChunk(decimal val, string? header = null)
public override string ToString() => $"{Value:0.00}";
public record ShortAddressChunk(
) : FileChunkGroup(Line1, Line2, Line3, Country);
public record LongAddressChunk(
) : FileChunkGroup(Line1, Line2, Line3, City, State, Zip, Country);
public record FileChunkGroup : IFileChunk
private char Delim { get; init; } = ',';
private List<IFileChunk> Chunks { get; init; }
public FileChunkGroup(params IFileChunk[] fileChunks)
: this(fileChunks.ToList())
public FileChunkGroup(char delim, params IFileChunk[] fileChunks)
: this(delim, fileChunks.ToList())
public FileChunkGroup(IEnumerable<IFileChunk> fileChunks)
Chunks = fileChunks.ToList();
public FileChunkGroup(char delim, IEnumerable<IFileChunk> fileChunks)
public string? ToHeaderChunk()
var headerChunks = Chunks.Select(fc => fc.ToHeaderChunk());
|| !headerChunks.All(hc => !string.IsNullOrWhiteSpace(hc))
return string.Join(Delim, headerChunks);
public string ToFileChunk()
var chunks = Chunks.Select(fc => fc.ToFileChunk());
return string.Join(Delim, chunks);
public abstract record FixedSizeFileChunk<TChunk> : FileChunk<TChunk>
protected int Size { get; init; }
public FixedSizeFileChunk(int size)
throw new ArgumentOutOfRangeException(nameof(size));
public override string ToFileChunk()
var chunk = ToString().Crop(Size);
throw new FormatException($@"
Cannot use value '{ToString()}' as type {nameof(FixedSizeFileChunk<TChunk>)} of size {Size}:
That value needs quotes but the size limit is too small fit quotes into.
chunk = Quote(chunk.Crop(Size - 2));
public class WholeFileChunk : IFileChunk
private string? Header { get; init; }
public List<FileChunkGroup> Records { get; init; }
public WholeFileChunk(List<FileChunkGroup> records)
Header = records.FirstOrDefault(fcg =>
!string.IsNullOrWhiteSpace(fcg.ToHeaderChunk())
public string? ToHeaderChunk() => Header;
public string ToFileChunk()
var sb = new StringBuilder();
var headerChunk = ToHeaderChunk();
if (!string.IsNullOrWhiteSpace(headerChunk))
sb.AppendLine(headerChunk);
foreach (var chunkGroup in Records)
sb.AppendLine(chunkGroup.ToFileChunk());
public static class StringExtensions
public static string Crop(this string input, int length)
return (input.Length > length) ? input.Remove(length) : input;
[AttributeUsage(AttributeTargets.Property)]
public class DetailFieldAttribute : Attribute
public int Index { get; init; }
public Type ChunkType { get; init; }
public string Header { get; init; }
public DetailFieldAttribute(int index, Type chunkType, string header)
[AttributeUsage(AttributeTargets.Property)]
public class StringFieldAttribute : DetailFieldAttribute
public StringFieldAttribute(int index, string header)
: base(index, typeof(StringChunk), header)
[AttributeUsage(AttributeTargets.Property)]
public class CurrencyFieldAttribute : DetailFieldAttribute
public CurrencyFieldAttribute(int index, string header)
: base(index, typeof(CurrencyChunk), header)
public Guid PaymentId { get; set; }
public string RecipientACCTNumber { get; set; }
public string? RecipientName { get; set; }
public string? RecipientAddress1 { get; set; }
public string? RecipientAddress2 { get; set; }
public string? RecipientAddress3 { get; set; }
public string? RecipientCountry { get; set; }
public string BeneficiaryName { get; set; }
public string BeneficiarySWIFT { get; set; }
public string BeneficiaryIBAN { get; set; }
public decimal Amount { get; set; }
RecipientACCTNumber = acct ?? string.Empty;
RecipientAddress1 = addr1;
RecipientAddress2 = addr2;
RecipientAddress3 = addr3;
RecipientCountry = country;
BeneficiaryName = benName;
BeneficiarySWIFT = benSwift ?? string.Empty;
BeneficiaryIBAN = benIban ?? string.Empty;
[StringField(0, "recipient_account")]
public string RecipientACCTNumber { get; set; }
[StringField(1, "recipient_name")]
public string? RecipientName { get; set; }
[StringField(2, "recipient_addr_1")]
public string? RecipientAddress1 { get; set; }
[StringField(3, "recipient_addr_2")]
public string? RecipientAddress2 { get; set; }
[StringField(4, "recipient_addr_3")]
public string? RecipientAddress3 { get; set; }
[StringField(5, "recipient_country")]
public string? RecipientCountry { get; set; }
[StringField(6, "beneficiary_name")]
public string BeneficiaryName { get; set; }
[StringField(7, "beneficiary_swift_bic")]
public string BeneficiarySWIFT { get; set; }
[StringField(8, "beneficiary_iban")]
public string BeneficiaryIBAN { get; set; }
[CurrencyField(9, "amount")]
public decimal Amount { get; set; }
public Guid PaymentId { get; set; }
PaymentId = dto.PaymentId;
RecipientACCTNumber = dto.RecipientACCTNumber;
RecipientName = dto.RecipientName;
RecipientAddress1 = dto.RecipientAddress1;
RecipientAddress2 = dto.RecipientAddress2;
RecipientAddress3 = dto.RecipientAddress3;
RecipientCountry = dto.RecipientCountry;
BeneficiaryName = dto.BeneficiaryName;
BeneficiarySWIFT = dto.BeneficiarySWIFT;
BeneficiaryIBAN = dto.BeneficiaryIBAN;
var providesAcct = !string.IsNullOrWhiteSpace(dto.RecipientACCTNumber);
var providesIban = !string.IsNullOrWhiteSpace(dto.BeneficiaryIBAN);
if (!(providesAcct ^ providesIban))
throw new InvalidOperationException(
"Must provide either an acct or benIban but not both."
if (string.IsNullOrWhiteSpace(dto.BeneficiarySWIFT))
throw new InvalidOperationException(
"Must provide a swift number."
public FileChunkGroup ToFileChunkGroup()
var chunks = new List<IFileChunk>();
from p in GetType().GetProperties()
let attr = p.GetCustomAttributes(typeof(DetailFieldAttribute), false).First() as DetailFieldAttribute
var chunk = (IFileChunk)Activator.CreateInstance(g.Attr.ChunkType, g.Prop.GetValue(this), g.Attr.Header)!;
return new FileChunkGroup(chunks);
public class FileContainer
public List<Detail> Details { get; init; }
public FileContainer(List<DTO> details)
Details = details.Select(detail => new Detail(detail)).ToList();
public string CreateFile()
var file = new WholeFileChunk(Details.Select(d => d.ToFileChunkGroup()).ToList());
return file.ToFileChunk();
public static void Main()
var dtos = new List<DTO>()
paymentId: Guid.NewGuid(),
paymentId: Guid.NewGuid(),
var file = new FileContainer(dtos);
Console.WriteLine(file.CreateFile());