Skip to content

Latest commit

 

History

History
198 lines (146 loc) · 6.16 KB

File metadata and controls

198 lines (146 loc) · 6.16 KB

Table of Contents

Motivation

The idea started with this tweet - specifically, this reply. I thought...how automatic can I make object deconstruction in C#? That's what this source generator is all about.

What is a Deconstructor?

Object deconstruction was added in C# 7.0. The documentation is here, and there's another article here. Basically, a deconstructor can be defined on either a type or as an extension method. In both cases, it has to be named "Deconstruct", it has to return void, and all of its parameters must be out parameters (the exception is with the extension method, where the first parameter is the type being extended). Furthermore, Deconstruct() methods can overloaded, but all Deconstruct() methods must have a unique number of out parameters. Here are two examples:

using System;

public sealed class Customer
{
  public Customer(Guid id, string name) =>
    (this.Id, this.Name) = (id, name);

  public void Deconstruct(out Guid id, out string name) =>
    (id, name) = (this.Id, this.Name);

  public Guid Id { get; }
  public string Name { get; }
}

var customer = new Customer(Guid.NewGuid(), "Jason");
var (id, name) = customer;

public struct Point
{
  public Point(int x, int y) =>
    (this.X, this.Y) = (x, y);
		
  public int X { get; }
  public int Y { get; }
}

public static class PointExtensions
{
  public static void Deconstruct(this Point self, out int x, out int y) =>
    (x, y) = (self.X, self.Y);
}

var point = new Point(2, 3);
var (x, y) = point;

Note that what values are deconstructed is up to the developer. That is, deconstruction does not require one to deconstruct all property or field values.

AutoDeconstruct Features

Marking Types

AutoDeconstruct looks to see if the target type has any instance Deconstruct() methods. If none exist, then AutoDeconstruct looks to see how many accessible, readable, instance properties exist. If there's at least 1, the library generates a Deconstruct() extension method in a static class defined in the same namespace as the target type. For example, if we have our Point type defined like this:

namespace Maths.Geometry;

[AutoDeconstruct]
public struct Point
{
  public Point(int x, int y) =>
    (this.X, this.Y) = (x, y);

  public int X { get; }
  public int Y { get; }
}

Then the library generates this:

#nullable enable

namespace Maths.Geometry
{
  public static partial class PointExtensions
  {
    public static void Deconstruct(this global::Maths.Geometry.Point @self, out int @x, out int @y) =>
      (@x, @y) = (@self.X, @self.Y);
  }
}

If the target type is a reference type, a null check will be generated. Furthermore, the Deconstruct() extension method will also be created if a Deconstruct() doesn't exist with the number of properties found. For example, let's say we have this:

using AutoDeconstruct;

namespace Models;

[AutoDeconstruct]
public sealed class Person
{
  public uint Age { get; init; }
  public Guid Id { get; init; }
  public string Name { get; init; }

  public void Deconstruct(out Guid id) =>
    id = this.Id;
}

AutoDeconstruct would see that there are three properties that could be used for a generated Deconstruct(). The Deconstruct() method that exists has one out parameter, so it will generate one that has all three properties as out parameters:

#nullable enable

namespace Models
{
  public static partial class PersonExtensions
  {
    public static void Deconstruct(this global::Models.Person @self, out global::System.Guid @id, out string @name, out uint @age)
    {
      global::System.ArgumentNullException.ThrowIfNull(@self);
        (@id, @name, @age) = (@self.Id, @self.Name, @self.Age);
    }
  }
}

Starting in 2.0.0, you can also use [TargetAutoDeconstruct] at the assembly level:

using AutoDeconstruct;

[assembly: TargetAutoDeconstruct(typeof(Person))]

namespace Models;

public sealed class Person
{
  public uint Age { get; init; }
  public Guid Id { get; init; }
  public string Name { get; init; }

  public void Deconstruct(out Guid id) =>
    id = this.Id;
}

You can target other types from other assemblies if you'd like:

using AutoDeconstruct;
using System;

[assembly: TargetAutoDeconstruct(typeof(Guid))]

// ...
var id = Guid.NewGuid();
var (variant, version) = id;

Take care in creating deconstructors to types you don't own. For example, in the case of Guid, getting just the Variant and Version values aren't extremely helpful.

Warning

If you add [AutoDeconstruct] to a type, and use [TargetAutoDeconstruct] targeting that same type, you will get a compilation error as a duplicate extension method will be made.

Filtering Properties

Starting with 3.0.0, AutoDeconstruct lets you specify properties that you want to either include or exclude in the generated Deconstruct() method. This is useful if you are inheriting from a type that has numerous properties that you do not want for deconstruction. Here's how it works (note that this also works with [TargetAutoDeconstruct]):

using AutoDeconstruct;

namespace Models;

[AutoDeconstruct(Filtering.Include, [nameof(Person.Age), nameof(Person.Name)])]
public sealed class Person
{
  public uint Age { get; init; }
  public Guid Id { get; init; }
  public string Name { get; init; }
}

In this case, Deconstruct() will have two out parameters: age and name. Properties can also be excluded:

using AutoDeconstruct;

namespace Models;

[AutoDeconstruct(Filtering.Exclude, [nameof(Person.Id)])]
public sealed class Person
{
  public uint Age { get; init; }
  public Guid Id { get; init; }
  public string Name { get; init; }
}

Both filtering examples lead to the same result.