-
Notifications
You must be signed in to change notification settings - Fork 151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Using Simple Injector in a game that relies heavily on dynamic assemblies and reflection? #425
Comments
In my experience, there is always a better solution than updating an existing container, and for that reason the container is locked at first use. Do note that this doesn't mean that it becomes impossible for registrations to get added, however, it is simply impossible to do this using the registration API (i.e. Can you describe what types are dynamically loaded and give some examples of this. I would like to know:
Please show examples of the design.
That is more easy to answer. Entities (or other data-centric objects) should not be built by a DI Container. Entities should be newed up directly in code and should not be constructed using dependencies that are resolved from a container. Instead, if data-centric objects have behavior that requires so called Volatile Dependencies (the things that containers resolve), use Method Injection and pass such dependency on to a method that requires the dependency. I happen to be writing a book that describes this subject in more detail. Here's an example from chapter 3 of the book that shows a public class Product
{
public string Name { get; set; }
public decimal UnitPrice { get; set; }
public bool IsFeatured { get; set; }
public DiscountedProduct ApplyDiscountFor(IUserContext user)
{
bool preferred = user.IsInRole(Role.PreferredCustomer);
decimal discount = preferred ? .95m : 1.00m;
return new DiscountedProduct(
name: this.Name,
unitPrice: this.UnitPrice * discount);
}
} In the chapter, the public class ProductService : IProductService
{
private readonly IProductRepository repository;
private readonly IUserContext userContext;
public ProductService(IProductRepository repository, IUserContext userContext)
{
this.repository = repository;
this.userContext = userContext;
}
public IEnumerable<DiscountedProduct> GetFeaturedProducts()
{
return
from product in this.repository.GetFeaturedProducts()
select product.ApplyDiscountFor(this.userContext);
}
} The
Your question is too broad. In general, it is okay for components to depend upon the container as long as those components are defined inside the Composition Root. Code outside the Composition Root (normal application code) should absolutely not depend on the container or an abstraction of any sort. Doing so leads to the Service Locator anti-pattern. Typically you should rely on Constructor Injection for injection of Dependencies into components (the classes that contain the application's behavior) and Method Injection for injection of Dependencies into data-centric objects, where injection happens at runtime, outside the composition root and outside sight of a container. |
The assemblies that would be loaded would be game content and the alike. Of course to keep things powerful, this content should be able to work mostly freely and of course define its own interfaces and types entirely. The reasson we need to have the container constructed before loading it is because it happens on connection to a server (it's a multiplayer game), but things like the main menu obviously need to load in before you're able to have a "connect" button. To give a simplified example of my use case, consider the following example. The game would load all children of the public class Enemy : Entity
{
// Called every second
public override void Update()
{
MoveRandom();
if (health <= 0)
{
Die();
}
}
private void Die()
{
IStatTracker tracker = // <???>
tracker.EntityKilled(this);
// ...
}
} Now, the code calling So while there is some overlap and that overlap will allow me communicate between the main and dynamic assembly, the dynamic assembly has a mind of its own.
Yep, which is actually what we currently have and I'm trying to phase out. |
Some extra clarification: this |
I understand that you have seperate assemblies that contain part of the game content and logic, but you yet have to explain why you need it to be loaded lazily, instead of during application start-up. It's also unclear to me whether you wish to have plug-in like behavior, where new assemblies are added while the application is running. Do note that content, and other data-related stuff are of no interest to the DI container, like I described above. Entities should not be initialized by, nor registered in the container. You might however have some sort of provider registered in the container that is able to provide the application with this data-related stuff. But again, lazy loading is possible, but how to implement it depends on what your application requires. Your basic options are:
|
Because we can't actually know which assembly to load into the game until the user connects to a multiplayer server, at which point things like the main menu, texture manager, game loop, etc... have all already initialized and these would of course have locked the container by now.
Yes, effectively.
Wouldn't this basically be taking over the job of the container for the types inside the dynamic assembly? Seems like kind of a waste in my ears.
Well, wouldn't you not be able to add them to the container since it's locked?
That's what I meant with "Should I add a type that has a reference to the container into the container itself, so certain objects can get the container if they have to?" in the original issue. Personally this feels like the best solution. But unless I'm misunderstanding what you're saying, that still doesn't solve the "what if this entity wants access to the |
Is your problem related to the client application or the game server? Does the server need to load assemblies based on a particular client? Does this mean you have many service instances? I can imagine one instance per game, with multiple players working on that game. Can you explain it's impossible for you to load all assemblies upon start-up? What is it that makes your application so different that pre-loading isn't an option? Does every client have its own set of assemblies? Do you create an assembly per client? Does each client require different implemented behavior? Since this is a multi-player game server, how does that work when you have multiple clients connecting to the server instance that require different behaviors? Do note however that being able to make registrations at runtime typically only artificially solves your problem, because it causes all kinds of hard to spot and hard to fix problems. For instance, how do handle thread-safety? You will likely want to be able to handle requests concurrently in your game server. Most containers don't guarantee register operations to be thread-safe and even if they do, you will have to do some locking yourself to prevent problems, which again might cause performance problems, especially in a high-performance game server. So it might seem to you that Simple Injector makes your life harder right now, but I would argue that it actually prevents you from making it almost impossible to solve problems that arise in the future because of changing an already built-up container. Do note that other DI Containers for .NET are actually now following Simple Injector in this respect. Just read at how Autofac is "deprecating the ability to update an Autofac container" and how Ninject introduced a new IReadonlyKernel in v4 to prevent updates being made to the container.
Like I said before, the only thing that is blocked is calling var container = new Container();
container.Register<ILogger, FileLogger>();
// Verift locks the container
container.Verify();
// GetInstance locks the container as well
var logger = container.GetInstance<ILogger>();
// Create an independent InstanceProducer. It can depend on stuff inside the container.
// example: class ServImpl : IService { public ServImpl(ILogger logger) { } }
var producer = Lifestyle.Transient.CreateProducer<IService, ServImpl>(container);
// Create a new ServImpl with an ILogger dependency
IService service = producer.GetInstance(); In other words, a provider can create new
I addressed this previously:
Seems unreasonable to state that Method Injection is off the table, since everything else is a bad practice. Can you explain why you think that Method Injection is not suitable in your case? |
Both the client and server will be loading an assembly or two, but it'd be different assemblies. On the server we can guarantee that the assemblies are loaded before the container is made (on startup), but the client every player uses to actually play the game can't. Players have a single installation, and the server sends over the assembly for the client to load on connect, so each server would have separate and individual content. The client can't really connect until after you display your main menu and load things and open a window etc... Method injection isn't possible because we can't know ahead of time what a specific entity type will want when you call generic methods like something that gets called on absolutely every entity in the game. The only real option you have at that point is to pass in literally everything, which would just ruin the point and basically be an even more unmaintainable version of the service locator. Say you have a generic virtual method Also, I'm aware that everybody is saying "modifying the container is bad", and I can see why, but I need a workable and maintainable alternative before I can accept that mentality. That's why I made this question. I'm fully willing to use full DI but not if it means making my code filled to the brim with slow and hard to understand reflection everywhere. |
It seems to me that your |
While that's nice in theory, it's not exactly practical in my experience when you've got a largely complex complex codebase and thousands of types (no joke) that all have custom dependencies and need to do something on click, interaction, keyboard event, ... The amount of complexity, hard to follow and debug code and all kinds of nonsense just confuses me to think about. |
So what did you do before you tried to integrate with Simple Injector and how did that work out? |
So, if I understand, the problem is primarily in the client application, since after it started, it will download its assemblies from the server. As I see it, this means that you are actually downloading the application itself. In other words, you could see the code that shows the menu, start-screen and downloads the dll as a different application. A shell that is responsible of starting the real application. In this respect, I see no problem in having either a start-up container (or use no container at all at that time) for the shell application, and a second application that gets populated after the assemblies have been downloaded. As a matter of fact, if you take a look at ASP.NET Core, you will see that they have the exact same approach. The |
We're still building the engine, and due to a long history the entire codebase is an inconsistent mess. Parts of it are using a static service locator inconsistently and inefficiently while the other parts are just impossible to follow messes. I can only say "it hasn't ever been fired up". What I do know however, is that the "an entity is an individual" works, it's been proven and modern game engines like Unity and UE4 use it. Code stays easy to follow and intuitive, yet it's powerful when used right. The client "reload" approach works I suppose but it's still not exactly that clean in practice. |
Well if you are absolutely sure that you need to be able to update an existing container and the suggestions provided here don't work in your particular, you will have to pick a different tool to do the job. You Always pick the right tool for the job. |
I'll see I guess. Thanks! |
So I'm not sure whether SimpleInjector is right for my use case or not. I want to use dependency injection, but due to the nature of my project, I feel like the lock after first use of a container might be a blocker. Is there a better way to solve my problem?
My first problem is as follows. I want to use DI in a multiplayer game. The issue is, I need to load content from an assembly at runtime, and I absolutely cannot do it before causing the container to lock. Of course I still want this dynamic content to be able to use DI, and access the singletons and alike inside my main engine container.
Is it possible to maybe make a second container for the dynamic content, that still has access to the main container?
My second problem is different. I'm using a lot of "get a list of all subtypes of X and instantiate them" for things like entity types. Now, because the container isn't generally accessible to the objects within it, how would I instantiate these types, while still allowing them to take advantage of DI?
Should I add a type that has a reference to the container into the container itself, so certain objects can get the container if they have to?
Thanks in advance.
The text was updated successfully, but these errors were encountered: