Skip to content
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

First draft of adding IOC container #1841

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

raphaelschmitz00
Copy link

PR Details

This is a first step for introducing an IOC container.
This would be a breaking change, so this PR is focused on only setting up the
"infrastructure" (which would be the braking change) without actually using it yet.

Description

The most important part is usage, in MyGameApp.cs
More at the issue

Related Issue

#1445

Motivation and Context

Types of changes

  • Docs change / refactoring / dependency upgrade
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • My change requires a change to the documentation.
  • I have added tests to cover my changes.
  • All new and existing tests passed.


public interface IGameBuilder
{
public ConfigurationManager Configuration { get; }
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConfigurationManager standardizes application settings in one place.
They can come from a file, command line argument, environment variable, etc.

They get combined and overwrite each other. For example, ASP default is
to have appsettings.json, the values of which can be overwritten in debug
or release mode, by having appsettings.Development.json or
appsettings.Production.json respectively.

We can set up a sensible default order for these sources, but out of the box
this ConfigurationManager allows any game developer to override it as they please.

A YAML configuration provider does not exist AFAIK, but should be trivial to implement by deriving from FileConfigurationProvider
https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.configuration.fileconfigurationprovider?view=dotnet-plat-ext-7.0

Another cool thing is that one possible source is "User Secrets"
https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-7.0&tabs=windows
Which allows a game developer team member to override any configuration value
without touching anything in the project folder.

It would also be trivial to add dotnet's code for what they call "the options pattern"
https://learn.microsoft.com/en-us/dotnet/core/extensions/options
which allows an injected class to choose between getting a configuration object that
A) Just has the options from when the app started
B) Has updated values if the file was changed after app start
C) Allows to register a listener for when the file changes
Don't know if there's use for that in game dev except debugging, though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to my comment below, we'd probably want them (and the engine) constructing configuration through a series of configuration actions created as a collection via the .ConfigureHostConfiguration() call or similar. This could store as a List<Action<ConfigurationBuilder>> or List<Action<HostBuilderContext, ConfigurationBuilder>>.

While building the Game instance, an IConfiguration would be built using these actions instead of relying on ConfigurationManager at runtime (which may make File I/O calls). The code below demonstrates how that could be built:

var configBuilder = new ConfigurationBuilder(); // Other extensions applied.

foreach (Action<HostBuilderContext, IConfigurationBuilder> buildAction in _configureAppConfigActions)
{
    buildAction(_hostBuilderContext, configBuilder);
}
_appConfiguration = configBuilder.Build();

I haven't personally used ConfigurationManager much, so I may be off base.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC at least in Sdk.Web you can do it either way, and one way or the other isn't really more idiomatic than the other. So I guess it kinda comes down to the choice of also giving the users both options, or making a judgement call what to support.

Also, just in general - as a comment that's also an answer to other things further down: In .NET 8, the generic host was made kinda... more "accessible" (?) - to the point where in my own project, switching to the standard IHost allowed me to get rid of some homebrew stuff in favour of the Microsoft-provided standard.

If that's also a good fit for Stride - eh, you know it far better than me :D

public interface IGameBuilder
{
public ConfigurationManager Configuration { get; }
public IServiceCollection Services { get; }
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IServiceCollection standardizes registering dependencies.
It comes with many, many extension methods for convenience.
https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.iservicecollection?view=dotnet-plat-ext-7.0#extension-methods

IServiceCollection can not resolve services, however. For that, we need to call
IServiceCollection.BuildServiceProvider() first, to get an IServiceProvider.
This is a different from Stride's ServiceRegistry,
which can have servces added at any time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I've implemented this sort of thing in the past, I followed Microsoft's pattern whereby a List<Action<HostBuilderContext, IServiceCollection>> collection was constructed and then all assembled during a PopulateServiceCollection() method. This allowed me to call .ConfigureServices((context, services) => ...) as many times as I wanted.

{
public ConfigurationManager Configuration { get; }
public IServiceCollection Services { get; }
public ILoggingBuilder Logging { get; }
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ILoggingBuilder standardizes a way of configuring where to log to.
Many NuGet packages exist that work against this standard, from simple things like
files, console, Windows Event Log, to professional products like
Splunk or Sentry.

Even Slack and Telegram apparently... Maybe those are for something else.
Not gonna look into that, I doubt these will matter :D

In objects created by IServiceProvider, an ILogger<> can be received like this:
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-7.0#create-logs

However, it will take time to move services over to IServiceProvider.
If we hook up an ILogger<> to Stride's existing logging mechanism,
people could already use all those Nuget packages mentioned above.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the Microsoft ILogger<> would be nice. In many cases, it seems Stride rolled their own due to potential licensing issues. Microsoft.Extensions.Logging is MIT licensed now.

public ConfigurationManager Configuration { get; }
public IServiceCollection Services { get; }
public ILoggingBuilder Logging { get; }
public IAppEnvironment Environment { get; }
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ASP Core has IHostEnvironment, I created IAppEnvironment as counterpart.
I know we already have something similar, but that is only created later
after new Game() is called.

I wanted to highlight that something like this is needed as part of this GameBuilder already, for 2 reasons:

A) If reading from a file like appsettings.json for Configuration,
it needs to correctly get that file. ASP uses
IHostEnvironment.ContentRootFileProvider for this - we would need to check
if Stride's file reading stuff can already work here, without other dependencies etc.

B) Setting up the other parts of this builder usually already needs the
the info if the application is running in debug vs release mode.
"Set up profiler only if we are in debug mode" etc.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we wouldn't use the term "host"? Microsoft makes a distinction between Host and WebHost in many places. They also refer to the non-web version as the "Generic Host". In this case, the Host refers to something that manages the communication with the window context and the game engine proper. Perhaps I'm taking liberties by expanding it beyond a purely network-centric definition.

