Dependency Injection in C#: How to Implement It

Neo Infoway - WEB & Mobile Development Company | Festival | Neo | Infoway | Leading software Development company | Top Software development company in India
Document

Dependency Injection in C#: How to Implement It

Every programmer has an obligation to create programs that require minimal maintenance and operate consistently and effectively. These apps’ coding also has to be easily extensible and maintained so that new features can be introduced to the codebase in later releases and upgrades.

It is advised to use dependency injection while writing code to make it easier to read and reuse. Loosely linked code is always better when it comes to testing, code reuse, and making it easier to add new features more quickly.

For this reason, dependency injection is used in applications to achieve loose coupling in code. This post will describe dependency injection in C# and show you how to use it to create code that is loosely connected.

What is Dependency Injection in C#?

To truly understand dependency injection, one must be conversant with both dependency inversion and inversion of control (IoC). The process of making more abstract modules dependent on concrete ones is known as dependency inversion.

Inversion of control allows.NET developers to change the way things usually get done. Stated differently, it helps reduce the need for external code. When inversion of control occurs, the object is sent to the framework, which takes over the responsibility of resolving the dependencies among the different classes and modules.

Because DI divides responsibilities across modules, it encourages developers to write less interconnected code. More precisely, DI lessens the amount of connection between the various parts of code, making it easier for programmers to write and edit. Additionally, it creates the code.

Types of Dependency Injection

Here are the three popular types of Dependency injection

Constructor Injection

Constructor injection is the most widely used type of dependency injection. It is a technique to delegate the task of acquiring necessary components to a class’s constructor. Every necessary part is provided as a distinct constructor argument. You should inject the corresponding interfaces rather than the actual classes when performing constructor dependency injection correctly. This occurrence is known as “interface injection.”

Implementing Dependency Injection Using Constructor Injection

The most often used technique for injecting dependencies is constructor dependency injection. When generating an object, the client class constructor requires an argument, which is required by this constructor dependence.

A constructor method is called upon when a class instance is created. In constructor injection, the client is required to provide an argument. By doing this, the client instance or object’s integrity is confirmed. The constructor receives the need as an input. Anywhere in the class is a good place to use the injection mechanism.

C-sharp code for using constructor injection is as follows:

                                                    
 using System;
 
namespace DependencyInjection
{
       public interface IEmployeeService
    {
            void Serve();
    }
                                                         
     // Initialize Employee1
    public class Employee1 : IEmployeeService
    {
        public void Serve()
        {
            Console.WriteLine("Employee 1 is Initialized.");
        }
    }
                                                         
        // Initialize Employee2
       public class Employee2 : IEmployeeService
        {
            public void Serve()
            {
				Console.WriteLine("Employee 2 is Initialized.");
            }
        }
                                                         
                public class Client
                {
                    // it's constructor injection
                        private IEmployeeService _service;
                            public Client(IEmployeeService service)
                            {
                                _service = service;
                            }
                                                         
                            public void Serve()
                            {
                                _service.Serve();
                            }
                }
                                                         
                public class Program
                {
                    public static void Main(string[] args)
                    {
                        Employee1 employee1 = new Employee1();
                         // Passing the Employee1 dependency
                        Client client = new Client(employee1);
                        client.Serve();
                                                         
                        Employee employees = new Employee2();
                        // Passing the Employee2 dependency
                        client = new Client(employee2);
                        client.Serve();
                                                         
                        Console.ReadKey();
                    }
                }
}
                                                        
                                                    
                                                    

In order to avoid the Service that implements the IEmployeeService Interface, the injection takes place in the constructor. A “Builder” assembles the dependencies, and their duties include the following:

  • being aware of each Employee Services kind.
  • Feed the client the abstract IEmployeeService in accordance with the request

Property Injection

“Property injection” is the process of adding a dependency using a property to a client class (dependent class). The main advantage of property injection is that it lets you add dependencies without changing the constructors that are already present in the class. An additional method for communicating this dependence is via lazy loading.

Stated differently, until the dependent class property is called, the concrete class remains unset. Alternatively, this injection type can be substituted with a setter method. This function merely has to take the dependent and put it into a variable.

