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.
MutableStudent
for a recordStudent
).Overloaded implicit conversion operators between the record and its mutable wrapper.
A
Build
method inside the mutable class that produces the immutable record.Extension methods:
Produce
,CreateDraft
,FinishDraft
for creating and using drafts, andAsMutable
,ToImmutable
for 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 Email
staysstring Email
), whereas reference types that are themselves records annotated with[MutableGeneration]
become their mutable counterparts (e.g.StudentDetails Details
becomesMutableStudentDetails Details
inMutableStudent
). Immutable collection types (likeImmutableList<T>
) are converted to mutable collections (likeList<MutableT>
ifT
is an annotated record, orList<T>
ifT
is 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#with
expression 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 aStudent
directly to aMutableStudent
variable or parameter. It simply calls the wrapper’s constructor internally (i.e., wraps the record in a new mutable object). This is why in theProduce
method (or even in your own code) you can treat aStudent
as aMutableStudent
without 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 aMutableStudent
wherever aStudent
is 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
mutate
receives the correctMutableTRecord
type without requiring you to specify type arguments. For example, for aStudent
record, 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
mutate
action.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
Produce
does initially). For example,MutableStudent draft = student.CreateDraft();
would produce aMutableStudent
from aStudent
record. 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 callingFinishDraft
to 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 newStudent
from 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. IfT
is 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> enrollments
in 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,
AsMutable
can 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. IfT
is 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
. EachMutableEnrollment
in the list is converted toEnrollment
via implicit conversion, and the result is anImmutableList
containing 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.