Interestingly, a custom configuration provider for Stride stuff could be created. I'm not really sure what that looks like though.

Copy link
Author

@raphaelschmitz00 raphaelschmitz00 Mar 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps I'm taking liberties by expanding it beyond a purely network-centric definition.

Actually, you did that exactly right. It took me some time to understand that the word "host" as they're using it in the context of "generic host" kinda has nothing to do with networking, and is more something along the lines of "the framework that 'hosts' your application on the computer".

So... "there are no solutions, only tradeoffs". If you end up going with the standard "host" naming - and taking into account that many people get into programming through game development - it would probably be a good idea to add documentation explaining this. Might save quite a few helpdesk tickets discord questions.

Comment on lines +51 to +54
using (var game = new Game())
{
game.Run();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would even go a step further and say that Game IS the host. Rather than inroducing another layer in the form of this StrideApplication, you'd simply let Game be worked out in Program.cs. This would allow something like:

Game.CreateBuilder().Build().Run();

and more advanced things like this:

Game.CreateBuilder()
.ConfigureServices(services => {
...
})
.Build()
.Run();

This is also the way Microsoft does it here (although their Host class is much more anemic than Game): https://github.com/dotnet/runtime/blob/57ea59f57eaa66523567bbef93275fbeaceccad1/src/libraries/Microsoft.Extensions.Hosting/src/Host.cs#L75

Currently, the Game class sets up the service registry during construction. We would take the construction of the ServiceProvider into the builder which would then be passed to the Game constructor. Event handling and everything else would remain the same mostly. We just need to figure out that mutability problem...

}

// Register dependencies
gameBuilder.Services.AddSingleton<Dependency>();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Singleton operates as expected, same with Transient, but Scoped needs special consideration. I believe that scoping to the Scene makes sense, but that would be quite a change to things. Use of serviceProvider.CreateScope() would be needed, but where would it go, and what does it mean for EntityProcessors and ScriptComponents?

I think the larger conversation around the mutability of registered services needs resolution. What are the primary use cases for mutating the service registry after initialization? Was it only necessary because there wasn't a strong DI framework? I suspect we can afford to be more opinionated about the service registry once a proper host builder is in place.

How would EntityProcessor and ScriptComponent use the new service provider?

Copy link
Author

@raphaelschmitz00 raphaelschmitz00 Mar 29, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I read on a blog post once that FOSS has 2 disadvantages compared to closed source software (also a bunch of advantages for sure, but the point is:):

  • Work that is less "attractive" is less likely to get done
  • People are much less likely to sit down in a corner of the office and draw funny shapes on a whiteboard.

IMHO using Scoped really needs the latter to add value. For example, when I discussed this with Manio, he was a proponent of a scope being a frame.


To give you my considerations from my game where I'm using this, for what it's worth:

It felt too much like "If you have a hammer, everything looks like a nail". It was certainly built with Sdk.Web in mind, and, well, think of the bread and butter for ASP, your usual B2B application: It's not out of the ordinary to have 1 million LOC, but a lot of it is edge-case stuff, like reports that are only generated once a month anyway. And a single web request only ever needs a very small part of that. Not having everything in memory constantly directly saves the company money by allowing cheaper server hosting with less RAM. Scoped just makes so much sense for such a web application.

But for a game engine, you're working with a whole different situation. Much more RAM available on average, and if it becomes a bottleneck, it's probably because of the assets, and not an overwhelming amount of code.

Honestly, I haven't encountered a hard reason for Transient in my game yet, either. My transient services are always referenced by services that are singletons in the end, so they effectively all have singleton lifetime - with the difference that multiple instances are created if it's used in multiple places. The only practical advantage I currently have is documentation a la "this class does not have state that I need to keep alive" (but that also kinda just because I decided to use it that way, for now).

Ugh, I've already written way more than I assumed this would be :D Anyway, my advice would be to just not even touch Scoped at first. If you want to do something with it, you can still add that later, the infrastructure is there - but it will become a whole own discussion.


And yeah, that kinda also goes for the EntityProcessor/ScriptComponent part, where I agree with the general sentiment that that's more of a future step. Once it is, indeed, in place - but also, sufficiently used throughout the codebase - some things will probably just "melt away" and produce a clearer picture how these things can work together.

I have a feeling already that the mechanism that's essentially activating and deactivating EntityProcessors should not be the same as the IoC container; that those are 2 distinct responsibilities... but that also strikes me as a question where you need some funny shapes on a whiteboard first.

