Skip to content

Commit

Permalink
Update packages, net452 and net6.0
Browse files Browse the repository at this point in the history
- As .NET 6.0 is the first LTS we make it the lowest tested .NET version
- updated net45 to net452 to match the development package
- Updated all packages to latest versions. The dotVariant.Runtime deps
  have not changed.
  • Loading branch information
mknejp committed Jul 8, 2023
1 parent 0ca8300 commit a8df587
Show file tree
Hide file tree
Showing 9 changed files with 73 additions and 28 deletions.
42 changes: 40 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# dotVariant [![GitHub](https://img.shields.io/github/license/mknejp/dotVariant)](/LICENSE.txt) [![Nuget verion](https://img.shields.io/nuget/v/dotVariant)](https://www.nuget.org/packages/dotVariant/) [![GitHub Workflow Status](https://img.shields.io/github/workflow/status/mknejp/dotvariant/test-package)](https://github.com/mknejp/dotvariant/actions)

A type-safe and space-efficient sum type for C# (comparable to unions in C or C++)

- [Overview of Variants](#overview-of-variants)
Expand All @@ -16,10 +17,13 @@ A type-safe and space-efficient sum type for C# (comparable to unions in C or C+
- [License](#license)

## Overview of Variants

A variant is similar to a struct or class, except that it can always only contain _one_ of its fields. A `class` declared with an `int` and a `string` property always contains one `int` value and one `string` value. In comparison to that a variant declared with an `int` option and a `string` option always contains _either_ an `int` value _or_ a `string` value, but never both. The library also makes a best effort at minimizing the amount of storage required by the variant as it can always only contain a single value. This is similar to C and C++ `union`, but tweaked to work under the restrictions of the .NET runtime.

### Declaring a Variant
Declaring a variant is very easy and requires only minimal amount of

Declaring a variant is very easy and requires only minimal amount of

```csharp
using dotVariant;

Expand All @@ -38,6 +42,7 @@ namespace MyNamespace
}
}
```

You are not restricted to just `class`. You can also use `struct`, `readonly struct`, `ref struct`, and so on. Only `record` is currently not supported and probably never will, but that should not stop you, as variants are immutable and have by-value comparison, just like records.

The `VariantOf` method is how you tell the generator what the possible values of this variant are. Anything that is a valid parameter and field is also a valid option here and the parameter names will be used as hints throughout the generated interface. Do not use `out`, `in` or `ref` modifiers, as those are reserved for future use.
Expand All @@ -49,7 +54,9 @@ Any constraints you put on the type parameters will be taken into consideration
**Note**: The .NET runtime forbids layout manipulation on generic types, so in the above example `MyVariant` will occupy less memory than `MyVariant<int, double, string>` despite seemingly having the same content. Be aware of this when using generic variants and try to use concrete types whenever possible.

### Using a Variant

With the above declaration, you are ready to use your new variant:

```csharp
var variant1 = new MyVariant(1);
variant1.Match(out int i);
Expand All @@ -67,7 +74,9 @@ MyVariant variant4 = 42; // implicitly create from accepted value type
void Foo(MyVariant x) { }
Foo("a string"); // implicitly create MyVariant instance
```

Note how we used the same type to store and retrieve different types of values. However, you are not limited to just `out` variables, you can also pass in functions:

```csharp
var variant1 = new MyVariant(1);
variant1.Match((int i) => Console.WriteLine(i)); // prints 1
Expand All @@ -78,7 +87,9 @@ variant2.Match((double d) => Console.WriteLine(d)); // prints 2.5
var variant3 = new MyVariant("world");
variant3.Match((string s) => Console.WriteLine(s)); // prints "world"
```

And you can even return values from these functions, which get piped through `Match`:

```csharp
var variant1 = new MyVariant(2);
var i = variant1.Match((int x) => x * 5); // i = 10
Expand All @@ -89,7 +100,9 @@ var d = variant2.Match((double x) => Math.Sin(x)); // d = 0.90929742682568171
var variant3 = new MyVariant("world");
var s = variant3.Match((string x) => $"hello {x}!"); // s = "hello world!"
```

What happens if you try to retrieve a value from a variant it currently does not contain? It throws an `InvalidOperationException`. To avoid this there are overloads of `Match` and `TryMatch` giving you tools to avoid this disappointing outcome:

```csharp
var variant = new MyVariant("not an int");

Expand All @@ -102,7 +115,9 @@ var b2 = variant.TryMatch(out string s); // b2 = true, s = "not an int"
var i = variant.Match((int x) => x, 42); // i = 42
var j = variant.Match((int x) => x, () => 1337); // j = 1337
```

Until now all you could do was get a single type of value out of the variant using `Match` or `TryMatch`, and these two functions are designed to do only that. The real power behind variants, however, comes from visitation, where you provide a delegate to handle each possibility.

```csharp
string GetContainedType(MyVariant variant)
{
Expand All @@ -115,12 +130,15 @@ GetContainedType(12); // returns "int"
GetContainedType(3.14); // returns "double"
GetContainedType("blubb"); // returns "string"
```

`Visit` accepts one delegate per possible type it _might_ contain, and at runtime invokes the one corresponding to the value it _does_ contain. Naturally, all delegates must return the same type.

There are many available overloads of `Match` and `Visit` which hopefully help you achieve your goal in every scenario.

### Custom Value Types

Of course you are not restricted to just using builtin types like `int` or `double`. Any type that is valid for fields and parameters is valid for variants. A useful pattern is to declare your own types nested to the variant.

```csharp
[Variant]
readonly partial struct MyAdvancedVariant
Expand All @@ -139,7 +157,9 @@ MyAdvancedVariant v = new MyAdvancedVariant.Option1(13); // implicitly converts
```

### Nullability

The generator fully supports nullability annotations. The generated source code honors the nullability context of where the class is defined and its generated interface will match the nullability annotations of the `VariantOf` parameters.

```csharp
#nullable enable

Expand Down Expand Up @@ -172,7 +192,9 @@ partial class Variant4 // code generated with #nullable disable
static partial void VariantOf(int a, string s); // s is nullable in all generated code
}
```

And of course nullable value types are also supported.

```csharp
[Variant]
partial class SomeVariant
Expand All @@ -182,7 +204,9 @@ partial class SomeVariant
```

### Emptiness

If you declare your variant as a `struct`-type, you need to be aware that a variant can be _empty_, meaning it does not hold _any_ value. This is an unfortunate consequence of value types always having a default constructor in .NET. A `class` variant should never be empty unless you define your own constructor and default-construct the private `_variant` field. Use the public `IsEmpty` property to check for emptiness. Attempting to retrieve a value out of an empty variant results in a `InvalidOperationException`, however there are overloads of `Match` and `Visit` with ways to deal with emptiness in a more fluid manner.

```csharp
[Variant]
partial struct MyStructVariant
Expand All @@ -203,20 +227,25 @@ variant.Match(
```

## Generated Code Features

The generated implemenation provides some additional features depending on the types you provide it, or third-party libraries available to you.

### `IDisposable` Support

If _at least one_ of the types included in the `VariantOf()` parameters implements `System.IDisposable`, or is a type parameter with a transitive `System.IDisposable` constraint, then the generated variant will also implement this interface and provide a public `Dispose()` member which delegates to the stored value's `Dispose()` if applicable.

If there already exists an implementation of `IDisposable.Dispose()` (either you defined one, or it is present in a base class) then the public `Dispose()` method is _not_ generated and it is your responsibility to take care of calling the private `_variant.Dispose()`.

### External Integrations

If your type is declared in such a way that providing extensions methods is possible you will get additional integration with .NET facilities, or popular external libraries, listed in this section. The visibility (`public` or `internal`) of the extension methods is made to match the accessibility of your type declaration.

The `static class` containing all extension methods is by default generated in the same namespace containing the variant type, but that is configurable (see [Extension Class Namespace](#extension-class-namespace)).

#### `IEnumerable<T>`

These allow for easy and powerful integration into `System.Linq`-like queries on `IEnumerable<T>` sequences, that let you manipulate a stream of variants based on the contained type.

```csharp
[Variant]
public readonly partial struct MyVariant
Expand All @@ -242,7 +271,9 @@ xs.Visit(
```

#### `IObservable<T>`

These allow for easy and powerful integration into `System.Reactive.Linq`-like queries on `IObservable<T>` sequences, that let you manipulate an asynchronous stream of variants based on the contained type.

```csharp
[Variant]
public readonly partial struct MyVariant
Expand Down Expand Up @@ -290,10 +321,13 @@ xs.VisitMany((i, d, s) => CombineLatest(i, d, s, (ix, dx, sx) => (ix, dx, sx));
```

## Customization

An overview of how you can customize the generated source code.

### Extension Class Namespace

As mentioned in [Third-party Integrations](#third-party-integrations) if the circumstances are right extension methods for integration with third-party libraries can be generated. By default the accompanying `static class` is put in the namespace containing the variant type.

```csharp
// Your declaration
namespace Foo.Bar.Baz
Expand All @@ -312,17 +346,21 @@ namespace Foo.Bar.Baz
internal static class _MyVariant_Ex { /* all extension methods for MyVariant go here */ }
}
```

However this means the extension methods are only accessible if you are inside namespace `Foo.Bar.Baz` or have `using Foo.Bar.Baz;` active in your scope. Thus if you are in namespace `Foo.Bar` and are handled a `IEnumerable<Foo.Bar.Baz.MyVariant>` then the extension methods won't be visible to you. If this is not what you want you can set a MSBuild property to change the namespace where all extension classes are generated (an additional per-class option is planned) to whichever place you put common extension methods. I highly recomment making them visible everywhere, you don't want to miss out on them!

```xml
<PropertyGroup>
<dotVariant-ExtensionClassNamespace>Foo.Extensions</dotVariant-ExtensionClassNamespace>
</PropertyGroup>
```

## Compatibility
- As this library is based on source generators you have to use the .NET 5 SDK to compile your project.

- To use the generator you must use the latest .NET SDK
- The generated code is compatible down to C# `7.3` and adjusts itself to the available language version and runtime facilities.
- The required runtime support library targets `netstandard1.0`.

## License

Licensed under the [Boost Software License 1.0](LICENSE.txt).
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeStyle" Version="3.9.0" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.CodeStyle" Version="4.6.0" />
</ItemGroup>

</Project>
18 changes: 10 additions & 8 deletions src/dotVariant.Generator.Test/dotVariant.Generator.Test.csproj
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="IsExternalInit" Version="1.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
<PackageReference Include="NUnit" Version="3.13.1" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="System.Interactive" Version="5.0.0" />
<PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="IsExternalInit" Version="1.0.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="System.Interactive" Version="6.0.1" />
<PackageReference Include="System.Reactive" Version="6.0.0" />
<PackageReference Include="Verify.NUnit" Version="20.4.0" />
<PackageReference Include="Verify.SourceGenerators" Version="2.1.0" />
</ItemGroup>

<ItemGroup>
Expand Down
10 changes: 5 additions & 5 deletions src/dotVariant.Generator/dotVariant.Generator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="IsExternalInit" Version="1.0.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.9.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0" PrivateAssets="all" />
<PackageReference Include="IsExternalInit" Version="1.0.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.6.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" PrivateAssets="all" />
<!-- Transitive closure of generator runtime dependencies -->
<PackageReference Include="Scriban" Version="5.0.0" GeneratePathProperty="true" PrivateAssets="all" />
<PackageReference Include="System.Interactive" Version="5.0.0" GeneratePathProperty="true" PrivateAssets="all" />
<PackageReference Include="Scriban" Version="5.7.0" GeneratePathProperty="true" PrivateAssets="all" />
<PackageReference Include="System.Interactive" Version="6.0.1" GeneratePathProperty="true" PrivateAssets="all" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" GeneratePathProperty="true" PrivateAssets="all" />
</ItemGroup>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net45</TargetFramework>
<TargetFramework>net452</TargetFramework>
</PropertyGroup>

<Import Project="..\dotVariant.Test\dotVariant.Test.projitems" Label="Shared" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

Expand Down
4 changes: 4 additions & 0 deletions src/dotVariant.Test/Variant+TryMatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,19 @@ public static void TryMatch_fails_on_inactive_value_1()
{
var v = new Class_int_float_string(1);
Assert.That(v.TryMatch(out float _), Is.False);
#pragma warning disable CS8601
Assert.That(v.TryMatch(out string _), Is.False);
#pragma warning restore CS8601
}

[Test]
public static void TryMatch_fails_on_inactive_value_2()
{
var v = new Class_int_float_string(1f);
Assert.That(v.TryMatch(out int _), Is.False);
#pragma warning disable CS8601
Assert.That(v.TryMatch(out string _), Is.False);
#pragma warning restore CS8601
}

[Test]
Expand Down
17 changes: 9 additions & 8 deletions src/dotVariant.Test/dotVariant.Test.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,24 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.1" />
<PackageReference Include="NUnit" Version="3.13.1" />
<PackageReference Include="NUnit3TestAdapter" Version="3.17.0" />
<PackageReference Include="System.Interactive" Version="5.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
</ItemGroup>

<Choose>
<When Condition="'$(TargetFramework)' == 'net5.0'">
<When Condition="'$(TargetFramework)' == 'net6.0'">
<ItemGroup>
<PackageReference Include="System.Reactive" Version="5.0.0" />
<PackageReference Include="Microsoft.Reactive.Testing" Version="5.0.0" />
</ItemGroup>
<PackageReference Include="System.Reactive" Version="6.0.0" />
<PackageReference Include="System.Interactive" Version="6.0.1" />
<PackageReference Include="Microsoft.Reactive.Testing" Version="6.0.0" />
</ItemGroup>
</When>
<Otherwise>
<ItemGroup>
<PackageReference Include="System.Reactive" Version="3.0.0" />
<PackageReference Include="System.Interactive" Version="3.0.0" />
<PackageReference Include="Microsoft.Reactive.Testing" Version="3.0.0" />
</ItemGroup>
</Otherwise>
Expand Down
4 changes: 2 additions & 2 deletions src/dotVariant.sln
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotVariant.Runtime", "dotVariant.Runtime\dotVariant.Runtime.csproj", "{4D754A47-95CE-4279-BA69-59D7EF484106}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotVariant.Test-net5.0", "dotVariant.Test-net5.0\dotVariant.Test-net5.0.csproj", "{5FA8381B-0576-498A-9968-006A38839D3C}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotVariant.Test-net6.0", "dotVariant.Test-net6.0\dotVariant.Test-net6.0.csproj", "{5FA8381B-0576-498A-9968-006A38839D3C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotVariant.Test-net45", "dotVariant.Test-net45\dotVariant.Test-net45.csproj", "{1305501C-65AB-4338-848F-3D2B919B7255}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotVariant.Test-net452", "dotVariant.Test-net452\dotVariant.Test-net452.csproj", "{1305501C-65AB-4338-848F-3D2B919B7255}"
EndProject
Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "dotVariant.Test", "dotVariant.Test\dotVariant.Test.shproj", "{193D7A79-92BF-46BE-BB02-60360DA0883D}"
EndProject
Expand Down

0 comments on commit a8df587

Please sign in to comment.