Go to content

Bulletcode.NET

Dependency Injection

ASP.NET Core relies on the dependency injection mechanism to ensure that applications can customize the framework by implementing custom services. Bulletcode.NET takes advantage of this by re-implementing some of the core ASP.NET services, but it also allows applications to customize its own services. To make this possible, both ASP.NET and Bulletcode.NET use the TryAdd() family of methods to register their services whenever possible.

Bulletcode.NET also relies on dependency injection in desktop and mobile applications. View models are created using the service provider, which means that dependencies can be injected into them, and their lifetime is scoped to the lifetime of the corresponding page or dialog in the UI. More information can be found in the MVVM chapter.

Built-in services

The following extension methods for registering Bulletcode.NET services are available in all applications:

The following extension methods are available in web applications:

  • AddConfiguration() — see Configuration
  • AddCoreMvcServices() and AddFrontViewServices() — see MVC
  • AddUserCookie(), AddUserToken() and AddApiKey() — see Authentication
  • AddRoleBasedAuthorization() — see Authorization
  • AddUserService() and AddEventService() — see Database
  • AddSqlSession() — see Session
  • AddMailService() — see Mail
  • AddStorageService() — see Storage

The following extension methods are available in desktop applications:

  • UseApplication(), UseMainWindow() and AddNavigationProvider() — see Shell
  • AddExceptionHandler() — see Diagnostics
  • RegisterDesktopServices() — see Services

The following extension methods are available in Bulletcode.UI:

  • AddNavigationProvider() — see Shell
  • AddExceptionHandler() — see Diagnostics
  • RegisterDesktopServices() — see Services

Service attributes

To make it easier to register custom services, you can use the following attributes: Singleton, Transient and Scoped. By default, the service type is the same as the implementation type decorated by one of these attributes:

[Singleton]
public class MyService
{
}

It is also possible to specify a custom service type used to access the service, for example:

[Singleton<IMyService>]
public class MyService : IMyService
{
}

If the AllowMultiple property of the service attribute is set to true, the service is registered by calling TryAddEnumerable instead of TryAdd, which means that multiple implementations of the same service can exist.

In order to register all services decorated with the service attributes, call the RegisterServices() extension method, passing the assembly containing the types to register, for example:

services.RegisterServices( typeof( Program ).Assembly );

Service aliases

The AddAlias<TService, TImplementation>() extension method makes it possible to register a class which implements two or more different services:

public class MyService : IMyService, IMyDataProvider
{
}

Normally, when you register such class as a singleton, two separate instances of the MyService class are created for each interface. To solve this problem, you can register the second service as an alias, which means that both services will share the same instance of the MyService class:

services.AddSingleton<IMyService, MyService>();
services.AddAlias<IMyDataProvider, MyService>();

AddAlias() can also be used for scoped services; in this case, both services will share the same instance in a given scope. Aliases cannot be used for transient services.

Aliases can be added for services registered using the Singleton or Scoped attributes, by specifying one or more Alias attributes:

[Singleton<IMyService>]
[Alias<IMyDataProvider>]
public class MyService : IMyService, IMyDataProvider
{
}

IScopedServiceFactory

ASP.NET Core supports scoped services whose lifetime is bound to a particular web requests. A similar mechanism is also implemented by Bulletcode.NET to bind the lifetime of view models and their dependencies to a particular page or dialog in a desktop or mobile application. Internally, this mechanism uses the IScopedServiceFactory, which contains a method for creating an instance of the specified type and its dependencies within its own scope, and to destroy the service together with its scope.

In order to use the IScopedServiceFactory to create custom scopes, call the AddScopedServiceFactory() extension method.

Lazy<T> resolver

A common problem when using dependency injection is avoiding circular dependencies between services. Bulletcode.NET provides a mechanism which allows to lazily resolve dependencies, which makes it possible to avoid circular dependencies. It is based on the standard .NET Lazy<T> class. In order to enable the lazy resolver, call the AddLazyResolver() extension method.

For example, the API client uses the IAuthenticationProvider service for authentication. However, an implementation of the IAuthenticationProvider service typically uses the IApiClient service in order to obtain the authentication tokens. To avoid a circular dependency, the API client contains a lazy reference to the IAuthenticationProvider service, which is only resolved when the Value property is accessed for the first time:

public class AuthenticationProvider : IAuthenticationProvider
{
    private readonly IApiClient _apiClient;

    public AuthenticationProvider( IApiClient apiClient )
    {
        _apiClient = apiClient;
    }
}

public class ApiClient : IApiClient
{
    private readonly Lazy<IAuthenticationProvider> _authenticationProvider;

    public ApiClient( Lazy<IAuthenticationProvider> authenticationProvider )
    {
        _authenticationProvider = authenticationProvider;
    }
}