gameBuilder.Services.AddSingleton<Dependency>();

// It is customary to use extension methods to group functionality.
gameBuilder.AddMyComponent();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idiomatic way would be to provide a series of built-in "configurators" as IHostBuilder extensions. These would facilitate the advanced startup configuration you're suggesting:

Game.CreateBuilder()
.ConfigureComponents((config, context, ...) => ...)
.ConfigureScenes(() => ...)
.Build()
.Run();

Having custom extensions is fine too. My argument is that some of these things should be provided by the engine for convenience.

Comment on lines +29 to +32
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clerical task, these should now be .NET 8.

}

// Again, these setup steps can be summarized into extension methods
app.UseMyComponent();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a comment on your code per se, but this got me thinking about aspnet middleware and I wonder how a similar app pipeline pattern might be leveraged in the context of a game engine.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought and experimented with that, too - and my result was, like another comment here "If you have a hammer...".

For a game engine, this is a solution in search of a problem; otherwise something probably immediately would have come to mind for this.


public interface IGameBuilder
{
public ConfigurationManager Configuration { get; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to my comment below, we'd probably want them (and the engine) constructing configuration through a series of configuration actions created as a collection via the .ConfigureHostConfiguration() call or similar. This could store as a List<Action<ConfigurationBuilder>> or List<Action<HostBuilderContext, ConfigurationBuilder>>.

While building the Game instance, an IConfiguration would be built using these actions instead of relying on ConfigurationManager at runtime (which may make File I/O calls). The code below demonstrates how that could be built:

var configBuilder = new ConfigurationBuilder(); // Other extensions applied.

foreach (Action<HostBuilderContext, IConfigurationBuilder> buildAction in _configureAppConfigActions)
{
    buildAction(_hostBuilderContext, configBuilder);
}
_appConfiguration = configBuilder.Build();

I haven't personally used ConfigurationManager much, so I may be off base.

Comment on lines +18 to +21
using (var game = new MyGameSetup().SetUpGame())
{
game.Run();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related to my above comment, this would be the point where we use:

Suggested change
using (var game = new MyGameSetup().SetUpGame())
{
game.Run();
}
Game.CreateBuilder().Build().Run();

It would then be idiomatic with other .NET setups. This could also simplify the setup of cross-platform projects. I wonder how initialization of certain services, scenes, or components could be further augmented within this.

Comment on lines +50 to +57
if (gameBuilder.Environment.IsDevelopment())
{
// set up logging
// The game imports Microsoft.Extensions.Logging.Console,
// But the engine only needs Microsoft.Extensions.Logging.
// (This example file is added to the engine so now it also needs console)
gameBuilder.Logging.AddConsole();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I'm too puritanical. I'm not a fan of reaching into properties on a Builder. Instead, I generally opt for a closed approach relying on configurator actions. Microsoft seems to have a similar mentality on this. Having overrides that accept the HostBuilderContext would give access to Environment within the scope of the configurator action. For example:

Game.CreateBuilder().ConfigureLogging((context, builder) => {
     if(context.Environment.IsDevelopment()) {
         builder.AddConsole();
     }
});

On a side note: we make heavy use of preprocessor directives. I wonder how we could ween ourselves off this reliance by providing a more robust environment class.

Comment on lines +77 to +86
private sealed class LoggingBuilder : ILoggingBuilder
{
public LoggingBuilder(IServiceCollection services)
{
Services = services;
}


public IServiceCollection Services { get; }
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beating a dead horse, configurator actions and supplying the raw ILoggingBuilder would be better. I also don't understand why the IServiceCollection would be set here since the ServiceProvider is the thing that matters and we likely want to control the collection instance within the builder.

Comment on lines +56 to +64
private static string GetEnvironment()
{
#if DEBUG
return Environments.Development;
#elif STAGING
return Environments.Staging;
#endif
return Environments.Production;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps more control could be given back to the developers. The ASPNETCORE_ENVIRONMENT environment variable is used for those kinds of application. A custom environment variable of our own to specify the game environment could be nice. It would also give devs more flexibility for custom environment types they may have. I do believe that expecting Development or Production as reserved values would be a good idea since we would want to use them for extension methods like .IsDevelopment().

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah looking back at this now, not happy with this anymore, especially since I learned that the whole idea of using Environment is to specifically not use the msbuild $(Configuration).

@niltor
Copy link

niltor commented Mar 5, 2024

I'd like to know what's going on with that proposal
The introduction of Host and IOC is a great job. They have been abstracted and suitable for all kinds of programs.

@VaclavElias
Copy link
Contributor

I guess we might need more feedback from the community? I personally like the familiarity with other .NET projects.

@raphaelschmitz00
Copy link
Author

Hey guys I just saw that there were these comments on this PR, but uh, it's like half a year later now. I've decided to go with openGL directly instead of Stride for my 2D game some time ago already, so just as an update, I'm unable to put much more energy into this.

However, on the positive side - at this point, I'm not really more familiar with these changes than another programmer familiar with the .NET generic host, so anybody can take ownership of this and not much of value is lost.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants