Code Immutability

 

We arrive at another point that I consider fundamental when building applications: the immutability of objects. This basically means that objects cannot change state once they're created.

Immutability is commonly seen as a feature of F# or functional programming, but if something works well, why not learn from it and adapt it when possible? This way, we can adapt our code to what is called "Immutable Architecture".

Note: I have never programmed in F#.

 

1 - Difference between a Mutable and an Immutable Class

As the name suggests, a mutable class is one that can mutate or change after it is created. In other words, we can update its values. In C#, we commonly do this via a `setter`.

On the other hand, immutable means that the class CANNOT be changed once created.

In C#, we have two options for immutability:

The keyword "const" which serves to create constants.

The keyword "readonly" which means that this property is read-only.

However, immutability doesn't end with these two keywords.

 

2 - Why should we care about making our code immutable?

The main reason why making code immutable is so important is because it helps us avoid many errors.

For beginners, you'll notice that codebases where all classes are mutable are much more difficult to maintain and much harder to debug than those that are immutable. And that's not to mention asynchronous code, which makes things even more complicated.

These errors commonly stem from a property not having a value when, according to the application's logic, it should. This is because we can assign a value to a property anywhere in the object, whereas if we use readonly, we can only set it in the constructor.

2.1 - Updating Immutable Objects

A "curious" point about immutability is that to update a value or property, we don't update it directly with a setter. Instead, what we do is create a copy of the object with the updated value.

public class EducationEntity
{
    public readonly int Id;
    public readonly DateTime StartDate;
    public readonly DateTime? EndDate;
    public readonly string CourseName;
    public readonly string UniversityName;

    private EducationEntity(int id, DateTime startDate, DateTime? endDate, string courseName, string universityName)
    {
        Id = id;
        StartDate = startDate;
        EndDate = endDate;
        CourseName = courseName ;
        UniversityName = universityName;
    }

    public static EducationEntity Create(int id, DateTime startDate, DateTime? endDate, string courseName, string universityName)
    {
        return new EducationEntity(id, startDate, endDate, courseName, universityName);
    }

    public EducationEntity UpdateEndDate(DateTime? endDate)
    {
        return EducationEntity.Create(Id, StartDate, endDate, CourseName, UniversityName);
    }
}

As we can see in the example, all properties are "readonly" so they must be set via the constructor. We update the end date through the `UpdateEndDate` method, which creates a "copy" of the same object, only changing the property we need. In a real scenario, you would update all necessary values, but in this case, it's just one property to make the example clear.

Note: this code is from the web C# series.

2.2 - Benefits of Immutable Code

  1. Easier to read and maintain.
    • If we implement an immutability pattern in our code, it will be much more readable and easier to understand.
  2. Safer code
    • Variable validation only happens once, in the constructor. Once the instance of the class is created (which is immutable), we can be 100% sure that class is valid. This prevents us from having an invalid "state".
  3. Safe for asynchronous code
    • It's safe when working with asynchronous programming. Since the code we’ve created is immutable, we won’t have any thread synchronization problems since the data shouldn’t change.
  4. Better encapsulation
    • Passing our object through different methods sometimes may lead to some of its values being changed, but with immutable code this is NOT possible since we can’t change its values.
  5. Easy to test
    1. It’s obvious that if our classes can’t change once created, testing them will be much easier and simpler since our code, being immutable, is written to avoid "side effects" that may come from changing the value of some properties.

 

3 - Example of Immutability in Code

For this example, I'm going to create a service for the website I mentioned earlier, so we have an endpoint that reads our personal profile from the database.

The profile is a DTO that contains user information, such as name, surname, and of course, the id. Along with this info, there is also a list of skills we want to highlight on our profile. But these skills reside in another table in the database.

The first time we read the user from the database, we use a text id, not a numeric one. The reason is that this information comes from the URL the user is using to visit our site and to create a better user experience, our URL is something like webpersonal.com/personalprofile/ivanabad. As you can see, this ivanabad is in the URL instead of the corresponding id.

These are the models or classes we're going to use.

public class UserIdEntity
{
    public readonly string UserName;
    public readonly int UserId;
}
public class PersonalProfileEntity
{
    public readonly int UserId;
    public readonly string FirstName;
    public readonly string LastName;
    public readonly string Description;
    public readonly string Phone;
    public readonly string Email;
    public readonly string Website;
    public readonly string GitHub;
}
public class InterestEntity
{
    public readonly int UserId;
    public readonly string Description;
}
public class SkillEntity
{
    public readonly int UserId;
    public readonly int Id;
    public readonly string Name;
    public readonly int Punctuation;
}

As you see, we have a central object that links the URL to the ID in the database.

Also, the main object is PersonalProfileEntity and SkillEntity and InterestEntity are objects that will be inside the PersonalProfileDto.

Note: For space, I’ve omitted the constructors.

The UserIdEntity object isn’t really necessary, it's just there to make the example more understandable.

What we’re going to do is create a service that returns that dto. At first glance, it’s clear we need the 4 entities previously mentioned, so let’s create a (mutable) service where we have these entities.

Note: the service is implemented using asynchronous programming.

public class PersonalProfileService
{
    private readonly IPersonalProfileServiceDependencies _dependencies;
    private UserIdEntity UserId { get; set; }
    private List<InterestEntity> Interests { get; set; }
    private List<SkillEntity> Skills { get; set; }
    private PersonalProfileEntity PersonalProfile { get; set; }

    public PersonalProfileService(IPersonalProfileServiceDependencies dependencies)
    {
        _dependencies = dependencies;
    }

    public Task<PersonalProfileDto> GetPersonalProfileDto(string name)
    {
        throw new NotImplementedException();
    }
}

As we can see, we inject dependencies via dependency injection into the constructor.

In another video, we’ll see how to structure an application to completely avoid circular references.

Here’s the dependency interface:

public interface IPersonalProfileServiceDependencies
{
    Task<UserIdEntity> GetUserId(string name);
    Task<List<InterestEntity>> GetInterests(int userId);
    Task<List<SkillEntity>> GetSkills(int id);
    Task<PersonalProfileEntity> GetPersonalProfile(int id);
}

Now we just need methods that receive those entities and assign their values in the GetPersonalProfileDto method.

public class PersonalProfileService
{
    private readonly IPersonalProfileServiceDependencies _dependencies;
    private UserIdEntity _userId { get; set; }
    private List<InterestEntity> _interests { get; set; }
    private List<SkillEntity> _skills { get; set; }
    private PersonalProfileEntity _personalProfile { get; set; }

    public PersonalProfileService(IPersonalProfileServiceDependencies dependencies)
    {
        _dependencies = dependencies;
    }


    public async Task<PersonalProfileDto> GetPersonalProfileDto(string name)
    {
        await GetUserId(name);
        _ = Task.WhenAll(
            GetInterests(),
            GetSkills(),
            GetPersonalProfile()
        );

        return Map();
    }

    private async Task GetUserId(string name)
    {
        _userId = await _dependencies.GetUserId(name);
    }

    private async Task GetInterests()
    {
        _interests = await _dependencies.GetInterests(_userId.UserId);
    }
    private async Task GetSkills()
    {
        _skills = await _dependencies.GetSkills(_userId.UserId);
    }

    private async Task GetPersonalProfile()
    {
        _personalProfile = await _dependencies.GetPersonalProfile(_userId.UserId);
    }

    private PersonalProfileDto Map()
    {
        return new PersonalProfileDto()
        {
            Description = _personalProfile.Description,
            Email = _personalProfile.Email,
            FirstName = _personalProfile.FirstName,
            LastName = _personalProfile.LastName,
            GitHub = _personalProfile.GitHub,
            UserId = _userId.UserId,
            Phone = _personalProfile.Phone,
            Website = _personalProfile.Website,
            Interests = _interests.Select(a => a.Description).ToList(),
            Skills = _skills.Select(a => new SkillDto()
            {
                Id = a.Id,
                Name = a.Name,
                Punctuation = a.Punctuation
            }).ToList()
        };
    }
}

So, if we wrote our code as mutable, the service would be finished, but unfortunately, this style of programming has several flaws.

First, we mutate the class state inside the class itself by assigning property values in methods.

Second, in our main method GetPersonalProfileDto there's a key dependency to keep in mind, which is the ordering of the methods we call. Nothing stops us or warns us about the order. For example, we could call GetSkills before GetUserId and there’s nothing that indicates we’re doing it wrong, since the code compiles correctly.

To fix this mess, all we have to do is make our class immutable, which means returning the type needed from each method and passing the required id as a parameter for the queries.

public class PersonalProfileService
{
    private readonly IPersonalProfileServiceDependencies _dependencies;


    public PersonalProfileService(IPersonalProfileServiceDependencies dependencies)
    {
        _dependencies = dependencies;
    }


    public async Task<PersonalProfileDto> GetPersonalProfileDto(string name)
    {
        UserIdEntity userid = await GetUserId(name);
        List<InterestEntity> interests = await GetInterests(userid);
        List<SkillEntity> skills = await GetSkills(userid);
        PersonalProfileEntity personalProfile = await GetPersonalProfile(userid);

        return Map(userid, personalProfile, interests, skills);
    }

    private async Task<UserIdEntity> GetUserId(string name) =>
        await _dependencies.GetUserId(name);


    private Task<List<InterestEntity>> GetInterests(UserIdEntity userId) =>
        _dependencies.GetInterests(userId.UserId);

    private Task<List<SkillEntity>> GetSkills(UserIdEntity userId) =>
    _dependencies.GetSkills(userId.UserId);


    private Task<PersonalProfileEntity> GetPersonalProfile(UserIdEntity userId) =>
        _dependencies.GetPersonalProfile(userId.UserId);


    private PersonalProfileDto Map(UserIdEntity userId, PersonalProfileEntity personalProfile, List<InterestEntity> interests, List<SkillEntity> skills)
    {
        return new PersonalProfileDto()
        {
            Description = personalProfile.Description,
            Email = personalProfile.Email,
            FirstName = personalProfile.FirstName,
            LastName = personalProfile.LastName,
            GitHub = personalProfile.GitHub,
            UserId = userId.UserId,
            Phone = personalProfile.Phone,
            Website = personalProfile.Website,
            Interests = interests.Select(a => a.Description).ToList(),
            Skills = skills.Select(a => new SkillDto()
            {
                Id = a.Id,
                Name = a.Name,
                Punctuation = a.Punctuation
            }).ToList()
        };
    }
}

As you can see, we no longer have properties in the class, but instead, we return them from the needed methods. Also, if we tried to place the GetSkills method as the first one, the code wouldn’t allow it, since there’s a dependency on its input parameter.

I wanted to use a service in this example because I think the concept I want to convey is much clearer this way.

 

Conclusion

Creating immutable code has many advantages, including what I think is the most important: it's easy to read and maintain, which is a fundamental part of our daily work.

Not all code has to be immutable; there may be times when we need to modify properties, so we shouldn’t always restrict ourselves to immutability.

In the example, we saw a use case where we made a service immutable, but we can extend this logic throughout our entire application.

This post was translated from Spanish. You can see the original one here.
If there is any problem you can add a comment bellow or contact me in the website's contact form

Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2025 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café