Recently I had a chance to work with Dependency Injection in Acumatica ERP. I want to share how it works and how one can use it.
Let’s assume that we have a REST Service, which we want to inject into Sales Order Entry and work with it using Actions.
First, we need to create interfaces that we will use for injecting the implementations into the Sales Order Entry.
public interface IRestServiceProvider { string URL { get; set; } object[] GetRecords(); bool CreateRecord(object rec); bool UpdateRecord(object rec); bool DeleteRecord(object rec); } public interface IRestServiceConfiguration { void Configure(IRestServiceProvider provider); }
Now we can add the implementations of the interfaces :
public class RestServiceConfiguration : IRestServiceConfiguration { public void Configure(IRestServiceProvider provider) { provider.URL = "TEST_URL"; } }
and
public class RestServiceProvider : IRestServiceProvider { public RestServiceProvider(IRestServiceConfiguration serviceConfiguration) { serviceConfiguration.Configure(this); } public string URL { get; set; } public bool CreateRecord(object rec) { throw new NotImplementedException(); } public bool DeleteRecord(object rec) { throw new NotImplementedException(); } public object[] GetRecords() { throw new NotImplementedException(); } public bool UpdateRecord(object rec) { throw new NotImplementedException(); } }
The final step will be to register our implementations with Autofac, which is done by defining an Autofac Module and overriding the Load method:
public class ServiceRegistrator : Module { protected override void Load(ContainerBuilder builder) { //We need to register our implementations with Autofac. builder.RegisterType<RestServiceConfiguration>().As<IRestServiceConfiguration>(); builder.RegisterType<RestServiceProvider>().As<IRestServiceProvider>(); } }
Now we can inject our types into the Sales Order Entry in a very simple way using InjectDependencyAttribute
and property:
public class SOOrderEntryExt : PXGraphExtension<SOOrderEntry> { [InjectDependency] public IRestServiceProvider ServiceProvider { get; set; } public PXAction<SOOrder> doRestRequest; [PXButton(CommitChanges = true)] [PXUIField(DisplayName ="Do Request")] protected IEnumerable DoRestRequest(PXAdapter adapter) { if(ServiceProvider!=null) { throw new PXException("Dependency Injection Worked! URL : {0}",ServiceProvider.URL); } else { throw new PXException("Something Went wrong"); } } }
As a result of our hard work we will get the following message:
The interesting part here is that we will find the below description if we check the InjectDependecyAttribute
:
The attribute that is used in the <see cref=”T:PX.Data.PXGraph” />, <see cref=”T:PX.Data.PXAction” />, or <see cref=”T:PX.Data.PXEventSubscriberAttribute” />-derived classes to create the properties that need to be injected via dependency injection.
And as we all remember PXGraphExtension<T>
is not derived from PXGraph
, so technically our code shouldn’t work, but it is working and let’s understand why.
Let’s start by reviewing the code of the PXGraph.CreateInstance<T>()
method of the PXGraph
class. Acumatica ERP development best practices require us to use that method for graph object creation and Acumatica ERP itself is using it for correct initialization of the graph.
I am using dnSpy for reverse engineering Acumatica’s Source codes from DLLs, but you can use whatever disassembler that you want.
Below is the full code of the internal method called behind the scene in the PXGraph.CreateInstance<T>()
:
internal static PXGraph CreateInstance(Type graphType, string prefix) { if (graphType == null) { throw new ArgumentNullException("graphType"); } if (!typeof(PXGraph).IsAssignableFrom(graphType)) { throw new ArgumentException(string.Format("The type '{0}' must inherit the PX.Data.PXGraph type.", graphType.FullName), "graphType"); } if (graphType.GetConstructor(new Type[0]) == null) { throw new ArgumentException(string.Format("The type '{0}' must contain a default constructor.", graphType.FullName), "graphType"); } string customizedTypeFullName = CustomizedTypeManager.GetCustomizedTypeFullName(graphType); Type type = graphType; PXGraph result; try { if (customizedTypeFullName != graphType.FullName) { type = (PXBuildManager.GetType(customizedTypeFullName, false) ?? graphType); } Type type2 = PXGraph.b(type).Wrapper ?? type; Type graphInstanceCreating = PXGraph.GraphInstanceCreating; try { PXGraph.GraphStatePrefix = prefix; PXGraph.e = true; PXGraph.GraphInstanceCreating = type2; PXGraph pxgraph = (PXGraph)Activator.CreateInstance(type2); InjectMethods.InjectDependencies(pxgraph, graphType, prefix); IGraphWithInitialization graphWithInitialization = pxgraph as IGraphWithInitialization; if (graphWithInitialization != null) { graphWithInitialization.Initialize(); } if (pxgraph.Extensions != null && pxgraph.Extensions.Length != 0) { PXGraphExtension[] extensions = pxgraph.Extensions; for (int i = 0; i < extensions.Length; i++) { extensions[i].Initialize(); } } try { if (PXDatabase.Provider is PXDatabaseProviderBase) { pxgraph._VeryFirstTimeStamp = ((PXDatabaseProviderBase)PXDatabase.Provider).selectTimestamp().Item1; } } catch { } result = pxgraph; } finally { PXGraph.GraphStatePrefix = ""; PXGraph.e = false; PXGraph.GraphInstanceCreating = graphInstanceCreating; } } catch (TargetInvocationException exception) { throw PXException.ExtractInner(exception); } return result; }
The interesting part for us is the one below:
PXGraph pxgraph = (PXGraph)Activator.CreateInstance(type2); InjectMethods.InjectDependencies(pxgraph, graphType, prefix);
As you can see Acumatica ERP is using InjectMethod.InjectDependencies
method for Dependency Injection. Let’s see what is happening inside that method.
internal static void InjectDependencies(PXGraph graph, Type graphType, string prefix) { if (!InjectMethods.b(graph)) { if (graph.Extensions == null) { return; } if (!graph.Extensions.Any((PXGraphExtension e) => InjectMethods.b(e))) { return; } } IDependencyInjector dependencyInjector = CompositionRoot.GetDependencyInjector(); if (dependencyInjector == null) { return; } dependencyInjector.InjectDependencies(graph, graphType, prefix); }
Now we need to understand what is returned by CompositionRoot.GetDependencyInjector()
Below is the code of that method:
internal static IDependencyInjector GetDependencyInjector() { IDependencyInjector dependencyInjector; if ((dependencyInjector = CompositionRoot.InjectorSlot.GetInjector(LifetimeScopeHelper.GetLifetimeScope())) == null) { dependencyInjector = (ServiceLocator.IsLocationProviderSet ? LazyInitializer.EnsureInitialized<IDependencyInjector>(ref CompositionRoot.a, new Func<IDependencyInjector>(ServiceLocator.Current.GetInstance<IDependencyInjector>)) : null); } IDependencyInjector result; if ((result = dependencyInjector) == null) { result = CompositionRoot.b; } return result; }
In short, this method is resolving the implementation of the IDependencyInjector
interface.
If you check the PX.Data.ServiceRegistration
class which represents an implementation of the Autofac’s Module class
namespace PX.Data { public class ServiceRegistration : Module { protected override void Load(ContainerBuilder builder) { builder.RegisterType<PropertyDependencyInjector>().As<IDependencyInjector>().InstancePerLifetimeScope().PreserveExistingDefaults<PropertyDependencyInjector, ConcreteReflectionActivatorData, SingleRegistrationStyle>(); builder.RegisterType<DataScreenFactory>().As<IDataScreenFactory>().SingleInstance(); builder.Register((IComponentContext c) => new ServerCompressionHandler(WebConfig.GetInt("CompressionThreshold", 0), new ICompressor[] { new GZipCompressor(), new DeflateCompressor() })).As<ServerCompressionHandler>();
You can find the below lines which means that we need to check the PropertyDependencyInjector
class to understand how the InjectPropertyAttribute
is working with PXGraphExtensions
.
builder.RegisterType<PropertyDependencyInjector>().As<IDependencyInjector>().InstancePerLifetimeScope().PreserveExistingDefaults<PropertyDependencyInjector, ConcreteReflectionActivatorData, SingleRegistrationStyle>();
Remember that thedependencyInjector.InjectDependencies
method is invoked in the InjectMethod.InjectDependencies
and PXGraph.CreateInstance
method is calling InjectMethod.InjectDependencies
on the created graph instance.
Below is the code of that method from PropertyDependencyInjector
class, which is the implementation of the IDependencyInjector
interface.
public void InjectDependencies(PXGraph graph, Type graphType, string prefix) { IEnumerable<PropertyInfo> injectableProperties = PropertyDependencyInjector.PropertyInjector<InjectDependencyAttribute>.GetInjectableProperties(graph); if (injectableProperties != null) { injectableProperties.ForEach(delegate(PropertyInfo p) { PropertyDependencyInjector.PropertyInjector<InjectDependencyAttribute>.InjectProperty(this._componentContext, graph, this.GetGraphParameters(graph), p); }); } if (graph.Extensions != null) { PXGraphExtension[] extensions = graph.Extensions; for (int i = 0; i < extensions.Length; i++) { PXGraphExtension extension = extensions[i]; IEnumerable<PropertyInfo> injectableProperties2 = PropertyDependencyInjector.PropertyInjector<InjectDependencyAttribute>.GetInjectableProperties(extension); if (injectableProperties2 != null) { injectableProperties2.ForEach(delegate(PropertyInfo p) { PropertyDependencyInjector.PropertyInjector<InjectDependencyAttribute>.InjectProperty(this._componentContext, extension, this.GetGraphExtensionParameters(graph, extension), p); }); } } } }
As we can see from the code of this method after injecting the properties of the PXGraph
, the method is checking if the graph has extensions, and if it does,the method is injecting the properties of them.
This will not work if you add Module in plain customization code. Module as all other plugins can only be used in external dll.