Avoid Complexity with the State Design Pattern in .NET
Struggling with complex if-else chains in your growing codebase? Let me show you how the state design pattern can simplify logic, boost maintainability, and make your code easier to scale—with a practical, real-world example of a document flow.
Avoid Complexity with the State Design Pattern in .NET
I am reading a lot of code on the internet in open-source repositories, when making reviews on pull-requests and understanding other developers' code. One thing that I often see is code full of if-else statements checking tons of various states 😅
It's fine when we got a small application doing a simple operation/task, but imagine a codebase that is growing. If your code has a lot of conditional branches (if-else statements), it can be hard to read and understand by others (or yourself in half a year, when you need to refactor something).
How can we solve it? That is what I am going to show you in this article about the state design pattern. It will help you implement a more maintainable, extensible and testable code base in your projects. ✌️
I will show you a practical example of how to do it. More specifically, we will be building a document processing system, where the documents will go through various transitions and change state from e.g. draft to review, approved and finally published. 🔥
Before I show the good stuff, I want to briefly tell you a little about the state design pattern and what the real problem is with conditional logic. If you are ready, let's get to it.
💡 What is the State Design Pattern?
So... What is it really all about? The State Design Pattern is a behavioral design pattern that allows an object to alter its behavior when its internal state changes. It will do it by making an object change its class, providing a clean way to organize your code that has state-dependent behavior. 💪
Some key takeaways about the state pattern that I want you to know are 🧠
The class that contains the state and delegates state-specific behavior to the current state object is what we refer to as context.
A common interface for all concrete states is defined as a state interface.
Classes that implement the state interface and provide the behavior associated with a particular state are what we refer to as concrete states.
🤔 The Problem with Conditional Logic
Before I show you how to use the state design pattern, I would like to address the problem with excessive use of conditional logic in our software.
To demonstrate is with code, I have written a simple program for the Document transition that I will be fixing later. When a new state or behavior is added, the code-base grows exponentially, which is the big problem.
Another thing that you should consider is that I am breaking the single responsibility principle as I make my class handle all business logic for the states. While adding new states, it requires us to modify existing code, which is a violation of the open/closed principle. You can read more about SOLID in the article below.
And the last but not least thing is that testing our code (which is crucial) gets more complex, at you would have to test all conditional branches separately. Let's take a look at it, shall we 😄
public class Document
{
public enum Status
{
Draft,
InReview,
Approved,
Published
}
public Status CurrentStatus { get; private set; } = Status.Draft;
public string Content { get; set; }
public void Edit(string newContent)
{
if (CurrentStatus == Status.Draft)
{
Content = newContent;
}
else if (CurrentStatus == Status.InReview)
{
throw new InvalidOperationException("Cannot edit document in review");
}
else if (CurrentStatus == Status.Approved)
{
throw new InvalidOperationException("Cannot edit approved document");
}
else if (CurrentStatus == Status.Published)
{
throw new InvalidOperationException("Cannot edit published document");
}
}
public void Submit()
{
if (CurrentStatus == Status.Draft)
{
CurrentStatus = Status.InReview;
}
else if (CurrentStatus == Status.InReview)
{
throw new InvalidOperationException("Document is already in review");
}
// More conditionals...
}
public void Approve()
{
if (CurrentStatus == Status.InReview)
{
CurrentStatus = Status.Approved;
}
else if (CurrentStatus == Status.Draft)
{
throw new InvalidOperationException("Document must be reviewed before approval");
}
// More conditionals...
}
public void Publish()
{
// Even more conditionals...
}
}
Can you see the problems? We will end up breaking the SOLID principles and have a codebase growing exponentially and getting more complex with each addition. Let's fix that! 💪
✍️ Visualizing the State Pattern for our solution
Below is a class diagram that visualizes the state pattern structure for our document transitioning app in this article. It will be totally different from yours as it all depends on your requirements for the application you are building.
All states are inheriting from IDocumentState to make sure that all business logic is taken care of in the state implementation itself. This will promote the SOLID principles and make transitions explicit. What's not to like!? 🔥
Let's take a look at how a document can change states and the lifecycle of the document in our app.
In all its essence, it is fairly simple. A document will always be created in a draft state (the context class will handle that when constructing the document).
🧑💻 Implementing the State Pattern
So, let's move on to the good stuff and write some code that will solve our problem. I will start by defining the IDocumentState that will define the states for a Document.
Let's implement the concrete classes for each state. As mentioned earlier, they will inherit from the abstraction `IDocumentState`.
public class DraftState : IDocumentState
{
public void Edit(Document document, string content)
{
document.Content = content;
}
public void Submit(Document document)
{
document.SetState(new ReviewState());
}
public void Approve(Document document)
{
throw new InvalidOperationException("Cannot approve a draft document. It must be submitted for review first.");
}
public void Publish(Document document)
{
throw new InvalidOperationException("Cannot publish a draft document. It must be reviewed and approved first.");
}
public void Reject(Document document)
{
throw new InvalidOperationException("Cannot reject a draft document. It must be submitted for review first.");
}
public string GetStateName() => "Draft";
}
public class ReviewState : IDocumentState
{
public void Edit(Document document, string content)
{
throw new InvalidOperationException("Cannot edit a document that is under review.");
}
public void Submit(Document document)
{
throw new InvalidOperationException("Document is already under review.");
}
public void Approve(Document document)
{
document.SetState(new ApprovedState());
}
public void Publish(Document document)
{
throw new InvalidOperationException("Cannot publish a document under review. It must be approved first.");
}
public void Reject(Document document)
{
document.SetState(new DraftState());
}
public string GetStateName() => "In Review";
}
public class ApprovedState : IDocumentState
{
public void Edit(Document document, string content)
{
throw new InvalidOperationException("Cannot edit an approved document.");
}
public void Submit(Document document)
{
throw new InvalidOperationException("Document is already approved.");
}
public void Approve(Document document)
{
throw new InvalidOperationException("Document is already approved.");
}
public void Publish(Document document)
{
document.SetState(new PublishedState());
}
public void Reject(Document document)
{
document.SetState(new DraftState());
}
public string GetStateName() => "Approved";
}
public class PublishedState : IDocumentState
{
public void Edit(Document document, string content)
{
throw new InvalidOperationException("Cannot edit a published document.");
}
public void Submit(Document document)
{
throw new InvalidOperationException("Document is already published.");
}
public void Approve(Document document)
{
throw new InvalidOperationException("Document is already published.");
}
public void Publish(Document document)
{
throw new InvalidOperationException("Document is already published.");
}
public void Reject(Document document)
{
throw new InvalidOperationException("Cannot reject a published document.");
}
public string GetStateName() => "Published";
}
Finally we will implement the context class for the Document where the state of the document can be changed.
public class Document
{
private IDocumentState _currentState;
public string Content { get; set; }
public Document()
{
// Initial state is Draft
_currentState = new DraftState();
}
public void SetState(IDocumentState state)
{
_currentState = state;
}
public string GetCurrentStateName() => _currentState.GetStateName();
// Operations delegated to the current state
public void Edit(string content) => _currentState.Edit(this, content);
public void Submit() => _currentState.Submit(this);
public void Approve() => _currentState.Approve(this);
public void Publish() => _currentState.Publish(this);
public void Reject() => _currentState.Reject(this);
}
As you can see a Document is now capable of changing state to any of the defined states, as they have been defined using the abstraction IDocumentState for the state implementation.
The constructor of the Document class makes sure that the initial state of the document is set to DraftState. When you try to change the state of the document, each state will make sure to validate if you can actually change the state to that state your are trying to. It's readable and maintainable! No more if-else statements! 🙌
💛 Key take aways from the changed implementation
By refactoring the implementation from the conditional logic to the state pattern, we have achieved several benefits.
We have made the Document state transitions explicit. This means transitions are now clear and contained within the Document itself.
All conditional logic has been removed, as each state is responsible of its own behavior. By doing this we conform with the single responsibility principle.
Adding new states is made easy, simply by adding a new state class and inheriting from the abstraction. By doing this we comply with the open/closed principle, as new states doesn't require us to change existing code.
Testing is now made much easier as we can test each state implementation isolated.
Now that you have seen how to implement the business logic, let's take a look at how we can unit test it properly.
✅ Unit Testing the implementation
Alright, we have the business logic in place but what about testing. Let's see how we can properly unit test our business logic for the states using xUnit, NSubstitute, and AwesomeAssertions for adding some syntactic sugar.
using System;
using FluentAssertions;
using NSubstitute;
using Xunit;
public class DocumentStateTests
{
[Fact]
public void Document_ShouldStartInDraftState()
{
// Arrange
var document = new Document();
// Act & Assert
document.GetCurrentStateName().Should().Be("Draft");
}
[Fact]
public void DraftDocument_CanBeEdited()
{
// Arrange
var document = new Document();
var newContent = "Updated content";
// Act
document.Edit(newContent);
// Assert
document.Content.Should().Be(newContent);
}
[Fact]
public void DraftDocument_CanBeSubmittedForReview()
{
// Arrange
var document = new Document();
// Act
document.Submit();
// Assert
document.GetCurrentStateName().Should().Be("In Review");
}
[Fact]
public void DocumentInReview_CannotBeEdited()
{
// Arrange
var document = new Document();
document.Submit(); // Move to review state
// Act & Assert
Action editAction = () => document.Edit("New content");
editAction.Should().Throw<InvalidOperationException>()
.WithMessage("Cannot edit a document that is under review.");
}
[Fact]
public void DocumentInReview_CanBeApproved()
{
// Arrange
var document = new Document();
document.Submit(); // Move to review state
// Act
document.Approve();
// Assert
document.GetCurrentStateName().Should().Be("Approved");
}
[Fact]
public void DocumentInReview_CanBeRejected()
{
// Arrange
var document = new Document();
document.Submit(); // Move to review state
// Act
document.Reject();
// Assert
document.GetCurrentStateName().Should().Be("Draft");
}
[Fact]
public void ApprovedDocument_CanBePublished()
{
// Arrange
var document = new Document();
document.Submit(); // Move to review state
document.Approve(); // Move to approved state
// Act
document.Publish();
// Assert
document.GetCurrentStateName().Should().Be("Published");
}
[Fact]
public void PublishedDocument_CannotBeEdited()
{
// Arrange
var document = new Document();
document.Submit(); // Move to review state
document.Approve(); // Move to approved state
document.Publish(); // Move to published state
// Act & Assert
Action editAction = () => document.Edit("New content");
editAction.Should().Throw<InvalidOperationException>()
.WithMessage("Cannot edit a published document.");
}
}
That is how you can test the transition workflow for a document. But what about a real-world project? Are you interested in seeing how this could be applied in e.g. a web API?
✨ A Real-World example - Document Workflow API
We have the business logic in place for how a document should be able to change state. Let's try implementing this in an actual application. For demonstration purposes, I will be creating a Document Workflow API which allows users to manage documents in a document repository.
Here is the code seen from a high-level to give you an idea on how I am designing the controller using the document repository.
This is a simple API that uses the state of each document to handle requests. I know business logic should not live in the controller actions, but for demo purposes it should be fine. 😅
🙌 Improving the pattern with dependency injection
You might have given it a thought, but how can we act in the state implementations when something happens and we want to trigger something outside the state class? I am so glad you asked 😆
In a real world scenario, we probably want to use dependency injection in our states to apply more complex logic. For demo purpose, I will show you an example with the ReviewState. We are going to refactor the Approve state a little by making it validating the document and ecent send a notification on success.
public class ReviewState : IDocumentState
{
private readonly INotificationService _notificationService;
private readonly IDocumentValidator _validator;
public ReviewState(INotificationService notificationService, IDocumentValidator validator)
{
_notificationService = notificationService;
_validator = validator;
}
public async Task Edit(Document document, string content)
{
throw new InvalidOperationException("Cannot edit a document that is under review.");
}
public async Task Submit(Document document)
{
throw new InvalidOperationException("Document is already under review.");
}
public async Task Approve(Document document)
{
// Check if the document meets requirements
var validationResult = await _validator.ValidateForApproval(document);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.ErrorMessage);
}
var approvedState = new ApprovedState(_notificationService);
document.SetState(approvedState);
// Notify relevant parties
await _notificationService.NotifyDocumentApproved(document);
}
// You can add more methods if you need that...
public string GetStateName() => "In Review";
}
Get the point? 😊 Now we are performing validation using IDocumentValidator where you can have different business logic to make sure that a document is OK to be approved. When the document is approved a notification is sent or published in the app using the INotificationService.
🗂️ Persisting State with Entity Framework Core
So... one thing that will be a challenge for you is persisting the state of a document in a database. I am almost always using EF Core to persist my data, as code is generated from the model itself.
Let's start by defining a DocumentEntity that we can use to represent documents in the database.
public class DocumentEntity
{
public int Id { get; set; }
public string Content { get; set; }
public string StateName { get; set; } // Store state name as string
}
Now let's add a simple implementation of the IDocumentRepository that we used in our DocumentController earlier. For this we need the database context and a factory to create state objects.
We can start by writing the factory. I will name it IDocumentStateFactory and add a method named CreateState which is responsible for creating the state using the state name.
public interface IDocumentStateFactory
{
IDocumentState CreateState(string stateName);
}
public class DocumentStateFactory : IDocumentStateFactory
{
private readonly IServiceProvider _serviceProvider;
public DocumentStateFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IDocumentState CreateState(string stateName)
{
return stateName switch
{
"Draft" => _serviceProvider.GetRequiredService<DraftState>(),
"In Review" => _serviceProvider.GetRequiredService<ReviewState>(),
"Approved" => _serviceProvider.GetRequiredService<ApprovedState>(),
"Published" => _serviceProvider.GetRequiredService<PublishedState>(),
_ => throw new ArgumentException($"Unknown state: {stateName}")
};
}
}
In the DocumentRepository we can now use this factory. It will just implement the two declared methods from the interface.
public class DocumentRepository : IDocumentRepository
{
private readonly DocumentDbContext _dbContext;
private readonly IDocumentStateFactory _stateFactory;
public DocumentRepository(DocumentDbContext dbContext, IDocumentStateFactory stateFactory)
{
_dbContext = dbContext;
_stateFactory = stateFactory;
}
public Document GetById(int id)
{
var entity = _dbContext.Documents.Find(id);
if (entity == null)
{
throw new DocumentNotFoundException(id);
}
var document = new Document();
document.Id = entity.Id;
document.Content = entity.Content;
// Recreate the state object based on the stored state name
var state = _stateFactory.CreateState(entity.StateName);
document.SetState(state);
return document;
}
public void Save(Document document)
{
var entity = _dbContext.Documents.Find(document.Id) ?? new DocumentEntity { Id = document.Id };
entity.Content = document.Content;
entity.StateName = document.GetCurrentStateName();
if (entity.Id == 0)
{
_dbContext.Documents.Add(entity);
}
_dbContext.SaveChanges();
}
}
Finally we have to register our services to the service container so the app knows how to resolve the services for dependency injection. I have made an extension class for IServiceCollection to handle that.
public void AddDocumentStates(this IServiceCollection services)
{
services.AddScoped<DraftState>();
services.AddScoped<ReviewState>();
services.AddScoped<ApprovedState>();
services.AddScoped<PublishedState>();
services.AddScoped<IDocumentStateFactory, DocumentStateFactory>();
services.AddScoped<IDocumentRepository, DocumentRepository>();
// Other services...
}
Add the migrations, and woila you have solution that is able to persist the states in the database as well.
💭 When should you use the state pattern, and when should you avoid it?
This is the hard part - when should you implement the state pattern, and when should you avoid it to keep the code base as small as possible? 😅
✅ When to use it?
If you are looking for a way to avoid duplicating state-checks, you can utilize the state pattern to centralize your check behavior. This is great in scenarios where you have multiple behaviors associated with a single object, as we had in the example above.
If the object behavior depends on the current state of the object and you need to update the state during runtime, it would be a good idea to use the state pattern. This way you can avoid writing tens of if statements making the code complex to maintain.
When your application might change in the future in relation to the states and you would like to make sure that nothing currently implemented will break during addition of new states.
❌ When to avoid it?
If the object is not going to change state often or you already know that the business logic behind the object will remain the same from the first deployment, skip it.
When your objects only contains a few states and their behavior is very basic, implementing the full pattern might be a little overkill. This could be the case if you are just throing an exception like I did above for most of the state change operations.
💡
You should always ask yourself this question: Do I have complex transitions in object making the state change? If yes, apply the pattern as it makes your life easier (speaking of experience 🥵).
📝 Summary
In this .NET tutorial I have shown you how powerful a tool the state pattern is to manage complex state-dependent behaviors. By encapsulating our state-specific business logic into separate state classes, we can achieve the following:
Zero to none sprawling conditional logic in our code. The tens of hundreds if-else statements are forever gone when it comes to the state change.
The codebase is made maintainable and future proof as we can easily extend it and it is testable at the same time!
State transitions are made explicit and controlled. The code is easier to read for you and for others who will refactor or extend it in the future.
One thing I will tell you about design patterns is that they are awesome tools we have in our toolbox, but use them where it makes sense. Dont force them into your app because a senior developer or architect told you to in a task-description if it doesn't make sense.
If you got any questions, please let me know in the comments below. Until next time - happy coding! ✌️
My name is Christian. I am a 29-year-old Solution Architect & Software Engineer with a passion for .NET, Cloud, and Containers. I love to share my knowledge and teach other like-minded about tech.
Avoid Complexity with the State Design Pattern in .NET
Struggling with complex if-else chains in your growing codebase? Let me show you how the state design pattern can simplify logic, boost maintainability, and make your code easier to scale—with a practical, real-world example of a document flow.
— Christian Schou Køster
Avoid Complexity with the State Design Pattern in .NET
I am reading a lot of code on the internet in open-source repositories, when making reviews on pull-requests and understanding other developers' code. One thing that I often see is code full of
if-else
statements checking tons of various states 😅It's fine when we got a small application doing a simple operation/task, but imagine a codebase that is growing. If your code has a lot of conditional branches (if-else statements), it can be hard to read and understand by others (or yourself in half a year, when you need to refactor something).
How can we solve it? That is what I am going to show you in this article about the state design pattern. It will help you implement a more maintainable, extensible and testable code base in your projects. ✌️
I will show you a practical example of how to do it. More specifically, we will be building a document processing system, where the documents will go through various transitions and change state from e.g. draft to review, approved and finally published. 🔥
Before I show the good stuff, I want to briefly tell you a little about the state design pattern and what the real problem is with conditional logic. If you are ready, let's get to it.
💡 What is the State Design Pattern?
So... What is it really all about? The State Design Pattern is a behavioral design pattern that allows an object to alter its behavior when its internal state changes. It will do it by making an object change its class, providing a clean way to organize your code that has state-dependent behavior. 💪
Some key takeaways about the state pattern that I want you to know are 🧠
🤔 The Problem with Conditional Logic
Before I show you how to use the state design pattern, I would like to address the problem with excessive use of conditional logic in our software.
To demonstrate is with code, I have written a simple program for the Document transition that I will be fixing later. When a new state or behavior is added, the code-base grows exponentially, which is the big problem.
Another thing that you should consider is that I am breaking the single responsibility principle as I make my class handle all business logic for the states. While adding new states, it requires us to modify existing code, which is a violation of the open/closed principle. You can read more about SOLID in the article below.
And the last but not least thing is that testing our code (which is crucial) gets more complex, at you would have to test all conditional branches separately. Let's take a look at it, shall we 😄
Can you see the problems? We will end up breaking the SOLID principles and have a codebase growing exponentially and getting more complex with each addition. Let's fix that! 💪
✍️ Visualizing the State Pattern for our solution
Below is a class diagram that visualizes the state pattern structure for our document transitioning app in this article. It will be totally different from yours as it all depends on your requirements for the application you are building.
All states are inheriting from
IDocumentState
to make sure that all business logic is taken care of in the state implementation itself. This will promote the SOLID principles and make transitions explicit. What's not to like!? 🔥Let's take a look at how a document can change states and the lifecycle of the document in our app.
In all its essence, it is fairly simple. A document will always be created in a draft state (the context class will handle that when constructing the document).
🧑💻 Implementing the State Pattern
So, let's move on to the good stuff and write some code that will solve our problem. I will start by defining the
IDocumentState
that will define the states for a Document.Let's implement the concrete classes for each state. As mentioned earlier, they will inherit from the abstraction `IDocumentState`.
Finally we will implement the context class for the
Document
where the state of the document can be changed.As you can see a
Document
is now capable of changing state to any of the defined states, as they have been defined using the abstractionIDocumentState
for the state implementation.The constructor of the
Document
class makes sure that the initial state of the document is set toDraftState
. When you try to change the state of the document, each state will make sure to validate if you can actually change the state to that state your are trying to. It's readable and maintainable! No moreif-else
statements! 🙌💛 Key take aways from the changed implementation
By refactoring the implementation from the conditional logic to the state pattern, we have achieved several benefits.
Document
state transitions explicit. This means transitions are now clear and contained within the Document itself.Now that you have seen how to implement the business logic, let's take a look at how we can unit test it properly.
✅ Unit Testing the implementation
Alright, we have the business logic in place but what about testing. Let's see how we can properly unit test our business logic for the states using xUnit, NSubstitute, and AwesomeAssertions for adding some syntactic sugar.
That is how you can test the transition workflow for a document. But what about a real-world project? Are you interested in seeing how this could be applied in e.g. a web API?
✨ A Real-World example - Document Workflow API
We have the business logic in place for how a document should be able to change state. Let's try implementing this in an actual application. For demonstration purposes, I will be creating a Document Workflow API which allows users to manage documents in a document repository.
Here is the code seen from a high-level to give you an idea on how I am designing the controller using the document repository.
This is a simple API that uses the state of each document to handle requests. I know business logic should not live in the controller actions, but for demo purposes it should be fine. 😅
🙌 Improving the pattern with dependency injection
You might have given it a thought, but how can we act in the state implementations when something happens and we want to trigger something outside the state class? I am so glad you asked 😆
In a real world scenario, we probably want to use dependency injection in our states to apply more complex logic. For demo purpose, I will show you an example with the
ReviewState
. We are going to refactor theApprove
state a little by making it validating the document and ecent send a notification on success.Get the point? 😊 Now we are performing validation using
IDocumentValidator
where you can have different business logic to make sure that a document is OK to be approved. When the document is approved a notification is sent or published in the app using theINotificationService
.🗂️ Persisting State with Entity Framework Core
So... one thing that will be a challenge for you is persisting the state of a document in a database. I am almost always using EF Core to persist my data, as code is generated from the model itself.
Let's start by defining a
DocumentEntity
that we can use to represent documents in the database.Now let's add a simple implementation of the
IDocumentRepository
that we used in ourDocumentController
earlier. For this we need the database context and a factory to create state objects.We can start by writing the factory. I will name it
IDocumentStateFactory
and add a method namedCreateState
which is responsible for creating the state using the state name.In the
DocumentRepository
we can now use this factory. It will just implement the two declared methods from the interface.Finally we have to register our services to the service container so the app knows how to resolve the services for dependency injection. I have made an extension class for
IServiceCollection
to handle that.Add the migrations, and woila you have solution that is able to persist the states in the database as well.
💭 When should you use the state pattern, and when should you avoid it?
This is the hard part - when should you implement the state pattern, and when should you avoid it to keep the code base as small as possible? 😅
✅ When to use it?
If you are looking for a way to avoid duplicating state-checks, you can utilize the state pattern to centralize your check behavior. This is great in scenarios where you have multiple behaviors associated with a single object, as we had in the example above.
If the object behavior depends on the current state of the object and you need to update the state during runtime, it would be a good idea to use the state pattern. This way you can avoid writing tens of if statements making the code complex to maintain.
When your application might change in the future in relation to the states and you would like to make sure that nothing currently implemented will break during addition of new states.
❌ When to avoid it?
If the object is not going to change state often or you already know that the business logic behind the object will remain the same from the first deployment, skip it.
When your objects only contains a few states and their behavior is very basic, implementing the full pattern might be a little overkill. This could be the case if you are just throing an exception like I did above for most of the state change operations.
📝 Summary
In this .NET tutorial I have shown you how powerful a tool the state pattern is to manage complex state-dependent behaviors. By encapsulating our state-specific business logic into separate state classes, we can achieve the following:
One thing I will tell you about design patterns is that they are awesome tools we have in our toolbox, but use them where it makes sense. Dont force them into your app because a senior developer or architect told you to in a task-description if it doesn't make sense.
If you got any questions, please let me know in the comments below. Until next time - happy coding! ✌️
🌐 References
My name is Christian. I am a 29-year-old Solution Architect & Software Engineer with a passion for .NET, Cloud, and Containers. I love to share my knowledge and teach other like-minded about tech.
On this page