Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/martinothamar/wrappervalueobject
A .NET source generator for creating simple value objects wrapping primitive types.
https://github.com/martinothamar/wrappervalueobject
csharp csharp-sourcegenerator dotnet dotnet-core dotnet-standard dotnetcore source-gen source-generation source-generators sourcegenerator value-object
Last synced: 6 days ago
JSON representation
A .NET source generator for creating simple value objects wrapping primitive types.
- Host: GitHub
- URL: https://github.com/martinothamar/wrappervalueobject
- Owner: martinothamar
- License: mit
- Created: 2020-08-15T09:15:32.000Z (over 4 years ago)
- Default Branch: master
- Last Pushed: 2022-11-17T09:04:06.000Z (almost 2 years ago)
- Last Synced: 2024-11-02T22:07:33.812Z (12 days ago)
- Topics: csharp, csharp-sourcegenerator, dotnet, dotnet-core, dotnet-standard, dotnetcore, source-gen, source-generation, source-generators, sourcegenerator, value-object
- Language: C#
- Homepage:
- Size: 51.8 KB
- Stars: 52
- Watchers: 5
- Forks: 3
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# WrapperValueObject
![Build](https://github.com/martinothamar/WrapperValueObject/workflows/Build/badge.svg)
[![NuGet](https://img.shields.io/nuget/v/WrapperValueObject.Generator.svg)](https://www.nuget.org/packages/WrapperValueObject.Generator/)> **Note**
> This library is not actively maintained at the moment, I recommend looking at [SteveDunn/Vogen](https://github.com/SteveDunn/Vogen)
A .NET source generator for creating
* Simple value objects wrapping other type(s), without the hassle of manual `Equals`/`GetHashCode`
* Value objects wrapping math primitives and other types
* I.e. `[WrapperValueObject(typeof(int))] readonly partial struct MeterLength { }` - the type is implicitly castable to `int`
* Math and comparison operator overloads are automatically generated
* `ToString` is generated with formatting options similar to those on the primitive type, i.e. `ToString(string? format, IFormatProvider? provider)` for math types
* Strongly typed ID's
* Similar to F# `type ProductId = ProductId of Guid`, here it becomes `[WrapperValueObject] readonly partial struct ProductId { }` with a `New()` function similar to `Guid.NewGuid()`The generator targets .NET Standard 2.0 and has been tested with `netcoreapp3.1` and `net5.0` target frameworks.
Note that record type feature for structs is planned for C# 10, at which point this library might be obsolete.
## Installation
Add to your project file:
```xml
all
runtime; build; native; contentfiles; analyzers```
Or install via CLI
```sh
dotnet add package WrapperValueObject.Generator --version 0.0.1
```This package is a build time dependency only.
## Usage
1. Use the attribute to specify the underlying type.
2. Declare the struct or class with the `partial` keyword.### Strongly typed ID
```csharp
[WrapperValueObject] readonly partial struct ProductId { }var id = ProductId.New(); // Strongly typed Guid wrapper, i.e. {1658db8c-89a4-46ea-b97e-8cf966cfb3f1}
Assert.NotEqual(ProductId.New(), id);
Assert.False(ProductId.New() == id);
```### Money type
```csharp
[WrapperValueObject(typeof(decimal))] readonly partial struct Money { }Money money = 2m;
var result = money + 2m; // 4.0
var result2 = money + new Money(2m);Assert.True(result == result2);
Assert.Equal(4m, (decimal)result);
```### Metric types
```csharp
[WrapperValueObject(typeof(int))]
public readonly partial struct MeterLength
{
public static implicit operator CentimeterLength(MeterLength meter) => meter.Value * 100; // .Value is the inner type, in this case int
}[WrapperValueObject(typeof(int))]
public readonly partial struct CentimeterLength
{
public static implicit operator MeterLength(CentimeterLength centiMeter) => centiMeter.Value / 100;
}MeterLength meters = 2;
CentimeterLength centiMeters = meters; // 200
Assert.Equal(200, (int)centiMeters);
```### Complex types
```csharp
[WrapperValueObject] // Is Guid ID by default
readonly partial struct MatchId { }[WrapperValueObject("HomeGoals", typeof(byte), "AwayGoals", typeof(byte))]
readonly partial struct MatchResult { }partial struct Match
{
public readonly MatchId MatchId { get; }public MatchResult Result { get; private set; }
public void SetResult(MatchResult result) => Result = result;
public Match(in MatchId matchId)
{
MatchId = matchId;
Result = default;
}
}var match = new Match(MatchId.New());
match.SetResult((1, 2)); // Complex types use value tuples underneath, so can be implicitly converted
match.SetResult(new MatchResult(1, 2)); // Or the full constructorvar otherResult = new MatchResult(2, 1);
Debug.Assert(otherResult != match.Result);
match.SetResult((2, 1));
Debug.Assert(otherResult == match.Result);Debug.Assert(match.MatchId != default);
Debug.Assert(match.Result != default);
Debug.Assert(match.Result.HomeGoals == 2);
Debug.Assert(match.Result.AwayGoals == 1);
```### Validation
To make sure only valid instances are created.
The validate function will be called in the generated constructors.```csharp
[WrapperValueObject] // Is Guid ID by default
readonly partial struct MatchId
{
static partial void Validate(Guid id)
{
if (id == Guid.Empty)
throw new ArgumentOutOfRangeException(nameof(id), $"{nameof(id)} must have value");
}
}[WrapperValueObject("HomeGoals", typeof(byte), "AwayGoals", typeof(byte))]
readonly partial struct MatchResult
{
static partial void Validate(byte homeGoals, byte awayGoals)
{
if (homeGoals < 0)
throw new ArgumentOutOfRangeException(nameof(homeGoals), $"{nameof(homeGoals)} value cannot be less than 0");
if (awayGoals < 0)
throw new ArgumentOutOfRangeException(nameof(awayGoals), $"{nameof(awayGoals)} value cannot be less than 0");
}
}
```## Limitations
* Need .NET 5 SDK (I think) due to source generators
* Does not support nested types
* Limited configuration options in terms of what code is generated## Related projects and inspiration
* [StronglyTypedId](https://github.com/andrewlock/StronglyTypedId) by @andrewlock
## TODO/under consideration
Further development on this PoC was prompted by this discussion: https://github.com/ironcev/awesome-roslyn/issues/17
* Replace one generic attribute (WrapperValueObject) with two (or more) that cleary identify the usecase. E.g. StronglyTypedIdAttribute, ImmutableStructAttribute, ...
* Support everything that StronglyTypedId supports (e.g. optional generation of JSON converters).
* Bring the documentation to the same level as in the StronglyTypedId project.
* Write tests.
* Create Nuget package.