Implementing Dependency Injection Using Property Injection

Regarding Property dependency Injection, the injector must inject the dependence object through a public property of the client class. We will examine an example of the same that is expressed in C# in the code below:

                                                        
   
using System;

	namespace DependencyInjection
	{
		public interface IEmployeeService
		{	
			void Serve();
		}

			// Initialize Employee1
		public class Employee1 : IEmployeeService
		{
			public void Serve()
			{
				Console.WriteLine("Employee 1 is Initialized.");
			}
		}			

		// Initialize Employee2
		public class Employee2 : IEmployeeService
		{
			public void Serve()
			{
				Console.WriteLine("Employee 2 is Initialized.");
			}
		}

		public class Client
		{
			private IEmployeeService _service;

			//Property Injection
			public IEmployeeService Service
			{           
				set { this._service = value; }
			}
			public void ServeMethod()
			{
				this._service.Serve();
			}
		}

		public class Program
		{
			public static void Main(string[] args)
			{
				//creating object
				Employee1 employee1 = new Employee1();

				Client client = new Client();
				client.Service = employee1; //passing dependency to property
				client.ServeMethod();

				Employee employees = new Employee2();
				client.Service = employee2; //passing dependency to property
				client.ServeMethod();

				Console.ReadLine();
			}
		}	
	}
	   
                                                            
                                                        
                                                        

The developer has defined a Client class in the code above. This class has a public property called Service, where instances of the Employee and Employee2 classes can be set

Method Injection

The developer has defined a Client class in the code above. This class has a public property called Service, where instances of the Employee and Employee2 classes can be set.

Implementing Dependency Injection Using Method Injection

                                                    
 using System;

	namespace DependencyInjection
	{
		public interface IEmployeeService
		{
		void Serve();
		}

		// Initialize Employee1
		public class Employee1 : IEmployeeService
		{
		public void Serve()
		{
			Console.WriteLine("Employee 1 is Initialized.");
		}
	}

	// Initialize Employee2
	public class Employee2 : IEmployeeService
	{
		public void Serve()
		{
			Console.WriteLine("Employee 2 is Initialized.");
		}
	}

	public class Client
	{
		public void ServeMethod(IEmployeeService service)
		{
			service.Serve();
		}
	}

	public class Program
	{
			public static void Main(string[] args)
			{
				Client client = new Client();

			//creating object
				Employee1 employee1 = new Employee1();         	
				client.ServeMethod(employee1); //passing dependency to method

				Employee employees = new Employee2();
				client.ServeMethod(employee2); //passing dependency to method

				Console.ReadLine();
			}
	}
	}

                                                    
                                                    

The Client class has a public method called ServeMethod, as seen in the C# code example above, where you can pass an instance of the Employee and Employee2 classes.

Benefits of Dependency Injection

You may not be aware of it, but dependency injection is a crucial idea in programming. We will discuss five key benefits of dependency injection for C# developers in this article.

Cleaner Code with Dependency Injection.

For programmers, one of the biggest sources of aggravation is an increasing number of dependencies. A common dependency injection pattern is to create a global variable that has a reference to the class or service that is being utilized. It works well for the time being. But, things become complex when you have multiple instances of a class or service in your code and you need to manipulate a single instance of that class or service. dependency injection, which divides the dependent component from the component supplying the dependence, solves this problem.

One of the main goals of software engineering is to provide code that is orderly and easy to fix. Simple to read and understand code is considered clean code. With closely linked programs, however, whose dependencies are not injected, this is not the case.

Classes that have to create their own dependencies or call singletons become more complicated and less reusable. There is an abundance of redundant code as a result.

Dependency injection allows dependencies to be “injected” into an object. This suggests that system-wide functionality is being achieved with fewer static classes.

Unit Tests with Dependency Injection.

One of the best ways to keep your code from crashing unexpectedly is to use unit tests. Unit testing for an object should never fail; it is the responsibility of the developer who comes after you in your career path.

If you’re not testing your code, you’re not doing it right. Testing isn’t always simple and straightforward, though. Mocking dependencies is not always simple, though. It is not possible to replicate the actions of a database that you depend on.

