The state pattern using Entity Framework

Have you heard about if-then driven logic? Or maybe something about switch driven logic? Aha, you have even seen it. Me too. Moreover, I keep seeing such logic quite often. Despite the fact that state pattern is known for decades from the Gang of Four, developers simply ignore it.

Let’s imagine typical class with if-then logic. Here is an example of login workflow.

public class User
{
    public int NumberOfAttempts { get; set; }
    public string Captcha { get; set; }
    public DateTimeOffset? BlockedUntil { get; set; }
    public UserState State { get; set; }

    public void Login(string password)
    {
        if (State != AttemptsToLogin) throw new InvalidOperationException();
        if (password == "test")
            State = IsAuthorized;
        else
        {
            NumberOfAttempts++;
            if (NumberOfAttempts > 2)
            {
                Captcha = Guid.NewGuid().ToString();
                State = InputsCaptcha;
            }
        }
    }

    public void InputCaptcha(string captcha)
    {
        if (State != InputsCaptcha) throw new InvalidOperationException();
        if (captcha == Captcha)
        {
            NumberOfAttempts = 0;
            State = AttemptsToLogin;
        }
        else
        {
            BlockedUntil = DateTimeOffset.UtcNow.AddHours(1);
            State = IsBlocked;
        }
    }

    public void Logout()
    {
        if (State != IsAuthorized) throw new InvalidOperationException();
        NumberOfAttempts = 0;
        State = AttemptsToLogin;
    }

    public enum UserState
    {
        AttemptsToLogin,
        IsAuthorized,
        InputsCaptcha,
        IsBlocked
    }
}

Problem

What’s wrong with if-then logic demonstrated above? I would say nothing while there are a few lines of code. Once the code gets bigger the logic is no longer looks OK. For instance, since the size of the class gets larger it would be nice to split.

Is there any easy way to apply the state pattern to the example above? I don’t want to break existing working logic by introducing huge change, which can take place by changing Entity Framework mapping.

Solution

Playing around, I have found as simple as a possible solution to refactor to the state pattern. You don’t even need to change Entity Framework mapping since state data are still persisted in its entity. Yep, it is a trade-off in favor of simplicity, why not?

public class User
{
    private UserState _userState;

    [NotMapped]
    public UserState State
    {
        get => _userState ?? (_userState = UserState.New(StateType, this));
        set => _userState = value;
    }

    public int Id { get; set; }
    public string StateType { get; set; }
    public int NumberOfAttempts { get; set; }
    public string Captcha { get; set; }
    public DateTimeOffset? BlockedUntil { get; set; }
}

public class UserAttemptsToLogin : UserState
{
    protected override void OnStart()
    {
        User.NumberOfAttempts = 0;
        User.Captcha = null;
    }

    public override void Login(string password)
    {
        if (password == "test")
            Become(new UserIsAuthorized());
        else
        {
            User.NumberOfAttempts++;
            if (User.NumberOfAttempts > 2)
                Become(new UserInputsCaptcha());
        }
    }
}

public class UserIsAuthorized : UserState
{
    public override bool HasAccess => true;

    public override void Logout()
    {
        Become(new UserAttemptsToLogin());
    }
}

public class UserInputsCaptcha : UserState
{
    protected override void OnStart()
    {
        User.Captcha = Guid.NewGuid().ToString();
    }

    public override void InputCaptcha(string captcha)
    {
        if (captcha == User.Captcha)
            Become(new UserAttemptsToLogin());
        else
            Become(new UserIsBlocked());
    }
}

public class UserIsBlocked : UserState
{
    protected override void OnStart()
    {
        User.BlockedUntil = DateTimeOffset.UtcNow.AddHours(1);
    }
}

Where UserState class is default state logic to derive from.

public abstract class UserState
{
    protected User User { get; private set; }
    public virtual void Login(string password) => throw new InvalidOperationException();
    public virtual void InputCaptcha(string captcha) => throw new InvalidOperationException();
    public virtual void Logout() => throw new InvalidOperationException();
    public virtual bool HasAccess => false;

    protected virtual void OnStart()
    {
    }

    protected void Become(UserState next)
    {
        next.User = User;
        next.OnStart();
        User.StateType = next.GetType().Name;
        User.State = next;
    }

    public static UserState New(string type, User user)
    {
        switch (type)
        {
            case nameof(UserIsAuthorized): return new UserIsAuthorized {User = user};
            case nameof(UserInputsCaptcha): return new UserInputsCaptcha {User = user};
            case nameof(UserIsBlocked): return new UserIsBlocked {User = user};
            default: return new UserAttemptsToLogin {User = user};
        }
    }
}

Unit tests may use HavingState extension method to turn the state into a required type.

[Test]
public void It_should_login()
{
    // Given
    var user = new User();

    // When
    user.State.Login("test");

    // Then
    Assert.That(user.State.HasAccess, Is.True);
    Assert.That(user.State, Is.TypeOf<UserIsAuthorized>());
}

[Test]
public void It_should_show_captcha()
{
    // Given
    var user = new User {NumberOfAttempts = 2}.HavingState<UserAttemptsToLogin>();

    // When
    user.State.Login("fail");

    // Then
    Assert.That(user.State.HasAccess, Is.False);
    Assert.That(user.Captcha, Is.Not.Null);
    Assert.That(user.State, Is.TypeOf<UserInputsCaptcha>());
}

[Test]
public void It_should_validate_captcha()
{
    // Given
    var captcha = Guid.NewGuid().ToString();
    var user = new User {Captcha = captcha}.HavingState<UserInputsCaptcha>();

    // When
    user.State.InputCaptcha(captcha);

    // Then
    Assert.That(user.Captcha, Is.Null);
    Assert.That(user.State, Is.TypeOf<UserAttemptsToLogin>());
}

[Test]
public void It_should_throw_error_in_UserAttemptsToLogin_state()
{
    // Given
    var user = new User().HavingState<UserAttemptsToLogin>();

    // When
    void InputCaptcha() => user.State.InputCaptcha("");
    void Logout() => user.State.Logout();

    // Then
    Assert.That(InputCaptcha, Throws.Exception.TypeOf<InvalidOperationException>());
    Assert.That(Logout, Throws.Exception.TypeOf<InvalidOperationException>());
}

In general, it became more expressive and easier to navigate through the code, just Go to Declaration. The similar approach described in the Akka-like state machine.

I believe this solution is super small and simple so the change from if-then to state pattern should not be a problem.

The complete example is in Gaev.Blog.Examples.StateViaEF.

Chat with blog beta
  • Assistant: Hello, I'm a blog assistant powered by GPT-3.5 Turbo. Ask me about the article.