1 module jaster.ioc.depinject; 2 3 /// Describes the lifetime of a service. 4 enum ServiceLifetime 5 { 6 /++ 7 + The service is constructed every single time it is requested. 8 + 9 + These services are the most expensive to use, as they need to be constructed *every single time* they're requested, which also 10 + puts strain on the GC. 11 + ++/ 12 Transient, 13 14 /++ 15 + The service is only constructed a single time for every `ServiceProvider`, regardless of which scope is used to access it. 16 + 17 + These services are the least expensive to use, as they are only constructed a single time per `ServiceProvider`. 18 + ++/ 19 Singleton, 20 21 /++ 22 + The service is constructed once per scope. 23 + 24 + These services are between `Transient` and `Singleton` in terms of performance. It mostly depends on how often scopes are created/destroyed 25 + in your program. 26 + 27 + See_Also: 28 + `ServiceProvider.createScope` 29 + ++/ 30 Scoped 31 } 32 33 // Mixin for things like asSingletonRuntime, or asTransient. aka: boilerplate 34 private mixin template ServiceLifetimeFunctions(ServiceLifetime Lifetime) 35 { 36 import std.conv : to; 37 static immutable Suffix = Lifetime.to!string; 38 static immutable FullLifetime = "ServiceLifetime."~Suffix; 39 40 @safe nothrow pure 41 public static 42 { 43 /// 44 mixin("alias as"~Suffix~"Runtime = asRuntime!("~FullLifetime~");"); 45 46 /// 47 mixin("alias as"~Suffix~"(alias BaseType, alias ImplType) = asTemplated!("~FullLifetime~", BaseType, ImplType);"); 48 49 /// 50 mixin("alias as"~Suffix~"(alias ImplType) = asTemplated!("~FullLifetime~", ImplType, ImplType);"); 51 } 52 } 53 54 /++ 55 + Describes a service. 56 + 57 + This struct shouldn't be created directly, you must use one of the static construction functions. 58 + 59 + For example, if you wanted the service to be a singleton, you could do `asSingleton!(IBaseType, ImplementationType)`. 60 + ++/ 61 struct ServiceInfo 62 { 63 alias FactoryFunc = Object delegate(ref ServiceScope); 64 alias FactoryFuncFor(T) = T delegate(ref ServiceScope); 65 enum isValidBaseType(T) = (is(T == class) || is(T == interface)); 66 enum isValidImplType(BaseT, T) = (is(T == class) && (is(T : BaseT) || is(T == BaseT))); 67 enum isValidImplType(T) = isValidImplType!(T, T); 68 69 private 70 { 71 TypeInfo _baseType; 72 TypeInfo _implType; 73 FactoryFunc _factory; 74 ServiceLifetime _lifetime; 75 TypeInfo[] _dependencies; 76 77 @safe @nogc 78 this(TypeInfo baseType, TypeInfo implType, FactoryFunc func, ServiceLifetime lifetime, TypeInfo[] dependencies) nothrow pure 79 { 80 this._baseType = baseType; 81 this._implType = implType; 82 this._factory = func; 83 this._lifetime = lifetime; 84 this._dependencies = dependencies; 85 86 assert(func !is null, "The factory function is null. The `asXXXRuntime` functions can't auto-generate one sadly, so provide your own."); 87 } 88 } 89 90 /// This is mostly for unit tests. 91 @safe 92 bool opEquals(const ServiceInfo rhs) const pure nothrow 93 { 94 return 95 ( 96 this._baseType is rhs._baseType 97 && this._implType is rhs._implType 98 && this._lifetime == rhs._lifetime 99 ); 100 } 101 102 /// So we can use this struct as an AA key more easily 103 @trusted // @trusted since we're only converting a pointer to a number, without doing anything else to it. 104 size_t toHash() const pure nothrow 105 { 106 const baseTypePointer = cast(size_t)(cast(void*)this._baseType); 107 const implTypePointer = cast(size_t)(cast(void*)this._implType); 108 const lifetimeAsNumber = cast(size_t)this._lifetime; 109 110 // NOTE: This is just something completely random I made up. I'll research into a proper technique eventually, this just has to exist *in some form* for now. 111 return (baseTypePointer ^ implTypePointer) * lifetimeAsNumber; 112 } 113 114 @safe nothrow pure 115 public static 116 { 117 /++ 118 + An internal function, public due to necessity, however will be used to explain the `asXXXRuntime` functions. 119 + 120 + e.g. `asSingletonRuntime`, `asTransientRuntime`, and `asScopedRuntime`. 121 + 122 + Notes: 123 + Unlike the `asTemplated` constructor (and things like `asSingleton`, `asScoped`, etc.), this function isn't able to produce 124 + a list of dependency types, so therefore will need to be provided by you, the user, if you're hoping to make use of `ServiceProvider`'s 125 + dependency loop guard. 126 + 127 + You only need to provide a list of types that the `factory` function will try to directly retrieve from a `ServiceScope`, not the *entire* dependency chain. 128 + ++/ 129 ServiceInfo asRuntime(ServiceLifetime Lifetime)(TypeInfo baseType, TypeInfo implType, FactoryFunc factory, TypeInfo[] dependencies = null) 130 { 131 return ServiceInfo(baseType, implType, factory, Lifetime, dependencies); 132 } 133 134 /++ 135 + An internal function, public due to necessity, however will be used to explain the `asXXX` functions. 136 + 137 + e.g. `asSingleton`, `asTransient`, and `asScoped`. 138 + 139 + Notes: 140 + This constructor is able to automatically generate the list of dependencies, which will allow `ServiceProvider` to check for 141 + dependency loops. 142 + 143 + If `factory` is `null`, then the factory becomes a call to `Injector.construct!ImplType`, which should be fine for most cases. 144 + ++/ 145 ServiceInfo asTemplated(ServiceLifetime Lifetime, alias BaseType, alias ImplType)(FactoryFuncFor!ImplType factory = null) 146 if(isValidBaseType!BaseType && isValidImplType!(BaseType, ImplType)) 147 { 148 import std.meta : Filter; 149 import std.traits : Parameters; 150 151 enum isClassOrInterface(T) = is(T == class) || is(T == interface); 152 alias ImplTypeCtor = Injector.FindCtor!ImplType; 153 alias CtorParams = Parameters!ImplTypeCtor; 154 alias CtorParamsFiltered = Filter!(isClassOrInterface, CtorParams); 155 156 TypeInfo[] deps; 157 deps.length = CtorParamsFiltered.length; 158 159 static foreach(i, dep; CtorParamsFiltered) 160 deps[i] = typeid(dep); 161 162 if(factory is null) 163 factory = (ref services) => Injector.construct!ImplType(services); 164 165 return ServiceInfo(typeid(BaseType), typeid(ImplType), factory, Lifetime, deps); 166 } 167 } 168 169 mixin ServiceLifetimeFunctions!(ServiceLifetime.Singleton); 170 mixin ServiceLifetimeFunctions!(ServiceLifetime.Transient); 171 mixin ServiceLifetimeFunctions!(ServiceLifetime.Scoped); 172 } 173 /// 174 //@safe nothrow pure 175 unittest 176 { 177 static interface I {} 178 static class C : I {} 179 180 Object dummyFactory(ref ServiceScope){ return null; } 181 182 // Testing: All 3 aliases can be found, 1 alias per lifetime, which also tests that all lifetimes are handled properly. 183 assert( 184 ServiceInfo.asSingletonRuntime(typeid(I), typeid(C), &dummyFactory) 185 == 186 ServiceInfo(typeid(I), typeid(C), &dummyFactory, ServiceLifetime.Singleton, null) 187 ); 188 189 assert( 190 ServiceInfo.asTransient!(I, C)((ref provider) => new C()) 191 == 192 ServiceInfo(typeid(I), typeid(C), &dummyFactory, ServiceLifetime.Transient, null) // NOTE: Factory func is ignored in opEquals, so `dummyFactory` here is fine. 193 ); 194 195 assert( 196 ServiceInfo.asScoped!C() 197 == 198 ServiceInfo(typeid(C), typeid(C), &dummyFactory, ServiceLifetime.Scoped, null) 199 ); 200 201 // opEquals and opHash don't care about dependencies (technically I think they should, but meh), so we have to test directly. 202 static class WithDeps 203 { 204 this(C c, I i, int a){} 205 } 206 207 auto deps = ServiceInfo.asScoped!WithDeps()._dependencies; 208 assert(deps.length == 2); 209 assert(deps[0] is typeid(C)); 210 assert(deps[1] is typeid(I)); 211 } 212 213 /++ 214 + Provides access to a service scope. 215 + 216 + Description: 217 + The idea of having 'scopes' for services comes from the fact that this library was inspired by ASP Core's Dependency Injection, 218 + which provides similar functionality. 219 + 220 + In ASP Core there is a need for each HTTP request to have its own 'ecosystem' for services (its own 'scope'). 221 + 222 + For example, you don't want your database context to be shared between different requests at the same time, as each request needs 223 + to make/discard their own chnages to the database. Having a seperate database context between each request (scope) allows this to be 224 + achieved easily. 225 + 226 + For a lot of programs, you probably won't need to use scopes at all, and can simply use `ServiceProvider.defaultScope` for all of your 227 + needs. Use what's best for your case. 228 + 229 + See https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection as the documentation there is mostly appropriate for this library as well. 230 + 231 + Master & Slave `ServiceScope`: 232 + There are two variants of `ServiceScope` that can be accessed - master scopes, and slave scopes. 233 + 234 + The principal is pretty simple: Master scopes are the 'true' accessor to the scope so therefore contain the ability to also destroy the scope. 235 + 236 + Slave scopes, however, can be seen more as a 'reference' accessor to the scope, meaning that they cannot destroy the underlying scope in anyway. 237 + 238 + Destroying a scope: 239 + There are two ways for a service scope to be destroyed. 240 + 241 + 1. The master `ServiceScope` object has its dtor run. Because the `ServiceScope` is the master accessor, then the scope's lifetime is directly tied to the `ServiceScope`'s lifetime. 242 + 243 + 2. A call to `ServiceProvider.destroyScope` is made, where a master `ServiceScope` is passed to it. 244 + 245 + The `ServiceScope` object is non-copyable, so can only be moved via functions like `std.algorithm.mutation.move` 246 + 247 + Currently when a scope it destroyed nothing really happens except that the `ServiceProvider` clears its cache of service instances for that specific scope, and allows 248 + another scope to be created in its place. 249 + 250 + In the future I will be adding more functionality onto when a scope is destroyed, as the current behaviour is a bit undesirable for multiple reasons (e.g. 251 + if you destroy a scope, any uses of a `ServiceScopeAccessor` for the destroyed scope can trigger a bug-check assert). 252 + 253 + See_Also: 254 + `ServiceProvider.createScope`, `ServiceProvider.destroyScope` 255 + ++/ 256 struct ServiceScope 257 { 258 private size_t _index; 259 private ServiceProvider _provider; 260 private bool _isMasterReference; // True = Scope is destroyed via this object. False = Scope can't be destroyed via this object. 261 262 @disable 263 this(this); 264 265 ~this() 266 { 267 if(this._provider !is null && this._isMasterReference) 268 this._provider.destroyScope(this); 269 } 270 271 /++ 272 + Attempts to retrieve a service of the given `baseType`, otherwise returns `null`. 273 + ++/ 274 Object getServiceOrNull(TypeInfo baseType) 275 { 276 return this._provider.getServiceOrNull(baseType, this); 277 } 278 279 /++ 280 + Attempts to retrieve a service of the given base type `T`, otherwise returns `null`. 281 + ++/ 282 T getServiceOrNull(alias T)() 283 if(ServiceInfo.isValidBaseType!T) 284 { 285 auto service = this.getServiceOrNull(typeid(T)); 286 if(service is null) 287 return null; 288 289 auto casted = cast(T)service; 290 assert(casted !is null, "Invalid cast."); 291 292 return casted; 293 } 294 /// 295 unittest 296 { 297 static interface IPrime 298 { 299 int getPrime(); 300 } 301 302 static class PrimeThree : IPrime 303 { 304 int getPrime() 305 { 306 return 3; 307 } 308 } 309 310 auto services = new ServiceProvider([ServiceInfo.asTransient!(IPrime, PrimeThree)]); 311 auto service = services.defaultScope.getServiceOrNull!IPrime(); 312 assert(service !is null); 313 assert(service.getPrime() == 3); 314 } 315 } 316 317 // Not an interface since a testing-specific implementation has no worth atm, and it'd mean making `serviceScope` virtual. 318 /++ 319 + A built-in service that allows access to a slave `ServiceScope` for whichever scope this service 320 + was constructed for. 321 + 322 + Description: 323 + Because `ServiceScope` cannot be copied, it can be a bit annoying for certain services to gain access to it should they need 324 + manual access of fetching scoped services. 325 + 326 + As an alternative, services can be injected with this helper service which allows the creation of slave `ServiceScope`s. 327 + 328 + See_Also: 329 + The documentation for `ServiceScope`. 330 + ++/ 331 final class ServiceScopeAccessor 332 { 333 // Since ServiceScope can't be copied, and we shouldn't exactly move it from its default location, we need to re-store 334 // some of its info for later. 335 private ServiceProvider _provider; 336 private size_t _index; 337 338 private this(ref ServiceScope serviceScope) 339 { 340 this._provider = serviceScope._provider; 341 this._index = serviceScope._index; 342 } 343 344 /++ 345 + Returns: A slave `ServiceScope`. 346 + ++/ 347 @property @safe @nogc 348 ServiceScope serviceScope() nothrow pure 349 { 350 return ServiceScope(this._index, this._provider, false); // false = Not master scope object. 351 } 352 } 353 354 // Not an interface since a testing-specific implementation has little worth atm, and it'd mean making functions virtual. 355 /++ 356 + Provides most of the functionality for managing and using services. 357 + 358 + Dependency_Checking: 359 + During construction, the `ServiceProvider` will perform a check to ensure that none of its registered services contain a 360 + dependency loop, i.e. making sure that no service directly/indirectly depends on itself. 361 + 362 + If you're creating your `ServiceInfo` via the `ServiceInfo.asSingleton`, `ServiceInfo.asTransient`, or `ServiceInfo.asScoped` constructors, 363 + then this functionality is entirely automatic. 364 + 365 + If however you're using the `asXXXRuntime` constructor varient, then it is down to the user to provide an array of `TypeInfo` to that constructor, 366 + representing the service's dependencies. Failing to provide the correct data will cause this guard check to not detect the loop, and later down the line 367 + this will cause an infinite loop leading into a crash. 368 + 369 + In the future, I may add a `version()` block that will make `ServiceProvider.getServiceOrNull` also perform dependency loop checks. The reason this is not 370 + performed by default is due to performance concerns (especially once your services start to grow in number). 371 + 372 + Lifetime_Checking: 373 + During construction, the `ServiceProvider` will perform a check to ensure that none of its registered services contains a dependency on 374 + another service with an incompatible lifetime. 375 + 376 + Likewise with depenency checking (see section above), you will need to ensure that you provide the correct data when using the `asXXXRuntime` constructors 377 + for `ServiceInfo`. 378 + 379 + The following is a table of valid lifetime pairings: 380 + 381 + * Transient - [Transient, Singleton] 382 + 383 + * Scoped - [Transient, Scoped, Singleton] 384 + 385 + * Singleton - [Transient, Singleton] 386 + ++/ 387 final class ServiceProvider 388 { 389 import std.typecons : Nullable, nullable; 390 391 alias ServiceInstanceDictionary = Object[ServiceInfo]; 392 enum BITS_PER_MASK = long.sizeof * 8; 393 394 private 395 { 396 struct ScopeInfo 397 { 398 ServiceInstanceDictionary instances; 399 ServiceScopeAccessor accessor; // Written in a way that they only have to be constructed once, so GC isn't as mad. 400 } 401 402 ServiceScope _defaultScope; 403 ServiceInfo[] _allServices; 404 ScopeInfo[] _scopes; 405 ServiceInstanceDictionary _singletons; 406 long[] _scopeInUseMasks; 407 408 Object getServiceOrNull(TypeInfo baseType, ref scope ServiceScope serviceScope) 409 { 410 assert(serviceScope._provider is this, "Attempting to use service scope who does not belong to this `ServiceProvider`."); 411 412 auto infoNullable = this.getServiceInfoForBaseType(baseType); 413 if(infoNullable.isNull) 414 return null; 415 416 auto info = infoNullable.get(); 417 final switch(info._lifetime) with(ServiceLifetime) 418 { 419 case Transient: 420 return info._factory(serviceScope); 421 422 case Scoped: 423 auto ptr = (cast()info in this._scopes[serviceScope._index].instances); // 'cus apparently const is too painful for it to handle. 424 if(ptr !is null) 425 return *ptr; 426 427 auto instance = info._factory(serviceScope); 428 this._scopes[serviceScope._index].instances[cast()info] = instance; 429 return instance; 430 431 case Singleton: 432 // TODO: Functionise this 433 auto ptr = (cast()info in this._singletons); // 'cus apparently const is too painful for it to handle. 434 if(ptr !is null) 435 return *ptr; 436 437 auto instance = info._factory(serviceScope); 438 this._singletons[cast()info] = instance; 439 return instance; 440 } 441 } 442 443 @safe 444 ref long getScopeMaskByScopeIndex(size_t index) nothrow 445 { 446 const indexIntoArray = (index / BITS_PER_MASK); 447 448 if(indexIntoArray >= this._scopeInUseMasks.length) 449 this._scopeInUseMasks.length = indexIntoArray + 1; 450 451 return this._scopeInUseMasks[indexIntoArray]; 452 } 453 454 @safe 455 bool isScopeInUse(size_t index) nothrow 456 { 457 const bitInMask = (index % BITS_PER_MASK); 458 const mask = this.getScopeMaskByScopeIndex(index); 459 460 return (mask & (1 << bitInMask)) > 0; 461 } 462 463 @safe 464 void setScopeInUse(size_t index, bool isInUse) nothrow 465 { 466 const bitInMask = (index % BITS_PER_MASK); 467 const bitToUse = (1 << bitInMask); 468 469 if(isInUse) 470 this.getScopeMaskByScopeIndex(index) |= bitToUse; 471 else 472 this.getScopeMaskByScopeIndex(index) &= ~bitToUse; 473 } 474 /// 475 unittest 476 { 477 auto services = new ServiceProvider(null); 478 assert(!services.isScopeInUse(65)); 479 480 services.setScopeInUse(65, true); 481 assert(services.isScopeInUse(65)); 482 assert(services._scopeInUseMasks.length == 2); 483 assert(services._scopeInUseMasks[1] == 0b10); // 65 % 64 = 1. 1 << 1 = 0b10 484 485 services.setScopeInUse(65, false); 486 assert(!services.isScopeInUse(65)); 487 } 488 489 @safe 490 void assertLifetimesAreCompatible() 491 { 492 @safe 493 bool areCompatible(const ServiceLifetime consumer, const ServiceLifetime dependency) 494 { 495 final switch(consumer) with(ServiceLifetime) 496 { 497 case Transient: 498 case Singleton: 499 return dependency != Scoped; 500 501 case Scoped: 502 return true; 503 } 504 } 505 506 foreach(service; this._allServices) 507 { 508 foreach(dependency; service._dependencies) 509 { 510 auto dependencyInfo = this.getServiceInfoForBaseType(dependency); 511 if(dependencyInfo.isNull) 512 continue; 513 514 if(!areCompatible(service._lifetime, dependencyInfo.get._lifetime)) 515 { 516 import std.format : format; 517 assert( 518 false, 519 "%s service %s cannot depend on %s service %s as their lifetimes are incompatible".format( 520 service._lifetime, 521 service._baseType, 522 dependencyInfo.get._lifetime, 523 dependency 524 ) 525 ); 526 } 527 } 528 } 529 } 530 // 531 unittest 532 { 533 import std.exception : assertThrown, assertNotThrown; 534 535 static class Scoped 536 { 537 } 538 539 static class Transient 540 { 541 this(Scoped){} 542 } 543 544 static class GoodTransient 545 { 546 } 547 548 static class GoodScoped 549 { 550 this(GoodTransient){} 551 } 552 553 assertThrown!Throwable(new ServiceProvider([ 554 ServiceInfo.asScoped!Scoped, 555 ServiceInfo.asTransient!Transient 556 ])); 557 558 assertNotThrown!Throwable(new ServiceProvider([ 559 ServiceInfo.asScoped!GoodScoped, 560 ServiceInfo.asTransient!GoodTransient 561 ])); 562 563 // Uncomment when tweaking the error message. 564 // new ServiceProvider([ 565 // ServiceInfo.asScoped!Scoped, 566 // ServiceInfo.asTransient!Transient 567 // ]); 568 } 569 570 @safe 571 void assertNoDependencyLoops() 572 { 573 TypeInfo[] typeToTestStack; 574 TypeInfo[] typeToTestStackWhenLoopIsFound; // Keep a copy of the stack once a loop is found, so we can print out extra info. 575 576 @trusted 577 bool dependsOn(TypeInfo typeToTest, TypeInfo dependencyType) 578 { 579 import std.algorithm : canFind; 580 581 if(typeToTestStack.canFind!"a is b"(typeToTest)) 582 return false; // Since we would've returned true otherwise, which would end the recursion. 583 584 typeToTestStack ~= typeToTest; 585 scope(exit) typeToTestStack.length -= 1; 586 587 auto serviceInfoNullable = this.getServiceInfoForBaseType(typeToTest); 588 if(serviceInfoNullable.isNull) 589 return false; // Since the service doesn't exist anyway. 590 591 auto serviceInfo = serviceInfoNullable.get(); 592 foreach(dependency; serviceInfo._dependencies) 593 { 594 if(dependency is dependencyType || dependsOn(cast()dependency, dependencyType)) 595 { 596 if(typeToTestStackWhenLoopIsFound.length == 0) 597 typeToTestStackWhenLoopIsFound = typeToTestStack.dup; 598 599 return true; 600 } 601 } 602 603 return false; 604 } 605 606 foreach(service; this._allServices) 607 { 608 if(dependsOn(service._baseType, service._baseType)) 609 { 610 import std.algorithm : map, joiner; 611 import std.format : format; 612 613 assert( 614 false, 615 "Circular dependency detected, %s depends on itself:\n%s -> %s".format( 616 service._baseType, 617 typeToTestStackWhenLoopIsFound.map!(t => t.toString()) 618 .joiner(" -> "), 619 service._baseType 620 ) 621 ); 622 } 623 } 624 } 625 /// 626 unittest 627 { 628 import std.exception : assertThrown; 629 630 // Technically shouldn't catch asserts, but this is just for testing. 631 assertThrown!Throwable(new ServiceProvider([ 632 ServiceInfo.asSingleton!CA, 633 ServiceInfo.asSingleton!CB 634 ])); 635 636 // Uncomment when tweaking with the error message. 637 // new ServiceProvider([ 638 // ServiceInfo.asSingleton!CA, 639 // ServiceInfo.asSingleton!CB 640 // ]); 641 } 642 version(unittest) // Because of forward referencing being required. 643 { 644 static class CA 645 { 646 this(CB){} 647 } 648 static class CB 649 { 650 this(CA) {} 651 } 652 } 653 } 654 655 /++ 656 + Constructs a new `ServiceProvider` that makes use of the given `services`. 657 + 658 + Builtin_Services: 659 + * [Singleton] `ServiceProvider` - `this` 660 + 661 + * [Scoped] `ServiceScopeAccessor` - A service for easily accessing slave `ServiceScope`s. 662 + 663 + Assertions: 664 + No service inside of `services` is allowed to directly or indirectly depend on itself. 665 + e.g. A depends on B which depends on A. This is not allowed. 666 + 667 + Params: 668 + services = Information about all of the services that can be provided. 669 + ++/ 670 this(ServiceInfo[] services) 671 { 672 this._defaultScope = this.createScope(); 673 674 // I'm doing it this weird way to try and make the GC less angry. 675 const EXTRA_SERVICES = 2; 676 this._allServices.length = services.length + EXTRA_SERVICES; 677 this._allServices[0..services.length] = services[0..$]; 678 this._allServices[services.length] = ServiceInfo.asSingleton!ServiceProvider((ref _) => this); // Allow ServiceProvider to be injected. 679 this._allServices[services.length + 1] = ServiceInfo.asScoped!ServiceScopeAccessor((ref serviceScope) // Add service to allowed services to access their scope's... scope. 680 { 681 auto instance = this._scopes[serviceScope._index].accessor; 682 if(instance !is null) 683 return instance; 684 685 instance = new ServiceScopeAccessor(serviceScope); 686 this._scopes[serviceScope._index].accessor = instance; 687 688 return instance; 689 }); 690 691 this.assertNoDependencyLoops(); 692 this.assertLifetimesAreCompatible(); 693 } 694 695 public final 696 { 697 /++ 698 + Creates a new scope. 699 + 700 + Performance: 701 + For creating a scope, most of the performance cost is only made during the very first creation of a scope of a specific 702 + index. e.g. If you create scope[index=1], destroy it, then make another scope[index=1], the second scope should be made faster. 703 + 704 + The speed difference is likely negligable either way though. 705 + 706 + Most of the performance costs of making a scope will come from the creation of scoped services for each new scope, but those 707 + are only performed lazily anyway. 708 + 709 + Returns: 710 + A master `ServiceScope`. 711 + ++/ 712 @safe 713 ServiceScope createScope() 714 { 715 size_t index = 0; 716 foreach(i; 0..ulong.sizeof * 8) 717 { 718 if(!this.isScopeInUse(i)) 719 { 720 index = i; 721 this.setScopeInUse(i, true); 722 break; 723 } 724 } 725 726 if(this._scopes.length <= index) 727 this._scopes.length = (index + 1); 728 729 return ServiceScope(index, this, true); 730 } 731 /// 732 unittest 733 { 734 import std.format : format; 735 736 // Basic test to make sure the "in use" mask works properly. 737 auto provider = new ServiceProvider(null); 738 739 ServiceScope[3] scopes; 740 foreach(i; 0..scopes.length) 741 { 742 scopes[i] = provider.createScope(); 743 assert(scopes[i]._index == i + 1, format("%s", scopes[i]._index)); 744 } 745 746 provider.destroyScope(scopes[1]); // Index 2 747 assert(scopes[1]._index == 0); 748 assert(scopes[1]._provider is null); 749 assert(provider._scopeInUseMasks[0] == 0b1011); 750 751 scopes[1] = provider.createScope(); 752 assert(scopes[1]._index == 2); 753 assert(provider._scopeInUseMasks[0] == 0b1111); 754 } 755 756 /++ 757 + Destroys a scope. 758 + 759 + Behaviour: 760 + Currently, destroying a scope simply means that the `ServiceProvider` can reuse a small amount of memory 761 + whenever a new scope is created. 762 + 763 + In the future I'd like to add more functionality, such as detecting scoped services that implement specific 764 + interfaces for things such as `IDisposableService`, `IReusableService`, etc. 765 + 766 + Params: 767 + serviceScope = The master `ServiceScope` representing the scope to destroy. 768 + ++/ 769 void destroyScope(ref scope ServiceScope serviceScope) 770 { 771 assert(serviceScope._provider is this, "Attempting to destroy service scope who does not belong to this `ServiceProvider`."); 772 assert(this.isScopeInUse(serviceScope._index), "Bug?"); 773 assert(serviceScope._isMasterReference, "Attempting to destroy service scope who is not the master reference for the scope. (Did this ServiceScope come from ServiceScopeAccessor?)"); 774 775 // For now, just clear the AA. Later on I'll want to add more behaviour though. 776 this.setScopeInUse(serviceScope._index, false); 777 this._scopes[serviceScope._index].instances.clear(); 778 779 serviceScope._index = 0; 780 serviceScope._provider = null; 781 } 782 783 /++ 784 + Returns: 785 + The `ServiceInfo` for the given `baseType`, or `null` if the `baseType` is not known by this `ServiceProvider`. 786 + ++/ 787 @safe 788 Nullable!(const(ServiceInfo)) getServiceInfoForBaseType(TypeInfo baseType) const nothrow pure 789 { 790 foreach(service; this._allServices) 791 { 792 if(service._baseType is baseType) 793 return nullable(service); 794 } 795 796 return typeof(return).init; 797 } 798 799 /// ditto 800 Nullable!(const(ServiceInfo)) getServiceInfoForBaseType(alias BaseType)() const 801 if(ServiceInfo.isValidBaseType!BaseType) 802 { 803 return this.getServiceInfoForBaseType(typeid(BaseType)); 804 } 805 /// 806 unittest 807 { 808 static class C {} 809 810 auto info = ServiceInfo.asScoped!C(); 811 const provider = new ServiceProvider([info]); 812 813 assert(provider.getServiceInfoForBaseType!C() == info); 814 } 815 816 @property @safe @nogc 817 ref ServiceScope defaultScope() nothrow pure 818 { 819 return this._defaultScope; 820 } 821 } 822 } 823 824 /++ 825 + A static class containing functions to easily construct objects with Dependency Injection, or 826 + execute functions where their parameters are injected via Dependency Injection. 827 + ++/ 828 static final class Injector 829 { 830 import std.traits : ReturnType, isSomeFunction; 831 832 public static final 833 { 834 /++ 835 + Executes a function where all of its parameters are retrieved from the given `ServiceScope`. 836 + 837 + Limitations: 838 + Currently you cannot provide your own values for parameters that shouldn't be injected. 839 + 840 + Behaviour: 841 + For parameters that are a `class` or `interface`, an attempt to retrieve them as a service from 842 + `services` is made. These parameters will be `null` if no service for them was found. 843 + 844 + For parameters of other types, they are left as their `.init` value. 845 + 846 + Params: 847 + services = The `ServiceScope` allowing access to any services to be injected into the function call. 848 + func = The function to execute. 849 + 850 + Returns: 851 + Whatever `func` returns. 852 + ++/ 853 ReturnType!F execute(F)(ref ServiceScope services, F func) 854 if(isSomeFunction!F) 855 { 856 import std.traits : Parameters; 857 858 alias FuncParams = Parameters!F; 859 FuncParams params; 860 861 static foreach(i, ParamT; FuncParams) 862 {{ 863 static if(is(ParamT == class) || is(ParamT == interface)) 864 params[i] = services.getServiceOrNull!ParamT; 865 }} 866 867 static if(is(ReturnType!F == void)) 868 func(params); 869 else 870 return func(params); 871 } 872 /// 873 unittest 874 { 875 static class AandB 876 { 877 int a = 1; 878 int b = 3; 879 } 880 881 static int addAandB(AandB ab) 882 { 883 return ab.a + ab.b; 884 } 885 886 auto services = new ServiceProvider([ServiceInfo.asSingleton!AandB]); 887 888 assert(Injector.execute(services.defaultScope, &addAandB) == 4); 889 } 890 891 /++ 892 + Constructs a `class` or `struct` via Dependency Injection. 893 + 894 + Limitations: 895 + See `Injector.execute`. 896 + 897 + There are no guard checks implemented to ensure services with incompatible lifetimes aren't being used together. However, `ServiceProvider` does contain 898 + a check for this, please refer to its documentation. 899 + 900 + There are no guard checks implemented to block circular references between services. However, `ServiceProvider` does contain 901 + a check for this, please refer to its documentation. 902 + 903 + Behaviour: 904 + See `Injector.execute` for what values are injected into the ctor's parameters. 905 + 906 + If the type has a normal ctor, then the result of `__traits(getMember, T, "__ctor")` is used as the constructor. 907 + Types with multiple ctors are undefined behaviour. 908 + 909 + If the type contains a static function called `injectionCtor`, then that function takes priority over any normal ctor 910 + and will be used to construct the object. Types with multiple `injectionCtor`s are undefined behaviour. 911 + 912 + If the type does not contain any of the above ctor functions then: 913 + 914 + * If the type is a class, `new T()` is used (if possible, otherwise compiler error). 915 + 916 + * If the type is a `struct`, `T.init` is used. 917 + 918 + Params: 919 + services = The `ServiceScope` allowing access to any services to be injected into the newly constructed object. 920 + 921 + Returns: 922 + The newly constructed object. 923 + ++/ 924 T construct(alias T)(ref ServiceScope services) 925 if(is(T == class) || is(T == struct)) 926 { 927 import std.traits : Parameters; 928 929 alias Ctor = Injector.FindCtor!T; 930 931 static if(Injector.isStaticFuncCtor!Ctor) // Special ctors like `injectionCtor` 932 { 933 alias CtorParams = Parameters!Ctor; 934 return Injector.execute(services, (CtorParams params) => T.injectionCtor(params)); 935 } 936 else static if(Injector.isBuiltinCtor!Ctor) // Normal ctor 937 { 938 alias CtorParams = Parameters!Ctor; 939 940 static if(is(T == class)) 941 return Injector.execute(services, (CtorParams params) => new T(params)); 942 else 943 return Injector.execute(services, (CtorParams params) => T(params)); 944 } 945 else // NoValidCtor 946 { 947 static if(is(T == class)) 948 return new T(); 949 else 950 return T.init; 951 } 952 } 953 954 /// `FindCtor` will evaluate to this no-op function if it can't find a proper ctor. 955 static void NoValidCtor(){} 956 957 /++ 958 + Finds the most appropriate ctor for use with injection. 959 + 960 + Notes: 961 + Types that have multiple overloads of a ctor produce undefined behaviour. 962 + 963 + You can use `Injector.isBuiltinCtor` and `Injector.isStaticFuncCtor` to determine what type of Ctor was chosen. 964 + 965 + Returns: 966 + Either the type's `__ctor`, the type's `injectionCtor`, or `NoValidCtor` if no appropriate ctor was found. 967 + ++/ 968 template FindCtor(T) 969 { 970 static if(__traits(hasMember, T, "injectionCtor")) 971 alias FindCtor = __traits(getMember, T, "injectionCtor"); 972 else static if(__traits(hasMember, T, "__ctor")) 973 alias FindCtor = __traits(getMember, T, "__ctor"); 974 else 975 alias FindCtor = NoValidCtor; 976 } 977 978 enum isBuiltinCtor(alias F) = __traits(identifier, F) == "__ctor"; 979 enum isStaticFuncCtor(alias F) = !isBuiltinCtor!F && __traits(identifier, F) != "NoValidCtor"; 980 } 981 } 982 983 // Testing transient services 984 unittest 985 { 986 static class Transient 987 { 988 int increment = 0; 989 990 int getI() 991 { 992 return this.increment++; 993 } 994 } 995 996 auto services = new ServiceProvider([ServiceInfo.asTransient!Transient]); 997 998 auto serviceA = services.defaultScope.getServiceOrNull!Transient(); 999 auto serviceB = services.defaultScope.getServiceOrNull!Transient(); 1000 1001 assert(serviceA.getI() == 0); 1002 assert(serviceA.getI() == 1); 1003 assert(serviceB.getI() == 0); 1004 } 1005 1006 // Testing scoped services 1007 unittest 1008 { 1009 static class Scoped 1010 { 1011 int increment = 0; 1012 1013 int getI() 1014 { 1015 return this.increment++; 1016 } 1017 } 1018 1019 auto services = new ServiceProvider([ServiceInfo.asScoped!Scoped]); 1020 auto scopeB = services.createScope(); 1021 1022 auto serviceA1 = services.defaultScope.getServiceOrNull!Scoped(); 1023 auto serviceA2 = services.defaultScope.getServiceOrNull!Scoped(); // So I can test that it's using the same one. 1024 auto serviceB = scopeB.getServiceOrNull!Scoped(); 1025 1026 assert(serviceA1 is serviceA2, "Scoped didn't work ;("); 1027 assert(serviceA1.getI() == 0); 1028 assert(serviceA2.getI() == 1); 1029 assert(serviceB.getI() == 0); 1030 1031 services.destroyScope(scopeB); 1032 1033 scopeB = services.createScope(); 1034 serviceB = scopeB.getServiceOrNull!Scoped(); 1035 assert(serviceB.getI() == 0); 1036 } 1037 1038 // Testing singleton services 1039 unittest 1040 { 1041 static class Singleton 1042 { 1043 int increment = 0; 1044 1045 int getI() 1046 { 1047 return this.increment++; 1048 } 1049 } 1050 1051 auto services = new ServiceProvider([ServiceInfo.asSingleton!Singleton]); 1052 auto scopeB = services.createScope(); 1053 1054 auto serviceA = services.defaultScope.getServiceOrNull!Singleton; 1055 auto serviceB = scopeB.getServiceOrNull!Singleton; 1056 1057 assert(serviceA !is null && serviceA is serviceB); 1058 assert(serviceA.getI() == 0); 1059 assert(serviceB.getI() == 1); 1060 } 1061 1062 // Testing scope dtor behaviour 1063 unittest 1064 { 1065 auto services = new ServiceProvider(null); 1066 auto scopeB = services.createScope(); 1067 assert(scopeB._index == 1); 1068 1069 // Test #1, seeing if setting a new value for a scope variable performs the dtor properly. 1070 { 1071 scopeB = services.createScope(); 1072 assert(scopeB._index == 2); 1073 assert(services._scopeInUseMasks[0] == 0b101); 1074 } 1075 1076 services.destroyScope(scopeB); 1077 1078 // Test #2, seeing if going out of scope uses the dtor properly (it should if the first case works, but may as well add a test anyway :P) 1079 { 1080 auto scopedScope = services.createScope(); 1081 assert(scopedScope._index == 1); 1082 1083 scopeB = services.createScope(); 1084 assert(scopeB._index == 2); 1085 } 1086 1087 assert(services._scopeInUseMasks[0] == 0b101); 1088 scopeB = services.createScope(); 1089 assert(scopeB._index == 1); 1090 assert(services._scopeInUseMasks[0] == 0b11); 1091 } 1092 1093 // Test ServiceProvider injection 1094 unittest 1095 { 1096 auto services = new ServiceProvider(null); 1097 1098 assert(services.defaultScope.getServiceOrNull!ServiceProvider() is services); 1099 } 1100 1101 // Test ServiceScopeAccessor 1102 unittest 1103 { 1104 auto services = new ServiceProvider(null); 1105 1106 // Also test to make sure the slaveScope doesn't destroy the scope outright. 1107 { 1108 const slaveScope = services.defaultScope.getServiceOrNull!ServiceScopeAccessor().serviceScope; 1109 assert(slaveScope._provider is services); 1110 assert(slaveScope._index == 0); 1111 assert(!slaveScope._isMasterReference); 1112 } 1113 assert(services._scopeInUseMasks[0] == 0b1); 1114 1115 // Test to make sure we only construct the accessor a single time per scope index. 1116 auto masterScope = services.createScope(); 1117 assert(services._scopes[1].accessor is null); 1118 assert(masterScope.getServiceOrNull!ServiceScopeAccessor() !is null); 1119 assert(services._scopes[1].accessor !is null); 1120 1121 auto accessor = masterScope.getServiceOrNull!ServiceScopeAccessor(); 1122 services.destroyScope(masterScope); 1123 masterScope = services.createScope(); 1124 assert(services._scopes[1].accessor is accessor); 1125 }