Your unit tests may run much more efficiently if you use dependency injection correctly. When you inject the interfaces of dependents, you can provide a test double (a dummy object or proxy object) for an injected interface. This suggests that you are in total control of the dependence that was injected:

  • Real-world data can be given to the under-test class.
  • A null value or an error may be given back.
  • You can check to see if another method is called correctly by your class.

Injecting Dependencies Promotes Separation of Concerns.

It is possible to isolate different concrete classes from one another via dependency injection. This can be achieved by injecting interfaces as opposed to actual classes. Software as a result has fewer dependencies.

The fact that your class depends on a particular concrete implementation of a dependency is concealed by this approach. It is just concerned that the dependent follows the guidelines provided by the interface.

When classes simply have loose couplings between their code, maintaining an application is not as difficult. Moreover, modifications to the component’s dependencies have no effect on your class instance.

Dependency injection improves the maintainability of programming. It’s common knowledge that software development is complex. Code has a complex and dynamic character. Developers are always trying to find ways to make the process of development simpler. Code maintenance can be facilitated by using dependency injection.

Dependency Injection Improves Code

Your web application uses MySQL to store its data. The decision is then made to use the MS SQL database for the website. Yes, provided your database layer is isolated from all other components by means of an interface. All that is needed to implement a new database is to recreate the database layer. However, if SQL code is dispersed throughout the entire service, it will be difficult to justify the extensive downtime needed to switch databases.

The ease of code maintenance directly affects the amount of time and resources required to make changes.

Code Configuration is consolidated via Dependency Injection.

Although dependency injection, or DI, is a widely used method, it can be challenging at first to implement. It is normal practice to develop an interface and to construct and connect individual pieces. Fortunately, there’s an easier fix.

You can use an Inversion of Control (IoC)-compatible container. All you have to do to configure an IoC container is tell it what kinds of objects you need and how to construct them. It is also helpful for joining different electronic parts.

Applications can be composed dynamically using IoC containers. Centralized use of dependency injection containers is another option. This suggests that one class, or at most a small group of classes, may be able to manage all dependent arrangements.

This means that you will only need to update the code once in the event that you need to change a dependent that is utilized elsewhere in the program.

Frequently Asked Questions (FAQs)

Dependency Injection is a design pattern used in C# (and other programming languages) to achieve loose coupling between classes by injecting dependencies rather than creating them internally. This pattern promotes modular, testable, and maintainable code.
In DI, dependencies of a class are provided from the outside, typically through constructor parameters or properties. This allows for easier testing and swapping of dependencies without modifying the class implementation.
  • Increased modularity: Classes become more focused on their specific responsibilities.
  • Improved testability: Dependencies can be easily mocked or stubbed during unit testing.
  • Reduced coupling: Classes are not tightly bound to their dependencies, making the codebase more flexible and maintainable.
  • Better code organization: Dependencies are clearly defined and managed externally, leading to cleaner and more organized code.
There are three main types of DI:
  • Constructor Injection: Dependencies are provided via constructor parameters.
  • Property Injection: Dependencies are injected into public properties of the dependent class.
  • Method Injection: Dependencies are passed as method parameters.
You can implement DI manually by creating instances of dependencies and passing them to dependent classes, or you can use DI containers/frameworks like Microsoft.Extensions.DependencyInjection, Autofac, or Unity to manage dependencies automatically.
An IoC container is a framework that manages the creation and resolution of dependencies in an application. It typically provides features for registering dependencies, resolving them when needed, and disposing of resources when they are no longer needed.
Dependency Injection is beneficial for most C# projects, especially those that require modularity, testability, and maintainability. However, it may introduce unnecessary complexity in small or simple projects where tight coupling is acceptable.
Dependency Injection is closely related to the SOLID principles, particularly the Dependency Inversion Principle (DIP) and the Single Responsibility Principle (SRP). DI promotes loose coupling (DIP) by allowing dependencies to be abstracted and injected, and it helps to ensure that classes have a single responsibility (SRP) by separating concerns and dependencies.
While there might be a slight performance overhead associated with resolving dependencies through DI containers, the benefits of loose coupling, testability, and maintainability usually outweigh this overhead. Additionally, modern DI containers are highly optimized and have minimal impact on performance.
  • Prefer constructor injection over property injection.
  • Register dependencies with the DI container at the application’s composition root.
  • Use interfaces to define dependencies to promote abstraction and decoupling
  • Avoid excessive nesting of DI containers within classes.

