2

I'm modeling a general RPG-game-style quest system where player choices lead to certain effects. The below examples have been simplified for the sake of clarity. This is a web-based context, which I suspect is relevant for the question at hand.

In short: How do I inject dependencies into a wide variety of implementations of the same interface, when those concrete classes will be serialized and stored, only to be retrieved at a later stage?


The scenario

When a player undertakes a quest, they will have to wait a while (quest time) and then be presented with an outcome (text), which provides some choices. Each choice will have some effects attached to them.

In reality, the backend already generated the outcome when you initially started your quest, and it stored this in the database. When you make your choice, the backend will simply execute the effect that was already predetermined (based on your choice).

As this is a web-based context, all the player's actions are separate web requests. No websockets, no (relevant) session tracking. Each web request stands on it own. The goal here is to generate the outcome in one request (StartQuest) and store it, and then when the other request comes in (MakeChoice), use the stored data to execute the planned actions.

This is the (simplified) model. QuestResult is a domain object that maps to a specific database table, but the Quest.Choices object will be serialized as a single JSON string and stored as a column in that table. This already works, using Newtonsoft which can track concrete types even when using interfaces for the property type. I'm omitting the actual DB entities here as this is not the focus of the question.

public class QuestResult
{
    public string Description { get; set; } // E.g. "you arrive at a fork in the road"
    public IEnuwerable<Choice> Choices { get; set; }
}

public class Choice
{
    public string Id { get; set; }        // Used as identifier
    public string Description { get; set; } // E.g. "go left"
    public IEnumerable<IEffect> Effects { get; set; }
}

public interface IEffect
{
    void Execute(MyDbContext context);
}

Note that I pass the context in Execute(), because I want to be able to pass in a specific context (and thus transaction) on which all listed effects can be executed. This context/transaction will be managed by the business service that resolves the player choice. It's job is essentially to receive the player's choice, deserialize the quest result, and perform the listed effects to the player's account.

For the sake of example, the possible effects are financial (money gain/loss) or being given an item. Because these data operations already happen in other parts of the codebase, there are existing IMoneyService and IInventoryService dependencies that do most of the heavy lifting of the actual underlying logic:

public interface IMoneyService
{
    void GiveMoneyTo(Guid playerId, long amount);
    void TakeMoneyFrom(Guid playerId, long amount);
}

public class MoneyService
{
    public MoneyService(MyDbContext context)
    {
        // ...
    }

    // Interface implementation
}

Important to note: the db context is being injected into the services. In the existing code, this is not an issue, since this is all resolved by dotnet's DI container.

I've created IEffect implementations that perform the needed effects:

public class MoneyEffect : IEffect 
{
    private Guid playerId;
    private long amount;

    public MoneyEffect(Guid playerId, long amount)
    {
        this.playerId = playerId;
        this.amount = amount
    }

    public void Execute(MyDbContext context)
    {
        // This is the problem! How to make use of IMoneyService?
    }
}

Serializing and deserializing the data works exactly like I expect it to. I can create a result:

var myQuestResult = new QuestResult()
{
    Description = "You find two chests, but only have enough time to open one.",
    Choices = new List<Choice>()
    {
        new Choice()
        {
            Id = "OPEN_BLUE",
            Description = "Open the blue chest",
            Effects = new List<IEffect>()
            {
                new MoneyEffect(myPlayerId, 500)
            }
        },
        new Choice()
        {
            Id = "OPEN_RED",
            Description = "Open the red chest",
            Effects = new List<IEffect>()
            {
                new MoneyEffect(myPlayerId, 1000)
            }
        }
    }
};

var questResultId = questResultRepository.Store(myQuestResult);

// In the future:

var deserializedQuestResult = questResultRepository.Get(questResultId);

I expected to be able to execute the made choice:

var playerChoiceId = "OPEN_RED";
var myDbContext = ...; 

var playerChoice = deserializedQuestResult.Choices.Single(x => x.Id == playerChoiceId);

foreach(var effect in playerChoice.Effects)
{
    effect.Execute(myDbContext);
}

The question here is how can I get MoneyEffect to make use of the registered IMoneyService dependency, without the parent logic needing to know the concrete type of each IEffect. Having to perform countless is MoneyEffect checks would be an OCP violation.


Something I may have forgotten to mention: my db context is registered as Scoped in the DI container, so there is only one context per web request. This means that I can blindly rely on my DI container to inject my context for me, rather than having to manually pass it all the way down the call stack.

