apexfarm / ApexDI

A dependency injection framework ported from .Net Core for Apex.
Apache License 2.0
37 stars 4 forks source link

Cannot get the correct Singleton service when with multiple `DI.getModule()` #1

Closed valorad closed 1 year ago

valorad commented 1 year ago

When I try to get two modules with the same service interface in the same context, the latter service is not retrieved correctly.

For example, when I run this in Dev console:

// use module to resolve services
DI.Module defaultModule = DI.getModule(DefaultServiceModule.class);
IAccountService accountServiceInstance = (IAccountService) defaultModule.getService(IAccountService.class);

List<Account> defaultResult = accountServiceInstance.getAccounts();
system.debug('Default result');
system.debug(defaultResult);

DI.Module mockModule = DI.getModule(MockServiceModule.class);
IAccountService mockAccountServiceInstance = (IAccountService) mockModule.getService(IAccountService.class);

List<Account> mockResult = mockAccountServiceInstance.getAccounts();
system.debug('mock result');
system.debug(mockResult);

I get the exact same result for defaultResult and mockResult

13:18:54:080 USER_DEBUG [17]|DEBUG|(Account:{Id=001Dn00000GY6PSIA1, Name=Sample Account for Entitlements}, Account:{Id=001Dn00000GgTgHIAV, Name=Edge Communications}, Account:{Id=001Dn00000GgTgIIAV, Name=Burlington Textiles Corp of America}, Account:{Id=001Dn00000GgTgJIAV, Name=Pyramid Construction Inc.}, Account:{Id=001Dn00000GgTgKIAV, Name=Dickenson plc}, Account:{Id=001Dn00000GgTgLIAV, Name=Grand Hotels & Resorts Ltd}, Account:{Id=001Dn00000GgTgMIAV, Name=United Oil & Gas Corp.}, Account:{Id=001Dn00000GgTgNIAV, Name=Expr

While the expected result should be different.

However, when I comment out all the code for defaultResult, then this time the mockResult is as expected.

13:17:36:072 USER_DEBUG [17]|DEBUG|(Account:{Name=qaa}, Account:{Name=qab}, Account:{Name=qac})

Here are my service implementations

public with sharing class AccountService implements IAccountService {
  public List<Account> getAccounts() {
    return [
      SELECT Id, Name
      FROM Account
    ];
  }
}
public with sharing class MockAccountService implements IAccountService {
  public List<Account> getAccounts() {
    List<Account> accounts = new List<Account> {
      new Account(Name='qaa'),
      new Account(Name='qab'),
      new Account(Name='qac')
    };

    return accounts;
  }
}
public with sharing class DefaultServiceModule extends DI.Module {
  public override void configure(DI.ServiceCollection services) {
    services.addSingleton('IAccountService', 'AccountService');
  }
}
public with sharing class MockServiceModule extends DI.Module {
  public override void configure(DI.ServiceCollection services) {
    services.addSingleton('IAccountService', 'MockAccountService');
  }
}
inksword commented 1 year ago

@valorad - Thanks a lot for the feedback and a very clear issue description.

Singletons are registered in a global context, once it is initialized for accountServiceInstance, its result will be cached and reused whenever there is an request to initialize the IAccountService. I haven't documented this mechanism clearly, and I am going to add this part.

The following features are not supported for singleton, and here are the reasons:

  1. Runtime Override: Why runtime singleton override is not implemented, because it could be dangerous, when each module registered its own singleton, and they loaded in different order every time, the final instance is not under control.
  2. Runtime Replacement: Usually when MockServiceModule used in test classes, the original service is not intended to be initialized. So the singleton replacement method is not necessarily provided in order to prevent abuse. I can provide one but I hesitated due to this reason. I also need strong evidence to know this is really a limitation before implementing it.

In your case, it will work if the IAccountService is registred as a scoped, or transient lifetime inside MockServiceModule.

public with sharing class MockServiceModule extends DI.Module {
  public override void configure(DI.ServiceCollection services) {
    services.addScoped('IAccountService', 'MockAccountService'); // use addScoped()
  }
}

If you have new thoughts, please just let me know. And sorry to respond you 4 days late, I am busy at a new library these days. It is published now, I think it is the best SOQL query builder you can ever find in github, wish you can also have a try https://github.com/apexfarm/ApexQuery.

inksword commented 1 year ago

I have just added a new section 1.2 Singleton Lifetime Caveat, to explain the singleton mechanism behind and how to mitigate the issue.

valorad commented 1 year ago

@inksword Got it. Thanks for your thorough explanation!