1 /** 2 * This module contains objects for defining and scoping dependency registrations. 3 * 4 * Part of the Poodinis Dependency Injection framework. 5 * 6 * Authors: 7 * Mike Bierlee, m.bierlee@lostmoment.com 8 * Copyright: 2014-2025 Mike Bierlee 9 * License: 10 * This software is licensed under the terms of the MIT license. 11 * The full terms of the license can be found in the LICENSE file. 12 */ 13 14 module poodinis.registration; 15 16 import poodinis.container : DependencyContainer; 17 import poodinis.factory : InstanceFactory, InstanceEventHandler, 18 InstanceCreationException, InstanceFactoryParameters, CreatesSingleton; 19 20 class Registration { 21 private TypeInfo _registeredType = null; 22 private TypeInfo_Class _instanceType = null; 23 private Registration linkedRegistration; 24 private shared(DependencyContainer) _originatingContainer; 25 private InstanceFactory _instanceFactory; 26 private void delegate() _preDestructor; 27 28 TypeInfo registeredType() { 29 return _registeredType; 30 } 31 32 TypeInfo_Class instanceType() { 33 return _instanceType; 34 } 35 36 shared(DependencyContainer) originatingContainer() { 37 return _originatingContainer; 38 } 39 40 InstanceFactory instanceFactory() { 41 return _instanceFactory; 42 } 43 44 void delegate() preDestructor() { 45 return _preDestructor; 46 } 47 48 protected void preDestructor(void delegate() preDestructor) { 49 _preDestructor = preDestructor; 50 } 51 52 this(TypeInfo registeredType, TypeInfo_Class instanceType, 53 InstanceFactory instanceFactory, shared(DependencyContainer) originatingContainer) { 54 this._registeredType = registeredType; 55 this._instanceType = instanceType; 56 this._originatingContainer = originatingContainer; 57 this._instanceFactory = instanceFactory; 58 } 59 60 Object getInstance(InstantiationContext context = new InstantiationContext()) { 61 if (linkedRegistration !is null) { 62 return linkedRegistration.getInstance(context); 63 } 64 65 if (instanceFactory is null) { 66 throw new InstanceCreationException( 67 "No instance factory defined for registration of type " ~ registeredType.toString()); 68 } 69 70 return instanceFactory.getInstance(); 71 } 72 73 Registration linkTo(Registration registration) { 74 this.linkedRegistration = registration; 75 return this; 76 } 77 78 Registration onConstructed(InstanceEventHandler handler) { 79 if (instanceFactory !is null) 80 instanceFactory.onConstructed(handler); 81 return this; 82 } 83 } 84 85 private InstanceFactoryParameters copyFactoryParameters(Registration registration) { 86 return registration.instanceFactory.factoryParameters; 87 } 88 89 private void setFactoryParameters(Registration registration, InstanceFactoryParameters newParameters) { 90 registration.instanceFactory.factoryParameters = newParameters; 91 } 92 93 /** 94 * Sets the registration's instance factory type the same as the registration's. 95 * 96 * This is not a registration scope. Typically used by Poodinis internally only. 97 */ 98 Registration initializeFactoryType(Registration registration) { 99 auto params = registration.copyFactoryParameters(); 100 params.instanceType = registration.instanceType; 101 registration.setFactoryParameters(params); 102 return registration; 103 } 104 105 /** 106 * Scopes registrations to return the same instance every time a given registration is resolved. 107 * 108 * Effectively makes the given registration a singleton. 109 */ 110 Registration singleInstance(Registration registration) { 111 auto params = registration.copyFactoryParameters(); 112 params.createsSingleton = CreatesSingleton.yes; 113 registration.setFactoryParameters(params); 114 return registration; 115 } 116 117 /** 118 * Scopes registrations to return a new instance every time the given registration is resolved. 119 */ 120 Registration newInstance(Registration registration) { 121 auto params = registration.copyFactoryParameters(); 122 params.createsSingleton = CreatesSingleton.no; 123 params.existingInstance = null; 124 registration.setFactoryParameters(params); 125 return registration; 126 } 127 128 /** 129 * Scopes registrations to return the given instance every time the given registration is resolved. 130 */ 131 Registration existingInstance(Registration registration, Object instance) { 132 auto params = registration.copyFactoryParameters(); 133 params.createsSingleton = CreatesSingleton.yes; 134 params.existingInstance = instance; 135 registration.setFactoryParameters(params); 136 return registration; 137 } 138 139 /** 140 * Scopes registrations to create new instances using the given initializer delegate. 141 */ 142 Registration initializedBy(T)(Registration registration, T delegate() initializer) 143 if (is(T == class) || is(T == interface)) { 144 auto params = registration.copyFactoryParameters(); 145 params.createsSingleton = CreatesSingleton.no; 146 params.factoryMethod = () => cast(Object) initializer(); 147 registration.setFactoryParameters(params); 148 return registration; 149 } 150 151 /** 152 * Scopes registrations to create a new instance using the given initializer delegate. On subsequent resolves the same instance is returned. 153 */ 154 Registration initializedOnceBy(T : Object)(Registration registration, T delegate() initializer) { 155 auto params = registration.copyFactoryParameters(); 156 params.createsSingleton = CreatesSingleton.yes; 157 params.factoryMethod = () => cast(Object) initializer(); 158 registration.setFactoryParameters(params); 159 return registration; 160 } 161 162 string toConcreteTypeListString(Registration[] registrations) { 163 auto concreteTypeListString = ""; 164 foreach (registration; registrations) { 165 if (concreteTypeListString.length > 0) { 166 concreteTypeListString ~= ", "; 167 } 168 concreteTypeListString ~= registration.instanceType.toString(); 169 } 170 return concreteTypeListString; 171 } 172 173 class InstantiationContext { 174 } 175 176 version (unittest) : // 177 178 import poodinis; 179 import poodinis.testclasses; 180 import std.exception; 181 182 // Test getting instance without scope defined throws exception 183 unittest { 184 Registration registration = new Registration(typeid(TestType), null, null, null); 185 assertThrown!(InstanceCreationException)(registration.getInstance(), null); 186 } 187 188 // Test set single instance scope using scope setter 189 unittest { 190 Registration registration = new Registration(null, typeid(TestType), 191 new InstanceFactory(), null).initializeFactoryType(); 192 auto chainedRegistration = registration.singleInstance(); 193 auto instance1 = registration.getInstance(); 194 auto instance2 = registration.getInstance(); 195 assert(instance1 is instance2, 196 "Registration with single instance scope did not return the same instance"); 197 assert(registration is chainedRegistration, 198 "Registration returned by scope setting is not the same as the registration being set"); 199 } 200 201 // Test set new instance scope using scope setter 202 unittest { 203 Registration registration = new Registration(null, typeid(TestType), 204 new InstanceFactory(), null).initializeFactoryType(); 205 auto chainedRegistration = registration.newInstance(); 206 auto instance1 = registration.getInstance(); 207 auto instance2 = registration.getInstance(); 208 assert(instance1 !is instance2, 209 "Registration with new instance scope did not return a different instance"); 210 assert(registration is chainedRegistration, 211 "Registration returned by scope setting is not the same as the registration being set"); 212 } 213 214 // Test set existing instance scope using scope setter 215 unittest { 216 Registration registration = new Registration(null, null, new InstanceFactory(), null); 217 auto expectedInstance = new TestType(); 218 auto chainedRegistration = registration.existingInstance(expectedInstance); 219 auto actualInstance = registration.getInstance(); 220 assert(expectedInstance is actualInstance, 221 "Registration with existing instance scope did not return the same instance"); 222 assert(registration is chainedRegistration, 223 "Registration returned by scope setting is not the same as the registration being set"); 224 } 225 226 // Test linking registrations 227 unittest { 228 Registration firstRegistration = new Registration(typeid(TestInterface), 229 typeid(TestImplementation), new InstanceFactory(), null).initializeFactoryType() 230 .singleInstance(); 231 Registration secondRegistration = new Registration(typeid(TestImplementation), 232 typeid(TestImplementation), new InstanceFactory(), null).initializeFactoryType() 233 .singleInstance().linkTo(firstRegistration); 234 235 auto firstInstance = firstRegistration.getInstance(); 236 auto secondInstance = secondRegistration.getInstance(); 237 238 assert(firstInstance is secondInstance); 239 } 240 241 // Test custom factory method via initializedBy 242 unittest { 243 Registration registration = new Registration(typeid(TestInterface), 244 typeid(TestImplementation), new InstanceFactory(), null); 245 246 registration.initializedBy({ 247 auto instance = new TestImplementation(); 248 instance.someContent = "createdbyinitializer"; 249 return instance; 250 }); 251 252 TestImplementation instanceOne = cast(TestImplementation) registration.getInstance(); 253 TestImplementation instanceTwo = cast(TestImplementation) registration.getInstance(); 254 assert(instanceOne.someContent == "createdbyinitializer"); 255 assert(instanceTwo.someContent == "createdbyinitializer"); 256 assert(instanceOne !is instanceTwo); 257 } 258 259 // Test custom factory method via initializedOnceBy 260 unittest { 261 Registration registration = new Registration(typeid(TestInterface), 262 typeid(TestImplementation), new InstanceFactory(), null); 263 264 registration.initializedOnceBy({ 265 auto instance = new TestImplementation(); 266 instance.someContent = "createdbyinitializer"; 267 return instance; 268 }); 269 270 TestImplementation instanceOne = cast(TestImplementation) registration.getInstance(); 271 TestImplementation instanceTwo = cast(TestImplementation) registration.getInstance(); 272 assert(instanceOne.someContent == "createdbyinitializer"); 273 assert(instanceTwo.someContent == "createdbyinitializer"); 274 assert(instanceOne is instanceTwo); 275 } 276 277 // Test chaining single/new instance scope to initializedBy will not overwrite the factory method. 278 unittest { 279 Registration registration = new Registration(typeid(TestInterface), 280 typeid(TestImplementation), new InstanceFactory(), null); 281 282 registration.initializedBy({ 283 auto instance = new TestImplementation(); 284 instance.someContent = "createdbyinitializer"; 285 return instance; 286 }); 287 288 registration.singleInstance(); 289 290 TestImplementation instanceOne = cast(TestImplementation) registration.getInstance(); 291 TestImplementation instanceTwo = cast(TestImplementation) registration.getInstance(); 292 assert(instanceOne.someContent == "createdbyinitializer"); 293 assert(instanceTwo.someContent == "createdbyinitializer"); 294 assert(instanceOne is instanceTwo); 295 }