API Reference
This section provides a detailed reference for the code elements that Mutty generates and the helper methods it provides. When you annotate a record with [MutableGeneration], Mutty’s source generator creates several things:
A mutable wrapper class for the record (e.g.
MutableStudentfor a recordStudent).Overloaded implicit conversion operators between the record and its mutable wrapper.
A
Buildmethod inside the mutable class that produces the immutable record.Extension methods:
Produce,CreateDraft,FinishDraftfor creating and using drafts, andAsMutable,ToImmutablefor working with immutable collections.
Below we describe each of these in detail.
MutableGeneration Attribute
Namespace: Mutty
Usage: Place [MutableGeneration] above an immutable record definition to indicate that Mutty should generate a mutable wrapper for it.
The attribute itself contains no properties – it’s simply a marker. It can be applied to record types (and should only be used on records intended to be immutable). There’s no need to add this attribute to classes or structs (Mutty is designed for record types specifically). Once a record is annotated, on the next build the Mutty generator will pick it up and produce the associated code.
Generated Mutable Wrapper Classes
For each annotated record, Mutty generates a partial class named Mutable<RecordName> in the same namespace as the original record. For example, given public record Student(...) with [MutableGeneration], a class public partial class MutableStudent is created.
Key characteristics of a mutable wrapper class:
Fields and Properties: It contains public auto-properties for each property of the original record, with identical names but types adjusted for mutability. Value types and immutable types remain the same (e.g.
string Emailstaysstring Email), whereas reference types that are themselves records annotated with[MutableGeneration]become their mutable counterparts (e.g.StudentDetails DetailsbecomesMutableStudentDetails DetailsinMutableStudent). Immutable collection types (likeImmutableList<T>) are converted to mutable collections (likeList<MutableT>ifTis an annotated record, orList<T>ifTis a value type or non-annotated reference). This allows you to add/remove items or modify contained records directly.Constructor: The wrapper has a constructor that takes the original record as a parameter. This constructor initializes the mutable object by copying over all values from the record into the wrapper’s properties. For nested records and collections, it will initialize the mutable properties by converting the original nested data into their mutable forms (e.g. by calling the appropriate
AsMutable()on an immutable list, or implicit conversion on a nested record). After construction, the mutable object is a deep copy (in mutable form) of the original record’s state.Build Method: Each mutable class has a method
public <RecordType> Build()which reconstructs the immutable record from the mutable state. Typically,Build()uses the original record and the C#withexpression to create a new instance with updated properties. Internally, it will convert collections back to their immutable counterparts (usingToImmutable()) and use any nested mutable objects’ implicit conversion back to their immutable types. You normally do not callBuild()yourself – it’s used behind the scenes by Mutty’s utilities – but you can if you ever need to manually finalize a draft.Implicit Operators: Mutty defines implicit conversion operators on the mutable class to simplify going back and forth:
public static implicit operator MutableStudent(Student record)– This allows you to assign aStudentdirectly to aMutableStudentvariable or parameter. It simply calls the wrapper’s constructor internally (i.e., wraps the record in a new mutable object). This is why in theProducemethod (or even in your own code) you can treat aStudentas aMutableStudentwithout explicit casting.public static implicit operator Student(MutableStudent mutable)– This converts a mutable draft back to an immutableStudent. It typically calls theBuild()method to produce the new record. This operator means you can use aMutableStudentwherever aStudentis expected, and the conversion will happen automatically.
Because these operators are implicit, using the Mutty API feels natural. For example, you can return a MutableStudent from a function that is declared to return a Student, and it will automatically convert to the immutable Student before returning.
Partial Class: The generated class is declared
partial, which means you can extend it by writing your own partial class with the same name. This is an advanced feature, but it allows you to add custom utility methods or validation logic to the mutable wrapper if needed. (Note that you shouldn’t override the generated behavior; rather, add new methods or properties. TheBuild()method is not virtual, so if you need to enforce invariants on building, you might callBuild()inside your own method that checks conditions.)
Example: If you have record Student(string Email, ImmutableList<Enrollment> Enrollments), Mutty will generate a class MutableStudent with a property public string Email { get; set; } and public List<MutableEnrollment> Enrollments { get; set; }, among others, plus the conversion operators. This lets you manipulate email or the enrollments list freely on a MutableStudent instance. The same pattern applies for all other records.
Produce Extension Method
Signature (conceptual):
The Produce method is an extension method generated for each annotated record type. It provides the primary high-level API for using Mutty. The method takes an immutable record instance (record) and a lambda mutate that receives the mutable version of that record. Within the lambda, you can modify the mutable draft freely. When the lambda exits, Produce returns a new immutable record of the same type with all the mutations applied.
The generic signature above is conceptual – in practice Mutty generates a concrete overload for each of your record types to ensure
mutatereceives the correctMutableTRecordtype without requiring you to specify type arguments. For example, for aStudentrecord, it generatespublic static Student Produce(this Student record, Action<MutableStudent> mutate).
Usage: You typically call Produce as myRecord.Produce(draft => { ...modify draft... });. Inside the lambda, you work with the Mutable<Record> type, which has the same field structure as your record but all fields are settable. You do not need to return anything from the lambda; the Produce method handles creating the new record for you.
What it does under the hood:
It takes the original immutable record and implicitly converts it to the mutable wrapper (by calling the implicit operator or constructor).
It passes this mutable object into your
mutateaction.Once the action completes, it implicitly converts the mutable object back to the immutable form (using the implicit conversion that calls
Build()). The new immutable record is then returned.
If no changes are made inside the lambda, the returned record will be effectively equal to the original (since Build() will produce the same state). If you do make changes, only those changes will differ – everything else will be copied from the original record.
This method is very convenient for inline modifications, especially for nested data. It also ensures the mutation scope is limited to the provided lambda, making it clear when and where changes occur.
Example:
In this example, Order is an immutable record annotated with [MutableGeneration], and it has a nested Customer record and a list of Items. The Produce call creates a MutableOrder (o) from order. We then update the customer’s name and increment the quantity of the first item. When Produce returns, updatedOrder is a new Order record with those changes applied. The original order remains unchanged.
CreateDraft and FinishDraft Methods
Mutty also provides two complementary methods for scenarios where you want to separate the creation of a draft from the finalization of changes. These are lower-level than Produce, but can be useful if you need to perform mutations across multiple methods or conditional logic without staying inside a single lambda.
CreateDraft: This is an extension method that takes an immutable record and returns a new mutable wrapper instance (similar to what
Producedoes initially). For example,MutableStudent draft = student.CreateDraft();would produce aMutableStudentfrom aStudentrecord. Under the hood, this just calls the implicit conversion (or constructor) to create the mutable object. After callingCreateDraft, you are responsible for making the desired changes on the draft and then callingFinishDraftto get a new immutable record.FinishDraft: This extension is the counterpart to
CreateDraft. It takes a mutable wrapper and produces an immutable record. For example,Student newStudent = draft.FinishDraft();will build a newStudentfrom aMutableStudent draft. Internally, this likely just invokes the implicit conversion back toStudent(which callsBuild()on the draft).
Using CreateDraft/FinishDraft explicitly is equivalent to what Produce does, but split into two steps. This gives you more control if needed. For instance:
In this example, we obtain a draft from an original student, perform multiple modifications on the draft (even in different scopes or based on conditions), and finally call FinishDraft to get the updated Student. This approach can improve code clarity when a series of changes can’t be neatly made in a single lambda. It also allows inspecting or intervening in the middle of a mutation process.
Important: After calling FinishDraft, you should discard the mutable draft object (or at least not use it further). If you call FinishDraft again on the same draft, it will just produce another new record with the same state (which might be harmless), but generally the draft is meant to be one-time use. Also, do not forget to call FinishDraft – if you create a draft and never finish it, the changes remain only in the mutable object and are not applied to any immutable record.
In summary, use CreateDraft/FinishDraft when you need fine-grained control, and use Produce for concise inline updates. They are different interfaces to the same mechanism.
AsMutable and ToImmutable Extension Methods
Mutty generates collection helper methods to handle converting between immutable collections and their mutable counterparts. These are especially useful for managing ImmutableList<T> or ImmutableArray<T> properties within your records.
AsMutable(): This extension method is available on immutable collection types (like
ImmutableList<T>). It produces a mutable copy of the collection. IfTis a reference type that has a mutable wrapper,AsMutable()will convert each element to its mutable form; otherwise, it will just copy the items as-is into aList<T>.For example, if you have
ImmutableList<Enrollment> enrollmentsin your record, the generated wrapper’s constructor will callenrollments.AsMutable(), resulting in aList<MutableEnrollment>. You can also call this directly in your code if needed. Similarly, it would handle anImmutableList<int>by returning aList<int>with the same values.In general,
AsMutablecan be thought of as: “give me a modifiable List copy of this immutable list”. This is used internally by Mutty to initialize mutable drafts, but you can use it yourself for other scenarios where you want to convert an immutable collection to a list for editing.ToImmutable(): This extension complements
AsMutable(). It is available on standard mutable collections likeList<T>(or more generally, anything that implementsIEnumerable<T>could be converted, but typically aList<T>). It returns an immutable collection (usually by calling.ToImmutableList()under the hood) containing the elements. IfTis a mutable wrapper, it will convert each element back to the immutable type before producing the immutable collection.For example, in the
MutableStudent.Build()method, after you finish editingList<MutableEnrollment> Enrollments, it callsEnrollments.ToImmutable()which produces anImmutableList<Enrollment>for the newStudent. EachMutableEnrollmentin the list is converted toEnrollmentvia implicit conversion, and the result is anImmutableListcontaining those records.
These methods make it easy to round-trip collections when building or tearing down immutable structures. They are generally used internally by the generated code, but are exposed if you need them in your own utility code.
Note: The AsMutable/ToImmutable naming is meant to align with common terminology. They handle only certain collection types (for example, ImmutableList<T> to List<T> and vice versa). If your records use other immutable collection types (like ImmutableDictionary or custom collections), Mutty may not automatically handle those unless it’s implemented to do so. In such cases, you might need to manually convert those in partial methods or avoid using those types with Mutty. For standard usage with lists (which is most common), these helpers cover the needs.
Putting It Together – Example
To see the relationship between these APIs, consider again an annotated Student record. With Mutty in place, you can do the following in your code:
Both approaches above yield a new Student with an incremented age and an additional enrollment, but Produce does it in one call. Under the hood, Produce was essentially doing the CreateDraft and FinishDraft steps for you.
Remember that the generated classes and methods are all static or compile-time – using Mutty does not add any runtime library dependency aside from the small overhead of copying values. Everything is resolved during compilation. You write code using these helpers as if they existed natively for your types, and the compiler (with Mutty’s help) takes care of the rest.
For more insight into how Mutty generates these and how it works behind the scenes, refer to the Architecture documentation.