RagnaRock
RagnaRock

Reputation: 2640

Can I access a static member of abstract class in a generic?

I'm trying to create a generic data exporter, where I can feed a bunch of rows and this can then get exported as excel, csv, etc...

To make it quick to specify I was planning on having the "headers" of the report specified on the same file as the rows, using static fields.

But this mess of having abstract classes with static members and the attempt of ussing them in a generic class is messy.

Is there a clean way to do this?

my attempt was having an abstract class with the base "interface" and a common method

public abstract class ReportRow
{
  public static readonly string[] ColumnNames;

  public static int ColumnCount => ColumnNames.Length;

  public string GetColumnValueAsString(int index)
  {
    var value = GetColumnValue(index);
    if (value is DateTime)
    {
      return value.ToString("dd/MM/yyyy");
    }

    return value.ToString();
  }
}

then I have a couple of class implementations, I leave this one as an example:

public class OrdersReportRow : ReportRow
{
  public new static readonly string[] ColumnNames =
  {
    "Date", "StoreId", "StoreName", "SkuId", "Quantity"
  };
  
  public DateTime Date { get; set; }
  public int StoreId { get; set; }
  public string StoreName { get; set; }
  public int SkuId { get; set; }
  public decimal Quantity { get; set; }
  
  public new dynamic GetColumnValue(int index)
  {
     return index switch
     {
       0 => Date,
       1 => StoreId,
       2 => StoreName,
       3 => SkuId,
       4 => Quantity,
       _ => throw new IndexOutOfRangeException(),
     };
  }
}

And the exporter class I wanted to be something like this:

public class ReportExporter<TReportRowType> where TReportRowType : ReportRow
{
  public void Export(IEnumerable<TReportRowType> reportEntries)
  {
    //code to add headers
    for (int i = 0; i < TReportRowType.ColumnCount; i++) // fails here because ColumnCount is not "visible"
    {
      AddColumn(TReportRowType.ColumnNames[i]); // also fails   
    }
    
    foreach(var entry in reportEntries) {
      for (int i = 0; i < TReportRowType.ColumnCount; i++) // fails here because ColumnCount is not "visible"
      {
        AddValue(entry.GetColumnValue(i));
        CommitLine()        
      }
    }
  }
}

Upvotes: 0

Views: 101

Answers (2)

golakwer
golakwer

Reputation: 373

Those column names should be on a collection or rows level in my opinion. So that I would try something like this.

Note that the column names are automatically populated using reflection.

public abstract class ReportRow
{
    public abstract dynamic GetColumnValue(int index);

    public string GetColumnValueAsString(int index)
    {
        var value = GetColumnValue(index);
        if (value is DateTime)
        {
        return value.ToString("dd/MM/yyyy");
        }

        return value.ToString();
    }
}

public abstract class ReportRows<T> : IEnumerable<T> where T : ReportRow
{
    public string[] ColumnNames { get => _cols.ToArray();  }
    public int ColumnCount { get => _cols.Count; }

    List<string> _cols;
    List<T> _rows;

    public ReportRows() {
        _rows = new List<T>();

        _cols = new List<string>();
        var tt = typeof(T);
        foreach(var p in tt.GetProperties()) {
            _cols.Add(p.Name);
        }
    }

    public void Add(T row) => _rows.Add(row);

    public IEnumerator<T> GetEnumerator() => _rows.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

public class OrdersReportRow : ReportRow
{
    public DateTime Date { get; set; }
    public int StoreId { get; set; }
    public string StoreName { get; set; }
    public int SkuId { get; set; }
    public decimal Quantity { get; set; }
    
    public override dynamic GetColumnValue(int index)
    {
        return index switch
        {
        0 => Date,
        1 => StoreId,
        2 => StoreName,
        3 => SkuId,
        4 => Quantity,
        _ => throw new IndexOutOfRangeException(),
        };
    }
}

public class OrdersReportRows : ReportRows<OrdersReportRow> { }

public class ReportExporter
{
    public void Export<T>(ReportRows<T> reportEntries) where T : ReportRow
    {
        //code to add headers
        for (int i = 0; i < reportEntries.ColumnCount; i++) // fails here because ColumnCount is not "visible"
        {
            AddColumn(reportEntries.ColumnNames[i]); // also fails
        }
        
        foreach(var entry in reportEntries) {
            for (int i = 0; i < reportEntries.ColumnCount; i++) // fails here because ColumnCount is not "visible"
            {
                AddValue(entry.GetColumnValue(i));
                CommitLine();
            }
        }
    }

    public void AddColumn(string s) {}
    public void AddValue(object s) {}
    public void CommitLine() {}
}

to call:

var or = new OrdersReportRows();
or.Add(new OrdersReportRow { Date = DateTime.Now });

var re = new ReportExporter();
re.Export(or);

Upvotes: 1

lidqy
lidqy

Reputation: 2453

I'd avoid using "new" for a method/property/field signature, I'd avoid the static public members (in combination with new). You can still use a static private field for implementation, but it's cleaner and easier to make the accessors for that field, i.e. the column names array and column count, non-static and abstract. At least "ColumnNames" should be abstract.

Sth like (untested):

    public abstract class ReportRow
    {
       public abstract string[] ColumnNames { get; }
       public int ColumnCount => ColumnNames.Length;

       public string GetColumnValueAsString(int index)
       {
         //as is
       }
   }

    public class OrdersReportRow : ReportRow
    {
      private static readonly string[] _columnNames =
      {
        "Date", "StoreId", "StoreName", "SkuId", "Quantity"
      };
    
      public override string[] ColumnNames => _columnNames;
    
      //no other changes ...
    }
    
    public class ReportExporter<TReportRowType> where TReportRowType : ReportRow
    {
        public void Export(IEnumerable<TReportRowType> reportEntries)
        {
           var firstEntry = reportEntries.FirstOrDefault();
           if (firstEntry == null) return;
           for (int i = 0; i < firstEntry.ColumnCount; i++) // shouldn't fail any longer, because ColumnCount is now "visible"
           {
             AddColumn(firstEntry.ColumnNames[i]); // also should work
           }
        
        foreach(var entry in reportEntries) {
          for (int i = 0; i < entry.ColumnCount; i++) // shouldn't fail any longer, because ColumnCount is now "visible"
          {
            AddValue(entry.GetColumnValue(i));
            CommitLine()        
          }
        }
      }

Upvotes: 1

Related Questions