Resolve types not in container

Issue #306 wontfix
Jos Visser created an issue

TL;DR:

I would like to resolve a class through the container which is not registered in the container, so I don't have to register all classes of my legacy framework in the container, but can still use constructor injection to inject new into the legacy classes.

Use case:

I got a big application, with a sort-of MVC setup with a 1000+ controllers. Many of them have screens, which have data modules, but the exact type and ancestry of screen may differ, and some have no screen at all, but will print something, open a URL, or whatever. The screens use visual inheritance, so I got for example a baseForm, a baseDBForm, a baseMaintenanceForm, a baseMDMaintenanceForm. So I got an order controller, which wants a TOrderForm, which wants a TOrderDataModule. Even though TOrderForm dictates that it want a TOrderDataModule, that datamodule is only constructed way up in the hierarchy, somewhere in baseDBForm, the first ancestor that introduces the dependency on the datamodule, where it basically does something like..

FDataModule := FDataModuleClass.Create(nil);

And, to ease in into dependency injection, it's in that place, where I would want to construct it (via a factory) through the container instead. This is all fine, if the class would be registered:

Container.Register<TDtmOrder, TDtmOrder>

I could then resolve it like this (but then in a factory, of course):

FDataModule := Container.Resolve(DatamoduleClass.ClassInfo) as TDataModule;

And if I'd reintroduce a new constructor anywhere in the hierarchy, the container would call that constructor, injecting any dependencies I introduced. Voila, dependency injection in my legacy framework. Done. Yay.

Attempt: Register all classes anyway. Result: Build takes too long

So, I then started an experiment to scrape the RTTIContext and register all my application's controllers, forms and datamodules in the container. But alas, with about 3000 classes, registering is still really fast (50ms or so), but Container.Build now takes a second on my (relatively powerful) development PC. Not something I want for my users.

Attempt: When trying to resolve, register class on the fly. Success! but a bit tacky..

So then I thought, maybe I can let the container resolve my TDtmOrder even if it isn't registered. After all, I don't want it to find the class, I already know which class, and I only want the container to construct it for me, so it will inject other dependencies. But I couldn't find a way of doing that out of the box.

In the end I wrote a new method that does this:

function TMyContainer.ResolveUnregistered(
  serviceType: PTypeInfo;
  const arguments: array of TValue): TValue;
var
  componentModel: TComponentModel;
  serviceName: string;
begin
  // Check if the type is registered normally. If so, resolve as normal.
  componentModel := Registry.FindDefault(serviceType);
  if componentModel <> nil then
  begin
    Result := Resolve(serviceType, arguments);
    Exit;
  end;

  // If not registered, register it by a specific service name.
  // Reason being: If the type is a class that implements an interface, the
  // default registration will use the interface, and resolving will fail
  // after registration.
  serviceName := 'JITreg>' + serviceType.TypeName;

  // Check if it was JITted before. If not add it.
  componentModel := Registry.FindOne(serviceName);
  if componentModel = nil then
  begin
    componentModel := Registry.RegisterComponent(serviceType);
    Registry.RegisterService(componentModel, serviceType, serviceName);
    Builder.Build(componentModel);
  end;

  // Result using JIT service name
  Result := Resolve(serviceName, arguments)
end;

Basically, what it tries to do is find the registration, the same way Resolve(PTypeInfo) would do, and resolve it. If it is not found, register a new component model and build only that model using a separate service name. Then resolve using that service name.

Especially the service name bothers me a bit. I had to use it, because some of the classes implement an interface, and then the component model would be registered as default for that interface rather than the class, and resolving using the PTypeInfo for the class would fail. I'm not sure if this is a potential bug in the internals (after all, I'm registering a service for the class, not for the interface), or I'm doing it wrong....

Anyway, I think "ResolveUnregistered", if it was made a bit smarted and nicer by people who actually know what they are doing, would be a nice addition for people who have big legacy applications and for whom this hybrid could be their step up to reorganizing their applications.

Comments (2)

  1. Stefan Glienke repo owner

    I am a bit confused that one second for the build call is an issue which only happens once when the application starts - even if that might be some more on a not so powerful non developer PC. Anyway the performance for building and gathering RTTI as well as resolving is subject for a future refactoring (which will not happen for 1.3 as originally planned but for the version after).

    Anyhow - your usecase can already be handled by a custom resolver.

    Here is a quick implementation that I slapped together - (future versions might contain this feature out of the box):

    type
      TUnknownTypeResolver = class(TInterfacedObject, ISubDependencyResolver)
      private
        fKernel: IKernel;
      public
        constructor Create(const kernel: IKernel);
        function CanResolve(const context: ICreationContext;
          const dependency: TDependencyModel; const argument: TValue): Boolean;
        function Resolve(const context: ICreationContext;
          const dependency: TDependencyModel; const argument: TValue): TValue;
      end;
    
    constructor TUnknownTypeResolver.Create(const kernel: IKernel);
    begin
      inherited Create;
      fKernel := kernel;
    end;
    
    function TUnknownTypeResolver.CanResolve(const context: ICreationContext;
      const dependency: TDependencyModel; const argument: TValue): Boolean;
    begin
      Result := not fKernel.Registry.HasService(dependency.TypeInfo)
        and (dependency.TypeInfo.Kind = tkClass);
    end;
    
    function TUnknownTypeResolver.Resolve(const context: ICreationContext;
      const dependency: TDependencyModel; const argument: TValue): TValue;
    var
      model: TComponentModel;
    begin
      model := fKernel.Registry.RegisterComponent(dependency.TypeInfo);
      fKernel.Registry.RegisterService(model, dependency.TypeInfo);
      fKernel.Builder.Build(model);
    
      Result := fKernel.Resolver.Resolve(context, dependency, argument);
    end;
    

    You just add that to the container:

    MyContainer.Kernel.Resolver.AddSubResolver(TUnknownTypeResolver.Create(MyContainer.Kernel));
    

    Keep in mind that while the Resolve call is thread-safe, Build calls are not. So if you ever resolve from different threads you might have a race condition.

  2. Log in to comment