AArnott / ImmutableObjectGraph

Code generation for immutable types
Other
169 stars 32 forks source link

Support stable API versioning strategy for adding fields #58

Closed AArnott closed 8 years ago

AArnott commented 9 years ago

If a library is shipped with code generated by this library and later shipped again with an added field to one of the immutable types, a breaking change occurs in the generated code in that the Create and With methods' signatures are changed. This is unacceptable for libraries which must ship stable APIs.

The proposed design is to add support for a new GenerationAttribute(int) to decorate new fields with to avoid binary breaking changes. For example, consider this type that appears in v1 of a library:

[GenerateImmutable]
partial class Person
{
    readonly string firstName;
    readonly string lastName;
}

In v1.1 of the library, the age field is added:

[GenerateImmutable]
partial class Person
{
    readonly string firstName;
    readonly string lastName;
    [Generation(2)]
    readonly int age;
}

The code generator then produces extra Create and With methods:

// original methods are preserved:
public static Person Create(ImmutableObjectGraph.Optional<System.String> firstName = default(ImmutableObjectGraph.Optional<System.String>), ImmutableObjectGraph.Optional<System.String> lastName = default(ImmutableObjectGraph.Optional<System.String>));
public Person With(ImmutableObjectGraph.Optional<System.String> firstName = default(ImmutableObjectGraph.Optional<System.String>), ImmutableObjectGraph.Optional<System.String> lastName = default(ImmutableObjectGraph.Optional<System.String>));

// additional methods are added to support the next version
public static Person Create2(ImmutableObjectGraph.Optional<System.String> firstName = default(ImmutableObjectGraph.Optional<System.String>), ImmutableObjectGraph.Optional<System.String> lastName = default(ImmutableObjectGraph.Optional<System.String>), ImmutableObjectGraph.Optional<System.Int32> age = default(ImmutableObjectGraph.Optional<System.Int32>));
public Person With2(ImmutableObjectGraph.Optional<System.String> firstName = default(ImmutableObjectGraph.Optional<System.String>), ImmutableObjectGraph.Optional<System.String> lastName = default(ImmutableObjectGraph.Optional<System.String>), ImmutableObjectGraph.Optional<System.Int32> age = default(ImmutableObjectGraph.Optional<System.Int32>));

Note that the additional methods have a 2 suffix on them to match the generation that appears in the [Generation(2)] attribute.

When [Generation(3)] appears, yet another set of methods (Create3 and With3) are added in the code generation.

Special care is taken to ensure that folks calling a With method from an earlier generation do not lose the data in the fields added in a later generation. This can likely be done simply by implementing the earlier generation methods in terms of the latest generation method.

Note we do not simply add overloads of Create and With, using the same method name as original, as that would cause source breaking changes as folks who try to compile will likely get errors from the C# compiler due to ambiguous method overload matches, on account of the optional parameters.

jviau commented 9 years ago

This C# proposal dotnet/roslyn/#5172 is a step towards fixing this issue. So long as the implementation of it will not introduce runtime errors.

AArnott commented 9 years ago

That's a very interesting proposal. I'll have to study it more in depth.

AArnott commented 8 years ago

I'm working on this now.