Immutable Record Mutation Made Easy
- Automated Mutable Wrappers: Automatically generates mutable wrapper classes for your immutable records using Roslyn's Incremental Source Generation.
- Deep Nesting Support: Easily handle complex nested structures without tedious and error-prone manual code.
- Immutable to Mutable Conversion: Seamlessly switch between immutable and mutable versions of your records using implicit conversions.
- Ideal for Flux Architecture: Works great with Flux architecture, allowing you to manage state changes in a predictable and immutable way.
- Helper Methods:
- Provides a
Produce
method to apply mutations to your immutable records using the generated mutable wrappers. - Also includes
CreateDraft
andFinishDraft
methods for more granular control... - ...and
AsMutable
andToImmutable
extension methods for collections.
- Provides a
Mutty uses a custom attribute [MutableGeneration]
to mark immutable records for which you want to generate mutable wrappers.
The Incremental Source Generator detects these records and generates corresponding mutable wrapper classes and extension methods.
The basic idea is that with Mutty, you will apply all your changes to a temporary mutable wrapper, which acts as a proxy of the immutable record. Once all your mutations are completed, Mutty will produce the next immutable state based on the mutations to the mutable wrapper. This means that you can interact with your data by simply modifying it while keeping all the benefits of immutable data.
Using Mutty is like having a personal assistant. The assistant takes a letter (the current state) and gives you a copy (mutable wrapper) to jot changes onto. Once you are done, the assistant will take your draft and produce the real immutable, final letter for you (the next state).
Suppose you have the following immutable records:
namespace Mutty.ConsoleApp;
[MutableGeneration]
public record Student(string Email, StudentDetails Details, ImmutableList<Enrollment> Enrollments);
[MutableGeneration]
public record StudentDetails(string Name, int Age);
[MutableGeneration]
public record Enrollment(Course Course, DateTime EnrollmentDate);
[MutableGeneration]
public record Course(string Title, string Description, ImmutableList<Module> Modules);
[MutableGeneration]
public record Module(string Name, ImmutableList<Lesson> Lessons);
[MutableGeneration]
public record Lesson(string Title, string Content);
Note: For simplicity, this example focuses on the
Student
record, but Mutty also generates similar mutable wrappers forStudentDetails
,Enrollment
,Course
,Module
, andLesson
.
When you add the [MutableGeneration]
attribute to your records, Mutty will automatically generate the corresponding mutable wrapper classes:
// <auto-generated />
// This file is auto-generated by Mutty.
using System.Collections.Immutable;
namespace Mutty.ConsoleApp
{
/// <summary>
/// The mutable wrapper for the <see cref="Student"/> record.
/// </summary>
public partial class MutableStudent
{
private Student _record;
/// <summary>
/// Initializes a new instance of the <see cref="MutableStudent"/> class.
/// </summary>
/// <param name="record">The record to wrap.</param>
public MutableStudent(Student record)
{
_record = record;
Email = _record.Email;
Details = _record.Details;
Enrollments = _record.Enrollments.AsMutable();
}
/// <summary>
/// Builds a new instance of the <see cref="Student"/> class.
/// </summary>
public Student Build()
{
return _record with
{
Email = this.Email,
Details = this.Details,
Enrollments = this.Enrollments.ToImmutable(),
};
}
/// <summary>
/// Performs an implicit conversion from <see cref="Student"/> to <see cref="MutableStudent"/>.
/// </summary>
public static implicit operator MutableStudent(Student record)
{
return new MutableStudent(record);
}
/// <summary>
/// Performs an implicit conversion from <see cref="MutableStudent"/> to <see cref="Student"/>.
/// </summary>
public static implicit operator Student(MutableStudent mutable)
{
return mutable.Build();
}
/// <summary>
/// Gets or sets the Email.
/// </summary>
public string Email { get; set; }
/// <summary>
/// Gets or sets the Record Details.
/// </summary>
public MutableStudentDetails Details { get; set; }
/// <summary>
/// Gets or sets the ImmutableCollection Enrollments.
/// </summary>
public List<MutableEnrollment> Enrollments { get; set; }
}
}
Once the code is generated, you can use the mutable wrappers to modify your immutable records as needed.
Here's an example demonstrating how easy it is to handle deeply nested structures using Mutty:
public sealed class ExampleImmutableArray : ExampleBase
{
public override void Run()
{
DisplayHeader("ImmutableArray Example");
// Initialize original immutable objects
Student student = Factories.CreateJohnDoe();
// Use the Produce method to create an updated student object with mutations
Student updatedStudent = student.Produce(mutable =>
{
// Modify the title of the first lesson in the first module of the first course
mutable.Enrollments[0].Course.Modules[0].Lessons[0].Title = "=== NEW TITLE ===";
});
// Display the original and updated student objects
DisplayStudentTree(student, 4);
DisplayStudentTree(updatedStudent, 4);
}
}
Without Mutty, updating deeply nested structures using the with
expression can become cumbersome and error-prone:
// Using 'with' notation
var updatedStudent = student with
{
Enrollments = student.Enrollments.SetItem(0, student.Enrollments[0] with
{
Course = student.Enrollments[0].Course with
{
Modules = student.Enrollments[0].Course.Modules.SetItem(0, student.Enrollments[0].Course.Modules[0] with
{
Lessons = student.Enrollments[0].Course.Modules[0].Lessons.SetItem(0, student.Enrollments[0].Course.Modules[0].Lessons[0] with
{
Title = "=== NEW TITLE ==="
})
})
}
})
};
Using Mutty, the same operation is simpler and more intuitive:
// Using Mutty
Student updatedStudent = student.Produce(mutable =>
{
mutable.Enrollments[0].Course.Modules[0].Lessons[0].Title = "=== NEW TITLE ===";
});
Mutty is an excellent fit for state management patterns like Flux. With Mutty, you can maintain immutable state while easily applying updates through the mutable wrappers. This keeps your state management predictable and efficient, especially in complex applications with deeply nested state.
To use Mutty in your project:
-
Add the Mutty package:
- You can add it as a NuGet package (if it's available as a package).
-
Annotate Your Records:
- Simply annotate your records with
[MutableGeneration]
to indicate that Mutty should generate a mutable wrapper for them.
- Simply annotate your records with
-
Build Your Project:
- The Incremental Source Generator will automatically detect the annotated records and generate the corresponding mutable wrappers and extension methods during the build process.
- Immutable by Default: Use immutable records for your core data models to ensure thread safety and prevent unintended side effects.
- Mutate with Care: Use the generated mutable wrappers when you need to make changes, but remember to always convert back to the immutable form before exposing the data.
- Leverage the Implicit Conversion: Mutty provides implicit conversions between the immutable and mutable versions of your records, making it easy to switch between the two.
If you want to contribute to Mutty or report issues:
- GitHub Repository: Mutty on GitHub
- Issues: Use the GitHub Issues tab to report bugs or request features.
Mutty is open-source software licensed under the Apache License 2.0.