C# Property Injection: Implementation Guide

published on 28 August 2024

Property injection is a flexible way to manage dependencies in C#. Here's what you need to know:

  • It sets dependencies through public properties
  • Useful for optional or runtime-changeable dependencies
  • Supported by major DI containers

Key points:

  • Can combine with constructor injection
  • Requires careful null-checking
  • Offers flexibility but can lead to issues if not managed properly

Implementation:

  1. Define public properties for dependencies
  2. Use a DI container to set properties
  3. Provide defaults for optional dependencies

Watch out for:

  • Null reference exceptions
  • Circular dependencies
  • Overcomplicating classes

Quick Container Comparison:

Framework Strengths Best For
Unity Lightweight Small-medium projects
Autofac Modular API Complex applications
Microsoft DI .NET Core support ASP.NET Core projects

Property injection explained

Property injection sets dependencies via public properties. It's flexible for optional dependencies or complex object graphs.

Basic example:

public class UserService
{
    public IUserRepository UserRepository { get; set; }

    public User GetUser(int id)
    {
        return UserRepository.GetById(id);
    }
}

Useful when:

  • Dependencies are optional
  • There's a good default
  • Dependencies may change after creation

But beware of null reference issues if not set properly.

Comparison:

Aspect Property Injection Constructor Injection
Required Optional Required
Object State Can be incomplete Always complete
Flexibility Changeable Fixed after creation
Defaults Easy Needs overloads
Clarity Less obvious Clear from constructor
Testing Easy mocking Needs all dependencies

Mark Seemann says: "Property Injection works well for optional dependencies with runtime defaults."

Tips:

  1. Use for optional or defaulted dependencies
  2. Watch for null references
  3. Consider using a DI container

Unity container example:

public class Driver
{
    [Dependency]
    public ICar Car { get; set; }

    public void RunCar()
    {
        Console.WriteLine($"Running {Car.GetType().Name} - {Car.Run()} miles");
    }
}

This allows flexible dependency management in C# apps.

Before you start

To use property injection in C#, you'll need:

  • Solid C# programming skills
  • Understanding of interfaces and OOP
  • Basic dependency injection concepts
  • .NET Core or .NET Framework
  • An IDE like Visual Studio

Setup steps:

  1. Choose your IDE (Visual Studio recommended)

  2. Install:

    • Latest .NET SDK
    • NuGet Package Manager
  3. Create a new C# project (Web API is good for practice)

  4. Add a DI container:

Container Description Install Command
Autofac Feature-rich Install-Package Autofac
Unity Lightweight Install-Package Unity
Ninject Simple to use Install-Package Ninject
  1. Configure DI in Program.cs or Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IMyService, MyService>();
}

Now you're set to start using property injection in your C# project.

How to use property injection in C#

Here's how to implement property injection:

  1. Create interfaces:
public interface IMessageService
{
    void SendMessage(string message);
}
  1. Add injectable properties:
public class NotificationService
{
    public IMessageService MessageService { get; set; }

    public void SendNotification(string message)
    {
        MessageService.SendMessage(message);
    }
}

With Unity, use the [Dependency] attribute:

public class Driver
{
    [Dependency]
    public ICar Car { get; set; }

    public void RunCar()
    {
        Console.WriteLine($"Running {Car.GetType().Name} - {Car.Run()} miles");
    }
}

Usage:

IUnityContainer container = new UnityContainer();
container.RegisterType<ICar, Maruti>();
Driver driver = container.Resolve<Driver>();
driver.RunCar();
Injection Type Pros Cons
Property Optional dependencies, post-creation setting Not guaranteed at construction, null reference risk
Constructor Guaranteed dependencies, clear requirements Large signatures, all dependencies required

Property injection works well for optional dependencies or post-creation changes, but be careful with null references.

sbb-itb-29cd4f6

Working with dependency injection containers

Let's look at property injection with Unity, Autofac, and Microsoft's DI container.

Unity Container

Unity

  1. Install Unity NuGet package
  2. Register dependencies
  3. Resolve instances

Example:

var container = new UnityContainer();
container.RegisterType<ICar, BMW>();
container.RegisterType<Driver>();

var driver = container.Resolve<Driver>();
driver.RunCar();

Use [Dependency] attribute:

public class Driver
{
    [Dependency]
    public ICar Car { get; set; }

    public void RunCar()
    {
        Console.WriteLine($"Running {Car.GetType().Name} - {Car.Run()} miles");
    }
}

Autofac

Autofac

  1. Install Autofac: Install-Package Autofac
  2. Create ContainerBuilder
  3. Register components
  4. Build container and resolve

Example:

var builder = new ContainerBuilder();
builder.RegisterType<EmailService>().As<IEmailService>();
builder.RegisterType<NotificationService>();

var container = builder.Build();
var notificationService = container.Resolve<NotificationService>();

For more control:

builder.RegisterType<NotificationService>().PropertiesAutowired();

Microsoft's DI container

Doesn't natively support property injection. Workaround:

  1. Create custom PropertyInjectionServiceProvider
  2. Wrap built-in IServiceProvider

Basic implementation:

public class PropertyInjectionServiceProvider : IServiceProvider
{
    private readonly IServiceProvider _serviceProvider;

