Custom aggregate function C# PivotData Examples


This article explains how to define and use custom summary function with NReco.PivotData OLAP library.

Aggregator interfaces

PivotData library defines 2 interfaces for incapsulating summary function logic:

  • IAggregator: represents accumulator: holds current value + how to calculate it by input data
  • IAggregatorFactory: represents a factory for creating IAggregator instances

Aggregator factory should implement Create method with 2 overloads:

  • IAggregator Create(): creates 'empty' aggregator
  • IAggregator Create(object state): creates aggregator initialized with specified state object

Aggregator should implement the following methods/properties:

  • void Push(object v, Func<Object, string, Object> getValue): accumulate input data. Field values may be accessed with help of getValue delegate.
  • void Merge(IAggregator aggr): include accumulated data from specified aggregator. This operation should be defined only for aggregators of the same type.
  • object GetState(): get serializable structure that represents accumulated data.
  • uint Count { get; }: number of input values accumulated by the aggregator.
  • object Value { get; }: object that represents accumulated value (in most cases this is a number - integer or decimal).

Implementation example sum function

SumAggregator -- incapsulates summary function logic (how to accumulate values, how to merge 2 aggregators):
public class SumAggregator : IAggregator {
	decimal total = 0; // default value for empty aggregator
	uint count = 0; // default value for empty aggregator
	string field;

	public SumAggregator(string f) {
		field = f;
	}
	public SumAggregator(string f, object state) : this(f) {
		// this constructor is used in SumAggregatorFactory.Create(object)
		var stateArr = state as object[];
		if (stateArr==null || stateArr.Length!=2)
			throw new InvalidOperationException("invalid state");
		count = Convert.ToUInt32(stateArr[0]);
		total = Convert.ToDecimal(stateArr[1]);
	}
	public void Push(object r, Func<object,string,object> getValue) {
		var v = ConvertHelper.ConvertToDecimal(getValue(r,field), Decimal.MinValue);
		if (v!=Decimal.MinValue) {
			// accumulate new value
			total += v;
			count++;
		}
	}
	public object Value { get { return total; } }
	public uint Count { get { return count; } }
	public void Merge(IAggregator aggr) {
		var sumAggr = aggr as SumAggregator;
		if (sumAggr==null)
			throw new ArgumentException("aggr");
		// combine accumulated values from 2 aggregators
		count += sumAggr.count;
		total += sumAggr.total;
	}
	public object GetState() {
		// aggregator state: compact serializable structure
		return new object[]{count, total};
	}
}
SumAggregatorFactory -- implementation is rather trivial:
public class SumAggregatorFactory : IAggregatorFactory {
	public string Field { get; private set; }

	public SumAggregatorFactory(string field) {
		Field = field;
	}
	public IAggregator Create() {
		return new SumAggregator(Field);
	}
	public IAggregator Create(object state) {
		return new SumAggregator(Field, state);
	}
	public override bool Equals(object obj) {
		var aggrFactory = obj as SumAggregatorFactory;
		if (aggrFactory==null)
			return false;
		return aggrFactory.fld==fld;
	}
	public override string ToString() {
		return String.Format("Sum of {0}", Field);
	}
}
That's all.

Register custom aggregator in PivotDataFactory

If you want to use your custom aggregator in reports created with web pivot builder (ToolkitPivotBuilderMvc example) it should be registered in PivotDataFactory component with RegisterAggregator method:

var pivotDataFactory = new PivotDataFactory();
pivotDataFactory.RegisterAggregator(
	// unique aggregator name across the factory
	"Sum",  

	// type of aggregator factory component
	typeof(SumAggregatorFactory),

	// delegate that creates aggr factory instance
	(aggrType, aggrParams) => new SumAggregatorFactory((string)aggrParams[0]), 

	// delegate that returns aggr factory parameters
	(aggr) => new object[] { ((SumAggregator)aggr).Field }
);