Skip to content
This repository has been archived by the owner on Jan 15, 2023. It is now read-only.

Dependency injection #91

Closed
RupertAvery opened this issue May 10, 2019 · 17 comments
Closed

Dependency injection #91

RupertAvery opened this issue May 10, 2019 · 17 comments

Comments

@RupertAvery
Copy link
Contributor

How are controllers dependency injected?

I tried creating a SimpleContainer and registering a dependency class and pass it onto the ChromelyConfiguration

IChromelyContainer container = new SimpleContainer();
container.RegisterInstance<IService>("myService", new MyService());
ChromelyConfiguration config = ChromelyConfiguration
                                              .Create(container)

But if I put anything in the constructor of my Controller, it never gets instantiated. The controller class is only instantiated if the constructor has no arguments.

public MyController(IService myservice)
@mattkol
Copy link
Member

mattkol commented May 10, 2019

@RupertAvery yes, that is by design. Only default constructor. The Controller in this regard is just a placeholder, a container, if you will, for actionable methods.

You can still achieve what you want though, but not via constructor dependency injection.

Something like:

On initialization:

IChromelyContainer container = new SimpleContainer();
container.RegisterInstance<IService>("myService", new MyService());
ChromelyConfiguration config = ChromelyConfiguration
                                              .Create(container)

In controller:

        private ChromelyResponse Execute(ChromelyRequest request)
        {
            var response = new ChromelyResponse(request.Id);
             ----
           var service = IoC.GetInstance(typeof(MyService), "myService");
            ---
            return response;
        }

@RupertAvery
Copy link
Contributor Author

Thanks, however, it kind of breaks the whole point of dependency injection.

I'm curious, is this an architectural limitation?

Also noticed that the framework creates the controllers only once, whereas in MVC the controllers are created per request. This may have implications on how objects should be instantiated, if created in the constructor.

@RupertAvery
Copy link
Contributor Author

I've taken a look at the code and seen how it works, thanks!

@mattkol
Copy link
Member

mattkol commented May 10, 2019

@RupertAvery yw.

Chromely is intentionally made super simple for most developers. But also flexible for advanced developers like you. If you want to make it work the way you want this is achievable.

The Controller attribute was designed intentionally for that - ControllerPropertyAttribute. So you can leverage the Name and Route properties for that.

Instead of creating the controller object on the fly using Activator, you use Name and the Route to access a pre-registered contoller in the Container.

So you will have something like:

    [ControllerProperty(Name = "DemoController001", Route = "democontroller001")]
    public class DemoController : ChromelyController
    {
    }
IChromelyContainer container = new MoreSoftistcatedContainer();
container.RegisterInstance<IService>("myService", new MyService());
container.RegisterInstance<ChromelyController>("DemoController001", typeof(MyController));
ChromelyConfiguration config = ChromelyConfiguration
                                              .Create(container)

.. and retrieve the controller object in the RouteScanner.

If this is considered a common requirement we can look into implementing it, but I bet you most developers will forget to add unique name and route which may cause collision.

@Fiahblade @FrankPfattheicher

@RupertAvery
Copy link
Contributor Author

I understand, it's just that developers may be coming from an MVC background, so it might make sense to follow existing conventions, such as using HttpPost, HttpGet, Route attributes (Is copying an API for the purpose of compatibility a problem?) and also perhaps using .net core's existing IServiceCollection would also help devs looking to port existing code over.

@mattkol
Copy link
Member

mattkol commented May 16, 2019

@RupertAvery actually this is "following existing convention". But not really MVC per se - NancyFx.

Nothing here is written in stone, but simplicity is the key word. So I think we should consider changes but still keeping the architecture as simple as possible.

@RupertAvery
Copy link
Contributor Author

Sure.

I've written a MVC-style approach that only requires ChromelyConfiguration to have an interface, and have most everything in Chromely.CefSharp.WinApi that uses ChromelyConfiguration
to change to IChromelyConfiguration, so a rather small change.

It's a separate project, right now I've named in Chromely.Mvc, maybe name it Chromely.Restful.Mvc? Or I could name it something else if I can't use Chromely.

Initialization looks like this:

                IChromelyConfiguration config = MvcChromelyConfiguration
                                        .Create()
                                        .WithAppArgs(args)
                                        .WithHostMode(WindowState.Normal, true)
                                        .WithHostTitle("delphi")
                                        .WithHostIconFile("chromely.ico")
                                        .WithStartUrl(startUrl)
                                        .WithLogFile("logs\\chromely.cef_new.log")
                                        .WithLogSeverity(LogSeverity.Info)
                                        .UseDefaultLogger()
                                        .UseDefaultResourceSchemeHandler("local", string.Empty)
                                        .UseDefaultHttpSchemeHandler("http", "chromely.com")
                                        .WithControllersAssembly(Assembly.GetExecutingAssembly())
                                        .UseMvc((routes) =>
                                        {
                                            routes.MapRoute("default", "/{controller}/{action}");
                                        })
                                        .RegisterServices((serviceCollection) =>
                                        {
                                            serviceCollection.AddTransient<ITimesheetBL, TimesheetBL>();
                                            serviceCollection.AddSingleton<ILocalDataBL, LocalDataBL>();
                                            serviceCollection.AddSingleton<IOracleService, OracleService>();
                                        })
                                        .UseDefaultJsHandler("boundControllerAsync", true);