    public PropertyInjectionServiceProvider(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public object GetService(Type serviceType)
    {
        var service = _serviceProvider.GetService(serviceType);
        if (service != null)
        {
            InjectProperties(service);
        }
        return service;
    }

    private void InjectProperties(object service)
    {
        var properties = service.GetType().GetProperties()
            .Where(p => p.CanWrite && p.GetCustomAttributes(typeof(InjectAttribute), true).Any());

        foreach (var property in properties)
        {
            var propertyType = property.PropertyType;
            var value = _serviceProvider.GetService(propertyType);
            if (value != null)
            {
                property.SetValue(service, value);
            }
        }
    }
}

Use in Startup.cs:

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    var serviceProvider = new PropertyInjectionServiceProvider(app.ApplicationServices);
    app.ApplicationServices = serviceProvider;
}
Container Native Support Ease of Use Performance
Unity Yes High Good
Autofac Yes High Excellent
Microsoft No (needs workaround) Medium Good

Choose based on your project needs and team expertise.

Tips for good property injection

Use property injection wisely:

  • For optional dependencies
  • When dependencies might change at runtime

Example:

public class Document
{
    private Lazy<IGrammarChecker> _grammarChecker = new Lazy<IGrammarChecker>(() => new DefaultGrammarChecker());

    public IGrammarChecker GrammarChecker
    {
        get => _grammarChecker.Value;
        set => _grammarChecker = new Lazy<IGrammarChecker>(() => value ?? new DefaultGrammarChecker());
    }

    public void CheckGrammar()
    {
        GrammarChecker.CheckGrammar();
    }
}

This uses lazy initialization and ensures a default is always available.

Avoid common mistakes:

Mistake Consequence Prevention
Uninitialized properties Runtime errors Use defaults or lazy init
Overuse Unclear dependencies Prefer constructor injection for essentials
Circular dependencies Stack overflow Carefully design object graph
Thread-safety issues Race conditions Use thread-safe patterns

Best practices:

  1. Use constructor injection for core dependencies
  2. Keep it simple
  3. Depend on abstractions
  4. Mind dependency lifecycles

Advanced property injection techniques

Combine injection methods for flexibility:

  1. Constructor injection for core dependencies:
public class UserService
{
    private readonly IUserRepository _userRepository;

    public UserService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    public ILogger Logger { get; set; }
}
  1. Property injection for optional dependencies:
public class EmailService
{
    private readonly ISmtpClient _smtpClient;

    public EmailService(ISmtpClient smtpClient)
    {
        _smtpClient = smtpClient;
    }

    public IEmailFormatter Formatter { get; set; } = new DefaultEmailFormatter();
}
  1. Method injection for operation-specific needs:
public class OrderProcessor
{
    public void ProcessOrder(Order order, IPaymentGateway paymentGateway)
    {
        // Use paymentGateway
    }
}

To break circular dependencies:

  1. Use property injection in one class:
public class ClassA
{
    public ClassB B { get; set; }
}

public class ClassB
{
    private readonly ClassA _a;

    public ClassB(ClassA a)
    {
        _a = a;
    }
}
  1. Introduce a shared interface:
public interface ISharedFunctionality
{
    void CommonMethod();
}

public class ClassA : ISharedFunctionality
{
    public ISharedFunctionality SharedFunctionality { get; set; }
    public void CommonMethod() { }
}

public class ClassB : ISharedFunctionality
{
    public ISharedFunctionality SharedFunctionality { get; set; }
    public void CommonMethod() { }
}
  1. Use a factory pattern:
public class DependencyFactory
{
    private ClassA _a;
    private ClassB _b;

    public ClassA GetClassA()
    {
        if (_a == null)
        {
            _a = new ClassA();
            _a.B = GetClassB();
        }
        return _a;
    }

    public ClassB GetClassB()
    {
        if (_b == null)
        {
            _b = new ClassB();
            _b.A = GetClassA();
        }
        return _b;
    }
}

Remember: Mixed injection can complicate code. Use clear naming and docs. Circular dependencies often signal design issues - consider refactoring.

Testing and fixing problems

To test classes with property injection:

  1. Create mocks
  2. Inject dependencies
  3. Test behavior

Example:

[Fact]
public void TestEmailService()
{
    var mockSmtpClient = new Mock<ISmtpClient>();
    var emailService = new EmailService(mockSmtpClient.Object);
    emailService.Formatter = new Mock<IEmailFormatter>().Object;

    emailService.SendEmail("test@example.com", "Test Subject", "Test Body");

    mockSmtpClient.Verify(x => x.Send(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once);
}

Common issues and fixes:

  1. Null reference exceptions: Use null-conditional operator or null object pattern.
public class UserService
{
    public ILogger Logger { get; set; } = NullLogger.Instance;

    public void DoSomething()
    {
        Logger?.Log("Doing something");
    }
}
  1. Circular dependencies: Use property injection to break the cycle.

  2. Overcomplication: Review class design, split if needed.

  3. Lifetime mismanagement: Understand service lifetimes.

Lifetime Description Use Case
Transient New instance each time Lightweight, stateless services
Scoped New instance per scope Services needing state within a scope
Singleton Single instance Expensive or global state services
  1. Late initialization: Use lazy initialization or defaults.
public class DataProcessor
{
    private Lazy<IDataSource> _dataSource;
    public IDataSource DataSource
    {
        get => _dataSource.Value;
        set => _dataSource = new Lazy<IDataSource>(() => value);
    }

    public DataProcessor()
    {
        _dataSource = new Lazy<IDataSource>(() => new DefaultDataSource());
    }
}

Wrap-up

Key takeaways:

  • Property injection offers flexible dependency management
  • Useful for optional dependencies and breaking cycles
  • Supported by major DI containers
  • Watch for null references and lifetime issues

Next steps:

  1. Choose the right DI framework
  2. Practice implementing property injection
  3. Explore advanced techniques
  4. Learn to write effective unit tests
  5. Stay updated on DI developments

Related posts

Read more