Flutter: Modularized Dependency Injection

Neo Infoway - WEB & Mobile Development Company | Festival | Neo | Infoway | Leading software Development company | Top Software development company in India
Document

Flutter: Modularized Dependency Injection

Let’s say that you’re in the phase where you’re Maintainability of of your Flutter project is a crucial aspect to consider, so it is important to make sure your project is adhering for the best practices to ensure a suitable structure and code quality and maintain it at a level that is satisfactory.In that situation, Separation of Concerns , Encapsulation , coupling , and cohesion These are the aspects you’d like to control in the architecture you choose to build.

It’s better to choose physical separation instead of logical, i.e. break your project up into Dart/Flutter packages instead of simply grouping things into various directories. If you have a tiny Flutter app that is merely the logical separation, you’ll fail to transform the app into physical files that reflect the directory structure you have. This is often due to the fact that it’s easy to violate or ignore architectural restrictions even when there’s no physical separation.

Dependency Injection in modules? How?

It’s obvious that the precise architecture will depend on the project, team and knowledge. I’m not planning to talk about architecture within the context of this article, but instead focus on the way you can arrange Dependency Injection (DI) in the form of a modularized Flutter application.

Refactoring The CounterApp

It’s obvious that the precise architecture will depend on the project, team and knowledge. I’m not planning to talk about architecture within the context of this article, but instead focus on the way you can arrange Dependency Injection (DI) in the form of a modularized Flutter application.

Cross-Cutting Concerns

A cross-cutting concern package generally contains items that impact the entire program and is able to be utilized by all layers. I added DI abstractions in it.

The first is known as DI This interface is accountable for retrieving objects from the DI container.

                    
                        abstract interface class DI {
                            T call({String? instanceName});
                            T get({String? instanceName});
                            T getWithParam(
                            P param, {
                            String? instanceName,
                           });
                           }
                         
                    

The other interface is called DIRegistrar and offers the API for registering dependencies within DI Containers. DI Container. This interface should be accessible only to the implementation and not to its abstraction as well as to the ModuleDependencies abstraction and implementation.

                    
                        typedef FactoryFunc = T Function();
                        typedef FactoryWithParamFunc = T Function(P param);
                        typedef DisposingFunc = FutureOr Function(T instance);
                        abstract interface class DIRegistrar implements DI {
                        void registerFactory(
                        FactoryFunc factoryFunc, {
                        String? instanceName,
                        });
                         void registerFactoryWithParam(
                        FactoryWithParamFunc factoryFunc, {
                         String? instanceName,
                        });
                        void registerSingleton(
                         T instance, {
                        String? instanceName,
                        DisposingFunc? disposingFunc,
                         });
                        void registerLazySingleton(
                        FactoryFunc factoryFunc, {
                        String? instanceName,
                        DisposingFunc? disposingFunc,
                         });
                        }
                        
                    

The third part of the puzzle is ModuleDependencies. It must be implemented by all the modules that have dependencies.

                        
                            abstract class ModuleDependencies {
                            Future register(DIRegistrar do);
                            Future runPostRegistrationActions(DIRegistrar do) => Future.value();
                            }
                            
                        

DI Abstractions Usage

The modules are designed to contain the implementation details and provide only the information essential.

For instance , the reason I included Flutter bloc state management into the presentation package is a design feature, which means it can be changed within this package without needing to alter any other packages in any way.

                    
                        class PresentationModuleDependencies extends ModuleDependencies {
                        @override
                        Future register(DIRegistrar do) async {
                        di.registerFactory(
                        () => CounterCubit(di()),
                         );
                        }
                        }
                         
                    

With the data package, I chose to implement shared_preferences to maintain the status of the counter in between app starts. It is also “known” only to the data package.

                        
                            class DataModuleDependencies extends ModuleDependencies { 
                            @override
                            Future register(DIRegistrar do) async {
                            final sharedPreferences = await SharedPreferences.getInstance();
                            di.registerFactory(
                            () => SharedPreferencesCounterRepository(sharedPreferences),
                            );
                            }
                            }
                            
                        

