Utility Type C# Source Generators, inspired by Typescript
This library is available as a nuget package
dotnet add package SG1.UtilityTypes
NOTE that this library is a work in progress and you should perform your own testing to confirm it is fit for production.
Ever had a case where you have a base model like:
public class Profile
{
public string FirstName { get; set; } = default!;
public string LastName { get; set; } = default!;
public int Age { get; set; }
}
And you need to write an identical class that only takes a partial set of data, like so:
public partial class ProfileUpdate
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public Nullable<int> Age { get; set; }
}
Commonly used for updates, etc.
The Partial
Utility Type can help you!
Instead of manually keeping your update model up to date, you can simply write
[Partial(typeof(Profile))]
public partial class ProfileUpdate { }
and the source generator will do the rest!
Sometimes you need a copy of a class but with only certain properties.
The Pick attribute can be used to transform a given class (let's use Profile
from above), with the following syntax
[Pick(typeof(Profile), nameof(Profile.FirstName))]
public partial class ProfilePickedFirstName { }
will produce a generated source of:
public partial class ProfilePickedFirstName
{
public string FirstName { get; set; } = default!;
}
where FirstName is the only property included on the new class.
Sometimes you need a copy of a class but with a certain property hidden.
The Omit attribute can be used to transform a given class (let's use Profile
from above), with the following syntax
[Omit(typeof(Profile), nameof(Profile.Age))]
public partial class ProfileOmittedAge { }
will produce a generated source of:
public partial class ProfileOmittedAge
{
public string FirstName { get; set; } = default!;
public string LastName { get; set; } = default!;
}
where Age is no longer a property of the type
Sometimes you need a readonly version of a class so that none of the properties can be updated.
The Readonly attribute can be used to transform a given class (let's use Profile
from above), with the following syntax
[Readonly(typeof(Profile))]
public partial class ProfileReadonly { }
will produce a generated source of:
public partial class ProfileReadonly
{
public string FirstName { get; }
public string LastName { get; }
public int Age { get; }
}
where all property setters have been removed. You will need to write constructors in order to initialise the properties.
Sometimes you want to implement a model interface. The ImplementsAttribute can help out with keeping your code DRY.
Given the interface
public interface IProfile
{
string FirstName { get; }
string LastName { get; }
int Age { get; set; }
}
[Implements(typeof(IProfile))]
public partial class ProfileImplementation { }
will produce a generated source of:
public partial class ProfileImplementation
{
public string FirstName { get; set; } = default!;
public string LastName { get; set; } = default!;
public int Age { get; set; }
}
By default, setters will be added. This behaviour can be overridden by adding the IsReadonly = true
parameter to the attribute.
This attribute will allow you to copy properties from a source type with no other transformation applied. For example if I had the Profile class above, as well as this class
public partial class ExtendedProfile
{
public string Bio { get; set; } = default!;
public string Website { get; set; } = default!;
}
I could combine the properties of both
[
PropertiesOf(typeof(Profile)),
PropertiesOf(typeof(ExtendedProfile)),
]
public partial class FullProfile { }
will produce a generated source of:
public partial class ProfileImplementation
{
public string FirstName { get; set; } = default!;
public string LastName { get; set; } = default!;
public int Age { get; set; }
public string Bio { get; set; } = default!;
public string Website { get; set; } = default!;
}
You can easily use these attributes together in order to get greater utility. For example, if I wanted an update model just for the name fields of the profile above, I can do:
[
Partial(typeof(Profile)),
Pick(typeof(Profile), nameof(Profile.FirstName), nameof(Profile.LastName))
]
public partial class ProfileNamesUpdateModel { }
And I will get
public partial class ProfileNamesUpdateModel
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
}
I can even combine properties from two models together, for example if I had the second class
public partial class ExtendedProfile
{
public string Bio { get; set; } = default!;
public string Website { get; set; } = default!;
}
I can make a unified update model by having:
[
Partial(typeof(Profile)),
Partial(typeof(ExtendedProfile))
]
public partial class FullProfileUpdate { }
which will produce
public partial class ProfileNamesUpdateModel
{
public string? FirstName { get; set; }
public string? LastName { get; set; }
public string? Bio { get; set; }
public string? Website { get; set; }
}
As there may be some conflicting rules, the "latest attribute" will break any ties.
Transformations are grouped by source type before being resolved.
All transform attributes inherit the following parameters which you can use to modify the behaviour of the attribute you're using.
These attributes are:
IsReadonly
This named parameter controls whether setters will be added to properties. If you are setting this totrue
, it is a shorthand for also adding the[Readonly]
attribute. Setting this to false will add setters to properties that did not have them, such as if you wanted to implement an interface.IncludeProperties
This named parameter controls the properties that should be included from the Source type. This is a shorthand for also adding the[Pick]
attributeExcludeProperties
This named parameter controls the properties that should be excluded from the Source type. This is a shorthand for also adding the[Omit]
attribute
Please raise issues using the Issue Template.
Please raise pull requests using the Pull Request Template.
If you have recommendations on improving this library, please get in touch.