Real-World Examples

Learn how to use PyNurseInjector in various scenarios with these practical examples.

Basic Web API Example

A simple Web API project demonstrating basic PyNurseInjector setup.

Project Structure

BookStore.Api/
├── Controllers/
│   └── BooksController.cs
├── Services/
│   ├── IBookService.cs
│   └── BookService.cs
├── Repositories/
│   ├── IBookRepository.cs
│   └── BookRepository.cs
├── Models/
│   └── Book.cs
└── Program.cs

Program.cs

using DotNurse.Injector;
using DotNurse.Injector.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

// Enable property injection
builder.Host.UseDotNurseInjector();

// Add services
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

// Register all services and repositories
builder.Services.AddServicesFrom("BookStore.Api.Services");
builder.Services.AddServicesFrom("BookStore.Api.Repositories", ServiceLifetime.Scoped);

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

Models/Book.cs

namespace BookStore.Api.Models;

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Author { get; set; }
    public string ISBN { get; set; }
    public decimal Price { get; set; }
    public DateTime PublishedDate { get; set; }
}

Services/IBookService.cs & BookService.cs

namespace BookStore.Api.Services;

public interface IBookService
{
    Task<IEnumerable<Book>> GetAllBooksAsync();
    Task<Book> GetBookByIdAsync(int id);
    Task<Book> CreateBookAsync(Book book);
    Task<Book> UpdateBookAsync(int id, Book book);
    Task DeleteBookAsync(int id);
}

public class BookService : IBookService
{
    [InjectService] 
    private readonly IBookRepository bookRepository;
    
    [InjectService] 
    private readonly ILogger<BookService> logger;

    public async Task<IEnumerable<Book>> GetAllBooksAsync()
    {
        logger.LogInformation("Fetching all books");
        return await bookRepository.GetAllAsync();
    }

    public async Task<Book> GetBookByIdAsync(int id)
    {
        logger.LogInformation("Fetching book with ID: {BookId}", id);
        var book = await bookRepository.GetByIdAsync(id);
        if (book == null)
        {
            logger.LogWarning("Book with ID {BookId} not found", id);
            throw new KeyNotFoundException($"Book with ID {id} not found");
        }
        return book;
    }

    public async Task<Book> CreateBookAsync(Book book)
    {
        logger.LogInformation("Creating new book: {Title}", book.Title);
        return await bookRepository.CreateAsync(book);
    }

    public async Task<Book> UpdateBookAsync(int id, Book book)
    {
        logger.LogInformation("Updating book with ID: {BookId}", id);
        book.Id = id;
        return await bookRepository.UpdateAsync(book);
    }

    public async Task DeleteBookAsync(int id)
    {
        logger.LogInformation("Deleting book with ID: {BookId}", id);
        await bookRepository.DeleteAsync(id);
    }
}

Controllers/BooksController.cs

namespace BookStore.Api.Controllers;

[ApiController]
[Route("api/[controller]")]
public class BooksController : ControllerBase
{
    [InjectService] 
    public IBookService BookService { get; private set; }

    [HttpGet]
    public async Task<ActionResult<IEnumerable<Book>>> GetBooks()
    {
        var books = await BookService.GetAllBooksAsync();
        return Ok(books);
    }

    [HttpGet("{id}")]
    public async Task<ActionResult<Book>> GetBook(int id)
    {
        try
        {
            var book = await BookService.GetBookByIdAsync(id);
            return Ok(book);
        }
        catch (KeyNotFoundException)
        {
            return NotFound();
        }
    }

    [HttpPost]
    public async Task<ActionResult<Book>> CreateBook(Book book)
    {
        var createdBook = await BookService.CreateBookAsync(book);
        return CreatedAtAction(nameof(GetBook), new { id = createdBook.Id }, createdBook);
    }

    [HttpPut("{id}")]
    public async Task<IActionResult> UpdateBook(int id, Book book)
    {
        try
        {
            await BookService.UpdateBookAsync(id, book);
            return NoContent();
        }
        catch (KeyNotFoundException)
        {
            return NotFound();
        }
    }

    [HttpDelete("{id}")]
    public async Task<IActionResult> DeleteBook(int id)
    {
        try
        {
            await BookService.DeleteBookAsync(id);
            return NoContent();
        }
        catch (KeyNotFoundException)
        {
            return NotFound();
        }
    }
}

Repository Pattern Example

Implementing a generic repository pattern with PyNurseInjector.

Generic Repository Interface

namespace MyApp.Core.Repositories;

public interface IRepository<T> where T : class
{
    Task<T> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();
    Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate);
    Task<T> AddAsync(T entity);
    Task UpdateAsync(T entity);
    Task DeleteAsync(T entity);
    Task<int> SaveChangesAsync();
}

public interface IUserRepository : IRepository<User>
{
    Task<User> GetByEmailAsync(string email);
    Task<IEnumerable<User>> GetActiveUsersAsync();
}

public interface IProductRepository : IRepository<Product>
{
    Task<IEnumerable<Product>> GetByCategoryAsync(string category);
    Task<IEnumerable<Product>> GetTopSellingAsync(int count);
}

Base Repository Implementation

namespace MyApp.Infrastructure.Repositories;

public abstract class BaseRepository<T> : IRepository<T> where T : class
{
    [InjectService] 
    protected AppDbContext Context { get; private set; }
    
    [InjectService] 
    protected ILogger<BaseRepository<T>> Logger { get; private set; }

    protected DbSet<T> DbSet => Context.Set<T>();

    public virtual async Task<T> GetByIdAsync(int id)
    {
        return await DbSet.FindAsync(id);
    }

    public virtual async Task<IEnumerable<T>> GetAllAsync()
    {
        return await DbSet.ToListAsync();
    }

    public virtual async Task<IEnumerable<T>> FindAsync(Expression<Func<T, bool>> predicate)
    {
        return await DbSet.Where(predicate).ToListAsync();
    }

    public virtual async Task<T> AddAsync(T entity)
    {
        await DbSet.AddAsync(entity);
        return entity;
    }

    public virtual async Task UpdateAsync(T entity)
    {
        DbSet.Update(entity);
    }

    public virtual async Task DeleteAsync(T entity)
    {
        DbSet.Remove(entity);
    }

    public async Task<int> SaveChangesAsync()
    {
        return await Context.SaveChangesAsync();
    }
}

Specific Repository Implementations

namespace MyApp.Infrastructure.Repositories;

[RegisterAs(typeof(IUserRepository))]
[ServiceLifeTime(ServiceLifetime.Scoped)]
public class UserRepository : BaseRepository<User>, IUserRepository
{
    public async Task<User> GetByEmailAsync(string email)
    {
        Logger.LogDebug("Getting user by email: {Email}", email);
        return await DbSet.FirstOrDefaultAsync(u => u.Email == email);
    }

    public async Task<IEnumerable<User>> GetActiveUsersAsync()
    {
        return await DbSet
            .Where(u => u.IsActive && !u.IsDeleted)
            .OrderBy(u => u.Name)
            .ToListAsync();
    }
}

[RegisterAs(typeof(IProductRepository))]
[ServiceLifeTime(ServiceLifetime.Scoped)]
public class ProductRepository : BaseRepository<Product>, IProductRepository
{
    public async Task<IEnumerable<Product>> GetByCategoryAsync(string category)
    {
        return await DbSet
            .Where(p => p.Category == category && p.IsAvailable)
            .ToListAsync();
    }

    public async Task<IEnumerable<Product>> GetTopSellingAsync(int count)
    {
        return await DbSet
            .OrderByDescending(p => p.SalesCount)
            .Take(count)
            .ToListAsync();
    }
}

Registration

// In Program.cs
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Register all repositories that inherit from BaseRepository
builder.Services.AddServicesFrom("MyApp.Infrastructure.Repositories", ServiceLifetime.Scoped, options =>
{
    options.ImplementationBase = typeof(BaseRepository<>);
});

Multi-Layer Architecture

Complete example of a multi-layered application using PyNurseInjector.

Solution Structure

MyEcommerce/
├── MyEcommerce.Domain/          # Domain entities and interfaces
│   ├── Entities/
│   ├── Interfaces/
│   └── ValueObjects/
├── MyEcommerce.Application/     # Business logic and DTOs
│   ├── Services/
│   ├── DTOs/
│   └── Mappings/
├── MyEcommerce.Infrastructure/  # Data access and external services
│   ├── Repositories/
│   ├── Services/
│   └── DbContext/
└── MyEcommerce.Api/            # Web API layer
    ├── Controllers/
    ├── Middleware/
    └── Program.cs

Domain Layer

// MyEcommerce.Domain/Interfaces/IOrderService.cs
namespace MyEcommerce.Domain.Interfaces;

public interface IOrderService
{
    Task<Order> CreateOrderAsync(CreateOrderDto orderDto);
    Task<Order> GetOrderByIdAsync(int orderId);
    Task<bool> ProcessPaymentAsync(int orderId, PaymentDto paymentDto);
    Task UpdateOrderStatusAsync(int orderId, OrderStatus status);
}

// MyEcommerce.Domain/Interfaces/IInventoryService.cs
public interface IInventoryService
{
    Task<bool> CheckAvailabilityAsync(int productId, int quantity);
    Task ReserveStockAsync(int productId, int quantity);
    Task ReleaseStockAsync(int productId, int quantity);
}

Application Layer

// MyEcommerce.Application/Services/OrderService.cs
namespace MyEcommerce.Application.Services;

[RegisterAs(typeof(IOrderService))]
[ServiceLifeTime(ServiceLifetime.Scoped)]
public class OrderService : IOrderService
{
    [InjectService] private IOrderRepository orderRepository;
    [InjectService] private IProductRepository productRepository;
    [InjectService] private IInventoryService inventoryService;
    [InjectService] private IPaymentService paymentService;
    [InjectService] private INotificationService notificationService;
    [InjectService] private ILogger<OrderService> logger;

    public async Task<Order> CreateOrderAsync(CreateOrderDto orderDto)
    {
        logger.LogInformation("Creating order for customer {CustomerId}", orderDto.CustomerId);
        
        // Validate products availability
        foreach (var item in orderDto.Items)
        {
            var isAvailable = await inventoryService.CheckAvailabilityAsync(item.ProductId, item.Quantity);
            if (!isAvailable)
            {
                throw new BusinessException($"Product {item.ProductId} is not available");
            }
        }

        // Reserve inventory
        foreach (var item in orderDto.Items)
        {
            await inventoryService.ReserveStockAsync(item.ProductId, item.Quantity);
        }

        // Create order
        var order = new Order
        {
            CustomerId = orderDto.CustomerId,
            OrderDate = DateTime.UtcNow,
            Status = OrderStatus.Pending,
            Items = orderDto.Items.Select(i => new OrderItem
            {
                ProductId = i.ProductId,
                Quantity = i.Quantity,
                Price = i.Price
            }).ToList()
        };

        await orderRepository.AddAsync(order);
        await orderRepository.SaveChangesAsync();

        // Send notification
        await notificationService.SendOrderConfirmationAsync(order);

        return order;
    }

    public async Task<bool> ProcessPaymentAsync(int orderId, PaymentDto paymentDto)
    {
        var order = await orderRepository.GetByIdAsync(orderId);
        if (order == null)
        {
            throw new NotFoundException($"Order {orderId} not found");
        }

        var paymentResult = await paymentService.ProcessPaymentAsync(order, paymentDto);
        
        if (paymentResult.Success)
        {
            order.Status = OrderStatus.Paid;
            order.PaymentId = paymentResult.TransactionId;
            await orderRepository.UpdateAsync(order);
            await orderRepository.SaveChangesAsync();
            
            await notificationService.SendPaymentConfirmationAsync(order);
        }

        return paymentResult.Success;
    }
}

Infrastructure Layer

// MyEcommerce.Infrastructure/Services/EmailNotificationService.cs
namespace MyEcommerce.Infrastructure.Services;

[RegisterAs(typeof(INotificationService))]
[ServiceLifeTime(ServiceLifetime.Singleton)]
public class EmailNotificationService : INotificationService
{
    [InjectService] private IEmailSender emailSender;
    [InjectService] private ITemplateEngine templateEngine;
    [InjectService] private IConfiguration configuration;

    public async Task SendOrderConfirmationAsync(Order order)
    {
        var template = await templateEngine.RenderAsync("OrderConfirmation", order);
        var email = new EmailMessage
        {
            To = order.Customer.Email,
            Subject = $"Order Confirmation - #{order.Id}",
            Body = template
        };
        
        await emailSender.SendAsync(email);
    }

    public async Task SendPaymentConfirmationAsync(Order order)
    {
        var template = await templateEngine.RenderAsync("PaymentConfirmation", order);
        var email = new EmailMessage
        {
            To = order.Customer.Email,
            Subject = $"Payment Confirmation - Order #{order.Id}",
            Body = template
        };
        
        await emailSender.SendAsync(email);
    }
}

API Layer Configuration

// MyEcommerce.Api/Program.cs
var builder = WebApplication.CreateBuilder(args);

// Enable property injection
builder.Host.UseDotNurseInjector();

// Add services
builder.Services.AddControllers();
builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

// Register services from all layers
builder.Services.AddServicesFrom("MyEcommerce.Application.Services", ServiceLifetime.Scoped);
builder.Services.AddServicesFrom("MyEcommerce.Infrastructure.Repositories", ServiceLifetime.Scoped);
builder.Services.AddServicesFrom("MyEcommerce.Infrastructure.Services", ServiceLifetime.Singleton);

// Add AutoMapper
builder.Services.AddAutoMapper(typeof(MappingProfile));

// Add authentication
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        // JWT configuration
    });

var app = builder.Build();

// Middleware pipeline
app.UseExceptionHandler("/error");
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

Background Services Example

Using PyNurseInjector with hosted services and background tasks.

Order Processing Background Service

namespace MyApp.Services.Background;

[RegisterAs(typeof(IHostedService))]
[ServiceLifeTime(ServiceLifetime.Singleton)]
public class OrderProcessingService : BackgroundService
{
    [InjectService] private IServiceProvider serviceProvider;
    [InjectService] private ILogger<OrderProcessingService> logger;
    [InjectService] private IConfiguration configuration;

    private readonly TimeSpan _period;

    public OrderProcessingService()
    {
        _period = TimeSpan.FromMinutes(5); // Process every 5 minutes
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation("Order Processing Service is starting.");

        using PeriodicTimer timer = new(_period);
        
        while (!stoppingToken.IsCancellationRequested &&
               await timer.WaitForNextTickAsync(stoppingToken))
        {
            await ProcessPendingOrdersAsync(stoppingToken);
        }
    }

    private async Task ProcessPendingOrdersAsync(CancellationToken cancellationToken)
    {
        try
        {
            using var scope = serviceProvider.CreateScope();
            
            // Resolve scoped services
            var orderService = scope.ServiceProvider.GetRequiredService<IOrderService>();
            var orderRepository = scope.ServiceProvider.GetRequiredService<IOrderRepository>();
            
            var pendingOrders = await orderRepository.GetPendingOrdersAsync();
            
            logger.LogInformation("Processing {Count} pending orders", pendingOrders.Count());
            
            foreach (var order in pendingOrders)
            {
                if (cancellationToken.IsCancellationRequested)
                    break;
                    
                try
                {
                    await orderService.ProcessOrderAsync(order.Id);
                    logger.LogInformation("Successfully processed order {OrderId}", order.Id);
                }
                catch (Exception ex)
                {
                    logger.LogError(ex, "Error processing order {OrderId}", order.Id);
                }
            }
        }
        catch (Exception ex)
        {
            logger.LogError(ex, "Error in order processing service");
        }
    }
}

Email Queue Service

[RegisterAs(typeof(IHostedService))]
[ServiceLifeTime(ServiceLifetime.Singleton)]
public class EmailQueueService : BackgroundService
{
    [InjectService] private IEmailQueue emailQueue;
    [InjectService] private IEmailSender emailSender;
    [InjectService] private ILogger<EmailQueueService> logger;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        logger.LogInformation("Email Queue Service is starting.");

        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                var email = await emailQueue.DequeueAsync(stoppingToken);
                
                if (email != null)
                {
                    await SendEmailWithRetryAsync(email, stoppingToken);
                }
                else
                {
                    // No emails in queue, wait a bit
                    await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
                }
            }
            catch (OperationCanceledException)
            {
                // Service is stopping
                break;
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Error in email queue service");
                await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
            }
        }
    }

    private async Task SendEmailWithRetryAsync(EmailMessage email, CancellationToken cancellationToken)
    {
        const int maxRetries = 3;
        var retryCount = 0;

        while (retryCount < maxRetries)
        {
            try
            {
                await emailSender.SendAsync(email);
                logger.LogInformation("Email sent successfully to {Recipient}", email.To);
                break;
            }
            catch (Exception ex)
            {
                retryCount++;
                logger.LogWarning(ex, "Failed to send email (attempt {Attempt}/{MaxAttempts})", 
                    retryCount, maxRetries);

                if (retryCount >= maxRetries)
                {
                    logger.LogError("Failed to send email after {MaxAttempts} attempts", maxRetries);
                    // Could move to dead letter queue here
                    break;
                }

                await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, retryCount)), cancellationToken);
            }
        }
    }
}

Unit Testing with PyNurseInjector

How to write unit tests for services using PyNurseInjector.

Test Setup

using Microsoft.Extensions.DependencyInjection;
using Xunit;
using Moq;

public class ServiceTestBase : IDisposable
{
    protected IServiceProvider ServiceProvider { get; }
    protected IServiceCollection Services { get; }

    public ServiceTestBase()
    {
        Services = new ServiceCollection();
        
        // Add logging
        Services.AddLogging(builder => builder.AddDebug());
        
        // Register test services
        RegisterTestServices();
        
        // Build service provider
        ServiceProvider = Services.BuildServiceProvider();
    }

    protected virtual void RegisterTestServices()
    {
        // Override in derived classes
    }

    protected T GetService<T>() where T : notnull
    {
        return ServiceProvider.GetRequiredService<T>();
    }

    public void Dispose()
    {
        if (ServiceProvider is IDisposable disposable)
        {
            disposable.Dispose();
        }
    }
}

Testing Services with Mocked Dependencies

public class BookServiceTests : ServiceTestBase
{
    private readonly Mock<IBookRepository> _bookRepositoryMock;
    private readonly Mock<ILogger<BookService>> _loggerMock;

    public BookServiceTests()
    {
        _bookRepositoryMock = new Mock<IBookRepository>();
        _loggerMock = new Mock<ILogger<BookService>>();
    }

    protected override void RegisterTestServices()
    {
        // Register mocks
        Services.AddSingleton(_bookRepositoryMock.Object);
        Services.AddSingleton(_loggerMock.Object);
        
        // Register service under test
        Services.AddTransient<BookService>();
        
        // Enable property injection for tests
        Services.AddSingleton<IServiceProvider>(provider => 
            new DotNurseServiceProvider(provider));
    }

    [Fact]
    public async Task GetBookByIdAsync_WhenBookExists_ReturnsBook()
    {
        // Arrange
        var bookId = 1;
        var expectedBook = new Book 
        { 
            Id = bookId, 
            Title = "Test Book",
            Author = "Test Author" 
        };
        
        _bookRepositoryMock
            .Setup(x => x.GetByIdAsync(bookId))
            .ReturnsAsync(expectedBook);

        var bookService = GetService<BookService>();

        // Act
        var result = await bookService.GetBookByIdAsync(bookId);

        // Assert
        Assert.NotNull(result);
        Assert.Equal(expectedBook.Id, result.Id);
        Assert.Equal(expectedBook.Title, result.Title);
        
        _bookRepositoryMock.Verify(x => x.GetByIdAsync(bookId), Times.Once);
    }

    [Fact]
    public async Task GetBookByIdAsync_WhenBookNotFound_ThrowsException()
    {
        // Arrange
        var bookId = 999;
        
        _bookRepositoryMock
            .Setup(x => x.GetByIdAsync(bookId))
            .ReturnsAsync((Book)null);

        var bookService = GetService<BookService>();

        // Act & Assert
        await Assert.ThrowsAsync<KeyNotFoundException>(
            () => bookService.GetBookByIdAsync(bookId));
        
        _loggerMock.Verify(
            x => x.Log(
                LogLevel.Warning,
                It.IsAny<EventId>(),
                It.Is<It.IsAnyType>((o, t) => o.ToString().Contains($"Book with ID {bookId} not found")),
                It.IsAny<Exception>(),
                It.IsAny<Func<It.IsAnyType, Exception, string>>()),
            Times.Once);
    }
}

Integration Testing

public class BookApiIntegrationTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;
    private readonly HttpClient _client;

    public BookApiIntegrationTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                // Remove real database
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType == typeof(DbContextOptions<AppDbContext>));
                if (descriptor != null)
                {
                    services.Remove(descriptor);
                }

                // Add in-memory database for testing
                services.AddDbContext<AppDbContext>(options =>
                {
                    options.UseInMemoryDatabase("TestDb");
                });

                // Ensure PyNurseInjector registrations
                services.AddServicesFrom("BookStore.Api.Services");
                services.AddServicesFrom("BookStore.Api.Repositories");
            });
        });

        _client = _factory.CreateClient();
    }

    [Fact]
    public async Task GetBooks_ReturnsSuccessAndCorrectContentType()
    {
        // Act
        var response = await _client.GetAsync("/api/books");

        // Assert
        response.EnsureSuccessStatusCode();
        Assert.Equal("application/json; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }

    [Fact]
    public async Task CreateBook_WithValidData_ReturnsCreatedBook()
    {
        // Arrange
        var newBook = new Book
        {
            Title = "Integration Test Book",
            Author = "Test Author",
            ISBN = "123-456-789",
            Price = 29.99m
        };

        var json = JsonSerializer.Serialize(newBook);
        var content = new StringContent(json, Encoding.UTF8, "application/json");

        // Act
        var response = await _client.PostAsync("/api/books", content);

        // Assert
        response.EnsureSuccessStatusCode();
        Assert.Equal(HttpStatusCode.Created, response.StatusCode);

        var responseContent = await response.Content.ReadAsStringAsync();
        var createdBook = JsonSerializer.Deserialize<Book>(responseContent);
        
        Assert.NotNull(createdBook);
        Assert.Equal(newBook.Title, createdBook.Title);
        Assert.True(createdBook.Id > 0);
    }
}

Advanced Scenarios

Complex use cases and advanced patterns with PyNurseInjector.

Decorator Pattern

// Base service interface
public interface ICacheService
{
    Task<T> GetAsync<T>(string key);
    Task SetAsync<T>(string key, T value, TimeSpan? expiry = null);
}

// Primary implementation
[RegisterAs(typeof(ICacheService))]
public class RedisCacheService : ICacheService
{
    [InjectService] private IConnectionMultiplexer redis;
    
    public async Task<T> GetAsync<T>(string key)
    {
        var db = redis.GetDatabase();
        var value = await db.StringGetAsync(key);
        return value.HasValue ? JsonSerializer.Deserialize<T>(value) : default;
    }
    
    public async Task SetAsync<T>(string key, T value, TimeSpan? expiry = null)
    {
        var db = redis.GetDatabase();
        var json = JsonSerializer.Serialize(value);
        await db.StringSetAsync(key, json, expiry);
    }
}

// Decorator with logging
[DontRegister] // Manual registration required for decorators
public class LoggingCacheDecorator : ICacheService
{
    private readonly ICacheService _innerCache;
    [InjectService] private ILogger<LoggingCacheDecorator> logger;

    public LoggingCacheDecorator(ICacheService innerCache)
    {
        _innerCache = innerCache;
    }

    public async Task<T> GetAsync<T>(string key)
    {
        logger.LogDebug("Getting cache value for key: {Key}", key);
        var value = await _innerCache.GetAsync<T>(key);
        logger.LogDebug("Cache {Result} for key: {Key}", 
            value != null ? "hit" : "miss", key);
        return value;
    }

    public async Task SetAsync<T>(string key, T value, TimeSpan? expiry = null)
    {
        logger.LogDebug("Setting cache value for key: {Key}", key);
        await _innerCache.SetAsync(key, value, expiry);
    }
}

// Registration with decorator
services.AddServicesFrom("MyApp.Services");
services.Decorate<ICacheService, LoggingCacheDecorator>();

Factory Pattern

// Factory interface
public interface IPaymentProcessorFactory
{
    IPaymentProcessor GetProcessor(PaymentMethod method);
}

// Payment processor interface
public interface IPaymentProcessor
{
    Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request);
    PaymentMethod SupportedMethod { get; }
}

// Implementations
[RegisterAs(typeof(IPaymentProcessor))]
public class CreditCardProcessor : IPaymentProcessor
{
    [InjectService] private ICreditCardGateway gateway;
    
    public PaymentMethod SupportedMethod => PaymentMethod.CreditCard;
    
    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        // Process credit card payment
        return await gateway.ChargeAsync(request);
    }
}

[RegisterAs(typeof(IPaymentProcessor))]
public class PayPalProcessor : IPaymentProcessor
{
    [InjectService] private IPayPalGateway gateway;
    
    public PaymentMethod SupportedMethod => PaymentMethod.PayPal;
    
    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        // Process PayPal payment
        return await gateway.ProcessAsync(request);
    }
}

// Factory implementation
[RegisterAs(typeof(IPaymentProcessorFactory))]
[ServiceLifeTime(ServiceLifetime.Singleton)]
public class PaymentProcessorFactory : IPaymentProcessorFactory
{
    private readonly Dictionary<PaymentMethod, IPaymentProcessor> _processors;

    public PaymentProcessorFactory(IEnumerable<IPaymentProcessor> processors)
    {
        _processors = processors.ToDictionary(p => p.SupportedMethod);
    }

    public IPaymentProcessor GetProcessor(PaymentMethod method)
    {
        if (!_processors.TryGetValue(method, out var processor))
        {
            throw new NotSupportedException($"Payment method {method} is not supported");
        }
        
        return processor;
    }
}

Multi-Tenant Services

// Tenant context
public interface ITenantContext
{
    string TenantId { get; }
    string ConnectionString { get; }
}

// Multi-tenant repository
public abstract class MultiTenantRepository<T> : IRepository<T> where T : class
{
    [InjectService] private ITenantContext tenantContext;
    [InjectService] private IDbContextFactory<AppDbContext> contextFactory;
    
    protected AppDbContext GetDbContext()
    {
        var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
        optionsBuilder.UseSqlServer(tenantContext.ConnectionString);
        return new AppDbContext(optionsBuilder.Options);
    }
    
    public async Task<T> GetByIdAsync(int id)
    {
        using var context = GetDbContext();
        return await context.Set<T>().FindAsync(id);
    }
    
    // Other repository methods...
}

// Tenant-specific service
[RegisterAs(typeof(IProductService))]
[ServiceLifeTime(ServiceLifetime.Scoped)]
public class TenantProductService : IProductService
{
    [InjectService] private ITenantContext tenantContext;
    [InjectService] private IProductRepository productRepository;
    [InjectService] private ICacheService cache;
    
    public async Task<IEnumerable<Product>> GetProductsAsync()
    {
        var cacheKey = $"products_{tenantContext.TenantId}";
        
        var cachedProducts = await cache.GetAsync<IEnumerable<Product>>(cacheKey);
        if (cachedProducts != null)
        {
            return cachedProducts;
        }
        
        var products = await productRepository.GetAllAsync();
        await cache.SetAsync(cacheKey, products, TimeSpan.FromMinutes(5));
        
        return products;
    }
}

Migration Guide

How to migrate existing projects to use PyNurseInjector.

From Manual Registration

Before (Manual Registration)

// Startup.cs - Lots of manual registrations
public void ConfigureServices(IServiceCollection services)
{
    // Services
    services.AddScoped<IUserService, UserService>();
    services.AddScoped<IProductService, ProductService>();
    services.AddScoped<IOrderService, OrderService>();
    services.AddScoped<IPaymentService, PaymentService>();
    services.AddScoped<IShippingService, ShippingService>();
    services.AddScoped<IInventoryService, InventoryService>();
    services.AddScoped<INotificationService, NotificationService>();
    
    // Repositories
    services.AddScoped<IUserRepository, UserRepository>();
    services.AddScoped<IProductRepository, ProductRepository>();
    services.AddScoped<IOrderRepository, OrderRepository>();
    
    // Singletons
    services.AddSingleton<ICacheService, RedisCacheService>();
    services.AddSingleton<IConfigurationService, ConfigurationService>();
    
    // ... many more registrations
}

After (PyNurseInjector)

// Program.cs - Clean and simple
builder.Host.UseDotNurseInjector(); // Enable property injection

// Register all services automatically
builder.Services.AddServicesFrom("MyApp.Services", ServiceLifetime.Scoped);
builder.Services.AddServicesFrom("MyApp.Repositories", ServiceLifetime.Scoped);

// Special cases only
builder.Services.AddServicesFrom("MyApp.Services.Cache", ServiceLifetime.Singleton);

From Constructor Injection

Before (Constructor Injection)

public class OrderController : Controller
{
    private readonly IOrderService _orderService;
    private readonly IUserService _userService;
    private readonly IProductService _productService;
    private readonly IPaymentService _paymentService;
    private readonly IShippingService _shippingService;
    private readonly ILogger<OrderController> _logger;
    
    public OrderController(
        IOrderService orderService,
        IUserService userService,
        IProductService productService,
        IPaymentService paymentService,
        IShippingService shippingService,
        ILogger<OrderController> logger)
    {
        _orderService = orderService;
        _userService = userService;
        _productService = productService;
        _paymentService = paymentService;
        _shippingService = shippingService;
        _logger = logger;
    }
    
    // Methods using the injected services
}

After (Property Injection)

public class OrderController : Controller
{
    [InjectService] public IOrderService OrderService { get; private set; }
    [InjectService] public IUserService UserService { get; private set; }
    [InjectService] public IProductService ProductService { get; private set; }
    [InjectService] public IPaymentService PaymentService { get; private set; }
    [InjectService] public IShippingService ShippingService { get; private set; }
    [InjectService] public ILogger<OrderController> Logger { get; private set; }
    
    // No constructor needed!
    // Methods can directly use the properties
}

Step-by-Step Migration

  1. Install PyNurseInjector packages
    dotnet add package DotNurse.Injector
    dotnet add package DotNurse.Injector.AspNetCore
  2. Enable property injection in Program.cs
    builder.Host.UseDotNurseInjector();
  3. Replace manual registrations with namespace registration
    // Remove individual registrations
    // Add namespace-based registration
    services.AddServicesFrom("YourApp.Services");
  4. Add attributes to special cases
    [ServiceLifeTime(ServiceLifetime.Singleton)]
    public class CacheService : ICacheService { }
  5. Convert constructors to property injection (optional)
    [InjectService] 
    public IMyService MyService { get; private set; }
  6. Test thoroughly
    • Verify all services are registered correctly
    • Check service lifetimes
    • Ensure property injection works