-
-
Notifications
You must be signed in to change notification settings - Fork 17
Source generator
The source generator is useful for a typical use case where you would want to create rows in a worksheet based on a C# class (or struct). With this approach, properties of the type are mapped to row cells. This mapping code can be typed by hand, but there are some of the advantages of using the source generator:
- The mapping code is generated automatically, saving you some key strokes. It also means that later changes to the C# type automatically updates the mapping code.
- The generated mapping code can in some cases have better performance compared to manually typed mapping code. As one example, the generated code will use
ArrayPool<T>
to avoid some allocations.
Since the code is generated during compile-time, there is no additional penalty to runtime performance, as can be the case for e.g. reflection-based implementations.
If you are using Visual Studio, you need to use Visual Studio 2022 version 17.9 or later. The C# version used in your project must also be 8.0 or greater.
First of all, the source generator needs to know which C# type to generate code for, and it needs a class on which to put the generated code. Both are handled by declaring a context class.
In the examples we will create rows for this C# class:
namespace MyNamespace;
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string? MiddleName { get; set; }
}
The context class must:
- Have the
partial
modifier. - Inherit
WorksheetRowContext
. - Have the
WorksheetRow
attribute with a type parameter.
Here is a context class for Person
:
using SpreadCheetah.SourceGeneration;
namespace MyNamespace;
[WorksheetRow(typeof(Person))]
public partial class PersonContext : WorksheetRowContext;
Once this has been created, the source generator should automatically generate the implementation of PersonContext
. The implementation will be created in another file in the same project. If you're curious, you should be able to see into that file by navigating to declarations of the context class in your IDE.
We are now ready to create worksheet rows. To do so we will call AddAsRowAsync
and pass an instance of Person
as well as the metadata type generated by the source generator. Here is an example:
await using var spreadsheet = await Spreadsheet.CreateNewAsync(stream);
await spreadsheet.StartWorksheetAsync("Sheet 1");
var person = new Person
{
FirstName = "Ola",
LastName = "Nordmann"
};
// This will use the code generated by the source generator
await spreadsheet.AddAsRowAsync(person, PersonContext.Default.Person);
await spreadsheet.FinishAsync();
If we have multiple instances of Person
, we can also use Spreadsheet.AddRangeAsRowsAsync
which accepts and IEnumerable<T>
.
By default, the column order is determined by the property order in the C# type. The column order can be changed by reordering the properties, or by using the ColumnOrder
attribute.
The ColumnOrder
attribute can be placed on one or more of the properties. The properties are then ordered by the attribute values in ascending order. In the following example, the column order becomes FirstName
, MiddleName
, LastName
:
public class Person
{
[ColumnOrder(3)]
public string LastName { get; set; }
[ColumnOrder(1)]
public string FirstName { get; set; }
[ColumnOrder(2)]
public string? MiddleName { get; set; }
}
The column order values decides the ordering between the columns, and the values don't need to match the actual column numbers in the worksheet. Two properties can not have the same column order value, and attempting to have that will lead to a build error.
Properties without the attribute will get a column order value implicitly. The implicit value will be the lowest number greater than 0 that has not been used on any other column.
The following snippet shows an example which will create the same column order as in the previous example (FirstName
, MiddleName
, LastName
):
public class Person
{
[ColumnOrder(10)]
public string LastName { get; set; }
[ColumnOrder(1)]
public string FirstName { get; set; }
public string? MiddleName { get; set; }
}
Here the MiddleName
property implicitly gets a column order value of 2. Note that LastName
will be in the third column, since column order values just decides the ordering, and not the actual column number.
A common use case is to first create a header row before creating rows with values. The source generator can be used to generate the header names as well. Spreadsheet
has an AddHeaderRowAsync
method that will add a row of header names to the worksheet. Similarly to AddAsRowAsync
, the generated metadata type must be passed as an argument. An optional StyleId
can also be passed as the second argument.
Here is an example:
await using var spreadsheet = await Spreadsheet.CreateNewAsync(stream);
await spreadsheet.StartWorksheetAsync("Sheet 1");
var style = new Style { Font = { Bold = true } };
var styleId = spreadsheet.AddStyle(style);
// This adds a row of header names generated by the source generator
await spreadsheet.AddHeaderRowAsync(PersonContext.Default.Person, styleId);
await spreadsheet.FinishAsync();
By default, the generated header names are based on the property names. You can use the ColumnHeader
attribute to set your own header names.
Here is an example of that:
public class Person
{
public string LastName { get; set; }
[ColumnHeader("First name")]
public string FirstName { get; set; }
public string? MiddleName { get; set; }
}
You can also reference resource (.resx
) files. For example if we have MyResources.resx
containing key Header_FirstName
, then we can use it like this:
public class Person
{
public string LastName { get; set; }
[ColumnHeader(typeof(MyResources), nameof(MyResources.Header_FirstName)]
public string FirstName { get; set; }
public string? MiddleName { get; set; }
}
If there are multiple translations for the resource, then similarly to a call to MyResources.Header_FirstName
directly, the current culture will determine which translation gets used. Note that the resource file must have a public access modifier. This feature is not just limited to resource files, and can also similarly reference other public static properties.
To have style on a cell when using the source generator, you need to create a named style on Spreadsheet
, and then reference the style name in the CellStyle
attribute. The named style must be created before the first call to AddAsRowAsync
.
Here is an example of how to get the last name of Person
in bold:
public class Person
{
[CellStyle("My bold style")]
public string LastName { get; set; }
public string FirstName { get; set; }
public string? MiddleName { get; set; }
}
await using var spreadsheet = await Spreadsheet.CreateNewAsync(stream);
await spreadsheet.StartWorksheetAsync("Sheet 1");
var style = new Style { Font = { Bold = true } };
// This adds a named style
spreadsheet.AddStyle(style, "My bold style");
var person = new Person
{
FirstName = "Ola",
LastName = "Nordmann"
};
await spreadsheet.AddAsRowAsync(person, PersonContext.Default.Person);
await spreadsheet.FinishAsync();
If the named style hadn't been added to Spreadsheet
in the example above, then an exception would be thrown.
If you need more flexibility in how property values are mapped to cells, then you can create an implementation of CellValueConverter<T>
which can be used by the generated code. CellValueConverter<T>
is an abstract class and derived classes must implement a method that takes the property value as input, and returns a DataCell
. To instruct the source generator to use your implementation, the property must have the CellValueConverter
attribute with a typeof
operator.
Here is an example where we will show N/A
for a person when the age is null
:
public class Person
{
public string LastName { get; set; }
public string FirstName { get; set; }
[CellValueConverter(typeof(AgeConverter))]
public int? Age { get; set; }
}
public class AgeConverter : CellValueConverter<int?>
{
public override DataCell ConvertToDataCell(int? age)
{
return age is null
? new DataCell("N/A")
: new DataCell(age.Value);
}
}