Using interfaces to bridge between .net framework and platform

584 words · 2 min read · 93 views

A lot of my professional work involves maintaining legacy software. I really like the challenges that come with bringing old software up to (semi) modern standards while keeping the same functionality. But sometimes that functionality is not what was desired at the time of creation, so we get to fix 10+ year old bugs.

The Ghost in the Machine

One issue I've been working on lately has been known for years but because the effort needed to fix it was so high compared to the perceived gain, it wasn't prioritized. But as other systems have evolved, this issue has become more visible, causing real pain for other teams.

A Classic Race Condition

At its core, it’s a race condition. Because of the way the systems were set up, the problem got more severe as the product got more popular. We had a structure where the web API fronts didn't publish messages to the queues straight away, but instead sent them to an intermediate publisher. This publisher would then read the state of the object, serialize the data, and publish it.

The problem is that over time, this caused messages to be filled with newer data than what was meant to be published. For example, an entity could be created and then updated within a few milliseconds. The publisher wasn't fast enough to see the "creation" message in time to query the correct data, so the data ended up being the same in both the "creation" and "update" messages.

Most consumers at the time used state propagation, so they didn't suffer much. But as newer consumers joined in, they began to feel the limits and the pains of various work-arounds.

The .NET Divide

What looked like a small fix turned out to be much bigger. Most of the complexity came from the fact that some projects in the solution had been migrated to .NET Core, while most of the actual APIs still used .NET Framework. This meant there was no single model for the entity data, as the newer projects used EF Core and the older ones were stuck on EF6. This also caused code duplication in the business logic layers since there were no shared .NET Standard objects. Bridging the Gap with Contracts

Since I wanted my solution to be as non-invasive as possible, I went with a contract-first approach. I created interfaces for all the shared EF entities in a .NET Standard library, exposing only the properties that were required. Then in the EF projects, I added partial definitions to the entities. C#

// Contracts (.NET Standard)
public interface ICart {
    ICollection<IProduct> Products { get; }
} 

public interface IProduct {
    string ArticleNumber { get; }
}

// Framework / Platform EF-entities
public partial class DbCart : Contracts.ICart {
    public ICollection<IProduct> Products => this.DbProducts.Cast<Contracts.IProduct>().ToList();
}

public partial class DbProduct : Contracts.IProduct {
    // Explicitly mapping the interface property to the DB column
    string Contracts.IProduct.ArticleNumber => this.ArticleNumber;
}

The Result: Reliable Snapshots

With this new set of interfaces, I could extract the message serializer so that it could be used from the web front ends as well as the publisher. Now, before we ask the publisher to do its thing, we serialize and save the payload snapshot.

This way, the publisher only needs to read the pre-packaged data, effectively killing the race condition. It’s a fair amount of code to bridge these gaps, but hopefully we can use these contracts to fix the code duplication issues as well.