Skip to content

The world only full-power circular Service Provider. It can resolve circular dependencies, has compile time safety and optimal performance.

License

Notifications You must be signed in to change notification settings

BlackWhiteYoshi/CircleDI

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

CircleDI

The world only full-power circular Service Provider.

/**
 * MyService1 has dependency on MyService2
 * MyService2 has dependency on MyService1
 *
 *  --------------         --------------
 *  |            | ------> |            |
 *  | MyService1 |         | MyService2 |
 *  |            | <------ |            |
 *  --------------         --------------
 *
 **/

using CircleDIAttributes;


[ServiceProvider]
[Singleton<IMyService1, MyService1>]
[Singleton<IMyService2, MyService2>]
public partial class CircleExampleProvider;


public interface IMyService1;
public class MyService1(IMyService2 myService2) : IMyService1;

public interface IMyService2;
public class MyService2 : IMyService2 {
    public required IMyService1 MyService1 { private get; init; }
}



/**
 * MyService has dependency on MyService (not meaningful, but it resolves)
 *
 *    ---------
 *    |       |
 *    |       v
 *  -------------
 *  |           |
 *  | MyService |
 *  |           |
 *  -------------
 *
 **/

[ServiceProvider]
[Singleton<MyService>]
public partial class SelfCircleExampleProvider;

public class MyService {
    public required MyService MyServiceCircle { private get; init; }
}



Requirements

  • Language Version C#12 (default of .NET 8)
    • if you are using an older TargetFramework:
      • .NET => just set the language version to at least C#12.
      • .NET Standard 2.1 => together with the LangVersion requirement you also need some polyfills, I recommend using PolySharp.
      • .NET Framework, UWP, .NET Standard 2.0 => LangVersion C#12 or newer, polyfills (PolySharp) and disable DisposeAsync generation.



Get Started

  1. Add PackageReference to your .csproj file.
<ItemGroup>
  <PackageReference Include="CircleDI" Version="{latest version}" PrivateAssets="all" />
</ItemGroup>
  1. Create a partial class with the ServiceProviderAttribute and register a singleton service.
using CircleDIAttributes;

[ServiceProvider]
[Singleton<IMyService, MyService>]
public partial class MyFirstServiceProvider;


public interface IMyService;

public class MyService : IMyService;
  1. Create an Instance and use it.
public static class Program {
    public static void Main() {
        // instantiate ServiceProvider
        MyFirstServiceProvider serviceProvider = new();
        // get Service
        MyService myService = serviceProvider.MyService;
    }
}

Dependency Injection

  • Constructor Parameter
public class MyService : IMyService {
    private readonly IService1 _service1;
    private readonly IService2 _service2;
    
    public MyService(IService1 service1, IService2 service2) {
        _service1 = service1;
        _service2 = service2;
    }
}
  • Primary Constructor
public class MyService(IService1 service1, IService2 service2) : IMyService;
  • Property Member
public class MyService : IMyService {
    public required IService1 Service1 { private get; init; }
    public required IService2 Service2 { private get; init; }
}





Examples

Comparison to Microsoft.Extensions.DependencyInjection.ServiceCollection

Source Code Generation Output



Configuration and Customization



Attributes and Enums



Features

Resolves Circular Dependencies

Singleton and Scoped Dependencies can be injected everywhere and it will not count as dependency circles. The only requirement is to inject these dependencies as properties.

Optimal Performance

No boilerplate, no overhead, no unnecessary if-checks, only just the necessary operations to provide the service. CircleDI aims for optimal performance.

Compile Time Safety

The dependency tree is resolved at compile time and emits compile errors, when something is missing. Also because of the nature of non-generic it is not possible to retrieve a service that is not registered.

No Runtime Dependencies

This NuGet package is only a source generator, so it is a development dependency. Only the generated code gets in your binary and that is not much.

AOT friendly

It has no runtime dependencies and the generated code uses no reflection or other AOT-critical statements. Actually the generated code is quite simple. However, to resolve circular init-only dependency injection, UnsafeAccessor are used.

Beautiful Code Generation

Every indentation, every line break, every space is intended. Additionally the code is quite easy to read, it is mostly just getters and constructor calls. You can also set a breakpoint and just step into to see step by step what is happening.

Non-Generic

Instead of the generic GetService<T>() method, for every service a dedicated getter-method is generated. This is not only more performant and has better IntelliSense guidance, it also prevents accessing a service that is not registered.

Non-Lazy Instantiation

Singleton/Scoped services are instantiated within the constructor of the Service Provider. You can also configure each service to instantiate when it is first time requested (lazy). Both approaches have advantages and disadvantages, you choose.

Object Oriented

No global definition or state. You can define as many different Service Provider classes you want and instantiate each defined Provider as often as you need. Additional, it automatically creates for itself and the containing Scope class an interface respectively to support loose coupling and polymorphism.

Customizable

You can configure a lot if you want: You can decide the class name of the Service Provider, set the name of the interface, set the namespace and modifiers of the class, provide your own constructor and dispose-methods, change the name of default services, decide for each service the creation timing and get accessor, toggle the generation of the Scope class or Dispose()-methods, or make the provider not thread safe for better performance.

Easy to Use

CircleDI has many default configurations, so you can just get started with minimal effort. But if you want, you can change the behavior of all services by changing a single property at the ServiceProviderAttribute or overwrite the behavior of a specific service by changing a property at that ServiceProviderAttribute.



Comparison to Jab

CircleDI is heavily inspired by Jab and has many similarities, but there are a handful of improvements:

However, the idea of circular dependency resolving was inspired by razor components. The mechanism for injecting dependencies at razor components works differently. A component is first instantiated with the parameterless constructor and then the dependencies are injected to the marked properties. So razor components are also capable of resolving circular dependencies, however, everything is done via reflection.

The performance difference is negligible. In benchmark testing the difference shows only a few nano-seconds and about 20 to 40 bytes memory-allocations difference. In most simple tests CircleDI is a few nanoseconds faster, but has more memory-allocations. The increased memory allocation is because of constructing some objects at instantiation (singleton/scoped-services, dispose-list) instead lazily and the increased execution speed is probably because of non-generic and less if-checks.



Disable Attribute Generation

You can disable the generation of the attributes by defining a constant for your compilation:

  <PropertyGroup>
    <DefineConstants>CIRCLEDI_EXCLUDE_ATTRIBUTES</DefineConstants>
  </PropertyGroup>

This functionality is specific for the use case when you have a project referencing another project, both projects using this generator and you have InternalsVisibleTo enabled. In that case you have the attributes defined twice in your referencing project and you get a warning about that. By defining this constant in your referencing project, you prevent one generation, so the attributes are only defined once in the referenced project.



Versions

CircleDI

  • 0.1.0
    • first version, includes all basic functionalities for generating a Service Provider
  • 0.2.0
    • breaking change: CreateScope() lists dependencies without [Dependency]-attribute as parameters and dependencies with [Dependency]-attribute are supplied by the ServiceProvider
    • native/built-in types are supported
  • 0.3.0
    • added support for passing value type services by reference
    • ServiceProvider can now also be struct, record or record struct
  • 0.4.0
    • added "NoDispose"-property to disable disposing for each distinct service
    • added functionality for omitting interface generation when InterfaceName is empty
    • added Attribute ServiceProvider<TInterface> to generate the interface into an existing one
  • 0.5.0
    • added CicleDI.Blazor
    • removed Error CDI029 "Dependency CreationTiming: Constructor on Lazy" and instead the lazy instantiated service will become constructor instantiated
  • 0.6.0
    • added Minimal.API
    • added support for ServiceProvider being generic
    • improved IServiceProvder.GetService(Type) method
  • 0.7.0
    • added ImportAttribute
  • 0.8.0
    • added ComponentModuleAttribute for cross project razor components importing
    • breaking change: ServiceProvider does not longer generate TransientAttributes, add ComponentModuleAttribute to the ServiceProvider to get the same behavior as before
    • improved service tree generation
  • 0.9.0
    • support for registering services with typeof()
    • registering open/unbound generic services
  • 0.9.1
    • small breaking change: The error-id of most error messages got changed
    • small breaking change: Modules implementing IDisposable/IAsyncDisposable imported as Service get disposed
    • dedicated lock-objects instead of locking provider or lists itself
    • some fixes for specific cases

  • 1.0.0
    • changed Blazor ComponentActivator to support the default constructor dependency injection
    • changed lock type to System.Threading.Lock when available

About

The world only full-power circular Service Provider. It can resolve circular dependencies, has compile time safety and optimal performance.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published