Also, don't worry about the specific type of MyDbContext. It's a homebrew UOW, but effectively mirrors the EF DbContext that is contained within.


Possible ideas so far

  • I could just update the IEffect interface to have Execute() take in every possible service, but that pollutes the interface and forces consumers to pass a whole lot of dependencies that will go unused.
  • I thought about injecting IMoneyService in the MoneyEffect constructor, and having it serialized and deserialized. However, since MoneyService has its own MyDbContext dependency, I don't want to be serializing my db context for obvious reasons.
  • I could make it so the db context is passed into the IMoneyService methods instead of being injected into the MoneyService constructor, but this would break an otherwise fully working solution for all other use cases of these services, where the DI container is able to instantiate the whole dependency graph as every dependency (including the db context) has neatly been registered in the DI container.
  • I could use a service locator to pass into Execute instead of my context itself.

Currently, the best solution I see is using the service locator, but I'm wondering if there are better solutions for this that I have glossed over or missed?

3
  • Just thinking out loud: Can your (de-)serialization code coordinate with the DI framework to fill the non-serialized dependencies of the objects that are being deserialized? – Bart van Ingen Schenau May 18 at 13:36
  • 1
    @BartvanIngenSchenau: I was thinking along those lines too, but I think there's a clash between the serializer and the framework both trying to share the same constructor. When you separate them explicitly, you almost inherently end up having a service locator (which is effectively what resolves the DI input), unless there's another way to achieve the same thing that I don't see? – Flater May 18 at 13:37
  • @BartvanIngenSchenau: I found a question on StackOverflow that allows JSON.NET to invoke a specific constructor. JSON.net: how to deserialize without using the default constructor?. This doesn't really solve the problem, unless you can resolve the custom JSON deserializer with your DI framework. With this level of indirection, a little restructuring of the code might be the easier solution. – Greg Burghardt May 18 at 14:43
4

Full disclosure: I don't share your aversion to Service Locator type patterns when they're useful. I have often combined service location with DI in one of two ways to make dependencies more visible and testing easier:

  1. Make your dependencies injectable; but also make your implementations know where to find what they need.
class MoneyEffect {
    public void Execute(MyDbContext context)
    {
        Execute(context, Services.MoneyService);
    }
    
    public void Execute(MyDbContext context, MoneyService theMonies)
    {
        theMonies.do(...);
    }
}

If you want slightly more DI purity ...

  1. Inject an aggregate context.

The biggest con for this option is testing: You'll likely be creating a full GameContext/ServiceRegistry for each test (or test suite). But, you probably do something like this for your DI framework anyway.

class MoneyEffect {
    public void Execute(MyGameContext context)
    {
        var db = context.database;
        var moneyService = context.money;
        moneyService.do(...);
        db.do(...);
        // etc.
    }
}

You stay in control of what they operate on at the call site, and you stay out of the business of continually updating lists of method signatures.

The only real negative is that you're sort of forced to build more dependencies than needed to test any given IEffect, because each individual IEffect is asking for an aggregate containing things it probably doesn't use.

But, it's OK! Your tests will have a createTestGameContext() that builds the aggregate, so your individual tests will still look clean, and you probably won't even notice how greedy your IEffect's are.


There are other options out there, but I wouldn't recommend them offhand, as the level of "indirection" and "hypergeneralization" I've seen occur with them is mind-numbing. It becomes hard to keep the type system happy (from what I recall). And, the code can get really hard for newcomers to trace through.

But, at a high level, you could also consider having your effects "name" the service(s) they want to operate on, and have your services each implement common CRUDy methods for use with effects. Your outer loop would then look something like this:

foreach (var effect in effects) {
  IService services = registry.get(effect.targetServiceName);
  effect.Execute(dbContext, service);
}

^^ Could be amazing; could be a counterproductive rabbit hole.

2
  • Thanks for the input. I was mostly on the fence about service locators because I suspected that I may have been missing a better implementation (as is often the case when I hear something being called an antipattern but don't quite agree with that yet), but my case seemed like a valid enough use case for it. The last code snippet won't work though, services are much too varied in their methods and method signatures to have a generalized IService interface. – Flater May 19 at 7:02
  • I've followed your advice and simply let go of the notion that a service locator is an antipattern for my case. I am, however, still toying with option 2, i.e. the idea of creating a more specialized service locator (as opposed to just handing out my global service provider) to inject, so I can at least curb which services can be located willy nilly - but I'm putting that decision off until later, when the codebase is more fleshed out and I have a better picture of it. – Flater May 26 at 12:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.