The application will,naturally, be able to have transitive dependency upon shared_preferences and Flutter_bloc in the end since this is a given and is designed to be part of the base package that eventually combined everything into one artifact. e.g. ipa, apk.

In the initialization phase of our application look through all the installed modules and instruct that they should register their dependencies.

                            
                                Future main() async {
                                WidgetsFlutterBinding.ensureInitialized();
                                final de = GetIrDI();
                                final modules = [
                                DomainModuleDependencies(),
                                PresentationModuleDependencies(),
                                DataModuleDependencies(),
                                ];
                                for (final module in modules) {
                                await module.register(di);
                                }
                                for (final module in modules) {
                                await module.runPostRegistrationActions(di);
                                }
                                runApp(App(di: di));
                                }
                                
                            

You might have been able to see the “GetItDI” program in the line of code below. The implementation is of DIRegistrar which I added to the application package. This implementation is based on get_it, which is the receive_it package. If you decide to change to a different DI Container it is possible to do it as simply as changing the design for DIRegistrar within the application layer, without impacting different packages.

The concept should be evident to you now however, you’re encouraged to look around this repository for the remaining pieces!

Frequently Asked Questions (FAQs)

Dependency injection is a design pattern used to manage the dependencies of objects within an application. In Flutter app development, DI helps decouple components, improve code maintainability, and facilitate testing by allowing dependencies to be provided externally rather than being hardcoded within classes.
Modularized dependency injection in Flutter involves organizing the application into separate modules or features, each with its own set of dependencies and services. This approach allows for better separation of concerns, easier code organization, and more flexible dependency management.
Some benefits of using modularized dependency injection in Flutter apps include:
  • Improved Code Organization: Modularization allows developers to organize code into smaller, more manageable modules, making it easier to understand and maintain.
  • Flexible Dependency Management: By breaking the application into modules, developers can manage dependencies more granularly, allowing for easier updates, substitutions, and testing of individual components.
  • Reduced Coupling: Modularized dependency injection reduces coupling between different parts of the application, making it easier to change or replace components without affecting other parts of the codebase.
  • Scalability: As the application grows, modularization enables developers to add new features or modules without impacting existing code, promoting scalability and extensibility.
In Flutter, modularized dependency injection typically involves using dependency injection containers or service locators to manage dependencies within each module or feature of the application. Each module defines its own set of services and dependencies, which can be provided and accessed within the module or shared with other modules as needed.
Some popular dependency injection libraries or frameworks for Flutter include:
  • get_it: A simple service locator for Dart and Flutter that allows for easy registration and retrieval of dependencies.
  • provider: A popular state management library for Flutter that also provides dependency injection capabilities through its Provider class.
  • injector: A lightweight dependency injection library for Dart and Flutter that supports modularization and lazy loading of dependencies.
Developers can implement modularized dependency injection in their Flutter apps by:
  • Identifying and defining separate modules or features within the application.
  • Deciding on the scope and lifecycle of dependencies within each module.
  • Using a dependency injection library or framework to register and provide dependencies within each module.
  • Injecting dependencies into classes or widgets as needed using constructor injection or other DI patterns.
Some best practices for using modularized dependency injection in Flutter apps include:
  • Keeping modules small and focused on a single responsibility.
  • Avoiding circular dependencies between modules.
  • Using named or tagged dependencies to differentiate between similar services within a module.
  • Testing each module in isolation to ensure that dependencies are correctly provided and injected.
While modularized dependency injection offers many benefits, it may introduce some complexity, especially in larger applications with many modules and dependencies. Developers should carefully consider the trade-offs and design decisions when implementing modularized dependency injection to ensure that it aligns with the needs and goals of the project.
Developers can find resources and tutorials for implementing modularized dependency injection in Flutter on official documentation provided by Flutter and Dart, community forums like Stack Overflow and GitHub, developer blogs and tutorials, online courses and webinars, and sample projects and code repositories. Additionally, exploring Flutter packages and plugins specific to dependency injection can provide additional insights and guidance for implementation.