So not too different. There are some attributes HttpGet, HttpPost, etc. Controllers can look like this:

    public class DelphiController : ChromelyController
    {
        private ITimesheetBL _timesheetBL;
        private ILocalDataBL _localDataBL;

        public DelphiController(ITimesheetBL timesheetBL, ILocalDataBL localDataBL)
        {
            _timesheetBL = timesheetBL;
            _localDataBL = localDataBL;
        }

        [HttpGet]
        public ChromelyResponse IsFirstRun(ChromelyRequest request)
        {
            ChromelyResponse response = new ChromelyResponse();
            response.Data = _localDataBL.IsFirstRun();
            return response;
        }

        [HttpGet]
        public ChromelyResponse GetLocalProjectTasks(ChromelyRequest request)
        {
            ChromelyResponse response = new ChromelyResponse();
            response.Data = _localDataBL.GetProjectTasks();
            return response;
        }

So it's only a matter of configuration, no major changes on Chromely side to use this method.

Controllers are created on demand (transient) and using the serviceProvder created inside the configuration, so Dependency Injection just works.

@mattkol
Copy link
Member

mattkol commented May 17, 2019

@RupertAvery ChromelyConfiguration is central to how Chromely works. Like you I love to program to interface too, as a matter of principle. But not everything is "better achieved" by interfaces. There was a reason why this approach was chosen. Don't get me wrong, I am not saying it cannot be changed, but I am not seeing a strong reason with what you want to do.

  • What you want to do can be done in a less intrusive way
  • Since ChromelyConfiguration was made a Singleton (for a good reason that I can explain), passing it around in classes is no longer required. I have just never had bandwidth to clean that up.
  • This change will also require huge regression testing, and that will require hours that I am not sure @Fiahblade @FrankPfattheicher or I may have that bandwidth for at the moment.
  • Also ChromelyConfiguration has singleton invocations all over the place, this will break that approach, as the developer may have different implementations.

debugging = ChromelyConfiguration.Instance.DebuggingMode;

debugging = ChromelyConfiguration.Instance.DebuggingMode;

else if (ChromelyConfiguration.Instance.HostFrameless)

But I am ready to learn if there are easier way around that.

Why is ChromelyConfiguration a singleton? If a developer wants to have more than one window opened in a process, CefGlue and CefSharp will throw an exception if you register the the same scheme handlers and resource handlers more than once. Also there are cases like the link I sent where changes are easier done by accessing functionalities in a singleton pattern without a huge change to the framework. But the primary reason is the scheme and resource handlers registation.

My suggestion for now is to use extension method or inheritance, then gradually we can have bandwidth for this change if need be.

@RupertAvery
Copy link
Contributor Author

Thanks for the feedback! I've rewritten it as a separate configuration object, which works out much simpler and avoids changes to Chromely.

@mattkol
Copy link
Member

mattkol commented May 17, 2019

@RupertAvery yw.

But you are still going to share that with us, right? The attributes on Controller actions and IOC constructor injection are still something that I think we should have. @Fiahblade @FrankPfattheicher and I can help include that.

@RupertAvery
Copy link
Contributor Author

Absolutely. Right now it's a standalone "add-on" to Chromely, as the main entry point is the CefSharpBoundObject which is injected, it was easy to create a new set of handlers without any Chromely code adjustments.

Here are the features I've implemented:

  • Use MVC's convention-based approach to writing and wiring up controllers.
  • Use .NET Core's built-in IServiceCollection for dependency injection.
  • Constructor and property injection on controller classes.
  • Controller classes are transient services - created for each request.
  • Annotate Controller action methods with HttpGet, HttpPost etc
  • Model binding

Model binding means you can write your methods without ChromelyResponse and ChromelyRequest:

public IEnumerable<WeatherData> GetWeatherForecast(DateTime date, string location)

Of course, there is still a lot of work to do with model binding, but it currently works with Get and Post and most basic data types, object graphs and arrays.

It's available here https://github.com/RupertAvery/Chromely.Mvc

@FrankPfattheicher
Copy link
Member

@RupertAvery i now added DI support for chromely controllers with commit e77afd1.
Do you like to test if this helps you?

@mattkol
Copy link
Member

mattkol commented May 20, 2019

Absolutely. Right now it's a standalone "add-on" to Chromely, ...
It's available here https://github.com/RupertAvery/Chromely.Mvc

@RupertAvery thanks. I will surely take Chromely.Mvc for a ride!

Please will you add Chromely.Mvc to:
#63
https://github.com/chromelyapps/Chromely/wiki/A-List-of-Apps-Built-with-Chromely

@RupertAvery
Copy link
Contributor Author

RupertAvery commented May 20, 2019 via email

@mattkol
Copy link
Member

mattkol commented May 20, 2019

@RupertAvery you can add it to #63 and I will move it over.

Thanks,

@RupertAvery
Copy link
Contributor Author

@mattkol just to let you know I've updated Chromely.Mvc with Chromely 5.0.0.6.

https://github.com/RupertAvery/Chromely.Mvc

@mattkol
Copy link
Member

mattkol commented Sep 23, 2020

@RupertAvery thanks for the update.

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

No branches or pull requests

3 participants