DEV Community

Cover image for Immutable DTOs in C# with interfaces
Jimmy
Jimmy

Posted on

Immutable DTOs in C# with interfaces

By using interfaces instead of classes when transferring data between our application layers, we can achieve immutability by only defining the getters in the interface.

public interface IEmployeeDto
{
    string Name { get; }
    string EmployeeNumber { get; }
}

The implementation of IEmployeeDto may have the setters to get the benefit of object initializers, just as a regular POCO.

public class EmployeeDto : IEmployeeDto 
{
    string Name { get; set; }
    string EmployeeNumber { get;  set; }
}

public class EmployeeService
{
    private readonly EmployeeRepository _employeeRepository;

    public EmployeeService(EmployeeRepository employeeRepository)
    {
        _employeeRepository = employeeRepository;
    }


    public void CreateNewEmployee(string name, string employeeNumber)
    {
        _employeeRepository.Add(new EmployeeDto()
        {
             Name = name,
             EmployeeNumber = employeeNumber
        }
    }
}

public class EmployeeRepository
{
    publc void Add(IEmployeeDto employee)
    {
        //employee is immutable
    }
}

Other implementations

That's the basics of it, but using interfaces as DTO's allow us to do more fun stuff with our code. I do a lot of integrations with other systems, and their api models never matches ours (surprise!). What I then do is something like the adapter pattern.

public interface IExternalEmployeeDto
{
    string FirstName { get; }
    string LastName { get; }
    int EmployeeNumber { get; }
}

public class ExternalEmployeeDto : IExternalEmployeeDto
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int EmployeeNumber { get;  set; }
}

public class EmployeeDto : IEmployeeDto 
{
    private readonly IExternalEmployeeDto _externalDto;

    string Name => "${_externalDto.FirstName} {_externalDto.LastName}";
    string EmployeeNumber => _externalDto.EmployeeNumber.ToString();

    public EmployeeDto(IExternalEmployeeDto externalDto)
    {
        _externalDto = externalDto;
    }
}

public class ExternalEmployeeService
{     
    private readonly EmployeeRepository _employeeRepository;

    public ExternalEmployeeService(EmployeeRepository employeeRepository)
    {
        _employeeRepository = employeeRepository;
    }

    public void CreateNewEmployee(IExternalEmployeeDto externalDto)
    {
        _employeeRepository.Add(new EmployeeDto(externalDto);
    }
}

Testing

Testing is another fun part that becomes easier with interface DTOs, as they don't depend on real implementations. We can create a Stub class for the interface with default values in the constructor (I must say, it does help to have ReSharper with the ctorp-snippet when it comes to create these).

public class StubExternalEmployeeDto : IExternalEmployeeDto
{
    public string FirstName { get; }
    public string LastName { get; }
    public int EmployeeNumber { get; }

    public StubExternalEmployeeDto(string firstName = "Jimmy", string lastName= "Mattsson", int empoyeeNumber = 1000)
    {
         FirstName = firstName;
         LastName = lastName;
         EmployeeNumber = empoyeeNumber;
    }
}

[TestFixture]
public class EmployeeDtoTests
{
    [Test]
    public void ShouldMapExternalEmployeeDtoToEmployeeDto()
    {
        //Asserting Name with default values
        Assert.AreEqual("Jimmy Mattsson", new EmployeeDto(new StubExternalEmployeeDto()).Name);

        //Asserting Name with diffrent LastName from default
        Assert.AreEqual("Jimmy Coding", new EmployeeDto(new StubExternalEmployeeDto(lastName: "Coding")).Name);

        //Asserting EmployeeNumber diffrent from default
        Assert.AreEqual("5000", new EmployeeDto(new StubExternalEmployeeDto(empoyeeNumber: 5000)).EmployeeNumber);
    }
}

Experiences

I must be horrible at testing, because almost all my time goes into making testing easier, so that I one day can become productive. The idea of using interfaces as DTOs actually comes from one of those experiences, when I had a much larger and nested model to work with.

The immutability actually came after that, it wasn't something I was hunting for and I take that as a bonus. To make the timeline correct, this article should be read backwards.

Trade-offs

Everything has trade-offs, so does this implementation. Having multiple implementations of an interface, both in production code and tests, makes it harder to change the interface. But as I view it, those interfaces are the contracts that our current business rules builds upon, if the interfaces changes, so have the business rules. It's an opportunity to look over all those implementations.

Upside is that the project won't even build if the updated interface isn't implemented. On other hand, the alternative to reusing the same class DTO in multiple cases may lead to some DTOs being overlooked.

Have I missed something? Comment!

Explore!

I admit, this may be a little over engineering in most cases. It's data being passing around between layers, it may not being suitable for a CRUD application.

But it's for a good cause, to make those complex test cases easier. I encourage you to explore and try things out. I've only written one test in this article, and it's testing getters. But the true power comes when there is more complexity, when the code should return different result depending on the input. At least I and my team has become more productive with this.

And you get immutability. If you need that.

Top comments (0)